Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding /post/like/list and /comment/like/list for admins. #4332

Merged
merged 1 commit into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions crates/api/src/comment/list_comment_likes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
comment::{ListCommentLikes, ListCommentLikesResponse},
context::LemmyContext,
utils::is_admin,
};
use lemmy_db_views::structs::{LocalUserView, VoteView};
use lemmy_utils::error::LemmyError;

/// Lists likes for a comment
#[tracing::instrument(skip(context))]
pub async fn list_comment_likes(
data: Query<ListCommentLikes>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<ListCommentLikesResponse>, LemmyError> {
// Make sure user is an admin
is_admin(&local_user_view)?;

let comment_likes =
VoteView::list_for_comment(&mut context.pool(), data.comment_id, data.page, data.limit).await?;

Ok(Json(ListCommentLikesResponse { comment_likes }))
}
1 change: 1 addition & 0 deletions crates/api/src/comment/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod distinguish;
pub mod like;
pub mod list_comment_likes;
pub mod save;
24 changes: 24 additions & 0 deletions crates/api/src/post/list_post_likes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
post::{ListPostLikes, ListPostLikesResponse},
utils::is_admin,
};
use lemmy_db_views::structs::{LocalUserView, VoteView};
use lemmy_utils::error::LemmyError;

/// Lists likes for a post
#[tracing::instrument(skip(context))]
pub async fn list_post_likes(
data: Query<ListPostLikes>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<ListPostLikesResponse>, LemmyError> {
// Make sure user is an admin
is_admin(&local_user_view)?;
Copy link
Member

@Nutomic Nutomic Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could also make this accessible for mods so admins dont have to inspect votes on their own. I dont think its a big deal considering that Kbin even publishes all votes.

Copy link
Member Author

@dessalines dessalines Jan 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be open to it, but we can bump that for a later time. Admins-only makes sense to me, both for privacy reasons, and since downvote trolls are an instance-wide problem, not community specific.


let post_likes =
VoteView::list_for_post(&mut context.pool(), data.post_id, data.page, data.limit).await?;

Ok(Json(ListPostLikesResponse { post_likes }))
}
1 change: 1 addition & 0 deletions crates/api/src/post/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod feature;
pub mod get_link_metadata;
pub mod like;
pub mod list_post_likes;
pub mod lock;
pub mod mark_read;
pub mod save;
21 changes: 20 additions & 1 deletion crates/api_common/src/comment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use lemmy_db_schema::{
CommentSortType,
ListingType,
};
use lemmy_db_views::structs::{CommentReportView, CommentView};
use lemmy_db_views::structs::{CommentReportView, CommentView, VoteView};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
Expand Down Expand Up @@ -176,3 +176,22 @@ pub struct ListCommentReports {
pub struct ListCommentReportsResponse {
pub comment_reports: Vec<CommentReportView>,
}

#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// List comment likes. Admins-only.
pub struct ListCommentLikes {
pub comment_id: CommentId,
pub page: Option<i64>,
pub limit: Option<i64>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The comment likes response
pub struct ListCommentLikesResponse {
pub comment_likes: Vec<VoteView>,
}
21 changes: 20 additions & 1 deletion crates/api_common/src/post.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use lemmy_db_schema::{
PostFeatureType,
SortType,
};
use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView};
use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView, VoteView};
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
Expand Down Expand Up @@ -252,3 +252,22 @@ pub struct SiteMetadata {
pub(crate) image: Option<DbUrl>,
pub embed_video_url: Option<DbUrl>,
}

#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// List post likes. Admins-only.
pub struct ListPostLikes {
pub post_id: PostId,
pub page: Option<i64>,
pub limit: Option<i64>,
}

#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The post likes response
pub struct ListPostLikesResponse {
pub post_likes: Vec<VoteView>,
}
2 changes: 2 additions & 0 deletions crates/db_views/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,5 @@ pub mod registration_application_view;
#[cfg(feature = "full")]
pub mod site_view;
pub mod structs;
#[cfg(feature = "full")]
pub mod vote_view;
11 changes: 11 additions & 0 deletions crates/db_views/src/structs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,3 +184,14 @@ pub struct CustomEmojiView {
pub custom_emoji: CustomEmoji,
pub keywords: Vec<CustomEmojiKeyword>,
}

#[skip_serializing_none]
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
/// A vote view for checking a post or comments votes.
pub struct VoteView {
pub creator: Person,
pub score: i16,
}
195 changes: 195 additions & 0 deletions crates/db_views/src/vote_view.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
use crate::structs::VoteView;
use diesel::{result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
newtypes::{CommentId, PostId},
schema::{comment_like, person, post_like},
utils::{get_conn, limit_and_offset, DbPool},
};

impl VoteView {
pub async fn list_for_post(
pool: &mut DbPool<'_>,
post_id: PostId,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
let (limit, offset) = limit_and_offset(page, limit)?;
Copy link
Member

@Nutomic Nutomic Jan 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This uses a default limit of 10 and maximum 50. As the vote objects are quite small and there can be lots of votes it would make sense to use higher values here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hrm... I'd rather not have a custom default just for this one specifically. The front ends can send a custom one anyway.


post_like::table
.inner_join(person::table)
.filter(post_like::post_id.eq(post_id))
.select((person::all_columns, post_like::score))
.order_by(post_like::score)
.limit(limit)
.offset(offset)
.load::<Self>(conn)
.await
}

pub async fn list_for_comment(
pool: &mut DbPool<'_>,
comment_id: CommentId,
page: Option<i64>,
limit: Option<i64>,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
let (limit, offset) = limit_and_offset(page, limit)?;

comment_like::table
.inner_join(person::table)
.filter(comment_like::comment_id.eq(comment_id))
.select((person::all_columns, comment_like::score))
.order_by(comment_like::score)
.limit(limit)
.offset(offset)
.load::<Self>(conn)
.await
}
}

#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]

use crate::structs::VoteView;
use lemmy_db_schema::{
source::{
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm},
community::{Community, CommunityInsertForm},
instance::Instance,
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm, PostLike, PostLikeForm},
},
traits::{Crud, Likeable},
utils::build_db_pool_for_tests,
};
use serial_test::serial;

#[tokio::test]
#[serial]
async fn post_and_comment_vote_views() {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();

let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();

let new_person = PersonInsertForm::builder()
.name("timmy_vv".into())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();

let inserted_timmy = Person::create(pool, &new_person).await.unwrap();

let new_person_2 = PersonInsertForm::builder()
.name("sara_vv".into())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();

let inserted_sara = Person::create(pool, &new_person_2).await.unwrap();

let new_community = CommunityInsertForm::builder()
.name("test community vv".to_string())
.title("nada".to_owned())
.public_key("pubkey".to_string())
.instance_id(inserted_instance.id)
.build();

let inserted_community = Community::create(pool, &new_community).await.unwrap();

let new_post = PostInsertForm::builder()
.name("A test post vv".into())
.creator_id(inserted_timmy.id)
.community_id(inserted_community.id)
.build();

let inserted_post = Post::create(pool, &new_post).await.unwrap();

let comment_form = CommentInsertForm::builder()
.content("A test comment vv".into())
.creator_id(inserted_timmy.id)
.post_id(inserted_post.id)
.build();

let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();

// Timmy upvotes his own post
let timmy_post_vote_form = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_timmy.id,
score: 1,
};
PostLike::like(pool, &timmy_post_vote_form).await.unwrap();

// Sara downvotes timmy's post
let sara_post_vote_form = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_sara.id,
score: -1,
};
PostLike::like(pool, &sara_post_vote_form).await.unwrap();

let expected_post_vote_views = [
VoteView {
creator: inserted_sara.clone(),
score: -1,
},
VoteView {
creator: inserted_timmy.clone(),
score: 1,
},
];

let read_post_vote_views = VoteView::list_for_post(pool, inserted_post.id, None, None)
.await
.unwrap();
assert_eq!(read_post_vote_views, expected_post_vote_views);

// Timothy votes down his own comment
let timmy_comment_vote_form = CommentLikeForm {
post_id: inserted_post.id,
comment_id: inserted_comment.id,
person_id: inserted_timmy.id,
score: -1,
};
CommentLike::like(pool, &timmy_comment_vote_form)
.await
.unwrap();

// Sara upvotes timmy's comment
let sara_comment_vote_form = CommentLikeForm {
post_id: inserted_post.id,
comment_id: inserted_comment.id,
person_id: inserted_sara.id,
score: 1,
};
CommentLike::like(pool, &sara_comment_vote_form)
.await
.unwrap();

let expected_comment_vote_views = [
VoteView {
creator: inserted_timmy.clone(),
score: -1,
},
VoteView {
creator: inserted_sara.clone(),
score: 1,
},
];

let read_comment_vote_views = VoteView::list_for_comment(pool, inserted_comment.id, None, None)
.await
.unwrap();
assert_eq!(read_comment_vote_views, expected_comment_vote_views);

// Cleanup
Instance::delete(pool, inserted_instance.id).await.unwrap();
}
}
10 changes: 9 additions & 1 deletion src/api_routes_http.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
use actix_web::{guard, web};
use lemmy_api::{
comment::{distinguish::distinguish_comment, like::like_comment, save::save_comment},
comment::{
distinguish::distinguish_comment,
like::like_comment,
list_comment_likes::list_comment_likes,
save::save_comment,
},
comment_report::{
create::create_comment_report,
list::list_comment_reports,
Expand Down Expand Up @@ -45,6 +50,7 @@ use lemmy_api::{
feature::feature_post,
get_link_metadata::get_link_metadata,
like::like_post,
list_post_likes::list_post_likes,
lock::lock_post,
mark_read::mark_post_as_read,
save::save_post,
Expand Down Expand Up @@ -202,6 +208,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
.route("/feature", web::post().to(feature_post))
.route("/list", web::get().to(list_posts))
.route("/like", web::post().to(like_post))
.route("/like/list", web::get().to(list_post_likes))
.route("/save", web::put().to(save_post))
.route("/report", web::post().to(create_post_report))
.route("/report/resolve", web::put().to(resolve_post_report))
Expand All @@ -226,6 +233,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) {
.route("/mark_as_read", web::post().to(mark_reply_as_read))
.route("/distinguish", web::post().to(distinguish_comment))
.route("/like", web::post().to(like_comment))
.route("/like/list", web::get().to(list_comment_likes))
.route("/save", web::put().to(save_comment))
.route("/list", web::get().to(list_comments))
.route("/report", web::post().to(create_comment_report))
Expand Down