diff --git a/crates/app/src/routes/api/v1/communities/posts.rs b/crates/app/src/routes/api/v1/communities/posts.rs index 3c07193..32f4b77 100644 --- a/crates/app/src/routes/api/v1/communities/posts.rs +++ b/crates/app/src/routes/api/v1/communities/posts.rs @@ -668,7 +668,10 @@ pub async fn from_communities_request( None => return Json(Error::NotAllowed.into()), }; - match data.get_popular_posts(12, props.page, 604_800_000).await { + match data + .get_posts_from_user_communities(user.id, 12, props.page) + .await + { Ok(posts) => { let ignore_users = crate::ignore_users_gen!(user!, #data); Json(ApiReturn { diff --git a/crates/app/src/routes/api/v1/communities/questions.rs b/crates/app/src/routes/api/v1/communities/questions.rs index 0b85bff..270197f 100644 --- a/crates/app/src/routes/api/v1/communities/questions.rs +++ b/crates/app/src/routes/api/v1/communities/questions.rs @@ -1,14 +1,23 @@ use axum::{ - extract::Path, + extract::{Path, Query}, http::{HeaderMap, HeaderValue}, response::IntoResponse, Extension, Json, }; use axum_extra::extract::CookieJar; use tetratto_core::model::{ - addr::RemoteAddr, auth::IpBlock, communities::Question, ApiReturn, Error, oauth, + addr::RemoteAddr, + auth::IpBlock, + communities::{CommunityReadAccess, Question}, + oauth, + permissions::FinePermission, + ApiReturn, Error, +}; +use crate::{ + get_user_from_token, + routes::{api::v1::CreateQuestion, pages::PaginatedQuery}, + State, }; -use crate::{get_user_from_token, routes::api::v1::CreateQuestion, State}; pub async fn create_request( jar: CookieJar, @@ -131,3 +140,261 @@ pub async fn ip_block_request( Err(e) => Json(e.into()), } } + +/// Get questions by the current user. +pub async fn outbox_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::UserReadQuestions) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if user.id != id && !user.permissions.check(FinePermission::MANAGE_QUESTIONS) { + return Json(Error::NotAllowed.into()); + } + + match data + .get_questions_by_owner_paginated(id, 12, props.page) + .await + { + Ok(questions) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data.fill_questions(questions, &ignore_users).await { + Ok(l) => Some(data.questions_owner_filter(&l)), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get questions in the given community. +pub async fn community_questions_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::UserReadQuestions) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + let community = match data.get_community_by_id(id).await { + Ok(c) => c, + Err(e) => return Json(e.into()), + }; + + if community.read_access == CommunityReadAccess::Joined { + if data + .get_membership_by_owner_community_no_void(user.id, community.id) + .await + .is_err() + { + return Json(Error::NotAllowed.into()); + } + } + + match data.get_questions_by_community(id, 12, props.page).await { + Ok(questions) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data.fill_questions(questions, &ignore_users).await { + Ok(l) => Some(data.questions_owner_filter(&l)), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get all questions (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::UserReadQuestions) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data + .get_questions_from_user_communities(user.id, 12, props.page) + .await + { + Ok(questions) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data.fill_questions(questions, &ignore_users).await { + Ok(l) => Some(data.questions_owner_filter(&l)), + 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::UserReadQuestions) { + 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 questions (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::UserReadQuestions) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data.get_latest_global_questions(12, props.page).await { + Ok(questions) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data.fill_questions(questions, &ignore_users).await { + Ok(l) => Some(data.questions_owner_filter(&l)), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get all questions (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::UserReadQuestions) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + match data + .get_questions_from_user_following(user.id, 12, props.page) + .await + { + Ok(questions) => { + let ignore_users = crate::ignore_users_gen!(user!, #data); + Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: match data.fill_questions(questions, &ignore_users).await { + Ok(l) => Some(data.questions_owner_filter(&l)), + Err(e) => return Json(e.into()), + }, + }) + } + Err(e) => Json(e.into()), + } +} + +/// Get a single question. +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::UserReadQuestions).is_none() { + return Json(Error::NotAllowed.into()); + } + + match data.get_question_by_id(id).await { + Ok(p) => Json(ApiReturn { + ok: true, + message: "Success".to_string(), + payload: Some(p), + }), + Err(e) => Json(e.into()), + } +} + +/// Get answers for the given question. +pub async fn answers_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_question(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 242dceb..0b5128b 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -210,6 +210,35 @@ pub fn routes() -> Router { "/questions/{id}/block_ip", post(communities::questions::ip_block_request), ) + .route( + "/questions/from_user/{id}", + get(communities::questions::outbox_request), + ) + .route( + "/questions/from_community/{id}", + get(communities::questions::community_questions_request), + ) + .route( + "/questions/timeline/communities", + get(communities::questions::from_communities_request), + ) + .route( + "/questions/timeline/popular", + get(communities::questions::popular_request), + ) + .route( + "/questions/timeline/all", + get(communities::questions::all_request), + ) + .route( + "/questions/timeline/following", + get(communities::questions::following_request), + ) + .route("/questions/{id}", get(communities::questions::get_request)) + .route( + "/questions/{id}/answers", + get(communities::questions::answers_request), + ) // auth // global .route("/auth/register", post(auth::register_request)) diff --git a/crates/core/src/database/questions.rs b/crates/core/src/database/questions.rs index 7748bfe..f76efc6 100644 --- a/crates/core/src/database/questions.rs +++ b/crates/core/src/database/questions.rs @@ -76,6 +76,21 @@ impl DataManager { Ok(out) } + /// Filter to update questions to clean their owner for public APIs. + pub fn questions_owner_filter( + &self, + questions: &Vec<(Question, User)>, + ) -> Vec<(Question, User)> { + let mut out: Vec<(Question, User)> = Vec::new(); + + for mut question in questions.clone() { + question.1.clean(); + out.push(question); + } + + out + } + /// Get all questions by `owner`. pub async fn get_questions_by_owner(&self, owner: usize) -> Result> { let conn = match self.0.connect().await { diff --git a/crates/core/src/model/oauth.rs b/crates/core/src/model/oauth.rs index 6ac3e83..a4e361f 100644 --- a/crates/core/src/model/oauth.rs +++ b/crates/core/src/model/oauth.rs @@ -58,6 +58,8 @@ pub enum AppScope { UserReadNotifications, /// Read the user's requests. UserReadRequests, + /// Read questions as the user. + UserReadQuestions, /// Create posts as the user. UserCreatePosts, /// Create messages as the user. @@ -155,6 +157,7 @@ impl AppScope { "user-read-sockets" => Self::UserReadSockets, "user-read-notifications" => Self::UserReadNotifications, "user-read-requests" => Self::UserReadRequests, + "user-read-questions" => Self::UserReadQuestions, "user-create-posts" => Self::UserCreatePosts, "user-create-messages" => Self::UserCreateMessages, "user-create-questions" => Self::UserCreateQuestions,