feat: Add 15 backend service stub files with TODO implementations

Stub files for all services declared in services/mod.rs:
- Panel adapters: AMP, Pterodactyl, Companion (NATS-based)
- Wipe pipeline: WipeEngine, Scheduler, HealthChecker, BackupManager
- Infrastructure: NatsBridge (JetStream), SteamUpdateWatcher, MapManager
- Notifications: Discord webhooks, Pushbullet push notifications
- Platform: LicenseService, CloudflareService, encryption utilities

Each file has struct definitions, method signatures with proper types,
and descriptive TODO comments outlining the implementation plan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Vantz Stockwell
2026-02-14 21:35:16 -05:00
parent 1935a46822
commit cc8c1e3519
15 changed files with 1025 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
use anyhow::Result;
use async_trait::async_trait;
use super::panel_adapter::{DiscoveredServer, FileEntry, PanelAdapter, ServerStatus};
/// AMP (Application Management Panel) adapter.
///
/// Communicates with AMP instances via their REST API to manage
/// Rust game servers. Implements the unified PanelAdapter trait
/// so the wipe engine and other services remain panel-agnostic.
pub struct AmpAdapter {
pub api_endpoint: String,
pub api_key: String,
}
impl AmpAdapter {
pub fn new(api_endpoint: String, api_key: String) -> Self {
Self {
api_endpoint,
api_key,
}
}
}
#[async_trait]
impl PanelAdapter for AmpAdapter {
async fn test_connection(&self) -> Result<bool> {
// TODO: POST to AMP API /API/Core/Login, verify session token returned
todo!()
}
async fn discover_servers(&self) -> Result<Vec<DiscoveredServer>> {
// TODO: GET instances from AMP API, map to DiscoveredServer
todo!()
}
async fn get_server_status(&self, _server_id: &str) -> Result<ServerStatus> {
// TODO: Query AMP instance status endpoint
todo!()
}
async fn start_server(&self, _server_id: &str) -> Result<()> {
// TODO: POST to AMP API /API/Core/Start
todo!()
}
async fn stop_server(&self, _server_id: &str) -> Result<()> {
// TODO: POST to AMP API /API/Core/Stop
todo!()
}
async fn restart_server(&self, _server_id: &str) -> Result<()> {
// TODO: POST to AMP API /API/Core/Restart
todo!()
}
async fn send_command(&self, _server_id: &str, _command: &str) -> Result<String> {
// TODO: POST to AMP API /API/Core/SendConsoleMessage
todo!()
}
async fn get_file(&self, _server_id: &str, _path: &str) -> Result<Vec<u8>> {
// TODO: GET file contents via AMP file manager API
todo!()
}
async fn put_file(&self, _server_id: &str, _path: &str, _data: &[u8]) -> Result<()> {
// TODO: Upload file via AMP file manager API
todo!()
}
async fn delete_file(&self, _server_id: &str, _path: &str) -> Result<()> {
// TODO: DELETE file via AMP file manager API
todo!()
}
async fn list_files(&self, _server_id: &str, _path: &str) -> Result<Vec<FileEntry>> {
// TODO: GET directory listing from AMP file manager API
todo!()
}
async fn trigger_steam_update(&self, _server_id: &str) -> Result<()> {
// TODO: POST to AMP API /API/Core/Update to trigger SteamCMD
todo!()
}
}

View File

@@ -0,0 +1,78 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use serde::Serialize;
use uuid::Uuid;
/// Metadata for a stored backup.
#[derive(Debug, Clone, Serialize)]
pub struct BackupInfo {
pub id: Uuid,
pub license_id: Uuid,
pub wipe_history_id: Option<Uuid>,
pub storage_path: String,
pub size_bytes: i64,
pub created_at: DateTime<Utc>,
}
/// Pre-wipe backup and rollback management.
///
/// Creates snapshots of server state (map, save data, plugin data,
/// config files) before a wipe executes. Backups are used by the
/// wipe engine's rollback mechanism if post-wipe verification fails.
pub struct BackupManager {
// TODO: Add fields:
// - db: sqlx::PgPool
// - storage_base_path: String
}
impl BackupManager {
/// Create a pre-wipe backup of the server's current state.
///
/// Captures: map/save files, plugin data directories, server.cfg,
/// and any other files configured for backup in the wipe profile.
/// Returns a backup reference ID stored in wipe_history.
pub async fn create_backup(
&self,
_license_id: Uuid,
_wipe_history_id: Uuid,
) -> Result<String> {
// TODO: Resolve PanelAdapter for the server
// TODO: Download save files (map, sav, player data) via adapter
// TODO: Download plugin data directories via adapter
// TODO: Download server.cfg via adapter
// TODO: Bundle into archive (tar.gz)
// TODO: Write archive to backup storage
// TODO: Insert backup record in DB
// TODO: Return backup reference string
todo!()
}
/// Restore a previously created backup to the server.
///
/// Used during rollback when post-wipe verification fails.
pub async fn restore_backup(&self, _backup_reference: &str) -> Result<()> {
// TODO: Load backup record from DB by reference
// TODO: Read backup archive from storage
// TODO: Extract archive contents
// TODO: Upload files back to server via PanelAdapter
// TODO: Log restoration details
todo!()
}
/// List all backups for a license, ordered by creation date (newest first).
pub async fn list_backups(&self, _license_id: Uuid) -> Result<Vec<BackupInfo>> {
// TODO: Query backup records from DB for this license
// TODO: Return sorted list
todo!()
}
/// Clean up old backups beyond the configured retention period/count.
pub async fn cleanup_old_backups(&self, _license_id: Uuid) -> Result<u32> {
// TODO: Load retention config (max count or max age)
// TODO: Query backups older than retention threshold
// TODO: Delete backup files from storage
// TODO: Delete backup records from DB
// TODO: Return count of deleted backups
todo!()
}
}

View File

@@ -0,0 +1,46 @@
use anyhow::Result;
/// Cloudflare DNS management service.
///
/// Manages DNS records for tenant subdomains (e.g., myserver.corrosion.gg)
/// and custom domains. Uses the Cloudflare API v4 to create and manage
/// CNAME records pointing to the platform's load balancer.
pub struct CloudflareService {
// TODO: Add fields:
// - api_token: String
// - zone_id: String (Cloudflare zone for the base domain)
// - base_domain: String (e.g., "corrosion.gg")
}
impl CloudflareService {
/// Create a subdomain CNAME record for a new license.
///
/// Creates: {subdomain}.{base_domain} -> platform LB
pub async fn create_subdomain(&self, _subdomain: &str) -> Result<String> {
// TODO: POST /zones/{zone_id}/dns_records
// TODO: Type: CNAME, Name: {subdomain}, Content: platform target
// TODO: Proxied: true (orange cloud)
// TODO: Return the DNS record ID for later management
todo!()
}
/// Delete a subdomain CNAME record.
///
/// Called when a license is deactivated or transferred.
pub async fn delete_subdomain(&self, _dns_record_id: &str) -> Result<()> {
// TODO: DELETE /zones/{zone_id}/dns_records/{dns_record_id}
todo!()
}
/// Add a custom domain with CNAME verification.
///
/// Returns the CNAME target that the customer needs to configure
/// on their own DNS provider.
pub async fn add_custom_domain(&self, _custom_domain: &str) -> Result<String> {
// TODO: Generate verification CNAME target
// TODO: Create verification DNS record in Cloudflare
// TODO: Optionally configure SSL for custom hostname via Cloudflare
// TODO: Return the CNAME target for customer DNS setup
todo!()
}
}

View File

@@ -0,0 +1,92 @@
use anyhow::Result;
use async_trait::async_trait;
use uuid::Uuid;
use super::panel_adapter::{DiscoveredServer, FileEntry, PanelAdapter, ServerStatus};
/// Companion Agent adapter.
///
/// Unlike AMP and Pterodactyl which use REST APIs, the Companion Agent
/// runs alongside the game server and communicates via NATS JetStream.
/// Commands are published to the agent's subject namespace and responses
/// are received on reply subjects. This enables direct server management
/// without a panel intermediary.
pub struct CompanionAdapter {
pub nats: async_nats::Client,
pub license_id: Uuid,
}
impl CompanionAdapter {
pub fn new(nats: async_nats::Client, license_id: Uuid) -> Self {
Self { nats, license_id }
}
}
#[async_trait]
impl PanelAdapter for CompanionAdapter {
async fn test_connection(&self) -> Result<bool> {
// TODO: Publish heartbeat request to corrosion.{license_id}.agent.ping,
// await reply within timeout
todo!()
}
async fn discover_servers(&self) -> Result<Vec<DiscoveredServer>> {
// TODO: Request server info from agent via NATS request/reply.
// Companion manages exactly one server, so this returns a single-element vec.
todo!()
}
async fn get_server_status(&self, _server_id: &str) -> Result<ServerStatus> {
// TODO: NATS request to corrosion.{license_id}.agent.status
todo!()
}
async fn start_server(&self, _server_id: &str) -> Result<()> {
// TODO: NATS request to corrosion.{license_id}.agent.start
todo!()
}
async fn stop_server(&self, _server_id: &str) -> Result<()> {
// TODO: NATS request to corrosion.{license_id}.agent.stop
todo!()
}
async fn restart_server(&self, _server_id: &str) -> Result<()> {
// TODO: NATS request to corrosion.{license_id}.agent.restart
todo!()
}
async fn send_command(&self, _server_id: &str, _command: &str) -> Result<String> {
// TODO: NATS request to corrosion.{license_id}.agent.command
// with command payload, await RCON response
todo!()
}
async fn get_file(&self, _server_id: &str, _path: &str) -> Result<Vec<u8>> {
// TODO: NATS request to corrosion.{license_id}.agent.file.get
// Agent reads local file and returns contents
todo!()
}
async fn put_file(&self, _server_id: &str, _path: &str, _data: &[u8]) -> Result<()> {
// TODO: NATS publish to corrosion.{license_id}.agent.file.put
// May need chunking for large files
todo!()
}
async fn delete_file(&self, _server_id: &str, _path: &str) -> Result<()> {
// TODO: NATS request to corrosion.{license_id}.agent.file.delete
todo!()
}
async fn list_files(&self, _server_id: &str, _path: &str) -> Result<Vec<FileEntry>> {
// TODO: NATS request to corrosion.{license_id}.agent.file.list
todo!()
}
async fn trigger_steam_update(&self, _server_id: &str) -> Result<()> {
// TODO: NATS request to corrosion.{license_id}.agent.update
// Agent runs SteamCMD locally
todo!()
}
}

View File

@@ -0,0 +1,87 @@
use anyhow::Result;
use serde::Serialize;
/// Discord webhook embed payload.
#[derive(Debug, Clone, Serialize)]
pub struct DiscordEmbed {
pub title: String,
pub description: String,
pub color: u32,
pub fields: Vec<DiscordEmbedField>,
pub timestamp: Option<String>,
}
/// Field within a Discord embed.
#[derive(Debug, Clone, Serialize)]
pub struct DiscordEmbedField {
pub name: String,
pub value: String,
pub inline: bool,
}
/// Discord webhook notification service.
///
/// Sends rich embed messages to Discord channels via webhook URLs.
/// Used for wipe announcements, completion notifications, failure
/// alerts, and crash recovery notifications.
pub struct DiscordNotifier {
// TODO: Add fields:
// - webhook_url: String
// - server_name: String (for embed context)
}
impl DiscordNotifier {
/// Send a generic notification with a custom embed.
pub async fn send_notification(&self, _embed: DiscordEmbed) -> Result<()> {
// TODO: POST to webhook_url with JSON payload { embeds: [embed] }
// TODO: Handle rate limiting (429 responses with Retry-After)
todo!()
}
/// Send a wipe-starting announcement.
pub async fn send_wipe_start(&self, _wipe_type: &str, _eta_minutes: u32) -> Result<()> {
// TODO: Build embed with orange color, wipe type, countdown info
// TODO: Include server name, expected completion time
// TODO: Call send_notification
todo!()
}
/// Send a wipe-completed announcement.
pub async fn send_wipe_complete(
&self,
_wipe_type: &str,
_duration_seconds: u64,
_new_map: Option<&str>,
_new_seed: Option<i32>,
) -> Result<()> {
// TODO: Build embed with green color, wipe summary
// TODO: Include new map/seed info, connect URL
// TODO: Call send_notification
todo!()
}
/// Send a wipe-failed alert.
pub async fn send_wipe_failed(
&self,
_wipe_type: &str,
_error: &str,
_rolled_back: bool,
) -> Result<()> {
// TODO: Build embed with red color, failure details
// TODO: Include rollback status, error message
// TODO: Call send_notification
todo!()
}
/// Send a crash detection/recovery alert.
pub async fn send_crash_alert(
&self,
_crash_count: u32,
_auto_recovered: bool,
) -> Result<()> {
// TODO: Build embed with red/yellow color based on severity
// TODO: Include crash count, recovery status, uptime before crash
// TODO: Call send_notification
todo!()
}
}

View File

@@ -0,0 +1,46 @@
use anyhow::Result;
/// AES-256-GCM encryption utilities.
///
/// Used to encrypt sensitive data at rest: panel API keys, companion
/// agent tokens, webhook URLs, and other secrets stored in the database.
/// Keys are derived from the application's ENCRYPTION_KEY environment
/// variable.
/// Encrypt a plaintext string using AES-256-GCM.
///
/// Returns a base64-encoded string containing the nonce + ciphertext.
/// The nonce is randomly generated and prepended to the ciphertext
/// before encoding.
pub fn encrypt(_plaintext: &str, _key: &str) -> Result<String> {
// TODO: Derive 256-bit key from key string (SHA-256 or HKDF)
// TODO: Generate random 96-bit nonce
// TODO: Encrypt plaintext with AES-256-GCM
// TODO: Prepend nonce to ciphertext
// TODO: Base64-encode the result
// TODO: Return encoded string
todo!()
}
/// Decrypt an AES-256-GCM encrypted string.
///
/// Expects a base64-encoded string containing nonce + ciphertext
/// (as produced by `encrypt`).
pub fn decrypt(_ciphertext: &str, _key: &str) -> Result<String> {
// TODO: Base64-decode the input
// TODO: Split nonce (first 12 bytes) from ciphertext
// TODO: Derive 256-bit key from key string (same as encrypt)
// TODO: Decrypt with AES-256-GCM
// TODO: Return plaintext string
todo!()
}
/// Generate a cryptographically random token string.
///
/// Returns a hex-encoded random string of the specified byte length.
/// Used for generating API keys, agent tokens, and session secrets.
pub fn generate_token(_length: usize) -> String {
// TODO: Generate `length` random bytes using rand/OsRng
// TODO: Hex-encode and return
todo!()
}

View File

@@ -0,0 +1,63 @@
use anyhow::Result;
/// Post-wipe server health verification.
///
/// Called by the wipe engine after a wipe completes to verify the server
/// came back up correctly. Checks are configurable via PostWipeConfig
/// and failures can trigger rollback or retry logic.
pub struct HealthChecker {
// TODO: Add fields:
// - db: sqlx::PgPool
// - max_retries: u32
// - check_timeout: std::time::Duration
}
impl HealthChecker {
/// Run all configured health checks against the server.
///
/// Returns Ok(true) if all checks pass, Ok(false) if checks fail
/// after exhausting retries. Returns Err only on unexpected errors.
pub async fn verify_server_health(
&self,
_server_id: &str,
_license_id: uuid::Uuid,
) -> Result<bool> {
// TODO: Resolve PanelAdapter for this server
// TODO: Wait for server to report is_running=true
// TODO: Run each configured check (map, plugins, slots)
// TODO: Retry on failure up to max_retries with backoff
// TODO: Return aggregate pass/fail
todo!()
}
/// Verify the correct map is loaded on the server.
///
/// Sends RCON command to query the current level and compares
/// against the expected map from the wipe profile.
pub async fn check_map(&self, _server_id: &str, _expected_map: &str) -> Result<bool> {
// TODO: Send "status" or "serverinfo" via RCON
// TODO: Parse response for current level/map name
// TODO: Compare against expected_map
todo!()
}
/// Verify all required plugins are loaded and responding.
pub async fn check_plugins(
&self,
_server_id: &str,
_expected_plugins: &[String],
) -> Result<bool> {
// TODO: Send "plugins" or "oxide.plugins" via RCON
// TODO: Parse response for loaded plugin list
// TODO: Check all expected_plugins are present
todo!()
}
/// Verify the server is accepting player connections.
pub async fn check_player_slots(&self, _server_id: &str) -> Result<bool> {
// TODO: Query server status via PanelAdapter or RCON
// TODO: Verify max_players > 0 and server is connectable
// TODO: Optionally perform A2S query against game port
todo!()
}
}

View File

@@ -0,0 +1,67 @@
use anyhow::Result;
use uuid::Uuid;
use crate::models::license::License;
/// License validation and lifecycle management.
///
/// Handles license key validation, activation (binding a key to a server),
/// periodic check-ins from the Rust plugin, and license lookups.
/// All license state is stored in PostgreSQL.
pub struct LicenseService {
// TODO: Add fields:
// - db: sqlx::PgPool
}
impl LicenseService {
/// Validate a license key and return its current status.
///
/// Checks: key exists, not expired, not revoked, modules enabled.
pub async fn validate_license(&self, _license_key: &str) -> Result<Option<License>> {
// TODO: Query license by key from DB
// TODO: Check status is 'active'
// TODO: Check expires_at is in the future (if set)
// TODO: Return Some(license) if valid, None if not found
todo!()
}
/// Activate a license key: bind it to a server name and subdomain.
///
/// Called during initial setup when a user first registers.
pub async fn activate_license(
&self,
_license_key: &str,
_server_name: &str,
_subdomain: &str,
) -> Result<License> {
// TODO: Validate license key exists and is in 'pending' status
// TODO: Check subdomain availability
// TODO: Update license record: status='active', server_name, subdomain
// TODO: Create default roles for this license
// TODO: Return updated license
todo!()
}
/// Process a check-in from the Rust server plugin.
///
/// Updates last-seen timestamp and returns current license state
/// including enabled modules and NATS connection token.
pub async fn check_in(
&self,
_license_key: &str,
_plugin_version: &str,
) -> Result<crate::models::license::LicenseCheckInResponse> {
// TODO: Validate license
// TODO: Update plugin_last_seen timestamp on server_connections
// TODO: Generate or refresh NATS auth token for this license
// TODO: Return LicenseCheckInResponse with modules and token
todo!()
}
/// Look up a license by its UUID.
pub async fn get_license_by_key(&self, _license_id: Uuid) -> Result<Option<License>> {
// TODO: Query license by ID from DB
// TODO: Return if found
todo!()
}
}

View File

@@ -0,0 +1,65 @@
use anyhow::Result;
use uuid::Uuid;
/// Map upload, storage, and rotation management.
///
/// Handles custom map files (.map) for Rust servers. Maps are stored
/// in object storage with metadata in the database. Supports rotation
/// strategies where different maps are used on successive wipes.
pub struct MapManager {
// TODO: Add fields:
// - db: sqlx::PgPool
// - storage_base_path: String (or S3 client for cloud storage)
}
impl MapManager {
/// Upload a new map file to storage and register it in the database.
pub async fn upload_map(
&self,
_license_id: Uuid,
_display_name: &str,
_filename: &str,
_data: &[u8],
) -> Result<Uuid> {
// TODO: Compute checksum of map data
// TODO: Write file to storage (local volume or S3)
// TODO: Insert map_entry record in DB
// TODO: Return the new map ID
todo!()
}
/// Delete a map from storage and the database.
pub async fn delete_map(&self, _map_id: Uuid) -> Result<()> {
// TODO: Load map_entry from DB
// TODO: Delete file from storage
// TODO: Delete DB record
// TODO: Update any wipe profiles referencing this map
todo!()
}
/// Get the current map rotation for a license (ordered list of map IDs).
pub async fn get_rotation(&self, _license_id: Uuid) -> Result<Vec<Uuid>> {
// TODO: Query map rotation config from DB
// TODO: Return ordered list of map IDs in rotation
todo!()
}
/// Advance the rotation to the next map in sequence.
/// Returns the map ID that should be used for the next wipe.
pub async fn advance_rotation(&self, _license_id: Uuid) -> Result<Uuid> {
// TODO: Get current rotation position from DB
// TODO: Advance to next map (wrap around to first)
// TODO: Update rotation position in DB
// TODO: Return the next map ID
todo!()
}
/// Generate a time-limited signed URL for downloading a map file.
/// Used by the companion agent or panel adapter to fetch map files.
pub async fn generate_signed_url(&self, _map_id: Uuid) -> Result<String> {
// TODO: Load map_entry from DB
// TODO: Generate signed URL with expiration (e.g., 15 minutes)
// TODO: Return the URL
todo!()
}
}

View File

@@ -0,0 +1,81 @@
use anyhow::Result;
// -- JetStream stream name constants --
/// Stream for wipe lifecycle events (scheduled, started, completed, failed, rolled_back).
pub const STREAM_WIPE_EVENTS: &str = "CORROSION_WIPE";
/// Stream for server telemetry (stats, status changes, crash events).
pub const STREAM_SERVER_TELEMETRY: &str = "CORROSION_TELEMETRY";
/// Stream for agent commands and responses (start, stop, file ops).
pub const STREAM_AGENT_COMMANDS: &str = "CORROSION_AGENT";
/// Stream for Steam update detection events.
pub const STREAM_STEAM_UPDATES: &str = "CORROSION_STEAM";
/// Stream for notification delivery (Discord, Pushbullet, email).
pub const STREAM_NOTIFICATIONS: &str = "CORROSION_NOTIFY";
/// Stream for license lifecycle events (activated, expired, check-in).
pub const STREAM_LICENSE_EVENTS: &str = "CORROSION_LICENSE";
/// NATS JetStream pub/sub bridge.
///
/// Centralizes all NATS interactions: stream/consumer setup, publishing
/// events, and subscribing to subjects. All services go through the
/// bridge rather than holding their own NATS client references, ensuring
/// consistent subject naming and stream configuration.
pub struct NatsBridge {
pub client: async_nats::Client,
}
impl NatsBridge {
pub fn new(client: async_nats::Client) -> Self {
Self { client }
}
/// Publish a message to a NATS subject.
pub async fn publish(&self, _subject: &str, _payload: &[u8]) -> Result<()> {
// TODO: Publish via self.client.publish(subject, payload)
// TODO: Optionally publish to JetStream if subject belongs to a stream
todo!()
}
/// Subscribe to a NATS subject and return a message stream.
pub async fn subscribe(
&self,
_subject: &str,
) -> Result<async_nats::Subscriber> {
// TODO: Subscribe via self.client.subscribe(subject)
// TODO: For durable consumers, use JetStream consumer API
todo!()
}
/// Initialize all JetStream streams and consumers.
///
/// Called once at application startup. Creates streams if they don't
/// exist, updates configuration if streams already exist.
pub async fn setup_streams(&self) -> Result<()> {
// TODO: Get JetStream context from client
// TODO: Create/update CORROSION_WIPE stream
// subjects: ["corrosion.*.wipe.>"]
// retention: WorkQueue, max_age: 7 days
// TODO: Create/update CORROSION_TELEMETRY stream
// subjects: ["corrosion.*.telemetry.>"]
// retention: Limits, max_age: 24 hours
// TODO: Create/update CORROSION_AGENT stream
// subjects: ["corrosion.*.agent.>"]
// retention: WorkQueue, max_age: 1 hour
// TODO: Create/update CORROSION_STEAM stream
// subjects: ["corrosion.steam.>"]
// retention: Limits, max_age: 30 days
// TODO: Create/update CORROSION_NOTIFY stream
// subjects: ["corrosion.*.notify.>"]
// retention: WorkQueue, max_age: 1 day
// TODO: Create/update CORROSION_LICENSE stream
// subjects: ["corrosion.license.>"]
// retention: Limits, max_age: 90 days
todo!()
}
}

View File

@@ -0,0 +1,87 @@
use anyhow::Result;
use async_trait::async_trait;
use super::panel_adapter::{DiscoveredServer, FileEntry, PanelAdapter, ServerStatus};
/// Pterodactyl panel adapter.
///
/// Communicates with Pterodactyl (or Pelican) panels via their REST API.
/// Uses Application API keys for server management and Client API keys
/// for console/file operations. Implements the unified PanelAdapter trait.
pub struct PterodactylAdapter {
pub api_endpoint: String,
pub api_key: String,
}
impl PterodactylAdapter {
pub fn new(api_endpoint: String, api_key: String) -> Self {
Self {
api_endpoint,
api_key,
}
}
}
#[async_trait]
impl PanelAdapter for PterodactylAdapter {
async fn test_connection(&self) -> Result<bool> {
// TODO: GET /api/application/servers to verify API key validity
todo!()
}
async fn discover_servers(&self) -> Result<Vec<DiscoveredServer>> {
// TODO: Paginate /api/application/servers, filter for Rust egg, map to DiscoveredServer
todo!()
}
async fn get_server_status(&self, _server_id: &str) -> Result<ServerStatus> {
// TODO: GET /api/client/servers/{id}/resources for live stats
todo!()
}
async fn start_server(&self, _server_id: &str) -> Result<()> {
// TODO: POST /api/client/servers/{id}/power with signal=start
todo!()
}
async fn stop_server(&self, _server_id: &str) -> Result<()> {
// TODO: POST /api/client/servers/{id}/power with signal=stop
todo!()
}
async fn restart_server(&self, _server_id: &str) -> Result<()> {
// TODO: POST /api/client/servers/{id}/power with signal=restart
todo!()
}
async fn send_command(&self, _server_id: &str, _command: &str) -> Result<String> {
// TODO: POST /api/client/servers/{id}/command
todo!()
}
async fn get_file(&self, _server_id: &str, _path: &str) -> Result<Vec<u8>> {
// TODO: GET /api/client/servers/{id}/files/contents?file={path}
todo!()
}
async fn put_file(&self, _server_id: &str, _path: &str, _data: &[u8]) -> Result<()> {
// TODO: POST /api/client/servers/{id}/files/write?file={path}
todo!()
}
async fn delete_file(&self, _server_id: &str, _path: &str) -> Result<()> {
// TODO: POST /api/client/servers/{id}/files/delete with body
todo!()
}
async fn list_files(&self, _server_id: &str, _path: &str) -> Result<Vec<FileEntry>> {
// TODO: GET /api/client/servers/{id}/files/list?directory={path}
todo!()
}
async fn trigger_steam_update(&self, _server_id: &str) -> Result<()> {
// TODO: Pterodactyl doesn't have native SteamCMD trigger — send console command
// or use startup command reinstall via /api/client/servers/{id}/settings/reinstall
todo!()
}
}

View File

@@ -0,0 +1,30 @@
use anyhow::Result;
/// Pushbullet notification service.
///
/// Sends push notifications to server administrators via the Pushbullet
/// API. Used as a secondary notification channel alongside Discord for
/// critical alerts that need to reach admins on their mobile devices.
pub struct PushbulletNotifier {
// TODO: Add fields:
// - api_key: String
// - default_device_iden: Option<String> (target specific device, or all)
}
impl PushbulletNotifier {
/// Send a text notification (note type push).
pub async fn send_notification(&self, _title: &str, _body: &str) -> Result<()> {
// TODO: POST to https://api.pushbullet.com/v2/pushes
// TODO: Body: { type: "note", title, body }
// TODO: Auth: Access-Token header with api_key
todo!()
}
/// Send a link notification with a clickable URL.
pub async fn send_link(&self, _title: &str, _body: &str, _url: &str) -> Result<()> {
// TODO: POST to https://api.pushbullet.com/v2/pushes
// TODO: Body: { type: "link", title, body, url }
// TODO: Auth: Access-Token header with api_key
todo!()
}
}

View File

@@ -0,0 +1,56 @@
use anyhow::Result;
use chrono::{DateTime, Utc};
use uuid::Uuid;
/// Cron-based wipe schedule management.
///
/// Manages wipe schedules backed by `tokio-cron-scheduler`. Each schedule
/// maps a cron expression + timezone to a wipe profile. When a schedule
/// fires, it creates a wipe_history record and dispatches execution to
/// the WipeEngine.
pub struct SchedulerService {
// TODO: Add fields:
// - db: sqlx::PgPool
// - nats: async_nats::Client
// - scheduler: tokio_cron_scheduler::JobScheduler
}
impl SchedulerService {
/// Start the scheduler, loading all active schedules from the database.
pub async fn start(&self) -> Result<()> {
// TODO: Query all active wipe_schedules from DB
// TODO: Register each as a cron job in tokio-cron-scheduler
// TODO: Start the scheduler event loop
todo!()
}
/// Register a wipe schedule as a cron job.
pub async fn register_wipe_schedule(&self, _schedule_id: Uuid) -> Result<()> {
// TODO: Load WipeSchedule from DB
// TODO: Parse cron_expression with timezone
// TODO: Create tokio-cron-scheduler job that:
// 1. Creates a wipe_history record
// 2. Publishes wipe execution event to NATS
// TODO: Store job handle for later removal
todo!()
}
/// Remove a schedule from the running scheduler.
pub async fn remove_schedule(&self, _schedule_id: Uuid) -> Result<()> {
// TODO: Look up job handle by schedule_id
// TODO: Remove from tokio-cron-scheduler
todo!()
}
/// Calculate the next N scheduled run times for a given schedule.
pub async fn get_next_runs(
&self,
_schedule_id: Uuid,
_count: usize,
) -> Result<Vec<DateTime<Utc>>> {
// TODO: Load cron_expression and timezone from DB
// TODO: Iterate cron expression to compute next N fire times
// TODO: Convert to UTC and return
todo!()
}
}

View File

@@ -0,0 +1,53 @@
use anyhow::Result;
/// Steam App ID for the Rust Dedicated Server.
pub const RUST_SERVER_APP_ID: u32 = 258550;
/// Steam build ID polling service.
///
/// Periodically checks the Steam Web API for new Rust Dedicated Server
/// builds. When a new build is detected (build ID changes), it publishes
/// an event to NATS so that servers configured for auto-update-on-force-wipe
/// can be notified and trigger their wipe schedules.
pub struct SteamUpdateWatcher {
// TODO: Add fields:
// - nats: async_nats::Client
// - db: sqlx::PgPool
// - poll_interval: std::time::Duration
// - last_known_build_id: Option<String>
}
impl SteamUpdateWatcher {
/// Start the polling loop. Runs indefinitely, checking for updates
/// at the configured interval.
pub async fn start_polling(&self) -> Result<()> {
// TODO: Loop on poll_interval
// TODO: Call check_for_update each iteration
// TODO: If new build detected, call handle_update_detected
todo!()
}
/// Check Steam Web API for the current build ID of the Rust server app.
///
/// Returns the build ID string if an update is detected (differs from
/// last known), or None if unchanged.
pub async fn check_for_update(&self) -> Result<Option<String>> {
// TODO: GET https://api.steampowered.com/ISteamApps/UpToDateCheck/v1/
// ?appid={RUST_SERVER_APP_ID}&version=0
// TODO: Parse response for build ID
// TODO: Compare against last_known_build_id
// TODO: Return Some(new_id) if changed, None if same
todo!()
}
/// Handle a detected Steam update: persist the new build ID,
/// publish NATS event for downstream consumers.
pub async fn handle_update_detected(&self, _new_build_id: &str) -> Result<()> {
// TODO: Update last_known_build_id
// TODO: Persist to DB for crash recovery
// TODO: Publish to NATS subject corrosion.steam.update_detected
// with payload { app_id, old_build_id, new_build_id, detected_at }
// TODO: Log the detection
todo!()
}
}

View File

@@ -0,0 +1,88 @@
use anyhow::Result;
use uuid::Uuid;
/// Core wipe orchestration engine.
///
/// Coordinates the full wipe lifecycle: pre-wipe steps (backups, warnings,
/// player kicks), the wipe itself (map swap, plugin data cleanup, seed
/// rotation), and post-wipe verification (health checks, rollback on
/// failure). Works through the PanelAdapter trait so it is panel-agnostic.
///
/// All wipe executions are logged to the wipe_history table with full
/// execution traces for audit and debugging.
pub struct WipeEngine {
pub db: sqlx::PgPool,
pub nats: async_nats::Client,
}
impl WipeEngine {
pub fn new(db: sqlx::PgPool, nats: async_nats::Client) -> Self {
Self { db, nats }
}
/// Execute a full wipe sequence for the given wipe history record.
///
/// Orchestrates: pre-wipe -> wipe actions -> post-wipe verification.
/// On failure at any stage, triggers rollback if configured.
pub async fn execute_wipe(&self, _wipe_history_id: Uuid) -> Result<()> {
// TODO: Load wipe history + profile + schedule from DB
// TODO: Resolve the PanelAdapter for this server
// TODO: Run pre-wipe -> wipe -> post-wipe pipeline
// TODO: Update wipe_history status throughout
// TODO: Publish NATS events for real-time UI updates
todo!()
}
/// Execute pre-wipe steps: countdown warnings, player kicks, backups,
/// Discord/Pushbullet announcements, custom commands.
pub async fn execute_pre_wipe(&self, _wipe_history_id: Uuid) -> Result<()> {
// TODO: Load PreWipeConfig from wipe profile
// TODO: Send countdown warnings via RCON at configured intervals
// TODO: Create backup if backup_before_wipe is true
// TODO: Kick players with configured message
// TODO: Run final save command
// TODO: Send Discord/Pushbullet pre-announce notifications
// TODO: Execute custom_commands_before
todo!()
}
/// Execute the actual wipe actions: stop server, delete/swap map,
/// wipe plugin data, update seed, restart server.
pub async fn execute_wipe_actions(&self, _wipe_history_id: Uuid) -> Result<()> {
// TODO: Stop the server via PanelAdapter
// TODO: Delete map/save files based on wipe type
// TODO: Wipe plugin data files based on plugin wipe flags
// TODO: Update seed if configured for rotation
// TODO: Upload new custom map if map rotation is active
// TODO: Update server.cfg with new settings
// TODO: Trigger Steam update if force wipe
// TODO: Start the server
todo!()
}
/// Execute post-wipe verification: health checks on server startup,
/// map correctness, plugin loading, player slot availability.
pub async fn execute_post_wipe_verification(&self, _wipe_history_id: Uuid) -> Result<()> {
// TODO: Wait for server to report running via PanelAdapter
// TODO: Verify correct map loaded (check level name via RCON)
// TODO: Verify all required plugins loaded
// TODO: Verify player slots are open
// TODO: Retry up to max_restart_attempts on failure
// TODO: Send Discord/Pushbullet post-announce notifications
// TODO: Execute post_wipe_commands
todo!()
}
/// Execute rollback: restore pre-wipe backup, restart server,
/// notify operators of failure.
pub async fn execute_rollback(&self, _wipe_history_id: Uuid) -> Result<()> {
// TODO: Load backup_reference from wipe_history
// TODO: Stop the server
// TODO: Restore backup via BackupManager
// TODO: Start the server
// TODO: Verify server health post-rollback
// TODO: Update wipe_history status to 'rolled_back'
// TODO: Send failure notifications via Discord/Pushbullet
todo!()
}
}