#![forbid(unsafe_code)] use std::sync::{Mutex, Arc}; use bytes::Bytes; use warp::{Filter, reject::{Rejection, Reject}, reply::{Reply, with_header, with_status}}; use chrono::{Local, DateTime}; #[derive(Debug, Clone, Copy)] struct State { pub last_heartbeat: DateTime } impl Default for State { fn default() -> Self { Self { last_heartbeat: DateTime::UNIX_EPOCH.into() } } } #[derive(Debug, Clone)] enum HttpReject { String(String) } impl Reject for HttpReject {} async fn beat_f(state: Arc>, body: Bytes) -> Result, Rejection> { if body != include_bytes!("secret.key").to_vec() { return Ok( Box::new( with_status("Invalid key", warp::http::StatusCode::FORBIDDEN) ) ) } let mut state = state.lock().map_err(|x| HttpReject::String(x.to_string()))?; state.last_heartbeat = Local::now().into(); Ok( Box::new("OK") ) } async fn gif_f(state: Arc>) -> Result, Rejection> { let state = state.lock().map_err(|x| HttpReject::String(x.to_string()))?; let online_gif = include_bytes!("me_online.gif"); let offline_gif = include_bytes!("me_offline.gif"); let duration = (Local::now() - state.last_heartbeat).num_minutes(); Ok( Box::new( with_header( with_header( { if duration < 2 { online_gif.to_vec() } else { offline_gif.to_vec() } }, "Content-Type", "image/gif" ), "X-Duration", duration.to_string() ) ) ) } fn beat(state: Arc>) -> impl Filter + Clone { warp::path!("beat") .and(warp::path::end()) .and(warp::post()) .map(move || state.clone()) .and(warp::body::bytes()) .and_then(beat_f) } fn gif(state: Arc>) -> impl Filter + Clone { warp::path!("gif") .and(warp::path::end()) .and(warp::get()) .map(move || state.clone()) .and_then(gif_f) } #[tokio::main] async fn main() { let state = Arc::new(Mutex::new(State::default())); warp::serve( beat(state.clone()) .or(gif(state)) ).run(([0, 0, 0, 0], 80)).await; }