diff --git a/src/db/mod.rs b/src/db/mod.rs index 42bb58f..ce145ad 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -6,10 +6,7 @@ pub use migrations::run as setup_db; use crate::{ error::Error, - logs::{ - schema::{ChannelLogDate, UserLogDate}, - stream::LogsStream, - }, + logs::{schema::LogRangeParams, stream::LogsStream}, web::schema::AvailableLogDate, Result, }; @@ -21,19 +18,25 @@ use tracing::info; pub async fn read_channel( db: &Client, channel_id: &str, - log_date: ChannelLogDate, - reverse: bool, - limit: Option, - offset: Option, + params: &LogRangeParams, ) -> Result { - let suffix = if reverse { "DESC" } else { "ASC" }; - let mut query = format!("SELECT raw FROM message WHERE channel_id = ? AND toStartOfDay(timestamp) = ? ORDER BY timestamp {suffix}"); - apply_limit_offset(&mut query, limit, offset); + let suffix = if params.logs_params.reverse { + "DESC" + } else { + "ASC" + }; + let mut query = format!("SELECT raw FROM message WHERE channel_id = ? AND timestamp >= ? AND timestamp < ? ORDER BY timestamp {suffix}"); + apply_limit_offset( + &mut query, + params.logs_params.limit, + params.logs_params.offset, + ); let cursor = db .query(&query) .bind(channel_id) - .bind(log_date.to_string()) + .bind(params.from.timestamp_millis() as f64 / 1000.0) + .bind(params.to.timestamp_millis() as f64 / 1000.0) .fetch()?; LogsStream::new_cursor(cursor).await } @@ -42,22 +45,27 @@ pub async fn read_user( db: &Client, channel_id: &str, user_id: &str, - log_date: UserLogDate, - reverse: bool, - limit: Option, - offset: Option, + params: &LogRangeParams, ) -> Result { - let suffix = if reverse { "DESC" } else { "ASC" }; - let mut query = format!("SELECT raw FROM message WHERE channel_id = ? AND user_id = ? AND toStartOfMonth(timestamp) = ? ORDER BY timestamp {suffix}"); - apply_limit_offset(&mut query, limit, offset); + let suffix = if params.logs_params.reverse { + "DESC" + } else { + "ASC" + }; + let mut query = format!("SELECT raw FROM message WHERE channel_id = ? AND user_id = ? AND timestamp >= ? AND timestamp < ? ORDER BY timestamp {suffix}"); + apply_limit_offset( + &mut query, + params.logs_params.limit, + params.logs_params.offset, + ); let cursor = db .query(&query) .bind(channel_id) .bind(user_id) - .bind(format!("{}-{:0>2}-1", log_date.year, log_date.month)) + .bind(params.from.timestamp_millis() as f64 / 1000.0) + .bind(params.to.timestamp_millis() as f64 / 1000.0) .fetch()?; - LogsStream::new_cursor(cursor).await } diff --git a/src/logs/schema/mod.rs b/src/logs/schema/mod.rs index 5c4c5a6..38dad9e 100644 --- a/src/logs/schema/mod.rs +++ b/src/logs/schema/mod.rs @@ -1,41 +1,21 @@ pub mod message; -use chrono::{Datelike, NaiveDate, Utc}; +use chrono::{DateTime, Utc}; use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::fmt::Display; +use serde::Deserialize; -#[derive(Serialize, Deserialize, Debug, JsonSchema, Clone, Copy)] -pub struct ChannelLogDate { - pub year: u32, - pub month: u32, - pub day: u32, -} +use crate::web::schema::LogsParams; -impl ChannelLogDate { - pub fn is_today(&self) -> bool { - Some(Utc::now().date_naive()) - == NaiveDate::from_ymd_opt(self.year as i32, self.month, self.day) - } -} - -impl Display for ChannelLogDate { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}-{:0>2}-{:0>2}", self.year, self.month, self.day) - } -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy)] -pub struct UserLogDate { - pub year: u32, - pub month: u32, -} - -impl UserLogDate { - pub fn is_current_month(&self) -> bool { - let current = Utc::now().date_naive(); - current.year() as u32 == self.year && current.month() == self.month - } +#[derive(Deserialize, JsonSchema)] +pub struct LogRangeParams { + #[schemars(with = "String")] + /// RFC 3339 start date + pub from: DateTime, + #[schemars(with = "String")] + /// RFC 3339 end date + pub to: DateTime, + #[serde(flatten)] + pub logs_params: LogsParams, } #[derive(Deserialize)] diff --git a/src/web/handlers.rs b/src/web/handlers.rs index cd3220f..d150309 100644 --- a/src/web/handlers.rs +++ b/src/web/handlers.rs @@ -1,8 +1,9 @@ use super::{ responders::logs::LogsResponse, schema::{ - AvailableLogs, AvailableLogsParams, Channel, ChannelIdType, ChannelLogsPath, ChannelParam, - ChannelsList, LogsParams, LogsPathChannel, UserLogPathParams, UserLogsPath, UserParam, + AvailableLogs, AvailableLogsParams, Channel, ChannelIdType, ChannelLogsByDatePath, + ChannelParam, ChannelsList, LogsParams, LogsPathChannel, UserLogPathParams, UserLogsPath, + UserParam, }, }; use crate::{ @@ -12,19 +13,18 @@ use crate::{ read_random_channel_line, read_random_user_line, read_user, }, error::Error, - logs::{ - schema::{ChannelLogDate, UserLogDate}, - stream::LogsStream, - }, + logs::{schema::LogRangeParams, stream::LogsStream}, + web::schema::LogsPathDate, Result, }; use aide::axum::IntoApiResponse; use axum::{ extract::{Path, Query, RawQuery, State}, headers::CacheControl, - response::Redirect, + response::{IntoResponse, Redirect, Response}, Json, TypedHeader, }; +use chrono::{Days, Months, NaiveDate, NaiveTime, Utc}; use rand::{distributions::Alphanumeric, thread_rng, Rng}; use std::time::Duration; use tracing::debug; @@ -47,46 +47,85 @@ pub async fn get_channels(app: State) -> impl IntoApiResponse { } pub async fn get_channel_logs( + Path(LogsPathChannel { + channel_id_type, + channel, + }): Path, + range_params: Option>, + RawQuery(query): RawQuery, app: State, - Path(channel_log_params): Path, +) -> Result { + let channel_id = match channel_id_type { + ChannelIdType::Name => app.get_user_id_by_name(&channel).await?, + ChannelIdType::Id => channel.clone(), + }; + + if let Some(Query(params)) = range_params { + let logs = get_channel_logs_inner(&app, &channel_id, params).await?; + Ok(logs.into_response()) + } else { + let available_logs = read_available_channel_logs(&app.db, &channel_id).await?; + let latest_log = available_logs.first().ok_or(Error::NotFound)?; + + let mut new_uri = format!("/{channel_id_type}/{channel}/{latest_log}"); + if let Some(query) = query { + new_uri.push('?'); + new_uri.push_str(&query); + } + + Ok(Redirect::to(&new_uri).into_response()) + } +} + +pub async fn get_channel_logs_by_date( + app: State, + Path(channel_log_params): Path, Query(logs_params): Query, ) -> Result { debug!("Params: {logs_params:?}"); let channel_id = match channel_log_params.channel_info.channel_id_type { - ChannelIdType::Name => app - .get_users( - vec![], - vec![channel_log_params.channel_info.channel.clone()], - ) - .await? - .into_keys() - .next() - .ok_or(Error::NotFound)?, + ChannelIdType::Name => { + app.get_user_id_by_name(&channel_log_params.channel_info.channel) + .await? + } ChannelIdType::Id => channel_log_params.channel_info.channel.clone(), }; + let LogsPathDate { year, month, day } = channel_log_params.date; + + let from = NaiveDate::from_ymd_opt(year.parse()?, month.parse()?, day.parse()?) + .ok_or_else(|| Error::InvalidParam("Invalid date".to_owned()))? + .and_time(NaiveTime::default()) + .and_utc(); + let to = from + .checked_add_days(Days::new(1)) + .ok_or_else(|| Error::InvalidParam("Date out of range".to_owned()))?; + + let params = LogRangeParams { + from, + to, + logs_params, + }; + + get_channel_logs_inner(&app, &channel_id, params).await +} + +async fn get_channel_logs_inner( + app: &App, + channel_id: &str, + channel_log_params: LogRangeParams, +) -> Result { app.check_opted_out(&channel_id, None)?; - let log_date = ChannelLogDate::try_from(channel_log_params.date)?; - debug!("Querying logs for date {log_date:?}"); - - let stream = read_channel( - &app.db, - &channel_id, - log_date, - logs_params.reverse, - logs_params.limit, - logs_params.offset, - ) - .await?; + let stream = read_channel(&app.db, &channel_id, &channel_log_params).await?; let logs = LogsResponse { - response_type: logs_params.response_type(), + response_type: channel_log_params.logs_params.response_type(), stream, }; - let cache = if log_date.is_today() { + let cache = if Utc::now() < channel_log_params.to { no_cache_header() } else { cache_header(36000) @@ -96,71 +135,132 @@ pub async fn get_channel_logs( } pub async fn get_user_logs_by_name( + path: Path, + range_params: Option>, + query: RawQuery, + app: State, +) -> Result { + get_user_logs(path, range_params, query, false, app).await +} + +pub async fn get_user_logs_id( + path: Path, + range_params: Option>, + query: RawQuery, + app: State, +) -> Result { + get_user_logs(path, range_params, query, true, app).await +} + +async fn get_user_logs( + Path(UserLogPathParams { + channel_id_type, + channel, + user, + }): Path, + range_params: Option>, + RawQuery(query): RawQuery, + user_is_id: bool, + app: State, +) -> Result { + let channel_id = match channel_id_type { + ChannelIdType::Name => app.get_user_id_by_name(&channel).await?, + ChannelIdType::Id => channel.clone(), + }; + let user_id = if user_is_id { + user.clone() + } else { + app.get_user_id_by_name(&user).await? + }; + + if let Some(Query(params)) = range_params { + let logs = get_user_logs_inner(&app, &channel_id, &user_id, params).await?; + Ok(logs.into_response()) + } else { + let available_logs = read_available_user_logs(&app.db, &channel_id, &user_id).await?; + let latest_log = available_logs.first().ok_or(Error::NotFound)?; + + let user_id_type = if user_is_id { "userid" } else { "user" }; + + let mut new_uri = + format!("/{channel_id_type}/{channel}/{user_id_type}/{user}/{latest_log}"); + if let Some(query) = query { + new_uri.push('?'); + new_uri.push_str(&query); + } + Ok(Redirect::to(&new_uri).into_response()) + } +} + +pub async fn get_user_logs_by_date_name( app: State, path: Path, params: Query, ) -> Result { - let user_id = app - .get_users(vec![], vec![path.user.clone()]) - .await? - .into_iter() - .next() - .ok_or(Error::NotFound)? - .0; + let user_id = app.get_user_id_by_name(&path.user).await?; - get_user_logs(app, path, params, user_id).await + get_user_logs_by_date(app, path, params, user_id).await } -pub async fn get_user_logs_by_id( +pub async fn get_user_logs_by_date_id( app: State, path: Path, params: Query, ) -> Result { let user_id = path.user.clone(); - get_user_logs(app, path, params, user_id).await + get_user_logs_by_date(app, path, params, user_id).await } -async fn get_user_logs( +async fn get_user_logs_by_date( app: State, Path(user_logs_path): Path, Query(logs_params): Query, user_id: String, ) -> Result { - let log_date = UserLogDate::try_from(&user_logs_path)?; - let channel_id = match user_logs_path.channel_info.channel_id_type { ChannelIdType::Name => { - let (id, _) = app - .get_users(vec![], vec![user_logs_path.channel_info.channel]) + app.get_user_id_by_name(&user_logs_path.channel_info.channel) .await? - .into_iter() - .next() - .ok_or(Error::NotFound)?; - id } - - ChannelIdType::Id => user_logs_path.channel_info.channel, + ChannelIdType::Id => user_logs_path.channel_info.channel.clone(), }; + let year = user_logs_path.year.parse()?; + let month = user_logs_path.month.parse()?; + + let from = NaiveDate::from_ymd_opt(year, month, 1) + .ok_or_else(|| Error::InvalidParam("Invalid date".to_owned()))? + .and_time(NaiveTime::default()) + .and_utc(); + let to = from + .checked_add_months(Months::new(1)) + .ok_or_else(|| Error::InvalidParam("Date out of range".to_owned()))?; + + let params = LogRangeParams { + from, + to, + logs_params, + }; + + get_user_logs_inner(&app, &channel_id, &user_id, params).await +} + +async fn get_user_logs_inner( + app: &App, + channel_id: &str, + user_id: &str, + log_params: LogRangeParams, +) -> Result { app.check_opted_out(&channel_id, Some(&user_id))?; - let stream = read_user( - &app.db, - &channel_id, - &user_id, - log_date, - logs_params.reverse, - logs_params.limit, - logs_params.offset, - ) - .await?; + let stream = read_user(&app.db, &channel_id, &user_id, &log_params).await?; let logs = LogsResponse { stream, - response_type: logs_params.response_type(), + response_type: log_params.logs_params.response_type(), }; - let cache = if log_date.is_current_month() { + let cache = if Utc::now() < log_params.to { no_cache_header() } else { cache_header(36000) @@ -197,82 +297,6 @@ pub async fn list_available_logs( } } -pub async fn redirect_to_latest_channel_logs( - Path(LogsPathChannel { - channel_id_type, - channel, - }): Path, - RawQuery(query): RawQuery, - app: State, -) -> Result { - let channel_id = match channel_id_type { - ChannelIdType::Name => app.get_user_id_by_name(&channel).await?, - ChannelIdType::Id => channel.clone(), - }; - - let available_logs = read_available_channel_logs(&app.db, &channel_id).await?; - let latest_log = available_logs.first().ok_or(Error::NotFound)?; - - let mut new_uri = format!("/{channel_id_type}/{channel}/{latest_log}"); - if let Some(query) = query { - new_uri.push('?'); - new_uri.push_str(&query); - } - - Ok(Redirect::to(&new_uri)) -} - -pub async fn redirect_to_latest_user_name_logs( - path: Path, - query: RawQuery, - app: State, -) -> Result { - redirect_to_latest_user_logs(path, query, false, app).await -} - -pub async fn redirect_to_latest_user_id_logs( - path: Path, - query: RawQuery, - app: State, -) -> Result { - redirect_to_latest_user_logs(path, query, true, app).await -} - -async fn redirect_to_latest_user_logs( - Path(UserLogPathParams { - channel_id_type, - channel, - user, - }): Path, - RawQuery(query): RawQuery, - user_is_id: bool, - app: State, -) -> Result { - let channel_id = match channel_id_type { - ChannelIdType::Name => app.get_user_id_by_name(&channel).await?, - ChannelIdType::Id => channel.clone(), - }; - let user_id = if user_is_id { - user.clone() - } else { - app.get_user_id_by_name(&user).await? - }; - - app.check_opted_out(&channel_id, Some(&user_id))?; - - let available_logs = read_available_user_logs(&app.db, &channel_id, &user_id).await?; - let latest_log = available_logs.first().ok_or(Error::NotFound)?; - - let user_id_type = if user_is_id { "userid" } else { "user" }; - - let mut new_uri = format!("/{channel_id_type}/{channel}/{user_id_type}/{user}/{latest_log}"); - if let Some(query) = query { - new_uri.push('?'); - new_uri.push_str(&query); - } - Ok(Redirect::to(&new_uri)) -} - pub async fn random_channel_line( app: State, Path(LogsPathChannel { diff --git a/src/web/mod.rs b/src/web/mod.rs index caffe04..1c4a6fb 100644 --- a/src/web/mod.rs +++ b/src/web/mod.rs @@ -77,38 +77,38 @@ pub async fn run(app: App, mut shutdown_rx: ShutdownRx, bot_tx: Sender for ChannelLogDate { - type Error = ParseIntError; - - fn try_from(value: ChannelLogDatePath) -> Result { - Ok(Self { - year: value.year.parse()?, - month: value.month.parse()?, - day: value.day.parse()?, - }) - } -} - #[derive(Deserialize, JsonSchema)] pub struct LogsPathChannel { pub channel_id_type: ChannelIdType, @@ -116,17 +103,6 @@ where Ok(Option::<&str>::deserialize(deserializer)?.is_some()) } -impl TryFrom<&UserLogsPath> for UserLogDate { - type Error = ParseIntError; - - fn try_from(params: &UserLogsPath) -> Result { - Ok(Self { - year: params.year.parse()?, - month: params.month.parse()?, - }) - } -} - #[derive(Serialize, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct AvailableLogs {