From 55bf371d04af45bdc1471670d8eca376aa308042 Mon Sep 17 00:00:00 2001 From: rascul Date: Sat, 28 May 2022 22:07:24 -0500 Subject: [PATCH] init --- src/action.rs | 61 ++++++++++++++++++++++++++++ src/bin/mudlet_replay.rs | 86 ++++++++++++++++++++++++++++++++++++++++ src/client/client.rs | 37 +++++++++++++++++ src/client/mod.rs | 5 +++ src/client/tintin.rs | 1 + src/form_data.rs | 36 +++++++++++++++++ src/line.rs | 27 +++++++++++++ src/main.rs | 51 ++++++++++++++++++++++++ src/options.rs | 27 +++++++++++++ src/routes/index.rs | 13 ++++++ src/routes/mod.rs | 2 + src/routes/submit.rs | 31 +++++++++++++++ src/wot_log.rs | 62 +++++++++++++++++++++++++++++ 13 files changed, 439 insertions(+) create mode 100644 src/action.rs create mode 100644 src/bin/mudlet_replay.rs create mode 100644 src/client/client.rs create mode 100644 src/client/mod.rs create mode 100644 src/client/tintin.rs create mode 100644 src/form_data.rs create mode 100644 src/line.rs create mode 100644 src/main.rs create mode 100644 src/options.rs create mode 100644 src/routes/index.rs create mode 100644 src/routes/mod.rs create mode 100644 src/routes/submit.rs create mode 100644 src/wot_log.rs diff --git a/src/action.rs b/src/action.rs new file mode 100644 index 0000000..a3177b6 --- /dev/null +++ b/src/action.rs @@ -0,0 +1,61 @@ +use lazy_static::lazy_static; +use regex::Regex; + +use crate::Client; + +// this lazy static stuff is so not to recompile the re every time +// unwrap is ok here because compiler will catch it if the string is bad +lazy_static! { + static ref RE: Regex = Regex::new(r"^#!(?P\w+)(?: (?P.*))?$").unwrap(); +} + +/// type of log action +#[derive(Debug)] +pub enum Action { + /// specify client type + Client(Client), + + /// insert a comment in the log + Comment(String), + + /// delay replay + /// duration is number of seconds or 1 if no seconds specified or if duration can't be parsed + Delay(u64), + + /// log line + /// duration is amount of time to sleep before printing the line + Line(String), + + /// insert a marker + Mark, + + /// No match + None, +} + +impl From<&str> for Action { + fn from(line: &str) -> Self { + if line.starts_with("#!") { + let caps = RE.captures(&line).unwrap(); + + let action = if let Some(action) = caps.name("action") { action.as_str() } else { "" }; + + let text = if let Some(text) = caps.name("text") { + text.as_str().to_owned() + } else { + String::new() + }; + + match action { + "client" => Self::Client(Client::from(&text)), + "delay" => Self::Delay(text.parse::().unwrap_or(1)), + "mark" => Self::Mark, + _ => Self::None, + } + } else if line.starts_with("##") { + Self::Comment(line.to_owned()) + } else { + Self::Line(String::new()) + } + } +} diff --git a/src/bin/mudlet_replay.rs b/src/bin/mudlet_replay.rs new file mode 100644 index 0000000..9440c9f --- /dev/null +++ b/src/bin/mudlet_replay.rs @@ -0,0 +1,86 @@ +use std::io; +use std::io::Read; +use std::thread::sleep; +use std::time::Duration; + +type Result = std::result::Result>; + +#[derive(Debug)] +struct Chunk { + pub delta: i32, + pub text: String, +} + +impl Chunk { + fn chunk32(chunk: &[u8; 8]) -> Result<(i32, i32)> { + let (dc, sc) = { + let (mut dc, mut sc): ([u8; 4], [u8; 4]) = ([0; 4], [0; 4]); + for i in 0..4 { + dc[i] = chunk[i]; + sc[i] = chunk[i + 4]; + } + (dc, sc) + }; + + let delta = i32::from_be_bytes(dc); + let size = i32::from_be_bytes(sc); + + if delta < 0 || size < 1 || size > 100000 { + return Err("delta source is not int32_t".into()); + } + + Ok((delta, size)) + } + + fn chunk64(dc: [u8; 8], sc: [u8; 4]) -> Result<(i32, i32)> { + let delta = i64::from_be_bytes(dc); + let size = i32::from_be_bytes(sc); + + if delta < 0 || delta > i32::MAX.into() || size < 1 || size > 100000 { + return Err("delta source is not int64_t".into()); + } + + let delta: i32 = delta.try_into()?; + + Ok((delta, size)) + } + + pub fn read_chunk() -> Result { + let mut stdin = io::stdin(); + + let mut chunk: [u8; 8] = [0; 8]; + stdin.read_exact(&mut chunk)?; + + let (delta, size) = { + if let Ok((delta, size)) = Self::chunk32(&chunk) { + (delta, size) + } else { + let mut next_chunk: [u8; 4] = [0; 4]; + stdin.read_exact(&mut next_chunk)?; + + if let Ok((delta, size)) = Self::chunk64(chunk, next_chunk) { + (delta, size) + } else { + return Err("broken".into()); + } + } + }; + + let mut text_chunk: Vec = vec![0; size as usize]; + stdin.read_exact(&mut text_chunk)?; + let text = String::from_utf8(text_chunk)?; + + Ok(Self { delta, text }) + } +} + +fn main() -> Result<()> { + while let Ok(chunk) = Chunk::read_chunk() { + sleep(Duration::from_millis(chunk.delta.try_into()?)); + print!("{}", chunk.text); + } + + println!(); + + Ok(()) +} diff --git a/src/client/client.rs b/src/client/client.rs new file mode 100644 index 0000000..97a3bb6 --- /dev/null +++ b/src/client/client.rs @@ -0,0 +1,37 @@ +/// mud client type +use std::default::Default; + +use serde::Deserialize; + +/// client type to know log format +#[derive(Debug, Deserialize, PartialEq)] +pub enum Client { + /// mudlet + Mudlet, + + /// tintin++ + TinTin, + + /// zmud + ZMud, + + /// no client specified + None, +} + +impl Default for Client { + fn default() -> Self { + Client::None + } +} + +impl From<&String> for Client { + fn from(s: &String) -> Self { + match s.to_lowercase().as_str() { + "mudlet" => Self::Mudlet, + "tintin" => Self::TinTin, + "zmud" => Self::ZMud, + _ => Self::None, + } + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..97f701f --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,5 @@ +mod client; +mod tintin; + +pub use client::Client; +pub use tintin::TinTin; diff --git a/src/client/tintin.rs b/src/client/tintin.rs new file mode 100644 index 0000000..e44ca3b --- /dev/null +++ b/src/client/tintin.rs @@ -0,0 +1 @@ +pub struct TinTin {} diff --git a/src/form_data.rs b/src/form_data.rs new file mode 100644 index 0000000..90dd457 --- /dev/null +++ b/src/form_data.rs @@ -0,0 +1,36 @@ +//! Data from log submission form + +use std::default::Default; + +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[serde(default = "FormData::default")] +pub struct FormData { + /// Title + #[serde(rename = "submit_title")] + pub title: String, + + /// Player + #[serde(rename = "submit_player")] + pub player: String, + + /// Public + #[serde(rename = "submit_public")] + pub public: String, + + /// Log + #[serde(rename = "submit_log")] + pub log: String, +} + +impl Default for FormData { + fn default() -> Self { + Self { + title: String::new(), + player: String::new(), + public: String::new(), + log: String::new(), + } + } +} diff --git a/src/line.rs b/src/line.rs new file mode 100644 index 0000000..61efd13 --- /dev/null +++ b/src/line.rs @@ -0,0 +1,27 @@ +// use serde::Serialize; + +// #[derive(Debug, Serialize)] +// pub struct Line { +// /// delay before printing the line, in milliseconds +// pub delay: u64, + +// /// text of the line +// pub text: String, +// } + +// impl From<&str> for Line { +// fn from(s: &str) -> Self { +// Self { +// delay: 0, +// text: s.to_owned(), +// } +// } +// } + +pub trait Line { + /// delay before printing the line, in milliseconds + fn delay(&self) -> u64; + + /// text to print + fn text(&self) -> &str; +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..a9704c3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,51 @@ +mod action; +mod client; +mod form_data; +mod line; +mod options; +mod routes; +mod wot_log; + +use actix_files::Files; +use actix_web::{middleware, web, App, HttpServer}; + +use handlebars::Handlebars; +use log::info; + +use action::Action; +use client::Client; +use form_data::FormData; +//use line::Line; +use options::Options; +use wot_log::WotLog; + +pub fn default_false() -> bool { + false +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let options = Options::init(); + rag::init().unwrap(); + + let mut handlebars = Handlebars::new(); + handlebars.register_templates_directory(".html", "./templates").unwrap(); + let handlebars_ref = web::Data::new(handlebars); + + info!("Configuration (see --help to change):"); + info!("{:?}", options); + + HttpServer::new(move || { + App::new() + .app_data(handlebars_ref.clone()) + .wrap(middleware::Logger::default()) + .service(routes::index::get) + .service(routes::submit::get) + .service(routes::submit::post) + .service(Files::new("/static", "static").show_files_listing()) + }) + .bind((options.address, options.port))? + .workers(options.workers) + .run() + .await +} diff --git a/src/options.rs b/src/options.rs new file mode 100644 index 0000000..14de5d6 --- /dev/null +++ b/src/options.rs @@ -0,0 +1,27 @@ +use clap::Parser; + +#[derive(Debug, Parser)] +#[clap(author, version, about, long_about = None)] +pub struct Options { + /// IP address to listen on + #[clap(short, long, default_value = "127.0.0.1")] + pub address: String, + + /// Port to use + #[clap(short, long, default_value = "8807")] + pub port: u16, + + /// URL to access + #[clap(short, long, default_value = "http://localhost:8807")] + pub url: String, + + /// Number of HTTP workers + #[clap(short, long, default_value = "4")] + pub workers: usize, +} + +impl Options { + pub fn init() -> Self { + Self::parse() + } +} diff --git a/src/routes/index.rs b/src/routes/index.rs new file mode 100644 index 0000000..8421512 --- /dev/null +++ b/src/routes/index.rs @@ -0,0 +1,13 @@ +use actix_web::{get, web, HttpResponse}; +use handlebars::Handlebars; +use serde_json::json; + +#[get("/")] +pub async fn get(hb: web::Data>) -> HttpResponse { + let data = json!({ + "name": "rewot" + }); + let body = hb.render("index", &data).unwrap(); + + HttpResponse::Ok().body(body) +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..7f58521 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,2 @@ +pub mod index; +pub mod submit; diff --git a/src/routes/submit.rs b/src/routes/submit.rs new file mode 100644 index 0000000..8a1ae7a --- /dev/null +++ b/src/routes/submit.rs @@ -0,0 +1,31 @@ +use actix_web::{get, post, web, HttpResponse}; +use handlebars::Handlebars; +use serde_json::json; + +use crate::FormData; +use crate::WotLog; + +#[get("/submit")] +pub async fn get(hb: web::Data>) -> HttpResponse { + let data = json!({ + "name": "rewot" + }); + let body = hb.render("submit", &data).unwrap(); + + HttpResponse::Ok().body(body) +} + +#[post("/submit")] +pub async fn post(form: web::Form, hb: web::Data>) -> HttpResponse { + let form_data = form.into_inner(); + let wot_log = WotLog::from(&form_data); + wot_log.parse(); + + let data = json!({ + "name": "rewot" + }); + + let body = hb.render("submit", &data).unwrap(); + + HttpResponse::Ok().body(body) +} diff --git a/src/wot_log.rs b/src/wot_log.rs new file mode 100644 index 0000000..35a415c --- /dev/null +++ b/src/wot_log.rs @@ -0,0 +1,62 @@ +use lazy_static::lazy_static; +use log::{debug, error, info}; +use regex::Regex; +use serde::Deserialize; + +use crate::default_false; +use crate::Action; +use crate::Client; +use crate::FormData; + +#[derive(Debug, Deserialize)] +pub struct WotLog { + /// ID of the log + #[serde(default = "String::new")] + pub id: String, + + /// Title of the log + #[serde(default = "String::new")] + pub title: String, + + /// Name of the player + #[serde(default = "String::new")] + pub player: String, + + /// Is the log public + #[serde(default = "default_false")] + pub public: bool, + + /// Log + #[serde(default = "String::new")] + pub log: String, + + /// Client type + #[serde(default = "Client::default")] + pub client: Client, +} + +impl From<&FormData> for WotLog { + fn from(form_data: &FormData) -> Self { + Self { + id: "".into(), + title: form_data.title.clone(), + player: form_data.player.clone(), + public: if form_data.public == "public" { true } else { false }, + log: form_data.log.clone(), + client: Client::None, + } + } +} + +impl WotLog { + /// Read and parse the log + pub fn parse(self: &Self) { + let mut lineno: u128 = 1; + for line in self.log.lines() { + let line = line.trim_end_matches('\r'); + + let action = Action::from(line); + lineno += 1; + } + } +}