Track open rate for new post notifications (user clicked the button in the link or not). No data about the user is collected during the process, it only uses an ID inserted by the issue delivery worker.
143 lines
3.9 KiB
Rust
143 lines
3.9 KiB
Rust
use crate::{
|
|
domain::PostEntry,
|
|
routes::AppError,
|
|
startup::AppState,
|
|
templates::{PostListTemplate, PostTemplate, PostsTemplate},
|
|
};
|
|
use anyhow::Context;
|
|
use askama::Template;
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
response::{Html, IntoResponse, Redirect, Response},
|
|
};
|
|
use sqlx::PgPool;
|
|
use uuid::Uuid;
|
|
|
|
const NUM_PER_PAGE: i64 = 3;
|
|
|
|
pub async fn list_posts(
|
|
State(AppState {
|
|
connection_pool, ..
|
|
}): State<AppState>,
|
|
) -> Result<Response, AppError> {
|
|
let count = get_posts_table_size(&connection_pool)
|
|
.await
|
|
.context("Could not fetch posts table size.")
|
|
.map_err(AppError::unexpected_page)?;
|
|
let next_page = if count > NUM_PER_PAGE { Some(2) } else { None };
|
|
let posts = get_posts(&connection_pool, NUM_PER_PAGE, None)
|
|
.await
|
|
.context("Could not fetch latest posts")
|
|
.map_err(AppError::unexpected_page)?;
|
|
let template = PostsTemplate { posts, next_page };
|
|
Ok(Html(template.render().unwrap()).into_response())
|
|
}
|
|
|
|
async fn get_posts(
|
|
connection_pool: &PgPool,
|
|
n: i64,
|
|
offset: Option<i64>,
|
|
) -> Result<Vec<PostEntry>, sqlx::Error> {
|
|
sqlx::query_as!(
|
|
PostEntry,
|
|
r#"
|
|
SELECT p.post_id, u.username AS author, p.title, p.content, p.published_at
|
|
FROM posts p
|
|
LEFT JOIN users u ON p.author_id = u.user_id
|
|
ORDER BY p.published_at DESC
|
|
LIMIT $1
|
|
OFFSET $2
|
|
"#,
|
|
n,
|
|
offset
|
|
)
|
|
.fetch_all(connection_pool)
|
|
.await
|
|
}
|
|
|
|
async fn get_posts_table_size(connection_pool: &PgPool) -> Result<i64, sqlx::Error> {
|
|
sqlx::query!("SELECT count(*) FROM posts")
|
|
.fetch_one(connection_pool)
|
|
.await
|
|
.map(|r| r.count.unwrap())
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct PostParams {
|
|
origin: Option<Uuid>,
|
|
}
|
|
|
|
pub async fn see_post(
|
|
State(AppState {
|
|
connection_pool, ..
|
|
}): State<AppState>,
|
|
Path(post_id): Path<Uuid>,
|
|
Query(PostParams { origin }): Query<PostParams>,
|
|
) -> Result<Response, AppError> {
|
|
if let Some(origin) = origin {
|
|
sqlx::query!(
|
|
"UPDATE notifications_delivered SET opened = TRUE WHERE email_id = $1",
|
|
origin,
|
|
)
|
|
.execute(&connection_pool)
|
|
.await
|
|
.context("Failed to mark email as opened.")
|
|
.map_err(AppError::unexpected_page)?;
|
|
return Ok(Redirect::to(&format!("/posts/{}", post_id)).into_response());
|
|
}
|
|
let post = get_post_data(&connection_pool, post_id)
|
|
.await
|
|
.context(format!("Failed to fetch post #{}", post_id))
|
|
.map_err(AppError::unexpected_page)?
|
|
.to_html()
|
|
.context("Could not render markdown with extension.")?;
|
|
let template = PostTemplate { post };
|
|
Ok(Html(template.render().unwrap()).into_response())
|
|
}
|
|
|
|
async fn get_post_data(connection_pool: &PgPool, post_id: Uuid) -> Result<PostEntry, sqlx::Error> {
|
|
sqlx::query_as!(
|
|
PostEntry,
|
|
r#"
|
|
SELECT p.post_id, u.username AS author, p.title, p.content, p.published_at
|
|
FROM posts p
|
|
LEFT JOIN users u ON p.author_id = u.user_id
|
|
WHERE p.post_id = $1
|
|
"#,
|
|
post_id
|
|
)
|
|
.fetch_one(connection_pool)
|
|
.await
|
|
}
|
|
|
|
#[derive(serde::Deserialize)]
|
|
pub struct LoadMoreParams {
|
|
page: i64,
|
|
}
|
|
|
|
pub async fn load_more(
|
|
State(AppState {
|
|
connection_pool, ..
|
|
}): State<AppState>,
|
|
Query(LoadMoreParams { page }): Query<LoadMoreParams>,
|
|
) -> Result<Response, AppError> {
|
|
let offset = (page - 1) * NUM_PER_PAGE;
|
|
let posts = get_posts(&connection_pool, NUM_PER_PAGE, Some(offset))
|
|
.await
|
|
.context("Could not fetch posts from database.")?;
|
|
let count = posts.len();
|
|
Ok(Html(
|
|
PostListTemplate {
|
|
posts,
|
|
next_page: if count as i64 == NUM_PER_PAGE {
|
|
Some(page + 1)
|
|
} else {
|
|
None
|
|
},
|
|
}
|
|
.render()
|
|
.unwrap(),
|
|
)
|
|
.into_response())
|
|
}
|