diff --git a/crates/httpd/src/feed.rs b/crates/httpd/src/feed.rs new file mode 100644 index 0000000..661da02 --- /dev/null +++ b/crates/httpd/src/feed.rs @@ -0,0 +1,69 @@ +use chrono::{DateTime, Utc}; +use std::net::SocketAddr; + +/// Export crawl results as the RSS +pub struct Feed(String); + +impl Feed { + pub fn new(title: &str, description: Option<&str>) -> Self { + let t = chrono::Utc::now().to_rfc2822(); + let mut b = String::new(); + + b.push_str(""); + + b.push_str(""); + b.push_str(&t); + b.push_str(""); + + b.push_str(""); + b.push_str(&t); + b.push_str(""); + + b.push_str(""); + b.push_str(title); + b.push_str(""); + + if let Some(d) = description { + b.push_str(""); + b.push_str(d); + b.push_str("") + } + + Self(b) + } + + /// Appends `item` to the feed `channel` + pub fn push( + &mut self, + time: &DateTime, + address: &SocketAddr, + host: &String, + map: &String, + online: u32, + ) { + let a = address.to_string(); // allocate once + + self.0.push_str(&format!( + "{}{host}udp://{a}", + time.timestamp() // must be unique as the event + )); + + self.0.push_str(""); + self.0.push_str(&format!("connect: {a}\n")); + self.0.push_str(&format!("map: {map}\n")); + self.0.push_str(&format!("online: {online}\n")); + self.0.push_str(""); + + self.0.push_str(""); + self.0.push_str(&time.to_rfc2822()); + self.0.push_str(""); + + self.0.push_str("") + } + + /// Write final bytes + pub fn commit(mut self) -> String { + self.0.push_str(""); + self.0 + } +} diff --git a/crates/httpd/src/main.rs b/crates/httpd/src/main.rs index c91306d..2ee44d5 100644 --- a/crates/httpd/src/main.rs +++ b/crates/httpd/src/main.rs @@ -3,23 +3,31 @@ extern crate rocket; mod argument; mod config; +mod feed; mod global; mod meta; mod scrape; use chrono::{DateTime, Utc}; +use feed::Feed; use global::Global; use meta::Meta; use rocket::{ State, + form::FromForm, http::Status, + response::content::RawXml, tokio::{spawn, sync::RwLock, time::sleep}, }; use rocket_dyn_templates::{Template, context}; use std::{collections::HashSet, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration}; struct Online { - scrape: scrape::Result, + /// Actual state container + current: scrape::Result, + /// Hold previous `current` state updated to compare and notify subscribers for online change + last: Option, + /// Snap time update: DateTime, } type Snap = Arc>>; @@ -37,12 +45,52 @@ async fn index( masters: &global.masters, title: &meta.title, version: &meta.version, - servers: snap.as_ref().map(|s|s.scrape.servers.clone()), + servers: snap.as_ref().map(|s|s.current.servers.clone()), updated: snap.as_ref().map(|s|s.update.to_rfc2822()) }, )) } +#[derive(FromForm, Debug)] +struct RssParams { + online: Option, + servers: Option>, +} +#[get("/rss?")] +async fn rss( + params: RssParams, + meta: &State, + online: &State, +) -> Result, Status> { + let mut f = Feed::new( + &meta.title, + None, // @TODO service description + ); + if let Some(state) = online.read().await.as_ref() { + for current in &state.current.servers { + if state.last.as_ref().is_none_or(|l| { + l.servers + .iter() + .any(|last| current.address == last.address && current.numcl > last.numcl) + }) && params.online.is_none_or(|online| current.numcl >= online) + && params + .servers + .as_ref() + .is_none_or(|servers| servers.iter().any(|address| address == ¤t.address)) + { + f.push( + &state.update, + ¤t.address, + ¤t.host, + ¤t.map, + current.numcl, + ) + } + } + } + Ok(RawXml(f.commit())) +} + #[launch] async fn rocket() -> _ { use clap::Parser; @@ -60,17 +108,18 @@ async fn rocket() -> _ { Ok(s) => match str::from_utf8(&s.stdout) { Ok(r) => { if s.status.success() { - *online.write().await = - match rocket::serde::json::serde_json::from_str(r) { - Ok(scrape) => Some(Online { - scrape, - update: Utc::now(), - }), - Err(e) => { - error!("Could not decode scrape response: `{e}`"); - None - } + let mut state = online.write().await; + *state = match rocket::serde::json::serde_json::from_str(r) { + Ok(current) => Some(Online { + current, + last: state.as_ref().map(|last| last.current.clone()), + update: Utc::now(), + }), + Err(e) => { + error!("Could not decode scrape response: `{e}`"); + None } + } } else { error!("Scrape request failed"); } @@ -103,7 +152,7 @@ async fn rocket() -> _ { title: config.title, version: env!("CARGO_PKG_VERSION").into(), }) - .mount("/", routes![index]) + .mount("/", routes![index, rss]) } /// Get servers online using `bin` from given `masters` diff --git a/crates/httpd/src/scrape.rs b/crates/httpd/src/scrape.rs index d762910..336cb43 100644 --- a/crates/httpd/src/scrape.rs +++ b/crates/httpd/src/scrape.rs @@ -1,7 +1,7 @@ use rocket::serde::{Deserialize, Serialize}; use std::net::SocketAddr; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] #[serde(crate = "rocket::serde")] pub struct Result { pub protocol: Vec, diff --git a/crates/httpd/templates/index.html.tera b/crates/httpd/templates/index.html.tera index 4ee9a4b..3c1f0e3 100644 --- a/crates/httpd/templates/index.html.tera +++ b/crates/httpd/templates/index.html.tera @@ -2,11 +2,11 @@ {% block content %}

Game

{% if servers %} -
+ - {# @TODO subscribe#} + @@ -26,7 +26,7 @@ {% for server in servers %} - {# @TODO subscribe#} + @@ -45,9 +45,12 @@ {% endfor %}
Address Host Ping
{{ server.address }} {{ server.host }} {{ server.ping }}
- {# @TODO subscribe - - #} +
+ + +
{% else %}
diff --git a/crates/httpd/templates/layout.html.tera b/crates/httpd/templates/layout.html.tera index 70034f0..26f3c43 100644 --- a/crates/httpd/templates/layout.html.tera +++ b/crates/httpd/templates/layout.html.tera @@ -94,6 +94,20 @@ max-width: var(--container-max-width); } + main > form > fieldset { + border: 0; + margin: 16px 0; + } + + main > form > fieldset input { + padding: 0 4px; + } + + main > form > fieldset input[type="number"] { + margin: 0 4px; + max-width: 36px; + } + footer { display: block; margin: 16px auto;