add: user follow requests

add: nsfw questions
fix: inherit nsfw status from questions
fix: inherit community from questions
This commit is contained in:
trisua 2025-04-14 17:21:52 -04:00
parent d6c7372610
commit ad17acec98
24 changed files with 492 additions and 59 deletions

View file

@ -1,6 +1,6 @@
[package]
name = "tetratto-core"
version = "1.0.4"
version = "1.0.5"
edition = "2024"
[features]

View file

@ -621,7 +621,7 @@ impl DataManager {
}
if !question.is_global {
self.delete_request(question.owner, question.id, &owner)
self.delete_request(question.owner, question.id, &owner, false)
.await?;
} else {
self.incr_question_answer_count(data.context.answering)
@ -629,15 +629,23 @@ impl DataManager {
}
// create notification for question owner
self.create_notification(Notification::new(
"Your question has received a new answer!".to_string(),
format!(
"[@{}](/api/v1/auth/user/find/{}) has answered your [question](/question/{}).",
owner.username, owner.id, question.id
),
question.owner,
))
.await?;
// (if the current user isn't the owner)
if question.owner != data.owner {
self.create_notification(Notification::new(
"Your question has received a new answer!".to_string(),
format!(
"[@{}](/api/v1/auth/user/find/{}) has answered your [question](/question/{}).",
owner.username, owner.id, question.id
),
question.owner,
))
.await?;
}
// inherit nsfw status if we didn't get it from the community
if question.context.is_nsfw {
data.context.is_nsfw = question.context.is_nsfw;
}
}
// check if we're reposting a post

View file

@ -37,6 +37,8 @@ impl DataManager {
// likes
likes: get!(x->8(i32)) as isize,
dislikes: get!(x->9(i32)) as isize,
// ...
context: serde_json::from_str(&get!(x->10(String))).unwrap(),
}
}
@ -300,6 +302,9 @@ impl DataManager {
{
return Err(Error::QuestionsDisabled);
}
// inherit nsfw status
data.context.is_nsfw = community.context.is_nsfw;
} else {
let receiver = self.get_user_by_id(data.receiver).await?;
@ -323,7 +328,7 @@ impl DataManager {
let res = execute!(
&conn,
"INSERT INTO questions VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)",
"INSERT INTO questions VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
params![
&(data.id as i64),
&(data.created as i64),
@ -334,7 +339,8 @@ impl DataManager {
&0_i32,
&(data.community as i64),
&0_i32,
&0_i32
&0_i32,
&serde_json::to_string(&data.context).unwrap()
]
);
@ -404,7 +410,7 @@ impl DataManager {
{
// requests are also deleted when a post is created answering the given question
// (unless the question is global)
self.delete_request(y.owner, y.id, user).await?;
self.delete_request(y.owner, y.id, user, false).await?;
}
// delete all posts answering question

View file

@ -119,13 +119,21 @@ impl DataManager {
Ok(())
}
pub async fn delete_request(&self, id: usize, linked_asset: usize, user: &User) -> Result<()> {
pub async fn delete_request(
&self,
id: usize,
linked_asset: usize,
user: &User,
force: bool,
) -> Result<()> {
let y = self
.get_request_by_id_linked_asset(id, linked_asset)
.await?;
if user.id != y.owner && !user.permissions.check(FinePermission::MANAGE_REQUESTS) {
return Err(Error::NotAllowed);
if !force {
if user.id != y.owner && !user.permissions.check(FinePermission::MANAGE_REQUESTS) {
return Err(Error::NotAllowed);
}
}
let conn = match self.connect().await {
@ -133,13 +141,21 @@ impl DataManager {
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
};
let res = execute!(&conn, "DELETE FROM requests WHERE id = $1", &[&(id as i64)]);
let res = execute!(
&conn,
"DELETE FROM requests WHERE id = $1",
&[&(y.id as i64)]
);
if let Err(e) = res {
return Err(Error::DatabaseError(e.to_string()));
}
self.2.remove(format!("atto.request:{}", id)).await;
self.2.remove(format!("atto.request:{}", y.id)).await;
self.2
.remove(format!("atto.request:{}:{}", id, linked_asset))
.await;
// decr request count
let owner = self.get_user_by_id(y.owner).await?;
@ -159,7 +175,8 @@ impl DataManager {
return Err(Error::NotAllowed);
}
self.delete_request(x.id, x.linked_asset, user).await?;
self.delete_request(x.id, x.linked_asset, user, false)
.await?;
// delete question
if x.action_type == ActionType::Answer {

View file

@ -1,5 +1,7 @@
use super::*;
use crate::cache::Cache;
use crate::model::auth::FollowResult;
use crate::model::requests::{ActionRequest, ActionType};
use crate::model::{Error, Result, auth::User, auth::UserFollow, permissions::FinePermission};
use crate::{auto_method, execute, get, query_row, query_rows, params};
@ -219,7 +221,26 @@ impl DataManager {
///
/// # Arguments
/// * `data` - a mock [`UserFollow`] object to insert
pub async fn create_userfollow(&self, data: UserFollow) -> Result<()> {
/// * `force` - if we should skip the request stage
pub async fn create_userfollow(&self, data: UserFollow, force: bool) -> Result<FollowResult> {
if !force {
let other_user = self.get_user_by_id(data.receiver).await?;
if other_user.settings.private_profile {
// send follow request instead
self.create_request(ActionRequest::with_id(
data.initiator,
data.receiver,
ActionType::Follow,
data.receiver,
))
.await?;
return Ok(FollowResult::Requested);
}
}
// ...
let conn = match self.connect().await {
Ok(c) => c,
Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
@ -248,13 +269,16 @@ impl DataManager {
self.incr_user_follower_count(data.receiver).await.unwrap();
// return
Ok(())
Ok(FollowResult::Followed)
}
pub async fn delete_userfollow(&self, id: usize, user: &User) -> Result<()> {
let follow = self.get_userfollow_by_id(id).await?;
if (user.id != follow.initiator) && (user.id != follow.receiver) && !user.permissions.check(FinePermission::MANAGE_FOLLOWS) {
if (user.id != follow.initiator)
&& (user.id != follow.receiver)
&& !user.permissions.check(FinePermission::MANAGE_FOLLOWS)
{
return Err(Error::NotAllowed);
}

View file

@ -322,6 +322,14 @@ impl UserFollow {
}
}
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum FollowResult {
/// Request sent to follow other user.
Requested,
/// Successfully followed other user.
Followed,
}
#[derive(Serialize, Deserialize)]
pub struct UserBlock {
pub id: usize,

View file

@ -307,6 +307,8 @@ pub struct Question {
pub likes: isize,
#[serde(default)]
pub dislikes: isize,
#[serde(default)]
pub context: QuestionContext,
}
impl Question {
@ -326,6 +328,19 @@ impl Question {
community: 0,
likes: 0,
dislikes: 0,
context: QuestionContext::default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuestionContext {
#[serde(default)]
pub is_nsfw: bool,
}
impl Default for QuestionContext {
fn default() -> Self {
Self { is_nsfw: false }
}
}

View file

@ -11,6 +11,10 @@ pub enum ActionType {
///
/// `questions` table.
Answer,
/// A request follow a private account.
///
/// `users` table.
Follow,
}
#[derive(Serialize, Deserialize)]