diff --git a/crates/icy_board/src/menu_runner/login.rs b/crates/icy_board/src/menu_runner/login.rs index 8a0454ca..2f8aafcb 100644 --- a/crates/icy_board/src/menu_runner/login.rs +++ b/crates/icy_board/src/menu_runner/login.rs @@ -26,7 +26,7 @@ impl PcbBoardCommand { self.state.reset_color(TerminalTarget::Both).await?; self.state.clear_screen(TerminalTarget::Both).await?; self.state.get_term_caps()?; - self.state.session.login_date = chrono::Local::now(); + self.state.session.login_date = chrono::Utc::now(); // intial_welcome let board_name = self.state.get_board().await.config.board.name.clone(); diff --git a/crates/icy_board_engine/src/icy_board/doors/chain_txt.rs b/crates/icy_board_engine/src/icy_board/doors/chain_txt.rs index b00c8215..7147844d 100644 --- a/crates/icy_board_engine/src/icy_board/doors/chain_txt.rs +++ b/crates/icy_board_engine/src/icy_board/doors/chain_txt.rs @@ -1,6 +1,6 @@ use std::fs; -use chrono::{Local, Timelike, Utc}; +use chrono::{Timelike, Utc}; use icy_engine::TextPane; use crate::{ @@ -53,7 +53,7 @@ pub async fn create_chain_txt(state: &IcyBoardState, path: &std::path::Path) -> contents.push_str(&format!("{}\r\n", board.config.board.name)); contents.push_str(&format!("{}\r\n", board.config.sysop.name)); contents.push_str(&format!("{}\r\n", state.session.login_date.time().num_seconds_from_midnight())); - contents.push_str(&format!("{}\r\n", (Local::now() - state.session.login_date).num_seconds())); + contents.push_str(&format!("{}\r\n", (Utc::now() - state.session.login_date).num_seconds())); contents.push_str(&format!("{}\r\n", state.session.current_user.as_ref().unwrap().stats.total_upld_bytes / 1024)); contents.push_str(&format!("{}\r\n", state.session.current_user.as_ref().unwrap().stats.num_uploads)); contents.push_str(&format!("{}\r\n", state.session.current_user.as_ref().unwrap().stats.today_dnld_bytes / 1024)); diff --git a/crates/icy_board_engine/src/icy_board/doors/pcboard.rs b/crates/icy_board_engine/src/icy_board/doors/pcboard.rs index 8896dd1d..ae99bbc2 100644 --- a/crates/icy_board_engine/src/icy_board/doors/pcboard.rs +++ b/crates/icy_board_engine/src/icy_board/doors/pcboard.rs @@ -10,7 +10,7 @@ use crate::{ tables::export_cp437_string, Res, }; -use chrono::{Local, Timelike}; +use chrono::{Timelike, Utc}; pub async fn create_pcboard(state: &IcyBoardState, path: &std::path::Path) -> Res<()> { create_pcboard_sys(state, path)?; create_user_sys(state, path).await?; @@ -39,7 +39,7 @@ fn create_pcboard_sys(state: &IcyBoardState, path: &std::path::Path) -> Res<()> contents.extend(export_cp437_string(&state.session.get_first_name(), 15, b' ')); // User's First Name (padded to 15 characters) contents.extend(export_cp437_string(&"SECRET", 12, b' ')); // User's Password (padded to 12 characters) contents.extend(u16::to_le_bytes((state.session.login_date.time().num_seconds_from_midnight() / 60) as u16)); // Time User Logged On (in minutes since midnight) - contents.extend(u16::to_le_bytes((Local::now() - state.session.login_date).num_minutes() as u16)); // Time used so far today (negative number of minutes) + contents.extend(u16::to_le_bytes((Utc::now() - state.session.login_date).num_minutes() as u16)); // Time used so far today (negative number of minutes) contents.extend(state.session.login_date.format("%H:%M").to_string().as_bytes()); // Time User Logged On (in "HH:MM" format) contents.extend(u16::to_le_bytes(32767)); // Time Allowed On (from PWRD file) contents.extend(u16::to_le_bytes(32767)); // Allowed K-Bytes for Download @@ -158,7 +158,7 @@ async fn create_user_sys(state: &IcyBoardState, path: &std::path::Path) -> Res<( contents.extend(export_cp437_string(&user.user_comment, 31, 0)); contents.extend(export_cp437_string(&user.sysop_comment, 31, 0)); contents.extend(u32::to_le_bytes(user.stats.today_dnld_bytes as u32)); - contents.extend(u32::to_le_bytes((Local::now() - state.session.login_date).num_minutes() as u32)); + contents.extend(u32::to_le_bytes((Utc::now() - state.session.login_date).num_minutes() as u32)); contents.extend(u16::to_le_bytes(0)); // Julian date for Registration Expiration Date contents.extend(u32::to_le_bytes(0)); // Expired Security Level contents.extend(u16::to_le_bytes(0)); // LastConference diff --git a/crates/icy_board_engine/src/icy_board/state/mod.rs b/crates/icy_board_engine/src/icy_board/state/mod.rs index fc477ccf..33caaffc 100644 --- a/crates/icy_board_engine/src/icy_board/state/mod.rs +++ b/crates/icy_board_engine/src/icy_board/state/mod.rs @@ -10,7 +10,7 @@ use std::{ use crate::{executable::Executable, icy_board::user_base::UserBase, Res}; use async_recursion::async_recursion; -use chrono::{DateTime, Datelike, Local, Timelike, Utc}; +use chrono::{DateTime, Datelike, Local, Utc}; use codepages::tables::UNICODE_TO_CP437; use dizbase::file_base::FileBase; use icy_engine::{ansi, OutputFormat, SaveOptions, ScreenPreperation}; @@ -70,6 +70,9 @@ pub struct DisplayOptions { pub show_on_screen: bool, pub in_file_list: Option, + + // Enable CTRL-X / CTRL-K checking for display_files + pub allow_break: bool, } impl DisplayOptions { @@ -94,6 +97,7 @@ impl Default for DisplayOptions { display_text: true, show_on_screen: true, in_file_list: None, + allow_break: true, } } } @@ -126,7 +130,7 @@ pub struct Session { pub is_local: bool, pub paged_sysop: bool, - pub login_date: DateTime, + pub login_date: DateTime, pub current_user: Option, pub cur_user_id: i32, @@ -187,6 +191,13 @@ pub struct Session { // The maximum number of files in flagged_files pub batch_limit: usize, pub flagged_files: Vec, + + /// The current message number read (used for @CURMSGNUM@ macro) + pub current_messagenumber: u32, + pub high_msg_num: u32, + pub low_msg_num: u32, + pub last_msg_read: u32, + pub highest_msg_read: u32, } impl Session { @@ -195,7 +206,7 @@ impl Session { disp_options: DisplayOptions::default(), current_conference_number: 0, current_conference: Conference::default(), - login_date: Local::now(), + login_date: Utc::now(), current_user: None, cur_user_id: -1, cur_security: 0, @@ -238,6 +249,11 @@ impl Session { // Seems to be hardcoded in PCBoard batch_limit: 30, + current_messagenumber: 0, + high_msg_num: 0, + low_msg_num: 0, + last_msg_read: 0, + highest_msg_read: 0, } } @@ -761,7 +777,6 @@ impl IcyBoardState { return Err(IcyBoardError::UserNumberInvalid(user_number).into()); } let mut user = self.get_board().await.users[user_number].clone(); - user.stats.last_on = Utc::now(); user.stats.num_times_on += 1; let last_conference: u16 = user.last_conference; self.get_board().await.statistics.add_caller(user.get_name().clone()); @@ -801,6 +816,16 @@ impl IcyBoardState { self.update_language().await; } + if let Some(user) = &mut self.session.current_user { + let login_date = self.session.login_date.to_utc(); + if user.stats.last_on.date_naive() != login_date.date_naive() { + user.stats.minutes_today = 0; + } + user.stats.minutes_today += (Utc::now() - login_date).num_minutes() as u16; + + user.stats.last_on = login_date; + } + if let Some(user) = &self.session.current_user { let mut board = self.get_board().await; for u in 0..board.users.len() { @@ -1317,8 +1342,11 @@ impl IcyBoardState { "CONFNUM" => result = self.session.current_conference_number.to_string(), // TODO - "CREDLEFT" | "CREDNOW" | "CREDSTART" | "CREDUSED" | "CURMSGNUM" => {} + "CREDLEFT" | "CREDNOW" | "CREDSTART" | "CREDUSED" => {} + "CURMSGNUM" => { + result = self.session.current_messagenumber.to_string(); + } "DATAPHONE" => { if let Some(user) = &self.session.current_user { result = user.bus_data_phone.to_string(); @@ -1379,12 +1407,28 @@ impl IcyBoardState { result = user.home_voice_phone.to_string(); } } - "HIGHMSGNUM" => {} + "HIGHMSGNUM" => result = self.session.high_msg_num.to_string(), "INAME" => {} "INCONF" => result = self.session.current_conference.name.to_string(), - "LOGDATE" => {} // Logon Date - "LOGTIME" => {} // Logon Time - "KBLEFT" | "KBLIMIT" | "LASTCALLERNODE" | "LASTCALLERSYSTEM" | "LASTDATEON" | "LASTTIMEON" | "LMR" | "LOWMSGNUM" | "MAXBYTES" | "MAXFILES" => {} + "LOGDATE" => result = self.format_date(self.session.login_date), + "LOGTIME" => result = self.format_time(self.session.login_date), + "LASTDATEON" => { + if let Some(user) = &self.session.current_user { + result = self.format_date(user.stats.last_on); + } + } + "LASTTIMEON" => { + if let Some(user) = &self.session.current_user { + result = self.format_time(user.stats.last_on); + } + } + "LOWMSGNUM" => { + result = self.session.low_msg_num.to_string(); + } + "LMR" => { + result = self.session.last_msg_read.to_string(); + } + "KBLEFT" | "KBLIMIT" | "LASTCALLERNODE" | "LASTCALLERSYSTEM" | "MAXBYTES" | "MAXFILES" => {} "MINLEFT" => result = "1000".to_string(), "MORE" => { let _ = self.more_promt().await; @@ -1463,7 +1507,27 @@ impl IcyBoardState { self.session.non_stop_off(); return None; } - "PROLTR" | "PRODESC" | "PWXDATE" | "PWXDAYS" | "QOFF" | "QON" | "RATIOBYTES" | "RATIOFILES" => {} + "PROLTR" => { + if let Some(user) = &self.session.current_user { + result = user.protocol.to_string(); + } + } + "PRODESC" => { + if let Some(user) = &self.session.current_user { + if let Some(prot) = self.board.lock().await.protocols.find_protocol(&user.protocol) { + result = prot.description.to_string(); + } + } + } + "QOFF" => { + self.session.disp_options.allow_break = false; + return None; + } + "QON" => { + self.session.disp_options.allow_break = true; + return None; + } + "PWXDATE" | "PWXDAYS" | "RATIOBYTES" | "RATIOFILES" => {} "RCPS" => result = self.transfer_statistics.uploaded_cps.to_string(), "RBYTES" => result = self.transfer_statistics.uploaded_bytes.to_string(), "RFILES" => result = self.transfer_statistics.uploaded_files.to_string(), @@ -1486,13 +1550,11 @@ impl IcyBoardState { "SYSOPIN" => result = self.get_board().await.config.sysop.sysop_start.to_string(), "SYSOPOUT" => result = self.get_board().await.config.sysop.sysop_stop.to_string(), "SYSTIME" => { - let now = Local::now(); - let t = now.time(); - result = format!("{:02}:{:02}", t.hour(), t.minute()); + result = self.format_time(Utc::now()); } "TIMELIMIT" => result = self.session.time_limit.to_string(), "TIMELEFT" => { - let now = Local::now(); + let now = Utc::now(); let time_on = now - self.session.login_date; if self.session.time_limit == 0 { result = "UNLIMITED".to_string(); @@ -1500,8 +1562,14 @@ impl IcyBoardState { result = (self.session.time_limit as i64 - time_on.num_minutes()).to_string(); } } - "TIMEUSED" => {} - "TOTALTIME" => {} + "TIMEUSED" => result = (Utc::now() - self.session.login_date).num_minutes().to_string(), + "TOTALTIME" => { + let mut current = (Utc::now() - self.session.login_date).num_minutes(); + if let Some(user) = &self.session.current_user { + current += user.stats.minutes_today as i64; + } + result = current.to_string(); + } "UPBYTES" => { if let Some(user) = &self.session.current_user { result = user.stats.total_upld_bytes.to_string(); @@ -1527,8 +1595,14 @@ impl IcyBoardState { result = "0".to_string(); } } - "WAIT" => {} - "WHO" => {} + "WAIT" => { + let _ = self.press_enter().await; + return None; + } + "WHO" => { + let _ = self.who_display_nodes().await; + return None; + } "XOFF" => { self.session.disp_options.grapics_mode = GraphicsMode::Ansi; return None; diff --git a/crates/icy_board_engine/src/icy_board/state/user_commands/login.rs b/crates/icy_board_engine/src/icy_board/state/user_commands/login.rs index 3ae2279d..4437002c 100644 --- a/crates/icy_board_engine/src/icy_board/state/user_commands/login.rs +++ b/crates/icy_board_engine/src/icy_board/state/user_commands/login.rs @@ -27,7 +27,7 @@ impl IcyBoardState { self.reset_color(TerminalTarget::Both).await?; self.clear_screen(TerminalTarget::Both).await?; self.get_term_caps()?; - self.session.login_date = chrono::Local::now(); + self.session.login_date = chrono::Utc::now(); // intial_welcome let board_name = self.get_board().await.config.board.name.clone(); diff --git a/crates/icy_board_engine/src/icy_board/state/user_commands/message_reader.rs b/crates/icy_board_engine/src/icy_board/state/user_commands/message_reader.rs index c57cea50..3da668fd 100644 --- a/crates/icy_board_engine/src/icy_board/state/user_commands/message_reader.rs +++ b/crates/icy_board_engine/src/icy_board/state/user_commands/message_reader.rs @@ -186,7 +186,7 @@ impl MessageViewer { } impl IcyBoardState { - pub async fn read_msgs_from_base(&mut self, message_base: JamMessageBase) -> Res<()> { + pub async fn read_msgs_from_base(&mut self, mut message_base: JamMessageBase) -> Res<()> { let viewer = MessageViewer::load(&self.display_text)?; while !self.session.disp_options.abort_printout { @@ -212,7 +212,7 @@ impl IcyBoardState { } if let Ok(number) = text.parse::() { - self.read_message_number(&message_base, &viewer, number, None).await?; + self.read_message_number(&mut message_base, &viewer, number, None).await?; } } self.press_enter().await?; @@ -222,7 +222,7 @@ impl IcyBoardState { pub async fn read_message_number( &mut self, - message_base: &JamMessageBase, + message_base: &mut JamMessageBase, viewer: &MessageViewer, mut number: u32, matches: Option>, @@ -230,6 +230,22 @@ impl IcyBoardState { if number == 0 { return Ok(()); } + self.session.current_messagenumber = number; + self.session.high_msg_num = message_base.base_messagenumber(); + self.session.high_msg_num = message_base.base_messagenumber() + message_base.active_messages(); + + unsafe { + let crc = JamMessageBase::get_crc(&bstr::BString::new(self.session.user_name.as_mut_vec().clone())); + let mut opt = message_base + .find_last_read(crc, self.session.cur_user_id as u32)? + .unwrap_or(message_base.create_last_read(crc, self.session.cur_user_id as u32)?); + self.session.last_msg_read = opt.last_read_msg; + self.session.highest_msg_read = opt.high_read_msg; + + opt.last_read_msg = number; + opt.high_read_msg = opt.high_read_msg.max(number); + message_base.write_last_read(opt)?; + } loop { match message_base.read_header(number) { Ok(header) => { diff --git a/crates/icy_board_engine/src/icy_board/state/user_commands/pcb/d_download.rs b/crates/icy_board_engine/src/icy_board/state/user_commands/pcb/d_download.rs index 4c9ab31f..0ce1901a 100644 --- a/crates/icy_board_engine/src/icy_board/state/user_commands/pcb/d_download.rs +++ b/crates/icy_board_engine/src/icy_board/state/user_commands/pcb/d_download.rs @@ -144,7 +144,7 @@ impl IcyBoardState { self.display_text(IceText::BatchSend, display_flags::LFBEFORE).await?; self.board.lock().await.statistics.add_download(&state); - self.board.lock().await.save_statistics(); + self.board.lock().await.save_statistics()?; } Err(e) => { log::error!("Error while initiating file transfer with {:?} : {}", protocol, e); diff --git a/crates/icy_board_engine/src/icy_board/state/user_commands/pcb/ts_text_search.rs b/crates/icy_board_engine/src/icy_board/state/user_commands/pcb/ts_text_search.rs index eb5ad0de..35e0a5c5 100644 --- a/crates/icy_board_engine/src/icy_board/state/user_commands/pcb/ts_text_search.rs +++ b/crates/icy_board_engine/src/icy_board/state/user_commands/pcb/ts_text_search.rs @@ -45,7 +45,7 @@ impl IcyBoardState { let message_base_file = &self.session.current_conference.areas[area].filename; let msgbase_file_resolved = self.get_board().await.resolve_file(message_base_file); match JamMessageBase::open(&msgbase_file_resolved) { - Ok(message_base) => { + Ok(mut message_base) => { let msg_search_from = if let Some(token) = self.session.tokens.pop_front() { token } else { @@ -73,7 +73,7 @@ impl IcyBoardState { let txt = message_base.read_msg_text(&msg)?; let matches = get_matches(&txt, &search_text); if !matches.is_empty() { - self.read_message_number(&message_base, &viewer, start, Some(matches)).await?; + self.read_message_number(&mut message_base, &viewer, start, Some(matches)).await?; } } start += 1; diff --git a/crates/icy_board_engine/src/icy_board/user_base.rs b/crates/icy_board_engine/src/icy_board/user_base.rs index 79a0bb2b..016123f4 100644 --- a/crates/icy_board_engine/src/icy_board/user_base.rs +++ b/crates/icy_board_engine/src/icy_board/user_base.rs @@ -190,6 +190,10 @@ pub struct UserStats { #[serde(default)] #[serde(skip_serializing_if = "is_null_64")] pub total_doors_executed: u64, + + #[serde(default)] + #[serde(skip_serializing_if = "is_null_16")] + pub minutes_today: u16, } #[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] @@ -594,6 +598,7 @@ impl User { today_num_downloads: 0, today_num_uploads: 0, total_doors_executed: 0, + minutes_today: 0, }, } } diff --git a/crates/icy_board_engine/src/icy_board/xfer_protocols.rs b/crates/icy_board_engine/src/icy_board/xfer_protocols.rs index 2415eba2..b74b6db3 100644 --- a/crates/icy_board_engine/src/icy_board/xfer_protocols.rs +++ b/crates/icy_board_engine/src/icy_board/xfer_protocols.rs @@ -64,6 +64,15 @@ impl SupportedProtocols { fs::write(output, res)?; Ok(()) } + + pub fn find_protocol(&self, char_code: &str) -> Option<&Protocol> { + for p in &self.protocols { + if p.char_code == char_code { + return Some(p); + } + } + None + } } impl Deref for SupportedProtocols { diff --git a/crates/icy_board_engine/src/vm/expressions/predefined_functions.rs b/crates/icy_board_engine/src/vm/expressions/predefined_functions.rs index a9f07aa8..1247626b 100644 --- a/crates/icy_board_engine/src/vm/expressions/predefined_functions.rs +++ b/crates/icy_board_engine/src/vm/expressions/predefined_functions.rs @@ -15,7 +15,7 @@ use crate::icy_board::user_base::Password; use crate::parser::CONFERENCE_ID; use crate::vm::{TerminalTarget, VirtualMachine}; use crate::Res; -use chrono::Local; +use chrono::Utc; use icy_engine::{update_crc32, Position, TextPane}; use radix_fmt::radix; use rand::Rng; // 0.8.5 @@ -663,7 +663,7 @@ pub async fn minleft(vm: &mut VirtualMachine<'_>, args: &[PPEExpr]) -> Res, args: &[PPEExpr]) -> Res { - let min = (Local::now() - vm.icy_board_state.session.login_date).num_minutes(); + let min = (Utc::now() - vm.icy_board_state.session.login_date).num_minutes(); Ok(VariableValue::new_int(min as i32)) } diff --git a/crates/jamjam/src/jam/last_read_storage.rs b/crates/jamjam/src/jam/last_read_storage.rs index efe6479b..53d63a36 100644 --- a/crates/jamjam/src/jam/last_read_storage.rs +++ b/crates/jamjam/src/jam/last_read_storage.rs @@ -31,10 +31,12 @@ impl JamLastReadStorage { } pub fn write(&self, file: &mut File) -> crate::Result<()> { - file.write_all(&self.user_crc.to_le_bytes())?; - file.write_all(&self.user_id.to_le_bytes())?; - file.write_all(&self.last_read_msg.to_le_bytes())?; - file.write_all(&self.high_read_msg.to_le_bytes())?; + let mut record = Vec::new(); + record.extend_from_slice(&self.user_id.to_le_bytes()); + record.extend_from_slice(&self.user_crc.to_le_bytes()); + record.extend_from_slice(&self.last_read_msg.to_le_bytes()); + record.extend_from_slice(&self.high_read_msg.to_le_bytes()); + file.write_all(&record)?; Ok(()) } } diff --git a/crates/jamjam/src/jam/mod.rs b/crates/jamjam/src/jam/mod.rs index 29cb07af..abe8ba41 100644 --- a/crates/jamjam/src/jam/mod.rs +++ b/crates/jamjam/src/jam/mod.rs @@ -1,5 +1,6 @@ use std::fs::{self, OpenOptions}; use std::io::{BufReader, BufWriter, Seek, SeekFrom, Write}; +use std::os::unix::fs::MetadataExt; use std::path::{Path, PathBuf}; use std::sync::atomic::AtomicBool; use std::time::{SystemTime, UNIX_EPOCH}; @@ -361,6 +362,28 @@ impl JamMessageBase { Ok(res) } + pub fn write_last_read(&self, opt: JamLastReadStorage) -> crate::Result<()> { + let last_read_file_name = self.file_name.with_extension(extensions::LASTREAD_INFO); + let mut file = fs::OpenOptions::new().write(true).open(last_read_file_name)?; + const LEN: u64 = 16; + file.seek(SeekFrom::Start(self.last_read_record as u64 * LEN))?; + opt.write(&mut file)?; + Ok(()) + } + + pub fn create_last_read(&mut self, user_name_crc: u32, id: u32) -> crate::Result { + let mut opt = JamLastReadStorage::default(); + opt.user_crc = user_name_crc; + opt.user_id = id; + let last_read_file_name = self.file_name.with_extension(extensions::LASTREAD_INFO); + + let mut file = fs::OpenOptions::new().append(true).open(last_read_file_name)?; + const LEN: u64 = 16; + self.last_read_record = (file.metadata().unwrap().size() / LEN) as i32; + opt.write(&mut file)?; + Ok(opt) + } + pub fn find_last_read(&mut self, user_name_crc: u32, id: u32) -> crate::Result> { let last_read_file_name = self.file_name.with_extension(extensions::LASTREAD_INFO); let file = File::open(last_read_file_name)?;