use axum::{ extract::{Path, Query}, http::{HeaderMap, HeaderValue}, response::IntoResponse, Extension, Json, }; use axum_extra::extract::CookieJar; use tetratto_core::model::{ addr::RemoteAddr, communities::{Poll, PollVote, Post}, oauth, permissions::FinePermission, uploads::{MediaType, MediaUpload}, ApiReturn, Error, }; use crate::{ check_user_blocked_or_private, get_user_from_token, image::{save_webp_buffer, JsonMultipart}, routes::{ api::v1::{ CreatePost, CreateRepost, UpdatePostContent, UpdatePostContext, UpdatePostIsOpen, VoteInPoll, }, pages::{PaginatedQuery, SearchedQuery}, }, State, }; // maximum file dimensions: 2048x2048px (4 MiB) pub const MAXIMUM_FILE_SIZE: usize = 4194304; pub async fn create_request( jar: CookieJar, headers: HeaderMap, Extension(data): Extension, JsonMultipart(images, req): JsonMultipart, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreatePosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; if !user.permissions.check(FinePermission::SUPPORTER) { if images.len() > 0 { // this is currently supporter only until it's been tested better... // after it's fully release, file limit will be raised to 8 MiB for supporters, // and left at 4 for non-supporters return Json(Error::RequiresSupporter.into()); } } if images.len() > 4 { return Json( Error::MiscError("Too many uploads. Please use a maximum of 4".to_string()).into(), ); } // get real ip let real_ip = headers .get(data.0.0.security.real_ip_header.to_owned()) .unwrap_or(&HeaderValue::from_static("")) .to_str() .unwrap_or("") .to_string(); // check for ip ban if data .get_ipban_by_addr(RemoteAddr::from(real_ip.as_str())) .await .is_ok() { return Json(Error::NotAllowed.into()); } // create poll let poll_id = if let Some(p) = req.poll { match data .create_poll(Poll::new( user.id, if let Some(expires) = p.expires { expires } else { 86400000 }, p.option_a, p.option_b, p.option_c, p.option_d, )) .await { Ok(p) => p, Err(e) => return Json(e.into()), } } else { 0 }; // ... let mut props = Post::new( req.content, match req.community.parse::() { Ok(x) => x, Err(e) => return Json(Error::MiscError(e.to_string()).into()), }, if let Some(rt) = req.replying_to { match rt.parse::() { Ok(x) => Some(x), Err(e) => return Json(Error::MiscError(e.to_string()).into()), } } else { None }, user.id, poll_id, ); if !req.answering.is_empty() { // we're answering a question! props.context.answering = match req.answering.parse::() { Ok(x) => x, Err(e) => return Json(Error::MiscError(e.to_string()).into()), }; } else { props.title = req.title; props.stack = match req.stack.parse::() { Ok(x) => x, Err(e) => return Json(Error::MiscError(e.to_string()).into()), }; } // check sizes for img in &images { if img.len() > MAXIMUM_FILE_SIZE { return Json(Error::FileTooLarge.into()); } } // create uploads for _ in 0..images.len() { props.uploads.push( match data .create_upload(MediaUpload::new(MediaType::Webp, props.owner)) .await { Ok(u) => u.id, Err(e) => return Json(e.into()), }, ); } // ... match data.create_post(props.clone()).await { Ok(id) => { // write to uploads for (i, upload_id) in props.uploads.iter().enumerate() { let image = match images.get(i) { Some(img) => img, None => { if let Err(e) = data.delete_upload(*upload_id).await { return Json(e.into()); } continue; } }; let upload = match data.get_upload_by_id(*upload_id).await { Ok(u) => u, Err(e) => return Json(e.into()), }; if let Err(e) = save_webp_buffer(&upload.path(&data.0.0).to_string(), image.to_vec(), None) { return Json(Error::MiscError(e.to_string()).into()); } } // return Json(ApiReturn { ok: true, message: "Post created".to_string(), payload: Some(id.to_string()), }) } Err(e) => Json(e.into()), } } pub async fn create_repost_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data, oauth::AppScope::UserCreatePosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; let mut props = Post::repost( req.content, match req.community.parse::() { Ok(x) => x, Err(e) => return Json(Error::MiscError(e.to_string()).into()), }, user.id, id, ); props.stack = match req.stack.parse::() { Ok(x) => x, Err(e) => return Json(Error::MiscError(e.to_string()).into()), }; // ... match data.create_post(props).await { Ok(id) => Json(ApiReturn { ok: true, message: "Post reposted".to_string(), payload: Some(id.to_string()), }), Err(e) => Json(e.into()), } } pub async fn delete_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data, oauth::AppScope::UserDeletePosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; match data.fake_delete_post(id, user, true).await { Ok(_) => Json(ApiReturn { ok: true, message: "Post deleted".to_string(), payload: (), }), Err(e) => Json(e.into()), } } pub async fn purge_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data, oauth::AppScope::ModPurgePosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; if !user.permissions.check(FinePermission::MANAGE_POSTS) { return Json(Error::NotAllowed.into()); } match data.delete_post(id, user).await { Ok(_) => Json(ApiReturn { ok: true, message: "Post deleted".to_string(), payload: (), }), Err(e) => Json(e.into()), } } pub async fn restore_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data, oauth::AppScope::ModDeletePosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; if !user.permissions.check(FinePermission::MANAGE_POSTS) { return Json(Error::NotAllowed.into()); } match data.fake_delete_post(id, user, false).await { Ok(_) => Json(ApiReturn { ok: true, message: "Post restored".to_string(), payload: (), }), Err(e) => Json(e.into()), } } pub async fn update_content_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data, oauth::AppScope::UserEditPosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; match data.update_post_content(id, user, req.content).await { Ok(_) => Json(ApiReturn { ok: true, message: "Post updated".to_string(), payload: (), }), Err(e) => Json(e.into()), } } pub async fn update_context_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data, oauth::AppScope::UserEditPosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; // check lengths if req.context.tags.len() > 512 { return Json(Error::DataTooLong("tags".to_string()).into()); } if req.context.content_warning.len() > 512 { return Json(Error::DataTooLong("warning".to_string()).into()); } // ... match data.update_post_context(id, user, req.context).await { Ok(_) => Json(ApiReturn { ok: true, message: "Post updated".to_string(), payload: (), }), Err(e) => Json(e.into()), } } pub async fn vote_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data, oauth::AppScope::UserVote) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; let post = match data.get_post_by_id(id).await { Ok(p) => p, Err(e) => return Json(e.into()), }; let poll = match data.get_poll_by_id(post.poll_id).await { Ok(p) => p, Err(e) => return Json(e.into()), }; // check associated accounts for prior votes for id in user.associated { if data.get_pollvote_by_owner_poll(id, poll.id).await.is_ok() { return Json( Error::MiscError( "You've already voted on this poll on a different account".to_string(), ) .into(), ); } } // ... match data .create_pollvote(PollVote::new(user.id, poll.id, req.option)) .await { Ok(_) => Json(ApiReturn { ok: true, message: "Vote cast".to_string(), payload: (), }), Err(e) => Json(e.into()), } } pub async fn update_is_open_request( jar: CookieJar, Extension(data): Extension, Path(id): Path, Json(req): Json, ) -> impl IntoResponse { let data = &(data.read().await).0; let user = match get_user_from_token!(jar, data, oauth::AppScope::UserEditPosts) { Some(ua) => ua, None => return Json(Error::NotAllowed.into()), }; match data.update_post_is_open(id, user, req.open).await { Ok(_) => Json(ApiReturn { ok: true, message: "Post updated".to_string(), payload: (), }), 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_posts_from_user_communities(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 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()), } }