From 8f16068a349d76c94053a236af768ef69bece172 Mon Sep 17 00:00:00 2001 From: trisua Date: Fri, 13 Jun 2025 17:47:00 -0400 Subject: [PATCH] add: implement 9 new scopes, 21 new api endpoints --- README.md | 2 + crates/app/src/macros.rs | 50 ++ crates/app/src/routes/api/v1/auth/social.rs | 89 ++- .../src/routes/api/v1/auth/user_warnings.rs | 69 ++- .../src/routes/api/v1/communities/posts.rs | 560 +++++++++++++++++- crates/app/src/routes/api/v1/mod.rs | 69 +++ crates/app/src/routes/api/v1/notifications.rs | 44 +- crates/app/src/routes/api/v1/requests.rs | 38 +- crates/app/src/routes/pages/communities.rs | 2 +- crates/core/src/database/posts.rs | 26 +- crates/core/src/database/user_warnings.rs | 2 +- crates/core/src/database/userfollows.rs | 12 + crates/core/src/model/auth.rs | 18 +- crates/core/src/model/oauth.rs | 27 + 14 files changed, 973 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 865ebae..9afd5bc 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ # Usage +Make sure you have AT LEAST rustc version 1.89.0-nightly. + Everything Tetratto needs will be built into the main binary. You can build Tetratto with the following command: ```bash diff --git a/crates/app/src/macros.rs b/crates/app/src/macros.rs index b91fbbb..451a348 100644 --- a/crates/app/src/macros.rs +++ b/crates/app/src/macros.rs @@ -292,6 +292,48 @@ macro_rules! check_user_blocked_or_private { } } }; + + ($user:expr, $other_user:ident, $data:ident, @api) => { + // check if we're blocked + if let Some(ref ua) = $user { + if $data + .get_userblock_by_initiator_receiver($other_user.id, ua.id) + .await + .is_ok() + && !ua + .permissions + .check(tetratto_core::model::permissions::FinePermission::MANAGE_USERS) + { + return Json( + tetratto_core::model::Error::MiscError("You're blocked".to_string()).into(), + ); + } + } + + // check for private profile + if $other_user.settings.private_profile { + if let Some(ref ua) = $user { + if (ua.id != $other_user.id) + && !ua + .permissions + .check(tetratto_core::model::permissions::FinePermission::MANAGE_USERS) + && $data + .get_userfollow_by_initiator_receiver($other_user.id, ua.id) + .await + .is_err() + { + return Json( + tetratto_core::model::Error::MiscError("Profile is private".to_string()) + .into(), + ); + } + } else { + return Json( + tetratto_core::model::Error::MiscError("Profile is private".to_string()).into(), + ); + } + } + }; } #[macro_export] @@ -318,4 +360,12 @@ macro_rules! ignore_users_gen { ] .concat() }; + + ($user:ident!, #$data:ident) => { + [ + $data.get_userblocks_receivers($user.id).await, + $data.get_userblocks_initiator_by_receivers($user.id).await, + ] + .concat() + }; } diff --git a/crates/app/src/routes/api/v1/auth/social.rs b/crates/app/src/routes/api/v1/auth/social.rs index e6d3bbc..cef005c 100644 --- a/crates/app/src/routes/api/v1/auth/social.rs +++ b/crates/app/src/routes/api/v1/auth/social.rs @@ -1,10 +1,19 @@ use crate::{ - State, get_user_from_token, + check_user_blocked_or_private, get_user_from_token, model::{ApiReturn, Error}, + routes::pages::PaginatedQuery, + State, +}; +use axum::{ + extract::{Path, Query}, + response::IntoResponse, + Extension, Json, }; -use axum::{Extension, Json, extract::Path, response::IntoResponse}; use axum_extra::extract::CookieJar; -use tetratto_core::model::auth::{FollowResult, IpBlock, Notification, UserBlock, UserFollow}; +use tetratto_core::model::{ + auth::{FollowResult, IpBlock, Notification, UserBlock, UserFollow}, + oauth, +}; /// Toggle following on the given user. pub async fn follow_request( @@ -13,7 +22,7 @@ pub async fn follow_request( Extension(data): Extension, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageFollowing) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -74,7 +83,7 @@ pub async fn cancel_follow_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageFollowing) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -95,7 +104,7 @@ pub async fn accept_follow_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageFollowers) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -142,7 +151,7 @@ pub async fn block_request( Extension(data): Extension, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageBlocks) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -205,7 +214,7 @@ pub async fn ip_block_request( Extension(data): Extension, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreateIpBlock) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -232,3 +241,67 @@ pub async fn ip_block_request( } } } + +/// Get the followers of the given user. +pub async fn followers_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadProfiles) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let other_user = match data.get_user_by_id(id).await { + Ok(ua) => ua, + Err(e) => return Json(e.into()), + }; + + check_user_blocked_or_private!(Some(&user), other_user, data, @api); + match data.get_userfollows_by_receiver(id, 12, props.page).await { + Ok(f) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data.fill_userfollows_with_initiator(f).await { + Ok(f) => Some(data.userfollows_user_filter(&f)), + Err(e) => return Json(e.into()), + }, + }), + Err(e) => Json(e.into()), + } +} + +/// Get the following of the given user. +pub async fn following_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadProfiles) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let other_user = match data.get_user_by_id(id).await { + Ok(ua) => ua, + Err(e) => return Json(e.into()), + }; + + check_user_blocked_or_private!(Some(&user), other_user, data, @api); + match data.get_userfollows_by_initiator(id, 12, props.page).await { + Ok(f) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data.fill_userfollows_with_receiver(f).await { + Ok(f) => Some(data.userfollows_user_filter(&f)), + Err(e) => return Json(e.into()), + }, + }), + Err(e) => Json(e.into()), + } +} diff --git a/crates/app/src/routes/api/v1/auth/user_warnings.rs b/crates/app/src/routes/api/v1/auth/user_warnings.rs index 49df570..321ab78 100644 --- a/crates/app/src/routes/api/v1/auth/user_warnings.rs +++ b/crates/app/src/routes/api/v1/auth/user_warnings.rs @@ -1,12 +1,16 @@ use crate::{ get_user_from_token, model::{ApiReturn, Error}, - routes::api::v1::CreateUserWarning, + routes::{api::v1::CreateUserWarning, pages::PaginatedQuery}, State, }; -use axum::{Extension, Json, extract::Path, response::IntoResponse}; +use axum::{ + extract::{Path, Query}, + response::IntoResponse, + Extension, Json, +}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{auth::UserWarning, permissions::FinePermission}; +use tetratto_core::model::{auth::UserWarning, oauth, permissions::FinePermission}; /// Create a new user warning. pub async fn create_request( @@ -16,7 +20,7 @@ pub async fn create_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::ModManageWarnings) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -45,7 +49,7 @@ pub async fn delete_request( Extension(data): Extension, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::ModManageWarnings) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -63,3 +67,58 @@ pub async fn delete_request( Err(e) => Json(e.into()), } } + +/// Get all warnings for the given user. +pub async fn on_user_request( + jar: CookieJar, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::ModManageWarnings) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if !user.permissions.check(FinePermission::MANAGE_WARNINGS) { + return Json(Error::NotAllowed.into()); + } + + match data + .get_user_warnings_by_user(user.id, 12, props.page) + .await + { + Ok(w) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(w), + }), + Err(e) => Json(e.into()), + } +} + +/// Get a single warning. +pub async fn get_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, oauth::AppScope::ModManageWarnings) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if !user.permissions.check(FinePermission::MANAGE_WARNINGS) { + return Json(Error::NotAllowed.into()); + } + + match data.get_user_warning_by_id(id).await { + Ok(w) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(w), + }), + Err(e) => Json(e.into()), + } +} diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs index dc3292e..3c07193 100644 --- a/crates/app/src/routes/api/v1/communities/posts.rs +++ b/crates/app/src/routes/api/v1/communities/posts.rs @@ -1,5 +1,5 @@ use axum::{ - extract::Path, + extract::{Path, Query}, http::{HeaderMap, HeaderValue}, response::IntoResponse, Extension, Json, @@ -14,11 +14,14 @@ use tetratto_core::model::{ ApiReturn, Error, }; use crate::{ - get_user_from_token, + check_user_blocked_or_private, get_user_from_token, image::{save_webp_buffer, JsonMultipart}, - routes::api::v1::{ - CreatePost, CreateRepost, UpdatePostContent, UpdatePostContext, UpdatePostIsOpen, - VoteInPoll, + routes::{ + api::v1::{ + CreatePost, CreateRepost, UpdatePostContent, UpdatePostContext, UpdatePostIsOpen, + VoteInPoll, + }, + pages::{PaginatedQuery, SearchedQuery}, }, State, }; @@ -409,3 +412,550 @@ pub async fn update_is_open_request( Err(e) => Json(e.into()), } } + +/// Get posts by the given user. +pub async fn posts_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let other_user = match data.get_user_by_id(id).await { + Ok(ua) => ua, + Err(e) => return Json(e.into()), + }; + + check_user_blocked_or_private!(Some(&user), other_user, data, @api); + match data + .get_posts_by_user(id, 12, props.page, &Some(user.clone())) + .await + { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get posts in the given community. +pub async fn community_posts_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_posts_by_community(id, 12, props.page).await { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get replies by the given user. +pub async fn replies_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let other_user = match data.get_user_by_id(id).await { + Ok(ua) => ua, + Err(e) => return Json(e.into()), + }; + + check_user_blocked_or_private!(Some(&user), other_user, data, @api); + match data + .get_replies_by_user(id, 12, props.page, &Some(user.clone())) + .await + { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get posts (with media) by the given user. +pub async fn posts_with_media_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let other_user = match data.get_user_by_id(id).await { + Ok(ua) => ua, + Err(e) => return Json(e.into()), + }; + + check_user_blocked_or_private!(Some(&user), other_user, data, @api); + match data + .get_media_posts_by_user(id, 12, props.page, &Some(user.clone())) + .await + { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get posts (searched) by the given user. +pub async fn posts_searched_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let other_user = match data.get_user_by_id(id).await { + Ok(ua) => ua, + Err(e) => return Json(e.into()), + }; + + check_user_blocked_or_private!(Some(&user), other_user, data, @api); + match data + .get_posts_by_user_searched(id, 12, props.page, &props.text, &Some(&user)) + .await + { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get all posts (searched). +pub async fn all_posts_searched_request( + jar: CookieJar, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if !user.permissions.check(FinePermission::SUPPORTER) { + return Json(Error::RequiresSupporter.into()); + } + + match data.get_posts_searched(12, props.page, &props.text).await { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get all posts (from user communities). +pub async fn from_communities_request( + jar: CookieJar, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_popular_posts(12, props.page, 604_800_000).await { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get all posts (from stack). +pub async fn from_stack_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let stack = match data.get_stack_by_id(id).await { + Ok(s) => s, + Err(e) => return Json(e.into()), + }; + + if stack.owner != user.id && !user.permissions.check(FinePermission::MANAGE_STACKS) { + return Json(Error::NotAllowed.into()); + } + + match data + .get_posts_from_stack(id, 12, props.page, stack.sort) + .await + { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get all posts (by likes). +pub async fn popular_request( + jar: CookieJar, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_popular_posts(12, props.page, 604_800_000).await { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get all posts (from any community). +pub async fn all_request( + jar: CookieJar, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_latest_posts(12, props.page).await { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get all posts (from following). +pub async fn following_request( + jar: CookieJar, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data + .get_posts_from_user_following(user.id, 12, props.page) + .await + { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get a single post. +pub async fn get_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, +) -> impl IntoResponse { + let data = &(data.read().await).0; + if get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts).is_none() { + return Json(Error::NotAllowed.into()); + } + + match data.get_post_by_id(id).await { + Ok(p) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(p), + }), + Err(e) => Json(e.into()), + } +} + +/// Get replies for the given post. +pub async fn post_replies_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_replies_by_post(id, 12, props.page).await { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get reposts for the given post. +pub async fn reposts_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_reposts_by_quoting(id, 12, props.page).await { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get quotes for the given post. +pub async fn quotes_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadPosts) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_quoting_posts_by_quoting(id, 12, props.page).await { + Ok(posts) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data + .fill_posts_with_community(posts, user.id, &ignore_users, &Some(user.clone())) + .await + { + Ok(l) => data.posts_owner_filter( + &data.posts_muted_phrase_filter(&l, Some(&user.settings.muted)), + ), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index 714f9ab..b912c04 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -133,6 +133,60 @@ pub fn routes() -> Router { "/posts/{id}/open", post(communities::posts::update_is_open_request), ) + .route( + "/posts/from_user/{id}", + get(communities::posts::posts_request), + ) + .route( + "/posts/from_user/{id}/replies", + get(communities::posts::replies_request), + ) + .route( + "/posts/from_user/{id}/media", + get(communities::posts::posts_with_media_request), + ) + .route( + "/posts/from_user/{id}/searched", + get(communities::posts::posts_searched_request), + ) + .route( + "/posts/from_community/{id}", + get(communities::posts::community_posts_request), + ) + .route( + "/posts/from_stack/{id}", + get(communities::posts::from_stack_request), + ) + .route( + "/posts/searched", + get(communities::posts::all_posts_searched_request), + ) + .route( + "/posts/timeline/communities", + get(communities::posts::from_communities_request), + ) + .route( + "/posts/timeline/popular", + get(communities::posts::popular_request), + ) + .route("/posts/timeline/all", get(communities::posts::all_request)) + .route( + "/posts/timeline/following", + get(communities::posts::following_request), + ) + .route("/posts/{id}", get(communities::posts::get_request)) + .route( + "/posts/{id}/replies", + delete(communities::posts::post_replies_request), + ) + .route( + "/posts/{id}/reposts", + delete(communities::posts::reposts_request), + ) + .route( + "/posts/{id}/quotes", + delete(communities::posts::quotes_request), + ) // drafts .route("/drafts", post(communities::drafts::create_request)) .route("/drafts/my", get(communities::drafts::get_drafts_request)) @@ -251,12 +305,25 @@ pub fn routes() -> Router { "/auth/user/{id}/_connect/{stream}/send", post(auth::profile::post_to_socket_request), ) + .route( + "/auth/user/{id}/following", + get(auth::social::following_request), + ) + .route( + "/auth/user/{id}/followers", + get(auth::social::followers_request), + ) // warnings + .route("/warnings/{id}", get(auth::user_warnings::get_request)) .route("/warnings/{id}", post(auth::user_warnings::create_request)) .route( "/warnings/{id}", delete(auth::user_warnings::delete_request), ) + .route( + "/warnings/on_user/{id}", + post(auth::user_warnings::on_user_request), + ) // notifications .route( "/notifications/my", @@ -275,6 +342,7 @@ pub fn routes() -> Router { "/notifications/all/read_status", post(notifications::update_all_read_status_request), ) + .route("/notifications/my", get(notifications::get_list_request)) // community memberships .route( "/communities/{id}/join", @@ -304,6 +372,7 @@ pub fn routes() -> Router { delete(requests::delete_request), ) .route("/requests/my", delete(requests::delete_all_request)) + .route("/requests/my", get(requests::get_list_request)) // connections .route( "/auth/user/connections/_data", diff --git a/crates/app/src/routes/api/v1/notifications.rs b/crates/app/src/routes/api/v1/notifications.rs index aa8664b..06b2397 100644 --- a/crates/app/src/routes/api/v1/notifications.rs +++ b/crates/app/src/routes/api/v1/notifications.rs @@ -1,8 +1,12 @@ use super::UpdateNotificationRead; -use crate::{State, get_user_from_token}; -use axum::{Extension, Json, extract::Path, response::IntoResponse}; +use crate::{get_user_from_token, routes::pages::PaginatedQuery, State}; +use axum::{ + extract::{Path, Query}, + response::IntoResponse, + Extension, Json, +}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{ApiReturn, Error}; +use tetratto_core::model::{oauth, ApiReturn, Error}; pub async fn delete_request( jar: CookieJar, @@ -10,7 +14,7 @@ pub async fn delete_request( Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageNotifications) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -30,7 +34,7 @@ pub async fn delete_all_request( Extension(data): Extension, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageNotifications) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -51,7 +55,7 @@ pub async fn delete_all_by_tag_request( Path(tag): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageNotifications) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -73,7 +77,7 @@ pub async fn update_read_status_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageNotifications) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -94,7 +98,7 @@ pub async fn update_all_read_status_request( Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageNotifications) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -108,3 +112,27 @@ pub async fn update_all_read_status_request( Err(e) => Json(e.into()), } } + +pub async fn get_list_request( + jar: CookieJar, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadNotifications) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data + .get_notifications_by_owner_paginated(user.id, 12, props.page) + .await + { + Ok(l) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(l), + }), + Err(e) => Json(e.into()), + } +} diff --git a/crates/app/src/routes/api/v1/requests.rs b/crates/app/src/routes/api/v1/requests.rs index 64da0ac..0169b72 100644 --- a/crates/app/src/routes/api/v1/requests.rs +++ b/crates/app/src/routes/api/v1/requests.rs @@ -1,7 +1,11 @@ -use crate::{State, get_user_from_token}; -use axum::{Extension, Json, extract::Path, response::IntoResponse}; +use crate::{get_user_from_token, routes::pages::PaginatedQuery, State}; +use axum::{ + extract::{Path, Query}, + response::IntoResponse, + Extension, Json, +}; use axum_extra::extract::CookieJar; -use tetratto_core::model::{ApiReturn, Error}; +use tetratto_core::model::{oauth, ApiReturn, Error}; pub async fn delete_request( jar: CookieJar, @@ -9,7 +13,7 @@ pub async fn delete_request( Path((id, linked_asset)): Path<(usize, usize)>, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageRequests) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -29,7 +33,7 @@ pub async fn delete_all_request( Extension(data): Extension, ) -> impl IntoResponse { let data = &(data.read().await).0; - let user = match get_user_from_token!(jar, data) { + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserManageRequests) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; @@ -43,3 +47,27 @@ pub async fn delete_all_request( Err(e) => Json(e.into()), } } + +pub async fn get_list_request( + jar: CookieJar, + Extension(data): Extension, + Query(props): Query, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data, oauth::AppScope::UserReadRequests) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data + .get_requests_by_owner_paginated(user.id, 12, props.page) + .await + { + Ok(l) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(l), + }), + Err(e) => Json(e.into()), + } +} diff --git a/crates/app/src/routes/pages/communities.rs b/crates/app/src/routes/pages/communities.rs index 9d7605d..d2363b8 100644 --- a/crates/app/src/routes/pages/communities.rs +++ b/crates/app/src/routes/pages/communities.rs @@ -738,7 +738,7 @@ pub async fn post_request( // ... let ignore_users = crate::ignore_users_gen!(user, data); - let feed = match data.0.get_post_comments(post.id, 12, props.page).await { + let feed = match data.0.get_replies_by_post(post.id, 12, props.page).await { Ok(p) => match data.0.fill_posts(p, &ignore_users, &user).await { Ok(p) => p, Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)), diff --git a/crates/core/src/database/posts.rs b/crates/core/src/database/posts.rs index 0f4d73c..5a6b2f1 100644 --- a/crates/core/src/database/posts.rs +++ b/crates/core/src/database/posts.rs @@ -136,7 +136,7 @@ impl DataManager { /// * `id` - the ID of the post the requested posts are commenting on /// * `batch` - the limit of posts in each page /// * `page` - the page number - pub async fn get_post_comments( + pub async fn get_replies_by_post( &self, id: usize, batch: usize, @@ -517,6 +517,30 @@ impl DataManager { out } + /// Filter to update posts to clean their owner for public APIs. + pub fn posts_owner_filter(&self, posts: &Vec) -> Vec { + let mut out: Vec = Vec::new(); + + for mut post in posts.clone() { + post.1.clean(); + + // reposting + if let Some((ref mut x, _)) = post.3 { + x.clean(); + } + + // question + if let Some((_, ref mut x)) = post.4 { + x.clean(); + } + + // ... + out.push(post); + } + + out + } + /// Get all posts from the given user (from most recent). /// /// # Arguments diff --git a/crates/core/src/database/user_warnings.rs b/crates/core/src/database/user_warnings.rs index 2094b59..4aebfca 100644 --- a/crates/core/src/database/user_warnings.rs +++ b/crates/core/src/database/user_warnings.rs @@ -27,7 +27,7 @@ impl DataManager { } } - auto_method!(get_user_warning_by_ip(&str)@get_user_warning_from_row -> "SELECT * FROM user_warnings WHERE ip = $1" --name="user warning" --returns=UserWarning --cache-key-tmpl="atto.user_warning:{}"); + auto_method!(get_user_warning_by_id(usize)@get_user_warning_from_row -> "SELECT * FROM user_warnings WHERE id = $1" --name="user warning" --returns=UserWarning --cache-key-tmpl="atto.user_warning:{}"); /// Get all user warnings by user (paginated). /// diff --git a/crates/core/src/database/userfollows.rs b/crates/core/src/database/userfollows.rs index 1d48705..12046c5 100644 --- a/crates/core/src/database/userfollows.rs +++ b/crates/core/src/database/userfollows.rs @@ -28,6 +28,18 @@ impl DataManager { auto_method!(get_userfollow_by_id()@get_userfollow_from_row -> "SELECT * FROM userfollows WHERE id = $1" --name="user follow" --returns=UserFollow --cache-key-tmpl="atto.userfollow:{}"); + /// Filter to update userfollows to clean their users for public APIs. + pub fn userfollows_user_filter(&self, x: &Vec<(UserFollow, User)>) -> Vec<(UserFollow, User)> { + let mut out: Vec<(UserFollow, User)> = Vec::new(); + + for mut y in x.clone() { + y.1.clean(); + out.push(y); + } + + out + } + /// Get a user follow by `initiator` and `receiver` (in that order). pub async fn get_userfollow_by_initiator_receiver( &self, diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index c99bdaf..af4d529 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -387,6 +387,22 @@ impl User { ) .ok() } + + /// Clean the struct for public viewing. + pub fn clean(&mut self) { + self.password = String::new(); + self.salt = String::new(); + + self.tokens = Vec::new(); + self.grants = Vec::new(); + + self.recovery_codes = Vec::new(); + self.totp = String::new(); + + self.settings = UserSettings::default(); + self.stripe_id = String::new(); + self.connections = HashMap::new(); + } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -446,7 +462,7 @@ impl Notification { } } -#[derive(Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct UserFollow { pub id: usize, pub created: usize, diff --git a/crates/core/src/model/oauth.rs b/crates/core/src/model/oauth.rs index 19e716b..006690f 100644 --- a/crates/core/src/model/oauth.rs +++ b/crates/core/src/model/oauth.rs @@ -36,6 +36,8 @@ pub enum PkceChallengeMethod { #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] pub enum AppScope { + /// Read the profile of other user's on behalf of the user. + UserReadProfiles, /// Read the user's profile (username, bio, etc). UserReadProfile, /// Read the user's settings. @@ -52,6 +54,10 @@ pub enum AppScope { UserReadCommunities, /// Connect to sockets on the user's behalf. UserReadSockets, + /// Read the user's notifications. + UserReadNotifications, + /// Read the user's requests. + UserReadRequests, /// Create posts as the user. UserCreatePosts, /// Create messages as the user. @@ -82,6 +88,16 @@ pub enum AppScope { /// /// Also includes managing the membership of users in the user's communities. UserManageMemberships, + /// Follow/unfollow users on behalf of the user. + UserManageFollowing, + /// Accept follow requests on behalf of the user. + UserManageFollowers, + /// Block/unblock users on behalf of the user. + UserManageBlocks, + /// Manage the user's notifications. + UserManageNotifications, + /// Manage the user's requests. + UserManageRequests, /// Edit posts created by the user. UserEditPosts, /// Edit drafts created by the user. @@ -94,6 +110,8 @@ pub enum AppScope { ModPurgePosts, /// Restore deleted posts. ModDeletePosts, + /// Manage user warnings. + ModManageWarnings, /// Get a list of all emojis available to the user. UserReadEmojis, /// Create emojis on behalf of the user. @@ -116,6 +134,7 @@ impl AppScope { let mut out: Vec = Vec::new(); for scope in input.split(" ") { out.push(match scope { + "user-read-profiles" => Self::UserReadProfiles, "user-read-profile" => Self::UserReadProfile, "user-read-settings" => Self::UserReadSettings, "user-read-sessions" => Self::UserReadSessions, @@ -124,6 +143,8 @@ impl AppScope { "user-read-drafts" => Self::UserReadDrafts, "user-read-communities" => Self::UserReadCommunities, "user-read-sockets" => Self::UserReadSockets, + "user-read-notifications" => Self::UserReadNotifications, + "user-read-requests" => Self::UserReadRequests, "user-create-posts" => Self::UserCreatePosts, "user-create-messages" => Self::UserCreateMessages, "user-create-questions" => Self::UserCreateQuestions, @@ -138,12 +159,18 @@ impl AppScope { "user-manage-stacks" => Self::UserManageStacks, "user-manage-relationships" => Self::UserManageRelationships, "user-manage-memberships" => Self::UserManageMemberships, + "user-manage-following" => Self::UserManageFollowing, + "user-manage-followers" => Self::UserManageFollowers, + "user-manage-blocks" => Self::UserManageBlocks, + "user-manage-notifications" => Self::UserManageNotifications, + "user-manage-requests" => Self::UserManageRequests, "user-edit-posts" => Self::UserEditPosts, "user-edit-drafts" => Self::UserEditDrafts, "user-vote" => Self::UserVote, "user-join-communities" => Self::UserJoinCommunities, "mod-purge-posts" => Self::ModPurgePosts, "mod-delete-posts" => Self::ModDeletePosts, + "mod-manage-warnings" => Self::ModManageWarnings, "user-read-emojis" => Self::UserReadEmojis, "community-create-emojis" => Self::CommunityCreateEmojis, "community-manage-emojis" => Self::CommunityManageEmojis,