diff --git a/Cargo.lock b/Cargo.lock index 674cb57..c9c49bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,15 +101,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "form_urlencoded" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" -dependencies = [ - "percent-encoding", -] - [[package]] name = "fs2" version = "0.4.3" @@ -165,16 +156,6 @@ dependencies = [ "utf8-width", ] -[[package]] -name = "idna" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" -dependencies = [ - "unicode-bidi", - "unicode-normalization", -] - [[package]] name = "instant" version = "0.1.12" @@ -195,7 +176,6 @@ name = "kchan" version = "0.1.0" dependencies = [ "bincode", - "byteorder", "html-escape", "lazy_static", "log", @@ -206,7 +186,6 @@ dependencies = [ "structopt", "thiserror", "threadpool", - "url", "urlencoding", ] @@ -294,12 +273,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "percent-encoding" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -326,18 +299,18 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.58" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1fb82fc0c281dd9671101b66b771ebbe1eaf967b96ac8740dcba4b70005ca8" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -374,7 +347,7 @@ checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.17", ] [[package]] @@ -453,9 +426,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "45b6ddbb36c5b969c182aec3c4a0bce7df3fbad4b77114706a49aacc80567388" dependencies = [ "proc-macro2", "quote", @@ -497,7 +470,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.17", ] [[package]] @@ -538,42 +511,12 @@ dependencies = [ "time-core", ] -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "unicode-bidi" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" - [[package]] name = "unicode-ident" version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" -[[package]] -name = "unicode-normalization" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-segmentation" version = "1.10.1" @@ -586,17 +529,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" -[[package]] -name = "url" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - [[package]] name = "urlencoding" version = "2.1.2" diff --git a/Cargo.toml b/Cargo.toml index 8de4b8c..58a6501 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,8 +18,6 @@ log = "0.4.17" simplelog = "0.12.1" html-escape = "0.2.13" urlencoding = "2.1.2" -byteorder = "1.4.3" -url = "2.3.1" #ctrlc = "3.3.1" #image = "0.24.6" diff --git a/README.md b/README.md index 66daa78..e22004d 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,4 @@ ### TODO: -1. proper logging -2. working threads and posts -3. add authorization and ip whitelist -4. parse http form `multipart/form-data` -5. add images 6. limit storage, maybe with pseudorandomness, find a way to clean old threads 7. better html and css 8. optimize/clean code, improve memory usage @@ -14,7 +9,6 @@ capacity - the least active thread will then be removed. + id's will be attached to every thread and every response based on the order of creation. -+ specific posts will be searchable under the `//` uri. ### POST usage diff --git a/src/errors.rs b/src/errors.rs index b548c14..4278819 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,3 +1,5 @@ +use crate::http::{Response, Status}; + use std::string::FromUtf8Error; use thiserror::Error; // TODO add NotFound and InternalServerError response here, @@ -9,26 +11,71 @@ pub enum RequestError { NotAForm, #[error("resource not found")] NotFound, + #[error("unused method")] + UnusedMethod, #[error("request not authorized, incorred hash: {0}")] NotAuthorized(String), #[error("urldecoding error")] UrlDecodeErr(#[from] FromUtf8Error), #[error("bad request")] BadRequest, + #[error("missing body in POST request")] + MissingBody, } #[derive(Debug, Error)] -pub enum InternalError { +pub enum DatabaseError { #[error("failed to read from database")] - DatabaseReadError, - #[error("failed to write to database")] - DatabaseWriteError, + SledError(#[from] sled::Error), + #[error("not found in database")] + NotInDb, + #[error("serialization error")] + SerError, } +pub enum InternalError {} + #[derive(Debug, Error)] pub enum HandlingError { #[error("client error: {0}")] ClientError(#[from] RequestError), #[error("server error: {0}")] - ServerError(#[from] InternalError), + ServerError(#[from] DatabaseError), + #[error("problem reading stream")] + Io(#[from] std::io::Error), +} + +impl From for Status { + fn from(err: DatabaseError) -> Self { + match err { + DatabaseError::SledError(e) => Status::InternalServerError, + DatabaseError::NotInDb => Status::BadRequest, + DatabaseError::SerError => Status::InternalServerError, + } + } +} + +impl From for Status { + fn from(err: RequestError) -> Self { + match err { + RequestError::NotAForm => Status::BadRequest, + RequestError::NotFound => Status::BadRequest, + RequestError::UnusedMethod => Status::BadRequest, + RequestError::NotAuthorized(_) => Status::Unauthorized, + RequestError::UrlDecodeErr(_) => Status::BadRequest, + RequestError::BadRequest => Status::BadRequest, + RequestError::MissingBody => Status::BadRequest, + } + } +} + +impl From for Response { + fn from(err: HandlingError) -> Self { + let status = match err { + HandlingError::ClientError(e) => Status::from(e), + HandlingError::ServerError(e) => Status::from(e), + HandlingError::Io(e) => Status::InternalServerError, + }; + Response::new(status, vec![], status.message().to_string()) + } } diff --git a/src/http.rs b/src/http.rs index 1f6d17c..6fc526c 100644 --- a/src/http.rs +++ b/src/http.rs @@ -8,17 +8,17 @@ const HTTP_VERSION: &'static str = "HTTP/1.1"; #[derive(Debug)] pub struct Request { - pub method: String, + pub method: Method, pub uri: String, pub headers: Vec<(String, String)>, - pub body: String, + pub body: Option>, } #[derive(Debug)] pub struct Response { pub status: Status, pub headers: Vec<(String, String)>, - pub body: String, + pub body: String, // make into Option> } #[derive(Debug, Clone, Copy)] @@ -31,6 +31,25 @@ pub enum Status { InternalServerError = 500, } +#[derive(Debug)] +pub enum Method { + Get, + Post, + Delete, +} + +impl FromStr for Method { + type Err = RequestError; + fn from_str(s: &str) -> Result { + match s { + "GET" => Ok(Self::Get), + "POST" => Ok(Self::Post), + "DELETE" => Ok(Self::Delete), + _ => Err(Self::Err::UnusedMethod), + } + } +} + impl Status { pub fn message(&self) -> &'static str { match self { @@ -58,34 +77,38 @@ impl Status { // } //} -impl FromStr for Request { - type Err = RequestError; +impl Request { + pub fn add_body(&mut self, body: Vec) { + self.body = Some(body); + } +} - fn from_str(s: &str) -> Result { +impl TryFrom> for Request { + type Error = RequestError; + + fn try_from(s: Vec) -> Result { //dbg!(&s); - let mut iter = s.lines(); + let mut iter = s.iter(); let mut first = iter .next() .ok_or(RequestError::BadRequest)? .split_whitespace(); - let method = first.next().ok_or(RequestError::BadRequest)?; - let uri = first.next().ok_or(RequestError::BadRequest)?; + let method: Method = first.next().ok_or(RequestError::BadRequest)?.parse()?; + let uri = first.next().ok_or(RequestError::BadRequest)?.to_string(); let mut headers: Vec<(String, String)> = vec![]; - for line in &mut iter { - if line.is_empty() { - break; - } - let line: Vec<&str> = line.split(":").take(2).collect(); - headers.push((line[0].into(), line[1].into())); + for line in iter { + let mut line = line.split(":").take(2); + let left = line.next().ok_or(RequestError::BadRequest)?.trim(); + let right = line.next().ok_or(RequestError::BadRequest)?.trim(); + headers.push((left.to_string(), right.to_string())); } - let body: String = iter.fold(String::new(), |a, b| format!("{}{}", a, b)); Ok(Self { - method: method.to_string(), - uri: uri.to_string(), + method, + uri, headers, - body: body.to_string(), + body: None, }) } } @@ -103,12 +126,6 @@ impl Response { } } -impl From for Response { - fn from(err: HandlingError) -> Self { - todo!(); - } -} - impl fmt::Display for Response { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let headers: String = self diff --git a/src/main.rs b/src/main.rs index 208a569..3d75b69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,22 +4,24 @@ mod http; mod post; use crate::errors::{HandlingError, InternalError, RequestError}; -use crate::html::{FAQ, INDEX, STYLE, FAVICON}; +use crate::html::{FAQ, FAVICON, INDEX, STYLE}; use crate::http::{Request, Response, Status}; use crate::post::{Post, Thread}; use std::error::Error; -use std::io::{Read, Write}; +use std::io; +use std::io::{BufRead, BufReader, Read, Write}; use std::net::{SocketAddr, TcpListener, TcpStream}; use std::path::{Path, PathBuf}; use std::str; use std::str::from_utf8; -use std::sync::{Arc, Mutex, MutexGuard}; +use std::sync::{Arc, RwLock}; use std::thread::JoinHandle; use bincode::{deserialize, serialize}; -use byteorder::{BigEndian, LittleEndian, ReadBytesExt, WriteBytesExt}; +use errors::DatabaseError; use html_escape::encode_text; +use http::Method; use lazy_static::lazy_static; use log::*; use simplelog::*; @@ -27,6 +29,9 @@ use structopt::StructOpt; use threadpool::ThreadPool; use urlencoding::decode; +// use this instead of hard-coding +type ID_TYPE = u32; + // represents command line arguments #[derive(StructOpt, Debug)] struct Opt { @@ -45,129 +50,217 @@ struct Opt { lazy_static! { // parse command line arguments static ref OPT: Opt = Opt::from_args(); + static ref DB: sled::Db = sled::open(&OPT.database).expect("failed to open db"); + static ref THREADS: sled::Tree = DB.open_tree(b"threads").expect("failed to initialize threads"); + static ref POSTS: sled::Tree = DB.open_tree(b"posts").expect("failed to intialize posts"); } -#[derive(Debug)] -struct State { - db: sled::Db, - id_counter: u32, - // cache here? +/// increments the id stored inside the db +fn increment(old: Option<&[u8]>) -> Option> { + let number = match old { + Some(s) => { + let buf: [u8; 4] = s.try_into().unwrap(); + u32::from_be_bytes(buf) + 1 + // FIXME + } + None => 0, + }; + Some(number.to_be_bytes().to_vec()) } -unsafe impl Send for State {} -unsafe impl Sync for State {} +// DATABASE ACCESS FUNCTIONS +/// returns post from id +fn get_post(id: u32) -> Result { + POSTS + .get(id.to_be_bytes())? + .ok_or(DatabaseError::NotInDb) + .and_then(|x| deserialize::(&x).map_err(|_| DatabaseError::SerError)) +} -impl State { - fn new(db_name: impl AsRef) -> Self { - let mut s = Self { - db: sled::open(db_name).unwrap(), - id_counter: 0, - }; - s.init_id(); - s - } +/// returns thread from id +fn get_thread(id: u32) -> Result, DatabaseError> { + THREADS + .get(id.to_be_bytes())? + .ok_or(DatabaseError::NotInDb) + .and_then(|x| deserialize::(&x).map_err(|_| DatabaseError::SerError))? + .into_iter() + .map(|x| get_post(x)) + .collect() + // let thread = THREADS.get(id.to_be_bytes()).and_then() +} - fn next_id(&mut self) -> String { - // TODO improve this - // means the highest possible id is 999_999_999, then it loops back to zero - // NOTE this could technically be used to limit the storage required, - // might possibly be a good feature - self.id_counter = (self.id_counter + 1) % 1_000_000_000; - let width = u32::MAX.ilog10() as usize; // happens to be 9 in this case - //let mut v = vec![]; - //v.write_u32::(self.id_counter).unwrap() - //self.db.insert(b"id", v); - format!("{:0width$}", self.id_counter) - } - fn init_id(&mut self) { - self.id_counter = match self.db.get(b"id").unwrap() { - Some(s) => (&(*s)).read_u32::().unwrap(), - None => 0, - } - } +/// generates next id +fn next_id() -> Result { + DB.fetch_and_update(b"id", increment) + .map(|x| match x { + Some(s) => { + let buf: [u8; 4] = (*s).try_into().unwrap(); + u32::from_be_bytes(buf) + } + None => 0u32, + }) + .map_err(|e| DatabaseError::from(e)) } -/// top level handling of requests -fn handle(request: &str, state: Arc>) -> Response { - let request = match request.parse::() { - Ok(s) => s, - Err(e) => return Response::new(Status::BadRequest, vec![], e.to_string()), - }; - let state = state.lock().unwrap(); +/// returns current id +/// not used +fn get_id() -> Result { + let s = DB.get(b"id")?.unwrap(); + let buf: [u8; 4] = (*s).try_into().unwrap(); + Ok(u32::from_be_bytes(buf)) +} - match request.method.as_str() { - "GET" => match get(&request.uri, state) { - Ok(s) => s, - Err(e) => Response::new(Status::NotFound, vec![], dbg!(e).to_string()), - }, - "POST" => match post(request, state) { - Ok(s) => s, - Err(e) => Response::new(Status::Ok, vec![], "".to_string()), - }, - // check admin hash - "DELETE" => Response::new(Status::Ok, vec![], "".to_string()), - _ => Response::new(Status::Ok, vec![], RequestError::BadRequest.to_string()), +/// lists all threads +fn list_threads() -> Result, DatabaseError> { + THREADS + .iter() + .take_while(|x| x.is_ok()) + .map(|x| x.unwrap().0) + .map(|x| { + POSTS.get(x).map(|x| match x { + Some(s) => s, + None => { + error!("couldn't find thread op among posts"); + panic!("unrecoverable error"); + } + }) + }) + .map(|x| { + x.map_err(|e| DatabaseError::from(e)) + .and_then(|i| deserialize::(&i).map_err(|_| DatabaseError::SerError)) + }) + .collect() +} + +// NOTE worst out of everything, but I guess it's not so bad +fn add_post(mut post: Post, thread_id: u32) -> Result<(), DatabaseError> { + let id = next_id()?; + post.id = id; + let mut thread = deserialize::( + &THREADS + .get(thread_id.to_be_bytes())? + .ok_or(DatabaseError::NotInDb)?, + ) + .map_err(|_| DatabaseError::SerError)?; + thread.push(post.id); + let thread = serialize(&thread).map_err(|_| DatabaseError::SerError)?; + THREADS.insert(thread_id.to_be_bytes(), thread)?; + + POSTS.insert( + id.to_be_bytes(), + serialize(&post).map_err(|_| DatabaseError::SerError)?, + )?; + Ok(()) +} + +fn add_thread(mut post: Post) -> Result<(), DatabaseError> { + let id = next_id()?; + let thread = Thread::new(id); + post.id = id; + THREADS.insert( + id.to_be_bytes(), + serialize(&thread).map_err(|_| DatabaseError::SerError)?, + )?; + POSTS.insert( + id.to_be_bytes(), + serialize(&post).map_err(|_| DatabaseError::SerError)?, + )?; + Ok(()) +} + +// END DATABASE ACCESS FUNCTIONS + +/// top level handling of requests +fn handle(mut reader: BufReader<&mut TcpStream>) -> Result { + let mut request: Request = reader + .by_ref() + .lines() + .take_while(|l| l.is_ok()) + .map(|l| l.unwrap()) + .take_while(|l| !l.is_empty()) + .collect::>() + .try_into()?; + + if let Some(s) = request + .headers + .iter() + .find(|(a, b)| a.to_lowercase() == "content-length") + { + let body: Vec = reader + .bytes() + .take(s.1.parse::().unwrap()) + .collect::, io::Error>>() + .unwrap(); + // TODO test if it works + // FIXME deunwrap + // TODO handle body too large + request.add_body(body); + } + dbg!(&request); + + match request.method { + Method::Get => get(&request.uri), + Method::Post => post(request), + // TODO check admin hash + _ => Ok(Response::from(HandlingError::from( + RequestError::BadRequest, + ))), } } /// get "/" returns head of every thread /// or returns posts from thread with id in uri -fn get(path: &str, state: MutexGuard) -> Result { +fn get(path: &str) -> Result { match path { // list threads in this case "/" => { - let content = content( - "index", - &state - .db - .iter() - .map(|x| x.unwrap()) //FIXME unwraps - .map(|x| deserialize::(&x.1).unwrap().head()) // FIXME here too - .fold(String::from(""), |a, b| format!("{}{}", a, b)), - ); - - Ok(Response::new(Status::Ok, vec![], content)) + // FIXME absolute unwrap hell + let ops = list_threads()? + .iter() + .fold(String::from(""), |a, b| format!("{a}\n{b}")); + let c = content("index", &ops); + + Ok(Response::new(Status::Ok, vec![], c)) } // TODO favicon.ico "/css" => Ok(Response::new(Status::Ok, vec![], String::from(STYLE))), "/faq" => Ok(Response::new(Status::Ok, vec![], String::from(FAQ))), - //"/favicon.ico" => Ok(Response::new(Status::Ok, vec![("content-type", "image/x-icon")], FAVICON)), + // TODO favicon // list specific thread here // FIXME unwrap hell s => { - let content = content( - s, - &deserialize::( - &state - .db - .get(&s[1..]) - .map_err(|_| HandlingError::ServerError(InternalError::DatabaseReadError))? - .ok_or(HandlingError::ClientError(RequestError::NotFound))?, - ) + let id = s + .trim_start_matches("/") + .split("/") + .next() .unwrap() - .to_string(), - ); - - Ok(Response::new(Status::Ok, vec![], content)) + .parse::() + .map_err(|_| RequestError::NotFound)?; + let c = get_thread(id)? + .iter() + .fold(String::from(""), |a, b| format!("{a}\n{b}")); + let c = content(&id.to_string(), &c); + + Ok(Response::new(Status::Ok, vec![], c)) } } } -fn post(request: Request, mut state: MutexGuard) -> Result { - let content = encode_text(&request.body).into_owned(); +fn post(request: Request) -> Result { + let binding = &request.body.ok_or(RequestError::MissingBody)?; + let c = String::from_utf8_lossy(&binding); + let c = encode_text(&c).into_owned(); + + let post = Post::new(0, c); + match request.uri.as_str() { // means we wish to create a new thread "/" => { - // TODO fix unwrap - let id = state.next_id(); - let thread = Thread(vec![Post::new(&id, content)]); - - state.db.insert(&id, serialize(&thread).unwrap()).unwrap(); - - info!("Created new thread, id: {}", id); + add_thread(post)?; + info!("Created new thread"); // TODO add redirect Ok(Response::new( @@ -179,41 +272,33 @@ fn post(request: Request, mut state: MutexGuard) -> Result { - // first we increment id - let id = state.next_id(); - let post = Post::new(&id, content); - - let mut thread = deserialize::( - &state - .db - .remove(&s[1..]) - .map_err(|_| HandlingError::ServerError(InternalError::DatabaseWriteError))? - .ok_or(HandlingError::ClientError(RequestError::NotFound))?, - ) - .unwrap(); - - thread.0.push(post); + let id = s + .trim_start_matches("/") + .split("/") + .next() + .unwrap() + .parse::() + .map_err(|_| RequestError::NotFound)?; + add_post(post, id)?; - state - .db - .insert(&s[1..], serialize(&thread).unwrap()) - .unwrap(); - // make this less weird - info!("Added new post to thread {}", s); + info!("Added new post to thread {id}"); // TODO add redirect - Ok(Response::new(Status::SeeOther, vec![("location", s)], String::from(""))) + Ok(Response::new( + Status::SeeOther, + vec![("location", s)], + String::from(""), + )) } } } -fn delete(path: &str, database: Arc>) -> Response { +fn delete(path: &str) -> Response { todo!(); } -#[inline] fn content(name: &str, main: &str) -> String { - INDEX.replace("{name}", &name).replace("{}", &main) + INDEX.replace("{name}", name).replace("{}", main) } fn main() { @@ -229,10 +314,7 @@ fn main() { TerminalMode::Mixed, ColorChoice::Auto, ) - .unwrap(); - // open database - let state = Arc::new(Mutex::new(State::new(&OPT.database))); - // TODO setup cache + .expect("failed to setup logger"); // wait for requests for stream in listener.incoming() { @@ -244,23 +326,16 @@ fn main() { } }; - let state = Arc::clone(&state); pool.execute(move || { - let ip = stream.peer_addr().unwrap(); - // TODO check if allowed ip before reading request - let mut buffer = [0; 1 << 10]; // 2 to the power of 10 - // how do I implement images with this? - - stream.read(&mut buffer).unwrap(); + let reader = BufReader::new(&mut stream); - // NOTE possibly problematic when we cosider images - let text = str::from_utf8(&buffer).unwrap().trim_matches(0 as char); - // ^-- gets rid of null at the end of buffer + let response = match handle(reader) { + Ok(s) => s, + Err(e) => Response::from(dbg!(e)), + }; // handle request - stream - .write(handle(text, state).to_string().as_bytes()) - .unwrap(); + stream.write(response.to_string().as_bytes()).unwrap(); }); } } diff --git a/src/post.rs b/src/post.rs index eddf6d2..304f88a 100644 --- a/src/post.rs +++ b/src/post.rs @@ -10,29 +10,47 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Post { - pub id: String, + pub id: u32, // technically reduntant but whatever //pub img: Option, pub body: String, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Thread(pub Vec); +pub struct Thread(Vec); + +impl IntoIterator for Thread { + type Item = u32; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} impl Thread { - pub fn head(&self) -> String { - let first = &self.0[0]; - format!( - "
{}
{}
", - first.id, - first.to_string(), - ) + pub fn new(op: u32) -> Self { + Thread(vec![op]) + } + pub fn push(&mut self, thing: u32) { + self.0.push(thing) } } +//impl Thread { +// pub fn head(&self) -> String { +// let first = &self.0[0]; +// format!( +// "
{}
{}
", +// first.id, +// first.to_string(), +// ) +// } +//} + impl Post { - pub fn new(id: &str, body: String) -> Self { + pub fn new(id: u32, body: String) -> Self { Self { - id: id.to_string(), + id, //img, body, } @@ -75,13 +93,3 @@ impl fmt::Display for Post { ) } } - -impl fmt::Display for Thread { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let posts: String = self - .0 - .iter() - .fold(String::from(""), |a, b| format!("{}{}", a, b)); - write!(f, "{}", posts) - } -}