use hyper::{HeaderMap, header::USER_AGENT};
use sqlx::{Row, sqlite::SqliteRow};
use std::net::SocketAddr;
use std::str::FromStr;
use uuid::Uuid;
pub(crate) async fn log<'e, E>(
url_id: &Uuid,
visitor_ip_addr: &SocketAddr,
header: &HeaderMap,
executor: E,
) -> sqlx::Result<()>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
sqlx::query!(
"INSERT INTO visit(url_id, visitor_ip_addr, visitor_user_agent) VALUES($1, $2, $3)",
url_id.as_bytes().as_slice(),
visitor_ip_addr.to_string(),
get_user_agent(&header)
)
.execute(executor)
.await?;
Ok(())
}
fn get_user_agent(header: &HeaderMap) -> Option<&str> {
header
.get(USER_AGENT)
.map(|v| match v.to_str() {
Ok(user_agent) => Some(user_agent),
Err(_) => {
eprintln!("[WARN] Could not parse `User-Agent` header");
None
}
})
.flatten()
}
pub(crate) struct Visit {
pub(crate) url_id: Uuid,
pub(crate) visit_timestamp: time::PrimitiveDateTime,
pub(crate) visitor_ip_addr: Option<SocketAddr>,
pub(crate) visitor_user_agent: Option<String>,
}
impl sqlx::FromRow<'_, SqliteRow> for Visit {
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
Ok(Self {
url_id: Uuid::try_from(row.try_get::<Vec<u8>, &str>("url_id")?).map_err(|err| {
sqlx::Error::ColumnDecode {
index: "url_id".into(),
source: Box::new(err),
}
})?,
visit_timestamp: row.try_get("visit_timestamp")?,
visitor_ip_addr: row.try_get::<Option<&str>, &str>("visitor_ip_addr")?
.map(SocketAddr::from_str)
.transpose()
.map_err(|err| sqlx::Error::ColumnDecode {
index: "visitor_ip_addr".into(),
source: Box::new(err),
})?,
visitor_user_agent: row.try_get("visitor_user_agent")?,
})
}
}
pub(crate) async fn get_url_visits<'e, E>(url_id: &Uuid, executor: E) -> sqlx::Result<Vec<Visit>>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
sqlx::query_as(
r#"
SELECT
url_id,
visit_timestamp,
visitor_ip_addr,
visitor_user_agent
FROM visit
WHERE url_id = $1
ORDER BY visit_timestamp, rowid ASC
"#,
)
.bind(url_id.as_bytes().as_slice())
.fetch_all(executor)
.await
}
pub(crate) async fn get_doc_visits<'e, E>(doc_id: &Uuid, executor: E) -> sqlx::Result<Vec<Visit>>
where
E: sqlx::Executor<'e, Database = sqlx::Sqlite>,
{
sqlx::query_as(
r#"
SELECT
visit.url_id,
visit_timestamp,
visitor_ip_addr,
visitor_user_agent
FROM visit
JOIN url ON url.url_id = visit.url_id
WHERE url.doc_id = $1
ORDER BY visit_timestamp, visit.rowid ASC
"#,
)
.bind(doc_id.as_bytes().as_slice())
.fetch_all(executor)
.await
}