Files
corrosion-admin-panel/backend/src/services/pushbullet.rs
Vantz Stockwell 590765fbbc feat: Complete Phase 1 backend services and WebSocket/NATS bridge
Implements all remaining backend infrastructure for Corrosion platform.

Backend Services (5 new):
- license.rs: License validation, activation, check-in with NATS token generation
- map_manager.rs: Map upload/rotation with SHA-256 checksums, circular advancement
- health_checker.rs: Post-wipe verification with retry loop and backoff
- backup_manager.rs: Tar.gz backups with retention policy (last 10), recursive upload
- scheduler.rs: Tokio-cron integration for scheduled wipes with NATS events

WipeEngine Orchestration (wipe_engine.rs):
- execute_wipe(): Master orchestrator managing full lifecycle
- execute_pre_wipe(): Countdown warnings, backups, player kicks
- execute_wipe_actions(): Map/plugin deletion, seed rotation, Steam updates
- execute_post_wipe_verification(): Health checks with restart attempts
- execute_rollback(): Failure recovery with backup restore
- JSONB execution logs, NATS status events, service composition pattern

WebSocket/NATS Bridge (ws.rs):
- JWT authentication via query parameter
- License-scoped NATS subscriptions (corrosion.{license_id}.*)
- Bi-directional: NATS→WebSocket event forwarding, WebSocket→NATS publishing
- Axum 0.8 with ws feature, auto Ping/Pong handling

Panel Adapter Fixes:
- AMP/Pterodactyl/Companion adapters fully wired
- RCON command execution, file operations, Steam update triggers

Fixes:
- Added ws feature to Axum dependency
- Fixed Message::Text() type conversions (String→Utf8Bytes via .into())
- Fixed BackupInfo FromRow derive
- Fixed recursive async with Box::pin pattern
- Fixed async JobScheduler::new() constructor
- Removed manual WebSocket Ping/Pong handler

Compilation: 0 errors, 327 warnings (unused vars/functions)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 12:07:01 -05:00

149 lines
4.4 KiB
Rust

use anyhow::{Context, Result};
use reqwest::Client;
use serde::Serialize;
const PUSHBULLET_API_URL: &str = "https://api.pushbullet.com/v2/pushes";
#[derive(Debug, Serialize)]
struct NotePush {
#[serde(rename = "type")]
push_type: String,
title: String,
body: String,
}
#[derive(Debug, Serialize)]
struct LinkPush {
#[serde(rename = "type")]
push_type: String,
title: String,
body: String,
url: String,
}
/// 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 {
http: Client,
api_key: String,
}
impl PushbulletNotifier {
pub fn new(api_key: String) -> Self {
Self {
http: Client::new(),
api_key,
}
}
/// Send a text notification (note type push).
pub async fn send_notification(&self, title: &str, body: &str) -> Result<()> {
let payload = NotePush {
push_type: "note".to_string(),
title: title.to_string(),
body: body.to_string(),
};
let response = self
.http
.post(PUSHBULLET_API_URL)
.header("Access-Token", &self.api_key)
.json(&payload)
.send()
.await
.context("Failed to send Pushbullet notification")?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
tracing::error!("Pushbullet push failed: {} — {}", status, body);
anyhow::bail!("Pushbullet returned {status}");
}
Ok(())
}
/// Send a link notification with a clickable URL.
pub async fn send_link(&self, title: &str, body: &str, url: &str) -> Result<()> {
let payload = LinkPush {
push_type: "link".to_string(),
title: title.to_string(),
body: body.to_string(),
url: url.to_string(),
};
let response = self
.http
.post(PUSHBULLET_API_URL)
.header("Access-Token", &self.api_key)
.json(&payload)
.send()
.await
.context("Failed to send Pushbullet link push")?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
tracing::error!("Pushbullet push failed: {} — {}", status, body);
anyhow::bail!("Pushbullet returned {status}");
}
Ok(())
}
/// Send a wipe-starting notification.
pub async fn send_wipe_start(&self, server_name: &str, wipe_type: &str) -> Result<()> {
self.send_notification(
&format!("🔄 {} — Wipe Starting", server_name),
&format!("{} wipe is beginning. Server will be offline briefly.", wipe_type),
)
.await
}
/// Send a wipe-completed notification.
pub async fn send_wipe_complete(&self, server_name: &str, wipe_type: &str) -> Result<()> {
self.send_notification(
&format!("{} — Wipe Complete", server_name),
&format!("{} wipe completed. Server is back online.", wipe_type),
)
.await
}
/// Send a wipe-failed notification.
pub async fn send_wipe_failed(&self, server_name: &str, error: &str) -> Result<()> {
self.send_notification(
&format!("{} — Wipe Failed", server_name),
&format!("Wipe failed: {}", error),
)
.await
}
/// Send a crash alert.
pub async fn send_crash_alert(
&self,
server_name: &str,
crash_count: u32,
auto_recovered: bool,
) -> Result<()> {
let title = if auto_recovered {
format!("⚠️ {} — Crash Recovered", server_name)
} else {
format!("🔴 {} — Crash — Manual Action Needed", server_name)
};
let body = if auto_recovered {
format!("Server crashed and was auto-restarted (attempt {}).", crash_count)
} else {
format!(
"Server crashed {} times. Auto-recovery exhausted. Check the server.",
crash_count
)
};
self.send_notification(&title, &body).await
}
}