diff --git a/data/src/client.rs b/data/src/client.rs index 03f4e03a..995bf3bb 100644 --- a/data/src/client.rs +++ b/data/src/client.rs @@ -7,7 +7,7 @@ use std::fmt; use std::time::{Duration, Instant}; use crate::isupport::{ChatHistorySubcommand, MessageReference}; -use crate::message::{server_time, source}; +use crate::message::{message_id, server_time, source}; use crate::time::Posix; use crate::user::{Nick, NickRef}; use crate::{config, dcc, isupport, message, mode, Buffer, Server, User}; @@ -62,6 +62,12 @@ pub enum Broadcast { }, } +#[derive(Debug, Clone)] +pub enum HistoryRequest { + Recent(DateTime), + Older, +} + #[derive(Debug)] pub enum Event { Single(message::Encoded, Nick), @@ -69,17 +75,9 @@ pub enum Event { Broadcast(Broadcast), Notification(message::Encoded, Nick, Notification), FileTransferRequest(file_transfer::ReceiveRequest), - ChatHistoryCommand( - ChatHistorySubcommand, - String, - Vec, - ), - ChatHistoryBatchFinished( - ChatHistorySubcommand, - String, - isupport::MessageReference, - usize, - ), + ChatHistoryRequest(ChatHistorySubcommand), + ChatHistoryRequestFromHistory(HistoryRequest, String, Vec), + ChatHistoryRequestReceived(ChatHistorySubcommand, usize), } pub struct Client { @@ -99,7 +97,7 @@ pub struct Client { supports_labels: bool, supports_away_notify: bool, supports_chathistory: bool, - chathistory_requests: HashMap, + chathistory_requests: HashMap, chathistory_exhausted: HashMap, highlight_blackout: HighlightBlackout, registration_required_channels: Vec, @@ -270,50 +268,73 @@ impl Client { } else { if self.supports_chathistory { if let Some(chathistory_target) = &finished.chathistory_target { - if let Some(ChatHistoryRequest { - subcommand, - message_reference, - limit, - }) = self.chathistory_requests.get(chathistory_target) + if let Some(subcommand) = + self.chathistory_requests.get(chathistory_target) { - if matches!(subcommand, ChatHistorySubcommand::Before) { + if let ChatHistorySubcommand::Before(_, _, limit) = + subcommand + { self.chathistory_exhausted.insert( chathistory_target.to_string(), finished.events.len() < *limit as usize, ); } - let continue_request = if matches!( - subcommand, - ChatHistorySubcommand::Latest(_) - ) { - finished.events.reverse(); - - if matches!( + let continue_chathistory = match subcommand { + ChatHistorySubcommand::Latest( + target, message_reference, - MessageReference::None - ) { - false - } else { - finished.events.len() == *limit as usize + limit, + ) => { + if matches!( + message_reference, + MessageReference::None + ) { + None + } else if finished.events.len() + == *limit as usize + { + continue_chathistory_between( + target, + &finished.events, + message_reference, + self.chathistory_limit(), + ) + } else { + None + } + } + ChatHistorySubcommand::Before(_, _, _) => None, + ChatHistorySubcommand::Between( + target, + _, + end_message_reference, + limit, + ) => { + if finished.events.len() == *limit as usize { + continue_chathistory_between( + target, + &finished.events, + end_message_reference, + self.chathistory_limit(), + ) + } else { + None + } } - } else { - false }; - finished.events.push(Event::ChatHistoryBatchFinished( - subcommand.clone(), - chathistory_target.to_string(), - message_reference.clone(), - finished.events.len(), - )); - - if continue_request { - finished.events.push(Event::ChatHistoryCommand( + finished.events.push( + Event::ChatHistoryRequestReceived( subcommand.clone(), - chathistory_target.to_string(), - self.chathistory_message_reference_types(), - )); + finished.events.len(), + ), + ); + + if let Some(subcommand) = continue_chathistory { + finished + .events + .push(Event::ChatHistoryRequest(subcommand)); } } } @@ -329,14 +350,16 @@ impl Client { return None; } _ if batch_tag.is_some() => { - let events = if let Some((ChatHistoryRequest { .. }, target)) = batch_tag + let events = if let Some(target) = batch_tag .as_ref() .and_then(|batch| self.batches.get(batch)) .and_then(|batch| batch.chathistory_target.clone()) .and_then(|target| { - self.chathistory_requests - .get(&target) - .map(|chathistory_request| (chathistory_request, target)) + if self.chathistory_requests.contains_key(&target) { + Some(target) + } else { + None + } }) { if Some(User::from(Nick::from("HistServ"))) == message.user() { // HistServ provides event-playback without event-playback @@ -870,8 +893,8 @@ impl Client { log::debug!("[{}] {channel} - WHO requested", self.server); if self.supports_chathistory { - return Some(vec![Event::ChatHistoryCommand( - ChatHistorySubcommand::Latest(server_time(&message)), + return Some(vec![Event::ChatHistoryRequestFromHistory( + HistoryRequest::Recent(server_time(&message)), channel.clone(), self.chathistory_message_reference_types(), )]); @@ -1178,106 +1201,142 @@ impl Client { } } - pub fn chathistory_request(&self, target: &str) -> Option { + pub fn chathistory_request(&self, target: &str) -> Option { self.chathistory_requests.get(target).cloned() } - pub fn send_chathistory_request( - &mut self, - subcommand: ChatHistorySubcommand, - target: &str, - message_reference: MessageReference, - ) { - let limit = self.chathistory_limit(); - - if self.supports_chathistory && !self.chathistory_requests.contains_key(target) { - self.chathistory_requests.insert( - target.to_string(), - ChatHistoryRequest { - subcommand: subcommand.clone(), - message_reference: message_reference.clone(), - limit, - }, - ); + pub fn send_chathistory_request(&mut self, subcommand: ChatHistorySubcommand) { + if self.supports_chathistory { + if let Some(target) = subcommand.target() { + if self.chathistory_requests.contains_key(target) { + return; + } else { + self.chathistory_requests + .insert(target.to_string(), subcommand.clone()); + } + } - if matches!(message_reference, MessageReference::None) { - let _ = self.handle.try_send(command!( - "CHATHISTORY", - "LATEST", - target, - message_reference.to_string(), - limit.to_string() - )); + match subcommand { + ChatHistorySubcommand::Latest(target, message_reference, limit) => { + let command_message_reference = match message_reference { + MessageReference::Timestamp(server_time) => { + TimeDelta::try_seconds(isupport::CHATHISTORY_FUZZ_SECONDS) + .and_then(|time_delta| server_time.checked_sub_signed(time_delta)) + .map_or(message_reference, |fuzzed_server_time| { + MessageReference::Timestamp(fuzzed_server_time) + }) + } + _ => message_reference, + }; - log::debug!( - "[{}] requesting {limit} latest messages in {target} since {}", - self.server, - message_reference, - ); - } else { - match subcommand { - ChatHistorySubcommand::Latest(_) => { - let command_message_reference = match message_reference { - MessageReference::Timestamp(server_time) => { - if let Some(fuzzed_server_time) = + log::debug!( + "[{}] requesting {limit} latest messages in {target} since {}", + self.server, + command_message_reference, + ); + + let _ = self.handle.try_send(command!( + "CHATHISTORY", + "LATEST", + target, + command_message_reference.to_string(), + limit.to_string() + )); + } + ChatHistorySubcommand::Before(target, message_reference, limit) => { + let command_message_reference = match message_reference { + MessageReference::Timestamp(server_time) => { + TimeDelta::try_seconds(isupport::CHATHISTORY_FUZZ_SECONDS) + .and_then(|time_delta| server_time.checked_add_signed(time_delta)) + .map_or(message_reference, |fuzzed_server_time| { + MessageReference::Timestamp(fuzzed_server_time) + }) + } + _ => message_reference, + }; + + log::debug!( + "[{}] requesting {limit} messages in {target} before {}", + self.server, + command_message_reference, + ); + + let _ = self.handle.try_send(command!( + "CHATHISTORY", + "BEFORE", + target, + command_message_reference.to_string(), + limit.to_string() + )); + } + ChatHistorySubcommand::Between( + target, + start_message_reference, + end_message_reference, + limit, + ) => { + let (command_start_message_reference, command_end_message_reference) = match ( + start_message_reference.clone(), + end_message_reference.clone(), + ) { + ( + MessageReference::Timestamp(start_server_time), + MessageReference::Timestamp(end_server_time), + ) => { + if start_server_time < end_server_time { + ( TimeDelta::try_seconds(isupport::CHATHISTORY_FUZZ_SECONDS) .and_then(|time_delta| { - server_time.checked_sub_signed(time_delta) + start_server_time.checked_sub_signed(time_delta) }) - { - MessageReference::Timestamp(fuzzed_server_time) - } else { - message_reference - } - } - _ => message_reference, - }; - - log::debug!( - "[{}] requesting {limit} latest messages in {target} since {}", - self.server, - command_message_reference, - ); - - let _ = self.handle.try_send(command!( - "CHATHISTORY", - "AFTER", - target, - command_message_reference.to_string(), - limit.to_string() - )); - } - ChatHistorySubcommand::Before => { - let command_message_reference = match message_reference { - MessageReference::Timestamp(reference_timestamp) => { - if let Some(fuzzed_reference_timestamp) = + .map_or(start_message_reference, |fuzzed_server_time| { + MessageReference::Timestamp(fuzzed_server_time) + }), TimeDelta::try_seconds(isupport::CHATHISTORY_FUZZ_SECONDS) .and_then(|time_delta| { - reference_timestamp.checked_add_signed(time_delta) + end_server_time.checked_add_signed(time_delta) }) - { - MessageReference::Timestamp(fuzzed_reference_timestamp) - } else { - message_reference - } + .map_or(end_message_reference, |fuzzed_server_time| { + MessageReference::Timestamp(fuzzed_server_time) + }), + ) + } else { + ( + TimeDelta::try_seconds(isupport::CHATHISTORY_FUZZ_SECONDS) + .and_then(|time_delta| { + start_server_time.checked_add_signed(time_delta) + }) + .map_or(start_message_reference, |fuzzed_server_time| { + MessageReference::Timestamp(fuzzed_server_time) + }), + TimeDelta::try_seconds(isupport::CHATHISTORY_FUZZ_SECONDS) + .and_then(|time_delta| { + end_server_time.checked_sub_signed(time_delta) + }) + .map_or(end_message_reference, |fuzzed_server_time| { + MessageReference::Timestamp(fuzzed_server_time) + }), + ) } - _ => message_reference, - }; + } + _ => (start_message_reference, end_message_reference), + }; - log::debug!( - "[{}] requesting {limit} messages in {target} before {}", - self.server, - command_message_reference, - ); - - let _ = self.handle.try_send(command!( - "CHATHISTORY", - "BEFORE", - target, - command_message_reference.to_string(), - limit.to_string() - )); - } + log::debug!( + "[{}] requesting {limit} messages in {target} between {} and {}", + self.server, + command_start_message_reference, + command_end_message_reference, + ); + + let _ = self.handle.try_send(command!( + "CHATHISTORY", + "BETWEEN", + target, + command_start_message_reference.to_string(), + command_end_message_reference.to_string(), + limit.to_string() + )); } } } @@ -1390,6 +1449,34 @@ impl Client { const CLIENT_CHATHISTORY_LIMIT: u16 = 500; +fn continue_chathistory_between( + target: &str, + events: &[Event], + end_message_reference: &MessageReference, + limit: u16, +) -> Option { + let start_message_reference = events.first().and_then(|first_event| match first_event { + Event::Single(message, _) | Event::WithTarget(message, _, _) => match end_message_reference + { + MessageReference::MessageId(_) => message_id(message).map(MessageReference::MessageId), + MessageReference::Timestamp(_) => { + Some(MessageReference::Timestamp(server_time(message))) + } + MessageReference::None => None, + }, + _ => None, + }); + + start_message_reference.map(|start_message_reference| { + ChatHistorySubcommand::Between( + target.to_string(), + start_message_reference, + end_message_reference.clone(), + limit, + ) + }) +} + #[derive(Debug)] enum HighlightBlackout { Blackout(Instant), @@ -1531,20 +1618,14 @@ impl Map { &self, server: &Server, target: &str, - ) -> Option { + ) -> Option { self.client(server) .and_then(|client| client.chathistory_request(target)) } - pub fn send_chathistory_request( - &mut self, - subcommand: ChatHistorySubcommand, - server: &Server, - target: &str, - message_reference: MessageReference, - ) { + pub fn send_chathistory_request(&mut self, server: &Server, subcommand: ChatHistorySubcommand) { if let Some(client) = self.client_mut(server) { - client.send_chathistory_request(subcommand, target, message_reference); + client.send_chathistory_request(subcommand); } } @@ -1737,13 +1818,6 @@ pub enum WhoStatus { Done(Instant), } -#[derive(Debug, Clone)] -pub struct ChatHistoryRequest { - pub subcommand: ChatHistorySubcommand, - pub message_reference: MessageReference, - pub limit: u16, -} - /// Group channels together into as few JOIN messages as possible fn group_joins<'a>( channels: &'a [String], diff --git a/data/src/history.rs b/data/src/history.rs index b75db5ed..317c522c 100644 --- a/data/src/history.rs +++ b/data/src/history.rs @@ -1,4 +1,4 @@ -use chrono::{DateTime, Utc}; +use chrono::{DateTime, TimeDelta, Utc}; use std::path::PathBuf; use std::time::Duration; use std::{fmt, io}; @@ -345,8 +345,14 @@ fn is_referenceable_message( message_reference_type: Option<&isupport::MessageReferenceType>, ) -> bool { if matches!(message.target.source(), message::Source::Internal(_)) { - false - } else if matches!( + return false; + } else if let message::Source::Server(Some(source)) = message.target.source() { + if matches!(source.kind(), message::source::server::Kind::ReplyTopic) { + return false; + } + } + + if matches!( message_reference_type, Some(isupport::MessageReferenceType::MessageId) ) { @@ -363,9 +369,6 @@ fn is_referenceable_message( /// of the incoming message. Either message IDs match, or server times /// have an exact match + target & content. fn insert_message(messages: &mut Vec, message: Message) -> bool { - #[allow(deprecated)] - const FUZZ_DURATION: chrono::Duration = chrono::Duration::seconds(1); - let message_triggers_unread = message.triggers_unread(); if messages.is_empty() { @@ -374,8 +377,12 @@ fn insert_message(messages: &mut Vec, message: Message) -> bool { return message_triggers_unread; } - let start = message.server_time - FUZZ_DURATION; - let end = message.server_time + FUZZ_DURATION; + let start = TimeDelta::try_seconds(isupport::CHATHISTORY_FUZZ_SECONDS) + .and_then(|time_delta| message.server_time.checked_sub_signed(time_delta)) + .unwrap_or(message.server_time); + let end = TimeDelta::try_seconds(isupport::CHATHISTORY_FUZZ_SECONDS) + .and_then(|time_delta| message.server_time.checked_add_signed(time_delta)) + .unwrap_or(message.server_time); let start_index = match messages.binary_search_by(|stored| stored.server_time.cmp(&start)) { Ok(match_index) => match_index, diff --git a/data/src/isupport.rs b/data/src/isupport.rs index 366dc03e..406c87eb 100644 --- a/data/src/isupport.rs +++ b/data/src/isupport.rs @@ -631,8 +631,19 @@ pub struct ChannelMode { #[derive(Clone, Debug, PartialEq)] pub enum ChatHistorySubcommand { - Latest(DateTime), - Before, + Latest(String, MessageReference, u16), + Before(String, MessageReference, u16), + Between(String, MessageReference, MessageReference, u16), +} + +impl ChatHistorySubcommand { + pub fn target(&self) -> Option<&str> { + match self { + ChatHistorySubcommand::Latest(target, _, _) + | ChatHistorySubcommand::Before(target, _, _) + | ChatHistorySubcommand::Between(target, _, _, _) => Some(target), + } + } } #[derive(Clone, Debug)] @@ -648,7 +659,7 @@ pub struct CommandTargetLimit { pub limit: Option, } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum MessageReference { Timestamp(DateTime), MessageId(String), diff --git a/src/main.rs b/src/main.rs index 524214ed..8acaf420 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,7 @@ use std::time::{Duration, Instant}; use chrono::Utc; use data::config::{self, Config}; -use data::isupport::ChatHistorySubcommand; +use data::isupport::{ChatHistorySubcommand, MessageReference}; use data::version::Version; use data::{environment, server, version, User}; use iced::advanced::Application; @@ -529,85 +529,117 @@ impl Application for Halloy { commands.push(command.map(Message::Dashboard)); } } - data::client::Event::ChatHistoryCommand( - subcommand, + data::client::Event::ChatHistoryRequest(subcommand) => { + self.clients.send_chathistory_request( + &server, + subcommand, + ); + } + data::client::Event::ChatHistoryRequestFromHistory( + history_request, target, - message_reference_types + message_reference_types, ) => { - match subcommand { - ChatHistorySubcommand::Latest(join_server_time) => { + let subcommand = match history_request { + data::client::HistoryRequest::Recent( + join_server_time, + ) => { dashboard.load_history_now(server.clone(), &target); - let latest_message_reference = dashboard.get_latest_message_reference( - &server, - &target, - &message_reference_types, - join_server_time, - ); - - self.clients.send_chathistory_request( - subcommand, - &server, - &target, - latest_message_reference, - ); + ChatHistorySubcommand::Latest( + target.clone(), + dashboard.get_latest_message_reference( + &server, + &target, + &message_reference_types, + join_server_time, + ), + self.clients.get_server_chathistory_limit(&server), + ) } - ChatHistorySubcommand::Before => { - let oldest_message_reference = dashboard.get_oldest_message_reference( - &server, - &target, - &message_reference_types, - ); + data::client::HistoryRequest::Older => { + let message_reference = dashboard.get_oldest_message_reference( + &server, + &target, + &message_reference_types, + ); - self.clients.send_chathistory_request( - subcommand, - &server, - &target, - oldest_message_reference, - ); + if matches!(message_reference, MessageReference::None) { + ChatHistorySubcommand::Latest( + target.clone(), + message_reference, + self.clients.get_server_chathistory_limit(&server), + ) + } else { + ChatHistorySubcommand::Before( + target.clone(), + message_reference, + self.clients.get_server_chathistory_limit(&server), + ) + } } - } + }; + + self.clients.send_chathistory_request( + &server, + subcommand, + ); } - data::client::Event::ChatHistoryBatchFinished( + data::client::Event::ChatHistoryRequestReceived( subcommand, - target, - message_reference, - batch_len + batch_len, ) => { - match subcommand { - ChatHistorySubcommand::Latest(_) => { + match &subcommand { + ChatHistorySubcommand::Latest(target, message_reference, _) => { log::debug!( "[{}] received latest {} messages in {} since {}", server, batch_len, target, - message_reference + message_reference, ); } - ChatHistorySubcommand::Before => { + ChatHistorySubcommand::Before(target, message_reference, _) => { log::debug!( "[{}] received {} messages in {} before {}", server, batch_len, target, - message_reference + message_reference, + ); + } + ChatHistorySubcommand::Between( + target, + start_message_reference, + end_message_reference, + _, + ) => { + log::debug!( + "[{}] received {} messages in {} between {} and {}", + server, + batch_len, + target, + start_message_reference, + end_message_reference, ); } } - if !dashboard.is_open(server.clone(), &target) { - dashboard.make_history_partial_now( - server.clone(), - &target, - if matches!(subcommand, ChatHistorySubcommand::Latest(_)) { - Some(message_reference) - } else { - None - }, - ); - } + if let Some(target) = subcommand.target() { + if !dashboard.is_open(server.clone(), target) { + dashboard.make_history_partial_now( + server.clone(), + target, + if let ChatHistorySubcommand::Latest(_, message_reference, _) = &subcommand { + Some(message_reference.clone()) + } else { + None + }, + ); + } - self.clients.clear_chathistory_request(&server, &target); + self.clients.clear_chathistory_request(&server, target); + } } } } diff --git a/src/screen/dashboard.rs b/src/screen/dashboard.rs index 1e136cb3..36b918d3 100644 --- a/src/screen/dashboard.rs +++ b/src/screen/dashboard.rs @@ -1018,10 +1018,12 @@ impl Dashboard { self.get_oldest_message_reference(server, &target, &message_reference_types); clients.send_chathistory_request( - ChatHistorySubcommand::Before, server, - &target, - oldest_message_reference, + ChatHistorySubcommand::Before( + target.clone(), + oldest_message_reference, + clients.get_server_chathistory_limit(server), + ), ); } }