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,39 @@
#ifndef __IACCOUNTEMAILS__H__
#define __IACCOUNTEMAILS__H__
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
// Account email addresses, single-use account tokens (password reset, email verification) and session invalidation
class IAccountEmails {
public:
struct EmailInfo {
std::string email;
bool confirmed{};
};
struct AccountToken {
uint32_t accountId{};
std::string data;
};
virtual std::optional<EmailInfo> GetAccountEmail(const uint32_t accountId) = 0;
virtual void SetAccountEmail(const uint32_t accountId, const std::string_view email, const bool confirmed) = 0;
// Account with this confirmed address (case-insensitive)
virtual std::optional<uint32_t> GetAccountIdByConfirmedEmail(const std::string_view email) = 0;
// Tokens are stored by hash only. Consuming returns and deletes an unexpired token.
virtual void InsertAccountToken(const std::string_view tokenHash, const uint32_t accountId, const std::string_view purpose, const std::string_view data, const int64_t expiresAt) = 0;
virtual std::optional<AccountToken> ConsumeAccountToken(const std::string_view tokenHash, const std::string_view purpose) = 0;
virtual void DeleteAccountTokens(const uint32_t accountId, const std::string_view purpose) = 0;
virtual void DeleteExpiredAccountTokens() = 0;
// Login sessions issued before this unix time are no longer accepted
virtual int64_t GetSessionsValidAfter(const uint32_t accountId) = 0;
virtual void SetSessionsValidAfter(const uint32_t accountId, const int64_t time) = 0;
};
#endif //!__IACCOUNTEMAILS__H__

View File

@@ -0,0 +1,37 @@
#ifndef __IACCOUNTNOTES__H__
#define __IACCOUNTNOTES__H__
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
/**
* An account's moderation history: notes and warnings staff write, and every ban, mute and lock with its reason.
* Also temporary bans: a ban with an end time is lifted at login (and by an hourly task) once it has passed.
*/
class IAccountNotes {
public:
struct AccountNote {
uint64_t id{};
uint32_t accountId{};
std::string kind; // note, warning, ban, unban, mute, unmute, lock, unlock
std::string text;
std::string actor;
int64_t createdAt{};
};
virtual void InsertAccountNote(const AccountNote& note) = 0;
virtual std::vector<AccountNote> GetAccountNotes(uint32_t accountId) = 0; // newest first
virtual std::optional<AccountNote> GetAccountNote(uint64_t id) = 0;
virtual void DeleteAccountNote(uint64_t id) = 0;
virtual uint32_t GetAccountWarningCount(uint32_t accountId) = 0;
// Ban or unban with an end time (0: permanent) and a reason shown to the player
virtual void SetAccountBan(uint32_t accountId, bool banned, int64_t expires, const std::string& reason) = 0;
// Unban every temporary ban that ended before `now`; returns their account ids
virtual std::vector<uint32_t> LiftExpiredBans(int64_t now) = 0;
};
#endif //!__IACCOUNTNOTES__H__

View File

@@ -0,0 +1,41 @@
#ifndef __IACCOUNTSTRIKES__H__
#define __IACCOUNTSTRIKES__H__
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "dCommonVars.h"
/**
* Strikes against accounts: kept when staff reject something a player made (a name, a pet name, a property) or
* remove a score and decide it deserves one, or given by hand. Not every rejection is a strike; staff choose.
* A revoked strike stays on record but no longer counts.
*/
class IAccountStrikes {
public:
struct Strike {
uint64_t id{};
uint32_t accountId{};
LWOOBJID characterId{}; // the character it was about, 0 for the account as a whole
std::string source; // eStrikeSource's name: MANUAL, NAME, PET_NAME, PROPERTY, LEADERBOARD, PLAYER_REPORT
std::string subject; // what was rejected: the name, the property's name, the leaderboard
std::string reason;
uint32_t givenById{};
std::string givenBy;
int64_t createdAt{};
int64_t revokedAt{}; // 0 while it counts
std::string revokedBy;
std::string revokeReason;
};
virtual uint64_t InsertStrike(const Strike& strike) = 0;
virtual std::vector<Strike> GetStrikes(uint32_t accountId) = 0; // newest first
virtual std::optional<Strike> GetStrike(uint64_t id) = 0;
virtual void RevokeStrike(uint64_t id, const std::string& revokedBy, const std::string& reason, int64_t time) = 0;
// Strikes that still count: not revoked and given at or after `since` (0: ever). accountId 0: every account
virtual uint32_t CountActiveStrikes(uint32_t accountId, int64_t since) = 0;
};
#endif //!__IACCOUNTSTRIKES__H__

View File

@@ -5,6 +5,7 @@
#include <optional>
#include <string>
#include <string_view>
#include "json.hpp"
enum class eGameMasterLevel : uint8_t;
@@ -17,6 +18,8 @@ public:
uint64_t muteExpire{};
bool banned{};
bool locked{};
int64_t banExpires{}; // unix time a temporary ban ends; 0 for a permanent one
std::string banReason;
eGameMasterLevel maxGmLevel{};
};
@@ -33,12 +36,49 @@ public:
virtual void UpdateAccountPassword(const uint32_t accountId, const std::string_view bcryptpassword) = 0;
// Add a new account to the database.
virtual void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword) = 0;
virtual void InsertNewAccount(const std::string_view username, const std::string_view bcryptpassword, const eGameMasterLevel gmLevel) = 0;
// Update the GameMaster level of an account.
virtual void UpdateAccountGmLevel(const uint32_t accountId, const eGameMasterLevel gmLevel) = 0;
virtual uint32_t GetAccountCount() = 0;
// Login attempt tracking methods
// Record a failed login attempt
virtual void RecordFailedAttempt(const uint32_t accountId) = 0;
// Clear failed login attempts and update last login time
virtual void ClearFailedAttempts(const uint32_t accountId) = 0;
// Throttle logins to the account until the given unix time (after too many failed attempts) and start a new failure count.
// This is not the staff lock; see SetAccountLocked. 0 clears the lockout.
virtual void SetLockout(const uint32_t accountId, const int64_t lockoutUntil) = 0;
// Whether the account's failed-login lockout (SetLockout) is still running
virtual bool IsLockedOut(const uint32_t accountId) = 0;
// Staff lock (accounts.locked): a locked account can't log in to the game or the dashboard until unlocked
virtual void SetAccountLocked(const uint32_t accountId, const bool locked) = 0;
// Get failed attempt count
virtual uint8_t GetFailedAttempts(const uint32_t accountId) = 0;
// Get paginated list of accounts with optional search/filtering for DataTables
// Returns a JSON object with the account data and metadata
virtual nlohmann::json GetAccountsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
// Get a single account by ID
// Returns a JSON object with the account details
virtual nlohmann::json GetAccountById(uint32_t accountId) = 0;
// Get characters for an account as JSON for dashboard display
virtual nlohmann::json GetAccountCharacters(uint32_t accountId) = 0;
// Delete an account and all related data
virtual void DeleteAccount(const uint32_t accountId) = 0;
// How many accounts at exactly this GM level can still sign in (not banned or locked), leaving out excludeAccountId
virtual uint32_t CountActiveAccountsAtGmLevel(const uint8_t gmLevel, const uint32_t excludeAccountId) = 0;
};
#endif //!__IACCOUNTS__H__

View File

@@ -2,6 +2,8 @@
#define __IACTIVITYLOG__H__
#include <cstdint>
#include <string>
#include <string_view>
#include "dCommonVars.h"
@@ -15,6 +17,12 @@ class IActivityLog {
public:
// Update the activity log for the given account.
virtual void UpdateActivityLog(const LWOOBJID characterId, const eActivityType activityType, const LWOMAPID mapId) = 0;
// Get paginated activity log data for DataTables display
virtual std::string GetActivityLogTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
// Get total count of activity log entries
virtual uint32_t GetActivityLogCount() = 0;
};
#endif //!__IACTIVITYLOG__H__

View File

@@ -0,0 +1,73 @@
#ifndef __IAISUGGESTIONS__H__
#define __IAISUGGESTIONS__H__
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "dCommonVars.h"
/**
* The dashboard's staff-side AI moderator helper: every suggestion it asked for (who asked, about what, the model,
* tokens used and the validated answer or the error), kept with the item so the same case isn't sent twice.
* Suggestions are drafts for staff; nothing here is ever shown to players.
*/
class IAiSuggestions {
public:
enum class eAiStatus : uint8_t {
OK = 0, // a validated suggestion
REJECTED = 1, // the model answered, but the answer failed validation
FAILED = 2 // no usable answer (API error, network, ...)
};
struct AiSuggestion {
uint64_t id{};
std::string kind; // player_report, chat_message, player_chat, name, pet_name, economy_flag
int64_t itemId{}; // the report, message, character, pet or flag
std::string fingerprint; // hash of what was sent (case, rules, model, prompt version)
uint32_t requestedById{};
std::string requestedBy;
int64_t createdAt{};
std::string model;
uint32_t inputTokens{};
uint32_t outputTokens{};
eAiStatus status{ eAiStatus::OK };
std::string suggestion; // the validated answer as JSON, empty unless OK
std::string error;
std::string context; // the case file that was sent, as JSON (for review)
};
virtual uint64_t InsertAiSuggestion(const AiSuggestion& suggestion) = 0;
// Newest first
virtual std::vector<AiSuggestion> GetAiSuggestions(const std::string& kind, int64_t itemId, uint32_t limit) = 0;
// The newest OK suggestion made from exactly this input
virtual std::optional<AiSuggestion> FindAiSuggestion(const std::string& kind, int64_t itemId, const std::string& fingerprint) = 0;
struct AiUsage {
uint32_t requests{};
uint64_t inputTokens{};
uint64_t outputTokens{};
};
// Requests (of any status) made at or after `since`, and their tokens
virtual AiUsage GetAiUsageSince(int64_t since) = 0;
// One economy flag's row, for sending it to the helper; nullopt when there is none
struct EconomyFlagRow {
uint64_t id{};
int64_t createdAt{};
uint32_t day{};
uint8_t kind{};
LWOOBJID characterId{};
int32_t lot{};
LWOOBJID itemId{};
int64_t value{};
int64_t baseline{};
std::string details;
uint8_t status{};
std::string note;
};
virtual std::optional<EconomyFlagRow> GetEconomyFlagRow(uint64_t id) = 0;
};
#endif //!__IAISUGGESTIONS__H__

View File

@@ -0,0 +1,18 @@
#ifndef __IAUDITLOG__H__
#define __IAUDITLOG__H__
#include <cstdint>
#include <string>
#include <string_view>
#include "dCommonVars.h"
class IAuditLog {
public:
// targetAccountId / targetCharacterId: what the action was about (0: nothing in particular)
virtual void InsertAuditLog(uint32_t accountId, const std::string_view accountName, const std::string_view action, const std::string_view description,
uint32_t targetAccountId, LWOOBJID targetCharacterId) = 0;
virtual std::string GetAuditLogTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
};
#endif //!__IAUDITLOG__H__

View File

@@ -2,8 +2,12 @@
#define __IBUGREPORTS__H__
#include <cstdint>
#include <string>
#include <string_view>
#include "dCommonVars.h"
#include "json.hpp"
class IBugReports {
public:
struct Info {
@@ -16,5 +20,19 @@ public:
// Add a new bug report to the database.
virtual void InsertNewBugReport(const Info& info) = 0;
// Get paginated list of bug reports with optional search/filtering for DataTables
// resolvedFilter: -1 = all, 0 = unresolved only, 1 = resolved only
virtual std::string GetBugReportsTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true, int8_t resolvedFilter = -1) = 0;
// Get a single bug report with reporter and resolution details, or {"error": ...} if missing
virtual nlohmann::json GetBugReport(const uint32_t id) = 0;
// Mark a bug report resolved. Column names match NexusDashboard so existing databases keep their data.
virtual void ResolveBugReport(const uint32_t id, const uint32_t resolverAccountId, const std::string_view resolution) = 0;
virtual void DeleteBugReport(const uint32_t id) = 0;
virtual uint32_t GetBugReportCount() = 0;
};
#endif //!__IBUGREPORTS__H__

View File

@@ -8,6 +8,7 @@
#include <vector>
#include "ePermissionMap.h"
#include "json.hpp"
class ICharInfo {
public:
@@ -24,6 +25,14 @@ public:
// Get the approved names of all characters.
virtual std::vector<std::string> GetApprovedCharacterNames() = 0;
// Get the id and name of every character in one query
virtual std::vector<std::pair<LWOOBJID, std::string>> GetCharacterIdsAndNames() = 0;
// Get characters with a pending name change, in DataTables format
virtual nlohmann::json GetPendingNamesTable(const uint32_t start, const uint32_t length) = 0;
virtual void SetCharacterPermissionMap(const LWOOBJID characterId, const uint64_t permissionMap) = 0;
// Get the character info for the given character id.
virtual std::optional<ICharInfo::Info> GetCharacterInfo(const LWOOBJID charId) = 0;
@@ -33,6 +42,9 @@ public:
// Get the character ids for the given account.
virtual std::vector<LWOOBJID> GetAccountCharacterIds(const LWOOBJID accountId) = 0;
// Get the total number of characters in the database.
virtual uint32_t GetCharacterCount() = 0;
// Insert a new character into the database.
virtual void InsertNewCharacter(const ICharInfo::Info info) = 0;
@@ -46,6 +58,8 @@ public:
virtual void UpdateLastLoggedInCharacter(const LWOOBJID characterId) = 0;
virtual bool IsNameInUse(const std::string_view name) = 0;
virtual nlohmann::json GetCharacterById(const LWOOBJID charId) = 0;
};
#endif //!__ICHARINFO__H__

View File

@@ -15,6 +15,10 @@ public:
// Insert the character xml for the given character id.
virtual void InsertCharacterXml(const LWOOBJID characterId, const std::string_view lxfml) = 0;
// Get paginated list of characters with optional search/filtering for DataTables
// Returns a JSON-formatted string with the character data and metadata
virtual std::string GetCharactersTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
};
#endif //!__ICHARXML__H__

View File

@@ -0,0 +1,43 @@
#ifndef __ICHARACTERSNAPSHOTS__H__
#define __ICHARACTERSNAPSHOTS__H__
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <vector>
#include "dCommonVars.h"
/**
* Earlier versions of characters' XML, taken before the dashboard changes a character and once a day for characters
* that changed, so staff can see what changed and put a character back. The XML is stored zlib-compressed.
*/
class ICharacterSnapshots {
public:
struct CharacterSnapshot {
uint64_t id{};
LWOOBJID characterId{};
int64_t takenAt{};
std::string reason;
std::string actor;
uint32_t size{}; // uncompressed bytes
std::string hash; // sha256 of the XML, to skip unchanged characters
std::string compressed;
};
virtual void InsertCharacterSnapshot(const CharacterSnapshot& snapshot) = 0;
// Newest first, without the XML
virtual std::vector<CharacterSnapshot> GetCharacterSnapshots(LWOOBJID characterId) = 0;
virtual std::optional<CharacterSnapshot> GetCharacterSnapshot(uint64_t id) = 0;
// The newest snapshot's hash for every character that has one
virtual std::map<LWOOBJID, std::string> GetLatestSnapshotHashes() = 0;
// Delete snapshots older than beforeTime, always keeping each character's newest `keep`
virtual uint32_t PruneCharacterSnapshots(int64_t beforeTime, uint32_t keep) = 0;
};
#endif //!__ICHARACTERSNAPSHOTS__H__

View File

@@ -0,0 +1,54 @@
#ifndef __ICHATLOG__H__
#define __ICHATLOG__H__
#include <cstdint>
#include <string>
#include <vector>
#include "dCommonVars.h"
#include "json.hpp"
/**
* What players say: zone chat (world servers), whispers and team chat (chat server), and messages sent into the game
* from the dashboard or a chat bridge. Read by the dashboard's chat log and its API for bots.
*/
class IChatLog {
public:
struct ChatMessage {
uint64_t id{};
int64_t time{};
std::string channel; // zone, whisper, team, web
LWOOBJID senderId{}; // the character; 0 for messages from the web
std::string senderName;
uint32_t accountId{};
LWOOBJID recipientId{}; // whispers
std::string recipientName;
uint32_t zoneId{};
uint32_t instanceId{};
uint32_t cloneId{};
std::string message;
bool blocked{}; // stopped by the chat filter: nobody saw it
};
struct ChatQuery {
uint64_t afterId{}; // only messages newer than this (for bots catching up)
uint32_t limit{ 100 };
std::string channel; // empty: any
bool includePrivate{}; // whispers and team chat
LWOOBJID characterId{}; // sent or received by this character (0: anyone)
uint32_t accountId{}; // sent by this account (0: anyone)
uint32_t zoneId{}; // 0: any
int64_t instanceId{ -1 }; // -1: any (instance 0 is character select)
int64_t since{}; // only messages at or after this time (0: any)
std::string search; // text in the message or a name
bool blockedOnly{};
bool newestFirst{};
uint32_t offset{};
};
virtual uint64_t InsertChatMessage(const ChatMessage& message) = 0;
virtual std::vector<ChatMessage> GetChatMessages(const ChatQuery& query) = 0;
virtual uint64_t CountChatMessages(const ChatQuery& query) = 0;
};
#endif //!__ICHATLOG__H__

View File

@@ -2,13 +2,17 @@
#define __ICOMMANDLOG__H__
#include <cstdint>
#include <string>
#include <string_view>
class ICommandLog {
public:
public:
// Insert a new slash command log entry.
virtual void InsertSlashCommandUsage(const LWOOBJID characterId, const std::string_view command) = 0;
// Get paginated command log data for DataTables display
virtual std::string GetCommandLogTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
};
#endif //!__ICOMMANDLOG__H__

View File

@@ -0,0 +1,145 @@
#ifndef __IDASHBOARDADMIN__H__
#define __IDASHBOARDADMIN__H__
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <vector>
#include "dCommonVars.h"
#include "json.hpp"
/**
* Dashboard administration data: outgoing webhooks, economy anomaly flags, small persistent state, two-factor
* login, and the queries the dashboard's daily jobs need (anomaly checks, ledger compaction).
*/
class IDashboardAdmin {
public:
// ---- Webhooks ----
struct Webhook {
uint32_t id{};
std::string name;
std::string url;
std::string format; // "discord", "slack" or "json"
std::string events; // comma separated event names, or "*"
std::string secret; // signs "json" deliveries (X-DLU-Signature), empty for none
bool enabled{ true };
int64_t createdAt{};
int64_t lastSentAt{};
int32_t lastStatus{};
std::string lastError;
};
virtual std::vector<Webhook> GetWebhooks() = 0;
virtual std::optional<Webhook> GetWebhook(uint32_t id) = 0;
virtual void InsertWebhook(const Webhook& webhook) = 0;
virtual void UpdateWebhook(const Webhook& webhook) = 0; // name, url, format, events, secret, enabled
virtual void DeleteWebhook(uint32_t id) = 0;
virtual void RecordWebhookResult(uint32_t id, int64_t time, int32_t status, const std::string& error) = 0;
// ---- Small persistent key/value state (job bookkeeping, scheduled restarts) ----
virtual std::optional<std::string> GetDashboardState(const std::string& name) = 0;
virtual void SetDashboardState(const std::string& name, const std::string& value) = 0;
virtual void DeleteDashboardState(const std::string& name) = 0;
// ---- Per-account view choices (a JSON object the pages own) ----
virtual std::string GetDashboardPreferences(uint32_t accountId) = 0; // "{}" when none are saved
virtual void SetDashboardPreferences(uint32_t accountId, const std::string& prefs) = 0;
// ---- Economy anomaly flags ----
enum class eFlagKind : uint8_t {
COIN_INCOME = 1, // a character earned far more coins in a day than players usually do
ITEM_SPIKE = 2, // far more of an item was created in a day than usual
DUPLICATE = 3, // an object id exists in more than one place (day 0, so each item is flagged once)
// different items share an object id and the login migration will not separate them: the characters holding
// them already migrated, or a copy is in mail (day 0)
ID_COLLISION = 4
};
enum class eFlagStatus : uint8_t { OPEN = 0, DISMISSED = 1, ACTIONED = 2 };
struct EconomyFlag {
uint32_t day{};
eFlagKind kind{};
LWOOBJID characterId{};
LOT lot{};
LWOOBJID itemId{};
int64_t value{};
int64_t baseline{};
std::string details;
};
// Adds the flag unless the same one exists already. Returns whether it was new.
virtual bool InsertEconomyFlag(const EconomyFlag& flag) = 0;
// Flags newest first, optionally only one status (status < 0 for all); DataTables format
virtual nlohmann::json GetEconomyFlagsTable(uint32_t start, uint32_t length, int32_t status) = 0;
virtual void ReviewEconomyFlag(uint64_t id, eFlagStatus status, uint32_t reviewerAccountId, const std::string& note) = 0;
virtual uint32_t GetOpenEconomyFlagCount() = 0;
// Coins each character earned on a day, not counting trades and mail and not counting staff
virtual std::vector<std::pair<LWOOBJID, int64_t>> GetDailyIncome(uint32_t day) = 0;
// Items created per LOT over a range of days, not counting staff
virtual std::map<LOT, int64_t> GetItemCreationTotals(uint32_t fromDay, uint32_t toDay) = 0;
// ---- Ledger size ----
/**
* Merge daily ledger rows from before cutoffDay into one row per month (on the month's first day), and map
* events from before mapCutoffDay likewise. Returns how many daily rows were merged away.
*/
virtual uint32_t CompactEconomy(uint32_t cutoffDay, uint32_t mapCutoffDay) = 0;
virtual uint32_t PruneTransfers(int64_t beforeTime) = 0;
enum class eLog : uint8_t { ACTIVITY, COMMAND, AUDIT, CHEAT_DETECTION, CHAT, LOGIN_ADDRESS };
// Delete log rows older than a unix time; rows without a time (0) are kept. Returns rows deleted.
virtual uint32_t PruneLog(eLog log, int64_t beforeTime) = 0;
// ---- Two-factor login ----
struct Totp {
std::string encryptedSecret; // empty when not set up
int64_t enabledAt{}; // 0 while off
int64_t lastStep{};
};
virtual Totp GetTotp(uint32_t accountId) = 0;
virtual void SetTotp(uint32_t accountId, const std::string& encryptedSecret, int64_t enabledAt) = 0;
// Accept a code's time step only once: records it and returns true if it is newer than the last one used
virtual bool UseTotpStep(uint32_t accountId, int64_t step) = 0;
// Recovery codes are stored hashed; each works once
virtual void ReplaceRecoveryCodes(uint32_t accountId, const std::vector<std::string>& codeHashes) = 0;
virtual bool UseRecoveryCode(uint32_t accountId, const std::string& codeHash) = 0;
virtual uint32_t GetRecoveryCodesLeft(uint32_t accountId) = 0;
// ---- Misc ----
// Bug reports with an id above afterId, oldest first: {id, body, client_version, other_player_id, selection, reporter_id, reporter_name}
virtual nlohmann::json GetBugReportsAfter(uint32_t afterId, uint32_t limit) = 0;
virtual uint32_t GetMaxBugReportId() = 0;
// Trades and mail (not inventory moves) involving any of these characters, newest first; DataTables format
virtual nlohmann::json GetTransfersForCharacters(const std::vector<LWOOBJID>& characterIds, uint32_t start, uint32_t length) = 0;
// Days since the Unix epoch of the first day of the month `day` falls in
static uint32_t MonthStartDay(uint32_t day) {
// Civil-from-days (Howard Hinnant's algorithm), then back to the first of that month
const int64_t z = static_cast<int64_t>(day) + 719468;
const int64_t era = (z >= 0 ? z : z - 146096) / 146097;
const int64_t doe = z - era * 146097;
const int64_t yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
const int64_t doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
const int64_t mp = (5 * doy + 2) / 153;
const int64_t dayOfMonth = doy - (153 * mp + 2) / 5 + 1;
return static_cast<uint32_t>(static_cast<int64_t>(day) - (dayOfMonth - 1));
}
};
#endif //!__IDASHBOARDADMIN__H__

View File

@@ -0,0 +1,31 @@
#ifndef __IDASHBOARDMAINTENANCE__H__
#define __IDASHBOARDMAINTENANCE__H__
#include <cstdint>
#include <functional>
#include <string>
#include <utility>
#include <vector>
#include "dCommonVars.h"
// Bulk data repairs run from the dashboard's maintenance page
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__

View File

@@ -0,0 +1,36 @@
#ifndef __IDASHBOARDSTATS__H__
#define __IDASHBOARDSTATS__H__
#include <cstdint>
class IDashboardStats {
public:
// Cheap aggregate values the dashboard compares between ticks to detect changes made by any writer
// (game servers, other tools). A table is considered changed when any of its values differ.
struct Snapshot {
uint64_t accounts{};
uint64_t accountsMaxId{};
uint64_t characters{};
uint64_t pendingNames{};
uint64_t properties{};
uint64_t pendingProperties{};
uint64_t playKeys{};
uint64_t bugReports{};
uint64_t unresolvedBugReports{};
uint64_t petNames{};
uint64_t pendingPetNames{};
uint64_t activityLogMaxId{};
uint64_t chatLogMaxId{};
uint64_t commandLogMaxId{};
uint64_t auditLogMaxId{};
uint64_t mailMaxId{};
uint64_t openEconomyFlags{};
uint64_t economyFlagsMaxId{};
bool operator==(const Snapshot&) const = default;
};
virtual Snapshot GetDashboardSnapshot() = 0;
};
#endif //!__IDASHBOARDSTATS__H__

View File

@@ -0,0 +1,200 @@
#ifndef __IECONOMYLEDGER__H__
#define __IECONOMYLEDGER__H__
#include <cstdint>
#include <functional>
#include <optional>
#include <string>
#include <vector>
#include "dCommonVars.h"
#include "json.hpp"
/**
* Economy ledger: daily coin/U-score/item flows, player-to-player transfers, where things happen in the world and
* daily totals of player statistics, written by world servers and read by the dashboard's reports. Days are counted
* since the Unix epoch (UTC).
*/
class IEconomyLedger {
public:
enum class eTransferMethod : uint8_t {
TRADE = 1,
MAIL_SENT = 2, // left the sender and is waiting in the mailbox
MAIL_CLAIMED = 3, // taken out of the mailbox by the receiver
INVENTORY_MOVE = 4 // moved between the character's own inventories (from = to = the character)
};
// Sources are eLootSourceType values, plus these for changes the game has no loot source for
static constexpr uint32_t DONATION_SOURCE = 100; // items given to a donation vendor
static constexpr uint32_t HARDCORE_DEATH_SOURCE = 101; // U-score lost on death in hardcore mode
static constexpr uint32_t HARDCORE_KILL_SOURCE = 102; // U-score for killing an enemy in hardcore mode
struct CurrencyFlow {
uint32_t day{};
LWOOBJID characterId{};
uint32_t source{};
int64_t gained{};
int64_t spent{};
};
struct UScoreFlow {
uint32_t day{};
LWOOBJID characterId{};
uint32_t source{};
int64_t gained{};
int64_t lost{};
};
struct ItemFlow {
uint32_t day{};
LOT lot{};
uint32_t source{};
bool gm{};
int64_t created{};
int64_t destroyed{};
};
enum class eMapEvent : uint8_t {
ENEMY_KILLS = 1, // enemy killed by a player
ITEM_DROPS = 2, // item dropped as loot
COIN_DROPS = 3, // coins dropped as loot (lot 0, quantity = coins)
PLAYER_DEATHS = 4, // a player died (lot = what killed them)
PLAYER_COIN_DROPS = 5, // coins a player dropped on death (lot 0, quantity = coins)
SMASHABLES_SMASHED = 6, // something other than an enemy or player smashed by a player (crates, barrels)
QUICKBUILDS_COMPLETED = 7, // a player finished a quickbuild (lot = the quickbuild)
POWERUP_DROPS = 8, // a powerup (Objects.type "Powerup": life, imagination, armor, ...) dropped (lot = the powerup)
POWERUP_PICKUPS = 9 // a player picked a powerup up (lot = the powerup); teammates may each pick up one drop
};
// World positions are bucketed into square cells of this many units (a minifigure is about 2 wide)
static constexpr float MAP_CELL_SIZE = 4.0f;
/**
* Property worlds run one instance per property, all on the property's zone, told apart by the clone id (the
* owner's property clone). Every other world has clone 0. Map events and player statistics record it, so one
* property's data isn't mixed with another's; rows from before that have clone 0 on property zones too.
*/
struct MapEvent {
uint32_t day{};
uint32_t zone{};
uint32_t clone{};
eMapEvent kind{};
LOT lot{};
int32_t cellX{};
int32_t cellZ{};
int64_t events{};
int64_t quantity{};
};
// A per-character statistic (StatisticID) summed over every player in a zone for a day
struct PlayerStat {
uint32_t day{};
uint32_t zone{};
uint32_t clone{};
uint32_t stat{};
bool gm{};
int64_t amount{};
};
struct ItemTransfer {
int64_t time{};
eTransferMethod method{};
LWOOBJID itemId{};
LWOOBJID newItemId{};
LOT lot{};
uint32_t count{};
int64_t coins{};
LWOOBJID fromCharacter{};
LWOOBJID toCharacter{};
uint32_t zone{};
bool merged{}; // newItemId is a stack the receiver already had, which has a history of its own
};
// Add a batch of flows (summed into existing daily rows) and transfers in one transaction
virtual void RecordEconomy(const std::vector<CurrencyFlow>& currency, const std::vector<UScoreFlow>& uscore,
const std::vector<ItemFlow>& items, const std::vector<ItemTransfer>& transfers, const std::vector<MapEvent>& mapEvents,
const std::vector<PlayerStat>& stats) = 0;
// ---- Reports ----
// Per day and source: {day, source, gained, spent}
virtual nlohmann::json GetCurrencyFlows(uint32_t fromDay, uint32_t toDay, bool excludeStaff) = 0;
// Per day and source: {day, source, gained, lost}
virtual nlohmann::json GetUScoreFlows(uint32_t fromDay, uint32_t toDay, bool excludeStaff) = 0;
// Per day and source for one item (lot > 0) or all items: {day, source, created, destroyed}
virtual nlohmann::json GetItemFlows(uint32_t fromDay, uint32_t toDay, LOT lot, bool excludeStaff) = 0;
// Characters with the largest coin income in the range: {character_id, name, gained, spent}
virtual nlohmann::json GetTopEarners(uint32_t fromDay, uint32_t toDay, uint32_t limit, bool excludeStaff) = 0;
// Items created most in the range: {lot, created, destroyed}
virtual nlohmann::json GetTopItems(uint32_t fromDay, uint32_t toDay, uint32_t limit, bool excludeStaff) = 0;
// Trades and mail (not inventory moves), newest first, optionally for one character (either side) or one lot; DataTables format
virtual nlohmann::json GetTransfers(uint32_t start, uint32_t length, LWOOBJID characterId, LOT lot) = 0;
// Transfers (every method) that involve any of the object ids as item_id or new_item_id, oldest first. Rows have
// "merged": true/false, or null for rows written before the game recorded it. The trace walks the chain with this.
virtual nlohmann::json GetTransfersForItems(const std::vector<LWOOBJID>& itemIds) = 0;
// Which zones and instances a map or statistics query covers
struct PlaceFilter {
std::vector<uint32_t> zones; // empty: every zone
std::optional<uint32_t> clone; // nullopt: every instance of those zones; else one clone (0: not a property instance)
// " AND zone IN (...) AND clone_id = n" for the filter (ids are numbers, so inlining them is safe)
std::string Sql() const {
std::string sql;
if (!zones.empty()) {
sql += " AND zone IN (";
for (size_t i = 0; i < zones.size(); i++) sql += (i ? "," : "") + std::to_string(zones[i]);
sql += ")";
}
if (clone) sql += " AND clone_id = " + std::to_string(*clone);
return sql;
}
};
// Zones and clones with map events of a kind in the range: {zone, clone, events, quantity}
virtual nlohmann::json GetMapZones(eMapEvent kind, uint32_t fromDay, uint32_t toDay) = 0;
// LOTs with the most events of a kind in a zone (one clone, or every instance): {lot, events, quantity}
virtual nlohmann::json GetMapLots(uint32_t zone, std::optional<uint32_t> clone, eMapEvent kind, uint32_t fromDay, uint32_t toDay, uint32_t limit) = 0;
// Events per cell for a zone (one clone, or every instance) and kind, optionally one LOT (lot > 0): {x, z, events, quantity}
virtual nlohmann::json GetMapCells(uint32_t zone, std::optional<uint32_t> clone, eMapEvent kind, uint32_t fromDay, uint32_t toDay, LOT lot) = 0;
// Map events of every kind per day in the places: {day, kind, events, quantity}
virtual nlohmann::json GetMapEventsPerDay(uint32_t fromDay, uint32_t toDay, const PlaceFilter& place) = 0;
// Map events of a kind per day, zone, clone and LOT in the places: {day, zone, clone, lot, events, quantity}.
// For kinds with few LOTs (powerups), at most `limit` rows.
virtual nlohmann::json GetMapEventsByLot(eMapEvent kind, uint32_t fromDay, uint32_t toDay, const PlaceFilter& place, uint32_t limit) = 0;
// Player statistics per day in the places: {day, stat, amount}
virtual nlohmann::json GetPlayerStatsPerDay(uint32_t fromDay, uint32_t toDay, bool excludeStaff, const PlaceFilter& place) = 0;
// Player statistics per zone and clone: {zone, clone, stat, amount}
virtual nlohmann::json GetPlayerStatsPerZone(uint32_t fromDay, uint32_t toDay, bool excludeStaff) = 0;
/**
* Who the property clones belong to: {owners: [{clone, character_id, name}] from charinfo.prop_clone_id (a clone
* exists before its properties are claimed), properties: [{clone, zone, id, name, owner_id}]}
*/
virtual nlohmann::json GetCloneOwners(const std::vector<uint32_t>& clones) = 0;
struct MailAttachment {
uint64_t mailId{};
LWOOBJID receiverId{};
LWOOBJID itemId{};
LOT lot{};
uint32_t count{};
};
// Every attachment still waiting to be claimed, for holdings and duplicate scans
virtual void ForEachMailAttachment(const std::function<void(const MailAttachment&)>& visit) = 0;
};
#endif //!__IECONOMYLEDGER__H__

View File

@@ -0,0 +1,43 @@
#ifndef __IFEATUREDPROPERTIES__H__
#define __IFEATUREDPROPERTIES__H__
#include <cstdint>
#include <string>
#include <vector>
#include "dCommonVars.h"
/**
* What each slot of the news screen's "Today's Top Properties" panel shows, chosen on the dashboard.
* A slot is a PropertyTemplate row (see HotPropertySlots.h); a slot without a row shows its world's top property.
*/
class IFeaturedProperties {
public:
struct FeaturedSlot {
uint32_t templateId{};
uint8_t mode{}; // HotPropertySlots::eMode
LWOOBJID propertyId{}; // the picked property (mode PICKED), 0 otherwise
int64_t updatedAt{};
std::string updatedBy;
uint32_t zoneId{}; // the property world it shows a property of; 0: the slot's own
};
// The whole panel's settings (featured_properties_settings)
struct FeaturedSettings {
bool fullAuto{}; // ignore the slots' rows: the top properties across every property world
int64_t updatedAt{};
std::string updatedBy;
};
virtual std::vector<FeaturedSlot> GetFeaturedPropertySlots() = 0;
// Insert or replace the row of slot.templateId
virtual void SetFeaturedPropertySlot(const FeaturedSlot& slot) = 0;
// The defaults (per slot) when nothing was saved yet
virtual FeaturedSettings GetFeaturedPropertiesSettings() = 0;
virtual void SetFeaturedPropertiesSettings(const FeaturedSettings& settings) = 0;
};
#endif //!__IFEATUREDPROPERTIES__H__

View File

@@ -2,10 +2,12 @@
#define __ILEADERBOARD__H__
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <vector>
#include "dCommonVars.h"
#include "eLeaderboardType.h"
class ILeaderboard {
public:
@@ -30,6 +32,21 @@ public:
float tertiaryScore{ 0.0f };
};
// Whether a lower primary score ranks higher: races store the time taken. Foot races store the time left on
// their countdown, so more is better there, like points.
static bool LowerIsBetter(const eLeaderboardType type) {
return type == eLeaderboardType::Racing || type == eLeaderboardType::MonumentRace;
}
// Every score on an activity's leaderboard, best first, in the order the game ranks it (it numbers rows in this order)
std::vector<Entry> GetRankedLeaderboard(const eLeaderboardType type, const uint32_t activityId) {
switch (type) {
case eLeaderboardType::SurvivalNS: return GetNsLeaderboard(activityId);
case eLeaderboardType::Survival: return GetAgsLeaderboard(activityId);
default: return LowerIsBetter(type) ? GetAscendingLeaderboard(activityId) : GetDescendingLeaderboard(activityId);
}
}
// Get the donation total for the given activity id.
virtual std::optional<uint32_t> GetDonationTotal(const uint32_t activityId) = 0;
@@ -43,6 +60,11 @@ public:
virtual void UpdateScore(const LWOOBJID playerId, const uint32_t gameId, const Score& score) = 0;
virtual void IncrementNumWins(const LWOOBJID playerId, const uint32_t gameId) = 0;
virtual void IncrementTimesPlayed(const LWOOBJID playerId, const uint32_t gameId) = 0;
// For the dashboard: how many scores each activity has, and removing scores (e.g. cheated ones)
virtual std::map<uint32_t, uint32_t> GetLeaderboardSizes() = 0;
virtual void DeleteLeaderboardScore(const LWOOBJID playerId, const uint32_t gameId) = 0;
virtual void ResetLeaderboard(const uint32_t gameId) = 0;
};
#endif //!__ILEADERBOARD__H__

View File

@@ -0,0 +1,146 @@
#ifndef __ILIVEOPS__H__
#define __ILIVEOPS__H__
#include <cstdint>
#include <map>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include "dCommonVars.h"
/**
* Live operations run from the dashboard: live events (treasure hunts, bonus multipliers, invasions, celebrations)
* that world servers run for a while in chosen zones, and server-wide community challenges counted from what the
* game records per character. The dashboard writes the definitions; world servers write progress.
*/
class ILiveOps {
public:
enum class eLiveEventState : uint8_t { ACTIVE = 0, ENDED = 1, CANCELLED = 2 };
struct LiveEvent {
uint64_t id{};
std::string type; // LiveOpsRules::eEventType name, e.g. "treasure_hunt"
std::string title;
std::string message; // announced when it starts
std::vector<uint32_t> zones; // empty: every world (bonus events only)
int32_t instanceId{ -1 }; // -1: every instance of the zones
std::string config; // JSON, per type
int64_t startsAt{};
int64_t endsAt{};
eLiveEventState state{ eLiveEventState::ACTIVE };
int64_t endedAt{};
std::string endReason;
std::string createdBy;
std::string endedBy;
};
// Newest first. activeOnly: only events still running (state ACTIVE, whatever the time)
virtual std::vector<LiveEvent> GetLiveEvents(bool activeOnly, uint32_t limit) = 0;
virtual std::optional<LiveEvent> GetLiveEvent(uint64_t id) = 0;
virtual uint64_t InsertLiveEvent(const LiveEvent& event) = 0;
// Ends a running event; false when it had already ended
virtual bool EndLiveEvent(uint64_t id, eLiveEventState state, int64_t endedAt, const std::string& endedBy, const std::string& reason) = 0;
struct LiveEventInstance {
uint64_t eventId{};
uint32_t zoneId{};
uint32_t instanceId{};
std::string status; // JSON the world reported
int64_t updatedAt{};
};
virtual std::vector<LiveEventInstance> GetLiveEventInstances(const std::vector<uint64_t>& eventIds) = 0;
// Insert or replace
virtual void SetLiveEventInstance(const LiveEventInstance& instance) = 0;
struct LiveOpsScore {
uint64_t id{}; // the event or challenge
LWOOBJID characterId{};
std::string name;
int64_t amount{};
};
// Adds to each character's score
virtual void AddLiveEventScores(uint64_t eventId, const std::vector<std::pair<LWOOBJID, int64_t>>& scores, int64_t time) = 0;
// Highest first, with character names
virtual std::vector<LiveOpsScore> GetLiveEventScores(uint64_t eventId, uint32_t limit) = 0;
enum class eChallengeState : uint8_t { OPEN = 0, COMPLETED = 1, EXPIRED = 2, CANCELLED = 3 };
enum class eChallengeMetric : uint8_t { STATISTIC = 0, MAP_EVENT = 1 };
struct Challenge {
uint64_t id{};
std::string title;
std::string description;
eChallengeMetric metricKind{ eChallengeMetric::STATISTIC };
uint32_t metric{}; // StatisticID or IEconomyLedger::eMapEvent
LOT lot{}; // map events: only this LOT (0: any)
std::vector<uint32_t> zones; // empty: every zone
int64_t target{};
int64_t startsAt{};
int64_t endsAt{};
bool includeStaff{};
bool isPublic{ true };
int64_t rewardCoins{};
std::string rewardItems{ "[]" }; // JSON [{lot, count}]
int64_t rewardMin{ 1 };
eChallengeState state{ eChallengeState::OPEN };
uint8_t milestone{}; // last percentage announced
int64_t completedAt{};
int64_t rewardedAt{};
uint32_t rewardedCount{};
int64_t endAnnouncedAt{}; // when players were told in game it was complete or over; 0: still to do
int64_t createdAt{};
std::string createdBy;
int64_t updatedAt{};
std::string updatedBy;
};
// Newest first
virtual std::vector<Challenge> GetChallenges(bool openOnly) = 0;
virtual std::optional<Challenge> GetChallenge(uint64_t id) = 0;
virtual uint64_t InsertChallenge(const Challenge& challenge) = 0;
virtual void UpdateChallenge(const Challenge& challenge) = 0;
// With its contributions and rewards
virtual void DeleteChallenge(uint64_t id) = 0;
struct Contribution {
uint64_t challengeId{};
LWOOBJID characterId{};
int64_t amount{};
};
// Adds to each character's contribution
virtual void AddChallengeContributions(const std::vector<Contribution>& contributions, int64_t time) = 0;
struct ChallengeTotal {
int64_t total{};
uint32_t contributors{};
};
virtual std::map<uint64_t, ChallengeTotal> GetChallengeTotals(const std::vector<uint64_t>& challengeIds) = 0;
// Highest first, with character names; limit 0: all
virtual std::vector<LiveOpsScore> GetChallengeContributions(uint64_t challengeId, uint32_t limit) = 0;
// challenge -> amount, for one character
virtual std::map<uint64_t, int64_t> GetCharacterContributions(LWOOBJID characterId) = 0;
struct Reward {
uint64_t challengeId{};
LWOOBJID characterId{};
int64_t amount{}; // what the character contributed
int64_t coins{};
int64_t rewardedAt{};
int64_t claimedAt{}; // 0: coins still waiting
};
virtual std::vector<Reward> GetChallengeRewards(uint64_t challengeId) = 0;
virtual void InsertChallengeReward(const Reward& reward) = 0;
// Rewards with coins the character hasn't been given yet
virtual std::vector<Reward> GetUnclaimedChallengeCoins(LWOOBJID characterId) = 0;
// Marks the coins given; false when they already were (so they are given once)
virtual bool ClaimChallengeCoins(uint64_t challengeId, LWOOBJID characterId, int64_t time) = 0;
};
#endif //!__ILIVEOPS__H__

View File

@@ -0,0 +1,102 @@
#ifndef __IMESSAGECAPTURES__H__
#define __IMESSAGECAPTURES__H__
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "dCommonVars.h"
/**
* Saved captures of the dashboard's game message inspector (see dDashboardServer/routes/Inspector.h): each capture's
* details and the messages it caught, with their raw bytes, so staff can look at them again after it ended. The
* dashboard writes them while the capture runs and deletes old ones (the Message capture pruning task).
*/
class IMessageCaptures {
public:
struct MessageCaptureSession {
uint64_t id{};
LWOOBJID characterId{};
std::string characterName;
uint32_t accountId{};
std::string accountName;
uint32_t startedById{}; // the staff account
std::string startedBy;
int64_t startedAt{}; // Unix seconds
int64_t endsAt{}; // its time limit
int64_t endedAt{}; // 0 while it runs
std::string endReason;
bool toServer{ true };
bool toClient{ true };
std::string onlyMessages; // message IDs, comma separated
std::string skipMessages;
uint32_t zoneId{}; // where it was last captured
uint32_t instanceId{};
uint32_t cloneId{};
std::string zones; // every world it was captured in, in order: "zone:instance:clone" separated by spaces
uint64_t messageCount{};
uint64_t byteCount{}; // stored bytes (raw bytes plus decoded fields)
uint64_t dropped{}; // messages the worlds left out (too many at once)
};
// One captured message
struct MessageCaptureRecord {
uint64_t sessionId{};
uint32_t seq{}; // from 1 in its session
int64_t timeMs{}; // Unix milliseconds
uint8_t direction{}; // eMessageDirection
uint16_t messageId{}; // MessageType::Game
LWOOBJID objectId{};
uint32_t bits{}; // the message's full size
uint32_t droppedBefore{}; // messages left out just before this one
uint32_t zoneId{};
uint32_t instanceId{};
uint32_t cloneId{};
std::string payload; // raw bytes after the object and message ID (may be cut short; see bits)
std::string decoded; // fields as JSON, or empty
};
// How GetMessageCaptureSessions sorts
enum class eSessionOrder : uint8_t {
STARTED,
CHARACTER,
STARTED_BY,
MESSAGES,
BYTES,
};
struct SessionQuery {
LWOOBJID characterId{}; // 0: any
uint32_t accountId{}; // the captured player's account (0: any)
std::string startedBy; // staff account name, any case (empty: anyone)
int64_t since{}; // started at or after (0: any)
int64_t until{}; // started before (0: any)
bool unfinishedOnly{}; // still running (ended_at 0)
eSessionOrder order{ eSessionOrder::STARTED };
bool ascending{};
uint32_t limit{ 50 };
uint32_t offset{};
};
// Returns the new id
virtual uint64_t InsertMessageCaptureSession(const MessageCaptureSession& session) = 0;
// Everything that changes while it runs: ends_at, ended_at, end_reason, where, zones and the counts
virtual void UpdateMessageCaptureSession(const MessageCaptureSession& session) = 0;
// In one transaction
virtual void InsertMessageCaptureEntries(const std::vector<MessageCaptureRecord>& entries) = 0;
virtual std::optional<MessageCaptureSession> GetMessageCaptureSession(uint64_t id) = 0;
virtual std::vector<MessageCaptureSession> GetMessageCaptureSessions(const SessionQuery& query) = 0;
virtual uint64_t CountMessageCaptureSessions(const SessionQuery& query) = 0;
// A session's messages with seq above afterSeq, in order
virtual std::vector<MessageCaptureRecord> GetMessageCaptureEntries(uint64_t sessionId, uint32_t afterSeq, uint32_t limit) = 0;
// The session and its messages
virtual void DeleteMessageCaptureSession(uint64_t id) = 0;
};
#endif //!__IMESSAGECAPTURES__H__

View File

@@ -0,0 +1,101 @@
#ifndef __IMODERATION__H__
#define __IMODERATION__H__
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "dCommonVars.h"
/**
* Moderation tools: the automatic steps strikes set off, reports players send from the game, the chat filter's words
* added by staff, and the addresses accounts log in from (for finding accounts that belong together).
*/
class IModeration {
public:
// ---- Strike steps ----
struct AppliedStrikeStep {
std::string step; // eStrikeStep's name
uint32_t count{}; // the number of active strikes it was applied for
int64_t time{};
};
// Note on a strike which step it set off, and for how many strikes
virtual void SetStrikeStep(uint64_t strikeId, const std::string& step, uint32_t count) = 0;
// Steps set off by the account's strikes given at or after `since` (revoked strikes included: the step still happened)
virtual std::vector<AppliedStrikeStep> GetAppliedStrikeSteps(uint32_t accountId, int64_t since) = 0;
// ---- Player reports ----
struct PlayerReport {
uint64_t id{};
int64_t createdAt{};
std::string kind; // ePlayerReportKind's name
LWOOBJID reporterId{}; // the reporting character
uint32_t reporterAccountId{};
LWOOBJID objectId{}; // what they picked in the game
int32_t objectLot{};
LWOOBJID targetCharacterId{}; // the player it is about: the reported player, or the model's or property's owner (0: unknown)
uint32_t targetAccountId{};
LWOOBJID propertyId{};
uint32_t zoneId{};
uint32_t instanceId{};
uint32_t cloneId{};
std::string body;
uint8_t status{}; // ePlayerReportStatus
std::string handledBy;
int64_t handledAt{};
std::string resolution;
};
struct PlayerReportQuery {
int16_t status{ -1 }; // ePlayerReportStatus, -1: any
uint32_t accountId{}; // reported account (0: any)
uint32_t limit{ 100 };
uint32_t offset{};
};
virtual uint64_t InsertPlayerReport(const PlayerReport& report) = 0;
virtual std::vector<PlayerReport> GetPlayerReports(const PlayerReportQuery& query) = 0; // newest first
virtual uint32_t CountPlayerReports(const PlayerReportQuery& query) = 0;
virtual std::optional<PlayerReport> GetPlayerReport(uint64_t id) = 0;
virtual void SetPlayerReportStatus(uint64_t id, uint8_t status, const std::string& handledBy, const std::string& resolution, int64_t time) = 0;
// ---- Chat filter words ----
struct ChatFilterWord {
std::string word; // lower case
bool allowed{}; // true: accepted in whitelisted chat; false: always stopped
std::string addedBy;
int64_t addedAt{};
};
virtual std::vector<ChatFilterWord> GetChatFilterWords() = 0;
// Adds the word, or moves it to the other list
virtual void SetChatFilterWord(const ChatFilterWord& word) = 0;
virtual bool DeleteChatFilterWord(const std::string& word) = 0;
// ---- Login addresses and linked accounts ----
// A successful game login from `address` (no port)
virtual void RecordLoginAddress(uint32_t accountId, const std::string& address, int64_t time) = 0;
struct LinkedAccount {
uint32_t accountId{};
std::string name;
uint8_t gmLevel{};
bool banned{};
uint8_t link{}; // eAccountLink
uint32_t shared{}; // LOGIN_ADDRESS: how many addresses they share; PLAY_KEY: accounts on the key
int64_t lastSeen{}; // LOGIN_ADDRESS: the other account's latest login from a shared address
};
// Other accounts with the same play key, email address or login address as this one
virtual std::vector<LinkedAccount> GetLinkedAccounts(uint32_t accountId) = 0;
// How many login addresses are recorded for the account
virtual uint32_t CountLoginAddresses(uint32_t accountId) = 0;
};
#endif //!__IMODERATION__H__

View File

@@ -3,12 +3,19 @@
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
#include "dCommonVars.h"
class IPetNames {
public:
struct Info {
std::string petName;
int32_t approvalStatus{};
// The character that owns the pet (0 when not known). Saving with 0 keeps the owner already stored.
LWOOBJID ownerId{};
};
// Set the pet name moderation status for the given pet id.
@@ -16,6 +23,19 @@ public:
// Get pet info for the given pet id.
virtual std::optional<IPetNames::Info> GetPetNameInfo(const LWOOBJID& petId) = 0;
// Get paginated pet names data for DataTables display
virtual std::string GetPetNamesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true, bool pendingOnly = false) = 0;
// Approve a pet name by id
virtual void ApprovePetName(const int64_t id) = 0;
// Reject (delete) a pet name by id
virtual void RejectPetName(const int64_t id) = 0;
// Pets whose owner was never recorded (named before owners were saved), and recording one (0: nobody has it)
virtual std::vector<LWOOBJID> GetPetsWithUnknownOwner() = 0;
virtual void SetPetOwner(const LWOOBJID petId, const LWOOBJID ownerId) = 0;
};
#endif //!__IPETNAMES__H__

View File

@@ -3,6 +3,10 @@
#include <cstdint>
#include <optional>
#include <string>
#include <string_view>
#include "json.hpp"
class IPlayKeys {
public:
@@ -10,6 +14,29 @@ public:
// Optional of bool may seem pointless, however the optional indicates if the playkey exists
// and the bool indicates if the playkey is active.
virtual std::optional<bool> IsPlaykeyActive(const int32_t playkeyId) = 0;
// Get paginated list of play keys with optional search/filtering for DataTables
// Returns a JSON-formatted string with the play key data and metadata
virtual std::string GetPlayKeysTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true) = 0;
virtual uint32_t GetPlayKeyCount() = 0;
virtual void CreatePlayKey(const std::string_view keyString, const uint32_t uses, const std::string_view notes = "") = 0;
virtual void SetPlayKeyActive(const int32_t playkeyId, const bool active) = 0;
// Get a single play key including the accounts that registered with it, or {"error": ...} if missing
virtual nlohmann::json GetPlayKey(const int32_t playkeyId) = 0;
virtual void UpdatePlayKey(const int32_t playkeyId, const uint32_t uses, const std::string_view notes, const bool active) = 0;
// Delete a play key, detaching any accounts that registered with it
virtual void DeletePlayKey(const int32_t playkeyId) = 0;
// Id of an active play key that still has uses left, for account registration
virtual std::optional<int32_t> GetRedeemablePlayKeyId(const std::string_view keyString) = 0;
virtual void SetAccountPlayKey(const uint32_t accountId, const int32_t playkeyId) = 0;
};
#endif //!__IPLAYKEYS__H__

View File

@@ -0,0 +1,61 @@
#ifndef __IPLAYERPOSITIONS__H__
#define __IPLAYERPOSITIONS__H__
#include <cstdint>
#include <optional>
#include <vector>
#include "dCommonVars.h"
#include "json.hpp"
/**
* Where players were: position samples the dashboard takes every few seconds from the positions world servers report,
* for replaying movement on the 3D world view (staff only, kept for position_history_days). Also the per-day map
* event cells behind the heat map timelapse.
*/
class IPlayerPositions {
public:
struct PositionSample {
int64_t time{};
LWOOBJID characterId{};
uint32_t zoneId{};
uint32_t instanceId{};
uint32_t cloneId{};
float x{};
float y{};
float z{};
};
// Adds a batch in one transaction; a character's second sample in the same second replaces the first
virtual void InsertPositionSamples(const std::vector<PositionSample>& samples) = 0;
/**
* Samples in a zone between from and to (instance 0: every instance), ordered by character and time. With
* bucketSeconds > 1 each character gets one sample per bucket and instance (its average position, at the bucket's
* first time). At most `limit` rows.
*/
virtual std::vector<PositionSample> GetPositionSamples(uint32_t zoneId, uint32_t instanceId, int64_t from, int64_t to, int64_t bucketSeconds, uint32_t limit) = 0;
struct PositionInstance {
uint32_t zoneId{};
uint32_t instanceId{};
uint32_t cloneId{};
int64_t first{};
int64_t last{};
uint32_t players{};
};
// World instances with samples between from and to (zone 0: every zone), with their first and last sample and players seen
virtual std::vector<PositionInstance> GetPositionInstances(uint32_t zoneId, int64_t from, int64_t to) = 0;
virtual uint32_t PrunePositionSamples(int64_t beforeTime) = 0;
// Map events of a kind (IEconomyLedger::eMapEvent) per day and cell for a zone (one clone, or every instance):
// [{day, x, z, events}], by day, at most `limit`
virtual nlohmann::json GetMapCellsPerDay(uint32_t zoneId, std::optional<uint32_t> cloneId, uint8_t kind, uint32_t fromDay, uint32_t toDay, uint32_t limit) = 0;
// Characters seen in a zone's clone (a property) between from and to: [{character_id, name, first, last}], latest first
virtual nlohmann::json GetCloneVisitors(uint32_t zoneId, uint32_t cloneId, int64_t from, int64_t to, uint32_t limit) = 0;
};
#endif //!__IPLAYERPOSITIONS__H__

View File

@@ -3,6 +3,8 @@
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
enum ePropertySortType : int32_t;
@@ -21,6 +23,7 @@ public:
uint32_t claimedTime{};
uint32_t reputation{};
float performanceCost{};
uint32_t zoneId{};
};
struct PropertyLookup {
@@ -65,5 +68,44 @@ public:
// Insert a new property into the database.
virtual void InsertNewProperty(const IProperty::Info& info, const uint32_t templateId, const LWOZONEID& zoneId) = 0;
// Get paginated list of properties with optional search/filtering for DataTables
// Returns a JSON-formatted string with the property data and metadata
virtual std::string GetPropertiesTable(uint32_t start, uint32_t length, const std::string_view search = "", uint32_t orderColumn = 0, bool orderAsc = true, bool pendingOnly = false) = 0;
virtual uint32_t GetPropertyCount() = 0;
// Approve a property by id
virtual void ApproveProperty(const LWOOBJID propertyId) = 0;
// ---- Property showcase (dashboard): only properties that are public and approved by a moderator ----
enum class ShowcaseSort : uint8_t {
REPUTATION, // highest first, like the game's "popular" list
NEWEST, // last changed first
NAME,
};
struct ShowcaseQuery {
std::string search; // matched against the property name, description and owner's name
uint32_t zoneId{}; // 0: every zone
ShowcaseSort sort{ ShowcaseSort::REPUTATION };
uint32_t start{};
uint32_t length{ 24 };
};
struct ShowcaseEntry {
IProperty::Info info;
std::string ownerName; // the owner character's name
uint32_t modelCount{};
};
struct ShowcaseResult {
uint32_t total{}; // matching the query without start/length
std::vector<ShowcaseEntry> entries;
};
// Approved (mod_approved = 1) public (privacy_option = 2) properties whose owner character still exists
virtual ShowcaseResult GetShowcaseProperties(const ShowcaseQuery& query) = 0;
};
#endif //!__IPROPERTY__H__

View File

@@ -46,5 +46,8 @@ public:
// Gets a model by ID
virtual std::optional<Model> GetModel(const LWOOBJID modelID) = 0;
// Gets the id of the property a model is placed on
virtual std::optional<LWOOBJID> GetModelPropertyId(const LWOOBJID modelID) = 0;
};
#endif //!__IPROPERTIESCONTENTS__H__

View File

@@ -0,0 +1,35 @@
#ifndef __IRELATEDDATA__H__
#define __IRELATEDDATA__H__
#include <cstdint>
#include <string>
#include "dCommonVars.h"
#include "json.hpp"
/**
* Everything tied to one account or character, for the "related" sections of the dashboard's account and character
* pages. Each returns a JSON array of rows, newest first where that means something.
*/
class IRelatedData {
public:
virtual nlohmann::json GetPropertiesOwnedBy(LWOOBJID characterId) = 0;
virtual nlohmann::json GetBugReportsBy(LWOOBJID characterId, uint32_t limit) = 0;
virtual nlohmann::json GetEconomyFlagsFor(LWOOBJID characterId, uint32_t limit) = 0;
virtual nlohmann::json GetFriendsOf(LWOOBJID characterId) = 0;
virtual nlohmann::json GetCheatDetectionsFor(uint32_t accountId, uint32_t limit) = 0;
// Dashboard actions about the account or one of its characters
virtual nlohmann::json GetAuditAbout(uint32_t accountId, uint32_t limit) = 0;
// Every column of one property, 64-bit ids as strings; empty object when missing
virtual nlohmann::json GetPropertyRecord(LWOOBJID propertyId) = 0;
// Every properties_contents column of the property's models, with the ugc blueprint (without the LXFML itself, only its
// size), the modular build config and the creator's name
virtual nlohmann::json GetPropertyModelRecords(LWOOBJID propertyId) = 0;
// Moderators' decisions on names, so players see why one was turned down. kind: "name" (subject: the character)
// or "pet_name" (subject: the pet)
virtual void InsertModerationDecision(const std::string& kind, int64_t subjectId, const std::string& subject, bool approved, const std::string& reason, int64_t time) = 0;
virtual nlohmann::json GetModerationDecisions(const std::string& kind, int64_t subjectId, uint32_t limit) = 0;
};
#endif //!__IRELATEDDATA__H__

View File

@@ -0,0 +1,61 @@
#ifndef __ISCHEDULEDTASKS__H__
#define __ISCHEDULEDTASKS__H__
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
#include "json.hpp"
/**
* Periodic dashboard tasks (economy checks, pruning, ...): their schedule and on/off switch as changed on the dashboard,
* and a history of runs with their logs. Tasks without a row use their built-in schedule.
*/
class IScheduledTasks {
public:
struct TaskSettings {
std::string name;
std::optional<std::string> schedule; // nullopt: the task's default
bool enabled{ true };
int64_t lastScheduledAt{}; // the last time the schedule fired (for catching up after downtime)
int64_t updatedAt{};
std::string updatedBy;
};
enum class eRunStatus : uint8_t { SUCCEEDED = 1, FAILED = 2, TIMED_OUT = 3, INTERRUPTED = 4 };
struct TaskRun {
uint64_t id{};
std::string task;
std::string trigger; // "schedule" or "manual"
std::string actor; // who started a manual run
int64_t startedAt{};
int64_t finishedAt{};
eRunStatus status{ eRunStatus::SUCCEEDED };
std::string summary;
std::string log;
};
virtual std::vector<TaskSettings> GetScheduledTasks() = 0;
// Change the schedule (nullopt: back to the default) and on/off switch
virtual void SetScheduledTask(const std::string& name, const std::optional<std::string>& schedule, bool enabled, const std::string& by) = 0;
virtual void SetTaskLastScheduled(const std::string& name, int64_t time) = 0;
// Runs are written once they finish
virtual void InsertTaskRun(const TaskRun& run) = 0;
// Newest first, without logs; empty task means every task. DataTables shape.
virtual nlohmann::json GetTaskRunsTable(const std::string& task, uint32_t start, uint32_t length) = 0;
virtual std::optional<TaskRun> GetTaskRun(uint64_t id) = 0;
// The latest finished run of every task
virtual std::vector<TaskRun> GetLatestTaskRuns() = 0;
virtual uint32_t PruneTaskRuns(int64_t beforeTime) = 0;
};
#endif //!__ISCHEDULEDTASKS__H__

View File

@@ -0,0 +1,70 @@
#ifndef __ISERVERCONFIG__H__
#define __ISERVERCONFIG__H__
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
/**
* Server settings kept in the database so they can be edited from the dashboard.
* Each row is one key of one config file (sharedconfig.ini, worldconfig.ini, ...).
*/
class IServerConfig {
public:
struct Setting {
std::string file;
std::string name;
std::optional<std::string> fileValue; // from the ini or environment when a server last started (never for secrets)
std::string fileSource; // "file", "env", or "" when neither sets it
std::optional<std::string> webValue; // set on the dashboard
bool webWins{}; // the web value beats the file and environment
bool secret{};
std::string description;
int64_t seenAt{}; // last time a server reported this key from its files
int64_t updatedAt{};
std::string updatedBy;
};
// A server reports a key it read from its files or environment (keeps any web value)
virtual void ReportConfigFromFile(const Setting& setting) = 0;
// Every setting, or only those of the given files
virtual std::vector<Setting> GetServerConfig(const std::vector<std::string>& files) = 0;
// Set or clear (nullopt) the web value of a setting, creating it if needed
virtual void SetWebConfigValue(const std::string& file, const std::string& name, const std::optional<std::string>& value, bool webWins, const std::string& by) = 0;
// Forget a setting completely
virtual void DeleteServerConfig(const std::string& file, const std::string& name) = 0;
/**
* One change of a setting's web value, written when it is saved (never parsed back out of the audit log).
* Secrets keep no values: only that they changed.
*/
struct SettingChange {
uint64_t id{};
std::string file;
std::string name;
std::optional<std::string> oldValue; // web value before; nullopt: none
bool oldWebWins{};
std::optional<std::string> newValue; // web value after; nullopt: cleared
bool newWebWins{};
std::optional<std::string> fileValue; // what the files or environment set at the time (what a cleared value falls back to)
bool secret{};
bool removed{}; // the whole setting was forgotten
uint64_t revertOf{}; // the change this one undid
int64_t changedAt{};
uint32_t accountId{};
std::string changedBy;
};
virtual uint64_t InsertSettingChange(const SettingChange& change) = 0;
// Newest first; empty file and name: every setting. total: how many match
virtual std::vector<SettingChange> GetSettingChanges(const std::string& file, const std::string& name, uint32_t start, uint32_t length, uint64_t& total) = 0;
virtual std::optional<SettingChange> GetSettingChange(uint64_t id) = 0;
};
#endif //!__ISERVERCONFIG__H__

View File

@@ -0,0 +1,44 @@
#ifndef __ISERVERHEALTH__H__
#define __ISERVERHEALTH__H__
#include <cstdint>
#include <vector>
/**
* A sample of how the server is doing, taken by the dashboard every minute, for the health history.
*/
class IServerHealth {
public:
struct HealthSample {
int64_t time{};
uint32_t players{};
uint32_t worlds{};
bool authOnline{};
bool chatOnline{};
uint64_t memoryKb{}; // all server processes together (Linux only, else 0)
};
virtual void InsertHealthSample(const HealthSample& sample) = 0;
// Samples between from and to, averaged into buckets of `bucketSeconds`; players and worlds are the bucket's highest
virtual std::vector<HealthSample> GetHealthSamples(int64_t from, int64_t to, int64_t bucketSeconds) = 0;
// Also prunes the per-instance samples
virtual uint32_t PruneHealthSamples(int64_t beforeTime) = 0;
// Players in one world instance at a sample
struct InstanceSample {
int64_t time{};
uint32_t zoneId{};
uint32_t instanceId{};
uint32_t cloneId{};
uint32_t players{};
};
virtual void InsertInstanceSamples(const std::vector<InstanceSample>& samples) = 0;
// Per bucket of `bucketSeconds` and instance, its highest player count; zoneId 0: every zone
virtual std::vector<InstanceSample> GetInstanceSamples(int64_t from, int64_t to, int64_t bucketSeconds, uint32_t zoneId) = 0;
};
#endif //!__ISERVERHEALTH__H__

View File

@@ -0,0 +1,86 @@
#ifndef __ISERVEROPERATIONS__H__
#define __ISERVEROPERATIONS__H__
#include <cstdint>
#include <optional>
#include <string>
#include <vector>
/**
* Things staff schedule to happen to the running server from the dashboard: repeating announcements, scheduled events
* (feature flags, vanity changes, live events, announcements and restarts switched on and off together) and per-zone
* instance limits the master applies.
*/
class IServerOperations {
public:
struct ScheduledAnnouncement {
uint64_t id{};
std::string title;
std::string message;
std::vector<uint32_t> zones; // empty: every world
std::string schedule; // a Cron.h schedule, in UTC
int64_t startsAt{}; // 0: straight away
int64_t endsAt{}; // 0: never
bool enabled{ true };
int64_t lastSentAt{};
uint32_t sentCount{};
int64_t createdAt{};
std::string createdBy;
int64_t updatedAt{};
std::string updatedBy;
};
virtual std::vector<ScheduledAnnouncement> GetScheduledAnnouncements() = 0;
virtual uint64_t InsertScheduledAnnouncement(const ScheduledAnnouncement& announcement) = 0;
// Everything but the send counters
virtual void UpdateScheduledAnnouncement(const ScheduledAnnouncement& announcement) = 0;
// Records one send
virtual void MarkAnnouncementSent(uint64_t id, int64_t time) = 0;
virtual void DeleteScheduledAnnouncement(uint64_t id) = 0;
// Stored as numbers, so only append
enum class eEventState : uint8_t { SCHEDULED = 0, ACTIVE = 1, ENDED = 2, CANCELLED = 3, MISSED = 4 };
/**
* A scheduled event (the Events page): parts that switch on and off together (a feature flag in event_1..event_8,
* vanity changes, a live event, announcements, a restart; EventParts.h), on once between two times or by recurring
* rules (ScheduleRules.h). The world servers read the vanity parts of the events that are on.
*/
struct ScheduledEvent {
uint64_t id{};
std::string name;
std::string note;
uint8_t mode{ 1 }; // ScheduleRules::eMode: off, on by its schedule, always on
std::string schedule; // recurring rules as JSON; empty: once, from startsAt to endsAt
int64_t startsAt{};
int64_t endsAt{};
int32_t priority{}; // higher is laid on later and wins where events change the same vanity NPCs or files
std::string parts; // JSON: [{kind, config, applied, state, status}]
eEventState state{ eEventState::SCHEDULED }; // ACTIVE while its parts are on; a once event ends ENDED, MISSED or CANCELLED
std::string status; // the last thing that happened to it
int64_t createdAt{};
std::string createdBy;
int64_t updatedAt{};
std::string updatedBy;
};
virtual std::vector<ScheduledEvent> GetScheduledEvents() = 0;
virtual uint64_t InsertScheduledEvent(const ScheduledEvent& event) = 0;
virtual void UpdateScheduledEvent(const ScheduledEvent& event) = 0;
virtual void DeleteScheduledEvent(uint64_t id) = 0;
struct ZoneLimit {
uint32_t zoneId{};
std::optional<uint32_t> softCap; // nullopt: the client's ZoneTable population_soft_cap
std::optional<uint32_t> hardCap; // nullopt: ZoneTable population_hard_cap
uint32_t spareInstances{}; // keep this many instances with room running
int64_t updatedAt{};
std::string updatedBy;
};
virtual std::vector<ZoneLimit> GetZoneLimits() = 0;
virtual void SetZoneLimit(const ZoneLimit& limit) = 0;
virtual void DeleteZoneLimit(uint32_t zoneId) = 0;
};
#endif //!__ISERVEROPERATIONS__H__

View File

@@ -0,0 +1,41 @@
#ifndef __ISLASHCOMMANDS__H__
#define __ISLASHCOMMANDS__H__
#include <cstdint>
#include <string>
#include <vector>
/**
* The in-game slash commands, as the world servers registered them, so the dashboard can list them and change who may
* use them (the levels themselves are settings: command_level_<name>, see SlashCommandLevels.h).
*/
class ISlashCommands {
public:
struct SlashCommand {
std::string name; // settings name, from the first alias
std::vector<std::string> aliases; // what players type, first one first
std::string help;
std::string info;
uint8_t defaultLevel{}; // the level in the code
uint8_t minLevel{}; // the lowest level it may be set to
bool fixed{}; // always defaultLevel
bool clientHandled{}; // the game client acts on it by itself (emotes, team and friend commands)
std::string note; // why it has a floor or is fixed
std::string dashboardPermission; // the dashboard permission that does the same thing, if any (its level is the command's)
std::string targetRule; // who it may be used on: SlashCommandLevels::TargetRuleName ("" acts on nobody else)
bool followsPermission{}; // uses dashboardPermission's level (false in rows stored before commands did)
bool operator==(const SlashCommand&) const = default;
};
// Every command, by name
virtual std::vector<SlashCommand> GetSlashCommands() = 0;
// Add or update one command
virtual void SetSlashCommand(const SlashCommand& command) = 0;
// Forget a command the code no longer has
virtual void DeleteSlashCommand(const std::string& name) = 0;
};
#endif //!__ISLASHCOMMANDS__H__