Add timestamps to messages

This commit is contained in:
pjht 2024-10-08 20:59:42 -05:00
parent 9c44bc892f
commit c4caac3a21
Signed by: pjht
GPG Key ID: CA239FC6934E6F3A
8 changed files with 207 additions and 32 deletions

133
Cargo.lock generated
View File

@ -32,6 +32,21 @@ dependencies = [
"alloc-no-stdlib", "alloc-no-stdlib",
] ]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.15" version = "0.6.15"
@ -312,6 +327,48 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets",
]
[[package]]
name = "ciborium"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
dependencies = [
"ciborium-io",
"ciborium-ll",
"serde",
]
[[package]]
name = "ciborium-io"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
[[package]]
name = "ciborium-ll"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
dependencies = [
"ciborium-io",
"half",
]
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.19" version = "4.5.19"
@ -358,6 +415,14 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
[[package]]
name = "common"
version = "0.1.0"
dependencies = [
"chrono",
"serde",
]
[[package]] [[package]]
name = "console_error_panic_hook" name = "console_error_panic_hook"
version = "0.1.7" version = "0.1.7"
@ -368,6 +433,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]] [[package]]
name = "cpufeatures" name = "cpufeatures"
version = "0.2.14" version = "0.2.14"
@ -386,6 +457,12 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "crunchy"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.6" version = "0.1.6"
@ -463,6 +540,9 @@ dependencies = [
name = "frontend" name = "frontend"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"chrono",
"ciborium",
"common",
"console_error_panic_hook", "console_error_panic_hook",
"gloo-console 0.3.0", "gloo-console 0.3.0",
"gloo-net 0.6.0", "gloo-net 0.6.0",
@ -985,6 +1065,16 @@ dependencies = [
"syn 2.0.79", "syn 2.0.79",
] ]
[[package]]
name = "half"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
dependencies = [
"cfg-if",
"crunchy",
]
[[package]] [[package]]
name = "hashbrown" name = "hashbrown"
version = "0.15.0" version = "0.15.0"
@ -1101,6 +1191,29 @@ dependencies = [
"tower-service", "tower-service",
] ]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]] [[package]]
name = "implicit-clone" name = "implicit-clone"
version = "0.4.9" version = "0.4.9"
@ -1273,6 +1386,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.16.0" version = "1.16.0"
@ -1622,7 +1744,9 @@ dependencies = [
"axum", "axum",
"axum-client-ip", "axum-client-ip",
"axum_static", "axum_static",
"ciborium",
"clap", "clap",
"common",
"futures", "futures",
"log", "log",
"slab", "slab",
@ -2180,6 +2304,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.52.0" version = "0.52.0"

View File

@ -1,7 +1,7 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [ members = [ "common",
"frontend", "frontend",
"server", "server",
] ]

8
common/Cargo.toml Normal file
View File

@ -0,0 +1,8 @@
[package]
name = "common"
version = "0.1.0"
edition = "2021"
[dependencies]
chrono = { version = "0.4.38", features = ["serde"] }
serde = { version = "1.0.210", features = ["derive"] }

8
common/src/lib.rs Normal file
View File

@ -0,0 +1,8 @@
use chrono::prelude::*;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ChatMessage {
pub message: String,
pub time: DateTime<Local>,
}

View File

@ -4,6 +4,9 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
chrono = { version = "0.4.38", features = ["wasmbind"] }
ciborium = "0.2.2"
common = { version = "0.1.0", path = "../common" }
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
gloo-console = "0.3.0" gloo-console = "0.3.0"
gloo-net = "0.6.0" gloo-net = "0.6.0"

View File

@ -1,11 +1,13 @@
use std::time::Duration; use std::time::Duration;
use chrono::Local;
use common::ChatMessage;
use gloo_console::log; use gloo_console::log;
use gloo_timers::future::sleep; use gloo_timers::future::sleep;
use yew::prelude::*; use yew::prelude::*;
use yew_router::prelude::*; use yew_router::prelude::*;
use yew_websocket::{ use yew_websocket::{
format::Text, format::Binary,
websocket::{WebSocketService, WebSocketStatus, WebSocketTask}, websocket::{WebSocketService, WebSocketStatus, WebSocketTask},
}; };
@ -32,13 +34,13 @@ fn switch(routes: Route) -> Html {
enum HomeMessage { enum HomeMessage {
SubmittedMessage(String), SubmittedMessage(String),
RecievedMessage(String), RecievedMessage(ChatMessage),
WsStateChange(WebSocketStatus), WsStateChange(WebSocketStatus),
WsReconnect, WsReconnect,
} }
struct Home { struct Home {
messages: Vec<String>, messages: Vec<ChatMessage>,
chat_ws: WebSocketTask, chat_ws: WebSocketTask,
ws_state: WebSocketStatus, ws_state: WebSocketStatus,
ws_reconnecting: bool, ws_reconnecting: bool,
@ -55,12 +57,14 @@ impl Home {
}; };
let api_url = format!("{}://{}/api/chat_ws", ws_proto, location.host().unwrap()); let api_url = format!("{}://{}/api/chat_ws", ws_proto, location.host().unwrap());
log!("Connecting to ", &api_url); log!("Connecting to ", &api_url);
WebSocketService::connect_text( WebSocketService::connect_binary(
&api_url, &api_url,
ctx.link() ctx.link().callback(|msg: Binary| {
.callback(|msg: Text| HomeMessage::RecievedMessage(msg.unwrap())), let msg = msg.unwrap();
ctx.link() let msg = ciborium::from_reader(msg.as_slice()).unwrap();
.callback(HomeMessage::WsStateChange), HomeMessage::RecievedMessage(msg)
}),
ctx.link().callback(HomeMessage::WsStateChange),
) )
.unwrap() .unwrap()
} }
@ -117,8 +121,17 @@ impl Component for Home {
{ "Connection to backend lost, trying to reconnect" } { "Connection to backend lost, trying to reconnect" }
</div> </div>
} }
<div ref={self.message_container_ref.clone()} class="border rounded flex-grow-1 overflow-auto mb-3" style = "flex-basis: 0"> <div
{ self.messages.iter().map(|msg| html!{<div>{msg}</div>}).collect::<Vec<_>>() } ref={self.message_container_ref.clone()}
class="border rounded flex-grow-1 overflow-auto mb-3"
style="flex-basis: 0"
>
{ self.messages.iter().map(|msg| html!{
<div class="d-flex justify-content-between align-items-start mb-2">
<div class="me-2">{msg.message.clone()}</div>
<div class="me-2 text-nowrap">{msg.time.format("%I:%M:%S %p").to_string()}</div>
</div>}
).collect::<Vec<_>>() }
</div> </div>
<div class="bottom-0 mb-3 mt-3"> <div class="bottom-0 mb-3 mt-3">
<input <input
@ -137,12 +150,18 @@ impl Component for Home {
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool { fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg { match msg {
HomeMessage::SubmittedMessage(msg) => { HomeMessage::SubmittedMessage(msg) => {
let msg = ChatMessage {
message: msg.clone(),
time: Local::now(),
};
self.messages.push(msg.clone()); self.messages.push(msg.clone());
self.chat_ws.send(msg); let mut buf = Vec::new();
ciborium::into_writer(&msg, &mut buf).unwrap();
self.chat_ws.send_binary(buf);
true true
} }
HomeMessage::RecievedMessage(msg) => { HomeMessage::RecievedMessage(msg) => {
self.messages.push(msg.clone()); self.messages.push(msg);
true true
} }
HomeMessage::WsStateChange(state) => { HomeMessage::WsStateChange(state) => {

View File

@ -7,7 +7,9 @@ edition = "2021"
axum = { version = "0.7.7", features = ["ws"] } axum = { version = "0.7.7", features = ["ws"] }
axum-client-ip = "0.6.1" axum-client-ip = "0.6.1"
axum_static = "1.7.1" axum_static = "1.7.1"
ciborium = "0.2.2"
clap = { version = "4.5.19", features = ["derive"] } clap = { version = "4.5.19", features = ["derive"] }
common = { version = "0.1.0", path = "../common" }
futures = "0.3.31" futures = "0.3.31"
log = "0.4.22" log = "0.4.22"
slab = "0.4.9" slab = "0.4.9"

View File

@ -6,6 +6,7 @@ use axum::http::StatusCode;
use axum::response::Response; use axum::response::Response;
use axum::{routing::get, Router}; use axum::{routing::get, Router};
use clap::Parser; use clap::Parser;
use common::ChatMessage;
use futures::stream::SplitSink; use futures::stream::SplitSink;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use slab::Slab; use slab::Slab;
@ -19,7 +20,7 @@ use tokio::sync::Mutex;
use tower::{ServiceBuilder, ServiceExt}; use tower::{ServiceBuilder, ServiceExt};
use tower_http::services::ServeDir; use tower_http::services::ServeDir;
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
use tracing::debug; use tracing::{debug, warn};
// Setup the command line interface with clap. // Setup the command line interface with clap.
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@ -42,21 +43,16 @@ struct Opt {
static_dir: PathBuf, static_dir: PathBuf,
} }
//#[derive(Clone)]
struct ServState { struct ServState {
client_sends: Slab<SplitSink<WebSocket, Message>>, client_sends: Slab<SplitSink<WebSocket, Message>>,
chat_log: Vec<String>, chat_log: Vec<ChatMessage>,
} }
impl ServState { impl ServState {
fn new() -> Self { fn new() -> Self {
let chat_log = std::fs::read_to_string("chat_log") let chat_log = std::fs::read("chat_log").map_or(Vec::new(), |chat_log| {
.unwrap_or_default() ciborium::from_reader(chat_log.as_slice()).unwrap()
.split('\n') });
.filter(|x| !x.is_empty())
.map(|x| x.to_string())
.collect::<Vec<_>>();
debug!("{:?}", chat_log);
Self { Self {
client_sends: Slab::new(), client_sends: Slab::new(),
chat_log, chat_log,
@ -142,37 +138,43 @@ async fn chat_ws(State(state): State<Arc<Mutex<ServState>>>, ws: WebSocketUpgrad
let (mut tx, mut rx) = socket.split(); let (mut tx, mut rx) = socket.split();
debug!("Client connected"); debug!("Client connected");
for msg in &state.lock().await.chat_log { for msg in &state.lock().await.chat_log {
tx.send(Message::Text(msg.clone())).await.unwrap(); let mut buf = Vec::new();
ciborium::into_writer(&msg, &mut buf).unwrap();
tx.send(Message::Binary(buf)).await.unwrap();
} }
let tx_idx = state.lock().await.client_sends.insert(tx); let tx_idx = state.lock().await.client_sends.insert(tx);
let mut close_code = ws::close_code::NORMAL; let mut close_code = ws::close_code::NORMAL;
while let Some(msg) = rx.next().await { while let Some(msg) = rx.next().await {
if let Ok(msg) = msg { if let Ok(msg) = msg {
let msg = match msg { let msg_bytes = match msg {
Message::Text(msg) => msg, Message::Binary(msg) => msg,
_ => { _ => {
close_code = ws::close_code::UNSUPPORTED; close_code = ws::close_code::UNSUPPORTED;
warn!("Got unsupported message");
break; break;
} }
}; };
state.lock().await.chat_log.push(msg.clone()); let msg: ChatMessage = ciborium::from_reader(msg_bytes.as_slice()).unwrap();
state.lock().await.chat_log.push(msg);
let mut log_file = OpenOptions::new() let mut log_file = OpenOptions::new()
.write(true) .write(true)
.append(true)
.create(true) .create(true)
.truncate(true)
.open("chat_log") .open("chat_log")
.await .await
.unwrap(); .unwrap();
log_file.write_all(msg.as_bytes()).await.unwrap(); let mut buf = Vec::new();
log_file.write_all(&[b'\n']).await.unwrap(); ciborium::into_writer(&state.lock().await.chat_log, &mut buf).unwrap();
log_file.write_all(&buf).await.unwrap();
for (i, client_tx) in state.lock().await.client_sends.iter_mut() { for (i, client_tx) in state.lock().await.client_sends.iter_mut() {
if i == tx_idx { if i == tx_idx {
continue; continue;
} }
let _ = client_tx.send(Message::Text(msg.clone())).await; let _ = client_tx.send(Message::Binary(msg_bytes.clone())).await;
} }
} else { } else {
close_code = ws::close_code::PROTOCOL; close_code = ws::close_code::PROTOCOL;
warn!("Websocket protocol error");
break; break;
}; };
} }