diff --git a/dDatabase/ConfigSync.cpp b/dDatabase/ConfigSync.cpp index e5f626f67..3ec66e245 100644 --- a/dDatabase/ConfigSync.cpp +++ b/dDatabase/ConfigSync.cpp @@ -58,6 +58,16 @@ namespace ConfigSync { Database::Get()->ReportConfigFromFile(setting); } + // A key taken out of a file this server read is forgotten: its row came from that file (not the environment, + // not only the dashboard) and nobody set it on the dashboard + std::set filesRead; + for (const auto& entry : config.GetFileEntries()) filesRead.insert(entry.file); + for (const auto& row : Database::Get()->GetServerConfig({ filesRead.begin(), filesRead.end() })) { + if (row.fileSource != "file" || row.webValue || setLocally.contains(row.name) || Permissions::IsPermissionSetting(row.name)) continue; + LOG("Forgetting the setting %s of %s: it is no longer in the file", row.name.c_str(), row.file.c_str()); + Database::Get()->DeleteServerConfig(row.file, row.name); + } + const auto rows = Database::Get()->GetServerConfig({ config.GetFileName(), "sharedconfig.ini" }); // Keys set only by environment variables count as set locally too for (const auto& row : rows) { diff --git a/docs/Dashboard.md b/docs/Dashboard.md index 63bd9bf8a..5d33e1fa5 100644 --- a/docs/Dashboard.md +++ b/docs/Dashboard.md @@ -224,6 +224,18 @@ the Settings page by earlier versions are ignored. Programs such as `mysqldump` Secrets (passwords, tokens, keys) from files are never copied into the database; secrets set on the web are stored in the database and can't be read back from the page. +Where the rows come from and when they go: every server reports the keys of the files it read when it starts +(`ConfigSync::Sync`, table `server_config`). A key taken out of a file is forgotten the next time a server that reads +that file starts: its row is deleted when it came from the file (not an environment variable), has no value set on this +page, and isn't a permission level. Rows with a value set here stay until that value is removed. + +The shipped `resources/*.ini` files list every setting in the catalog (`dDashboardServer/routes/SettingsCatalog.cpp`) +as `key=default` under a comment with its title and description, or name it in a comment (numbered families such as +`event_1`...`event_8` and the icon framing keys); the test `SettingsCatalogTests.ShippedFilesListEveryCatalogSetting` +fails when one is missing, and running it with `DLU_WRITE_INI_TEMPLATES=1` adds the missing ones to the files under +their section. CMake copies a file that isn't in the build folder yet and appends missing keys (without comments) to +one that is; values already in a server's files are never changed by it. + ### Setting history The **History** tab of the Settings page (same `settings` permission) lists every change made on the Settings page, and diff --git a/resources/authconfig.ini b/resources/authconfig.ini index 7938b5cca..66df3cc16 100644 --- a/resources/authconfig.ini +++ b/resources/authconfig.ini @@ -12,3 +12,11 @@ dont_use_keys=0 # 4 allows LEGOClub access # 30 makes the client not consume bricks when in bbb mode rewardcodes=4,30 + +# ---- Logging in ---- +# Staff only: Only accounts with a GM level can log in. +closed_to_non_devs=0 +# Remember login addresses: Keep the network address each account logs in to the game from, so staff see accounts that +# share one. Addresses are personal data: they're only shown as links between accounts, and deleted after +# log_login_address_days. +log_login_addresses=1 diff --git a/resources/dashboardconfig.ini b/resources/dashboardconfig.ini index 14e3b660d..a9a4b6567 100644 --- a/resources/dashboardconfig.ini +++ b/resources/dashboardconfig.ini @@ -143,3 +143,149 @@ log_task_days=90 # Change the lowest GM level (1-9) allowed to do something on the dashboard, e.g. permission_accounts_ban=3. # Every permission and its name is listed on the dashboard's Permissions page, where GM 9 can also change them; # a level set there beats this file. + +# ---- Dashboard sign-in ---- +# Password reset with recovery codes: Players with two-factor login can choose a new password on /forgot_password with a +# code from their authenticator app and one of their recovery codes, no email needed. +password_reset_recovery_codes=1 + +# ---- OAuth2 sign-in ---- +# Refresh token: Set by Connect mail account; only set it by hand for a token from elsewhere. Read when the server +# starts. +smtp_oauth2_refresh_token= + +# ---- Tools ---- +# Challenge milestones: Percentages of a community challenge announced in game as it gets there, e.g. 25,50,75 +# (completing it is always announced). +challenge_milestones=25,50,75,100 + +# ---- Public pages ---- +# Public server status: The page at /status, its JSON at /api/public/status (for server lists) and a widget for other +# sites. +public_status=0 +# Server name: Shown on the status page and widget. +public_server_name=DarkflameServer +# Players per world: How many players are in each world. +public_status_players=1 +# Names of players online: Character names (never staff, never account names) by world. +public_status_names=0 +# Uptime: How long the server has been up, and how much of the last day and week. +public_status_uptime=1 +# Server health: Whether login and chat are up, and how many worlds are running. +public_status_health=1 +# Leaderboard places: The top places of every leaderboard. 0 leaves leaderboards out. (places) +public_status_leaderboard_top=3 +# Community challenges: Public challenges that are running or finished in the last week, with their progress. +public_status_challenges=1 +# Live events: Live events running now (their title, where, and until when). +public_status_events=1 +# Widget for other sites: A small page at /status/widget other sites may show in an iframe. +public_status_widget=1 +# Refresh every: The status is worked out at most this often, however many people ask. (seconds) +public_status_cache_seconds=60 +# Property showcase for everyone: Let people who aren't signed in browse approved public properties at /showcase. +# Signed-in players need the showcase_view permission. +showcase_public=0 + +# ---- Metrics ---- +# API key rate limit: Requests a minute an API key may make unless the key sets its own limit (up to 6000). (/min) +api_key_rate_limit=120 +# Metrics endpoint: Off: /metrics answers 404. On: scrapers send an API token of an account with metrics_view, or the +# token below. +metrics_enabled=0 +# Scraper token: A shared secret scrapers may send as Authorization: Bearer instead of an API token. At least 16 +# characters; empty: API tokens only. +metrics_token= +# Allowed addresses: Comma separated addresses or IPv4 ranges (10.0.0.0/8) that may fetch metrics. Empty: any. +metrics_allowed_ips= +# Refresh every: Metrics are worked out at most this often, however often they are fetched. (seconds) +metrics_cache_seconds=10 + +# ---- Strikes ---- +# Strikes count for: Older strikes stay on the account's record but no longer count. 0: they always count. (days) +strike_expiry_days=0 +# Warn at: When an account reaches this many active strikes it gets a warning, shown to the player if they're online. 0: +# never. (strikes) +strike_warn_at=0 +# Mute at: ...is muted for the days below. 0: never. (strikes) +strike_mute_at=0 +# Mute for (days) +strike_mute_days=3 +# Ban at: ...is banned for the days below. 0: never. (strikes) +strike_ban_at=0 +# Ban for: 0: permanently. (days) +strike_ban_days=7 + +# ---- AI moderator helper ---- +# AI moderator helper: Off: the Suggest buttons say the helper is off and nothing is sent. +ai_helper_enabled=0 +# Claude API key: From console.anthropic.com. Never shown back or logged. +claude_api_key= +# API address: Only change it for a proxy or a local test server (http:// only for this machine). +claude_api_base=https://api.anthropic.com +# Model: Any model ID works. claude-sonnet-5 is a good balance; claude-haiku-4-5-20251001 is cheaper, claude-opus-5-5 +# more careful. +claude_model=claude-sonnet-5 +# Hold answers to a schema: Ask the API for JSON of the expected shape (structured outputs). Turn off only if a model +# refuses it; answers are checked either way. +claude_structured_output=1 +# Server rules: Your code of conduct, as players know it. Sent with every request so suggestions follow your rules. +ai_helper_rules= +# Time out after: Per try; failed tries (busy API, network) are retried up to three times. (seconds) +ai_helper_timeout=60 +# Longest answer: max_tokens per request: caps what one suggestion can cost. (tokens) +ai_helper_max_tokens=2000 +# Requests per minute: For the whole dashboard. (requests) +ai_helper_per_minute=5 +# Requests per day: For the whole dashboard, per UTC day. Asking about the same thing again reuses the stored suggestion +# and costs nothing. (requests) +ai_helper_per_day=200 +# Chat around the item: How far before and after a report or message the player's chat is included. (minutes) +ai_helper_chat_minutes=10 + +# ---- Backups ---- +# Backup folder: Relative to the server binaries unless absolute. +backup_folder=backups +# Backups to keep: Older backups are deleted after each new one. 0 keeps them all. (backups) +backup_keep=7 +# mysqldump program: The mysqldump (or mariadb-dump) to run. +backup_mysqldump=mysqldump + +# ---- Logs ---- +# Chat log (days) +log_chat_days=90 +# Login addresses: Addresses an account hasn't logged in from for this long are forgotten. (days) +log_login_address_days=90 +# Server health history: Minute-by-minute player counts and uptime. (days) +health_days=30 +# Server traffic history: Minute-by-minute packets, bytes and HTTP requests of every server (Diagnostics). The last hour +# at one second is only kept in memory. (days) +traffic_days=30 + +# ---- Log bundles ---- +# At most: How much log text (before compression) one download may hold. Bundles are built in the system's temporary +# folder and deleted once sent. (MB) +log_bundle_max_mb=512 + +# ---- Player movement ---- +# Record player movement: Keeps a player's position every few seconds while they move, and every 30 seconds while they +# stand still. +position_history=1 +# Record every: While a player moves. Less often keeps the table smaller; replays then move in straighter lines. +# (seconds) +position_history_seconds=5 +# Keep movement for: About 17,000 rows per player online around the clock per day at 5 seconds. (days) +position_history_days=3 + +# ---- Message inspector ---- +# Keep captures for: 0: no age limit. (days) +inspector_session_days=30 +# At most: When all saved captures together take more, the oldest are deleted. A busy 15 minute capture can take 50 MB. +# 0: no size limit. (MB) +inspector_max_mb=1024 + +# ---- Character history ---- +# Snapshots: Older snapshots are deleted, but each character keeps its newest few (below). (days) +snapshot_days=90 +# Always keep per character (snapshots) +snapshot_keep=10 diff --git a/resources/sharedconfig.ini b/resources/sharedconfig.ini index 1b3d5c762..03bdbaadb 100644 --- a/resources/sharedconfig.ini +++ b/resources/sharedconfig.ini @@ -104,3 +104,9 @@ ugc_manifest=0 # gets every model's LXFML and builds it first (for its collision: the UGC server makes no physics), then is switched to # the served mesh a few seconds after it has loaded. See docs/UgcServer.md. ugc_manifest_models=0 + +# ---- Chat log ---- +# Keep a chat log: Zone chat, and what the chat filter stopped. +log_chat=1 +# Include whispers and team chat: Only staff with the Read private chat permission see them. +log_private_chat=1 diff --git a/resources/worldconfig.ini b/resources/worldconfig.ini index 0c45ddf81..f03271b55 100644 --- a/resources/worldconfig.ini +++ b/resources/worldconfig.ini @@ -121,3 +121,50 @@ save_property_location=0 # by the zone's scene transitions, and the global scene) instead of the objects within the ghosting distances. # Needs the zone's terrain scene map; zones without one keep distance ghosting. ghosting_scenes=0 + +# ---- Logging and crashes ---- +# Crash dumps from world servers: Write a dump when a world server crashes (needs the crash dump folder). Read when the +# server starts. +generate_dump=0 + +# ---- Property rent ---- +# Charge rent: Rent is taken from the owner's coins when they log in. Unpaid rent makes the property private until it is +# paid. +property_rent_enabled=0 +# Grace period: How long rent can be unpaid before the property is made private. (days) +property_rent_grace_days=3 + +# ---- Property reputation ---- +# Properties earn reputation +property_reputation_enabled=1 +# Minimum visit: A visit earns nothing before this (live's property reputation delay). (seconds) +property_reputation_min_visit=120 +# Points per minute: Times the property's reputationPerMinute (1). Raise it for small servers. (×) +property_reputation_multiplier=1 +# Minutes per visit: At most this many minutes of one visit count. (minutes) +property_reputation_max_minutes=30 +# Per visitor per day: Most one account can give one property in a day. (points) +property_reputation_visitor_daily_cap=30 +# Per property per day: Most a property can get in a day from everyone. (points) +property_reputation_daily_cap=300 +# Repeat visitor falloff: A visitor who gave the property reputation on d recent days earns 1 / (1 + this × d) as much. +property_reputation_repeat_falloff=0.5 +# Recent days: How far back repeat visits are counted. (days) +property_reputation_repeat_days=30 +# Only active visitors: A minute only counts if the visitor moved; idle characters earn nothing. +property_reputation_require_activity=1 +# Ignore staff: Accounts with a GM level don't give reputation. +property_reputation_ignore_staff=1 +# Ignore linked accounts: Accounts sharing a play key, email or login address with the owner's don't give reputation. +property_reputation_ignore_linked=1 + +# ---- Chat log ---- +# Filter messages sent into the game: Messages from the dashboard or a chat bridge go through the same chat filter as +# players'. +chat_bridge_filter=1 + +# ---- Contraband ---- +# Don't check staff: Accounts with a GM level keep listed items and aren't flagged. +contraband_ignore_staff=1 +# Tell players: A mail (at login) or a chat message (when received) says which item was removed and why. +contraband_notify_players=1 diff --git a/tests/dWebTests/SettingsCatalogTests.cpp b/tests/dWebTests/SettingsCatalogTests.cpp index 7d7b9ccc4..e49e615ea 100644 --- a/tests/dWebTests/SettingsCatalogTests.cpp +++ b/tests/dWebTests/SettingsCatalogTests.cpp @@ -3,7 +3,10 @@ #include #include #include +#include +#include #include +#include #include "SettingsCatalog.h" @@ -170,3 +173,62 @@ TEST(SettingsCatalogTests, CoversEverySettingTheCodeReads) { EXPECT_GT(found, 100u); for (const auto& key : missing) ADD_FAILURE() << key << " is read by the code but not in SettingsCatalog.cpp"; } + +// Every setting in the catalog is in its shipped .ini (as `key=` or named in a comment), so a server's files list what +// it reads. With DLU_WRITE_INI_TEMPLATES=1 the missing ones are added to resources/*.ini instead, under their section, +// with their description and default. +TEST(SettingsCatalogTests, ShippedFilesListEveryCatalogSetting) { + const std::filesystem::path resources = std::filesystem::path(DLU_SOURCE_DIR) / "resources"; + const bool write = std::getenv("DLU_WRITE_INI_TEMPLATES") && std::string(std::getenv("DLU_WRITE_INI_TEMPLATES")) == "1"; + const auto mentions = [](const std::string& text, const std::string& key) { + const std::regex named("(^|[\\s#,:(])" + key + "(=|[\\s,:.)]|$)", std::regex::multiline); + return std::regex_search(text, named); + }; + // file -> section -> the settings missing from it, in catalog order + std::map>>> missing; + for (const auto& setting : SettingsCatalog::All()) { + if (setting.unused || setting.key.ends_with('_')) continue; // old names; numbered families (event_1, help_1...) + std::ifstream in(resources / setting.file); + const std::string text((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + if (mentions(text, setting.key)) continue; + auto& sections = missing[setting.file]; + auto it = std::find_if(sections.begin(), sections.end(), [&](const auto& s) { return s.first == setting.section; }); + if (it == sections.end()) it = sections.insert(sections.end(), { setting.section, {} }); + it->second.push_back(&setting); + } + if (!write) { + for (const auto& [file, sections] : missing) { + for (const auto& [section, settings] : sections) { + for (const auto* setting : settings) ADD_FAILURE() << file << " " << setting->key << " is in SettingsCatalog.cpp but not in resources/" << file << " (run with DLU_WRITE_INI_TEMPLATES=1 to add it)"; + } + } + return; + } + // A comment wrapped to 120 columns + const auto comment = [](const std::string& text) { + std::string out, line = "#"; + std::istringstream words(text); + std::string word; + while (words >> word) { + if (line.size() + 1 + word.size() > 120) { + out += line + "\n"; + line = "#"; + } + line += " " + word; + } + return line == "#" ? out : out + line + "\n"; + }; + for (const auto& [file, sections] : missing) { + std::ofstream out(resources / file, std::ios::app); + for (const auto& [section, settings] : sections) { + out << "\n# ---- " << section << " ----\n"; + for (const auto* setting : settings) { + std::string text = setting->title; + if (!setting->description.empty()) text += ": " + setting->description; + if (!setting->unit.empty()) text += " (" + setting->unit + ")"; + if (setting->restart) text += " Read when the server starts."; + out << comment(text) << setting->key << "=" << (setting->type == SettingsCatalog::eType::SECRET ? "" : setting->defaultValue) << "\n"; + } + } + } +}