rustlog/src/web/handlers.rs

403 lines
12 KiB
Rust

use super::{
responders::logs::LogsResponse,
schema::{
AvailableLogs, AvailableLogsParams, Channel, ChannelIdType, ChannelLogsByDatePath,
ChannelParam, ChannelsList, LogsParams, LogsPathChannel, UserLogPathParams, UserLogsPath,
UserParam,
},
};
use crate::{
app::App,
db::{
read_available_channel_logs, read_available_user_logs, read_channel,
read_random_channel_line, read_random_user_line, read_user,
},
error::Error,
logs::{schema::LogRangeParams, stream::LogsStream},
web::schema::LogsPathDate,
Result,
};
use aide::axum::IntoApiResponse;
use axum::{
extract::{Path, Query, RawQuery, State},
headers::CacheControl,
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;
pub async fn get_channels(app: State<App>) -> impl IntoApiResponse {
let channel_ids = app.config.channels.read().unwrap().clone();
let channels = app
.get_users(Vec::from_iter(channel_ids), vec![])
.await
.unwrap();
let json = Json(ChannelsList {
channels: channels
.into_iter()
.map(|(user_id, name)| Channel { name, user_id })
.collect(),
});
(cache_header(600), json)
}
pub async fn get_channel_logs(
Path(LogsPathChannel {
channel_id_type,
channel,
}): Path<LogsPathChannel>,
range_params: Option<Query<LogRangeParams>>,
RawQuery(query): RawQuery,
app: State<App>,
) -> Result<Response> {
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<App>,
Path(channel_log_params): Path<ChannelLogsByDatePath>,
Query(logs_params): Query<LogsParams>,
) -> Result<impl IntoApiResponse> {
debug!("Params: {logs_params:?}");
let channel_id = match channel_log_params.channel_info.channel_id_type {
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<impl IntoApiResponse> {
app.check_opted_out(channel_id, None)?;
let stream = read_channel(&app.db, channel_id, &channel_log_params).await?;
let logs = LogsResponse {
response_type: channel_log_params.logs_params.response_type(),
stream,
};
let cache = if Utc::now() < channel_log_params.to {
no_cache_header()
} else {
cache_header(36000)
};
Ok((cache, logs))
}
pub async fn get_user_logs_by_name(
path: Path<UserLogPathParams>,
range_params: Option<Query<LogRangeParams>>,
query: RawQuery,
app: State<App>,
) -> Result<impl IntoApiResponse> {
get_user_logs(path, range_params, query, false, app).await
}
pub async fn get_user_logs_id(
path: Path<UserLogPathParams>,
range_params: Option<Query<LogRangeParams>>,
query: RawQuery,
app: State<App>,
) -> Result<impl IntoApiResponse> {
get_user_logs(path, range_params, query, true, app).await
}
async fn get_user_logs(
Path(UserLogPathParams {
channel_id_type,
channel,
user,
}): Path<UserLogPathParams>,
range_params: Option<Query<LogRangeParams>>,
RawQuery(query): RawQuery,
user_is_id: bool,
app: State<App>,
) -> Result<impl IntoApiResponse> {
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<App>,
path: Path<UserLogsPath>,
params: Query<LogsParams>,
) -> Result<impl IntoApiResponse> {
let user_id = app.get_user_id_by_name(&path.user).await?;
get_user_logs_by_date(app, path, params, user_id).await
}
pub async fn get_user_logs_by_date_id(
app: State<App>,
path: Path<UserLogsPath>,
params: Query<LogsParams>,
) -> Result<impl IntoApiResponse> {
let user_id = path.user.clone();
get_user_logs_by_date(app, path, params, user_id).await
}
async fn get_user_logs_by_date(
app: State<App>,
Path(user_logs_path): Path<UserLogsPath>,
Query(logs_params): Query<LogsParams>,
user_id: String,
) -> Result<impl IntoApiResponse> {
let channel_id = match user_logs_path.channel_info.channel_id_type {
ChannelIdType::Name => {
app.get_user_id_by_name(&user_logs_path.channel_info.channel)
.await?
}
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<impl IntoApiResponse> {
app.check_opted_out(channel_id, Some(user_id))?;
let stream = read_user(&app.db, channel_id, user_id, &log_params).await?;
let logs = LogsResponse {
stream,
response_type: log_params.logs_params.response_type(),
};
let cache = if Utc::now() < log_params.to {
no_cache_header()
} else {
cache_header(36000)
};
Ok((cache, logs))
}
pub async fn list_available_logs(
Query(AvailableLogsParams { user, channel }): Query<AvailableLogsParams>,
app: State<App>,
) -> Result<impl IntoApiResponse> {
let channel_id = match channel {
ChannelParam::ChannelId(id) => id,
ChannelParam::Channel(name) => app.get_user_id_by_name(&name).await?,
};
let available_logs = if let Some(user) = user {
let user_id = match user {
UserParam::UserId(id) => id,
UserParam::User(name) => app.get_user_id_by_name(&name).await?,
};
app.check_opted_out(&channel_id, Some(&user_id))?;
read_available_user_logs(&app.db, &channel_id, &user_id).await?
} else {
app.check_opted_out(&channel_id, None)?;
read_available_channel_logs(&app.db, &channel_id).await?
};
if !available_logs.is_empty() {
Ok((cache_header(600), Json(AvailableLogs { available_logs })))
} else {
Err(Error::NotFound)
}
}
pub async fn random_channel_line(
app: State<App>,
Path(LogsPathChannel {
channel_id_type,
channel,
}): Path<LogsPathChannel>,
Query(logs_params): Query<LogsParams>,
) -> Result<impl IntoApiResponse> {
let channel_id = match channel_id_type {
ChannelIdType::Name => app.get_user_id_by_name(&channel).await?,
ChannelIdType::Id => channel,
};
let random_line = read_random_channel_line(&app.db, &channel_id).await?;
let stream = LogsStream::new_provided(vec![random_line])?;
let logs = LogsResponse {
stream,
response_type: logs_params.response_type(),
};
Ok((no_cache_header(), logs))
}
pub async fn random_user_line_by_name(
app: State<App>,
Path(UserLogPathParams {
channel_id_type,
channel,
user,
}): Path<UserLogPathParams>,
query: Query<LogsParams>,
) -> Result<impl IntoApiResponse> {
let user_id = app.get_user_id_by_name(&user).await?;
random_user_line(app, channel_id_type, channel, user_id, query).await
}
pub async fn random_user_line_by_id(
app: State<App>,
Path(UserLogPathParams {
channel_id_type,
channel,
user,
}): Path<UserLogPathParams>,
query: Query<LogsParams>,
) -> Result<impl IntoApiResponse> {
random_user_line(app, channel_id_type, channel, user, query).await
}
async fn random_user_line(
app: State<App>,
channel_id_type: ChannelIdType,
channel: String,
user_id: String,
Query(logs_params): Query<LogsParams>,
) -> Result<impl IntoApiResponse> {
let channel_id = match channel_id_type {
ChannelIdType::Name => app.get_user_id_by_name(&channel).await?,
ChannelIdType::Id => channel,
};
app.check_opted_out(&channel_id, Some(&user_id))?;
let random_line = read_random_user_line(&app.db, &channel_id, &user_id).await?;
let stream = LogsStream::new_provided(vec![random_line])?;
let logs = LogsResponse {
stream,
response_type: logs_params.response_type(),
};
Ok((no_cache_header(), logs))
}
// pub async fn optout(app: State<App>) -> Json<String> {
// let mut rng = thread_rng();
// let optout_code: String = (0..5).map(|_| rng.sample(Alphanumeric) as char).collect();
// app.optout_codes.insert(optout_code.clone());
// {
// let codes = app.optout_codes.clone();
// let optout_code = optout_code.clone();
// tokio::spawn(async move {
// tokio::time::sleep(Duration::from_secs(60)).await;
// if codes.remove(&optout_code).is_some() {
// debug!("Dropping optout code {optout_code}");
// }
// });
// }
// Json(optout_code)
// }
fn cache_header(secs: u64) -> TypedHeader<CacheControl> {
TypedHeader(
CacheControl::new()
.with_public()
.with_max_age(Duration::from_secs(secs)),
)
}
pub fn no_cache_header() -> TypedHeader<CacheControl> {
TypedHeader(CacheControl::new().with_no_cache())
}