From 15dc2e5d71237870065edcf81e7cd0c941268f8f Mon Sep 17 00:00:00 2001 From: trisua Date: Mon, 9 Jun 2025 20:56:32 -0400 Subject: [PATCH] add: great post average --- crates/app/src/public/html/profile/base.lisp | 8 +++ crates/app/src/routes/api/v1/auth/profile.rs | 30 ++++++++ crates/app/src/routes/api/v1/mod.rs | 4 ++ crates/app/src/routes/pages/profile.rs | 6 ++ crates/core/src/database/posts.rs | 74 ++++++++++++++++++++ 5 files changed, 122 insertions(+) diff --git a/crates/app/src/public/html/profile/base.lisp b/crates/app/src/public/html/profile/base.lisp index 9698f1e..15ee7bf 100644 --- a/crates/app/src/public/html/profile/base.lisp +++ b/crates/app/src/public/html/profile/base.lisp @@ -160,6 +160,14 @@ (text "Posts")) (span (text "{{ profile.post_count }}"))) + (div + ("class" "w-full flex justify-between items-center") + ("title" "great post average (limited time)") + (span + ("class" "notification chip") + (text "GPA 🐇")) + (span + (text "{{ gpa }}"))) (text "{% if not profile.settings.private_last_seen or is_self or is_helper %}") (div ("class" "w-full flex justify-between items-center") diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index f7f88d9..df0eea4 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -645,3 +645,33 @@ pub async fn post_to_socket_request( payload: (), }) } + +/// Calculate the user's great post average. +pub async fn get_user_gpa_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if !user.permissions.check(FinePermission::MANAGE_USERS) { + return Json(Error::NotAllowed.into()); + } + + let gpa = data.calculate_user_gpa(id).await; + return Json(ApiReturn { + ok: true, + message: if gpa >= 3 { + "cool".to_string() + } else if gpa >= 4 { + "extraordinary".to_string() + } else { + "ok".to_string() + }, + payload: Some(gpa), + }); +} diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 9cdfd5d..27c34e8 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -221,6 +221,10 @@ pub fn routes() -> Router { get(auth::profile::redirect_from_ip), ) .route("/auth/ip/{ip}/block", post(auth::social::ip_block_request)) + .route( + "/auth/user/{id}/gpa", + get(auth::profile::get_user_gpa_request), + ) .route( "/auth/user/{id}/_connect/{stream}", any(auth::profile::subscription_handler), diff --git a/crates/app/src/routes/pages/profile.rs b/crates/app/src/routes/pages/profile.rs index 3efe2f0..ef48326 100644 --- a/crates/app/src/routes/pages/profile.rs +++ b/crates/app/src/routes/pages/profile.rs @@ -350,6 +350,7 @@ pub async fn posts_request( context.insert("pinned", &pinned); context.insert("page", &props.page); context.insert("tag", &props.tag); + context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await); profile_context( &mut context, &user, @@ -453,6 +454,7 @@ pub async fn replies_request( context.insert("posts", &posts); context.insert("page", &props.page); + context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await); profile_context( &mut context, &user, @@ -558,6 +560,7 @@ pub async fn media_request( context.insert("posts", &posts); context.insert("page", &props.page); + context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await); profile_context( &mut context, &user, @@ -652,6 +655,7 @@ pub async fn outbox_request( context.insert("questions", &questions); context.insert("page", &props.page); + context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await); profile_context( &mut context, &Some(user), @@ -746,6 +750,7 @@ pub async fn following_request( context.insert("list", &list); context.insert("page", &props.page); + context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await); profile_context( &mut context, &user, @@ -840,6 +845,7 @@ pub async fn followers_request( context.insert("list", &list); context.insert("page", &props.page); + context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await); profile_context( &mut context, &user, diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index f3e483d..9633c01 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -510,6 +510,80 @@ impl DataManager { Ok(res.unwrap()) } + /// Calculate the GPA (great post average) of a given user. + /// + /// To be considered a "great post", a post must have a score ((likes - dislikes) / (likes + dislikes)) + /// of at least 0.6. + /// + /// GPA is calculated based on the user's last 250 posts. + pub async fn calculate_user_gpa(&self, id: usize) -> usize { + // just for note, this is SUPER bad for performance... which is why we + // only calculate this when it expires in the cache (every week) + if let Some(cached) = self.0.1.get(format!("atto.user.gpa:{}", id)).await { + if let Ok(c) = cached.parse() { + return c; + } + } + + // ... + let conn = match self.0.connect().await { + Ok(c) => c, + Err(_) => return 0, + }; + + let res = query_rows!( + &conn, + &format!("SELECT * FROM posts WHERE owner = $1 ORDER BY created DESC LIMIT 250",), + &[&(id as i64)], + |x| { Self::get_post_from_row(x) } + ); + + if res.is_err() { + return 0; + } + + // ... + let mut real_posts_count: usize = 0; // posts which can be scored + let mut good_posts: usize = 0; + // let mut bad_posts: usize = 0; + + let posts = res.unwrap(); + + for post in posts { + if post.likes == 0 && post.dislikes == 0 { + // post has no likes or dislikes... doesn't count + continue; + } + + real_posts_count += 1; + + // likes percentage / total likes + let score: f32 = (post.likes as f32 - post.dislikes as f32) + / (post.likes as f32 + post.dislikes as f32); + + if score.is_sign_negative() { + // bad_posts += 1; + continue; + } + + if score > 0.6 { + good_posts += 1; + } + // } else { + // bad_posts += 1; + // } + } + + let gpa = ((good_posts as f32 / real_posts_count as f32) * 4.0).round() as usize; + + self.0 + .1 + .set(format!("atto.user.gpa:{}", id), gpa.to_string()) + .await; + + gpa + } + /// Get all replies from the given user (from most recent). /// /// # Arguments