tetratto/crates/app/src/routes/pages/profile.rs

848 lines
24 KiB
Rust

use super::{render_error, PaginatedQuery, ProfileQuery};
use crate::{
assets::initial_context, check_user_blocked_or_private, get_lang, get_user_from_token,
ignore_users_gen, State,
};
use axum::{
Extension,
extract::{Path, Query},
response::{Html, IntoResponse},
};
use axum_extra::extract::CookieJar;
use serde::Deserialize;
use tera::Context;
use tetratto_core::model::{auth::User, communities::Community, permissions::FinePermission, Error};
use tetratto_shared::hash::hash;
use contrasted::{Color, MINIMUM_CONTRAST_THRESHOLD};
#[derive(Deserialize)]
pub struct SettingsProps {
#[serde(default)]
pub username: String,
#[serde(default)]
pub page: usize,
}
/// `/settings`
pub async fn settings_request(
jar: CookieJar,
Extension(data): Extension<State>,
Query(req): Query<SettingsProps>,
) -> impl IntoResponse {
let data = data.read().await;
let user = match get_user_from_token!(jar, data.0) {
Some(ua) => ua,
None => {
return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &None).await,
));
}
};
let profile = if req.username.is_empty() | !user.permissions.check(FinePermission::MANAGE_USERS)
{
user.clone()
} else {
match data.0.get_user_by_username(&req.username).await {
Ok(ua) => ua,
Err(e) => {
return Err(Html(render_error(e, &jar, &data, &None).await));
}
}
};
let stacks = match data.0.get_stacks_by_user(profile.id).await {
Ok(ua) => ua,
Err(e) => {
return Err(Html(render_error(e, &jar, &data, &None).await));
}
};
let following = match data
.0
.fill_userfollows_with_receiver(
data.0
.get_userfollows_by_initiator_all(profile.id)
.await
.unwrap_or(Vec::new()),
)
.await
{
Ok(r) => r,
Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)),
};
let blocks = match data
.0
.fill_userblocks_receivers(data.0.get_userblocks_by_initiator(profile.id).await)
.await
{
Ok(r) => r,
Err(e) => return Err(Html(render_error(e, &jar, &data, &None).await)),
};
let stackblocks = {
let mut out = Vec::new();
for block in data.0.get_stackblocks_by_initiator(profile.id).await {
out.push(match data.0.get_stack_by_id(block.stack).await {
Ok(s) => s,
Err(_) => continue,
});
}
out
};
let uploads = match data.0.get_uploads_by_owner(profile.id, 12, req.page).await {
Ok(ua) => ua,
Err(e) => {
return Err(Html(render_error(e, &jar, &data, &None).await));
}
};
let tokens = profile.tokens.clone();
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &Some(user)).await;
context.insert("profile", &profile);
context.insert("page", &req.page);
context.insert("uploads", &uploads);
context.insert("stacks", &stacks);
context.insert("following", &following);
context.insert("blocks", &blocks);
context.insert("stackblocks", &stackblocks);
context.insert(
"user_tokens_serde",
&serde_json::to_string(&tokens)
.unwrap()
.replace("\"", "\\\""),
);
context.insert("profile_grants", &{
let mut out = Vec::new();
for grant in profile.grants {
out.push((
match data.0.get_app_by_id(grant.app).await {
Ok(a) => a,
// TODO: remove grant from user (app deleted)
Err(_) => continue,
},
grant,
));
}
out
});
// check color contrasts
let mut failing_color_keys: Vec<(&str, f64)> = Vec::new();
let settings_map = serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(
&serde_json::to_string(&profile.settings).unwrap(),
)
.unwrap();
if let Some(color_surface) = settings_map.get("theme_color_surface") {
let color_surface = color_surface.as_str().unwrap();
for setting in &settings_map {
if !setting.0.starts_with("theme_color_text")
| (setting.0 == "theme_color_text_primary")
| (setting.0 == "theme_color_text_secondary")
{
continue;
}
let value = setting.1.as_str().unwrap();
if !value.starts_with("#") {
// we can only parse hex right now
continue;
}
let c1 = Color::from(color_surface);
let c2 = Color::from(value);
let contrast = c1.contrast(&c2);
if contrast < MINIMUM_CONTRAST_THRESHOLD {
failing_color_keys.push((setting.0, contrast));
}
}
context.insert("failing_color_keys", &failing_color_keys);
} else {
context.insert("failing_color_keys", &Vec::<&str>::new());
}
// return
Ok(Html(
data.1.render("profile/settings.html", &context).unwrap(),
))
}
pub fn profile_context(
context: &mut Context,
user: &Option<User>,
profile: &User,
communities: &Vec<Community>,
is_self: bool,
is_following: bool,
is_following_you: bool,
is_blocking: bool,
) {
context.insert("profile", &profile);
context.insert("communities", &communities);
context.insert("is_self", &is_self);
context.insert("is_following", &is_following);
context.insert("is_following_you", &is_following_you);
context.insert("is_blocking", &is_blocking);
context.insert("warning_hash", &hash(profile.settings.warning.clone()));
context.insert(
"is_supporter",
&profile.permissions.check(FinePermission::SUPPORTER),
);
if let Some(ua) = user {
if !ua.settings.disable_other_themes | is_self {
context.insert("use_user_theme", &false);
}
} else {
context.insert("use_user_theme", &false);
}
}
/// `/@{username}`
pub async fn posts_request(
jar: CookieJar,
Path(username): Path<String>,
Query(props): Query<ProfileQuery>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = data.read().await;
let user = get_user_from_token!(jar, data.0);
let other_user = match data.0.get_user_by_username(&username).await {
Ok(ua) => ua,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
check_user_blocked_or_private!(user, other_user, data, jar);
// check for warning
if props.warning {
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await;
context.insert("profile", &other_user);
context.insert("warning_hash", &hash(other_user.settings.warning.clone()));
return Ok(Html(
data.1.render("profile/warning.html", &context).unwrap(),
));
}
// fetch pinned
let ignore_users = ignore_users_gen!(user, data);
let pinned = if props.tag.is_empty() {
match data.0.get_pinned_posts_by_user(other_user.id).await {
Ok(p) => match data
.0
.fill_posts_with_community(
p,
if let Some(ref ua) = user { ua.id } else { 0 },
&ignore_users,
&user,
)
.await
{
Ok(p) => Some(data.0.posts_muted_phrase_filter(
&p,
if let Some(ref ua) = user {
Some(&ua.settings.muted)
} else {
None
},
)),
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
}
} else {
None
};
let communities = match data.0.get_memberships_by_owner(other_user.id).await {
Ok(m) => match data.0.fill_communities(m).await {
Ok(m) => m,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
// init context
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await;
let is_self = if let Some(ref ua) = user {
ua.id == other_user.id
} else {
false
};
let is_following = if let Some(ref ua) = user {
data.0
.get_userfollow_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
let is_following_you = if let Some(ref ua) = user {
data.0
.get_userfollow_by_receiver_initiator(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
let is_blocking = if let Some(ref ua) = user {
data.0
.get_userblock_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
context.insert("pinned", &pinned);
context.insert("page", &props.page);
context.insert("tag", &props.tag);
context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await);
profile_context(
&mut context,
&user,
&other_user,
&communities,
is_self,
is_following,
is_following_you,
is_blocking,
);
// return
Ok(Html(data.1.render("profile/posts.html", &context).unwrap()))
}
/// `/@{username}/replies`
pub async fn replies_request(
jar: CookieJar,
Path(username): Path<String>,
Query(props): Query<PaginatedQuery>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = data.read().await;
let user = get_user_from_token!(jar, data.0);
let other_user = match data.0.get_user_by_username(&username).await {
Ok(ua) => ua,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
check_user_blocked_or_private!(user, other_user, data, jar);
// fetch data
let ignore_users = crate::ignore_users_gen!(user, data);
let posts = match data
.0
.get_replies_by_user(other_user.id, 12, props.page, &user)
.await
{
Ok(p) => match data
.0
.fill_posts_with_community(
p,
if let Some(ref ua) = user { ua.id } else { 0 },
&ignore_users,
&user,
)
.await
{
Ok(p) => data.0.posts_muted_phrase_filter(
&p,
if let Some(ref ua) = user {
Some(&ua.settings.muted)
} else {
None
},
),
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
let communities = match data.0.get_memberships_by_owner(other_user.id).await {
Ok(m) => match data.0.fill_communities(m).await {
Ok(m) => m,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
// init context
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await;
let is_self = if let Some(ref ua) = user {
ua.id == other_user.id
} else {
false
};
let is_following = if let Some(ref ua) = user {
data.0
.get_userfollow_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
let is_following_you = if let Some(ref ua) = user {
data.0
.get_userfollow_by_receiver_initiator(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
let is_blocking = if let Some(ref ua) = user {
data.0
.get_userblock_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
context.insert("posts", &posts);
context.insert("page", &props.page);
context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await);
profile_context(
&mut context,
&user,
&other_user,
&communities,
is_self,
is_following,
is_following_you,
is_blocking,
);
// return
Ok(Html(
data.1.render("profile/replies.html", &context).unwrap(),
))
}
/// `/@{username}/media`
pub async fn media_request(
jar: CookieJar,
Path(username): Path<String>,
Query(props): Query<PaginatedQuery>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = data.read().await;
let user = get_user_from_token!(jar, data.0);
let other_user = match data.0.get_user_by_username(&username).await {
Ok(ua) => ua,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
check_user_blocked_or_private!(user, other_user, data, jar);
// fetch data
let ignore_users = crate::ignore_users_gen!(user, data);
let posts = match data
.0
.get_media_posts_by_user(other_user.id, 12, props.page, &user)
.await
{
Ok(p) => match data
.0
.fill_posts_with_community(
p,
if let Some(ref ua) = user { ua.id } else { 0 },
&ignore_users,
&user,
)
.await
{
Ok(p) => data.0.posts_muted_phrase_filter(
&p,
if let Some(ref ua) = user {
Some(&ua.settings.muted)
} else {
None
},
),
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
let communities = match data.0.get_memberships_by_owner(other_user.id).await {
Ok(m) => match data.0.fill_communities(m).await {
Ok(m) => m,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
// init context
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await;
let is_self = if let Some(ref ua) = user {
ua.id == other_user.id
} else {
false
};
let is_following = if let Some(ref ua) = user {
data.0
.get_userfollow_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
let is_following_you = if let Some(ref ua) = user {
data.0
.get_userfollow_by_receiver_initiator(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
let is_blocking = if let Some(ref ua) = user {
data.0
.get_userblock_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
context.insert("posts", &posts);
context.insert("page", &props.page);
context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await);
profile_context(
&mut context,
&user,
&other_user,
&communities,
is_self,
is_following,
is_following_you,
is_blocking,
);
// return
Ok(Html(data.1.render("profile/media.html", &context).unwrap()))
}
/// `/@{username}/outbox`
pub async fn outbox_request(
jar: CookieJar,
Path(username): Path<String>,
Query(props): Query<PaginatedQuery>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = data.read().await;
let user = match get_user_from_token!(jar, data.0) {
Some(ua) => ua,
None => {
return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &None).await,
));
}
};
let other_user = match data.0.get_user_by_username(&username).await {
Ok(ua) => ua,
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
};
if user.id != other_user.id && !user.permissions.check(FinePermission::MANAGE_QUESTIONS) {
return Err(Html(
render_error(Error::NotAllowed, &jar, &data, &Some(user)).await,
));
}
check_user_blocked_or_private!(Some(user.clone()), other_user, data, jar);
// fetch data
let ignore_users = crate::ignore_users_gen!(user!, data);
let questions = match data
.0
.get_questions_by_owner_paginated(other_user.id, 12, props.page)
.await
{
Ok(p) => match data.0.fill_questions(p, &ignore_users).await {
Ok(p) => p,
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
};
let communities = match data.0.get_memberships_by_owner(other_user.id).await {
Ok(m) => match data.0.fill_communities(m).await {
Ok(m) => m,
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &Some(user)).await)),
};
// init context
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &Some(user.clone())).await;
let is_self = user.id == other_user.id;
let is_following = data
.0
.get_userfollow_by_initiator_receiver(user.id, other_user.id)
.await
.is_ok();
let is_following_you = data
.0
.get_userfollow_by_receiver_initiator(user.id, other_user.id)
.await
.is_ok();
let is_blocking = data
.0
.get_userblock_by_initiator_receiver(user.id, other_user.id)
.await
.is_ok();
context.insert("questions", &questions);
context.insert("page", &props.page);
context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await);
profile_context(
&mut context,
&Some(user),
&other_user,
&communities,
is_self,
is_following,
is_following_you,
is_blocking,
);
// return
Ok(Html(
data.1.render("profile/outbox.html", &context).unwrap(),
))
}
/// `/@{username}/following`
pub async fn following_request(
jar: CookieJar,
Path(username): Path<String>,
Query(props): Query<PaginatedQuery>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = data.read().await;
let user = get_user_from_token!(jar, data.0);
let other_user = match data.0.get_user_by_username(&username).await {
Ok(ua) => ua,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
check_user_blocked_or_private!(user, other_user, data, jar);
// fetch data
let list = match data
.0
.get_userfollows_by_initiator(other_user.id, 12, props.page)
.await
{
Ok(l) => match data.0.fill_userfollows_with_receiver(l).await {
Ok(l) => l,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
let communities = match data.0.get_memberships_by_owner(other_user.id).await {
Ok(m) => match data.0.fill_communities(m).await {
Ok(m) => m,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
// init context
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await;
let is_self = if let Some(ref ua) = user {
ua.id == other_user.id
} else {
false
};
let is_following = if let Some(ref ua) = user {
data.0
.get_userfollow_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
let is_following_you = if let Some(ref ua) = user {
data.0
.get_userfollow_by_receiver_initiator(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
let is_blocking = if let Some(ref ua) = user {
data.0
.get_userblock_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
context.insert("list", &list);
context.insert("page", &props.page);
context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await);
profile_context(
&mut context,
&user,
&other_user,
&communities,
is_self,
is_following,
is_following_you,
is_blocking,
);
// return
Ok(Html(
data.1.render("profile/following.html", &context).unwrap(),
))
}
/// `/@{username}/followers`
pub async fn followers_request(
jar: CookieJar,
Path(username): Path<String>,
Query(props): Query<PaginatedQuery>,
Extension(data): Extension<State>,
) -> impl IntoResponse {
let data = data.read().await;
let user = get_user_from_token!(jar, data.0);
let other_user = match data.0.get_user_by_username(&username).await {
Ok(ua) => ua,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
check_user_blocked_or_private!(user, other_user, data, jar);
// fetch data
let list = match data
.0
.get_userfollows_by_receiver(other_user.id, 12, props.page)
.await
{
Ok(l) => match data.0.fill_userfollows_with_initiator(l).await {
Ok(l) => l,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
let communities = match data.0.get_memberships_by_owner(other_user.id).await {
Ok(m) => match data.0.fill_communities(m).await {
Ok(m) => m,
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
},
Err(e) => return Err(Html(render_error(e, &jar, &data, &user).await)),
};
// init context
let lang = get_lang!(jar, data.0);
let mut context = initial_context(&data.0.0.0, lang, &user).await;
let is_self = if let Some(ref ua) = user {
ua.id == other_user.id
} else {
false
};
let is_following = if let Some(ref ua) = user {
data.0
.get_userfollow_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
let is_following_you = if let Some(ref ua) = user {
data.0
.get_userfollow_by_receiver_initiator(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
let is_blocking = if let Some(ref ua) = user {
data.0
.get_userblock_by_initiator_receiver(ua.id, other_user.id)
.await
.is_ok()
} else {
false
};
context.insert("list", &list);
context.insert("page", &props.page);
context.insert("gpa", &data.0.calculate_user_gpa(other_user.id).await);
profile_context(
&mut context,
&user,
&other_user,
&communities,
is_self,
is_following,
is_following_you,
is_blocking,
);
// return
Ok(Html(
data.1.render("profile/followers.html", &context).unwrap(),
))
}