add: custom emojis

fix: don't show reposts of posts from blocked users
fix: don't show questions when they're from users you've blocked
This commit is contained in:
trisua 2025-05-10 21:58:02 -04:00
parent 9f187039e6
commit 275dd0a1eb
25 changed files with 697 additions and 61 deletions

View file

@ -1,7 +1,12 @@
use std::time::Duration;
use axum::{http::HeaderMap, response::IntoResponse, Extension, Json};
use tetratto_core::model::{auth::Notification, permissions::FinePermission, ApiReturn, Error};
use tetratto_core::model::{
auth::{User, Notification},
moderation::AuditLogEntry,
permissions::FinePermission,
ApiReturn, Error,
};
use stripe::{EventObject, EventType};
use crate::State;
@ -70,14 +75,50 @@ pub async fn stripe_webhook(
let customer_id = invoice.customer.unwrap().id();
// allow 30s for everything to finalize
tokio::time::sleep(Duration::from_secs(30)).await;
// pull user and update role
let user = match data.get_user_by_stripe_id(customer_id.as_str()).await {
Ok(ua) => ua,
Err(e) => return Json(e.into()),
};
let mut retries: usize = 0;
let mut user: Option<User> = None;
loop {
if retries >= 5 {
// we've already tried 5 times (10 seconds of waiting)... it's not
// going to happen
//
// we're going to report this error to the audit log so someone can
// check manually later
if let Err(e) = data
.create_audit_log_entry(AuditLogEntry::new(
0,
format!("invoice tier update failed: stripe {customer_id}"),
))
.await
{
return Json(e.into());
}
return Json(Error::GeneralNotFound("user".to_string()).into());
}
match data.get_user_by_stripe_id(customer_id.as_str()).await {
Ok(ua) => {
if !user.is_none() {
break;
}
user = Some(ua);
break;
}
Err(_) => {
tracing::info!("checkout session not stored in db yet");
retries += 1;
tokio::time::sleep(Duration::from_secs(2)).await;
continue;
}
}
}
let user = user.unwrap();
tracing::info!("found subscription user in {retries} tries");
if user.permissions.check(FinePermission::SUPPORTER) {
return Json(ApiReturn {

View file

@ -216,7 +216,7 @@ pub async fn logout_request(
Json(ApiReturn {
ok: true,
message: "Goodbye!".to_string(),
payload: (),
payload: Some(user.username.clone()),
}),
)
}

View file

@ -1,4 +1,19 @@
use axum::response::IntoResponse;
use std::fs::exists;
use image::ImageFormat;
use pathbufd::PathBufD;
use crate::{
get_user_from_token,
image::{save_buffer, Image},
routes::api::v1::{auth::images::read_image, UpdateEmojiName},
State,
};
use axum::{body::Body, extract::Path, response::IntoResponse, Extension, Json};
use axum_extra::extract::CookieJar;
use tetratto_core::model::{
uploads::{CustomEmoji, MediaType, MediaUpload},
ApiReturn, Error,
};
/// Expand a unicode emoji into its Gemoji shortcode.
pub async fn get_emoji_shortcode(emoji: String) -> impl IntoResponse {
@ -10,3 +25,209 @@ pub async fn get_emoji_shortcode(emoji: String) -> impl IntoResponse {
None => String::new(),
}
}
pub async fn get_request(
Path((community, name)): Path<(usize, String)>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
if community == 0 {
return Err((
[("Content-Type", "image/svg+xml")],
Body::from(read_image(PathBufD::current().extend(&[
data.0.dirs.media.as_str(),
"images",
"default-avatar.svg",
]))),
));
}
let emoji = match data.get_emoji_by_community_name(community, &name).await {
Ok(ua) => ua,
Err(_) => {
return Err((
[("Content-Type", "image/svg+xml")],
Body::from(read_image(PathBufD::current().extend(&[
data.0.dirs.media.as_str(),
"images",
"default-avatar.svg",
]))),
));
}
};
let upload = data
.get_upload_by_id(emoji.0.unwrap().upload_id)
.await
.unwrap();
let path = upload.path(&data.0);
if !exists(&path).unwrap() {
return Err((
[("Content-Type", "image/svg+xml")],
Body::from(read_image(PathBufD::current().extend(&[
data.0.dirs.media.as_str(),
"images",
"default-avatar.svg",
]))),
));
}
Ok((
[("Content-Type", upload.what.mime())],
Body::from(read_image(path)),
))
}
// maximum file dimensions: 512x512px (256KiB)
pub const MAXIUMUM_FILE_SIZE: usize = 262144;
pub async fn create_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path((community, name)): Path<(usize, String)>,
img: Image,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
// check file size
if img.0.len() > MAXIUMUM_FILE_SIZE {
return Json(Error::DataTooLong("image".to_string()).into());
}
// make sure emoji doesn't already exist in community
if data
.get_emoji_by_community_name(community, &name)
.await
.is_ok()
{
return Json(
Error::MiscError("This emoji name is already in use in this community".to_string())
.into(),
);
}
// create upload
let upload = match data
.create_upload(MediaUpload::new(
if img.1 == "image/gif" {
MediaType::Gif
} else {
MediaType::Webp
},
user.id,
))
.await
{
Ok(u) => u,
Err(e) => return Json(e.into()),
};
// create emoji
let is_animated = img.1 == "image/gif";
match data
.create_emoji(CustomEmoji::new(
user.id,
community,
upload.id,
name,
is_animated,
))
.await
{
Ok(_) => {
if let Err(e) = save_buffer(
&upload.path(&data.0).to_string(),
img.0.to_vec(),
if is_animated {
ImageFormat::Gif
} else {
ImageFormat::WebP
},
) {
return Json(Error::MiscError(e.to_string()).into());
}
Json(ApiReturn {
ok: true,
message: "Emoji created".to_string(),
payload: (),
})
}
Err(e) => {
if let Err(e) = upload.remove(&data.0) {
return Json(e.into());
}
Json(e.into())
}
}
}
pub async fn update_name_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
Json(req): Json<UpdateEmojiName>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.update_emoji_name(id, user, &req.name).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Emoji updated".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn delete_request(
jar: CookieJar,
Extension(data): Extension<State>,
Path(id): Path<usize>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.delete_emoji(id, &user).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "Emoji deleted".to_string(),
payload: (),
}),
Err(e) => Json(e.into()),
}
}
pub async fn get_my_request(
jar: CookieJar,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = &(data.read().await).0;
let user = match get_user_from_token!(jar, data) {
Some(ua) => ua,
None => return Json(Error::NotAllowed.into()),
};
match data.get_user_emojis(user.id).await {
Ok(d) => Json(ApiReturn {
ok: true,
message: d.len().to_string(),
payload: Some(d),
}),
Err(e) => Json(e.into()),
}
}

View file

@ -322,6 +322,23 @@ pub fn routes() -> Router {
"/lookup_emoji",
post(communities::emojis::get_emoji_shortcode),
)
.route("/my_emojis", get(communities::emojis::get_my_request))
.route(
"/communities/{id}/emojis/{name}",
get(communities::emojis::get_request),
)
.route(
"/communities/{id}/emojis/{name}",
post(communities::emojis::create_request),
)
.route(
"/emojis_id/{id}/name",
post(communities::emojis::update_name_request),
)
.route(
"/emojis_id/{id}",
delete(communities::emojis::delete_request),
)
// stacks
.route("/stacks", post(stacks::create_request))
.route("/stacks/{id}/name", post(stacks::update_name_request))
@ -547,3 +564,8 @@ pub struct UpdateStackSort {
pub struct AddOrRemoveStackUser {
pub username: String,
}
#[derive(Deserialize)]
pub struct UpdateEmojiName {
pub name: String,
}