use chrono::{DateTime, Utc};
use std::collections::HashMap;
use thiserror::Error;
use crate::protocol::{Message, Reply};
#[derive(Error, Debug)]
pub enum ReadMarkerError {
#[error("Need more parameters")]
NeedMoreParams,
#[error("Invalid parameters")]
InvalidParams,
#[error("Internal error")]
InternalError,
#[error("Target not found")]
TargetNotFound,
#[error("Permission denied")]
PermissionDenied,
}
#[derive(Debug, Clone)]
pub struct ReadMarker {
pub user_id: u64,
pub target: String,
pub timestamp: Option<DateTime<Utc>>,
pub updated_at: DateTime<Utc>,
}
impl ReadMarker {
pub fn new(user_id: u64, target: String, timestamp: Option<DateTime<Utc>>) -> Self {
Self {
user_id,
target,
timestamp,
updated_at: Utc::now(),
}
}
pub fn update_timestamp(&mut self, timestamp: Option<DateTime<Utc>>) -> bool {
match (&self.timestamp, ×tamp) {
(None, _) => {
self.timestamp = timestamp;
self.updated_at = Utc::now();
true
}
(Some(_), None) => false, (Some(current), Some(new)) => {
if new >= current {
self.timestamp = timestamp;
self.updated_at = Utc::now();
true
} else {
false
}
}
}
}
pub fn timestamp_string(&self) -> String {
match &self.timestamp {
Some(ts) => ts.to_rfc3339(),
None => "*".to_string(),
}
}
}
pub struct ReadMarkerManager {
markers: HashMap<(u64, String), ReadMarker>,
}
impl ReadMarkerManager {
pub fn new() -> Self {
Self {
markers: HashMap::new(),
}
}
pub fn set_read_marker(
&mut self,
user_id: u64,
target: String,
timestamp: Option<DateTime<Utc>>,
) -> Result<DateTime<Utc>, ReadMarkerError> {
let key = (user_id, target.clone());
match self.markers.get_mut(&key) {
Some(marker) => {
if marker.update_timestamp(timestamp) {
Ok(marker.updated_at)
} else {
Err(ReadMarkerError::InvalidParams)
}
}
None => {
let marker = ReadMarker::new(user_id, target, timestamp);
let server_timestamp = marker.updated_at;
self.markers.insert(key, marker);
Ok(server_timestamp)
}
}
}
pub fn get_read_marker(&self, user_id: u64, target: &str) -> Option<&ReadMarker> {
self.markers.get(&(user_id, target.to_string()))
}
pub fn remove_user_markers(&mut self, user_id: u64) {
self.markers.retain(|(uid, _), _| *uid != user_id);
}
pub fn remove_target_markers(&mut self, target: &str) {
self.markers.retain(|(_, tgt), _| tgt != target);
}
pub fn get_user_markers(&self, user_id: u64) -> Vec<&ReadMarker> {
self.markers
.iter()
.filter(|((uid, _), _)| *uid == user_id)
.map(|(_, marker)| marker)
.collect()
}
}
pub struct ReadMarkerProcessor;
impl ReadMarkerProcessor {
pub fn parse_markread_command(params: &[String]) -> Result<MarkReadCommand, ReadMarkerError> {
if params.is_empty() {
return Err(ReadMarkerError::NeedMoreParams);
}
let target = params[0].clone();
if target.is_empty() {
return Err(ReadMarkerError::InvalidParams);
}
match params.len() {
1 => {
Ok(MarkReadCommand::Get { target })
}
2 => {
let timestamp_str = ¶ms[1];
if timestamp_str == "*" {
Ok(MarkReadCommand::Set {
target,
timestamp: None
})
} else {
let timestamp = DateTime::parse_from_rfc3339(timestamp_str)
.map_err(|_| ReadMarkerError::InvalidParams)?
.with_timezone(&Utc);
Ok(MarkReadCommand::Set {
target,
timestamp: Some(timestamp)
})
}
}
_ => Err(ReadMarkerError::InvalidParams),
}
}
pub fn create_markread_response(
target: String,
timestamp: Option<DateTime<Utc>>,
server_name: &str,
) -> Message {
let timestamp_str = match timestamp {
Some(ts) => ts.to_rfc3339(),
None => "*".to_string(),
};
Message::new("MARKREAD")
.with_prefix(server_name)
.with_params(vec![target, timestamp_str])
}
pub fn create_markread_fail(
error: ReadMarkerError,
context: &str,
) -> Message {
let error_code = match error {
ReadMarkerError::NeedMoreParams => "NEED_MORE_PARAMS",
ReadMarkerError::InvalidParams => "INVALID_PARAMS",
ReadMarkerError::InternalError => "INTERNAL_ERROR",
ReadMarkerError::TargetNotFound => "INVALID_PARAMS",
ReadMarkerError::PermissionDenied => "INVALID_PARAMS",
};
Message::new("FAIL")
.with_params(vec!["MARKREAD".to_string(), error_code.to_string(), context.to_string(), error.to_string()])
}
pub fn validate_target(target: &str, user_id: u64, server_state: &crate::state::ServerState) -> bool {
if target.starts_with('#') || target.starts_with('&') {
if let Some(channel) = server_state.channels.get(target) {
return channel.is_member(user_id);
}
return false;
}
if let Some(_) = server_state.nicknames.get(&target.to_lowercase()) {
return true;
}
false
}
pub fn should_send_on_join() -> bool {
true
}
}
#[derive(Debug, Clone)]
pub enum MarkReadCommand {
Get { target: String },
Set { target: String, timestamp: Option<DateTime<Utc>> },
}
pub fn validate_timestamp_format(timestamp: &str) -> Result<DateTime<Utc>, ReadMarkerError> {
if timestamp == "*" {
return Err(ReadMarkerError::InvalidParams);
}
DateTime::parse_from_rfc3339(timestamp)
.map_err(|_| ReadMarkerError::InvalidParams)
.map(|dt| dt.with_timezone(&Utc))
}
pub fn timestamp_from_message_time_tag(tags: &std::collections::HashMap<String, Option<String>>) -> Option<DateTime<Utc>> {
tags.get("time")
.and_then(|opt| opt.as_ref())
.and_then(|time_str| DateTime::parse_from_rfc3339(time_str).ok())
.map(|dt| dt.with_timezone(&Utc))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_markread_command_parsing() {
let params = vec!["#channel".to_string()];
let cmd = ReadMarkerProcessor::parse_markread_command(¶ms).unwrap();
match cmd {
MarkReadCommand::Get { target } => assert_eq!(target, "#channel"),
_ => panic!("Expected Get command"),
}
let params = vec![
"#channel".to_string(),
"2024-01-01T12:00:00Z".to_string(),
];
let cmd = ReadMarkerProcessor::parse_markread_command(¶ms).unwrap();
match cmd {
MarkReadCommand::Set { target, timestamp } => {
assert_eq!(target, "#channel");
assert!(timestamp.is_some());
}
_ => panic!("Expected Set command"),
}
let params = vec!["#channel".to_string(), "*".to_string()];
let cmd = ReadMarkerProcessor::parse_markread_command(¶ms).unwrap();
match cmd {
MarkReadCommand::Set { target, timestamp } => {
assert_eq!(target, "#channel");
assert!(timestamp.is_none());
}
_ => panic!("Expected Set command"),
}
}
#[test]
fn test_read_marker_timestamp_updates() {
let mut marker = ReadMarker::new(
1,
"#channel".to_string(),
Some(Utc::now())
);
let future_time = Utc::now() + chrono::Duration::hours(1);
let past_time = Utc::now() - chrono::Duration::hours(1);
assert!(marker.update_timestamp(Some(future_time)));
assert!(!marker.update_timestamp(Some(past_time)));
}
#[test]
fn test_read_marker_manager() {
let mut manager = ReadMarkerManager::new();
let now = Utc::now();
let server_ts = manager.set_read_marker(
1,
"#channel".to_string(),
Some(now)
).unwrap();
let marker = manager.get_read_marker(1, "#channel").unwrap();
assert_eq!(marker.timestamp, Some(now));
assert_eq!(marker.updated_at, server_ts);
let future = now + chrono::Duration::minutes(5);
let updated_ts = manager.set_read_marker(
1,
"#channel".to_string(),
Some(future)
).unwrap();
let updated_marker = manager.get_read_marker(1, "#channel").unwrap();
assert_eq!(updated_marker.timestamp, Some(future));
assert!(updated_marker.updated_at > server_ts);
}
}