add: implement 9 new scopes, 21 new api endpoints

This commit is contained in:
trisua 2025-06-13 17:47:00 -04:00
parent c3139ef1d2
commit 8f16068a34
14 changed files with 973 additions and 35 deletions

View file

@ -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

View file

@ -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()
};
}

View file

@ -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<State>,
) -> 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<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::UserManageFollowing) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
@ -95,7 +104,7 @@ pub async fn accept_follow_request(
Path(id): Path<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::UserManageFollowers) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
@ -142,7 +151,7 @@ pub async fn block_request(
Extension(data): Extension<State>,
) -> 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<State>,
) -> 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<usize>,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<usize>,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> 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()),
}
}

View file

@ -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<CreateUserWarning>,
) -> 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<State>,
) -> 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<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<usize>,
Extension(data): Extension<State>,
) -> 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()),
}
}

View file

@ -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<usize>,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<usize>,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<usize>,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<usize>,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<usize>,
Extension(data): Extension<State>,
Query(props): Query<SearchedQuery>,
) -> 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<State>,
Query(props): Query<SearchedQuery>,
) -> 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<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<usize>,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<usize>,
Extension(data): Extension<State>,
) -> 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<usize>,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<usize>,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> 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<usize>,
Extension(data): Extension<State>,
Query(props): Query<PaginatedQuery>,
) -> 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()),
}
}

View file

@ -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",

View file

@ -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<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::UserManageNotifications) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
@ -30,7 +34,7 @@ pub async fn delete_all_request(
Extension(data): Extension<State>,
) -> 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<String>,
) -> 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<UpdateNotificationRead>,
) -> 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<UpdateNotificationRead>,
) -> 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<State>,
Query(props): Query<PaginatedQuery>,
) -> 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()),
}
}

View file

@ -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<State>,
) -> 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<State>,
Query(props): Query<PaginatedQuery>,
) -> 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()),
}
}

View file

@ -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)),

View file

@ -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<FullPost>) -> Vec<FullPost> {
let mut out: Vec<FullPost> = 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

View file

@ -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).
///

View file

@ -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,

View file

@ -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,

View file

@ -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<AppScope> = 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,