mirror of https://github.com/LemmyNet/lemmy.git
reports: update db tables, combine api impl
parent
6d43202efb
commit
d6b1c8df2f
|
@ -1054,7 +1054,6 @@ dependencies = [
|
||||||
"pq-sys",
|
"pq-sys",
|
||||||
"r2d2",
|
"r2d2",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"uuid 0.6.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1834,7 +1833,6 @@ dependencies = [
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"url",
|
"url",
|
||||||
"uuid 0.6.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1900,7 +1898,6 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"serde 1.0.117",
|
"serde 1.0.117",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"uuid 0.6.5",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -267,15 +267,6 @@ pub async fn match_websocket_operation(
|
||||||
do_websocket_operation::<CreatePostLike>(context, id, op, data).await
|
do_websocket_operation::<CreatePostLike>(context, id, op, data).await
|
||||||
}
|
}
|
||||||
UserOperation::SavePost => do_websocket_operation::<SavePost>(context, id, op, data).await,
|
UserOperation::SavePost => do_websocket_operation::<SavePost>(context, id, op, data).await,
|
||||||
UserOperation::CreatePostReport => {
|
|
||||||
do_websocket_operation::<CreatePostReport>(context, id, op, data).await
|
|
||||||
}
|
|
||||||
UserOperation::ListPostReports => {
|
|
||||||
do_websocket_operation::<ListPostReports>(context, id, op, data).await
|
|
||||||
}
|
|
||||||
UserOperation::ResolvePostReport => {
|
|
||||||
do_websocket_operation::<ResolvePostReport>(context, id, op, data).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// Comment ops
|
// Comment ops
|
||||||
UserOperation::CreateComment => {
|
UserOperation::CreateComment => {
|
||||||
|
@ -302,14 +293,16 @@ pub async fn match_websocket_operation(
|
||||||
UserOperation::CreateCommentLike => {
|
UserOperation::CreateCommentLike => {
|
||||||
do_websocket_operation::<CreateCommentLike>(context, id, op, data).await
|
do_websocket_operation::<CreateCommentLike>(context, id, op, data).await
|
||||||
}
|
}
|
||||||
UserOperation::CreateCommentReport => {
|
|
||||||
do_websocket_operation::<CreateCommentReport>(context, id, op, data).await
|
// report ops
|
||||||
},
|
UserOperation::CreateReport => {
|
||||||
UserOperation::ListCommentReports => {
|
do_websocket_operation::<CreateReport>(context, id, op, data).await
|
||||||
do_websocket_operation::<ListCommentReports>(context, id, op, data).await
|
}
|
||||||
},
|
UserOperation::ListReports => {
|
||||||
UserOperation::ResolveCommentReport => {
|
do_websocket_operation::<ListReports>(context, id, op, data).await
|
||||||
do_websocket_operation::<ResolveCommentReport>(context, id, op, data).await
|
}
|
||||||
|
UserOperation::ResolveReport => {
|
||||||
|
do_websocket_operation::<ResolveReport>(context, id, op, data).await
|
||||||
}
|
}
|
||||||
UserOperation::GetReportCount => {
|
UserOperation::GetReportCount => {
|
||||||
do_websocket_operation::<GetReportCount>(context, id, op, data).await
|
do_websocket_operation::<GetReportCount>(context, id, op, data).await
|
||||||
|
|
|
@ -1,119 +1,55 @@
|
||||||
use actix_web::web::Data;
|
use actix_web::web::Data;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use lemmy_db::{
|
use lemmy_db::{comment_report::*, comment_view::*, post_report::*, post_view::*, Reportable, ReportType,};
|
||||||
comment_report::*,
|
|
||||||
comment_view::*,
|
|
||||||
community_view::*,
|
|
||||||
post_report::*,
|
|
||||||
post_view::*,
|
|
||||||
Reportable,
|
|
||||||
user_view::UserView,
|
|
||||||
};
|
|
||||||
use lemmy_structs::{blocking, report::*};
|
use lemmy_structs::{blocking, report::*};
|
||||||
use lemmy_utils::{APIError, ConnectionId, LemmyError};
|
use lemmy_utils::{APIError, ConnectionId, LemmyError};
|
||||||
use lemmy_websocket::LemmyContext;
|
use lemmy_websocket::{LemmyContext, UserOperation, messages::SendUserRoomMessage};
|
||||||
|
|
||||||
use crate::{check_community_ban, get_user_from_jwt, Perform};
|
use crate::{check_community_ban, get_user_from_jwt, is_mod_or_admin, Perform};
|
||||||
|
|
||||||
const MAX_REPORT_LEN: usize = 1000;
|
const MAX_REPORT_LEN: usize = 1000;
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl Perform for CreateCommentReport {
|
impl Perform for CreateReport {
|
||||||
type Response = CommentReportResponse;
|
type Response = CreateReportResponse;
|
||||||
|
|
||||||
async fn perform(
|
async fn perform(
|
||||||
&self,
|
&self,
|
||||||
context: &Data<LemmyContext>,
|
context: &Data<LemmyContext>,
|
||||||
_websocket_id: Option<ConnectionId>,
|
websocket_id: Option<ConnectionId>,
|
||||||
) -> Result<CommentReportResponse, LemmyError> {
|
) -> Result<CreateReportResponse, LemmyError> {
|
||||||
let data: &CreateCommentReport = &self;
|
let data: &CreateReport = &self;
|
||||||
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
||||||
|
|
||||||
// Check size of report and check for whitespace
|
// check size of report and check for whitespace
|
||||||
let reason: Option<String> = match data.reason.clone() {
|
let reason = data.reason.clone();
|
||||||
Some(s) if s.trim().is_empty() => None,
|
if reason.trim().is_empty() {
|
||||||
Some(s) if s.len() > MAX_REPORT_LEN => {
|
return Err(APIError::err("report_reason_required").into());
|
||||||
return Err(APIError::err("report_too_long").into());
|
}
|
||||||
}
|
if reason.len() > MAX_REPORT_LEN {
|
||||||
Some(s) => Some(s),
|
return Err(APIError::err("report_too_long").into());
|
||||||
None => None,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch comment information
|
let report_type = ReportType::from_str(&data.report_type)?;
|
||||||
let comment_id = data.comment;
|
let user_id = user.id;
|
||||||
let comment = blocking(context.pool(), move |conn| CommentView::read(&conn, comment_id, None)).await??;
|
match report_type {
|
||||||
|
ReportType::Comment => { create_comment_report(context, data, user_id).await?; }
|
||||||
|
ReportType::Post => { create_post_report(context, data, user_id).await?; }
|
||||||
|
}
|
||||||
|
|
||||||
// Check for community ban
|
// to build on this, the user should get a success response, however
|
||||||
check_community_ban(user.id, comment.community_id, context.pool()).await?;
|
// mods should get a different response with more details
|
||||||
|
let res = CreateReportResponse { success: true };
|
||||||
|
|
||||||
// Insert the report
|
context.chat_server().do_send(SendUserRoomMessage {
|
||||||
let comment_time = match comment.updated {
|
op: UserOperation::CreateReport,
|
||||||
Some(s) => s,
|
response: res.clone(),
|
||||||
None => comment.published,
|
recipient_id: user.id,
|
||||||
};
|
websocket_id,
|
||||||
let report_form = CommentReportForm {
|
});
|
||||||
time: None, // column defaults to now() in table
|
|
||||||
reason,
|
|
||||||
resolved: None, // column defaults to false
|
|
||||||
user_id: user.id,
|
|
||||||
comment_id,
|
|
||||||
comment_text: comment.content,
|
|
||||||
comment_time,
|
|
||||||
};
|
|
||||||
blocking(context.pool(), move |conn| CommentReport::report(conn, &report_form)).await??;
|
|
||||||
|
|
||||||
Ok(CommentReportResponse { success: true })
|
Ok(res)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl Perform for CreatePostReport {
|
|
||||||
type Response = PostReportResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
context: &Data<LemmyContext>,
|
|
||||||
_websocket_id: Option<ConnectionId>,
|
|
||||||
) -> Result<PostReportResponse, LemmyError> {
|
|
||||||
let data: &CreatePostReport = &self;
|
|
||||||
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
|
||||||
|
|
||||||
// Check size of report and check for whitespace
|
|
||||||
let reason: Option<String> = match data.reason.clone() {
|
|
||||||
Some(s) if s.trim().is_empty() => None,
|
|
||||||
Some(s) if s.len() > MAX_REPORT_LEN => {
|
|
||||||
return Err(APIError::err("report_too_long").into());
|
|
||||||
}
|
|
||||||
Some(s) => Some(s),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Fetch post information from the database
|
|
||||||
let post_id = data.post;
|
|
||||||
let post = blocking(context.pool(), move |conn| PostView::read(&conn, post_id, None)).await??;
|
|
||||||
|
|
||||||
// Check for community ban
|
|
||||||
check_community_ban(user.id, post.community_id, context.pool()).await?;
|
|
||||||
|
|
||||||
// Insert the report
|
|
||||||
let post_time = match post.updated {
|
|
||||||
Some(s) => s,
|
|
||||||
None => post.published,
|
|
||||||
};
|
|
||||||
let report_form = PostReportForm {
|
|
||||||
time: None, // column defaults to now() in table
|
|
||||||
reason,
|
|
||||||
resolved: None, // column defaults to false
|
|
||||||
user_id: user.id,
|
|
||||||
post_id,
|
|
||||||
post_name: post.name,
|
|
||||||
post_url: post.url,
|
|
||||||
post_body: post.body,
|
|
||||||
post_time,
|
|
||||||
};
|
|
||||||
blocking(context.pool(), move |conn| PostReport::report(conn, &report_form)).await??;
|
|
||||||
|
|
||||||
Ok(PostReportResponse { success: true })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -124,39 +60,14 @@ impl Perform for GetReportCount {
|
||||||
async fn perform(
|
async fn perform(
|
||||||
&self,
|
&self,
|
||||||
context: &Data<LemmyContext>,
|
context: &Data<LemmyContext>,
|
||||||
_websocket_id: Option<ConnectionId>,
|
websocket_id: Option<ConnectionId>,
|
||||||
) -> Result<GetReportCountResponse, LemmyError> {
|
) -> Result<GetReportCountResponse, LemmyError> {
|
||||||
let data: &GetReportCount = &self;
|
let data: &GetReportCount = &self;
|
||||||
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
||||||
|
|
||||||
let community_id = data.community;
|
let community_id = data.community;
|
||||||
//Check community exists.
|
|
||||||
let community_id = blocking(context.pool(), move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, None)
|
|
||||||
})
|
|
||||||
.await??
|
|
||||||
.id;
|
|
||||||
|
|
||||||
// Check community ban
|
// Check for mod/admin privileges
|
||||||
check_community_ban(user.id, data.community, context.pool()).await?;
|
is_mod_or_admin(context.pool(), user.id, community_id).await?;
|
||||||
|
|
||||||
let mut mod_ids: Vec<i32> = Vec::new();
|
|
||||||
mod_ids.append(
|
|
||||||
&mut blocking(context.pool(), move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
mod_ids.append(
|
|
||||||
&mut blocking(context.pool(), move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
if !mod_ids.contains(&user.id) {
|
|
||||||
return Err(APIError::err("report_view_not_allowed").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let comment_reports = blocking(context.pool(), move |conn| {
|
let comment_reports = blocking(context.pool(), move |conn| {
|
||||||
CommentReportQueryBuilder::create(conn)
|
CommentReportQueryBuilder::create(conn)
|
||||||
|
@ -173,59 +84,42 @@ impl Perform for GetReportCount {
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
let response = GetReportCountResponse {
|
let res = GetReportCountResponse {
|
||||||
community: community_id,
|
community: community_id,
|
||||||
comment_reports,
|
comment_reports,
|
||||||
post_reports,
|
post_reports,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(response)
|
context.chat_server().do_send(SendUserRoomMessage {
|
||||||
|
op: UserOperation::ListReports,
|
||||||
|
response: res.clone(),
|
||||||
|
recipient_id: user.id,
|
||||||
|
websocket_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl Perform for ListCommentReports {
|
impl Perform for ListReports {
|
||||||
type Response = ListCommentReportResponse;
|
type Response = ListReportsResponse;
|
||||||
|
|
||||||
async fn perform(
|
async fn perform(
|
||||||
&self,
|
&self,
|
||||||
context: &Data<LemmyContext>,
|
context: &Data<LemmyContext>,
|
||||||
_websocket_id: Option<ConnectionId>,
|
websocket_id: Option<ConnectionId>,
|
||||||
) -> Result<ListCommentReportResponse, LemmyError> {
|
) -> Result<ListReportsResponse, LemmyError> {
|
||||||
let data: &ListCommentReports = &self;
|
let data: &ListReports = &self;
|
||||||
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
||||||
|
|
||||||
let community_id = data.community;
|
let community_id = data.community;
|
||||||
//Check community exists.
|
|
||||||
let community_id = blocking(context.pool(), move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, None)
|
|
||||||
})
|
|
||||||
.await??
|
|
||||||
.id;
|
|
||||||
|
|
||||||
check_community_ban(user.id, data.community, context.pool()).await?;
|
// Check for mod/admin privileges
|
||||||
|
is_mod_or_admin(context.pool(), user.id, community_id).await?;
|
||||||
let mut mod_ids: Vec<i32> = Vec::new();
|
|
||||||
mod_ids.append(
|
|
||||||
&mut blocking(context.pool(), move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
mod_ids.append(
|
|
||||||
&mut blocking(context.pool(), move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
if !mod_ids.contains(&user.id) {
|
|
||||||
return Err(APIError::err("report_view_not_allowed").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let page = data.page;
|
let page = data.page;
|
||||||
let limit = data.limit;
|
let limit = data.limit;
|
||||||
let reports = blocking(context.pool(), move |conn| {
|
let comments = blocking(context.pool(), move |conn| {
|
||||||
CommentReportQueryBuilder::create(conn)
|
CommentReportQueryBuilder::create(conn)
|
||||||
.community_id(community_id)
|
.community_id(community_id)
|
||||||
.page(page)
|
.page(page)
|
||||||
|
@ -234,161 +128,173 @@ impl Perform for ListCommentReports {
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
Ok(ListCommentReportResponse { reports })
|
let posts = blocking(context.pool(), move |conn| {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
|
||||||
impl Perform for ListPostReports {
|
|
||||||
type Response = ListPostReportResponse;
|
|
||||||
|
|
||||||
async fn perform(
|
|
||||||
&self,
|
|
||||||
context: &Data<LemmyContext>,
|
|
||||||
_websocket_id: Option<ConnectionId>,
|
|
||||||
) -> Result<ListPostReportResponse, LemmyError> {
|
|
||||||
let data: &ListPostReports = &self;
|
|
||||||
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
|
||||||
|
|
||||||
let community_id = data.community;
|
|
||||||
//Check community exists.
|
|
||||||
let community_id = blocking(context.pool(), move |conn| {
|
|
||||||
CommunityView::read(conn, community_id, None)
|
|
||||||
})
|
|
||||||
.await??
|
|
||||||
.id;
|
|
||||||
// Check for community ban
|
|
||||||
check_community_ban(user.id, data.community, context.pool()).await?;
|
|
||||||
|
|
||||||
let mut mod_ids: Vec<i32> = Vec::new();
|
|
||||||
mod_ids.append(
|
|
||||||
&mut blocking(context.pool(), move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, community_id)
|
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
mod_ids.append(
|
|
||||||
&mut blocking(context.pool(), move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
if !mod_ids.contains(&user.id) {
|
|
||||||
return Err(APIError::err("report_view_not_allowed").into());
|
|
||||||
}
|
|
||||||
|
|
||||||
let page = data.page;
|
|
||||||
let limit = data.limit;
|
|
||||||
let reports = blocking(context.pool(), move |conn| {
|
|
||||||
PostReportQueryBuilder::create(conn)
|
PostReportQueryBuilder::create(conn)
|
||||||
.community_id(community_id)
|
.community_id(community_id)
|
||||||
.page(page)
|
.page(page)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.list()
|
.list()
|
||||||
})
|
})
|
||||||
.await??;
|
.await??;
|
||||||
|
|
||||||
Ok(ListPostReportResponse { reports })
|
let res = ListReportsResponse { comments, posts };
|
||||||
|
|
||||||
|
context.chat_server().do_send(SendUserRoomMessage {
|
||||||
|
op: UserOperation::ListReports,
|
||||||
|
response: res.clone(),
|
||||||
|
recipient_id: user.id,
|
||||||
|
websocket_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
#[async_trait::async_trait(?Send)]
|
||||||
impl Perform for ResolveCommentReport {
|
impl Perform for ResolveReport {
|
||||||
type Response = ResolveCommentReportResponse;
|
type Response = ResolveReportResponse;
|
||||||
|
|
||||||
async fn perform(
|
async fn perform(
|
||||||
&self,
|
&self,
|
||||||
context: &Data<LemmyContext>,
|
context: &Data<LemmyContext>,
|
||||||
_websocket_id: Option<ConnectionId>,
|
websocket_id: Option<ConnectionId>,
|
||||||
) -> Result<ResolveCommentReportResponse, LemmyError> {
|
) -> Result<ResolveReportResponse, LemmyError> {
|
||||||
let data: &ResolveCommentReport = &self;
|
let data: &ResolveReport = &self;
|
||||||
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
||||||
|
|
||||||
// Fetch the report view
|
let report_type = ReportType::from_str(&data.report_type)?;
|
||||||
let report_id = data.report;
|
let user_id = user.id;
|
||||||
let report = blocking(context.pool(), move |conn| CommentReportView::read(&conn, &report_id)).await??;
|
match report_type {
|
||||||
|
ReportType::Comment => { resolve_comment_report(context, data, user_id).await?; }
|
||||||
// Check for community ban
|
ReportType::Post => { resolve_post_report(context, data, user_id).await?; }
|
||||||
check_community_ban(user.id, report.community_id, context.pool()).await?;
|
|
||||||
|
|
||||||
// Check for mod/admin privileges
|
|
||||||
let mut mod_ids: Vec<i32> = Vec::new();
|
|
||||||
mod_ids.append(
|
|
||||||
&mut blocking(context.pool(), move |conn| {
|
|
||||||
CommunityModeratorView::for_community(conn, report.community_id)
|
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
mod_ids.append(
|
|
||||||
&mut blocking(context.pool(), move |conn| {
|
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
|
||||||
})
|
|
||||||
.await??,
|
|
||||||
);
|
|
||||||
if !mod_ids.contains(&user.id) {
|
|
||||||
return Err(APIError::err("resolve_report_not_allowed").into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blocking(context.pool(), move |conn| {
|
let report_id = data.report_id;
|
||||||
CommentReport::resolve(conn, &report_id.clone())
|
let res = ResolveReportResponse {
|
||||||
})
|
report_type: data.report_type.to_owned(),
|
||||||
.await??;
|
report_id,
|
||||||
|
|
||||||
Ok(ResolveCommentReportResponse {
|
|
||||||
report: report_id,
|
|
||||||
resolved: true,
|
resolved: true,
|
||||||
})
|
};
|
||||||
|
|
||||||
|
context.chat_server().do_send(SendUserRoomMessage {
|
||||||
|
op: UserOperation::ResolveReport,
|
||||||
|
response: res.clone(),
|
||||||
|
recipient_id: user.id,
|
||||||
|
websocket_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait(?Send)]
|
async fn create_comment_report(
|
||||||
impl Perform for ResolvePostReport {
|
context: &Data<LemmyContext>,
|
||||||
type Response = ResolvePostReportResponse;
|
data: &CreateReport,
|
||||||
|
user_id: i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let comment_id = data.entity_id;
|
||||||
|
let comment = blocking(context.pool(), move |conn| {
|
||||||
|
CommentView::read(&conn, comment_id, None)
|
||||||
|
}).await??;
|
||||||
|
|
||||||
async fn perform(
|
check_community_ban(user_id, comment.community_id, context.pool()).await?;
|
||||||
&self,
|
|
||||||
context: &Data<LemmyContext>,
|
|
||||||
_websocket_id: Option<ConnectionId>,
|
|
||||||
) -> Result<ResolvePostReportResponse, LemmyError> {
|
|
||||||
let data: &ResolvePostReport = &self;
|
|
||||||
let user = get_user_from_jwt(&data.auth, context.pool()).await?;
|
|
||||||
|
|
||||||
// Fetch the report view
|
let report_form = CommentReportForm {
|
||||||
let report_id = data.report;
|
creator_id: user_id,
|
||||||
let report = blocking(context.pool(), move |conn| PostReportView::read(&conn, &report_id)).await??;
|
comment_id,
|
||||||
|
comment_text: comment.content,
|
||||||
|
reason: data.reason.to_owned(),
|
||||||
|
};
|
||||||
|
|
||||||
// Check for community ban
|
return match blocking(context.pool(), move |conn| {
|
||||||
check_community_ban(user.id, report.community_id, context.pool()).await?;
|
CommentReport::report(conn, &report_form)
|
||||||
|
}).await? {
|
||||||
// Check for mod/admin privileges
|
Ok(_) => Ok(()),
|
||||||
let mut mod_ids: Vec<i32> = Vec::new();
|
Err(_e) => Err(APIError::err("couldnt_create_report").into())
|
||||||
mod_ids.append(
|
};
|
||||||
&mut blocking(context.pool(), move |conn| {
|
}
|
||||||
CommunityModeratorView::for_community(conn, report.community_id)
|
|
||||||
.map(|v| v.into_iter().map(|m| m.user_id).collect())
|
async fn create_post_report(
|
||||||
})
|
context: &Data<LemmyContext>,
|
||||||
.await??,
|
data: &CreateReport,
|
||||||
);
|
user_id: i32,
|
||||||
mod_ids.append(
|
) -> Result<(), LemmyError> {
|
||||||
&mut blocking(context.pool(), move |conn| {
|
let post_id = data.entity_id;
|
||||||
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
|
let post = blocking(context.pool(), move |conn| {
|
||||||
})
|
PostView::read(&conn, post_id, None)
|
||||||
.await??,
|
}).await??;
|
||||||
);
|
|
||||||
if !mod_ids.contains(&user.id) {
|
check_community_ban(user_id, post.community_id, context.pool()).await?;
|
||||||
return Err(APIError::err("resolve_report_not_allowed").into());
|
|
||||||
}
|
let report_form = PostReportForm {
|
||||||
|
creator_id: user_id,
|
||||||
blocking(context.pool(), move |conn| {
|
post_id,
|
||||||
PostReport::resolve(conn, &report_id.clone())
|
post_name: post.name,
|
||||||
})
|
post_url: post.url,
|
||||||
.await??;
|
post_body: post.body,
|
||||||
|
reason: data.reason.to_owned(),
|
||||||
Ok(ResolvePostReportResponse {
|
};
|
||||||
report: report_id,
|
|
||||||
resolved: true,
|
return match blocking(context.pool(), move |conn| {
|
||||||
})
|
PostReport::report(conn, &report_form)
|
||||||
}
|
}).await? {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(_e) => Err(APIError::err("couldnt_create_report").into())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_comment_report(
|
||||||
|
context: &Data<LemmyContext>,
|
||||||
|
data: &ResolveReport,
|
||||||
|
user_id: i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let report_id = data.report_id;
|
||||||
|
let report = blocking(context.pool(), move |conn| {
|
||||||
|
CommentReportView::read(&conn, report_id)
|
||||||
|
}).await??;
|
||||||
|
|
||||||
|
is_mod_or_admin(context.pool(), user_id, report.community_id).await?;
|
||||||
|
|
||||||
|
let resolved = data.resolved;
|
||||||
|
let resolve_fun = move |conn: &'_ _| {
|
||||||
|
if resolved {
|
||||||
|
CommentReport::resolve(conn, report_id.clone(), user_id)
|
||||||
|
} else {
|
||||||
|
CommentReport::unresolve(conn, report_id.clone())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if blocking(context.pool(),resolve_fun).await?.is_err() {
|
||||||
|
return Err(APIError::err("couldnt_resolve_report").into())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn resolve_post_report(
|
||||||
|
context: &Data<LemmyContext>,
|
||||||
|
data: &ResolveReport,
|
||||||
|
user_id: i32,
|
||||||
|
) -> Result<(), LemmyError> {
|
||||||
|
let report_id = data.report_id;
|
||||||
|
let report = blocking(context.pool(), move |conn| {
|
||||||
|
PostReportView::read(&conn, report_id)
|
||||||
|
}).await??;
|
||||||
|
|
||||||
|
is_mod_or_admin(context.pool(), user_id, report.community_id).await?;
|
||||||
|
|
||||||
|
let resolved = data.resolved;
|
||||||
|
let resolve_fun = move |conn: &'_ _| {
|
||||||
|
if resolved {
|
||||||
|
PostReport::resolve(conn, report_id.clone(), user_id)
|
||||||
|
} else {
|
||||||
|
PostReport::unresolve(conn, report_id.clone())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if blocking(context.pool(),resolve_fun).await?.is_err() {
|
||||||
|
return Err(APIError::err("couldnt_resolve_report").into())
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lemmy_utils = { path = "../lemmy_utils" }
|
lemmy_utils = { path = "../lemmy_utils" }
|
||||||
diesel = { version = "1.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json", "uuid"] }
|
diesel = { version = "1.4", features = ["postgres","chrono","r2d2","64-column-tables","serde_json"] }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"]}
|
serde_json = { version = "1.0", features = ["preserve_order"]}
|
||||||
|
@ -21,4 +21,3 @@ bcrypt = "0.8"
|
||||||
url = { version = "2.1", features = ["serde"] }
|
url = { version = "2.1", features = ["serde"] }
|
||||||
lazy_static = "1.3"
|
lazy_static = "1.3"
|
||||||
regex = "1.3"
|
regex = "1.3"
|
||||||
uuid = { version = "0.6.5", features = ["serde", "v4"] }
|
|
||||||
|
|
|
@ -1,31 +1,24 @@
|
||||||
use diesel::{PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods, insert_into, update};
|
use diesel::{dsl::*, pg::Pg, result::Error, *};
|
||||||
use diesel::pg::Pg;
|
|
||||||
use diesel::result::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{limit_and_offset, MaybeOptional, schema::comment_report, comment::Comment, Reportable, naive_now};
|
||||||
limit_and_offset,
|
|
||||||
MaybeOptional,
|
|
||||||
schema::comment_report,
|
|
||||||
comment::Comment,
|
|
||||||
Reportable,
|
|
||||||
};
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
comment_report_view (id) {
|
comment_report_view (id) {
|
||||||
id -> Uuid,
|
id -> Int4,
|
||||||
time -> Timestamp,
|
creator_id -> Int4,
|
||||||
reason -> Nullable<Text>,
|
|
||||||
resolved -> Bool,
|
|
||||||
user_id -> Int4,
|
|
||||||
comment_id -> Int4,
|
comment_id -> Int4,
|
||||||
comment_text -> Text,
|
comment_text -> Text,
|
||||||
comment_time -> Timestamp,
|
reason -> Text,
|
||||||
|
resolved -> Bool,
|
||||||
|
resolver_id -> Nullable<Int4>,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
post_id -> Int4,
|
post_id -> Int4,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
user_name -> Varchar,
|
|
||||||
creator_id -> Int4,
|
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
comment_creator_id -> Int4,
|
||||||
|
comment_creator_name -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,26 +26,24 @@ table! {
|
||||||
#[belongs_to(Comment)]
|
#[belongs_to(Comment)]
|
||||||
#[table_name = "comment_report"]
|
#[table_name = "comment_report"]
|
||||||
pub struct CommentReport {
|
pub struct CommentReport {
|
||||||
pub id: uuid::Uuid,
|
pub id: i32,
|
||||||
pub time: chrono::NaiveDateTime,
|
pub creator_id: i32,
|
||||||
pub reason: Option<String>,
|
|
||||||
pub resolved: bool,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub comment_id: i32,
|
pub comment_id: i32,
|
||||||
pub comment_text: String,
|
pub comment_text: String,
|
||||||
pub comment_time: chrono::NaiveDateTime,
|
pub reason: String,
|
||||||
|
pub resolved: bool,
|
||||||
|
pub resolver_id: Option<i32>,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone)]
|
#[derive(Insertable, AsChangeset, Clone)]
|
||||||
#[table_name = "comment_report"]
|
#[table_name = "comment_report"]
|
||||||
pub struct CommentReportForm {
|
pub struct CommentReportForm {
|
||||||
pub time: Option<chrono::NaiveDateTime>,
|
pub creator_id: i32,
|
||||||
pub reason: Option<String>,
|
|
||||||
pub resolved: Option<bool>,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub comment_id: i32,
|
pub comment_id: i32,
|
||||||
pub comment_text: String,
|
pub comment_text: String,
|
||||||
pub comment_time: chrono::NaiveDateTime,
|
pub reason: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reportable<CommentReportForm> for CommentReport {
|
impl Reportable<CommentReportForm> for CommentReport {
|
||||||
|
@ -63,32 +54,47 @@ impl Reportable<CommentReportForm> for CommentReport {
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve(conn: &PgConnection, report_id: &uuid::Uuid) -> Result<usize, Error> {
|
fn resolve(conn: &PgConnection, report_id: i32, by_user_id: i32) -> Result<usize, Error> {
|
||||||
use crate::schema::comment_report::dsl::*;
|
use crate::schema::comment_report::dsl::*;
|
||||||
update(comment_report.find(report_id))
|
update(comment_report.find(report_id))
|
||||||
.set(resolved.eq(true))
|
.set((
|
||||||
|
resolved.eq(true),
|
||||||
|
resolver_id.eq(by_user_id),
|
||||||
|
updated.eq(naive_now()),
|
||||||
|
))
|
||||||
|
.execute(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unresolve(conn: &PgConnection, report_id: i32) -> Result<usize, Error> {
|
||||||
|
use crate::schema::comment_report::dsl::*;
|
||||||
|
update(comment_report.find(report_id))
|
||||||
|
.set((
|
||||||
|
resolved.eq(false),
|
||||||
|
updated.eq(naive_now()),
|
||||||
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
|
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, Clone,
|
||||||
)]
|
)]
|
||||||
#[table_name = "comment_report_view"]
|
#[table_name = "comment_report_view"]
|
||||||
pub struct CommentReportView {
|
pub struct CommentReportView {
|
||||||
pub id: uuid::Uuid,
|
pub id: i32,
|
||||||
pub time: chrono::NaiveDateTime,
|
pub creator_id: i32,
|
||||||
pub reason: Option<String>,
|
|
||||||
pub resolved: bool,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub comment_id: i32,
|
pub comment_id: i32,
|
||||||
pub comment_text: String,
|
pub comment_text: String,
|
||||||
pub comment_time: chrono::NaiveDateTime,
|
pub reason: String,
|
||||||
|
pub resolved: bool,
|
||||||
|
pub resolver_id: Option<i32>,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
pub user_name: String,
|
|
||||||
pub creator_id: i32,
|
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub comment_creator_id: i32,
|
||||||
|
pub comment_creator_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CommentReportQueryBuilder<'a> {
|
pub struct CommentReportQueryBuilder<'a> {
|
||||||
|
@ -101,7 +107,7 @@ pub struct CommentReportQueryBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommentReportView {
|
impl CommentReportView {
|
||||||
pub fn read(conn: &PgConnection, report_id: &uuid::Uuid) -> Result<Self, Error> {
|
pub fn read(conn: &PgConnection, report_id: i32) -> Result<Self, Error> {
|
||||||
use super::comment_report::comment_report_view::dsl::*;
|
use super::comment_report::comment_report_view::dsl::*;
|
||||||
comment_report_view
|
comment_report_view
|
||||||
.filter(id.eq(report_id))
|
.filter(id.eq(report_id))
|
||||||
|
@ -161,7 +167,7 @@ impl<'a> CommentReportQueryBuilder<'a> {
|
||||||
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
||||||
|
|
||||||
query
|
query
|
||||||
.order_by(time.desc())
|
.order_by(published.desc())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.load::<CommentReportView>(self.conn)
|
.load::<CommentReportView>(self.conn)
|
||||||
|
|
|
@ -115,7 +115,10 @@ pub trait Reportable<T> {
|
||||||
fn report(conn: &PgConnection, form: &T) -> Result<Self, Error>
|
fn report(conn: &PgConnection, form: &T) -> Result<Self, Error>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
fn resolve(conn: &PgConnection, report_id: &uuid::Uuid) -> Result<usize, Error>
|
fn resolve(conn: &PgConnection, report_id: i32, user_id: i32) -> Result<usize, Error>
|
||||||
|
where
|
||||||
|
Self: Sized;
|
||||||
|
fn unresolve(conn: &PgConnection, report_id: i32) -> Result<usize, Error>
|
||||||
where
|
where
|
||||||
Self: Sized;
|
Self: Sized;
|
||||||
}
|
}
|
||||||
|
@ -170,6 +173,12 @@ pub enum SearchType {
|
||||||
Url,
|
Url,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum ReportType {
|
||||||
|
Comment,
|
||||||
|
Post,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn fuzzy_search(q: &str) -> String {
|
pub fn fuzzy_search(q: &str) -> String {
|
||||||
let replaced = q.replace(" ", "%");
|
let replaced = q.replace(" ", "%");
|
||||||
format!("%{}%", replaced)
|
format!("%{}%", replaced)
|
||||||
|
|
|
@ -1,32 +1,25 @@
|
||||||
use diesel::{PgConnection, QueryDsl, RunQueryDsl, ExpressionMethods, insert_into, update};
|
use diesel::{dsl::*, pg::Pg, result::Error, *};
|
||||||
use diesel::pg::Pg;
|
|
||||||
use diesel::result::*;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{limit_and_offset, MaybeOptional, schema::post_report, post::Post, Reportable, naive_now};
|
||||||
limit_and_offset,
|
|
||||||
MaybeOptional,
|
|
||||||
schema::post_report,
|
|
||||||
post::Post,
|
|
||||||
Reportable,
|
|
||||||
};
|
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
post_report_view (id) {
|
post_report_view (id) {
|
||||||
id -> Uuid,
|
id -> Int4,
|
||||||
time -> Timestamp,
|
creator_id -> Int4,
|
||||||
reason -> Nullable<Text>,
|
post_id -> Int4,
|
||||||
resolved -> Bool,
|
post_name -> Varchar,
|
||||||
user_id -> Int4,
|
post_url -> Nullable<Text>,
|
||||||
post_id -> Int4,
|
post_body -> Nullable<Text>,
|
||||||
post_name -> Varchar,
|
reason -> Text,
|
||||||
post_url -> Nullable<Text>,
|
resolved -> Bool,
|
||||||
post_body -> Nullable<Text>,
|
resolver_id -> Nullable<Int4>,
|
||||||
post_time -> Timestamp,
|
published -> Timestamp,
|
||||||
community_id -> Int4,
|
updated -> Nullable<Timestamp>,
|
||||||
user_name -> Varchar,
|
community_id -> Int4,
|
||||||
creator_id -> Int4,
|
creator_name -> Varchar,
|
||||||
creator_name -> Varchar,
|
post_creator_id -> Int4,
|
||||||
|
post_creator_name -> Varchar,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,30 +27,28 @@ table! {
|
||||||
#[belongs_to(Post)]
|
#[belongs_to(Post)]
|
||||||
#[table_name = "post_report"]
|
#[table_name = "post_report"]
|
||||||
pub struct PostReport {
|
pub struct PostReport {
|
||||||
pub id: uuid::Uuid,
|
pub id: i32,
|
||||||
pub time: chrono::NaiveDateTime,
|
pub creator_id: i32,
|
||||||
pub reason: Option<String>,
|
|
||||||
pub resolved: bool,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub post_name: String,
|
pub post_name: String,
|
||||||
pub post_url: Option<String>,
|
pub post_url: Option<String>,
|
||||||
pub post_body: Option<String>,
|
pub post_body: Option<String>,
|
||||||
pub post_time: chrono::NaiveDateTime,
|
pub reason: String,
|
||||||
|
pub resolved: bool,
|
||||||
|
pub resolver_id: Option<i32>,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone)]
|
#[derive(Insertable, AsChangeset, Clone)]
|
||||||
#[table_name = "post_report"]
|
#[table_name = "post_report"]
|
||||||
pub struct PostReportForm {
|
pub struct PostReportForm {
|
||||||
pub time: Option<chrono::NaiveDateTime>,
|
pub creator_id: i32,
|
||||||
pub reason: Option<String>,
|
|
||||||
pub resolved: Option<bool>,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub post_name: String,
|
pub post_name: String,
|
||||||
pub post_url: Option<String>,
|
pub post_url: Option<String>,
|
||||||
pub post_body: Option<String>,
|
pub post_body: Option<String>,
|
||||||
pub post_time: chrono::NaiveDateTime,
|
pub reason: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Reportable<PostReportForm> for PostReport {
|
impl Reportable<PostReportForm> for PostReport {
|
||||||
|
@ -68,37 +59,52 @@ impl Reportable<PostReportForm> for PostReport {
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve(conn: &PgConnection, report_id: &uuid::Uuid) -> Result<usize, Error> {
|
fn resolve(conn: &PgConnection, report_id: i32, by_user_id: i32) -> Result<usize, Error> {
|
||||||
use crate::schema::post_report::dsl::*;
|
use crate::schema::post_report::dsl::*;
|
||||||
update(post_report.find(report_id))
|
update(post_report.find(report_id))
|
||||||
.set(resolved.eq(true))
|
.set((
|
||||||
|
resolved.eq(true),
|
||||||
|
resolver_id.eq(by_user_id),
|
||||||
|
updated.eq(naive_now()),
|
||||||
|
))
|
||||||
|
.execute(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unresolve(conn: &PgConnection, report_id: i32) -> Result<usize, Error> {
|
||||||
|
use crate::schema::post_report::dsl::*;
|
||||||
|
update(post_report.find(report_id))
|
||||||
|
.set((
|
||||||
|
resolved.eq(false),
|
||||||
|
updated.eq(naive_now()),
|
||||||
|
))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
|
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, Clone,
|
||||||
)]
|
)]
|
||||||
#[table_name = "post_report_view"]
|
#[table_name = "post_report_view"]
|
||||||
pub struct PostReportView {
|
pub struct PostReportView {
|
||||||
pub id: uuid::Uuid,
|
pub id: i32,
|
||||||
pub time: chrono::NaiveDateTime,
|
pub creator_id: i32,
|
||||||
pub reason: Option<String>,
|
|
||||||
pub resolved: bool,
|
|
||||||
pub user_id: i32,
|
|
||||||
pub post_id: i32,
|
pub post_id: i32,
|
||||||
pub post_name: String,
|
pub post_name: String,
|
||||||
pub post_url: Option<String>,
|
pub post_url: Option<String>,
|
||||||
pub post_body: Option<String>,
|
pub post_body: Option<String>,
|
||||||
pub post_time: chrono::NaiveDateTime,
|
pub reason: String,
|
||||||
|
pub resolved: bool,
|
||||||
|
pub resolver_id: Option<i32>,
|
||||||
|
pub published: chrono::NaiveDateTime,
|
||||||
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
pub user_name: String,
|
|
||||||
pub creator_id: i32,
|
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub post_creator_id: i32,
|
||||||
|
pub post_creator_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostReportView {
|
impl PostReportView {
|
||||||
pub fn read(conn: &PgConnection, report_id: &uuid::Uuid) -> Result<Self, Error> {
|
pub fn read(conn: &PgConnection, report_id: i32) -> Result<Self, Error> {
|
||||||
use super::post_report::post_report_view::dsl::*;
|
use super::post_report::post_report_view::dsl::*;
|
||||||
post_report_view
|
post_report_view
|
||||||
.filter(id.eq(report_id))
|
.filter(id.eq(report_id))
|
||||||
|
@ -167,7 +173,7 @@ impl<'a> PostReportQueryBuilder<'a> {
|
||||||
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
||||||
|
|
||||||
query
|
query
|
||||||
.order_by(time.desc())
|
.order_by(published.desc())
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.offset(offset)
|
.offset(offset)
|
||||||
.load::<PostReportView>(self.conn)
|
.load::<PostReportView>(self.conn)
|
||||||
|
|
|
@ -83,14 +83,15 @@ table! {
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
comment_report (id) {
|
comment_report (id) {
|
||||||
id -> Uuid,
|
id -> Int4,
|
||||||
time -> Timestamp,
|
creator_id -> Int4,
|
||||||
reason -> Nullable<Text>,
|
|
||||||
resolved -> Bool,
|
|
||||||
user_id -> Int4,
|
|
||||||
comment_id -> Int4,
|
comment_id -> Int4,
|
||||||
comment_text -> Text,
|
comment_text -> Text,
|
||||||
comment_time -> Timestamp,
|
reason -> Text,
|
||||||
|
resolved -> Bool,
|
||||||
|
resolver_id -> Nullable<Int4>,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -385,16 +386,17 @@ table! {
|
||||||
|
|
||||||
table! {
|
table! {
|
||||||
post_report (id) {
|
post_report (id) {
|
||||||
id -> Uuid,
|
id -> Int4,
|
||||||
time -> Timestamp,
|
creator_id -> Int4,
|
||||||
reason -> Nullable<Text>,
|
|
||||||
resolved -> Bool,
|
|
||||||
user_id -> Int4,
|
|
||||||
post_id -> Int4,
|
post_id -> Int4,
|
||||||
post_name -> Varchar,
|
post_name -> Varchar,
|
||||||
post_url -> Nullable<Text>,
|
post_url -> Nullable<Text>,
|
||||||
post_body -> Nullable<Text>,
|
post_body -> Nullable<Text>,
|
||||||
post_time -> Timestamp,
|
reason -> Text,
|
||||||
|
resolved -> Bool,
|
||||||
|
resolver_id -> Nullable<Int4>,
|
||||||
|
published -> Timestamp,
|
||||||
|
updated -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -516,7 +518,6 @@ joinable!(comment_like -> comment (comment_id));
|
||||||
joinable!(comment_like -> post (post_id));
|
joinable!(comment_like -> post (post_id));
|
||||||
joinable!(comment_like -> user_ (user_id));
|
joinable!(comment_like -> user_ (user_id));
|
||||||
joinable!(comment_report -> comment (comment_id));
|
joinable!(comment_report -> comment (comment_id));
|
||||||
joinable!(comment_report -> user_ (user_id));
|
|
||||||
joinable!(comment_saved -> comment (comment_id));
|
joinable!(comment_saved -> comment (comment_id));
|
||||||
joinable!(comment_saved -> user_ (user_id));
|
joinable!(comment_saved -> user_ (user_id));
|
||||||
joinable!(community -> category (category_id));
|
joinable!(community -> category (category_id));
|
||||||
|
@ -547,7 +548,6 @@ joinable!(post_like -> user_ (user_id));
|
||||||
joinable!(post_read -> post (post_id));
|
joinable!(post_read -> post (post_id));
|
||||||
joinable!(post_read -> user_ (user_id));
|
joinable!(post_read -> user_ (user_id));
|
||||||
joinable!(post_report -> post (post_id));
|
joinable!(post_report -> post (post_id));
|
||||||
joinable!(post_report -> user_ (user_id));
|
|
||||||
joinable!(post_saved -> post (post_id));
|
joinable!(post_saved -> post (post_id));
|
||||||
joinable!(post_saved -> user_ (user_id));
|
joinable!(post_saved -> user_ (user_id));
|
||||||
joinable!(site -> user_ (creator_id));
|
joinable!(site -> user_ (creator_id));
|
||||||
|
@ -556,38 +556,38 @@ joinable!(user_mention -> comment (comment_id));
|
||||||
joinable!(user_mention -> user_ (recipient_id));
|
joinable!(user_mention -> user_ (recipient_id));
|
||||||
|
|
||||||
allow_tables_to_appear_in_same_query!(
|
allow_tables_to_appear_in_same_query!(
|
||||||
activity,
|
activity,
|
||||||
category,
|
category,
|
||||||
comment,
|
comment,
|
||||||
comment_aggregates_fast,
|
comment_aggregates_fast,
|
||||||
comment_like,
|
comment_like,
|
||||||
comment_report,
|
comment_report,
|
||||||
comment_saved,
|
comment_saved,
|
||||||
community,
|
community,
|
||||||
community_aggregates_fast,
|
community_aggregates_fast,
|
||||||
community_follower,
|
community_follower,
|
||||||
community_moderator,
|
community_moderator,
|
||||||
community_user_ban,
|
community_user_ban,
|
||||||
mod_add,
|
mod_add,
|
||||||
mod_add_community,
|
mod_add_community,
|
||||||
mod_ban,
|
mod_ban,
|
||||||
mod_ban_from_community,
|
mod_ban_from_community,
|
||||||
mod_lock_post,
|
mod_lock_post,
|
||||||
mod_remove_comment,
|
mod_remove_comment,
|
||||||
mod_remove_community,
|
mod_remove_community,
|
||||||
mod_remove_post,
|
mod_remove_post,
|
||||||
mod_sticky_post,
|
mod_sticky_post,
|
||||||
password_reset_request,
|
password_reset_request,
|
||||||
post,
|
post,
|
||||||
post_aggregates_fast,
|
post_aggregates_fast,
|
||||||
post_like,
|
post_like,
|
||||||
post_read,
|
post_read,
|
||||||
post_report,
|
post_report,
|
||||||
post_saved,
|
post_saved,
|
||||||
private_message,
|
private_message,
|
||||||
site,
|
site,
|
||||||
user_,
|
user_,
|
||||||
user_ban,
|
user_ban,
|
||||||
user_fast,
|
user_fast,
|
||||||
user_mention,
|
user_mention,
|
||||||
);
|
);
|
||||||
|
|
|
@ -17,4 +17,3 @@ diesel = "1.4"
|
||||||
actix-web = { version = "3.0" }
|
actix-web = { version = "3.0" }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
serde_json = { version = "1.0", features = ["preserve_order"]}
|
serde_json = { version = "1.0", features = ["preserve_order"]}
|
||||||
uuid = { version = "0.6.5", features = ["serde", "v4"] }
|
|
|
@ -1,57 +1,31 @@
|
||||||
use lemmy_db::{
|
use lemmy_db::{comment_report::CommentReportView, post_report::PostReportView};
|
||||||
comment_report::CommentReportView,
|
|
||||||
post_report::PostReportView,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct CreateCommentReport {
|
pub struct CreateReport {
|
||||||
pub comment: i32,
|
pub report_type: String,
|
||||||
pub reason: Option<String>,
|
pub entity_id: i32,
|
||||||
|
pub reason: String,
|
||||||
pub auth: String,
|
pub auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct CommentReportResponse {
|
pub struct CreateReportResponse {
|
||||||
pub success: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
|
||||||
pub struct CreatePostReport {
|
|
||||||
pub post: i32,
|
|
||||||
pub reason: Option<String>,
|
|
||||||
pub auth: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone)]
|
|
||||||
pub struct PostReportResponse {
|
|
||||||
pub success: bool,
|
pub success: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct ListCommentReports {
|
pub struct ListReports {
|
||||||
pub page: Option<i64>,
|
pub page: Option<i64>,
|
||||||
pub limit: Option<i64>,
|
pub limit: Option<i64>,
|
||||||
pub community: i32,
|
pub community: i32,
|
||||||
pub auth: String,
|
pub auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct ListCommentReportResponse {
|
pub struct ListReportsResponse {
|
||||||
pub reports: Vec<CommentReportView>,
|
pub posts: Vec<PostReportView>,
|
||||||
}
|
pub comments: Vec<CommentReportView>,
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ListPostReports {
|
|
||||||
pub page: Option<i64>,
|
|
||||||
pub limit: Option<i64>,
|
|
||||||
pub community: i32,
|
|
||||||
pub auth: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ListPostReportResponse {
|
|
||||||
pub reports: Vec<PostReportView>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
@ -60,7 +34,7 @@ pub struct GetReportCount {
|
||||||
pub auth: String,
|
pub auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct GetReportCountResponse {
|
pub struct GetReportCountResponse {
|
||||||
pub community: i32,
|
pub community: i32,
|
||||||
pub comment_reports: usize,
|
pub comment_reports: usize,
|
||||||
|
@ -68,25 +42,16 @@ pub struct GetReportCountResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub struct ResolveCommentReport {
|
pub struct ResolveReport {
|
||||||
pub report: uuid::Uuid,
|
pub report_type: String,
|
||||||
|
pub report_id: i32,
|
||||||
|
pub resolved: bool,
|
||||||
pub auth: String,
|
pub auth: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub struct ResolveCommentReportResponse {
|
pub struct ResolveReportResponse {
|
||||||
pub report: uuid::Uuid,
|
pub report_type: String,
|
||||||
pub resolved: bool,
|
pub report_id: i32,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ResolvePostReport {
|
|
||||||
pub report: uuid::Uuid,
|
|
||||||
pub auth: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
|
||||||
pub struct ResolvePostReportResponse {
|
|
||||||
pub report: uuid::Uuid,
|
|
||||||
pub resolved: bool,
|
pub resolved: bool,
|
||||||
}
|
}
|
||||||
|
|
|
@ -97,9 +97,6 @@ pub enum UserOperation {
|
||||||
MarkCommentAsRead,
|
MarkCommentAsRead,
|
||||||
SaveComment,
|
SaveComment,
|
||||||
CreateCommentLike,
|
CreateCommentLike,
|
||||||
CreateCommentReport,
|
|
||||||
ListCommentReports,
|
|
||||||
ResolveCommentReport,
|
|
||||||
GetPosts,
|
GetPosts,
|
||||||
CreatePostLike,
|
CreatePostLike,
|
||||||
EditPost,
|
EditPost,
|
||||||
|
@ -108,9 +105,10 @@ pub enum UserOperation {
|
||||||
LockPost,
|
LockPost,
|
||||||
StickyPost,
|
StickyPost,
|
||||||
SavePost,
|
SavePost,
|
||||||
CreatePostReport,
|
CreateReport,
|
||||||
ListPostReports,
|
ResolveReport,
|
||||||
ResolvePostReport,
|
ListReports,
|
||||||
|
GetReportCount,
|
||||||
EditCommunity,
|
EditCommunity,
|
||||||
DeleteCommunity,
|
DeleteCommunity,
|
||||||
RemoveCommunity,
|
RemoveCommunity,
|
||||||
|
@ -121,7 +119,6 @@ pub enum UserOperation {
|
||||||
GetUserMentions,
|
GetUserMentions,
|
||||||
MarkUserMentionAsRead,
|
MarkUserMentionAsRead,
|
||||||
GetModlog,
|
GetModlog,
|
||||||
GetReportCount,
|
|
||||||
BanFromCommunity,
|
BanFromCommunity,
|
||||||
AddModToCommunity,
|
AddModToCommunity,
|
||||||
CreateSite,
|
CreateSite,
|
||||||
|
|
|
@ -2,4 +2,3 @@ drop view comment_report_view;
|
||||||
drop view post_report_view;
|
drop view post_report_view;
|
||||||
drop table comment_report;
|
drop table comment_report;
|
||||||
drop table post_report;
|
drop table post_report;
|
||||||
drop extension "uuid-ossp";
|
|
||||||
|
|
|
@ -1,51 +1,51 @@
|
||||||
create extension "uuid-ossp";
|
|
||||||
|
|
||||||
create table comment_report (
|
create table comment_report (
|
||||||
id uuid primary key default uuid_generate_v4(),
|
id serial primary key,
|
||||||
time timestamp not null default now(),
|
creator_id int references user_ on update cascade on delete cascade not null, -- user reporting comment
|
||||||
reason text,
|
|
||||||
resolved bool not null default false,
|
|
||||||
user_id int references user_ on update cascade on delete cascade not null, -- user reporting comment
|
|
||||||
comment_id int references comment on update cascade on delete cascade not null, -- comment being reported
|
comment_id int references comment on update cascade on delete cascade not null, -- comment being reported
|
||||||
comment_text text not null,
|
comment_text text not null,
|
||||||
comment_time timestamp not null,
|
reason text not null,
|
||||||
unique(comment_id, user_id) -- users should only be able to report a comment once
|
resolved bool not null default false,
|
||||||
|
resolver_id int references user_ on update cascade on delete cascade not null, -- user resolving report
|
||||||
|
published timestamp not null default now(),
|
||||||
|
updated timestamp null,
|
||||||
|
unique(comment_id, creator_id) -- users should only be able to report a comment once
|
||||||
);
|
);
|
||||||
|
|
||||||
create table post_report (
|
create table post_report (
|
||||||
id uuid primary key default uuid_generate_v4(),
|
id serial primary key,
|
||||||
time timestamp not null default now(),
|
creator_id int references user_ on update cascade on delete cascade not null, -- user reporting post
|
||||||
reason text,
|
|
||||||
resolved bool not null default false,
|
|
||||||
user_id int references user_ on update cascade on delete cascade not null, -- user reporting post
|
|
||||||
post_id int references post on update cascade on delete cascade not null, -- post being reported
|
post_id int references post on update cascade on delete cascade not null, -- post being reported
|
||||||
post_name varchar(100) not null,
|
post_name varchar(100) not null,
|
||||||
post_url text,
|
post_url text,
|
||||||
post_body text,
|
post_body text,
|
||||||
post_time timestamp not null,
|
reason text not null,
|
||||||
unique(post_id, user_id) -- users should only be able to report a post once
|
resolved bool not null default false,
|
||||||
|
resolver_id int references user_ on update cascade on delete cascade not null, -- user resolving report
|
||||||
|
published timestamp not null default now(),
|
||||||
|
updated timestamp null,
|
||||||
|
unique(post_id, creator_id) -- users should only be able to report a post once
|
||||||
);
|
);
|
||||||
|
|
||||||
create or replace view comment_report_view as
|
create or replace view comment_report_view as
|
||||||
select cr.*,
|
select cr.*,
|
||||||
c.post_id,
|
c.post_id,
|
||||||
p.community_id,
|
p.community_id,
|
||||||
f.name as user_name,
|
f.name as creator_name,
|
||||||
u.id as creator_id,
|
u.id as comment_creator_id,
|
||||||
u.name as creator_name
|
u.name as comment_creator_name
|
||||||
from comment_report cr
|
from comment_report cr
|
||||||
left join comment c on c.id = cr.comment_id
|
left join comment c on c.id = cr.comment_id
|
||||||
left join post p on p.id = c.post_id
|
left join post p on p.id = c.post_id
|
||||||
left join user_ u on u.id = c.creator_id
|
left join user_ u on u.id = c.creator_id
|
||||||
left join user_ f on f.id = cr.user_id;
|
left join user_ f on f.id = cr.creator_id;
|
||||||
|
|
||||||
create or replace view post_report_view as
|
create or replace view post_report_view as
|
||||||
select pr.*,
|
select pr.*,
|
||||||
p.community_id,
|
p.community_id,
|
||||||
f.name as user_name,
|
f.name as creator_name,
|
||||||
u.id as creator_id,
|
u.id as post_creator_id,
|
||||||
u.name as creator_name
|
u.name as post_creator_name
|
||||||
from post_report pr
|
from post_report pr
|
||||||
left join post p on p.id = pr.post_id
|
left join post p on p.id = pr.post_id
|
||||||
left join user_ u on u.id = p.creator_id
|
left join user_ u on u.id = p.creator_id
|
||||||
left join user_ f on f.id = pr.user_id;
|
left join user_ f on f.id = pr.creator_id;
|
||||||
|
|
|
@ -58,9 +58,6 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||||
.route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
|
.route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
|
||||||
.route("/mod", web::post().to(route_post::<AddModToCommunity>))
|
.route("/mod", web::post().to(route_post::<AddModToCommunity>))
|
||||||
.route("/join", web::post().to(route_post::<CommunityJoin>))
|
.route("/join", web::post().to(route_post::<CommunityJoin>))
|
||||||
.route("/comment_reports",web::get().to(route_get::<ListCommentReports>))
|
|
||||||
.route("/post_reports", web::get().to(route_get::<ListPostReports>))
|
|
||||||
.route("/reports", web::get().to(route_get::<GetReportCount>)),
|
|
||||||
)
|
)
|
||||||
// Post
|
// Post
|
||||||
.service(
|
.service(
|
||||||
|
@ -83,12 +80,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||||
.route("/like", web::post().to(route_post::<CreatePostLike>))
|
.route("/like", web::post().to(route_post::<CreatePostLike>))
|
||||||
.route("/save", web::put().to(route_post::<SavePost>))
|
.route("/save", web::put().to(route_post::<SavePost>))
|
||||||
.route("/join", web::post().to(route_post::<PostJoin>))
|
.route("/join", web::post().to(route_post::<PostJoin>))
|
||||||
.route("/report", web::put().to(route_post::<CreatePostReport>))
|
|
||||||
.route("/resolve_report",web::post().to(route_post::<ResolvePostReport>)),
|
|
||||||
)
|
)
|
||||||
// Comment
|
// Comment
|
||||||
.service(
|
.service(
|
||||||
web::scope("/comment")
|
web::scope("/comment")
|
||||||
.wrap(rate_limit.message())
|
.wrap(rate_limit.message())
|
||||||
.route("", web::post().to(route_post::<CreateComment>))
|
.route("", web::post().to(route_post::<CreateComment>))
|
||||||
.route("", web::put().to(route_post::<EditComment>))
|
.route("", web::put().to(route_post::<EditComment>))
|
||||||
|
@ -101,8 +96,6 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||||
.route("/like", web::post().to(route_post::<CreateCommentLike>))
|
.route("/like", web::post().to(route_post::<CreateCommentLike>))
|
||||||
.route("/save", web::put().to(route_post::<SaveComment>))
|
.route("/save", web::put().to(route_post::<SaveComment>))
|
||||||
.route("/list", web::get().to(route_get::<GetComments>))
|
.route("/list", web::get().to(route_get::<GetComments>))
|
||||||
.route("/report", web::put().to(route_post::<CreateCommentReport>))
|
|
||||||
.route("/resolve_report",web::post().to(route_post::<ResolveCommentReport>)),
|
|
||||||
)
|
)
|
||||||
// Private Message
|
// Private Message
|
||||||
.service(
|
.service(
|
||||||
|
@ -177,6 +170,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||||
web::resource("/admin/add")
|
web::resource("/admin/add")
|
||||||
.wrap(rate_limit.message())
|
.wrap(rate_limit.message())
|
||||||
.route(web::post().to(route_post::<AddAdmin>)),
|
.route(web::post().to(route_post::<AddAdmin>)),
|
||||||
|
)
|
||||||
|
// Reports
|
||||||
|
.service(
|
||||||
|
web::scope("/report")
|
||||||
|
.wrap(rate_limit.message())
|
||||||
|
.route("", web::get().to(route_get::<GetReportCount>))
|
||||||
|
.route("",web::post().to(route_post::<CreateReport>))
|
||||||
|
.route("/resolve",web::put().to(route_post::<ResolveReport>))
|
||||||
|
.route("/list", web::get().to(route_get::<ListReports>))
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue