mirror of
https://github.com/YGGverse/hlstate-rs.git
synced 2026-03-31 17:15:37 +00:00
implement game event subscriptions via rss
This commit is contained in:
parent
31c31d2106
commit
28c64036ce
5 changed files with 155 additions and 20 deletions
69
crates/httpd/src/feed.rs
Normal file
69
crates/httpd/src/feed.rs
Normal file
|
|
@ -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("<?xml version=\"1.0\" encoding=\"UTF-8\"?><rss version=\"2.0\"><channel>");
|
||||||
|
|
||||||
|
b.push_str("<pubDate>");
|
||||||
|
b.push_str(&t);
|
||||||
|
b.push_str("</pubDate>");
|
||||||
|
|
||||||
|
b.push_str("<lastBuildDate>");
|
||||||
|
b.push_str(&t);
|
||||||
|
b.push_str("</lastBuildDate>");
|
||||||
|
|
||||||
|
b.push_str("<title>");
|
||||||
|
b.push_str(title);
|
||||||
|
b.push_str("</title>");
|
||||||
|
|
||||||
|
if let Some(d) = description {
|
||||||
|
b.push_str("<description>");
|
||||||
|
b.push_str(d);
|
||||||
|
b.push_str("</description>")
|
||||||
|
}
|
||||||
|
|
||||||
|
Self(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appends `item` to the feed `channel`
|
||||||
|
pub fn push(
|
||||||
|
&mut self,
|
||||||
|
time: &DateTime<Utc>,
|
||||||
|
address: &SocketAddr,
|
||||||
|
host: &String,
|
||||||
|
map: &String,
|
||||||
|
online: u32,
|
||||||
|
) {
|
||||||
|
let a = address.to_string(); // allocate once
|
||||||
|
|
||||||
|
self.0.push_str(&format!(
|
||||||
|
"<item><guid>{}</guid><title>{host}</title><link>udp://{a}</link>",
|
||||||
|
time.timestamp() // must be unique as the event
|
||||||
|
));
|
||||||
|
|
||||||
|
self.0.push_str("<description>");
|
||||||
|
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("</description>");
|
||||||
|
|
||||||
|
self.0.push_str("<pubDate>");
|
||||||
|
self.0.push_str(&time.to_rfc2822());
|
||||||
|
self.0.push_str("</pubDate>");
|
||||||
|
|
||||||
|
self.0.push_str("</item>")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write final bytes
|
||||||
|
pub fn commit(mut self) -> String {
|
||||||
|
self.0.push_str("</channel></rss>");
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,23 +3,31 @@ extern crate rocket;
|
||||||
|
|
||||||
mod argument;
|
mod argument;
|
||||||
mod config;
|
mod config;
|
||||||
|
mod feed;
|
||||||
mod global;
|
mod global;
|
||||||
mod meta;
|
mod meta;
|
||||||
mod scrape;
|
mod scrape;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use feed::Feed;
|
||||||
use global::Global;
|
use global::Global;
|
||||||
use meta::Meta;
|
use meta::Meta;
|
||||||
use rocket::{
|
use rocket::{
|
||||||
State,
|
State,
|
||||||
|
form::FromForm,
|
||||||
http::Status,
|
http::Status,
|
||||||
|
response::content::RawXml,
|
||||||
tokio::{spawn, sync::RwLock, time::sleep},
|
tokio::{spawn, sync::RwLock, time::sleep},
|
||||||
};
|
};
|
||||||
use rocket_dyn_templates::{Template, context};
|
use rocket_dyn_templates::{Template, context};
|
||||||
use std::{collections::HashSet, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
|
use std::{collections::HashSet, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
|
||||||
|
|
||||||
struct Online {
|
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<scrape::Result>,
|
||||||
|
/// Snap time
|
||||||
update: DateTime<Utc>,
|
update: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
type Snap = Arc<RwLock<Option<Online>>>;
|
type Snap = Arc<RwLock<Option<Online>>>;
|
||||||
|
|
@ -37,12 +45,52 @@ async fn index(
|
||||||
masters: &global.masters,
|
masters: &global.masters,
|
||||||
title: &meta.title,
|
title: &meta.title,
|
||||||
version: &meta.version,
|
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())
|
updated: snap.as_ref().map(|s|s.update.to_rfc2822())
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(FromForm, Debug)]
|
||||||
|
struct RssParams {
|
||||||
|
online: Option<u32>,
|
||||||
|
servers: Option<Vec<SocketAddr>>,
|
||||||
|
}
|
||||||
|
#[get("/rss?<params..>")]
|
||||||
|
async fn rss(
|
||||||
|
params: RssParams,
|
||||||
|
meta: &State<Meta>,
|
||||||
|
online: &State<Snap>,
|
||||||
|
) -> Result<RawXml<String>, 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]
|
#[launch]
|
||||||
async fn rocket() -> _ {
|
async fn rocket() -> _ {
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
@ -60,10 +108,11 @@ async fn rocket() -> _ {
|
||||||
Ok(s) => match str::from_utf8(&s.stdout) {
|
Ok(s) => match str::from_utf8(&s.stdout) {
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
if s.status.success() {
|
if s.status.success() {
|
||||||
*online.write().await =
|
let mut state = online.write().await;
|
||||||
match rocket::serde::json::serde_json::from_str(r) {
|
*state = match rocket::serde::json::serde_json::from_str(r) {
|
||||||
Ok(scrape) => Some(Online {
|
Ok(current) => Some(Online {
|
||||||
scrape,
|
current,
|
||||||
|
last: state.as_ref().map(|last| last.current.clone()),
|
||||||
update: Utc::now(),
|
update: Utc::now(),
|
||||||
}),
|
}),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
@ -103,7 +152,7 @@ async fn rocket() -> _ {
|
||||||
title: config.title,
|
title: config.title,
|
||||||
version: env!("CARGO_PKG_VERSION").into(),
|
version: env!("CARGO_PKG_VERSION").into(),
|
||||||
})
|
})
|
||||||
.mount("/", routes![index])
|
.mount("/", routes![index, rss])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get servers online using `bin` from given `masters`
|
/// Get servers online using `bin` from given `masters`
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use rocket::serde::{Deserialize, Serialize};
|
use rocket::serde::{Deserialize, Serialize};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize, Clone)]
|
||||||
#[serde(crate = "rocket::serde")]
|
#[serde(crate = "rocket::serde")]
|
||||||
pub struct Result {
|
pub struct Result {
|
||||||
pub protocol: Vec<i32>,
|
pub protocol: Vec<i32>,
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,11 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h2>Game</h2>
|
<h2>Game</h2>
|
||||||
{% if servers %}
|
{% if servers %}
|
||||||
<form name="subscribe" method="get" action="/">
|
<form name="rss" method="get" action="/rss">
|
||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{# @TODO subscribe<th></th>#}
|
<th></th>
|
||||||
<th>Address</th>
|
<th>Address</th>
|
||||||
<th>Host</th>
|
<th>Host</th>
|
||||||
<th>Ping</th>
|
<th>Ping</th>
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
{% for server in servers %}
|
{% for server in servers %}
|
||||||
<tr>
|
<tr>
|
||||||
{# @TODO subscribe<td><input type="checkbox" name="server[]" value="{{ server.address }}" /></td>#}
|
<td><input type="checkbox" name="server[]" value="{{ server.address }}" /></td>
|
||||||
<td>{{ server.address }}</td>
|
<td>{{ server.address }}</td>
|
||||||
<td>{{ server.host }}</td>
|
<td>{{ server.host }}</td>
|
||||||
<td>{{ server.ping }}</td>
|
<td>{{ server.ping }}</td>
|
||||||
|
|
@ -45,9 +45,12 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{# @TODO subscribe
|
<fieldset>
|
||||||
<input type="number" name="online" value="1" />
|
<label>
|
||||||
<input type="submit" value="Subscribe" />#}
|
min. online: <input type="number" name="online" min="0" value="1" />
|
||||||
|
</label>
|
||||||
|
<input type="submit" value="RSS" />
|
||||||
|
</fieldset>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,20 @@
|
||||||
max-width: var(--container-max-width);
|
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 {
|
footer {
|
||||||
display: block;
|
display: block;
|
||||||
margin: 16px auto;
|
margin: 16px auto;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue