veloren_server_cli/web/
chat.rs1use axum::{
2 Json, Router,
3 extract::{ConnectInfo, Query, Request, State},
4 middleware::Next,
5 response::{IntoResponse, Response},
6 routing::get,
7};
8use chrono::DateTime;
9use hyper::StatusCode;
10use serde::{Deserialize, Deserializer};
11use server::chat::ChatCache;
12use std::{
13 collections::HashSet,
14 net::{IpAddr, SocketAddr},
15 str::FromStr,
16 sync::Arc,
17};
18use tokio::sync::Mutex;
19
20#[derive(Clone)]
22struct ChatToken {
23 secret_token: Option<String>,
24}
25
26#[derive(Clone, Default)]
27struct IpAddresses {
28 users: Arc<Mutex<HashSet<IpAddr>>>,
29}
30
31async fn validate_secret(
32 State(token): State<ChatToken>,
33 req: Request,
34 next: Next,
35) -> Result<Response, StatusCode> {
36 let secret_token = token.secret_token.ok_or(StatusCode::METHOD_NOT_ALLOWED)?;
38
39 pub const X_SECRET_TOKEN: &str = "X-Secret-Token";
40 let session_cookie = req
41 .headers()
42 .get(X_SECRET_TOKEN)
43 .ok_or(StatusCode::UNAUTHORIZED)?;
44
45 if session_cookie.as_bytes() != secret_token.as_bytes() {
46 return Err(StatusCode::UNAUTHORIZED);
47 }
48
49 Ok(next.run(req).await)
50}
51
52async fn log_users(
54 State(ip_addresses): State<IpAddresses>,
55 ConnectInfo(addr): ConnectInfo<SocketAddr>,
56 req: Request,
57 next: Next,
58) -> Result<Response, StatusCode> {
59 let mut ip_addresses = ip_addresses.users.lock().await;
60 let ip_addr = addr.ip();
61 if !ip_addresses.contains(&ip_addr) {
62 ip_addresses.insert(ip_addr);
63 let users_so_far = ip_addresses.len();
64 tracing::info!(?ip_addr, ?users_so_far, "Is accessing the /chat endpoint");
65 }
66 Ok(next.run(req).await)
67}
68
69pub fn router(cache: ChatCache, secret_token: Option<String>) -> Router {
70 let token = ChatToken { secret_token };
71 let ip_addrs = IpAddresses::default();
72 Router::new()
73 .route("/history", get(history))
74 .layer(axum::middleware::from_fn_with_state(ip_addrs, log_users))
75 .layer(axum::middleware::from_fn_with_state(token, validate_secret))
76 .with_state(cache)
77}
78
79#[derive(Debug, Deserialize)]
80struct Params {
81 #[serde(default, deserialize_with = "empty_string_as_none")]
82 from_time_exclusive_rfc3339: Option<String>,
84}
85
86fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
87where
88 D: Deserializer<'de>,
89 T: FromStr,
90 T::Err: core::fmt::Display,
91{
92 let opt = Option::<String>::deserialize(de)?;
93 match opt.as_deref() {
94 None | Some("") => Ok(None),
95 Some(s) => FromStr::from_str(s)
96 .map_err(serde::de::Error::custom)
97 .map(Some),
98 }
99}
100
101async fn history(
102 State(cache): State<ChatCache>,
103 Query(params): Query<Params>,
104) -> Result<impl IntoResponse, StatusCode> {
105 let from_time_exclusive = if let Some(rfc3339) = params.from_time_exclusive_rfc3339 {
107 Some(DateTime::parse_from_rfc3339(&rfc3339).map_err(|_| StatusCode::BAD_REQUEST)?)
108 } else {
109 None
110 };
111
112 let messages = cache.messages.lock().await;
113 let filtered: Vec<_> = match from_time_exclusive {
114 Some(from_time_exclusive) => messages
115 .iter()
116 .filter(|msg| msg.time > from_time_exclusive)
117 .cloned()
118 .collect(),
119 None => messages.iter().cloned().collect(),
120 };
121 Ok(Json(filtered))
122}