First pass at adding comment trees. (#2362)

* First pass at adding comment trees.

- Extracted comment replies into its own table.
- Added ltree column to comment
- Added parent_id param to GetComments to fetch a tree branch
- No paging / limiting yet

* Adding child_count to comment_aggregates.

* Adding parent comment update counts

* Fix unit tests.

* Comment tree paging mostly done.

* Fix clippy

* Fix drone tests wrong postgres version.

* Fix unit tests.

* Add back in delete in unit test.

* Add postgres upgrade script.

* Fixing some PR comments.

* Move update ltree into Comment::create

* Updating based on comments.

* Fix send soft fail.
This commit is contained in:
Dessalines 2022-07-29 23:55:59 -04:00 committed by GitHub
parent becb8b4f66
commit 9c3efe32e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
72 changed files with 3692 additions and 2661 deletions

View file

@ -155,7 +155,7 @@ steps:
services: services:
- name: database - name: database
image: postgres:12-alpine image: postgres:14-alpine
environment: environment:
POSTGRES_USER: lemmy POSTGRES_USER: lemmy
POSTGRES_PASSWORD: password POSTGRES_PASSWORD: password

43
Cargo.lock generated
View file

@ -834,12 +834,12 @@ dependencies = [
[[package]] [[package]]
name = "darling" name = "darling"
version = "0.13.1" version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c"
dependencies = [ dependencies = [
"darling_core 0.13.1", "darling_core 0.13.4",
"darling_macro 0.13.1", "darling_macro 0.13.4",
] ]
[[package]] [[package]]
@ -868,9 +868,9 @@ dependencies = [
[[package]] [[package]]
name = "darling_core" name = "darling_core"
version = "0.13.1" version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a340f241d2ceed1deb47ae36c4144b2707ec7dd0b649f894cb39bb595986324" checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610"
dependencies = [ dependencies = [
"fnv", "fnv",
"ident_case", "ident_case",
@ -907,11 +907,11 @@ dependencies = [
[[package]] [[package]]
name = "darling_macro" name = "darling_macro"
version = "0.13.1" version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835"
dependencies = [ dependencies = [
"darling_core 0.13.1", "darling_core 0.13.4",
"quote 1.0.18", "quote 1.0.18",
"syn 1.0.96", "syn 1.0.96",
] ]
@ -1058,6 +1058,16 @@ dependencies = [
"syn 1.0.96", "syn 1.0.96",
] ]
[[package]]
name = "diesel_ltree"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55a0b2b2e948a2d8ab673ccee9f37b20bdcc8b7acb40a242a0fdf53d4c2678b0"
dependencies = [
"byteorder",
"diesel",
]
[[package]] [[package]]
name = "diesel_migrations" name = "diesel_migrations"
version = "1.4.0" version = "1.4.0"
@ -1132,7 +1142,7 @@ name = "doku-derive"
version = "0.11.0" version = "0.11.0"
source = "git+https://github.com/anixe/doku#10a0339a82be92b5f160aac325d11c9c2ef875e1" source = "git+https://github.com/anixe/doku#10a0339a82be92b5f160aac325d11c9c2ef875e1"
dependencies = [ dependencies = [
"darling 0.13.1", "darling 0.13.4",
"proc-macro2 1.0.39", "proc-macro2 1.0.39",
"quote 1.0.18", "quote 1.0.18",
"syn 1.0.96", "syn 1.0.96",
@ -1984,6 +1994,7 @@ dependencies = [
"chrono", "chrono",
"diesel", "diesel",
"diesel-derive-newtype", "diesel-derive-newtype",
"diesel_ltree",
"diesel_migrations", "diesel_migrations",
"lemmy_utils", "lemmy_utils",
"once_cell", "once_cell",
@ -2002,6 +2013,7 @@ name = "lemmy_db_views"
version = "0.16.5" version = "0.16.5"
dependencies = [ dependencies = [
"diesel", "diesel",
"diesel_ltree",
"lemmy_db_schema", "lemmy_db_schema",
"serde", "serde",
"serial_test", "serial_test",
@ -3507,22 +3519,21 @@ dependencies = [
[[package]] [[package]]
name = "serde_with" name = "serde_with"
version = "1.12.0" version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec1e6ec4d8950e5b1e894eac0d360742f3b1407a6078a604a731c4b3f49cefbc" checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff"
dependencies = [ dependencies = [
"rustversion",
"serde", "serde",
"serde_with_macros", "serde_with_macros",
] ]
[[package]] [[package]]
name = "serde_with_macros" name = "serde_with_macros"
version = "1.5.1" version = "1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12e47be9471c72889ebafb5e14d5ff930d89ae7a67bbdb5f8abb564f845a927e" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082"
dependencies = [ dependencies = [
"darling 0.13.1", "darling 0.13.4",
"proc-macro2 1.0.39", "proc-macro2 1.0.39",
"quote 1.0.18", "quote 1.0.18",
"syn 1.0.96", "syn 1.0.96",

View file

@ -12,14 +12,17 @@
"api-test": "jest -i follow.spec.ts && jest -i src/post.spec.ts && jest -i comment.spec.ts && jest -i private_message.spec.ts && jest -i user.spec.ts && jest -i community.spec.ts" "api-test": "jest -i follow.spec.ts && jest -i src/post.spec.ts && jest -i comment.spec.ts && jest -i private_message.spec.ts && jest -i user.spec.ts && jest -i community.spec.ts"
}, },
"devDependencies": { "devDependencies": {
"@sniptt/monads": "^0.5.10",
"@types/jest": "^26.0.23", "@types/jest": "^26.0.23",
"eslint": "^7.30.0", "class-transformer": "^0.5.1",
"eslint-plugin-jane": "^9.0.3", "eslint": "^8.20.0",
"eslint-plugin-jane": "^11.2.2",
"jest": "^27.0.6", "jest": "^27.0.6",
"lemmy-js-client": "0.17.0-rc.11", "lemmy-js-client": "0.17.0-rc.37",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"prettier": "^2.3.2", "prettier": "^2.7.1",
"reflect-metadata": "^0.1.13",
"ts-jest": "^27.0.3", "ts-jest": "^27.0.3",
"typescript": "^4.3.5" "typescript": "^4.6.4"
} }
} }

View file

@ -1,4 +1,8 @@
jest.setTimeout(180000); jest.setTimeout(180000);
import {None, Some} from '@sniptt/monads';
import { CommentView } from 'lemmy-js-client';
import { PostResponse } from 'lemmy-js-client';
import { import {
alpha, alpha,
beta, beta,
@ -24,10 +28,9 @@ import {
randomString, randomString,
API, API,
unfollows, unfollows,
getComments,
getCommentParentId,
} from './shared'; } from './shared';
import { CommentView } from 'lemmy-js-client';
import { PostResponse } from 'lemmy-js-client';
let postRes: PostResponse; let postRes: PostResponse;
@ -39,7 +42,7 @@ beforeAll(async () => {
let betaCommunity = (await resolveBetaCommunity(alpha)).community; let betaCommunity = (await resolveBetaCommunity(alpha)).community;
postRes = await createPost( postRes = await createPost(
alpha, alpha,
betaCommunity.community.id betaCommunity.unwrap().community.id
); );
}); });
@ -62,14 +65,14 @@ function assertCommentFederation(
} }
test('Create a comment', async () => { test('Create a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post_view.post.id, None);
expect(commentRes.comment_view.comment.content).toBeDefined(); expect(commentRes.comment_view.comment.content).toBeDefined();
expect(commentRes.comment_view.community.local).toBe(false); expect(commentRes.comment_view.community.local).toBe(false);
expect(commentRes.comment_view.creator.local).toBe(true); expect(commentRes.comment_view.creator.local).toBe(true);
expect(commentRes.comment_view.counts.score).toBe(1); expect(commentRes.comment_view.counts.score).toBe(1);
// Make sure that comment is liked on beta // Make sure that comment is liked on beta
let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment.unwrap();
expect(betaComment).toBeDefined(); expect(betaComment).toBeDefined();
expect(betaComment.community.local).toBe(true); expect(betaComment.community.local).toBe(true);
expect(betaComment.creator.local).toBe(false); expect(betaComment.creator.local).toBe(false);
@ -78,15 +81,15 @@ test('Create a comment', async () => {
}); });
test('Create a comment in a non-existent post', async () => { test('Create a comment in a non-existent post', async () => {
let commentRes = await createComment(alpha, -1); let commentRes = await createComment(alpha, -1, None) as any;
expect(commentRes).toStrictEqual({ error: 'couldnt_find_post' }); expect(commentRes.error).toBe('couldnt_find_post');
}); });
test('Update a comment', async () => { test('Update a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post_view.post.id, None);
// Federate the comment first // Federate the comment first
let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment;
assertCommentFederation(betaComment, commentRes.comment_view); assertCommentFederation(betaComment.unwrap(), commentRes.comment_view);
let updateCommentRes = await editComment( let updateCommentRes = await editComment(
alpha, alpha,
@ -102,7 +105,7 @@ test('Update a comment', async () => {
let betaCommentUpdated = (await resolveComment( let betaCommentUpdated = (await resolveComment(
beta, beta,
commentRes.comment_view.comment commentRes.comment_view.comment
)).comment; )).comment.unwrap();
assertCommentFederation( assertCommentFederation(
betaCommentUpdated, betaCommentUpdated,
updateCommentRes.comment_view updateCommentRes.comment_view
@ -110,7 +113,7 @@ test('Update a comment', async () => {
}); });
test('Delete a comment', async () => { test('Delete a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post_view.post.id, None);
let deleteCommentRes = await deleteComment( let deleteCommentRes = await deleteComment(
alpha, alpha,
@ -121,8 +124,8 @@ test('Delete a comment', async () => {
expect(deleteCommentRes.comment_view.comment.content).toBe(""); expect(deleteCommentRes.comment_view.comment.content).toBe("");
// Make sure that comment is undefined on beta // Make sure that comment is undefined on beta
let betaCommentRes: any = await resolveComment(beta, commentRes.comment_view.comment); let betaCommentRes = await resolveComment(beta, commentRes.comment_view.comment) as any;
expect(betaCommentRes).toStrictEqual({ error: 'couldnt_find_object' }); expect(betaCommentRes.error).toBe('couldnt_find_object');
let undeleteCommentRes = await deleteComment( let undeleteCommentRes = await deleteComment(
alpha, alpha,
@ -132,7 +135,7 @@ test('Delete a comment', async () => {
expect(undeleteCommentRes.comment_view.comment.deleted).toBe(false); expect(undeleteCommentRes.comment_view.comment.deleted).toBe(false);
// Make sure that comment is undeleted on beta // Make sure that comment is undeleted on beta
let betaComment2 = (await resolveComment(beta, commentRes.comment_view.comment)).comment; let betaComment2 = (await resolveComment(beta, commentRes.comment_view.comment)).comment.unwrap();
expect(betaComment2.comment.deleted).toBe(false); expect(betaComment2.comment.deleted).toBe(false);
assertCommentFederation( assertCommentFederation(
betaComment2, betaComment2,
@ -141,12 +144,12 @@ test('Delete a comment', async () => {
}); });
test('Remove a comment from admin and community on the same instance', async () => { test('Remove a comment from admin and community on the same instance', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post_view.post.id, None);
// Get the id for beta // Get the id for beta
let betaCommentId = ( let betaCommentId = (
await resolveComment(beta, commentRes.comment_view.comment) await resolveComment(beta, commentRes.comment_view.comment)
).comment.comment.id; ).comment.unwrap().comment.id;
// The beta admin removes it (the community lives on beta) // The beta admin removes it (the community lives on beta)
let removeCommentRes = await removeComment(beta, true, betaCommentId); let removeCommentRes = await removeComment(beta, true, betaCommentId);
@ -154,17 +157,17 @@ test('Remove a comment from admin and community on the same instance', async ()
expect(removeCommentRes.comment_view.comment.content).toBe(""); expect(removeCommentRes.comment_view.comment.content).toBe("");
// Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it) // Make sure that comment is removed on alpha (it gets pushed since an admin from beta removed it)
let refetchedPost = await getPost(alpha, postRes.post_view.post.id); let refetchedPostComments = await getComments(alpha, postRes.post_view.post.id);
expect(refetchedPost.comments[0].comment.removed).toBe(true); expect(refetchedPostComments.comments[0].comment.removed).toBe(true);
let unremoveCommentRes = await removeComment(beta, false, betaCommentId); let unremoveCommentRes = await removeComment(beta, false, betaCommentId);
expect(unremoveCommentRes.comment_view.comment.removed).toBe(false); expect(unremoveCommentRes.comment_view.comment.removed).toBe(false);
// Make sure that comment is unremoved on beta // Make sure that comment is unremoved on beta
let refetchedPost2 = await getPost(alpha, postRes.post_view.post.id); let refetchedPostComments2 = await getComments(alpha, postRes.post_view.post.id);
expect(refetchedPost2.comments[0].comment.removed).toBe(false); expect(refetchedPostComments2.comments[0].comment.removed).toBe(false);
assertCommentFederation( assertCommentFederation(
refetchedPost2.comments[0], refetchedPostComments2.comments[0],
unremoveCommentRes.comment_view unremoveCommentRes.comment_view
); );
}); });
@ -182,11 +185,11 @@ test('Remove a comment from admin and community on different instance', async ()
newAlphaApi, newAlphaApi,
newCommunity.community_view.community.id newCommunity.community_view.community.id
); );
let commentRes = await createComment(newAlphaApi, newPost.post_view.post.id); let commentRes = await createComment(newAlphaApi, newPost.post_view.post.id, None);
expect(commentRes.comment_view.comment.content).toBeDefined(); expect(commentRes.comment_view.comment.content).toBeDefined();
// Beta searches that to cache it, then removes it // Beta searches that to cache it, then removes it
let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment.unwrap();
let removeCommentRes = await removeComment( let removeCommentRes = await removeComment(
beta, beta,
true, true,
@ -195,18 +198,18 @@ test('Remove a comment from admin and community on different instance', async ()
expect(removeCommentRes.comment_view.comment.removed).toBe(true); expect(removeCommentRes.comment_view.comment.removed).toBe(true);
// Make sure its not removed on alpha // Make sure its not removed on alpha
let refetchedPost = await getPost(newAlphaApi, newPost.post_view.post.id); let refetchedPostComments = await getComments(alpha, newPost.post_view.post.id);
expect(refetchedPost.comments[0].comment.removed).toBe(false); expect(refetchedPostComments.comments[0].comment.removed).toBe(false);
assertCommentFederation(refetchedPost.comments[0], commentRes.comment_view); assertCommentFederation(refetchedPostComments.comments[0], commentRes.comment_view);
}); });
test('Unlike a comment', async () => { test('Unlike a comment', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post_view.post.id, None);
let unlike = await likeComment(alpha, 0, commentRes.comment_view.comment); let unlike = await likeComment(alpha, 0, commentRes.comment_view.comment);
expect(unlike.comment_view.counts.score).toBe(0); expect(unlike.comment_view.counts.score).toBe(0);
// Make sure that post is unliked on beta // Make sure that post is unliked on beta
let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment.unwrap();
expect(betaComment).toBeDefined(); expect(betaComment).toBeDefined();
expect(betaComment.community.local).toBe(true); expect(betaComment.community.local).toBe(true);
expect(betaComment.creator.local).toBe(false); expect(betaComment.creator.local).toBe(false);
@ -214,23 +217,23 @@ test('Unlike a comment', async () => {
}); });
test('Federated comment like', async () => { test('Federated comment like', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post_view.post.id, None);
// Find the comment on beta // Find the comment on beta
let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment.unwrap();
let like = await likeComment(beta, 1, betaComment.comment); let like = await likeComment(beta, 1, betaComment.comment);
expect(like.comment_view.counts.score).toBe(2); expect(like.comment_view.counts.score).toBe(2);
// Get the post from alpha, check the likes // Get the post from alpha, check the likes
let post = await getPost(alpha, postRes.post_view.post.id); let postComments = await getComments(alpha, postRes.post_view.post.id);
expect(post.comments[0].counts.score).toBe(2); expect(postComments.comments[0].counts.score).toBe(2);
}); });
test('Reply to a comment', async () => { test('Reply to a comment', async () => {
// Create a comment on alpha, find it on beta // Create a comment on alpha, find it on beta
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post_view.post.id, None);
let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment.unwrap();
// find that comment id on beta // find that comment id on beta
@ -238,22 +241,22 @@ test('Reply to a comment', async () => {
let replyRes = await createComment( let replyRes = await createComment(
beta, beta,
betaComment.post.id, betaComment.post.id,
betaComment.comment.id Some(betaComment.comment.id)
); );
expect(replyRes.comment_view.comment.content).toBeDefined(); expect(replyRes.comment_view.comment.content).toBeDefined();
expect(replyRes.comment_view.community.local).toBe(true); expect(replyRes.comment_view.community.local).toBe(true);
expect(replyRes.comment_view.creator.local).toBe(true); expect(replyRes.comment_view.creator.local).toBe(true);
expect(replyRes.comment_view.comment.parent_id).toBe(betaComment.comment.id); expect(getCommentParentId(replyRes.comment_view.comment).unwrap()).toBe(betaComment.comment.id);
expect(replyRes.comment_view.counts.score).toBe(1); expect(replyRes.comment_view.counts.score).toBe(1);
// Make sure that comment is seen on alpha // Make sure that comment is seen on alpha
// TODO not sure why, but a searchComment back to alpha, for the ap_id of betas // TODO not sure why, but a searchComment back to alpha, for the ap_id of betas
// comment, isn't working. // comment, isn't working.
// let searchAlpha = await searchComment(alpha, replyRes.comment); // let searchAlpha = await searchComment(alpha, replyRes.comment);
let post = await getPost(alpha, postRes.post_view.post.id); let postComments = await getComments(alpha, postRes.post_view.post.id);
let alphaComment = post.comments[0]; let alphaComment = postComments.comments[0];
expect(alphaComment.comment.content).toBeDefined(); expect(alphaComment.comment.content).toBeDefined();
expect(alphaComment.comment.parent_id).toBe(post.comments[1].comment.id); expect(getCommentParentId(alphaComment.comment).unwrap()).toBe(postComments.comments[1].comment.id);
expect(alphaComment.community.local).toBe(false); expect(alphaComment.community.local).toBe(false);
expect(alphaComment.creator.local).toBe(false); expect(alphaComment.creator.local).toBe(false);
expect(alphaComment.counts.score).toBe(1); expect(alphaComment.counts.score).toBe(1);
@ -263,11 +266,11 @@ test('Reply to a comment', async () => {
test('Mention beta', async () => { test('Mention beta', async () => {
// Create a mention on alpha // Create a mention on alpha
let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8551'; let mentionContent = 'A test mention of @lemmy_beta@lemmy-beta:8551';
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post_view.post.id, None);
let mentionRes = await createComment( let mentionRes = await createComment(
alpha, alpha,
postRes.post_view.post.id, postRes.post_view.post.id,
commentRes.comment_view.comment.id, Some(commentRes.comment_view.comment.id),
mentionContent mentionContent
); );
expect(mentionRes.comment_view.comment.content).toBeDefined(); expect(mentionRes.comment_view.comment.content).toBeDefined();
@ -283,8 +286,8 @@ test('Mention beta', async () => {
}); });
test('Comment Search', async () => { test('Comment Search', async () => {
let commentRes = await createComment(alpha, postRes.post_view.post.id); let commentRes = await createComment(alpha, postRes.post_view.post.id, None);
let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment; let betaComment = (await resolveComment(beta, commentRes.comment_view.comment)).comment.unwrap();
assertCommentFederation(betaComment, commentRes.comment_view); assertCommentFederation(betaComment, commentRes.comment_view);
}); });
@ -295,14 +298,14 @@ test('A and G subscribe to B (center) A posts, G mentions B, it gets announced t
expect(alphaPost.post_view.community.local).toBe(true); expect(alphaPost.post_view.community.local).toBe(true);
// Make sure gamma sees it // Make sure gamma sees it
let gammaPost = (await resolvePost(gamma, alphaPost.post_view.post)).post; let gammaPost = (await resolvePost(gamma, alphaPost.post_view.post)).post.unwrap();
let commentContent = let commentContent =
'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8551'; 'A jest test federated comment announce, lets mention @lemmy_beta@lemmy-beta:8551';
let commentRes = await createComment( let commentRes = await createComment(
gamma, gamma,
gammaPost.post.id, gammaPost.post.id,
undefined, None,
commentContent commentContent
); );
expect(commentRes.comment_view.comment.content).toBe(commentContent); expect(commentRes.comment_view.comment.content).toBe(commentContent);
@ -311,12 +314,12 @@ test('A and G subscribe to B (center) A posts, G mentions B, it gets announced t
expect(commentRes.comment_view.counts.score).toBe(1); expect(commentRes.comment_view.counts.score).toBe(1);
// Make sure alpha sees it // Make sure alpha sees it
let alphaPost2 = await getPost(alpha, alphaPost.post_view.post.id); let alphaPostComments2 = await getComments(alpha, alphaPost.post_view.post.id);
expect(alphaPost2.comments[0].comment.content).toBe(commentContent); expect(alphaPostComments2.comments[0].comment.content).toBe(commentContent);
expect(alphaPost2.comments[0].community.local).toBe(true); expect(alphaPostComments2.comments[0].community.local).toBe(true);
expect(alphaPost2.comments[0].creator.local).toBe(false); expect(alphaPostComments2.comments[0].creator.local).toBe(false);
expect(alphaPost2.comments[0].counts.score).toBe(1); expect(alphaPostComments2.comments[0].counts.score).toBe(1);
assertCommentFederation(alphaPost2.comments[0], commentRes.comment_view); assertCommentFederation(alphaPostComments2.comments[0], commentRes.comment_view);
// Make sure beta has mentions // Make sure beta has mentions
let mentionsRes = await getMentions(beta); let mentionsRes = await getMentions(beta);
@ -342,9 +345,9 @@ test('Check that activity from another instance is sent to third instance', asyn
expect(betaPost.post_view.community.local).toBe(true); expect(betaPost.post_view.community.local).toBe(true);
// Make sure gamma and alpha see it // Make sure gamma and alpha see it
let gammaPost = (await resolvePost(gamma, betaPost.post_view.post)).post; let gammaPost = (await resolvePost(gamma, betaPost.post_view.post)).post.unwrap();
expect(gammaPost.post).toBeDefined(); expect(gammaPost.post).toBeDefined();
let alphaPost = (await resolvePost(alpha, betaPost.post_view.post)).post; let alphaPost = (await resolvePost(alpha, betaPost.post_view.post)).post.unwrap();
expect(alphaPost.post).toBeDefined(); expect(alphaPost.post).toBeDefined();
// The bug: gamma comments, and alpha should see it. // The bug: gamma comments, and alpha should see it.
@ -352,7 +355,7 @@ test('Check that activity from another instance is sent to third instance', asyn
let commentRes = await createComment( let commentRes = await createComment(
gamma, gamma,
gammaPost.post.id, gammaPost.post.id,
undefined, None,
commentContent commentContent
); );
expect(commentRes.comment_view.comment.content).toBe(commentContent); expect(commentRes.comment_view.comment.content).toBe(commentContent);
@ -361,12 +364,12 @@ test('Check that activity from another instance is sent to third instance', asyn
expect(commentRes.comment_view.counts.score).toBe(1); expect(commentRes.comment_view.counts.score).toBe(1);
// Make sure alpha sees it // Make sure alpha sees it
let alphaPost2 = await getPost(alpha, alphaPost.post.id); let alphaPostComments2 = await getComments(alpha, alphaPost.post.id);
expect(alphaPost2.comments[0].comment.content).toBe(commentContent); expect(alphaPostComments2.comments[0].comment.content).toBe(commentContent);
expect(alphaPost2.comments[0].community.local).toBe(false); expect(alphaPostComments2.comments[0].community.local).toBe(false);
expect(alphaPost2.comments[0].creator.local).toBe(false); expect(alphaPostComments2.comments[0].creator.local).toBe(false);
expect(alphaPost2.comments[0].counts.score).toBe(1); expect(alphaPostComments2.comments[0].counts.score).toBe(1);
assertCommentFederation(alphaPost2.comments[0], commentRes.comment_view); assertCommentFederation(alphaPostComments2.comments[0], commentRes.comment_view);
await unfollowRemotes(alpha); await unfollowRemotes(alpha);
await unfollowRemotes(gamma); await unfollowRemotes(gamma);
@ -376,7 +379,7 @@ test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedde
// Unfollow all remote communities // Unfollow all remote communities
let site = await unfollowRemotes(alpha); let site = await unfollowRemotes(alpha);
expect( expect(
site.my_user.follows.filter(c => c.community.local == false).length site.my_user.unwrap().follows.filter(c => c.community.local == false).length
).toBe(0); ).toBe(0);
// B creates a post, and two comments, should be invisible to A // B creates a post, and two comments, should be invisible to A
@ -387,7 +390,7 @@ test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedde
let parentCommentRes = await createComment( let parentCommentRes = await createComment(
beta, beta,
postRes.post_view.post.id, postRes.post_view.post.id,
undefined, None,
parentCommentContent parentCommentContent
); );
expect(parentCommentRes.comment_view.comment.content).toBe( expect(parentCommentRes.comment_view.comment.content).toBe(
@ -399,7 +402,7 @@ test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedde
let childCommentRes = await createComment( let childCommentRes = await createComment(
beta, beta,
postRes.post_view.post.id, postRes.post_view.post.id,
parentCommentRes.comment_view.comment.id, Some(parentCommentRes.comment_view.comment.id),
childCommentContent childCommentContent
); );
expect(childCommentRes.comment_view.comment.content).toBe( expect(childCommentRes.comment_view.comment.content).toBe(
@ -421,12 +424,13 @@ test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedde
expect(updateRes.comment_view.comment.content).toBe(updatedCommentContent); expect(updateRes.comment_view.comment.content).toBe(updatedCommentContent);
// Get the post from alpha // Get the post from alpha
let alphaPostB = (await resolvePost(alpha, postRes.post_view.post)).post; let alphaPostB = (await resolvePost(alpha, postRes.post_view.post)).post.unwrap();
let alphaPost = await getPost(alpha, alphaPostB.post.id); let alphaPost = await getPost(alpha, alphaPostB.post.id);
let alphaPostComments = await getComments(alpha, alphaPostB.post.id);
expect(alphaPost.post_view.post.name).toBeDefined(); expect(alphaPost.post_view.post.name).toBeDefined();
assertCommentFederation(alphaPost.comments[1], parentCommentRes.comment_view); assertCommentFederation(alphaPostComments.comments[1], parentCommentRes.comment_view);
assertCommentFederation(alphaPost.comments[0], updateRes.comment_view); assertCommentFederation(alphaPostComments.comments[0], updateRes.comment_view);
expect(alphaPost.post_view.community.local).toBe(false); expect(alphaPost.post_view.community.local).toBe(false);
expect(alphaPost.post_view.creator.local).toBe(false); expect(alphaPost.post_view.creator.local).toBe(false);
@ -435,13 +439,13 @@ test('Fetch in_reply_tos: A is unsubbed from B, B makes a post, and some embedde
test('Report a comment', async () => { test('Report a comment', async () => {
let betaCommunity = (await resolveBetaCommunity(beta)).community; let betaCommunity = (await resolveBetaCommunity(beta)).community.unwrap();
let postRes = (await createPost(beta, betaCommunity.community.id)).post_view.post; let postRes = (await createPost(beta, betaCommunity.community.id)).post_view.post;
expect(postRes).toBeDefined(); expect(postRes).toBeDefined();
let commentRes = (await createComment(beta, postRes.id)).comment_view.comment; let commentRes = (await createComment(beta, postRes.id, None)).comment_view.comment;
expect(commentRes).toBeDefined(); expect(commentRes).toBeDefined();
let alphaComment = (await resolveComment(alpha, commentRes)).comment.comment; let alphaComment = (await resolveComment(alpha, commentRes)).comment.unwrap().comment;
let alphaReport = (await reportComment(alpha, alphaComment.id, randomString(10))) let alphaReport = (await reportComment(alpha, alphaComment.id, randomString(10)))
.comment_report_view.comment_report; .comment_report_view.comment_report;
@ -451,3 +455,7 @@ test('Report a comment', async () => {
expect(betaReport.original_comment_text).toBe(alphaReport.original_comment_text); expect(betaReport.original_comment_text).toBe(alphaReport.original_comment_text);
expect(betaReport.reason).toBe(alphaReport.reason); expect(betaReport.reason).toBe(alphaReport.reason);
}); });
function N(gamma: API, id: number, N: any, commentContent: string) {
throw new Error('Function not implemented.');
}

View file

@ -1,4 +1,6 @@
jest.setTimeout(120000); jest.setTimeout(120000);
import { CommunityView } from 'lemmy-js-client';
import { import {
alpha, alpha,
beta, beta,
@ -10,7 +12,6 @@ import {
getCommunity, getCommunity,
followCommunity, followCommunity,
} from './shared'; } from './shared';
import { CommunityView } from 'lemmy-js-client';
beforeAll(async () => { beforeAll(async () => {
await setupLogins(); await setupLogins();
@ -23,11 +24,11 @@ function assertCommunityFederation(
expect(communityOne.community.actor_id).toBe(communityTwo.community.actor_id); expect(communityOne.community.actor_id).toBe(communityTwo.community.actor_id);
expect(communityOne.community.name).toBe(communityTwo.community.name); expect(communityOne.community.name).toBe(communityTwo.community.name);
expect(communityOne.community.title).toBe(communityTwo.community.title); expect(communityOne.community.title).toBe(communityTwo.community.title);
expect(communityOne.community.description).toBe( expect(communityOne.community.description.unwrapOr("none")).toBe(
communityTwo.community.description communityTwo.community.description.unwrapOr("none")
); );
expect(communityOne.community.icon).toBe(communityTwo.community.icon); expect(communityOne.community.icon.unwrapOr("none")).toBe(communityTwo.community.icon.unwrapOr("none"));
expect(communityOne.community.banner).toBe(communityTwo.community.banner); expect(communityOne.community.banner.unwrapOr("none")).toBe(communityTwo.community.banner.unwrapOr("none"));
expect(communityOne.community.published).toBe( expect(communityOne.community.published).toBe(
communityTwo.community.published communityTwo.community.published
); );
@ -47,7 +48,7 @@ test('Create community', async () => {
// Cache the community on beta, make sure it has the other fields // Cache the community on beta, make sure it has the other fields
let searchShort = `!${prevName}@lemmy-alpha:8541`; let searchShort = `!${prevName}@lemmy-alpha:8541`;
let betaCommunity = (await resolveCommunity(beta, searchShort)).community; let betaCommunity = (await resolveCommunity(beta, searchShort)).community.unwrap();
assertCommunityFederation(betaCommunity, communityRes.community_view); assertCommunityFederation(betaCommunity, communityRes.community_view);
}); });
@ -56,7 +57,7 @@ test('Delete community', async () => {
// Cache the community on Alpha // Cache the community on Alpha
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`; let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community; let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community.unwrap();
assertCommunityFederation(alphaCommunity, communityRes.community_view); assertCommunityFederation(alphaCommunity, communityRes.community_view);
// Follow the community from alpha // Follow the community from alpha
@ -107,7 +108,7 @@ test('Remove community', async () => {
// Cache the community on Alpha // Cache the community on Alpha
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`; let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community; let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community.unwrap();
assertCommunityFederation(alphaCommunity, communityRes.community_view); assertCommunityFederation(alphaCommunity, communityRes.community_view);
// Follow the community from alpha // Follow the community from alpha
@ -158,6 +159,6 @@ test('Search for beta community', async () => {
expect(communityRes.community_view.community.name).toBeDefined(); expect(communityRes.community_view.community.name).toBeDefined();
let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`; let searchShort = `!${communityRes.community_view.community.name}@lemmy-beta:8551`;
let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community; let alphaCommunity = (await resolveCommunity(alpha, searchShort)).community.unwrap();
assertCommunityFederation(alphaCommunity, communityRes.community_view); assertCommunityFederation(alphaCommunity, communityRes.community_view);
}); });

View file

@ -19,7 +19,7 @@ afterAll(async () => {
}); });
test('Follow federated community', async () => { test('Follow federated community', async () => {
let betaCommunity = (await resolveBetaCommunity(alpha)).community; let betaCommunity = (await resolveBetaCommunity(alpha)).community.unwrap();
let follow = await followCommunity( let follow = await followCommunity(
alpha, alpha,
true, true,
@ -33,11 +33,11 @@ test('Follow federated community', async () => {
// Check it from local // Check it from local
let site = await getSite(alpha); let site = await getSite(alpha);
let remoteCommunityId = site.my_user.follows.find( let remoteCommunityId = site.my_user.unwrap().follows.find(
c => c.community.local == false c => c.community.local == false
).community.id; ).community.id;
expect(remoteCommunityId).toBeDefined(); expect(remoteCommunityId).toBeDefined();
expect(site.my_user.follows.length).toBe(1); expect(site.my_user.unwrap().follows.length).toBe(1);
// Test an unfollow // Test an unfollow
let unfollow = await followCommunity(alpha, false, remoteCommunityId); let unfollow = await followCommunity(alpha, false, remoteCommunityId);
@ -45,5 +45,5 @@ test('Follow federated community', async () => {
// Make sure you are unsubbed locally // Make sure you are unsubbed locally
let siteUnfollowCheck = await getSite(alpha); let siteUnfollowCheck = await getSite(alpha);
expect(siteUnfollowCheck.my_user.follows.length).toBe(0); expect(siteUnfollowCheck.my_user.unwrap().follows.length).toBe(0);
}); });

View file

@ -1,4 +1,6 @@
jest.setTimeout(120000); jest.setTimeout(120000);
import {None} from '@sniptt/monads';
import { PostView, CommunityView } from 'lemmy-js-client';
import { import {
alpha, alpha,
beta, beta,
@ -32,13 +34,12 @@ import {
getSite, getSite,
unfollows unfollows
} from './shared'; } from './shared';
import { PostView, CommunityView } from 'lemmy-js-client';
let betaCommunity: CommunityView; let betaCommunity: CommunityView;
beforeAll(async () => { beforeAll(async () => {
await setupLogins(); await setupLogins();
betaCommunity = (await resolveBetaCommunity(alpha)).community; betaCommunity = (await resolveBetaCommunity(alpha)).community.unwrap();
expect(betaCommunity).toBeDefined(); expect(betaCommunity).toBeDefined();
await unfollows(); await unfollows();
}); });
@ -50,12 +51,12 @@ afterAll(async () => {
function assertPostFederation(postOne: PostView, postTwo: PostView) { function assertPostFederation(postOne: PostView, postTwo: PostView) {
expect(postOne.post.ap_id).toBe(postTwo.post.ap_id); expect(postOne.post.ap_id).toBe(postTwo.post.ap_id);
expect(postOne.post.name).toBe(postTwo.post.name); expect(postOne.post.name).toBe(postTwo.post.name);
expect(postOne.post.body).toBe(postTwo.post.body); expect(postOne.post.body.unwrapOr("none")).toBe(postTwo.post.body.unwrapOr("none"));
expect(postOne.post.url).toBe(postTwo.post.url); expect(postOne.post.url.unwrapOr("none")).toBe(postTwo.post.url.unwrapOr("none"));
expect(postOne.post.nsfw).toBe(postTwo.post.nsfw); expect(postOne.post.nsfw).toBe(postTwo.post.nsfw);
expect(postOne.post.embed_title).toBe(postTwo.post.embed_title); expect(postOne.post.embed_title.unwrapOr("none")).toBe(postTwo.post.embed_title.unwrapOr("none"));
expect(postOne.post.embed_description).toBe(postTwo.post.embed_description); expect(postOne.post.embed_description.unwrapOr("none")).toBe(postTwo.post.embed_description.unwrapOr("none"));
expect(postOne.post.embed_html).toBe(postTwo.post.embed_html); expect(postOne.post.embed_html.unwrapOr("none")).toBe(postTwo.post.embed_html.unwrapOr("none"));
expect(postOne.post.published).toBe(postTwo.post.published); expect(postOne.post.published).toBe(postTwo.post.published);
expect(postOne.community.actor_id).toBe(postTwo.community.actor_id); expect(postOne.community.actor_id).toBe(postTwo.community.actor_id);
expect(postOne.post.locked).toBe(postTwo.post.locked); expect(postOne.post.locked).toBe(postTwo.post.locked);
@ -71,7 +72,7 @@ test('Create a post', async () => {
expect(postRes.post_view.counts.score).toBe(1); expect(postRes.post_view.counts.score).toBe(1);
// Make sure that post is liked on beta // Make sure that post is liked on beta
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
expect(betaPost).toBeDefined(); expect(betaPost).toBeDefined();
expect(betaPost.community.local).toBe(true); expect(betaPost.community.local).toBe(true);
@ -81,16 +82,16 @@ test('Create a post', async () => {
// Delta only follows beta, so it should not see an alpha ap_id // Delta only follows beta, so it should not see an alpha ap_id
let deltaPost = (await resolvePost(delta, postRes.post_view.post)).post; let deltaPost = (await resolvePost(delta, postRes.post_view.post)).post;
expect(deltaPost).toBeUndefined(); expect(deltaPost.isNone()).toBe(true)
// Epsilon has alpha blocked, it should not see the alpha post // Epsilon has alpha blocked, it should not see the alpha post
let epsilonPost = (await resolvePost(epsilon, postRes.post_view.post)).post; let epsilonPost = (await resolvePost(epsilon, postRes.post_view.post)).post;
expect(epsilonPost).toBeUndefined(); expect(epsilonPost.isNone()).toBe(true);
}); });
test('Create a post in a non-existent community', async () => { test('Create a post in a non-existent community', async () => {
let postRes = await createPost(alpha, -2); let postRes = await createPost(alpha, -2) as any;
expect(postRes).toStrictEqual({ error: 'couldnt_find_community' }); expect(postRes.error).toBe('couldnt_find_community');
}); });
test('Unlike a post', async () => { test('Unlike a post', async () => {
@ -103,7 +104,7 @@ test('Unlike a post', async () => {
expect(unlike2.post_view.counts.score).toBe(0); expect(unlike2.post_view.counts.score).toBe(0);
// Make sure that post is unliked on beta // Make sure that post is unliked on beta
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
expect(betaPost).toBeDefined(); expect(betaPost).toBeDefined();
expect(betaPost.community.local).toBe(true); expect(betaPost.community.local).toBe(true);
expect(betaPost.creator.local).toBe(false); expect(betaPost.creator.local).toBe(false);
@ -121,26 +122,26 @@ test('Update a post', async () => {
expect(updatedPost.post_view.creator.local).toBe(true); expect(updatedPost.post_view.creator.local).toBe(true);
// Make sure that post is updated on beta // Make sure that post is updated on beta
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
expect(betaPost.community.local).toBe(true); expect(betaPost.community.local).toBe(true);
expect(betaPost.creator.local).toBe(false); expect(betaPost.creator.local).toBe(false);
expect(betaPost.post.name).toBe(updatedName); expect(betaPost.post.name).toBe(updatedName);
assertPostFederation(betaPost, updatedPost.post_view); assertPostFederation(betaPost, updatedPost.post_view);
// Make sure lemmy beta cannot update the post // Make sure lemmy beta cannot update the post
let updatedPostBeta = await editPost(beta, betaPost.post); let updatedPostBeta = await editPost(beta, betaPost.post) as any;
expect(updatedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' }); expect(updatedPostBeta.error).toBe('no_post_edit_allowed');
}); });
test('Sticky a post', async () => { test('Sticky a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let postRes = await createPost(alpha, betaCommunity.community.id);
let betaPost1 = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost1 = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
let stickiedPostRes = await stickyPost(beta, true, betaPost1.post); let stickiedPostRes = await stickyPost(beta, true, betaPost1.post);
expect(stickiedPostRes.post_view.post.stickied).toBe(true); expect(stickiedPostRes.post_view.post.stickied).toBe(true);
// Make sure that post is stickied on beta // Make sure that post is stickied on beta
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
expect(betaPost.community.local).toBe(true); expect(betaPost.community.local).toBe(true);
expect(betaPost.creator.local).toBe(false); expect(betaPost.creator.local).toBe(false);
expect(betaPost.post.stickied).toBe(true); expect(betaPost.post.stickied).toBe(true);
@ -150,15 +151,15 @@ test('Sticky a post', async () => {
expect(unstickiedPost.post_view.post.stickied).toBe(false); expect(unstickiedPost.post_view.post.stickied).toBe(false);
// Make sure that post is unstickied on beta // Make sure that post is unstickied on beta
let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
expect(betaPost2.community.local).toBe(true); expect(betaPost2.community.local).toBe(true);
expect(betaPost2.creator.local).toBe(false); expect(betaPost2.creator.local).toBe(false);
expect(betaPost2.post.stickied).toBe(false); expect(betaPost2.post.stickied).toBe(false);
// Make sure that gamma cannot sticky the post on beta // Make sure that gamma cannot sticky the post on beta
let gammaPost = (await resolvePost(gamma, postRes.post_view.post)).post; let gammaPost = (await resolvePost(gamma, postRes.post_view.post)).post.unwrap();
let gammaTrySticky = await stickyPost(gamma, true, gammaPost.post); let gammaTrySticky = await stickyPost(gamma, true, gammaPost.post);
let betaPost3 = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost3 = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
expect(gammaTrySticky.post_view.post.stickied).toBe(true); expect(gammaTrySticky.post_view.post.stickied).toBe(true);
expect(betaPost3.post.stickied).toBe(false); expect(betaPost3.post.stickied).toBe(false);
}); });
@ -168,7 +169,7 @@ test('Lock a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let postRes = await createPost(alpha, betaCommunity.community.id);
// Lock the post // Lock the post
let betaPost1 = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost1 = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
let lockedPostRes = await lockPost(beta, true, betaPost1.post); let lockedPostRes = await lockPost(beta, true, betaPost1.post);
expect(lockedPostRes.post_view.post.locked).toBe(true); expect(lockedPostRes.post_view.post.locked).toBe(true);
@ -178,7 +179,7 @@ test('Lock a post', async () => {
expect(alphaPost1.post.locked).toBe(true); expect(alphaPost1.post.locked).toBe(true);
// Try to make a new comment there, on alpha // Try to make a new comment there, on alpha
let comment: any = await createComment(alpha, alphaPost1.post.id); let comment: any = await createComment(alpha, alphaPost1.post.id, None);
expect(comment['error']).toBe('locked'); expect(comment['error']).toBe('locked');
// Unlock a post // Unlock a post
@ -193,7 +194,7 @@ test('Lock a post', async () => {
expect(alphaPost2.post.locked).toBe(false); expect(alphaPost2.post.locked).toBe(false);
// Try to create a new comment, on alpha // Try to create a new comment, on alpha
let commentAlpha = await createComment(alpha, alphaPost1.post.id); let commentAlpha = await createComment(alpha, alphaPost1.post.id, None);
expect(commentAlpha).toBeDefined(); expect(commentAlpha).toBeDefined();
}); });
@ -208,32 +209,32 @@ test('Delete a post', async () => {
// Make sure lemmy beta sees post is deleted // Make sure lemmy beta sees post is deleted
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost = (await resolvePost(beta, postRes.post_view.post)).post;
// This will be undefined because of the tombstone // This will be undefined because of the tombstone
expect(betaPost).toBeUndefined(); expect(betaPost.isNone()).toBe(true);
// Undelete // Undelete
let undeletedPost = await deletePost(alpha, false, postRes.post_view.post); let undeletedPost = await deletePost(alpha, false, postRes.post_view.post);
expect(undeletedPost.post_view.post.deleted).toBe(false); expect(undeletedPost.post_view.post.deleted).toBe(false);
// Make sure lemmy beta sees post is undeleted // Make sure lemmy beta sees post is undeleted
let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
expect(betaPost2.post.deleted).toBe(false); expect(betaPost2.post.deleted).toBe(false);
assertPostFederation(betaPost2, undeletedPost.post_view); assertPostFederation(betaPost2, undeletedPost.post_view);
// Make sure lemmy beta cannot delete the post // Make sure lemmy beta cannot delete the post
let deletedPostBeta = await deletePost(beta, true, betaPost2.post); let deletedPostBeta = await deletePost(beta, true, betaPost2.post) as any;
expect(deletedPostBeta).toStrictEqual({ error: 'no_post_edit_allowed' }); expect(deletedPostBeta.error).toStrictEqual('no_post_edit_allowed');
}); });
test('Remove a post from admin and community on different instance', async () => { test('Remove a post from admin and community on different instance', async () => {
let postRes = await createPost(gamma, betaCommunity.community.id); let postRes = await createPost(gamma, betaCommunity.community.id);
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post; let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post.unwrap();
let removedPost = await removePost(alpha, true, alphaPost.post); let removedPost = await removePost(alpha, true, alphaPost.post);
expect(removedPost.post_view.post.removed).toBe(true); expect(removedPost.post_view.post.removed).toBe(true);
expect(removedPost.post_view.post.name).toBe(postRes.post_view.post.name); expect(removedPost.post_view.post.name).toBe(postRes.post_view.post.name);
// Make sure lemmy beta sees post is NOT removed // Make sure lemmy beta sees post is NOT removed
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
expect(betaPost.post.removed).toBe(false); expect(betaPost.post.removed).toBe(false);
// Undelete // Undelete
@ -241,7 +242,7 @@ test('Remove a post from admin and community on different instance', async () =>
expect(undeletedPost.post_view.post.removed).toBe(false); expect(undeletedPost.post_view.post.removed).toBe(false);
// Make sure lemmy beta sees post is undeleted // Make sure lemmy beta sees post is undeleted
let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost2 = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
expect(betaPost2.post.removed).toBe(false); expect(betaPost2.post.removed).toBe(false);
assertPostFederation(betaPost2, undeletedPost.post_view); assertPostFederation(betaPost2, undeletedPost.post_view);
}); });
@ -261,7 +262,7 @@ test('Remove a post from admin and community on same instance', async () => {
expect(removePostRes.post_view.post.removed).toBe(true); expect(removePostRes.post_view.post.removed).toBe(true);
// Make sure lemmy alpha sees post is removed // Make sure lemmy alpha sees post is removed
let alphaPost = await getPost(alpha, postRes.post_view.post.id); // let alphaPost = await getPost(alpha, postRes.post_view.post.id);
// expect(alphaPost.post_view.post.removed).toBe(true); // TODO this shouldn't be commented // expect(alphaPost.post_view.post.removed).toBe(true); // TODO this shouldn't be commented
// assertPostFederation(alphaPost.post_view, removePostRes.post_view); // assertPostFederation(alphaPost.post_view, removePostRes.post_view);
@ -281,8 +282,7 @@ test('Search for a post', async () => {
let postRes = await createPost(alpha, betaCommunity.community.id); let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined(); expect(postRes.post_view.post).toBeDefined();
let betaPost = (await resolvePost(beta, postRes.post_view.post)).post; let betaPost = (await resolvePost(beta, postRes.post_view.post)).post.unwrap();
expect(betaPost.post.name).toBeDefined(); expect(betaPost.post.name).toBeDefined();
}); });
@ -294,9 +294,9 @@ test('Enforce site ban for federated user', async () => {
client: alpha.client, client: alpha.client,
auth: alphaUserJwt.jwt, auth: alphaUserJwt.jwt,
}; };
let alphaUserActorId = (await getSite(alpha_user)).my_user.local_user_view.person.actor_id; let alphaUserActorId = (await getSite(alpha_user)).my_user.unwrap().local_user_view.person.actor_id;
expect(alphaUserActorId).toBeDefined(); expect(alphaUserActorId).toBeDefined();
let alphaPerson = (await resolvePerson(alpha_user, alphaUserActorId)).person; let alphaPerson = (await resolvePerson(alpha_user, alphaUserActorId)).person.unwrap();
expect(alphaPerson).toBeDefined(); expect(alphaPerson).toBeDefined();
// alpha makes post in beta community, it federates to beta instance // alpha makes post in beta community, it federates to beta instance
@ -310,7 +310,7 @@ test('Enforce site ban for federated user', async () => {
// alpha ban should be federated to beta // alpha ban should be federated to beta
let alphaUserOnBeta1 = await resolvePerson(beta, alphaUserActorId); let alphaUserOnBeta1 = await resolvePerson(beta, alphaUserActorId);
expect(alphaUserOnBeta1.person.person.banned).toBe(true); expect(alphaUserOnBeta1.person.unwrap().person.banned).toBe(true);
// existing alpha post should be removed on beta // existing alpha post should be removed on beta
let searchBeta2 = await searchPostLocal(beta, postRes1.post_view.post); let searchBeta2 = await searchPostLocal(beta, postRes1.post_view.post);
@ -326,12 +326,12 @@ test('Enforce site ban for federated user', async () => {
expect(searchBeta3.posts[0]).toBeDefined(); expect(searchBeta3.posts[0]).toBeDefined();
let alphaUserOnBeta2 = await resolvePerson(beta, alphaUserActorId) let alphaUserOnBeta2 = await resolvePerson(beta, alphaUserActorId)
expect(alphaUserOnBeta2.person.person.banned).toBe(false); expect(alphaUserOnBeta2.person.unwrap().person.banned).toBe(false);
}); });
test('Enforce community ban for federated user', async () => { test('Enforce community ban for federated user', async () => {
let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`; let alphaShortname = `@lemmy_alpha@lemmy-alpha:8541`;
let alphaPerson = (await resolvePerson(beta, alphaShortname)).person; let alphaPerson = (await resolvePerson(beta, alphaShortname)).person.unwrap();
expect(alphaPerson).toBeDefined(); expect(alphaPerson).toBeDefined();
// make a post in beta, it goes through // make a post in beta, it goes through
@ -376,16 +376,16 @@ test('A and G subscribe to B (center) A posts, it gets announced to G', async ()
let postRes = await createPost(alpha, betaCommunity.community.id); let postRes = await createPost(alpha, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined(); expect(postRes.post_view.post).toBeDefined();
let betaPost = (await resolvePost(gamma, postRes.post_view.post)).post; let betaPost = (await resolvePost(gamma, postRes.post_view.post)).post.unwrap();
expect(betaPost.post.name).toBeDefined(); expect(betaPost.post.name).toBeDefined();
}); });
test('Report a post', async () => { test('Report a post', async () => {
let betaCommunity = (await resolveBetaCommunity(beta)).community; let betaCommunity = (await resolveBetaCommunity(beta)).community.unwrap();
let postRes = await createPost(beta, betaCommunity.community.id); let postRes = await createPost(beta, betaCommunity.community.id);
expect(postRes.post_view.post).toBeDefined(); expect(postRes.post_view.post).toBeDefined();
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post; let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post.unwrap();
let alphaReport = (await reportPost(alpha, alphaPost.post.id, randomString(10))) let alphaReport = (await reportPost(alpha, alphaPost.post.id, randomString(10)))
.post_report_view.post_report; .post_report_view.post_report;
@ -393,7 +393,7 @@ test('Report a post', async () => {
expect(betaReport).toBeDefined(); expect(betaReport).toBeDefined();
expect(betaReport.resolved).toBe(false); expect(betaReport.resolved).toBe(false);
expect(betaReport.original_post_name).toBe(alphaReport.original_post_name); expect(betaReport.original_post_name).toBe(alphaReport.original_post_name);
expect(betaReport.original_post_url).toBe(alphaReport.original_post_url); expect(betaReport.original_post_url.unwrapOr("none")).toBe(alphaReport.original_post_url.unwrapOr("none"));
expect(betaReport.original_post_body).toBe(alphaReport.original_post_body); expect(betaReport.original_post_body.unwrapOr("none")).toBe(alphaReport.original_post_body.unwrapOr("none"));
expect(betaReport.reason).toBe(alphaReport.reason); expect(betaReport.reason).toBe(alphaReport.reason);
}); });

View file

@ -1,3 +1,4 @@
import {None, Some, Option} from '@sniptt/monads';
import { import {
Login, Login,
LoginResponse, LoginResponse,
@ -58,65 +59,74 @@ import {
ListCommentReports, ListCommentReports,
ListCommentReportsResponse, ListCommentReportsResponse,
DeleteAccount, DeleteAccount,
DeleteAccountResponse DeleteAccountResponse,
EditSite,
CommentSortType,
GetComments,
GetCommentsResponse
} from 'lemmy-js-client'; } from 'lemmy-js-client';
export interface API { export interface API {
client: LemmyHttp; client: LemmyHttp;
auth?: string; auth: Option<string>;
} }
export let alpha: API = { export let alpha: API = {
client: new LemmyHttp('http://127.0.0.1:8541'), client: new LemmyHttp('http://127.0.0.1:8541'),
auth: None,
}; };
export let beta: API = { export let beta: API = {
client: new LemmyHttp('http://127.0.0.1:8551'), client: new LemmyHttp('http://127.0.0.1:8551'),
auth: None,
}; };
export let gamma: API = { export let gamma: API = {
client: new LemmyHttp('http://127.0.0.1:8561'), client: new LemmyHttp('http://127.0.0.1:8561'),
auth: None,
}; };
export let delta: API = { export let delta: API = {
client: new LemmyHttp('http://127.0.0.1:8571'), client: new LemmyHttp('http://127.0.0.1:8571'),
auth: None,
}; };
export let epsilon: API = { export let epsilon: API = {
client: new LemmyHttp('http://127.0.0.1:8581'), client: new LemmyHttp('http://127.0.0.1:8581'),
auth: None,
}; };
const password = 'lemmylemmy' const password = 'lemmylemmy'
export async function setupLogins() { export async function setupLogins() {
let formAlpha: Login = { let formAlpha = new Login({
username_or_email: 'lemmy_alpha', username_or_email: 'lemmy_alpha',
password, password,
}; });
let resAlpha = alpha.client.login(formAlpha); let resAlpha = alpha.client.login(formAlpha);
let formBeta = { let formBeta = new Login({
username_or_email: 'lemmy_beta', username_or_email: 'lemmy_beta',
password, password,
}; });
let resBeta = beta.client.login(formBeta); let resBeta = beta.client.login(formBeta);
let formGamma = { let formGamma = new Login({
username_or_email: 'lemmy_gamma', username_or_email: 'lemmy_gamma',
password, password,
}; });
let resGamma = gamma.client.login(formGamma); let resGamma = gamma.client.login(formGamma);
let formDelta = { let formDelta = new Login({
username_or_email: 'lemmy_delta', username_or_email: 'lemmy_delta',
password, password,
}; });
let resDelta = delta.client.login(formDelta); let resDelta = delta.client.login(formDelta);
let formEpsilon = { let formEpsilon = new Login({
username_or_email: 'lemmy_epsilon', username_or_email: 'lemmy_epsilon',
password, password,
}; });
let resEpsilon = epsilon.client.login(formEpsilon); let resEpsilon = epsilon.client.login(formEpsilon);
let res = await Promise.all([ let res = await Promise.all([
@ -133,12 +143,36 @@ export async function setupLogins() {
delta.auth = res[3].jwt; delta.auth = res[3].jwt;
epsilon.auth = res[4].jwt; epsilon.auth = res[4].jwt;
// regstration applications are now enabled by default, need to disable them // Registration applications are now enabled by default, need to disable them
await alpha.client.editSite({ require_application: false, auth: alpha.auth}); let editSiteForm = new EditSite({
await beta.client.editSite({ require_application: false, auth: beta.auth}); name: None,
await gamma.client.editSite({ require_application: false, auth: gamma.auth}); sidebar: None,
await delta.client.editSite({ require_application: false, auth: delta.auth}); description: None,
await epsilon.client.editSite({ require_application: false, auth: epsilon.auth}); icon: None,
banner: None,
enable_downvotes: None,
open_registration: None,
enable_nsfw: None,
community_creation_admin_only: None,
require_email_verification: None,
require_application: Some(false),
application_question: None,
private_instance: None,
default_theme: None,
legal_information: None,
default_post_listing_type: None,
auth: "",
});
editSiteForm.auth = alpha.auth.unwrap();
await alpha.client.editSite(editSiteForm);
editSiteForm.auth = beta.auth.unwrap();
await beta.client.editSite(editSiteForm);
editSiteForm.auth = gamma.auth.unwrap();
await gamma.client.editSite(editSiteForm);
editSiteForm.auth = delta.auth.unwrap();
await delta.client.editSite(editSiteForm);
editSiteForm.auth = epsilon.auth.unwrap();
await epsilon.client.editSite(editSiteForm);
// Create the main beta community, follow it // Create the main beta community, follow it
await createCommunity(beta, "main"); await createCommunity(beta, "main");
@ -150,27 +184,30 @@ export async function createPost(
community_id: number community_id: number
): Promise<PostResponse> { ): Promise<PostResponse> {
let name = randomString(5); let name = randomString(5);
let body = randomString(10); let body = Some(randomString(10));
let url = 'https://google.com/'; let url = Some('https://google.com/');
let form: CreatePost = { let form = new CreatePost({
name, name,
url, url,
body, body,
auth: api.auth, auth: api.auth.unwrap(),
community_id, community_id,
nsfw: false, nsfw: None,
}; honeypot: None,
});
return api.client.createPost(form); return api.client.createPost(form);
} }
export async function editPost(api: API, post: Post): Promise<PostResponse> { export async function editPost(api: API, post: Post): Promise<PostResponse> {
let name = 'A jest test federated post, updated'; let name = Some('A jest test federated post, updated');
let form: EditPost = { let form = new EditPost({
name, name,
post_id: post.id, post_id: post.id,
auth: api.auth, auth: api.auth.unwrap(),
nsfw: false, nsfw: None,
}; url: None,
body: None,
});
return api.client.editPost(form); return api.client.editPost(form);
} }
@ -179,11 +216,11 @@ export async function deletePost(
deleted: boolean, deleted: boolean,
post: Post post: Post
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: DeletePost = { let form = new DeletePost({
post_id: post.id, post_id: post.id,
deleted: deleted, deleted: deleted,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.deletePost(form); return api.client.deletePost(form);
} }
@ -192,11 +229,12 @@ export async function removePost(
removed: boolean, removed: boolean,
post: Post post: Post
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: RemovePost = { let form = new RemovePost({
post_id: post.id, post_id: post.id,
removed, removed,
auth: api.auth, auth: api.auth.unwrap(),
}; reason: None,
});
return api.client.removePost(form); return api.client.removePost(form);
} }
@ -205,11 +243,11 @@ export async function stickyPost(
stickied: boolean, stickied: boolean,
post: Post post: Post
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: StickyPost = { let form = new StickyPost({
post_id: post.id, post_id: post.id,
stickied, stickied,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.stickyPost(form); return api.client.stickyPost(form);
} }
@ -218,11 +256,11 @@ export async function lockPost(
locked: boolean, locked: boolean,
post: Post post: Post
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: LockPost = { let form = new LockPost({
post_id: post.id, post_id: post.id,
locked, locked,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.lockPost(form); return api.client.lockPost(form);
} }
@ -230,9 +268,10 @@ export async function resolvePost(
api: API, api: API,
post: Post post: Post
): Promise<ResolveObjectResponse> { ): Promise<ResolveObjectResponse> {
let form: ResolveObject = { let form = new ResolveObject({
q: post.ap_id, q: post.ap_id,
}; auth: api.auth,
});
return api.client.resolveObject(form); return api.client.resolveObject(form);
} }
@ -240,11 +279,18 @@ export async function searchPostLocal(
api: API, api: API,
post: Post post: Post
): Promise<SearchResponse> { ): Promise<SearchResponse> {
let form: Search = { let form = new Search({
q: post.name, q: post.name,
type_: SearchType.Posts, type_: Some(SearchType.Posts),
sort: SortType.TopAll, sort: Some(SortType.TopAll),
}; community_id: None,
community_name: None,
creator_id: None,
listing_type: None,
page: None,
limit: None,
auth: api.auth,
});
return api.client.search(form); return api.client.search(form);
} }
@ -252,19 +298,42 @@ export async function getPost(
api: API, api: API,
post_id: number post_id: number
): Promise<GetPostResponse> { ): Promise<GetPostResponse> {
let form: GetPost = { let form = new GetPost({
id: post_id, id: Some(post_id),
}; comment_id: None,
auth: api.auth,
});
return api.client.getPost(form); return api.client.getPost(form);
} }
export async function getComments(
api: API,
post_id: number
): Promise<GetCommentsResponse> {
let form = new GetComments({
post_id: Some(post_id),
type_: Some(ListingType.All),
sort: Some(CommentSortType.New), // TODO this sort might be wrong
max_depth: None,
page: None,
limit: None,
community_id: None,
community_name: None,
saved_only: None,
parent_id: None,
auth: api.auth,
});
return api.client.getComments(form);
}
export async function resolveComment( export async function resolveComment(
api: API, api: API,
comment: Comment comment: Comment
): Promise<ResolveObjectResponse> { ): Promise<ResolveObjectResponse> {
let form: ResolveObject = { let form = new ResolveObject({
q: comment.ap_id, q: comment.ap_id,
}; auth: api.auth,
});
return api.client.resolveObject(form); return api.client.resolveObject(form);
} }
@ -272,9 +341,10 @@ export async function resolveBetaCommunity(
api: API api: API
): Promise<ResolveObjectResponse> { ): Promise<ResolveObjectResponse> {
// Use short-hand search url // Use short-hand search url
let form: ResolveObject = { let form = new ResolveObject({
q: '!main@lemmy-beta:8551', q: '!main@lemmy-beta:8551',
}; auth: api.auth,
});
return api.client.resolveObject(form); return api.client.resolveObject(form);
} }
@ -282,9 +352,10 @@ export async function resolveCommunity(
api: API, api: API,
q: string q: string
): Promise<ResolveObjectResponse> { ): Promise<ResolveObjectResponse> {
let form: ResolveObject = { let form = new ResolveObject({
q, q,
}; auth: api.auth,
});
return api.client.resolveObject(form); return api.client.resolveObject(form);
} }
@ -292,9 +363,10 @@ export async function resolvePerson(
api: API, api: API,
apShortname: string apShortname: string
): Promise<ResolveObjectResponse> { ): Promise<ResolveObjectResponse> {
let form: ResolveObject = { let form = new ResolveObject({
q: apShortname, q: apShortname,
}; auth: api.auth,
});
return api.client.resolveObject(form); return api.client.resolveObject(form);
} }
@ -305,12 +377,14 @@ export async function banPersonFromSite(
remove_data: boolean remove_data: boolean
): Promise<BanPersonResponse> { ): Promise<BanPersonResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha // Make sure lemmy-beta/c/main is cached on lemmy_alpha
let form: BanPerson = { let form = new BanPerson({
person_id, person_id,
ban, ban,
remove_data, remove_data: Some(remove_data),
auth: api.auth, auth: api.auth.unwrap(),
}; reason: None,
expires: None,
});
return api.client.banPerson(form); return api.client.banPerson(form);
} }
@ -321,13 +395,15 @@ export async function banPersonFromCommunity(
remove_data: boolean, remove_data: boolean,
ban: boolean ban: boolean
): Promise<BanFromCommunityResponse> { ): Promise<BanFromCommunityResponse> {
let form: BanFromCommunity = { let form = new BanFromCommunity({
person_id, person_id,
community_id, community_id,
remove_data, remove_data: Some(remove_data),
ban, ban,
auth: api.auth, reason: None,
}; expires: None,
auth: api.auth.unwrap(),
});
return api.client.banFromCommunity(form); return api.client.banFromCommunity(form);
} }
@ -336,11 +412,11 @@ export async function followCommunity(
follow: boolean, follow: boolean,
community_id: number community_id: number
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let form: FollowCommunity = { let form = new FollowCommunity({
community_id, community_id,
follow, follow,
auth: api.auth, auth: api.auth.unwrap()
}; });
return api.client.followCommunity(form); return api.client.followCommunity(form);
} }
@ -349,11 +425,11 @@ export async function likePost(
score: number, score: number,
post: Post post: Post
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: CreatePostLike = { let form = new CreatePostLike({
post_id: post.id, post_id: post.id,
score: score, score: score,
auth: api.auth, auth: api.auth.unwrap()
}; });
return api.client.likePost(form); return api.client.likePost(form);
} }
@ -361,15 +437,16 @@ export async function likePost(
export async function createComment( export async function createComment(
api: API, api: API,
post_id: number, post_id: number,
parent_id?: number, parent_id: Option<number>,
content = 'a jest test comment' content = 'a jest test comment'
): Promise<CommentResponse> { ): Promise<CommentResponse> {
let form: CreateComment = { let form = new CreateComment({
content, content,
post_id, post_id,
parent_id, parent_id,
auth: api.auth, form_id: None,
}; auth: api.auth.unwrap(),
});
return api.client.createComment(form); return api.client.createComment(form);
} }
@ -378,11 +455,12 @@ export async function editComment(
comment_id: number, comment_id: number,
content = 'A jest test federated comment update' content = 'A jest test federated comment update'
): Promise<CommentResponse> { ): Promise<CommentResponse> {
let form: EditComment = { let form = new EditComment({
content, content,
comment_id, comment_id,
auth: api.auth, form_id: None,
}; auth: api.auth.unwrap()
});
return api.client.editComment(form); return api.client.editComment(form);
} }
@ -391,11 +469,11 @@ export async function deleteComment(
deleted: boolean, deleted: boolean,
comment_id: number comment_id: number
): Promise<CommentResponse> { ): Promise<CommentResponse> {
let form: DeleteComment = { let form = new DeleteComment({
comment_id, comment_id,
deleted, deleted,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.deleteComment(form); return api.client.deleteComment(form);
} }
@ -404,20 +482,23 @@ export async function removeComment(
removed: boolean, removed: boolean,
comment_id: number comment_id: number
): Promise<CommentResponse> { ): Promise<CommentResponse> {
let form: RemoveComment = { let form = new RemoveComment({
comment_id, comment_id,
removed, removed,
auth: api.auth, reason: None,
}; auth: api.auth.unwrap(),
});
return api.client.removeComment(form); return api.client.removeComment(form);
} }
export async function getMentions(api: API): Promise<GetPersonMentionsResponse> { export async function getMentions(api: API): Promise<GetPersonMentionsResponse> {
let form: GetPersonMentions = { let form = new GetPersonMentions({
sort: SortType.New, sort: Some(CommentSortType.New),
unread_only: false, unread_only: Some(false),
auth: api.auth, auth: api.auth.unwrap(),
}; page: None,
limit: None,
});
return api.client.getPersonMentions(form); return api.client.getPersonMentions(form);
} }
@ -426,11 +507,11 @@ export async function likeComment(
score: number, score: number,
comment: Comment comment: Comment
): Promise<CommentResponse> { ): Promise<CommentResponse> {
let form: CreateCommentLike = { let form = new CreateCommentLike({
comment_id: comment.id, comment_id: comment.id,
score, score,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.likeComment(form); return api.client.likeComment(form);
} }
@ -438,14 +519,17 @@ export async function createCommunity(
api: API, api: API,
name_: string = randomString(5) name_: string = randomString(5)
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let description = 'a sample description'; let description = Some('a sample description');
let form: CreateCommunity = { let form = new CreateCommunity({
name: name_, name: name_,
title: name_, title: name_,
description, description,
nsfw: false, nsfw: None,
auth: api.auth, icon: None,
}; banner: None,
posting_restricted_to_mods: None,
auth: api.auth.unwrap(),
});
return api.client.createCommunity(form); return api.client.createCommunity(form);
} }
@ -453,9 +537,11 @@ export async function getCommunity(
api: API, api: API,
id: number id: number
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let form: GetCommunity = { let form = new GetCommunity({
id, id: Some(id),
}; name: None,
auth: api.auth,
});
return api.client.getCommunity(form); return api.client.getCommunity(form);
} }
@ -464,11 +550,11 @@ export async function deleteCommunity(
deleted: boolean, deleted: boolean,
community_id: number community_id: number
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let form: DeleteCommunity = { let form = new DeleteCommunity({
community_id, community_id,
deleted, deleted,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.deleteCommunity(form); return api.client.deleteCommunity(form);
} }
@ -477,11 +563,13 @@ export async function removeCommunity(
removed: boolean, removed: boolean,
community_id: number community_id: number
): Promise<CommunityResponse> { ): Promise<CommunityResponse> {
let form: RemoveCommunity = { let form = new RemoveCommunity({
community_id, community_id,
removed, removed,
auth: api.auth, reason: None,
}; expires: None,
auth: api.auth.unwrap(),
});
return api.client.removeCommunity(form); return api.client.removeCommunity(form);
} }
@ -490,11 +578,11 @@ export async function createPrivateMessage(
recipient_id: number recipient_id: number
): Promise<PrivateMessageResponse> { ): Promise<PrivateMessageResponse> {
let content = 'A jest test federated private message'; let content = 'A jest test federated private message';
let form: CreatePrivateMessage = { let form = new CreatePrivateMessage({
content, content,
recipient_id, recipient_id,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.createPrivateMessage(form); return api.client.createPrivateMessage(form);
} }
@ -503,11 +591,11 @@ export async function editPrivateMessage(
private_message_id: number private_message_id: number
): Promise<PrivateMessageResponse> { ): Promise<PrivateMessageResponse> {
let updatedContent = 'A jest test federated private message edited'; let updatedContent = 'A jest test federated private message edited';
let form: EditPrivateMessage = { let form = new EditPrivateMessage({
content: updatedContent, content: updatedContent,
private_message_id, private_message_id,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.editPrivateMessage(form); return api.client.editPrivateMessage(form);
} }
@ -516,11 +604,11 @@ export async function deletePrivateMessage(
deleted: boolean, deleted: boolean,
private_message_id: number private_message_id: number
): Promise<PrivateMessageResponse> { ): Promise<PrivateMessageResponse> {
let form: DeletePrivateMessage = { let form = new DeletePrivateMessage({
deleted, deleted,
private_message_id, private_message_id,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.deletePrivateMessage(form); return api.client.deletePrivateMessage(form);
} }
@ -528,32 +616,77 @@ export async function registerUser(
api: API, api: API,
username: string = randomString(5) username: string = randomString(5)
): Promise<LoginResponse> { ): Promise<LoginResponse> {
let form: Register = { let form = new Register({
username, username,
password, password,
password_verify: password, password_verify: password,
show_nsfw: true, show_nsfw: true,
}; email: None,
captcha_uuid: None,
captcha_answer: None,
honeypot: None,
answer: None,
});
return api.client.register(form); return api.client.register(form);
} }
export async function saveUserSettingsBio( export async function saveUserSettingsBio(
api: API api: API
): Promise<LoginResponse> { ): Promise<LoginResponse> {
let form: SaveUserSettings = { let form = new SaveUserSettings({
show_nsfw: true, show_nsfw: Some(true),
theme: 'darkly', theme: Some('darkly'),
default_sort_type: Object.keys(SortType).indexOf(SortType.Active), default_sort_type: Some(Object.keys(SortType).indexOf(SortType.Active)),
default_listing_type: Object.keys(ListingType).indexOf(ListingType.All), default_listing_type: Some(Object.keys(ListingType).indexOf(ListingType.All)),
lang: 'en', lang: Some('en'),
show_avatars: true, show_avatars: Some(true),
send_notifications_to_email: false, send_notifications_to_email: Some(false),
bio: 'a changed bio', bio: Some('a changed bio'),
auth: api.auth, avatar: None,
}; banner: None,
display_name: None,
email: None,
matrix_user_id: None,
show_scores: None,
show_read_posts: None,
show_bot_accounts: None,
show_new_post_notifs: None,
bot_account: None,
auth: api.auth.unwrap(),
});
return saveUserSettings(api, form); return saveUserSettings(api, form);
} }
export async function saveUserSettingsFederated(
api: API
): Promise<LoginResponse> {
let avatar = Some('https://image.flaticon.com/icons/png/512/35/35896.png');
let banner = Some('https://image.flaticon.com/icons/png/512/36/35896.png');
let bio = Some('a changed bio');
let form = new SaveUserSettings({
show_nsfw: Some(false),
theme: Some(''),
default_sort_type: Some(Object.keys(SortType).indexOf(SortType.Hot)),
default_listing_type: Some(Object.keys(ListingType).indexOf(ListingType.All)),
lang: Some(''),
avatar,
banner,
display_name: Some('user321'),
show_avatars: Some(false),
send_notifications_to_email: Some(false),
bio,
email: None,
show_scores: None,
show_read_posts: None,
matrix_user_id: None,
bot_account: None,
show_bot_accounts: None,
show_new_post_notifs: None,
auth: api.auth.unwrap(),
});
return await saveUserSettings(alpha, form);
}
export async function saveUserSettings( export async function saveUserSettings(
api: API, api: API,
form: SaveUserSettings form: SaveUserSettings
@ -564,29 +697,31 @@ export async function saveUserSettings(
export async function deleteUser( export async function deleteUser(
api: API api: API
): Promise<DeleteAccountResponse> { ): Promise<DeleteAccountResponse> {
let form: DeleteAccount = { let form = new DeleteAccount({
auth: api.auth, auth: api.auth.unwrap(),
password password
}; });
return api.client.deleteAccount(form); return api.client.deleteAccount(form);
} }
export async function getSite( export async function getSite(
api: API api: API
): Promise<GetSiteResponse> { ): Promise<GetSiteResponse> {
let form: GetSite = { let form = new GetSite({
auth: api.auth, auth: api.auth,
}; });
return api.client.getSite(form); return api.client.getSite(form);
} }
export async function listPrivateMessages( export async function listPrivateMessages(
api: API api: API
): Promise<PrivateMessagesResponse> { ): Promise<PrivateMessagesResponse> {
let form: GetPrivateMessages = { let form = new GetPrivateMessages({
auth: api.auth, auth: api.auth.unwrap(),
unread_only: false, unread_only: Some(false),
}; page: None,
limit: None,
});
return api.client.getPrivateMessages(form); return api.client.getPrivateMessages(form);
} }
@ -595,7 +730,7 @@ export async function unfollowRemotes(
): Promise<GetSiteResponse> { ): Promise<GetSiteResponse> {
// Unfollow all remote communities // Unfollow all remote communities
let site = await getSite(api); let site = await getSite(api);
let remoteFollowed = site.my_user.follows.filter( let remoteFollowed = site.my_user.unwrap().follows.filter(
c => c.community.local == false c => c.community.local == false
); );
for (let cu of remoteFollowed) { for (let cu of remoteFollowed) {
@ -607,9 +742,11 @@ export async function unfollowRemotes(
export async function followBeta(api: API): Promise<CommunityResponse> { export async function followBeta(api: API): Promise<CommunityResponse> {
let betaCommunity = (await resolveBetaCommunity(api)).community; let betaCommunity = (await resolveBetaCommunity(api)).community;
if (betaCommunity) { if (betaCommunity.isSome()) {
let follow = await followCommunity(api, true, betaCommunity.community.id); let follow = await followCommunity(api, true, betaCommunity.unwrap().community.id);
return follow; return follow;
} else {
return Promise.reject("no community worked");
} }
} }
@ -618,18 +755,22 @@ export async function reportPost(
post_id: number, post_id: number,
reason: string reason: string
): Promise<PostReportResponse> { ): Promise<PostReportResponse> {
let form: CreatePostReport = { let form = new CreatePostReport({
post_id, post_id,
reason, reason,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.createPostReport(form); return api.client.createPostReport(form);
} }
export async function listPostReports(api: API): Promise<ListPostReportsResponse> { export async function listPostReports(api: API): Promise<ListPostReportsResponse> {
let form: ListPostReports = { let form = new ListPostReports({
auth: api.auth, auth: api.auth.unwrap(),
}; page: None,
limit: None,
community_id: None,
unresolved_only: None,
});
return api.client.listPostReports(form); return api.client.listPostReports(form);
} }
@ -638,18 +779,22 @@ export async function reportComment(
comment_id: number, comment_id: number,
reason: string reason: string
): Promise<CommentReportResponse> { ): Promise<CommentReportResponse> {
let form: CreateCommentReport = { let form = new CreateCommentReport({
comment_id, comment_id,
reason, reason,
auth: api.auth, auth: api.auth.unwrap(),
}; });
return api.client.createCommentReport(form); return api.client.createCommentReport(form);
} }
export async function listCommentReports(api: API): Promise<ListCommentReportsResponse> { export async function listCommentReports(api: API): Promise<ListCommentReportsResponse> {
let form: ListCommentReports = { let form = new ListCommentReports({
auth: api.auth, page: None,
}; limit: None,
community_id: None,
unresolved_only: None,
auth: api.auth.unwrap(),
});
return api.client.listCommentReports(form); return api.client.listCommentReports(form);
} }
@ -681,3 +826,15 @@ export async function unfollows() {
await unfollowRemotes(delta); await unfollowRemotes(delta);
await unfollowRemotes(epsilon); await unfollowRemotes(epsilon);
} }
export function getCommentParentId(comment: Comment): Option<number> {
let split = comment.path.split(".");
// remove the 0
split.shift();
if (split.length > 1) {
return Some(Number(split[split.length - 2]));
} else {
return None;
}
}

View file

@ -1,10 +1,14 @@
jest.setTimeout(120000); jest.setTimeout(120000);
import {None} from '@sniptt/monads';
import {
PersonViewSafe,
} from 'lemmy-js-client';
import { import {
alpha, alpha,
beta, beta,
registerUser, registerUser,
resolvePerson, resolvePerson,
saveUserSettings,
getSite, getSite,
createPost, createPost,
resolveCommunity, resolveCommunity,
@ -14,23 +18,18 @@ import {
resolvePost, resolvePost,
API, API,
resolveComment, resolveComment,
saveUserSettingsFederated,
} from './shared'; } from './shared';
import {
PersonViewSafe,
SaveUserSettings,
SortType,
ListingType,
} from 'lemmy-js-client';
let apShortname: string; let apShortname: string;
function assertUserFederation(userOne: PersonViewSafe, userTwo: PersonViewSafe) { function assertUserFederation(userOne: PersonViewSafe, userTwo: PersonViewSafe) {
expect(userOne.person.name).toBe(userTwo.person.name); expect(userOne.person.name).toBe(userTwo.person.name);
expect(userOne.person.display_name).toBe(userTwo.person.display_name); expect(userOne.person.display_name.unwrapOr("none")).toBe(userTwo.person.display_name.unwrapOr("none"));
expect(userOne.person.bio).toBe(userTwo.person.bio); expect(userOne.person.bio.unwrapOr("none")).toBe(userTwo.person.bio.unwrapOr("none"));
expect(userOne.person.actor_id).toBe(userTwo.person.actor_id); expect(userOne.person.actor_id).toBe(userTwo.person.actor_id);
expect(userOne.person.avatar).toBe(userTwo.person.avatar); expect(userOne.person.avatar.unwrapOr("none")).toBe(userTwo.person.avatar.unwrapOr("none"));
expect(userOne.person.banner).toBe(userTwo.person.banner); expect(userOne.person.banner.unwrapOr("none")).toBe(userTwo.person.banner.unwrapOr("none"));
expect(userOne.person.published).toBe(userTwo.person.published); expect(userOne.person.published).toBe(userTwo.person.published);
} }
@ -41,31 +40,13 @@ test('Create user', async () => {
let site = await getSite(alpha); let site = await getSite(alpha);
expect(site.my_user).toBeDefined(); expect(site.my_user).toBeDefined();
apShortname = `@${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`; apShortname = `@${site.my_user.unwrap().local_user_view.person.name}@lemmy-alpha:8541`;
}); });
test('Set some user settings, check that they are federated', async () => { test('Set some user settings, check that they are federated', async () => {
let avatar = 'https://image.flaticon.com/icons/png/512/35/35896.png'; await saveUserSettingsFederated(alpha);
let banner = 'https://image.flaticon.com/icons/png/512/36/35896.png'; let alphaPerson = (await resolvePerson(alpha, apShortname)).person.unwrap();
let bio = 'a changed bio'; let betaPerson = (await resolvePerson(beta, apShortname)).person.unwrap();
let form: SaveUserSettings = {
show_nsfw: false,
theme: '',
default_sort_type: Object.keys(SortType).indexOf(SortType.Hot),
default_listing_type: Object.keys(ListingType).indexOf(ListingType.All),
lang: '',
avatar,
banner,
display_name: 'user321',
show_avatars: false,
send_notifications_to_email: false,
bio,
auth: alpha.auth,
};
await saveUserSettings(alpha, form);
let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
let betaPerson = (await resolvePerson(beta, apShortname)).person;
assertUserFederation(alphaPerson, betaPerson); assertUserFederation(alphaPerson, betaPerson);
}); });
@ -78,23 +59,23 @@ test('Delete user', async () => {
} }
// make a local post and comment // make a local post and comment
let alphaCommunity = (await resolveCommunity(user, '!main@lemmy-alpha:8541')).community; let alphaCommunity = (await resolveCommunity(user, '!main@lemmy-alpha:8541')).community.unwrap();
let localPost = (await createPost(user, alphaCommunity.community.id)).post_view.post; let localPost = (await createPost(user, alphaCommunity.community.id)).post_view.post;
expect(localPost).toBeDefined(); expect(localPost).toBeDefined();
let localComment = (await createComment(user, localPost.id)).comment_view.comment; let localComment = (await createComment(user, localPost.id, None)).comment_view.comment;
expect(localComment).toBeDefined(); expect(localComment).toBeDefined();
// make a remote post and comment // make a remote post and comment
let betaCommunity = (await resolveBetaCommunity(user)).community; let betaCommunity = (await resolveBetaCommunity(user)).community.unwrap();
let remotePost = (await createPost(user, betaCommunity.community.id)).post_view.post; let remotePost = (await createPost(user, betaCommunity.community.id)).post_view.post;
expect(remotePost).toBeDefined(); expect(remotePost).toBeDefined();
let remoteComment = (await createComment(user, remotePost.id)).comment_view.comment; let remoteComment = (await createComment(user, remotePost.id, None)).comment_view.comment;
expect(remoteComment).toBeDefined(); expect(remoteComment).toBeDefined();
await deleteUser(user); await deleteUser(user);
expect((await resolvePost(alpha, localPost)).post).toBeUndefined(); expect((await resolvePost(alpha, localPost)).post.isNone()).toBe(true);
expect((await resolveComment(alpha, localComment)).comment).toBeUndefined(); expect((await resolveComment(alpha, localComment)).comment.isNone()).toBe(true)
expect((await resolvePost(alpha, remotePost)).post).toBeUndefined(); expect((await resolvePost(alpha, remotePost)).post.isNone()).toBe(true)
expect((await resolveComment(alpha, remoteComment)).comment).toBeUndefined(); expect((await resolveComment(alpha, remoteComment)).comment.isNone()).toBe(true)
}); });

File diff suppressed because it is too large Load diff

View file

@ -13,7 +13,10 @@ use lemmy_apub::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::LocalUserId, newtypes::LocalUserId,
source::comment::{CommentLike, CommentLikeForm}, source::{
comment::{CommentLike, CommentLikeForm},
comment_reply::CommentReply,
},
traits::Likeable, traits::Likeable,
}; };
use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_db_views::structs::{CommentView, LocalUserView};
@ -53,8 +56,13 @@ impl Perform for CreateCommentLike {
) )
.await?; .await?;
// Add parent user to recipients // Add parent poster or commenter to recipients
let recipient_id = orig_comment.get_recipient_id(); let comment_reply = blocking(context.pool(), move |conn| {
CommentReply::read_by_comment(conn, comment_id)
})
.await?;
if let Ok(reply) = comment_reply {
let recipient_id = reply.recipient_id;
if let Ok(local_recipient) = blocking(context.pool(), move |conn| { if let Ok(local_recipient) = blocking(context.pool(), move |conn| {
LocalUserView::read_person(conn, recipient_id) LocalUserView::read_person(conn, recipient_id)
}) })
@ -62,6 +70,7 @@ impl Perform for CreateCommentLike {
{ {
recipient_ids.push(local_recipient.local_user.id); recipient_ids.push(local_recipient.local_user.id);
} }
}
let like_form = CommentLikeForm { let like_form = CommentLikeForm {
comment_id: data.comment_id, comment_id: data.comment_id,

View file

@ -1,61 +0,0 @@
use crate::Perform;
use actix_web::web::Data;
use lemmy_api_common::{
comment::{CommentResponse, MarkCommentAsRead},
utils::{blocking, get_local_user_view_from_jwt},
};
use lemmy_db_schema::source::comment::Comment;
use lemmy_db_views::structs::CommentView;
use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl Perform for MarkCommentAsRead {
type Response = CommentResponse;
#[tracing::instrument(skip(context, _websocket_id))]
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommentResponse, LemmyError> {
let data: &MarkCommentAsRead = self;
let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
let comment_id = data.comment_id;
let orig_comment = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, None)
})
.await??;
// Verify that only the recipient can mark as read
if local_user_view.person.id != orig_comment.get_recipient_id() {
return Err(LemmyError::from_message("no_comment_edit_allowed"));
}
// Do the mark as read
let read = data.read;
blocking(context.pool(), move |conn| {
Comment::update_read(conn, comment_id, read)
})
.await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
// Refetch it
let comment_id = data.comment_id;
let person_id = local_user_view.person.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
let res = CommentResponse {
comment_view,
recipient_ids: Vec::new(),
form_id: None,
};
Ok(res)
}
}

View file

@ -1,3 +1,2 @@
mod like; mod like;
mod mark_as_read;
mod save; mod save;

View file

@ -60,6 +60,9 @@ pub async fn match_websocket_operation(
UserOperation::MarkPersonMentionAsRead => { UserOperation::MarkPersonMentionAsRead => {
do_websocket_operation::<MarkPersonMentionAsRead>(context, id, op, data).await do_websocket_operation::<MarkPersonMentionAsRead>(context, id, op, data).await
} }
UserOperation::MarkCommentReplyAsRead => {
do_websocket_operation::<MarkCommentReplyAsRead>(context, id, op, data).await
}
UserOperation::MarkAllAsRead => { UserOperation::MarkAllAsRead => {
do_websocket_operation::<MarkAllAsRead>(context, id, op, data).await do_websocket_operation::<MarkAllAsRead>(context, id, op, data).await
} }
@ -155,9 +158,6 @@ pub async fn match_websocket_operation(
} }
// Comment ops // Comment ops
UserOperation::MarkCommentAsRead => {
do_websocket_operation::<MarkCommentAsRead>(context, id, op, data).await
}
UserOperation::SaveComment => { UserOperation::SaveComment => {
do_websocket_operation::<SaveComment>(context, id, op, data).await do_websocket_operation::<SaveComment>(context, id, op, data).await
} }

View file

@ -27,12 +27,15 @@ impl Perform for GetPersonMentions {
let limit = data.limit; let limit = data.limit;
let unread_only = data.unread_only; let unread_only = data.unread_only;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
let mentions = blocking(context.pool(), move |conn| { let mentions = blocking(context.pool(), move |conn| {
PersonMentionQueryBuilder::create(conn) PersonMentionQueryBuilder::create(conn)
.recipient_id(person_id) .recipient_id(person_id)
.my_person_id(person_id) .my_person_id(person_id)
.sort(sort) .sort(sort)
.unread_only(unread_only) .unread_only(unread_only)
.show_bot_accounts(show_bot_accounts)
.page(page) .page(page)
.limit(limit) .limit(limit)
.list() .list()

View file

@ -4,7 +4,7 @@ use lemmy_api_common::{
person::{GetReplies, GetRepliesResponse}, person::{GetReplies, GetRepliesResponse},
utils::{blocking, get_local_user_view_from_jwt}, utils::{blocking, get_local_user_view_from_jwt},
}; };
use lemmy_db_views::comment_view::CommentQueryBuilder; use lemmy_db_views_actor::comment_reply_view::CommentReplyQueryBuilder;
use lemmy_utils::{error::LemmyError, ConnectionId}; use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -30,12 +30,12 @@ impl Perform for GetReplies {
let show_bot_accounts = local_user_view.local_user.show_bot_accounts; let show_bot_accounts = local_user_view.local_user.show_bot_accounts;
let replies = blocking(context.pool(), move |conn| { let replies = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn) CommentReplyQueryBuilder::create(conn)
.recipient_id(person_id)
.my_person_id(person_id)
.sort(sort) .sort(sort)
.unread_only(unread_only) .unread_only(unread_only)
.recipient_id(person_id)
.show_bot_accounts(show_bot_accounts) .show_bot_accounts(show_bot_accounts)
.my_person_id(person_id)
.page(page) .page(page)
.limit(limit) .limit(limit)
.list() .list()

View file

@ -5,11 +5,10 @@ use lemmy_api_common::{
utils::{blocking, get_local_user_view_from_jwt}, utils::{blocking, get_local_user_view_from_jwt},
}; };
use lemmy_db_schema::source::{ use lemmy_db_schema::source::{
comment::Comment, comment_reply::CommentReply,
person_mention::PersonMention, person_mention::PersonMention,
private_message::PrivateMessage, private_message::PrivateMessage,
}; };
use lemmy_db_views::comment_view::CommentQueryBuilder;
use lemmy_utils::{error::LemmyError, ConnectionId}; use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -26,40 +25,26 @@ impl Perform for MarkAllAsRead {
let data: &MarkAllAsRead = self; let data: &MarkAllAsRead = self;
let local_user_view = let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?; get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let replies = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.my_person_id(person_id)
.recipient_id(person_id)
.unread_only(true)
.page(1)
.limit(std::i64::MAX)
.list()
})
.await??;
// TODO: this should probably be a bulk operation // Mark all comment_replies as read
// Not easy to do as a bulk operation, blocking(context.pool(), move |conn| {
// because recipient_id isn't in the comment table CommentReply::mark_all_as_read(conn, person_id)
for comment_view in &replies { })
let reply_id = comment_view.comment.id;
let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
blocking(context.pool(), mark_as_read)
.await? .await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
}
// Mark all user mentions as read // Mark all user mentions as read
let update_person_mentions = blocking(context.pool(), move |conn| {
move |conn: &'_ _| PersonMention::mark_all_as_read(conn, person_id); PersonMention::mark_all_as_read(conn, person_id)
blocking(context.pool(), update_person_mentions) })
.await? .await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?; .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
// Mark all private_messages as read // Mark all private_messages as read
let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, person_id); blocking(context.pool(), move |conn| {
blocking(context.pool(), update_pm) PrivateMessage::mark_all_as_read(conn, person_id)
})
.await? .await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_update_private_message"))?; .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_private_message"))?;

View file

@ -0,0 +1,52 @@
use crate::Perform;
use actix_web::web::Data;
use lemmy_api_common::{
person::{CommentReplyResponse, MarkCommentReplyAsRead},
utils::{blocking, get_local_user_view_from_jwt},
};
use lemmy_db_schema::{source::comment_reply::CommentReply, traits::Crud};
use lemmy_db_views_actor::structs::CommentReplyView;
use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext;
#[async_trait::async_trait(?Send)]
impl Perform for MarkCommentReplyAsRead {
type Response = CommentReplyResponse;
#[tracing::instrument(skip(context, _websocket_id))]
async fn perform(
&self,
context: &Data<LemmyContext>,
_websocket_id: Option<ConnectionId>,
) -> Result<CommentReplyResponse, LemmyError> {
let data = self;
let local_user_view =
get_local_user_view_from_jwt(&data.auth, context.pool(), context.secret()).await?;
let comment_reply_id = data.comment_reply_id;
let read_comment_reply = blocking(context.pool(), move |conn| {
CommentReply::read(conn, comment_reply_id)
})
.await??;
if local_user_view.person.id != read_comment_reply.recipient_id {
return Err(LemmyError::from_message("couldnt_update_comment"));
}
let comment_reply_id = read_comment_reply.id;
let read = data.read;
let update_reply = move |conn: &'_ _| CommentReply::update_read(conn, comment_reply_id, read);
blocking(context.pool(), update_reply)
.await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
let comment_reply_id = read_comment_reply.id;
let person_id = local_user_view.person.id;
let comment_reply_view = blocking(context.pool(), move |conn| {
CommentReplyView::read(conn, comment_reply_id, Some(person_id))
})
.await??;
Ok(CommentReplyResponse { comment_reply_view })
}
}

View file

@ -2,4 +2,5 @@ mod list_mentions;
mod list_replies; mod list_replies;
mod mark_all_read; mod mark_all_read;
mod mark_mention_read; mod mark_mention_read;
mod mark_reply_read;
mod unread_count; mod unread_count;

View file

@ -4,8 +4,8 @@ use lemmy_api_common::{
person::{GetUnreadCount, GetUnreadCountResponse}, person::{GetUnreadCount, GetUnreadCountResponse},
utils::{blocking, get_local_user_view_from_jwt}, utils::{blocking, get_local_user_view_from_jwt},
}; };
use lemmy_db_views::structs::{CommentView, PrivateMessageView}; use lemmy_db_views::structs::PrivateMessageView;
use lemmy_db_views_actor::structs::PersonMentionView; use lemmy_db_views_actor::structs::{CommentReplyView, PersonMentionView};
use lemmy_utils::{error::LemmyError, ConnectionId}; use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -26,7 +26,7 @@ impl Perform for GetUnreadCount {
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let replies = blocking(context.pool(), move |conn| { let replies = blocking(context.pool(), move |conn| {
CommentView::get_unread_replies(conn, person_id) CommentReplyView::get_unread_replies(conn, person_id)
}) })
.await??; .await??;

View file

@ -5,7 +5,12 @@ use lemmy_api_common::{
utils::{blocking, check_private_instance, get_local_user_view_from_jwt_opt}, utils::{blocking, check_private_instance, get_local_user_view_from_jwt_opt},
}; };
use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity}; use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity};
use lemmy_db_schema::{source::community::Community, traits::DeleteableOrRemoveable, SearchType}; use lemmy_db_schema::{
source::community::Community,
traits::DeleteableOrRemoveable,
utils::post_to_comment_sort_type,
SearchType,
};
use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder}; use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder};
use lemmy_db_views_actor::{ use lemmy_db_views_actor::{
community_view::CommunityQueryBuilder, community_view::CommunityQueryBuilder,
@ -88,7 +93,7 @@ impl Perform for Search {
SearchType::Comments => { SearchType::Comments => {
comments = blocking(context.pool(), move |conn| { comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn) CommentQueryBuilder::create(conn)
.sort(sort) .sort(sort.map(post_to_comment_sort_type))
.listing_type(listing_type) .listing_type(listing_type)
.search_term(q) .search_term(q)
.show_bot_accounts(show_bot_accounts) .show_bot_accounts(show_bot_accounts)
@ -155,7 +160,7 @@ impl Perform for Search {
comments = blocking(context.pool(), move |conn| { comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn) CommentQueryBuilder::create(conn)
.sort(sort) .sort(sort.map(post_to_comment_sort_type))
.listing_type(listing_type) .listing_type(listing_type)
.search_term(q) .search_term(q)
.show_bot_accounts(show_bot_accounts) .show_bot_accounts(show_bot_accounts)

View file

@ -1,8 +1,8 @@
use crate::sensitive::Sensitive; use crate::sensitive::Sensitive;
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommentId, CommentReportId, CommunityId, LocalUserId, PostId}, newtypes::{CommentId, CommentReportId, CommunityId, LocalUserId, PostId},
CommentSortType,
ListingType, ListingType,
SortType,
}; };
use lemmy_db_views::structs::{CommentReportView, CommentView}; use lemmy_db_views::structs::{CommentReportView, CommentView};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -45,13 +45,6 @@ pub struct RemoveComment {
pub auth: Sensitive<String>, pub auth: Sensitive<String>,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct MarkCommentAsRead {
pub comment_id: CommentId,
pub read: bool,
pub auth: Sensitive<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct SaveComment { pub struct SaveComment {
pub comment_id: CommentId, pub comment_id: CommentId,
@ -76,11 +69,14 @@ pub struct CreateCommentLike {
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct GetComments { pub struct GetComments {
pub type_: Option<ListingType>, pub type_: Option<ListingType>,
pub sort: Option<SortType>, pub sort: Option<CommentSortType>,
pub max_depth: Option<i32>,
pub page: Option<i64>, pub page: Option<i64>,
pub limit: Option<i64>, pub limit: Option<i64>,
pub community_id: Option<CommunityId>, pub community_id: Option<CommunityId>,
pub community_name: Option<String>, pub community_name: Option<String>,
pub post_id: Option<PostId>,
pub parent_id: Option<CommentId>,
pub saved_only: Option<bool>, pub saved_only: Option<bool>,
pub auth: Option<Sensitive<String>>, pub auth: Option<Sensitive<String>>,
} }

View file

@ -1,6 +1,11 @@
use crate::sensitive::Sensitive; use crate::sensitive::Sensitive;
use lemmy_db_views::structs::{CommentView, PostView, PrivateMessageView}; use lemmy_db_views::structs::{CommentView, PostView, PrivateMessageView};
use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonMentionView, PersonViewSafe}; use lemmy_db_views_actor::structs::{
CommentReplyView,
CommunityModeratorView,
PersonMentionView,
PersonViewSafe,
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
@ -9,7 +14,8 @@ pub struct Login {
pub password: Sensitive<String>, pub password: Sensitive<String>,
} }
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommunityId, PersonId, PersonMentionId, PrivateMessageId}, newtypes::{CommentReplyId, CommunityId, PersonId, PersonMentionId, PrivateMessageId},
CommentSortType,
SortType, SortType,
}; };
@ -105,7 +111,7 @@ pub struct GetPersonDetailsResponse {
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct GetRepliesResponse { pub struct GetRepliesResponse {
pub replies: Vec<CommentView>, pub replies: Vec<CommentReplyView>,
} }
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@ -171,7 +177,7 @@ pub struct BlockPersonResponse {
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct GetReplies { pub struct GetReplies {
pub sort: Option<SortType>, pub sort: Option<CommentSortType>,
pub page: Option<i64>, pub page: Option<i64>,
pub limit: Option<i64>, pub limit: Option<i64>,
pub unread_only: Option<bool>, pub unread_only: Option<bool>,
@ -180,7 +186,7 @@ pub struct GetReplies {
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct GetPersonMentions { pub struct GetPersonMentions {
pub sort: Option<SortType>, pub sort: Option<CommentSortType>,
pub page: Option<i64>, pub page: Option<i64>,
pub limit: Option<i64>, pub limit: Option<i64>,
pub unread_only: Option<bool>, pub unread_only: Option<bool>,
@ -199,6 +205,18 @@ pub struct PersonMentionResponse {
pub person_mention_view: PersonMentionView, pub person_mention_view: PersonMentionView,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct MarkCommentReplyAsRead {
pub comment_reply_id: CommentReplyId,
pub read: bool,
pub auth: Sensitive<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct CommentReplyResponse {
pub comment_reply_view: CommentReplyView,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct DeleteAccount { pub struct DeleteAccount {
pub password: Sensitive<String>, pub password: Sensitive<String>,

View file

@ -1,10 +1,10 @@
use crate::sensitive::Sensitive; use crate::sensitive::Sensitive;
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommunityId, DbUrl, PostId, PostReportId}, newtypes::{CommentId, CommunityId, DbUrl, PostId, PostReportId},
ListingType, ListingType,
SortType, SortType,
}; };
use lemmy_db_views::structs::{CommentView, PostReportView, PostView}; use lemmy_db_views::structs::{PostReportView, PostView};
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use url::Url; use url::Url;
@ -27,7 +27,8 @@ pub struct PostResponse {
#[derive(Debug, Serialize, Deserialize, Clone, Default)] #[derive(Debug, Serialize, Deserialize, Clone, Default)]
pub struct GetPost { pub struct GetPost {
pub id: PostId, pub id: Option<PostId>,
pub comment_id: Option<CommentId>,
pub auth: Option<Sensitive<String>>, pub auth: Option<Sensitive<String>>,
} }
@ -35,7 +36,6 @@ pub struct GetPost {
pub struct GetPostResponse { pub struct GetPostResponse {
pub post_view: PostView, pub post_view: PostView,
pub community_view: CommunityView, pub community_view: CommunityView,
pub comments: Vec<CommentView>,
pub moderators: Vec<CommunityModeratorView>, pub moderators: Vec<CommunityModeratorView>,
pub online: usize, pub online: usize,
} }

View file

@ -20,11 +20,11 @@ use lemmy_apub::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
comment::{Comment, CommentForm, CommentLike, CommentLikeForm}, comment::{Comment, CommentForm, CommentLike, CommentLikeForm},
comment_reply::CommentReply,
person_mention::PersonMention, person_mention::PersonMention,
}, },
traits::{Crud, Likeable}, traits::{Crud, Likeable},
}; };
use lemmy_db_views::structs::CommentView;
use lemmy_utils::{ use lemmy_utils::{
error::LemmyError, error::LemmyError,
utils::{remove_slurs, scrape_text_for_mentions}, utils::{remove_slurs, scrape_text_for_mentions},
@ -67,14 +67,18 @@ impl PerformCrud for CreateComment {
return Err(LemmyError::from_message("locked")); return Err(LemmyError::from_message("locked"));
} }
// If there's a parent_id, check to make sure that comment is in that post // Fetch the parent, if it exists
if let Some(parent_id) = data.parent_id { let parent_opt = if let Some(parent_id) = data.parent_id {
// Make sure the parent comment exists blocking(context.pool(), move |conn| Comment::read(conn, parent_id))
let parent = blocking(context.pool(), move |conn| Comment::read(conn, parent_id))
.await? .await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_create_comment"))?; .ok()
} else {
None
};
// Strange issue where sometimes the post ID is incorrect // If there's a parent_id, check to make sure that comment is in that post
// Strange issue where sometimes the post ID of the parent comment is incorrect
if let Some(parent) = parent_opt.as_ref() {
if parent.post_id != post_id { if parent.post_id != post_id {
return Err(LemmyError::from_message("couldnt_create_comment")); return Err(LemmyError::from_message("couldnt_create_comment"));
} }
@ -82,7 +86,6 @@ impl PerformCrud for CreateComment {
let comment_form = CommentForm { let comment_form = CommentForm {
content: content_slurs_removed, content: content_slurs_removed,
parent_id: data.parent_id.to_owned(),
post_id: data.post_id, post_id: data.post_id,
creator_id: local_user_view.person.id, creator_id: local_user_view.person.id,
..CommentForm::default() ..CommentForm::default()
@ -90,8 +93,9 @@ impl PerformCrud for CreateComment {
// Create the comment // Create the comment
let comment_form2 = comment_form.clone(); let comment_form2 = comment_form.clone();
let parent_path = parent_opt.to_owned().map(|t| t.path);
let inserted_comment = blocking(context.pool(), move |conn| { let inserted_comment = blocking(context.pool(), move |conn| {
Comment::create(conn, &comment_form2) Comment::create(conn, &comment_form2, parent_path.as_ref())
}) })
.await? .await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_create_comment"))?; .map_err(|e| LemmyError::from_error_message(e, "couldnt_create_comment"))?;
@ -148,35 +152,21 @@ impl PerformCrud for CreateComment {
) )
.await?; .await?;
let person_id = local_user_view.person.id;
let comment_id = inserted_comment.id;
let comment_view = blocking(context.pool(), move |conn| {
CommentView::read(conn, comment_id, Some(person_id))
})
.await??;
// If its a comment to yourself, mark it as read
if local_user_view.person.id == comment_view.get_recipient_id() {
let comment_id = inserted_comment.id;
blocking(context.pool(), move |conn| {
Comment::update_read(conn, comment_id, true)
})
.await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_update_comment"))?;
}
// If its a reply, mark the parent as read // If its a reply, mark the parent as read
if let Some(parent_id) = data.parent_id { if let Some(parent) = parent_opt {
let parent_comment = blocking(context.pool(), move |conn| { let parent_id = parent.id;
CommentView::read(conn, parent_id, Some(person_id)) let comment_reply = blocking(context.pool(), move |conn| {
CommentReply::read_by_comment(conn, parent_id)
}) })
.await??; .await?;
if local_user_view.person.id == parent_comment.get_recipient_id() { if let Ok(reply) = comment_reply {
blocking(context.pool(), move |conn| { blocking(context.pool(), move |conn| {
Comment::update_read(conn, parent_id, true) CommentReply::update_read(conn, reply.id, true)
}) })
.await? .await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_update_parent_comment"))?; .map_err(|e| LemmyError::from_error_message(e, "couldnt_update_replies"))?;
} }
// If the parent has PersonMentions mark them as read too // If the parent has PersonMentions mark them as read too
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let person_mention = blocking(context.pool(), move |conn| { let person_mention = blocking(context.pool(), move |conn| {

View file

@ -10,7 +10,10 @@ use lemmy_api_common::{
}, },
}; };
use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity}; use lemmy_apub::{fetcher::resolve_actor_identifier, objects::community::ApubCommunity};
use lemmy_db_schema::{source::community::Community, traits::DeleteableOrRemoveable}; use lemmy_db_schema::{
source::{comment::Comment, community::Community},
traits::{Crud, DeleteableOrRemoveable},
};
use lemmy_db_views::comment_view::CommentQueryBuilder; use lemmy_db_views::comment_view::CommentQueryBuilder;
use lemmy_utils::{error::LemmyError, ConnectionId}; use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -49,16 +52,34 @@ impl PerformCrud for GetComments {
None None
}; };
let sort = data.sort; let sort = data.sort;
let max_depth = data.max_depth;
let saved_only = data.saved_only; let saved_only = data.saved_only;
let page = data.page; let page = data.page;
let limit = data.limit; let limit = data.limit;
let parent_id = data.parent_id;
// If a parent_id is given, fetch the comment to get the path
let parent_path = if let Some(parent_id) = parent_id {
Some(
blocking(context.pool(), move |conn| Comment::read(conn, parent_id))
.await??
.path,
)
} else {
None
};
let post_id = data.post_id;
let mut comments = blocking(context.pool(), move |conn| { let mut comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn) CommentQueryBuilder::create(conn)
.listing_type(listing_type) .listing_type(listing_type)
.sort(sort) .sort(sort)
.max_depth(max_depth)
.saved_only(saved_only) .saved_only(saved_only)
.community_id(community_id) .community_id(community_id)
.community_actor_id(community_actor_id) .community_actor_id(community_actor_id)
.parent_path(parent_path)
.post_id(post_id)
.my_person_id(person_id) .my_person_id(person_id)
.show_bot_accounts(show_bot_accounts) .show_bot_accounts(show_bot_accounts)
.page(page) .page(page)

View file

@ -4,8 +4,11 @@ use lemmy_api_common::{
post::{GetPost, GetPostResponse}, post::{GetPost, GetPostResponse},
utils::{blocking, check_private_instance, get_local_user_view_from_jwt_opt, mark_post_as_read}, utils::{blocking, check_private_instance, get_local_user_view_from_jwt_opt, mark_post_as_read},
}; };
use lemmy_db_schema::traits::DeleteableOrRemoveable; use lemmy_db_schema::{
use lemmy_db_views::{comment_view::CommentQueryBuilder, structs::PostView}; source::comment::Comment,
traits::{Crud, DeleteableOrRemoveable},
};
use lemmy_db_views::structs::PostView;
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
use lemmy_utils::{error::LemmyError, ConnectionId}; use lemmy_utils::{error::LemmyError, ConnectionId};
use lemmy_websocket::{messages::GetPostUsersOnline, LemmyContext}; use lemmy_websocket::{messages::GetPostUsersOnline, LemmyContext};
@ -27,35 +30,33 @@ impl PerformCrud for GetPost {
check_private_instance(&local_user_view, context.pool()).await?; check_private_instance(&local_user_view, context.pool()).await?;
let show_bot_accounts = local_user_view
.as_ref()
.map(|t| t.local_user.show_bot_accounts);
let person_id = local_user_view.map(|u| u.person.id); let person_id = local_user_view.map(|u| u.person.id);
let id = data.id; // I'd prefer fetching the post_view by a comment join, but it adds a lot of boilerplate
let post_id = if let Some(id) = data.id {
id
} else if let Some(comment_id) = data.comment_id {
blocking(context.pool(), move |conn| Comment::read(conn, comment_id))
.await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_find_post"))?
.post_id
} else {
Err(LemmyError::from_message("couldnt_find_post"))?
};
let mut post_view = blocking(context.pool(), move |conn| { let mut post_view = blocking(context.pool(), move |conn| {
PostView::read(conn, id, person_id) PostView::read(conn, post_id, person_id)
}) })
.await? .await?
.map_err(|e| LemmyError::from_error_message(e, "couldnt_find_post"))?; .map_err(|e| LemmyError::from_error_message(e, "couldnt_find_post"))?;
// Mark the post as read // Mark the post as read
let post_id = post_view.post.id;
if let Some(person_id) = person_id { if let Some(person_id) = person_id {
mark_post_as_read(person_id, id, context.pool()).await?; mark_post_as_read(person_id, post_id, context.pool()).await?;
} }
let id = data.id; // Necessary for the sidebar subscribed
let mut comments = blocking(context.pool(), move |conn| {
CommentQueryBuilder::create(conn)
.my_person_id(person_id)
.show_bot_accounts(show_bot_accounts)
.post_id(id)
.limit(std::i64::MAX)
.list()
})
.await??;
// Necessary for the sidebar
let community_id = post_view.community.id; let community_id = post_view.community.id;
let mut community_view = blocking(context.pool(), move |conn| { let mut community_view = blocking(context.pool(), move |conn| {
CommunityView::read(conn, community_id, person_id) CommunityView::read(conn, community_id, person_id)
@ -69,12 +70,6 @@ impl PerformCrud for GetPost {
post_view.post = post_view.post.blank_out_deleted_or_removed_info(); post_view.post = post_view.post.blank_out_deleted_or_removed_info();
} }
for cv in comments
.iter_mut()
.filter(|cv| cv.comment.deleted || cv.comment.removed)
{
cv.comment = cv.to_owned().comment.blank_out_deleted_or_removed_info();
}
if community_view.community.deleted || community_view.community.removed { if community_view.community.deleted || community_view.community.removed {
community_view.community = community_view.community.blank_out_deleted_or_removed_info(); community_view.community = community_view.community.blank_out_deleted_or_removed_info();
} }
@ -87,7 +82,7 @@ impl PerformCrud for GetPost {
let online = context let online = context
.chat_server() .chat_server()
.send(GetPostUsersOnline { post_id: data.id }) .send(GetPostUsersOnline { post_id })
.await .await
.unwrap_or(1); .unwrap_or(1);
@ -95,7 +90,6 @@ impl PerformCrud for GetPost {
Ok(GetPostResponse { Ok(GetPostResponse {
post_view, post_view,
community_view, community_view,
comments,
moderators, moderators,
online, online,
}) })

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
utils::{blocking, check_private_instance, get_local_user_view_from_jwt_opt}, utils::{blocking, check_private_instance, get_local_user_view_from_jwt_opt},
}; };
use lemmy_apub::{fetcher::resolve_actor_identifier, objects::person::ApubPerson}; use lemmy_apub::{fetcher::resolve_actor_identifier, objects::person::ApubPerson};
use lemmy_db_schema::source::person::Person; use lemmy_db_schema::{source::person::Person, utils::post_to_comment_sort_type};
use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder}; use lemmy_db_views::{comment_view::CommentQueryBuilder, post_view::PostQueryBuilder};
use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonViewSafe}; use lemmy_db_views_actor::structs::{CommunityModeratorView, PersonViewSafe};
use lemmy_utils::{error::LemmyError, ConnectionId}; use lemmy_utils::{error::LemmyError, ConnectionId};
@ -88,7 +88,7 @@ impl PerformCrud for GetPersonDetails {
let mut comments_query = CommentQueryBuilder::create(conn) let mut comments_query = CommentQueryBuilder::create(conn)
.my_person_id(person_id) .my_person_id(person_id)
.show_bot_accounts(show_bot_accounts) .show_bot_accounts(show_bot_accounts)
.sort(sort) .sort(sort.map(post_to_comment_sort_type))
.saved_only(saved_only) .saved_only(saved_only)
.community_id(community_id) .community_id(community_id)
.page(page) .page(page)

View file

@ -104,7 +104,7 @@ async fn get_comment_parent_creator(
pool: &DbPool, pool: &DbPool,
comment: &Comment, comment: &Comment,
) -> Result<ApubPerson, LemmyError> { ) -> Result<ApubPerson, LemmyError> {
let parent_creator_id = if let Some(parent_comment_id) = comment.parent_id { let parent_creator_id = if let Some(parent_comment_id) = comment.parent_comment_id() {
let parent_comment = let parent_comment =
blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??; blocking(pool, move |conn| Comment::read(conn, parent_comment_id)).await??;
parent_comment.creator_id parent_comment.creator_id

View file

@ -98,7 +98,7 @@ impl ApubObject for ApubComment {
}) })
.await??; .await??;
let in_reply_to = if let Some(comment_id) = self.parent_id { let in_reply_to = if let Some(comment_id) = self.parent_comment_id() {
let parent_comment = let parent_comment =
blocking(context.pool(), move |conn| Comment::read(conn, comment_id)).await??; blocking(context.pool(), move |conn| Comment::read(conn, comment_id)).await??;
ObjectId::<PostOrComment>::new(parent_comment.ap_id) ObjectId::<PostOrComment>::new(parent_comment.ap_id)
@ -170,7 +170,7 @@ impl ApubObject for ApubComment {
.attributed_to .attributed_to
.dereference(context, local_instance(context), request_counter) .dereference(context, local_instance(context), request_counter)
.await?; .await?;
let (post, parent_comment_id) = note.get_parents(context, request_counter).await?; let (post, parent_comment) = note.get_parents(context, request_counter).await?;
let content = read_from_string_or_source(&note.content, &note.media_type, &note.source); let content = read_from_string_or_source(&note.content, &note.media_type, &note.source);
let content_slurs_removed = remove_slurs(&content, &context.settings().slur_regex()); let content_slurs_removed = remove_slurs(&content, &context.settings().slur_regex());
@ -178,17 +178,19 @@ impl ApubObject for ApubComment {
let form = CommentForm { let form = CommentForm {
creator_id: creator.id, creator_id: creator.id,
post_id: post.id, post_id: post.id,
parent_id: parent_comment_id,
content: content_slurs_removed, content: content_slurs_removed,
removed: None, removed: None,
read: None,
published: note.published.map(|u| u.naive_local()), published: note.published.map(|u| u.naive_local()),
updated: note.updated.map(|u| u.naive_local()), updated: note.updated.map(|u| u.naive_local()),
deleted: None, deleted: None,
ap_id: Some(note.id.into()), ap_id: Some(note.id.into()),
local: Some(false), local: Some(false),
}; };
let comment = blocking(context.pool(), move |conn| Comment::upsert(conn, &form)).await??; let parent_comment_path = parent_comment.map(|t| t.0.path);
let comment = blocking(context.pool(), move |conn| {
Comment::create(conn, &form, parent_comment_path.as_ref())
})
.await??;
Ok(comment.into()) Ok(comment.into())
} }
} }

View file

@ -15,7 +15,7 @@ use activitypub_federation::{
use activitystreams_kinds::object::NoteType; use activitystreams_kinds::object::NoteType;
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use lemmy_api_common::utils::blocking; use lemmy_api_common::utils::blocking;
use lemmy_db_schema::{newtypes::CommentId, source::post::Post, traits::Crud}; use lemmy_db_schema::{source::post::Post, traits::Crud};
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -51,7 +51,7 @@ impl Note {
&self, &self,
context: &LemmyContext, context: &LemmyContext,
request_counter: &mut i32, request_counter: &mut i32,
) -> Result<(ApubPost, Option<CommentId>), LemmyError> { ) -> Result<(ApubPost, Option<ApubComment>), LemmyError> {
// Fetch parent comment chain in a box, otherwise it can cause a stack overflow. // Fetch parent comment chain in a box, otherwise it can cause a stack overflow.
let parent = Box::pin( let parent = Box::pin(
self self
@ -61,16 +61,14 @@ impl Note {
); );
match parent.deref() { match parent.deref() {
PostOrComment::Post(p) => { PostOrComment::Post(p) => {
// Workaround because I cant figure out how to get the post out of the box (and we dont let post = p.deref().to_owned();
// want to stackoverflow in a deep comment hierarchy). Ok((post, None))
let post_id = p.id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
Ok((post.into(), None))
} }
PostOrComment::Comment(c) => { PostOrComment::Comment(c) => {
let post_id = c.post_id; let post_id = c.post_id;
let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??; let post = blocking(context.pool(), move |conn| Post::read(conn, post_id)).await??;
Ok((post.into(), Some(c.id))) let comment = c.deref().to_owned();
Ok((post.into(), Some(comment)))
} }
} }
} }

View file

@ -33,6 +33,7 @@ diesel_migrations = { version = "1.4.0", optional = true }
sha2 = { version = "0.10.2", optional = true } sha2 = { version = "0.10.2", optional = true }
regex = { version = "1.5.5", optional = true } regex = { version = "1.5.5", optional = true }
once_cell = { version = "1.10.0", optional = true } once_cell = { version = "1.10.0", optional = true }
diesel_ltree = "0.2.7"
[dev-dependencies] [dev-dependencies]
serial_test = "0.6.0" serial_test = "0.6.0"

View file

@ -74,17 +74,17 @@ mod tests {
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let inserted_comment = Comment::create(&conn, &comment_form, None).unwrap();
let child_comment_form = CommentForm { let child_comment_form = CommentForm {
content: "A test comment".into(), content: "A test comment".into(),
creator_id: inserted_person.id, creator_id: inserted_person.id,
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: Some(inserted_comment.id),
..CommentForm::default() ..CommentForm::default()
}; };
let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap(); let _inserted_child_comment =
Comment::create(&conn, &child_comment_form, Some(&inserted_comment.path)).unwrap();
let comment_like = CommentLikeForm { let comment_like = CommentLikeForm {
comment_id: inserted_comment.id, comment_id: inserted_comment.id,

View file

@ -107,17 +107,17 @@ mod tests {
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let inserted_comment = Comment::create(&conn, &comment_form, None).unwrap();
let child_comment_form = CommentForm { let child_comment_form = CommentForm {
content: "A test comment".into(), content: "A test comment".into(),
creator_id: inserted_person.id, creator_id: inserted_person.id,
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: Some(inserted_comment.id),
..CommentForm::default() ..CommentForm::default()
}; };
let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap(); let _inserted_child_comment =
Comment::create(&conn, &child_comment_form, Some(&inserted_comment.path)).unwrap();
let community_aggregates_before_delete = let community_aggregates_before_delete =
CommunityAggregates::read(&conn, inserted_community.id).unwrap(); CommunityAggregates::read(&conn, inserted_community.id).unwrap();

View file

@ -78,7 +78,7 @@ mod tests {
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let inserted_comment = Comment::create(&conn, &comment_form, None).unwrap();
let mut comment_like = CommentLikeForm { let mut comment_like = CommentLikeForm {
comment_id: inserted_comment.id, comment_id: inserted_comment.id,
@ -89,15 +89,15 @@ mod tests {
let _inserted_comment_like = CommentLike::like(&conn, &comment_like).unwrap(); let _inserted_comment_like = CommentLike::like(&conn, &comment_like).unwrap();
let mut child_comment_form = CommentForm { let child_comment_form = CommentForm {
content: "A test comment".into(), content: "A test comment".into(),
creator_id: inserted_person.id, creator_id: inserted_person.id,
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: Some(inserted_comment.id),
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap(); let inserted_child_comment =
Comment::create(&conn, &child_comment_form, Some(&inserted_comment.path)).unwrap();
let child_comment_like = CommentLikeForm { let child_comment_like = CommentLikeForm {
comment_id: inserted_child_comment.id, comment_id: inserted_child_comment.id,
@ -123,14 +123,15 @@ mod tests {
// Remove a parent comment (the scores should also be removed) // Remove a parent comment (the scores should also be removed)
Comment::delete(&conn, inserted_comment.id).unwrap(); Comment::delete(&conn, inserted_comment.id).unwrap();
Comment::delete(&conn, inserted_child_comment.id).unwrap();
let after_parent_comment_delete = PersonAggregates::read(&conn, inserted_person.id).unwrap(); let after_parent_comment_delete = PersonAggregates::read(&conn, inserted_person.id).unwrap();
assert_eq!(0, after_parent_comment_delete.comment_count); assert_eq!(0, after_parent_comment_delete.comment_count);
assert_eq!(0, after_parent_comment_delete.comment_score); assert_eq!(0, after_parent_comment_delete.comment_score);
// Add in the two comments again, then delete the post. // Add in the two comments again, then delete the post.
let new_parent_comment = Comment::create(&conn, &comment_form).unwrap(); let new_parent_comment = Comment::create(&conn, &comment_form, None).unwrap();
child_comment_form.parent_id = Some(new_parent_comment.id); let _new_child_comment =
Comment::create(&conn, &child_comment_form).unwrap(); Comment::create(&conn, &child_comment_form, Some(&new_parent_comment.path)).unwrap();
comment_like.comment_id = new_parent_comment.id; comment_like.comment_id = new_parent_comment.id;
CommentLike::like(&conn, &comment_like).unwrap(); CommentLike::like(&conn, &comment_like).unwrap();
let after_comment_add = PersonAggregates::read(&conn, inserted_person.id).unwrap(); let after_comment_add = PersonAggregates::read(&conn, inserted_person.id).unwrap();

View file

@ -70,17 +70,17 @@ mod tests {
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let inserted_comment = Comment::create(&conn, &comment_form, None).unwrap();
let child_comment_form = CommentForm { let child_comment_form = CommentForm {
content: "A test comment".into(), content: "A test comment".into(),
creator_id: inserted_person.id, creator_id: inserted_person.id,
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: Some(inserted_comment.id),
..CommentForm::default() ..CommentForm::default()
}; };
let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap(); let inserted_child_comment =
Comment::create(&conn, &child_comment_form, Some(&inserted_comment.path)).unwrap();
let post_like = PostLikeForm { let post_like = PostLikeForm {
post_id: inserted_post.id, post_id: inserted_post.id,
@ -113,8 +113,9 @@ mod tests {
assert_eq!(1, post_aggs_after_dislike.upvotes); assert_eq!(1, post_aggs_after_dislike.upvotes);
assert_eq!(1, post_aggs_after_dislike.downvotes); assert_eq!(1, post_aggs_after_dislike.downvotes);
// Remove the parent comment // Remove the comments
Comment::delete(&conn, inserted_comment.id).unwrap(); Comment::delete(&conn, inserted_comment.id).unwrap();
Comment::delete(&conn, inserted_child_comment.id).unwrap();
let after_comment_delete = PostAggregates::read(&conn, inserted_post.id).unwrap(); let after_comment_delete = PostAggregates::read(&conn, inserted_post.id).unwrap();
assert_eq!(0, after_comment_delete.comments); assert_eq!(0, after_comment_delete.comments);
assert_eq!(0, after_comment_delete.score); assert_eq!(0, after_comment_delete.score);

View file

@ -72,17 +72,17 @@ mod tests {
}; };
// Insert two of those comments // Insert two of those comments
let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let inserted_comment = Comment::create(&conn, &comment_form, None).unwrap();
let child_comment_form = CommentForm { let child_comment_form = CommentForm {
content: "A test comment".into(), content: "A test comment".into(),
creator_id: inserted_person.id, creator_id: inserted_person.id,
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: Some(inserted_comment.id),
..CommentForm::default() ..CommentForm::default()
}; };
let _inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap(); let _inserted_child_comment =
Comment::create(&conn, &child_comment_form, Some(&inserted_comment.path)).unwrap();
let site_aggregates_before_delete = SiteAggregates::read(&conn).unwrap(); let site_aggregates_before_delete = SiteAggregates::read(&conn).unwrap();

View file

@ -20,6 +20,7 @@ pub struct CommentAggregates {
pub upvotes: i64, pub upvotes: i64,
pub downvotes: i64, pub downvotes: i64,
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub child_count: i32,
} }
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)] #[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]

View file

@ -12,6 +12,7 @@ use crate::{
utils::naive_now, utils::naive_now,
}; };
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use diesel_ltree::Ltree;
use url::Url; use url::Url;
impl Comment { impl Comment {
@ -74,17 +75,6 @@ impl Comment {
.get_results::<Self>(conn) .get_results::<Self>(conn)
} }
pub fn update_read(
conn: &PgConnection,
comment_id: CommentId,
new_read: bool,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn update_content( pub fn update_content(
conn: &PgConnection, conn: &PgConnection,
comment_id: CommentId, comment_id: CommentId,
@ -96,14 +86,71 @@ impl Comment {
.get_result::<Self>(conn) .get_result::<Self>(conn)
} }
pub fn upsert(conn: &PgConnection, comment_form: &CommentForm) -> Result<Comment, Error> { pub fn create(
conn: &PgConnection,
comment_form: &CommentForm,
parent_path: Option<&Ltree>,
) -> Result<Comment, Error> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
insert_into(comment)
// Insert, to get the id
let inserted_comment = insert_into(comment)
.values(comment_form) .values(comment_form)
.on_conflict(ap_id) .on_conflict(ap_id)
.do_update() .do_update()
.set(comment_form) .set(comment_form)
.get_result::<Self>(conn) .get_result::<Self>(conn);
if let Ok(comment_insert) = inserted_comment {
let comment_id = comment_insert.id;
// You need to update the ltree column
let ltree = Ltree(if let Some(parent_path) = parent_path {
// The previous parent will already have 0 in it
// Append this comment id
format!("{}.{}", parent_path.0, comment_id)
} else {
// '0' is always the first path, append to that
format!("{}.{}", 0, comment_id)
});
let updated_comment = diesel::update(comment.find(comment_id))
.set(path.eq(ltree))
.get_result::<Self>(conn);
// Update the child count for the parent comment_aggregates
// You could do this with a trigger, but since you have to do this manually anyway,
// you can just have it here
if let Some(parent_path) = parent_path {
// You have to update counts for all parents, not just the immediate one
// TODO if the performance of this is terrible, it might be better to do this as part of a
// scheduled query... although the counts would often be wrong.
//
// The child_count query for reference:
// select c.id, c.path, count(c2.id) as child_count from comment c
// left join comment c2 on c2.path <@ c.path and c2.path != c.path
// group by c.id
let top_parent = format!("0.{}", parent_path.0.split('.').collect::<Vec<&str>>()[1]);
let update_child_count_stmt = format!(
"
update comment_aggregates ca set child_count = c.child_count
from (
select c.id, c.path, count(c2.id) as child_count from comment c
join comment c2 on c2.path <@ c.path and c2.path != c.path
and c.path <@ '{}'
group by c.id
) as c
where ca.comment_id = c.id",
top_parent
);
sql_query(update_child_count_stmt).execute(conn)?;
}
updated_comment
} else {
inserted_comment
}
} }
pub fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result<Option<Self>, Error> { pub fn read_from_apub_id(conn: &PgConnection, object_id: Url) -> Result<Option<Self>, Error> {
use crate::schema::comment::dsl::*; use crate::schema::comment::dsl::*;
@ -116,6 +163,19 @@ impl Comment {
.map(Into::into), .map(Into::into),
) )
} }
pub fn parent_comment_id(&self) -> Option<CommentId> {
let mut ltree_split: Vec<&str> = self.path.0.split('.').collect();
ltree_split.remove(0); // The first is always 0
if ltree_split.len() > 1 {
ltree_split[ltree_split.len() - 2]
.parse::<i32>()
.map(CommentId)
.ok()
} else {
None
}
}
} }
impl Crud for Comment { impl Crud for Comment {
@ -131,11 +191,9 @@ impl Crud for Comment {
diesel::delete(comment.find(comment_id)).execute(conn) diesel::delete(comment.find(comment_id)).execute(conn)
} }
fn create(conn: &PgConnection, comment_form: &CommentForm) -> Result<Self, Error> { /// This is unimplemented, use [[Comment::create]]
use crate::schema::comment::dsl::*; fn create(_conn: &PgConnection, _comment_form: &CommentForm) -> Result<Self, Error> {
insert_into(comment) unimplemented!();
.values(comment_form)
.get_result::<Self>(conn)
} }
fn update( fn update(
@ -218,6 +276,7 @@ mod tests {
traits::{Crud, Likeable, Saveable}, traits::{Crud, Likeable, Saveable},
utils::establish_unpooled_connection, utils::establish_unpooled_connection,
}; };
use diesel_ltree::Ltree;
use serial_test::serial; use serial_test::serial;
#[test] #[test]
@ -258,7 +317,7 @@ mod tests {
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let inserted_comment = Comment::create(&conn, &comment_form, None).unwrap();
let expected_comment = Comment { let expected_comment = Comment {
id: inserted_comment.id, id: inserted_comment.id,
@ -267,8 +326,7 @@ mod tests {
post_id: inserted_post.id, post_id: inserted_post.id,
removed: false, removed: false,
deleted: false, deleted: false,
read: false, path: Ltree(format!("0.{}", inserted_comment.id)),
parent_id: None,
published: inserted_comment.published, published: inserted_comment.published,
updated: None, updated: None,
ap_id: inserted_comment.ap_id.to_owned(), ap_id: inserted_comment.ap_id.to_owned(),
@ -279,11 +337,12 @@ mod tests {
content: "A child comment".into(), content: "A child comment".into(),
creator_id: inserted_person.id, creator_id: inserted_person.id,
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: Some(inserted_comment.id), // path: Some(text2ltree(inserted_comment.id),
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_child_comment = Comment::create(&conn, &child_comment_form).unwrap(); let inserted_child_comment =
Comment::create(&conn, &child_comment_form, Some(&inserted_comment.path)).unwrap();
// Comment Like // Comment Like
let comment_like_form = CommentLikeForm { let comment_like_form = CommentLikeForm {
@ -335,8 +394,8 @@ mod tests {
assert_eq!(expected_comment_like, inserted_comment_like); assert_eq!(expected_comment_like, inserted_comment_like);
assert_eq!(expected_comment_saved, inserted_comment_saved); assert_eq!(expected_comment_saved, inserted_comment_saved);
assert_eq!( assert_eq!(
expected_comment.id, format!("0.{}.{}", expected_comment.id, inserted_child_comment.id),
inserted_child_comment.parent_id.unwrap() inserted_child_comment.path.0,
); );
assert_eq!(1, like_removed); assert_eq!(1, like_removed);
assert_eq!(1, saved_removed); assert_eq!(1, saved_removed);

View file

@ -0,0 +1,166 @@
use crate::{
newtypes::{CommentId, CommentReplyId, PersonId},
source::comment_reply::*,
traits::Crud,
};
use diesel::{dsl::*, result::Error, *};
impl Crud for CommentReply {
type Form = CommentReplyForm;
type IdType = CommentReplyId;
fn read(conn: &PgConnection, comment_reply_id: CommentReplyId) -> Result<Self, Error> {
use crate::schema::comment_reply::dsl::*;
comment_reply.find(comment_reply_id).first::<Self>(conn)
}
fn create(conn: &PgConnection, comment_reply_form: &CommentReplyForm) -> Result<Self, Error> {
use crate::schema::comment_reply::dsl::*;
// since the return here isnt utilized, we dont need to do an update
// but get_result doesnt return the existing row here
insert_into(comment_reply)
.values(comment_reply_form)
.on_conflict((recipient_id, comment_id))
.do_update()
.set(comment_reply_form)
.get_result::<Self>(conn)
}
fn update(
conn: &PgConnection,
comment_reply_id: CommentReplyId,
comment_reply_form: &CommentReplyForm,
) -> Result<Self, Error> {
use crate::schema::comment_reply::dsl::*;
diesel::update(comment_reply.find(comment_reply_id))
.set(comment_reply_form)
.get_result::<Self>(conn)
}
}
impl CommentReply {
pub fn update_read(
conn: &PgConnection,
comment_reply_id: CommentReplyId,
new_read: bool,
) -> Result<CommentReply, Error> {
use crate::schema::comment_reply::dsl::*;
diesel::update(comment_reply.find(comment_reply_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn mark_all_as_read(
conn: &PgConnection,
for_recipient_id: PersonId,
) -> Result<Vec<CommentReply>, Error> {
use crate::schema::comment_reply::dsl::*;
diesel::update(
comment_reply
.filter(recipient_id.eq(for_recipient_id))
.filter(read.eq(false)),
)
.set(read.eq(true))
.get_results::<Self>(conn)
}
pub fn read_by_comment(conn: &PgConnection, for_comment_id: CommentId) -> Result<Self, Error> {
use crate::schema::comment_reply::dsl::*;
comment_reply
.filter(comment_id.eq(for_comment_id))
.first::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{
source::{
comment::*,
comment_reply::*,
community::{Community, CommunityForm},
person::*,
post::*,
},
traits::Crud,
utils::establish_unpooled_connection,
};
use serial_test::serial;
#[test]
#[serial]
fn test_crud() {
let conn = establish_unpooled_connection();
let new_person = PersonForm {
name: "terrylake".into(),
public_key: Some("pubkey".to_string()),
..PersonForm::default()
};
let inserted_person = Person::create(&conn, &new_person).unwrap();
let recipient_form = PersonForm {
name: "terrylakes recipient".into(),
public_key: Some("pubkey".to_string()),
..PersonForm::default()
};
let inserted_recipient = Person::create(&conn, &recipient_form).unwrap();
let new_community = CommunityForm {
name: "test community lake".to_string(),
title: "nada".to_owned(),
public_key: Some("pubkey".to_string()),
..CommunityForm::default()
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
let new_post = PostForm {
name: "A test post".into(),
creator_id: inserted_person.id,
community_id: inserted_community.id,
..PostForm::default()
};
let inserted_post = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm {
content: "A test comment".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
..CommentForm::default()
};
let inserted_comment = Comment::create(&conn, &comment_form, None).unwrap();
let comment_reply_form = CommentReplyForm {
recipient_id: inserted_recipient.id,
comment_id: inserted_comment.id,
read: None,
};
let inserted_reply = CommentReply::create(&conn, &comment_reply_form).unwrap();
let expected_reply = CommentReply {
id: inserted_reply.id,
recipient_id: inserted_reply.recipient_id,
comment_id: inserted_reply.comment_id,
read: false,
published: inserted_reply.published,
};
let read_reply = CommentReply::read(&conn, inserted_reply.id).unwrap();
let updated_reply =
CommentReply::update(&conn, inserted_reply.id, &comment_reply_form).unwrap();
Comment::delete(&conn, inserted_comment.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap();
Person::delete(&conn, inserted_person.id).unwrap();
Person::delete(&conn, inserted_recipient.id).unwrap();
assert_eq!(expected_reply, read_reply);
assert_eq!(expected_reply, inserted_reply);
assert_eq!(expected_reply, updated_reply);
}
}

View file

@ -1,5 +1,6 @@
pub mod activity; pub mod activity;
pub mod comment; pub mod comment;
pub mod comment_reply;
pub mod comment_report; pub mod comment_report;
pub mod community; pub mod community;
pub mod community_block; pub mod community_block;

View file

@ -411,7 +411,7 @@ mod tests {
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let inserted_comment = Comment::create(&conn, &comment_form, None).unwrap();
// Now the actual tests // Now the actual tests

View file

@ -136,7 +136,7 @@ mod tests {
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let inserted_comment = Comment::create(&conn, &comment_form, None).unwrap();
let person_mention_form = PersonMentionForm { let person_mention_form = PersonMentionForm {
recipient_id: inserted_recipient.id, recipient_id: inserted_recipient.id,

View file

@ -30,6 +30,7 @@ pub enum SortType {
Active, Active,
Hot, Hot,
New, New,
Old,
TopDay, TopDay,
TopWeek, TopWeek,
TopMonth, TopMonth,
@ -39,6 +40,14 @@ pub enum SortType {
NewComments, NewComments,
} }
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy)]
pub enum CommentSortType {
Hot,
Top,
New,
Old,
}
#[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq)] #[derive(EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq)]
pub enum ListingType { pub enum ListingType {
All, All,

View file

@ -1,3 +1,4 @@
use diesel_ltree::Ltree;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
fmt, fmt,
@ -68,12 +69,21 @@ pub struct CommentReportId(i32);
#[cfg_attr(feature = "full", derive(DieselNewType))] #[cfg_attr(feature = "full", derive(DieselNewType))]
pub struct PostReportId(i32); pub struct PostReportId(i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType))]
pub struct CommentReplyId(i32);
#[repr(transparent)] #[repr(transparent)]
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] #[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "full", derive(AsExpression, FromSqlRow))] #[cfg_attr(feature = "full", derive(AsExpression, FromSqlRow))]
#[cfg_attr(feature = "full", sql_type = "diesel::sql_types::Text")] #[cfg_attr(feature = "full", sql_type = "diesel::sql_types::Text")]
pub struct DbUrl(pub(crate) Url); pub struct DbUrl(pub(crate) Url);
#[derive(Serialize, Deserialize)]
#[serde(remote = "Ltree")]
/// Do remote derivation for the Ltree struct
pub struct LtreeDef(pub String);
impl Display for DbUrl { impl Display for DbUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.to_owned().0.fmt(f) self.to_owned().0.fmt(f)

View file

@ -11,19 +11,21 @@ table! {
} }
table! { table! {
use diesel_ltree::sql_types::Ltree;
use diesel::sql_types::*;
comment (id) { comment (id) {
id -> Int4, id -> Int4,
creator_id -> Int4, creator_id -> Int4,
post_id -> Int4, post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text, content -> Text,
removed -> Bool, removed -> Bool,
read -> Bool,
published -> Timestamp, published -> Timestamp,
updated -> Nullable<Timestamp>, updated -> Nullable<Timestamp>,
deleted -> Bool, deleted -> Bool,
ap_id -> Varchar, ap_id -> Varchar,
local -> Bool, local -> Bool,
path -> Ltree,
} }
} }
@ -35,6 +37,7 @@ table! {
upvotes -> Int8, upvotes -> Int8,
downvotes -> Int8, downvotes -> Int8,
published -> Timestamp, published -> Timestamp,
child_count -> Int4,
} }
} }
@ -340,6 +343,16 @@ table! {
} }
} }
table! {
comment_reply (id) {
id -> Int4,
recipient_id -> Int4,
comment_id -> Int4,
read -> Bool,
published -> Timestamp,
}
}
table! { table! {
post (id) { post (id) {
id -> Int4, id -> Int4,
@ -501,23 +514,6 @@ table! {
} }
// These are necessary since diesel doesn't have self joins / aliases // These are necessary since diesel doesn't have self joins / aliases
table! {
comment_alias_1 (id) {
id -> Int4,
creator_id -> Int4,
post_id -> Int4,
parent_id -> Nullable<Int4>,
content -> Text,
removed -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
deleted -> Bool,
ap_id -> Varchar,
local -> Bool,
}
}
table! { table! {
person_alias_1 (id) { person_alias_1 (id) {
id -> Int4, id -> Int4,
@ -647,9 +643,8 @@ table! {
} }
} }
joinable!(comment_alias_1 -> person_alias_1 (creator_id));
joinable!(comment -> comment_alias_1 (parent_id));
joinable!(person_mention -> person_alias_1 (recipient_id)); joinable!(person_mention -> person_alias_1 (recipient_id));
joinable!(comment_reply -> person_alias_1 (recipient_id));
joinable!(post -> person_alias_1 (creator_id)); joinable!(post -> person_alias_1 (creator_id));
joinable!(comment -> person_alias_1 (creator_id)); joinable!(comment -> person_alias_1 (creator_id));
@ -696,6 +691,8 @@ joinable!(person_aggregates -> person (person_id));
joinable!(person_ban -> person (person_id)); joinable!(person_ban -> person (person_id));
joinable!(person_mention -> comment (comment_id)); joinable!(person_mention -> comment (comment_id));
joinable!(person_mention -> person (recipient_id)); joinable!(person_mention -> person (recipient_id));
joinable!(comment_reply -> comment (comment_id));
joinable!(comment_reply -> person (recipient_id));
joinable!(post -> community (community_id)); joinable!(post -> community (community_id));
joinable!(post -> person (creator_id)); joinable!(post -> person (creator_id));
joinable!(post_aggregates -> post (post_id)); joinable!(post_aggregates -> post (post_id));
@ -751,6 +748,7 @@ allow_tables_to_appear_in_same_query!(
person_ban, person_ban,
person_block, person_block,
person_mention, person_mention,
comment_reply,
post, post,
post_aggregates, post_aggregates,
post_like, post_like,
@ -760,7 +758,6 @@ allow_tables_to_appear_in_same_query!(
private_message, private_message,
site, site,
site_aggregates, site_aggregates,
comment_alias_1,
person_alias_1, person_alias_1,
person_alias_2, person_alias_2,
admin_purge_comment, admin_purge_comment,

View file

@ -1,15 +1,9 @@
use crate::newtypes::{CommentId, DbUrl, PersonId, PostId}; use crate::newtypes::{CommentId, DbUrl, LtreeDef, PersonId, PostId};
use diesel_ltree::Ltree;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[cfg(feature = "full")] #[cfg(feature = "full")]
use crate::schema::{comment, comment_alias_1, comment_like, comment_saved}; use crate::schema::{comment, comment_like, comment_saved};
// WITH RECURSIVE MyTree AS (
// SELECT * FROM comment WHERE parent_id IS NULL
// UNION ALL
// SELECT m.* FROM comment AS m JOIN MyTree AS t ON m.parent_id = t.id
// )
// SELECT * FROM MyTree;
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)] #[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))] #[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))]
@ -19,34 +13,15 @@ pub struct Comment {
pub id: CommentId, pub id: CommentId,
pub creator_id: PersonId, pub creator_id: PersonId,
pub post_id: PostId, pub post_id: PostId,
pub parent_id: Option<CommentId>,
pub content: String, pub content: String,
pub removed: bool, pub removed: bool,
pub read: bool, // Whether the recipient has read the comment or not
pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub ap_id: DbUrl,
pub local: bool,
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))]
#[cfg_attr(feature = "full", belongs_to(crate::source::post::Post))]
#[cfg_attr(feature = "full", table_name = "comment_alias_1")]
pub struct CommentAlias1 {
pub id: CommentId,
pub creator_id: PersonId,
pub post_id: PostId,
pub parent_id: Option<CommentId>,
pub content: String,
pub removed: bool,
pub read: bool, // Whether the recipient has read the comment or not
pub published: chrono::NaiveDateTime, pub published: chrono::NaiveDateTime,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool, pub deleted: bool,
pub ap_id: DbUrl, pub ap_id: DbUrl,
pub local: bool, pub local: bool,
#[serde(with = "LtreeDef")]
pub path: Ltree,
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -56,9 +31,7 @@ pub struct CommentForm {
pub creator_id: PersonId, pub creator_id: PersonId,
pub post_id: PostId, pub post_id: PostId,
pub content: String, pub content: String,
pub parent_id: Option<CommentId>,
pub removed: Option<bool>, pub removed: Option<bool>,
pub read: Option<bool>,
pub published: Option<chrono::NaiveDateTime>, pub published: Option<chrono::NaiveDateTime>,
pub updated: Option<chrono::NaiveDateTime>, pub updated: Option<chrono::NaiveDateTime>,
pub deleted: Option<bool>, pub deleted: Option<bool>,

View file

@ -0,0 +1,26 @@
use crate::newtypes::{CommentId, CommentReplyId, PersonId};
use serde::{Deserialize, Serialize};
#[cfg(feature = "full")]
use crate::schema::comment_reply;
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Associations, Identifiable))]
#[cfg_attr(feature = "full", belongs_to(crate::source::comment::Comment))]
#[cfg_attr(feature = "full", table_name = "comment_reply")]
/// This table keeps a list of replies to comments and posts.
pub struct CommentReply {
pub id: CommentReplyId,
pub recipient_id: PersonId,
pub comment_id: CommentId,
pub read: bool,
pub published: chrono::NaiveDateTime,
}
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", table_name = "comment_reply")]
pub struct CommentReplyForm {
pub recipient_id: PersonId,
pub comment_id: CommentId,
pub read: Option<bool>,
}

View file

@ -1,6 +1,7 @@
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod activity; pub mod activity;
pub mod comment; pub mod comment;
pub mod comment_reply;
pub mod comment_report; pub mod comment_report;
pub mod community; pub mod community;
pub mod community_block; pub mod community_block;

View file

@ -1,4 +1,4 @@
use crate::newtypes::DbUrl; use crate::{newtypes::DbUrl, CommentSortType, SortType};
use activitypub_federation::{core::object_id::ObjectId, traits::ApubObject}; use activitypub_federation::{core::object_id::ObjectId, traits::ApubObject};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use diesel::{ use diesel::{
@ -118,6 +118,19 @@ pub fn naive_now() -> NaiveDateTime {
chrono::prelude::Utc::now().naive_utc() chrono::prelude::Utc::now().naive_utc()
} }
pub fn post_to_comment_sort_type(sort: SortType) -> CommentSortType {
match sort {
SortType::Active | SortType::Hot => CommentSortType::Hot,
SortType::New | SortType::NewComments | SortType::MostComments => CommentSortType::New,
SortType::Old => CommentSortType::Old,
SortType::TopDay
| SortType::TopAll
| SortType::TopWeek
| SortType::TopYear
| SortType::TopMonth => CommentSortType::Top,
}
}
static EMAIL_REGEX: Lazy<Regex> = Lazy::new(|| { static EMAIL_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$") Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$")
.expect("compile email regex") .expect("compile email regex")

View file

@ -19,6 +19,7 @@ lemmy_db_schema = { version = "=0.16.5", path = "../db_schema" }
diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"], optional = true } diesel = { version = "1.4.8", features = ["postgres","chrono","r2d2","serde_json"], optional = true }
serde = { version = "1.0.136", features = ["derive"] } serde = { version = "1.0.136", features = ["derive"] }
tracing = { version = "0.1.32", optional = true } tracing = { version = "0.1.32", optional = true }
diesel_ltree = "0.2.7"
[dev-dependencies] [dev-dependencies]
serial_test = "0.6.0" serial_test = "0.6.0"

View file

@ -377,7 +377,7 @@ mod tests {
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let inserted_comment = Comment::create(&conn, &comment_form, None).unwrap();
// sara reports // sara reports
let sara_report_form = CommentReportForm { let sara_report_form = CommentReportForm {
@ -472,6 +472,7 @@ mod tests {
upvotes: 0, upvotes: 0,
downvotes: 0, downvotes: 0,
published: agg.published, published: agg.published,
child_count: 0,
}, },
my_vote: None, my_vote: None,
resolver: None, resolver: None,

View file

@ -1,12 +1,12 @@
use crate::structs::CommentView; use crate::structs::CommentView;
use diesel::{dsl::*, result::Error, *}; use diesel::{dsl::*, result::Error, *};
use diesel_ltree::{nlevel, subpath, Ltree, LtreeExtensions};
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::CommentAggregates, aggregates::structs::CommentAggregates,
newtypes::{CommentId, CommunityId, DbUrl, PersonId, PostId}, newtypes::{CommentId, CommunityId, DbUrl, PersonId, PostId},
schema::{ schema::{
comment, comment,
comment_aggregates, comment_aggregates,
comment_alias_1,
comment_like, comment_like,
comment_saved, comment_saved,
community, community,
@ -14,28 +14,25 @@ use lemmy_db_schema::{
community_follower, community_follower,
community_person_ban, community_person_ban,
person, person,
person_alias_1,
person_block, person_block,
post, post,
}, },
source::{ source::{
comment::{Comment, CommentAlias1, CommentSaved}, comment::{Comment, CommentSaved},
community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe}, community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe},
person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1}, person::{Person, PersonSafe},
person_block::PersonBlock, person_block::PersonBlock,
post::Post, post::Post,
}, },
traits::{MaybeOptional, ToSafe, ViewToVec}, traits::{MaybeOptional, ToSafe, ViewToVec},
utils::{functions::hot_rank, fuzzy_search, limit_and_offset_unlimited}, utils::{functions::hot_rank, fuzzy_search, limit_and_offset_unlimited},
CommentSortType,
ListingType, ListingType,
SortType,
}; };
type CommentViewTuple = ( type CommentViewTuple = (
Comment, Comment,
PersonSafe, PersonSafe,
Option<CommentAlias1>,
Option<PersonSafeAlias1>,
Post, Post,
CommunitySafe, CommunitySafe,
CommentAggregates, CommentAggregates,
@ -58,8 +55,6 @@ impl CommentView {
let ( let (
comment, comment,
creator, creator,
_parent_comment,
recipient,
post, post,
community, community,
counts, counts,
@ -71,9 +66,6 @@ impl CommentView {
) = comment::table ) = comment::table
.find(comment_id) .find(comment_id)
.inner_join(person::table) .inner_join(person::table)
// recipient here
.left_join(comment_alias_1::table.on(comment_alias_1::id.nullable().eq(comment::parent_id)))
.left_join(person_alias_1::table.on(person_alias_1::id.eq(comment_alias_1::creator_id)))
.inner_join(post::table) .inner_join(post::table)
.inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(comment_aggregates::table) .inner_join(comment_aggregates::table)
@ -120,8 +112,6 @@ impl CommentView {
.select(( .select((
comment::all_columns, comment::all_columns,
Person::safe_columns_tuple(), Person::safe_columns_tuple(),
comment_alias_1::all_columns.nullable(),
PersonAlias1::safe_columns_tuple().nullable(),
post::all_columns, post::all_columns,
Community::safe_columns_tuple(), Community::safe_columns_tuple(),
comment_aggregates::all_columns, comment_aggregates::all_columns,
@ -143,7 +133,6 @@ impl CommentView {
Ok(CommentView { Ok(CommentView {
comment, comment,
recipient,
post, post,
creator, creator,
community, community,
@ -155,73 +144,24 @@ impl CommentView {
my_vote, my_vote,
}) })
} }
/// Gets the recipient person id.
/// If there is no parent comment, its the post creator
pub fn get_recipient_id(&self) -> PersonId {
match &self.recipient {
Some(parent_commenter) => parent_commenter.id,
None => self.post.creator_id,
}
}
/// Gets the number of unread replies
pub fn get_unread_replies(conn: &PgConnection, my_person_id: PersonId) -> Result<i64, Error> {
use diesel::dsl::*;
comment::table
// recipient here
.left_join(comment_alias_1::table.on(comment_alias_1::id.nullable().eq(comment::parent_id)))
.left_join(person_alias_1::table.on(person_alias_1::id.eq(comment_alias_1::creator_id)))
.inner_join(post::table)
.inner_join(community::table.on(post::community_id.eq(community::id)))
.left_join(
person_block::table.on(
comment::creator_id
.eq(person_block::target_id)
.and(person_block::person_id.eq(my_person_id)),
),
)
.left_join(
community_block::table.on(
community::id
.eq(community_block::community_id)
.and(community_block::person_id.eq(my_person_id)),
),
)
.filter(person_alias_1::id.eq(my_person_id)) // Gets the comment replies
.or_filter(
comment::parent_id
.is_null()
.and(post::creator_id.eq(my_person_id)),
) // Gets the top level replies
.filter(comment::read.eq(false))
.filter(comment::deleted.eq(false))
.filter(comment::removed.eq(false))
// Don't show blocked communities or persons
.filter(community_block::person_id.is_null())
.filter(person_block::person_id.is_null())
.select(count(comment::id))
.first::<i64>(conn)
}
} }
pub struct CommentQueryBuilder<'a> { pub struct CommentQueryBuilder<'a> {
conn: &'a PgConnection, conn: &'a PgConnection,
listing_type: Option<ListingType>, listing_type: Option<ListingType>,
sort: Option<SortType>, sort: Option<CommentSortType>,
community_id: Option<CommunityId>, community_id: Option<CommunityId>,
community_actor_id: Option<DbUrl>, community_actor_id: Option<DbUrl>,
post_id: Option<PostId>, post_id: Option<PostId>,
parent_path: Option<Ltree>,
creator_id: Option<PersonId>, creator_id: Option<PersonId>,
recipient_id: Option<PersonId>,
my_person_id: Option<PersonId>, my_person_id: Option<PersonId>,
search_term: Option<String>, search_term: Option<String>,
saved_only: Option<bool>, saved_only: Option<bool>,
unread_only: Option<bool>,
show_bot_accounts: Option<bool>, show_bot_accounts: Option<bool>,
page: Option<i64>, page: Option<i64>,
limit: Option<i64>, limit: Option<i64>,
max_depth: Option<i32>,
} }
impl<'a> CommentQueryBuilder<'a> { impl<'a> CommentQueryBuilder<'a> {
@ -233,15 +173,15 @@ impl<'a> CommentQueryBuilder<'a> {
community_id: None, community_id: None,
community_actor_id: None, community_actor_id: None,
post_id: None, post_id: None,
parent_path: None,
creator_id: None, creator_id: None,
recipient_id: None,
my_person_id: None, my_person_id: None,
search_term: None, search_term: None,
saved_only: None, saved_only: None,
unread_only: None,
show_bot_accounts: None, show_bot_accounts: None,
page: None, page: None,
limit: None, limit: None,
max_depth: None,
} }
} }
@ -250,7 +190,7 @@ impl<'a> CommentQueryBuilder<'a> {
self self
} }
pub fn sort<T: MaybeOptional<SortType>>(mut self, sort: T) -> Self { pub fn sort<T: MaybeOptional<CommentSortType>>(mut self, sort: T) -> Self {
self.sort = sort.get_optional(); self.sort = sort.get_optional();
self self
} }
@ -265,11 +205,6 @@ impl<'a> CommentQueryBuilder<'a> {
self self
} }
pub fn recipient_id<T: MaybeOptional<PersonId>>(mut self, recipient_id: T) -> Self {
self.recipient_id = recipient_id.get_optional();
self
}
pub fn community_id<T: MaybeOptional<CommunityId>>(mut self, community_id: T) -> Self { pub fn community_id<T: MaybeOptional<CommunityId>>(mut self, community_id: T) -> Self {
self.community_id = community_id.get_optional(); self.community_id = community_id.get_optional();
self self
@ -295,13 +230,13 @@ impl<'a> CommentQueryBuilder<'a> {
self self
} }
pub fn unread_only<T: MaybeOptional<bool>>(mut self, unread_only: T) -> Self { pub fn show_bot_accounts<T: MaybeOptional<bool>>(mut self, show_bot_accounts: T) -> Self {
self.unread_only = unread_only.get_optional(); self.show_bot_accounts = show_bot_accounts.get_optional();
self self
} }
pub fn show_bot_accounts<T: MaybeOptional<bool>>(mut self, show_bot_accounts: T) -> Self { pub fn parent_path<T: MaybeOptional<Ltree>>(mut self, parent_path: T) -> Self {
self.show_bot_accounts = show_bot_accounts.get_optional(); self.parent_path = parent_path.get_optional();
self self
} }
@ -315,6 +250,11 @@ impl<'a> CommentQueryBuilder<'a> {
self self
} }
pub fn max_depth<T: MaybeOptional<i32>>(mut self, max_depth: T) -> Self {
self.max_depth = max_depth.get_optional();
self
}
pub fn list(self) -> Result<Vec<CommentView>, Error> { pub fn list(self) -> Result<Vec<CommentView>, Error> {
use diesel::dsl::*; use diesel::dsl::*;
@ -323,9 +263,6 @@ impl<'a> CommentQueryBuilder<'a> {
let mut query = comment::table let mut query = comment::table
.inner_join(person::table) .inner_join(person::table)
// recipient here
.left_join(comment_alias_1::table.on(comment_alias_1::id.nullable().eq(comment::parent_id)))
.left_join(person_alias_1::table.on(person_alias_1::id.eq(comment_alias_1::creator_id)))
.inner_join(post::table) .inner_join(post::table)
.inner_join(community::table.on(post::community_id.eq(community::id))) .inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(comment_aggregates::table) .inner_join(comment_aggregates::table)
@ -379,8 +316,6 @@ impl<'a> CommentQueryBuilder<'a> {
.select(( .select((
comment::all_columns, comment::all_columns,
Person::safe_columns_tuple(), Person::safe_columns_tuple(),
comment_alias_1::all_columns.nullable(),
PersonAlias1::safe_columns_tuple().nullable(),
post::all_columns, post::all_columns,
Community::safe_columns_tuple(), Community::safe_columns_tuple(),
comment_aggregates::all_columns, comment_aggregates::all_columns,
@ -392,24 +327,6 @@ impl<'a> CommentQueryBuilder<'a> {
)) ))
.into_boxed(); .into_boxed();
// The replies
if let Some(recipient_id) = self.recipient_id {
query = query
// TODO needs lots of testing
.filter(person_alias_1::id.eq(recipient_id)) // Gets the comment replies
.or_filter(
comment::parent_id
.is_null()
.and(post::creator_id.eq(recipient_id)),
) // Gets the top level replies
.filter(comment::deleted.eq(false))
.filter(comment::removed.eq(false));
}
if self.unread_only.unwrap_or(false) {
query = query.filter(comment::read.eq(false));
}
if let Some(creator_id) = self.creator_id { if let Some(creator_id) = self.creator_id {
query = query.filter(comment::creator_id.eq(creator_id)); query = query.filter(comment::creator_id.eq(creator_id));
}; };
@ -418,6 +335,10 @@ impl<'a> CommentQueryBuilder<'a> {
query = query.filter(comment::post_id.eq(post_id)); query = query.filter(comment::post_id.eq(post_id));
}; };
if let Some(parent_path) = self.parent_path.as_ref() {
query = query.filter(comment::path.contained_by(parent_path));
};
if let Some(search_term) = self.search_term { if let Some(search_term) = self.search_term {
query = query.filter(comment::content.ilike(fuzzy_search(&search_term))); query = query.filter(comment::content.ilike(fuzzy_search(&search_term)));
}; };
@ -460,36 +381,47 @@ impl<'a> CommentQueryBuilder<'a> {
query = query.filter(person::bot_account.eq(false)); query = query.filter(person::bot_account.eq(false));
}; };
query = match self.sort.unwrap_or(SortType::New) {
SortType::Hot | SortType::Active => query
.order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc())
.then_order_by(comment_aggregates::published.desc()),
SortType::New | SortType::MostComments | SortType::NewComments => {
query.order_by(comment::published.desc())
}
SortType::TopAll => query.order_by(comment_aggregates::score.desc()),
SortType::TopYear => query
.filter(comment::published.gt(now - 1.years()))
.order_by(comment_aggregates::score.desc()),
SortType::TopMonth => query
.filter(comment::published.gt(now - 1.months()))
.order_by(comment_aggregates::score.desc()),
SortType::TopWeek => query
.filter(comment::published.gt(now - 1.weeks()))
.order_by(comment_aggregates::score.desc()),
SortType::TopDay => query
.filter(comment::published.gt(now - 1.days()))
.order_by(comment_aggregates::score.desc()),
};
// Don't show blocked communities or persons // Don't show blocked communities or persons
if self.my_person_id.is_some() { if self.my_person_id.is_some() {
query = query.filter(community_block::person_id.is_null()); query = query.filter(community_block::person_id.is_null());
query = query.filter(person_block::person_id.is_null()); query = query.filter(person_block::person_id.is_null());
} }
// A Max depth given means its a tree fetch
let (limit, offset) = if let Some(max_depth) = self.max_depth {
let depth_limit = if let Some(parent_path) = self.parent_path.as_ref() {
parent_path.0.split('.').count() as i32 + max_depth
// Add one because of root "0"
} else {
max_depth + 1
};
query = query.filter(nlevel(comment::path).le(depth_limit));
// Always order by the parent path first
query = query.order_by(subpath(comment::path, 0, -1));
// TODO limit question. Limiting does not work for comment threads ATM, only max_depth
// For now, don't do any limiting for tree fetches
// https://stackoverflow.com/questions/72983614/postgres-ltree-how-to-limit-the-max-number-of-children-at-any-given-level
// Don't use the regular error-checking one, many more comments must ofter be fetched. // Don't use the regular error-checking one, many more comments must ofter be fetched.
let (limit, offset) = limit_and_offset_unlimited(self.page, self.limit); // This does not work for comment trees, and the limit should be manually set to a high number
//
// If a max depth is given, then you know its a tree fetch, and limits should be ignored
(i64::MAX, 0)
} else {
limit_and_offset_unlimited(self.page, self.limit)
};
query = match self.sort.unwrap_or(CommentSortType::Hot) {
CommentSortType::Hot => query
.then_order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc())
.then_order_by(comment_aggregates::published.desc()),
CommentSortType::New => query.then_order_by(comment::published.desc()),
CommentSortType::Old => query.then_order_by(comment::published.asc()),
CommentSortType::Top => query.order_by(comment_aggregates::score.desc()),
};
// Note: deleted and removed comments are done on the front side // Note: deleted and removed comments are done on the front side
let res = query let res = query
@ -509,15 +441,14 @@ impl ViewToVec for CommentView {
.map(|a| Self { .map(|a| Self {
comment: a.0.to_owned(), comment: a.0.to_owned(),
creator: a.1.to_owned(), creator: a.1.to_owned(),
recipient: a.3.to_owned(), post: a.2.to_owned(),
post: a.4.to_owned(), community: a.3.to_owned(),
community: a.5.to_owned(), counts: a.4.to_owned(),
counts: a.6.to_owned(), creator_banned_from_community: a.5.is_some(),
creator_banned_from_community: a.7.is_some(), subscribed: CommunityFollower::to_subscribed_type(&a.6),
subscribed: CommunityFollower::to_subscribed_type(&a.8), saved: a.7.is_some(),
saved: a.9.is_some(), creator_blocked: a.8.is_some(),
creator_blocked: a.10.is_some(), my_vote: a.9,
my_vote: a.11,
}) })
.collect::<Vec<Self>>() .collect::<Vec<Self>>()
} }
@ -574,24 +505,72 @@ mod tests {
let inserted_post = Post::create(&conn, &new_post).unwrap(); let inserted_post = Post::create(&conn, &new_post).unwrap();
let comment_form = CommentForm { // Create a comment tree with this hierarchy
content: "A test comment 32".into(), // 0
// \ \
// 1 2
// \
// 3 4
// \
// 5
let comment_form_0 = CommentForm {
content: "Comment 0".into(),
creator_id: inserted_person.id, creator_id: inserted_person.id,
post_id: inserted_post.id, post_id: inserted_post.id,
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_comment = Comment::create(&conn, &comment_form).unwrap(); let inserted_comment_0 = Comment::create(&conn, &comment_form_0, None).unwrap();
let comment_form_2 = CommentForm { let comment_form_1 = CommentForm {
content: "A test blocked comment".into(), content: "Comment 1, A test blocked comment".into(),
creator_id: inserted_person_2.id, creator_id: inserted_person_2.id,
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: Some(inserted_comment.id),
..CommentForm::default() ..CommentForm::default()
}; };
let inserted_comment_2 = Comment::create(&conn, &comment_form_2).unwrap(); let inserted_comment_1 =
Comment::create(&conn, &comment_form_1, Some(&inserted_comment_0.path)).unwrap();
let comment_form_2 = CommentForm {
content: "Comment 2".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
..CommentForm::default()
};
let inserted_comment_2 =
Comment::create(&conn, &comment_form_2, Some(&inserted_comment_0.path)).unwrap();
let comment_form_3 = CommentForm {
content: "Comment 3".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
..CommentForm::default()
};
let _inserted_comment_3 =
Comment::create(&conn, &comment_form_3, Some(&inserted_comment_1.path)).unwrap();
let comment_form_4 = CommentForm {
content: "Comment 4".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
..CommentForm::default()
};
let inserted_comment_4 =
Comment::create(&conn, &comment_form_4, Some(&inserted_comment_1.path)).unwrap();
let comment_form_5 = CommentForm {
content: "Comment 5".into(),
creator_id: inserted_person.id,
post_id: inserted_post.id,
..CommentForm::default()
};
let _inserted_comment_5 =
Comment::create(&conn, &comment_form_5, Some(&inserted_comment_4.path)).unwrap();
let timmy_blocks_sara_form = PersonBlockForm { let timmy_blocks_sara_form = PersonBlockForm {
person_id: inserted_person.id, person_id: inserted_person.id,
@ -610,7 +589,7 @@ mod tests {
assert_eq!(expected_block, inserted_block); assert_eq!(expected_block, inserted_block);
let comment_like_form = CommentLikeForm { let comment_like_form = CommentLikeForm {
comment_id: inserted_comment.id, comment_id: inserted_comment_0.id,
post_id: inserted_post.id, post_id: inserted_post.id,
person_id: inserted_person.id, person_id: inserted_person.id,
score: 1, score: 1,
@ -618,8 +597,9 @@ mod tests {
let _inserted_comment_like = CommentLike::like(&conn, &comment_like_form).unwrap(); let _inserted_comment_like = CommentLike::like(&conn, &comment_like_form).unwrap();
let agg = CommentAggregates::read(&conn, inserted_comment.id).unwrap(); let agg = CommentAggregates::read(&conn, inserted_comment_0.id).unwrap();
let top_path = inserted_comment_0.to_owned().path;
let expected_comment_view_no_person = CommentView { let expected_comment_view_no_person = CommentView {
creator_banned_from_community: false, creator_banned_from_community: false,
my_vote: None, my_vote: None,
@ -627,18 +607,17 @@ mod tests {
saved: false, saved: false,
creator_blocked: false, creator_blocked: false,
comment: Comment { comment: Comment {
id: inserted_comment.id, id: inserted_comment_0.id,
content: "A test comment 32".into(), content: "Comment 0".into(),
creator_id: inserted_person.id, creator_id: inserted_person.id,
post_id: inserted_post.id, post_id: inserted_post.id,
parent_id: None,
removed: false, removed: false,
deleted: false, deleted: false,
read: false, published: inserted_comment_0.published,
published: inserted_comment.published, ap_id: inserted_comment_0.ap_id,
ap_id: inserted_comment.ap_id,
updated: None, updated: None,
local: true, local: true,
path: top_path,
}, },
creator: PersonSafe { creator: PersonSafe {
id: inserted_person.id, id: inserted_person.id,
@ -660,7 +639,6 @@ mod tests {
matrix_user_id: None, matrix_user_id: None,
ban_expires: None, ban_expires: None,
}, },
recipient: None,
post: Post { post: Post {
id: inserted_post.id, id: inserted_post.id,
name: inserted_post.name.to_owned(), name: inserted_post.name.to_owned(),
@ -701,11 +679,12 @@ mod tests {
}, },
counts: CommentAggregates { counts: CommentAggregates {
id: agg.id, id: agg.id,
comment_id: inserted_comment.id, comment_id: inserted_comment_0.id,
score: 1, score: 1,
upvotes: 1, upvotes: 1,
downvotes: 0, downvotes: 0,
published: agg.published, published: agg.published,
child_count: 5,
}, },
}; };
@ -717,38 +696,96 @@ mod tests {
.list() .list()
.unwrap(); .unwrap();
assert_eq!(
expected_comment_view_no_person,
read_comment_views_no_person[0]
);
let read_comment_views_with_person = CommentQueryBuilder::create(&conn) let read_comment_views_with_person = CommentQueryBuilder::create(&conn)
.post_id(inserted_post.id) .post_id(inserted_post.id)
.my_person_id(inserted_person.id) .my_person_id(inserted_person.id)
.list() .list()
.unwrap(); .unwrap();
let read_comment_from_blocked_person =
CommentView::read(&conn, inserted_comment_2.id, Some(inserted_person.id)).unwrap();
let like_removed = CommentLike::remove(&conn, inserted_person.id, inserted_comment.id).unwrap();
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
Comment::delete(&conn, inserted_comment_2.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap();
Person::delete(&conn, inserted_person.id).unwrap();
Person::delete(&conn, inserted_person_2.id).unwrap();
// Make sure its 1, not showing the blocked comment
assert_eq!(1, read_comment_views_with_person.len());
assert_eq!(
expected_comment_view_no_person,
read_comment_views_no_person[1]
);
assert_eq!( assert_eq!(
expected_comment_view_with_person, expected_comment_view_with_person,
read_comment_views_with_person[0] read_comment_views_with_person[0]
); );
// Make sure its 1, not showing the blocked comment
assert_eq!(5, read_comment_views_with_person.len());
let read_comment_from_blocked_person =
CommentView::read(&conn, inserted_comment_1.id, Some(inserted_person.id)).unwrap();
// Make sure block set the creator blocked // Make sure block set the creator blocked
assert!(read_comment_from_blocked_person.creator_blocked); assert!(read_comment_from_blocked_person.creator_blocked);
let top_path = inserted_comment_0.path;
let read_comment_views_top_path = CommentQueryBuilder::create(&conn)
.post_id(inserted_post.id)
.parent_path(top_path)
.list()
.unwrap();
let child_path = inserted_comment_1.to_owned().path;
let read_comment_views_child_path = CommentQueryBuilder::create(&conn)
.post_id(inserted_post.id)
.parent_path(child_path)
.list()
.unwrap();
// Make sure the comment parent-limited fetch is correct
assert_eq!(6, read_comment_views_top_path.len());
assert_eq!(4, read_comment_views_child_path.len());
// Make sure it contains the parent, but not the comment from the other tree
let child_comments = read_comment_views_child_path
.into_iter()
.map(|c| c.comment)
.collect::<Vec<Comment>>();
assert!(child_comments.contains(&inserted_comment_1));
assert!(!child_comments.contains(&inserted_comment_2));
let read_comment_views_top_max_depth = CommentQueryBuilder::create(&conn)
.post_id(inserted_post.id)
.max_depth(1)
.list()
.unwrap();
// Make sure a depth limited one only has the top comment
assert_eq!(
expected_comment_view_no_person,
read_comment_views_top_max_depth[0]
);
assert_eq!(1, read_comment_views_top_max_depth.len());
let child_path = inserted_comment_1.path;
let read_comment_views_parent_max_depth = CommentQueryBuilder::create(&conn)
.post_id(inserted_post.id)
.parent_path(child_path)
.max_depth(1)
.sort(CommentSortType::New)
.list()
.unwrap();
// Make sure a depth limited one, and given child comment 1, has 3
assert!(read_comment_views_parent_max_depth[2]
.comment
.content
.eq("Comment 3"));
assert_eq!(3, read_comment_views_parent_max_depth.len());
// Delete everything
let like_removed =
CommentLike::remove(&conn, inserted_person.id, inserted_comment_0.id).unwrap();
let num_deleted = Comment::delete(&conn, inserted_comment_0.id).unwrap();
Comment::delete(&conn, inserted_comment_1.id).unwrap();
Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap();
Person::delete(&conn, inserted_person.id).unwrap();
Person::delete(&conn, inserted_person_2.id).unwrap();
assert_eq!(1, num_deleted); assert_eq!(1, num_deleted);
assert_eq!(1, like_removed); assert_eq!(1, like_removed);
} }

View file

@ -429,6 +429,7 @@ impl<'a> PostQueryBuilder<'a> {
.then_order_by(hot_rank(post_aggregates::score, post_aggregates::published).desc()) .then_order_by(hot_rank(post_aggregates::score, post_aggregates::published).desc())
.then_order_by(post_aggregates::published.desc()), .then_order_by(post_aggregates::published.desc()),
SortType::New => query.then_order_by(post_aggregates::published.desc()), SortType::New => query.then_order_by(post_aggregates::published.desc()),
SortType::Old => query.then_order_by(post_aggregates::published.asc()),
SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()), SortType::NewComments => query.then_order_by(post_aggregates::newest_comment_time.desc()),
SortType::MostComments => query SortType::MostComments => query
.then_order_by(post_aggregates::comments.desc()) .then_order_by(post_aggregates::comments.desc())

View file

@ -34,7 +34,6 @@ pub struct CommentReportView {
pub struct CommentView { pub struct CommentView {
pub comment: Comment, pub comment: Comment,
pub creator: PersonSafe, pub creator: PersonSafe,
pub recipient: Option<PersonSafeAlias1>, // Left joins to comment and person
pub post: Post, pub post: Post,
pub community: CommunitySafe, pub community: CommunitySafe,
pub counts: CommentAggregates, pub counts: CommentAggregates,

View file

@ -0,0 +1,344 @@
use crate::structs::CommentReplyView;
use diesel::{dsl::*, result::Error, *};
use lemmy_db_schema::{
aggregates::structs::CommentAggregates,
newtypes::{CommentReplyId, PersonId},
schema::{
comment,
comment_aggregates,
comment_like,
comment_reply,
comment_saved,
community,
community_follower,
community_person_ban,
person,
person_alias_1,
person_block,
post,
},
source::{
comment::{Comment, CommentSaved},
comment_reply::CommentReply,
community::{Community, CommunityFollower, CommunityPersonBan, CommunitySafe},
person::{Person, PersonAlias1, PersonSafe, PersonSafeAlias1},
person_block::PersonBlock,
post::Post,
},
traits::{MaybeOptional, ToSafe, ViewToVec},
utils::{functions::hot_rank, limit_and_offset},
CommentSortType,
};
type CommentReplyViewTuple = (
CommentReply,
Comment,
PersonSafe,
Post,
CommunitySafe,
PersonSafeAlias1,
CommentAggregates,
Option<CommunityPersonBan>,
Option<CommunityFollower>,
Option<CommentSaved>,
Option<PersonBlock>,
Option<i16>,
);
impl CommentReplyView {
pub fn read(
conn: &PgConnection,
comment_reply_id: CommentReplyId,
my_person_id: Option<PersonId>,
) -> Result<Self, Error> {
// The left join below will return None in this case
let person_id_join = my_person_id.unwrap_or(PersonId(-1));
let (
comment_reply,
comment,
creator,
post,
community,
recipient,
counts,
creator_banned_from_community,
follower,
saved,
creator_blocked,
my_vote,
) = comment_reply::table
.find(comment_reply_id)
.inner_join(comment::table)
.inner_join(person::table.on(comment::creator_id.eq(person::id)))
.inner_join(post::table.on(comment::post_id.eq(post::id)))
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(person_alias_1::table)
.inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id)))
.left_join(
community_person_ban::table.on(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(comment::creator_id))
.and(
community_person_ban::expires
.is_null()
.or(community_person_ban::expires.gt(now)),
),
),
)
.left_join(
community_follower::table.on(
post::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(person_id_join)),
),
)
.left_join(
comment_saved::table.on(
comment::id
.eq(comment_saved::comment_id)
.and(comment_saved::person_id.eq(person_id_join)),
),
)
.left_join(
person_block::table.on(
comment::creator_id
.eq(person_block::target_id)
.and(person_block::person_id.eq(person_id_join)),
),
)
.left_join(
comment_like::table.on(
comment::id
.eq(comment_like::comment_id)
.and(comment_like::person_id.eq(person_id_join)),
),
)
.select((
comment_reply::all_columns,
comment::all_columns,
Person::safe_columns_tuple(),
post::all_columns,
Community::safe_columns_tuple(),
PersonAlias1::safe_columns_tuple(),
comment_aggregates::all_columns,
community_person_ban::all_columns.nullable(),
community_follower::all_columns.nullable(),
comment_saved::all_columns.nullable(),
person_block::all_columns.nullable(),
comment_like::score.nullable(),
))
.first::<CommentReplyViewTuple>(conn)?;
Ok(CommentReplyView {
comment_reply,
comment,
creator,
post,
community,
recipient,
counts,
creator_banned_from_community: creator_banned_from_community.is_some(),
subscribed: CommunityFollower::to_subscribed_type(&follower),
saved: saved.is_some(),
creator_blocked: creator_blocked.is_some(),
my_vote,
})
}
/// Gets the number of unread replies
pub fn get_unread_replies(conn: &PgConnection, my_person_id: PersonId) -> Result<i64, Error> {
use diesel::dsl::*;
comment_reply::table
.filter(comment_reply::recipient_id.eq(my_person_id))
.filter(comment_reply::read.eq(false))
.select(count(comment_reply::id))
.first::<i64>(conn)
}
}
pub struct CommentReplyQueryBuilder<'a> {
conn: &'a PgConnection,
my_person_id: Option<PersonId>,
recipient_id: Option<PersonId>,
sort: Option<CommentSortType>,
unread_only: Option<bool>,
show_bot_accounts: Option<bool>,
page: Option<i64>,
limit: Option<i64>,
}
impl<'a> CommentReplyQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self {
CommentReplyQueryBuilder {
conn,
my_person_id: None,
recipient_id: None,
sort: None,
unread_only: None,
show_bot_accounts: None,
page: None,
limit: None,
}
}
pub fn sort<T: MaybeOptional<CommentSortType>>(mut self, sort: T) -> Self {
self.sort = sort.get_optional();
self
}
pub fn unread_only<T: MaybeOptional<bool>>(mut self, unread_only: T) -> Self {
self.unread_only = unread_only.get_optional();
self
}
pub fn show_bot_accounts<T: MaybeOptional<bool>>(mut self, show_bot_accounts: T) -> Self {
self.show_bot_accounts = show_bot_accounts.get_optional();
self
}
pub fn recipient_id<T: MaybeOptional<PersonId>>(mut self, recipient_id: T) -> Self {
self.recipient_id = recipient_id.get_optional();
self
}
pub fn my_person_id<T: MaybeOptional<PersonId>>(mut self, my_person_id: T) -> Self {
self.my_person_id = my_person_id.get_optional();
self
}
pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
self.page = page.get_optional();
self
}
pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
self.limit = limit.get_optional();
self
}
pub fn list(self) -> Result<Vec<CommentReplyView>, Error> {
use diesel::dsl::*;
// The left join below will return None in this case
let person_id_join = self.my_person_id.unwrap_or(PersonId(-1));
let mut query = comment_reply::table
.inner_join(comment::table)
.inner_join(person::table.on(comment::creator_id.eq(person::id)))
.inner_join(post::table.on(comment::post_id.eq(post::id)))
.inner_join(community::table.on(post::community_id.eq(community::id)))
.inner_join(person_alias_1::table)
.inner_join(comment_aggregates::table.on(comment::id.eq(comment_aggregates::comment_id)))
.left_join(
community_person_ban::table.on(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(comment::creator_id))
.and(
community_person_ban::expires
.is_null()
.or(community_person_ban::expires.gt(now)),
),
),
)
.left_join(
community_follower::table.on(
post::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(person_id_join)),
),
)
.left_join(
comment_saved::table.on(
comment::id
.eq(comment_saved::comment_id)
.and(comment_saved::person_id.eq(person_id_join)),
),
)
.left_join(
person_block::table.on(
comment::creator_id
.eq(person_block::target_id)
.and(person_block::person_id.eq(person_id_join)),
),
)
.left_join(
comment_like::table.on(
comment::id
.eq(comment_like::comment_id)
.and(comment_like::person_id.eq(person_id_join)),
),
)
.select((
comment_reply::all_columns,
comment::all_columns,
Person::safe_columns_tuple(),
post::all_columns,
Community::safe_columns_tuple(),
PersonAlias1::safe_columns_tuple(),
comment_aggregates::all_columns,
community_person_ban::all_columns.nullable(),
community_follower::all_columns.nullable(),
comment_saved::all_columns.nullable(),
person_block::all_columns.nullable(),
comment_like::score.nullable(),
))
.into_boxed();
if let Some(recipient_id) = self.recipient_id {
query = query.filter(comment_reply::recipient_id.eq(recipient_id));
}
if self.unread_only.unwrap_or(false) {
query = query.filter(comment_reply::read.eq(false));
}
if !self.show_bot_accounts.unwrap_or(true) {
query = query.filter(person::bot_account.eq(false));
};
query = match self.sort.unwrap_or(CommentSortType::Hot) {
CommentSortType::Hot => query
.then_order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc())
.then_order_by(comment_aggregates::published.desc()),
CommentSortType::New => query.then_order_by(comment::published.desc()),
CommentSortType::Old => query.then_order_by(comment::published.asc()),
CommentSortType::Top => query.order_by(comment_aggregates::score.desc()),
};
let (limit, offset) = limit_and_offset(self.page, self.limit)?;
let res = query
.limit(limit)
.offset(offset)
.load::<CommentReplyViewTuple>(self.conn)?;
Ok(CommentReplyView::from_tuple_to_vec(res))
}
}
impl ViewToVec for CommentReplyView {
type DbTuple = CommentReplyViewTuple;
fn from_tuple_to_vec(items: Vec<Self::DbTuple>) -> Vec<Self> {
items
.into_iter()
.map(|a| Self {
comment_reply: a.0,
comment: a.1,
creator: a.2,
post: a.3,
community: a.4,
recipient: a.5,
counts: a.6,
creator_banned_from_community: a.7.is_some(),
subscribed: CommunityFollower::to_subscribed_type(&a.8),
saved: a.9.is_some(),
creator_blocked: a.10.is_some(),
my_vote: a.11,
})
.collect::<Vec<Self>>()
}
}

View file

@ -1,4 +1,6 @@
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod comment_reply_view;
#[cfg(feature = "full")]
pub mod community_block_view; pub mod community_block_view;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod community_follower_view; pub mod community_follower_view;

View file

@ -27,7 +27,7 @@ use lemmy_db_schema::{
}, },
traits::{MaybeOptional, ToSafe, ViewToVec}, traits::{MaybeOptional, ToSafe, ViewToVec},
utils::{functions::hot_rank, limit_and_offset}, utils::{functions::hot_rank, limit_and_offset},
SortType, CommentSortType,
}; };
type PersonMentionViewTuple = ( type PersonMentionViewTuple = (
@ -163,8 +163,9 @@ pub struct PersonMentionQueryBuilder<'a> {
conn: &'a PgConnection, conn: &'a PgConnection,
my_person_id: Option<PersonId>, my_person_id: Option<PersonId>,
recipient_id: Option<PersonId>, recipient_id: Option<PersonId>,
sort: Option<SortType>, sort: Option<CommentSortType>,
unread_only: Option<bool>, unread_only: Option<bool>,
show_bot_accounts: Option<bool>,
page: Option<i64>, page: Option<i64>,
limit: Option<i64>, limit: Option<i64>,
} }
@ -177,12 +178,13 @@ impl<'a> PersonMentionQueryBuilder<'a> {
recipient_id: None, recipient_id: None,
sort: None, sort: None,
unread_only: None, unread_only: None,
show_bot_accounts: None,
page: None, page: None,
limit: None, limit: None,
} }
} }
pub fn sort<T: MaybeOptional<SortType>>(mut self, sort: T) -> Self { pub fn sort<T: MaybeOptional<CommentSortType>>(mut self, sort: T) -> Self {
self.sort = sort.get_optional(); self.sort = sort.get_optional();
self self
} }
@ -192,6 +194,11 @@ impl<'a> PersonMentionQueryBuilder<'a> {
self self
} }
pub fn show_bot_accounts<T: MaybeOptional<bool>>(mut self, show_bot_accounts: T) -> Self {
self.show_bot_accounts = show_bot_accounts.get_optional();
self
}
pub fn recipient_id<T: MaybeOptional<PersonId>>(mut self, recipient_id: T) -> Self { pub fn recipient_id<T: MaybeOptional<PersonId>>(mut self, recipient_id: T) -> Self {
self.recipient_id = recipient_id.get_optional(); self.recipient_id = recipient_id.get_optional();
self self
@ -289,26 +296,17 @@ impl<'a> PersonMentionQueryBuilder<'a> {
query = query.filter(person_mention::read.eq(false)); query = query.filter(person_mention::read.eq(false));
} }
query = match self.sort.unwrap_or(SortType::Hot) { if !self.show_bot_accounts.unwrap_or(true) {
SortType::Hot | SortType::Active => query query = query.filter(person::bot_account.eq(false));
.order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc()) };
query = match self.sort.unwrap_or(CommentSortType::Hot) {
CommentSortType::Hot => query
.then_order_by(hot_rank(comment_aggregates::score, comment_aggregates::published).desc())
.then_order_by(comment_aggregates::published.desc()), .then_order_by(comment_aggregates::published.desc()),
SortType::New | SortType::MostComments | SortType::NewComments => { CommentSortType::New => query.then_order_by(comment::published.desc()),
query.order_by(comment::published.desc()) CommentSortType::Old => query.then_order_by(comment::published.asc()),
} CommentSortType::Top => query.order_by(comment_aggregates::score.desc()),
SortType::TopAll => query.order_by(comment_aggregates::score.desc()),
SortType::TopYear => query
.filter(comment::published.gt(now - 1.years()))
.order_by(comment_aggregates::score.desc()),
SortType::TopMonth => query
.filter(comment::published.gt(now - 1.months()))
.order_by(comment_aggregates::score.desc()),
SortType::TopWeek => query
.filter(comment::published.gt(now - 1.weeks()))
.order_by(comment_aggregates::score.desc()),
SortType::TopDay => query
.filter(comment::published.gt(now - 1.days()))
.order_by(comment_aggregates::score.desc()),
}; };
let (limit, offset) = limit_and_offset(self.page, self.limit)?; let (limit, offset) = limit_and_offset(self.page, self.limit)?;

View file

@ -109,6 +109,7 @@ impl<'a> PersonQueryBuilder<'a> {
SortType::New | SortType::MostComments | SortType::NewComments => { SortType::New | SortType::MostComments | SortType::NewComments => {
query.order_by(person::published.desc()) query.order_by(person::published.desc())
} }
SortType::Old => query.order_by(person::published.asc()),
SortType::TopAll => query.order_by(person_aggregates::comment_score.desc()), SortType::TopAll => query.order_by(person_aggregates::comment_score.desc()),
SortType::TopYear => query SortType::TopYear => query
.filter(person::published.gt(now - 1.years())) .filter(person::published.gt(now - 1.years()))

View file

@ -2,6 +2,7 @@ use lemmy_db_schema::{
aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates}, aggregates::structs::{CommentAggregates, CommunityAggregates, PersonAggregates},
source::{ source::{
comment::Comment, comment::Comment,
comment_reply::CommentReply,
community::CommunitySafe, community::CommunitySafe,
person::{PersonSafe, PersonSafeAlias1}, person::{PersonSafe, PersonSafeAlias1},
person_mention::PersonMention, person_mention::PersonMention,
@ -65,6 +66,22 @@ pub struct PersonMentionView {
pub my_vote: Option<i16>, // Left join to CommentLike pub my_vote: Option<i16>, // Left join to CommentLike
} }
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
pub struct CommentReplyView {
pub comment_reply: CommentReply,
pub comment: Comment,
pub creator: PersonSafe,
pub post: Post,
pub community: CommunitySafe,
pub recipient: PersonSafeAlias1,
pub counts: CommentAggregates,
pub creator_banned_from_community: bool, // Left Join to CommunityPersonBan
pub subscribed: SubscribedType, // Left join to CommunityFollower
pub saved: bool, // Left join to CommentSaved
pub creator_blocked: bool, // Left join to PersonBlock
pub my_vote: Option<i16>, // Left join to CommentLike
}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct PersonViewSafe { pub struct PersonViewSafe {
pub person: PersonSafe, pub person: PersonSafe,

View file

@ -7,17 +7,18 @@ use lemmy_db_schema::{
newtypes::LocalUserId, newtypes::LocalUserId,
source::{community::Community, local_user::LocalUser, person::Person}, source::{community::Community, local_user::LocalUser, person::Person},
traits::{ApubActor, Crud}, traits::{ApubActor, Crud},
CommentSortType,
ListingType, ListingType,
SortType, SortType,
}; };
use lemmy_db_views::{ use lemmy_db_views::{
comment_view::CommentQueryBuilder,
post_view::PostQueryBuilder, post_view::PostQueryBuilder,
structs::{CommentView, PostView, SiteView}, structs::{PostView, SiteView},
}; };
use lemmy_db_views_actor::{ use lemmy_db_views_actor::{
comment_reply_view::CommentReplyQueryBuilder,
person_mention_view::PersonMentionQueryBuilder, person_mention_view::PersonMentionQueryBuilder,
structs::PersonMentionView, structs::{CommentReplyView, PersonMentionView},
}; };
use lemmy_utils::{claims::Claims, error::LemmyError, utils::markdown_to_html}; use lemmy_utils::{claims::Claims, error::LemmyError, utils::markdown_to_html};
use lemmy_websocket::LemmyContext; use lemmy_websocket::LemmyContext;
@ -284,9 +285,9 @@ fn get_feed_inbox(
let person_id = local_user.person_id; let person_id = local_user.person_id;
let show_bot_accounts = local_user.show_bot_accounts; let show_bot_accounts = local_user.show_bot_accounts;
let sort = SortType::New; let sort = CommentSortType::New;
let replies = CommentQueryBuilder::create(conn) let replies = CommentReplyQueryBuilder::create(conn)
.recipient_id(person_id) .recipient_id(person_id)
.my_person_id(person_id) .my_person_id(person_id)
.show_bot_accounts(show_bot_accounts) .show_bot_accounts(show_bot_accounts)
@ -297,6 +298,7 @@ fn get_feed_inbox(
let mentions = PersonMentionQueryBuilder::create(conn) let mentions = PersonMentionQueryBuilder::create(conn)
.recipient_id(person_id) .recipient_id(person_id)
.my_person_id(person_id) .my_person_id(person_id)
.show_bot_accounts(show_bot_accounts)
.sort(sort) .sort(sort)
.limit(RSS_FETCH_LIMIT) .limit(RSS_FETCH_LIMIT)
.list()?; .list()?;
@ -319,7 +321,7 @@ fn get_feed_inbox(
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
fn create_reply_and_mention_items( fn create_reply_and_mention_items(
replies: Vec<CommentView>, replies: Vec<CommentReplyView>,
mentions: Vec<PersonMentionView>, mentions: Vec<PersonMentionView>,
protocol_and_hostname: &str, protocol_and_hostname: &str,
) -> Result<Vec<Item>, LemmyError> { ) -> Result<Vec<Item>, LemmyError> {

View file

@ -440,7 +440,7 @@ impl ChatServer {
fn sendit(&self, message: &str, id: ConnectionId) { fn sendit(&self, message: &str, id: ConnectionId) {
if let Some(info) = self.sessions.get(&id) { if let Some(info) = self.sessions.get(&id) {
let _ = info.addr.do_send(WsMessage(message.to_owned())); info.addr.do_send(WsMessage(message.to_owned()));
} }
} }

View file

@ -95,7 +95,6 @@ where
pub enum UserOperation { pub enum UserOperation {
Login, Login,
GetCaptcha, GetCaptcha,
MarkCommentAsRead,
SaveComment, SaveComment,
CreateCommentLike, CreateCommentLike,
CreateCommentReport, CreateCommentReport,
@ -116,6 +115,7 @@ pub enum UserOperation {
GetReplies, GetReplies,
GetPersonMentions, GetPersonMentions,
MarkPersonMentionAsRead, MarkPersonMentionAsRead,
MarkCommentReplyAsRead,
GetModlog, GetModlog,
BanFromCommunity, BanFromCommunity,
AddModToCommunity, AddModToCommunity,

View file

@ -14,6 +14,7 @@ use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId, PrivateMessageId}, newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId, PrivateMessageId},
source::{ source::{
comment::Comment, comment::Comment,
comment_reply::{CommentReply, CommentReplyForm},
person::Person, person::Person,
person_mention::{PersonMention, PersonMentionForm}, person_mention::{PersonMention, PersonMentionForm},
post::Post, post::Post,
@ -223,12 +224,13 @@ pub async fn send_local_notifs(
} }
} }
// Send notifs to the parent commenter / poster // Send comment_reply to the parent commenter / poster
match comment.parent_id { if let Some(parent_comment_id) = comment.parent_comment_id() {
Some(parent_id) => { let parent_comment = blocking(context.pool(), move |conn| {
let parent_comment = Comment::read(conn, parent_comment_id)
blocking(context.pool(), move |conn| Comment::read(conn, parent_id)).await?; })
if let Ok(parent_comment) = parent_comment { .await??;
// Get the parent commenter local_user // Get the parent commenter local_user
let parent_creator_id = parent_comment.creator_id; let parent_creator_id = parent_comment.creator_id;
@ -246,6 +248,20 @@ pub async fn send_local_notifs(
if let Ok(parent_user_view) = user_view { if let Ok(parent_user_view) = user_view {
recipient_ids.push(parent_user_view.local_user.id); recipient_ids.push(parent_user_view.local_user.id);
let comment_reply_form = CommentReplyForm {
recipient_id: parent_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
blocking(context.pool(), move |conn| {
CommentReply::create(conn, &comment_reply_form)
})
.await?
.ok();
if do_send_email { if do_send_email {
let lang = get_user_lang(&parent_user_view); let lang = get_user_lang(&parent_user_view);
send_email_to_user( send_email_to_user(
@ -257,11 +273,8 @@ pub async fn send_local_notifs(
} }
} }
} }
} } else {
} // If there's no parent, its the post creator
// Its a post
// Don't send a notif to yourself
None => {
// Only add to recipients if that person isn't blocked // Only add to recipients if that person isn't blocked
let creator_blocked = check_person_block(person.id, post.creator_id, context.pool()) let creator_blocked = check_person_block(person.id, post.creator_id, context.pool())
.await .await
@ -276,6 +289,20 @@ pub async fn send_local_notifs(
if let Ok(parent_user_view) = parent_user { if let Ok(parent_user_view) = parent_user {
recipient_ids.push(parent_user_view.local_user.id); recipient_ids.push(parent_user_view.local_user.id);
let comment_reply_form = CommentReplyForm {
recipient_id: parent_user_view.person.id,
comment_id: comment.id,
read: None,
};
// Allow this to fail softly, since comment edits might re-update or replace it
// Let the uniqueness handle this fail
blocking(context.pool(), move |conn| {
CommentReply::create(conn, &comment_reply_form)
})
.await?
.ok();
if do_send_email { if do_send_email {
let lang = get_user_lang(&parent_user_view); let lang = get_user_lang(&parent_user_view);
send_email_to_user( send_email_to_user(
@ -288,6 +315,6 @@ pub async fn send_local_notifs(
} }
} }
} }
};
Ok(recipient_ids) Ok(recipient_ids)
} }

View file

@ -39,7 +39,7 @@ services:
- lemmy - lemmy
postgres: postgres:
image: postgres:12-alpine image: postgres:14-alpine
ports: ports:
# use a different port so it doesnt conflict with postgres running on the host # use a different port so it doesnt conflict with postgres running on the host
- "5433:5432" - "5433:5432"

View file

@ -47,7 +47,7 @@ services:
ports: ports:
- "8541:8541" - "8541:8541"
postgres_alpha: postgres_alpha:
image: postgres:12-alpine image: postgres:14-alpine
environment: environment:
- POSTGRES_USER=lemmy - POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password - POSTGRES_PASSWORD=password
@ -75,7 +75,7 @@ services:
ports: ports:
- "8551:8551" - "8551:8551"
postgres_beta: postgres_beta:
image: postgres:12-alpine image: postgres:14-alpine
environment: environment:
- POSTGRES_USER=lemmy - POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password - POSTGRES_PASSWORD=password
@ -103,7 +103,7 @@ services:
ports: ports:
- "8561:8561" - "8561:8561"
postgres_gamma: postgres_gamma:
image: postgres:12-alpine image: postgres:14-alpine
environment: environment:
- POSTGRES_USER=lemmy - POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password - POSTGRES_PASSWORD=password
@ -132,7 +132,7 @@ services:
ports: ports:
- "8571:8571" - "8571:8571"
postgres_delta: postgres_delta:
image: postgres:12-alpine image: postgres:14-alpine
environment: environment:
- POSTGRES_USER=lemmy - POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password - POSTGRES_PASSWORD=password
@ -161,7 +161,7 @@ services:
ports: ports:
- "8581:8581" - "8581:8581"
postgres_epsilon: postgres_epsilon:
image: postgres:12-alpine image: postgres:14-alpine
environment: environment:
- POSTGRES_USER=lemmy - POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password - POSTGRES_PASSWORD=password

View file

@ -2,7 +2,7 @@ version: '2.2'
services: services:
postgres: postgres:
image: postgres:12-alpine image: postgres:14-alpine
environment: environment:
- POSTGRES_USER=lemmy - POSTGRES_USER=lemmy
- POSTGRES_PASSWORD=password - POSTGRES_PASSWORD=password

View file

@ -0,0 +1,25 @@
alter table comment add column parent_id integer;
-- Constraints and index
alter table comment add constraint comment_parent_id_fkey foreign key (parent_id) REFERENCES comment(id) ON UPDATE CASCADE ON DELETE CASCADE;
create index idx_comment_parent on comment (parent_id);
-- Update the parent_id column
-- subpath(subpath(0, -1), -1) gets the immediate parent but it fails null checks
update comment set parent_id = cast(ltree2text(nullif(subpath(nullif(subpath(path, 0, -1), '0'), -1), '0')) as INTEGER);
alter table comment drop column path;
alter table comment_aggregates drop column child_count;
drop extension ltree;
-- Add back in the read column
alter table comment add column read boolean default false not null;
update comment c set read = cr.read
from comment_reply cr where cr.comment_id = c.id;
create view comment_alias_1 as select * from comment;
drop table comment_reply;

View file

@ -0,0 +1,83 @@
-- Remove the comment.read column, and create a new comment_reply table,
-- similar to the person_mention table.
--
-- This is necessary because self-joins using ltrees would be too tough with SQL views
--
-- Every comment should have a row here, because all comments have a recipient,
-- either the post creator, or the parent commenter.
create table comment_reply(
id serial primary key,
recipient_id int references person on update cascade on delete cascade not null,
comment_id int references comment on update cascade on delete cascade not null,
read boolean default false not null,
published timestamp not null default now(),
unique(recipient_id, comment_id)
);
-- Ones where parent_id is null, use the post creator recipient
insert into comment_reply (recipient_id, comment_id, read)
select p.creator_id, c.id, c.read from comment c
inner join post p on c.post_id = p.id
where c.parent_id is null;
-- Ones where there is a parent_id, self join to comment to get the parent comment creator
insert into comment_reply (recipient_id, comment_id, read)
select c2.creator_id, c.id, c.read from comment c
inner join comment c2 on c.parent_id = c2.id;
-- Drop comment_alias view
drop view comment_alias_1;
alter table comment drop column read;
create extension ltree;
alter table comment add column path ltree not null default '0';
alter table comment_aggregates add column child_count integer not null default 0;
-- The ltree path column should be the comment_id parent paths, separated by dots.
-- Stackoverflow: building an ltree from a parent_id hierarchical tree:
-- https://stackoverflow.com/a/1144848/1655478
create temporary table comment_temp as
WITH RECURSIVE q AS (
SELECT h, 1 AS level, ARRAY[id] AS breadcrumb
FROM comment h
WHERE parent_id is null
UNION ALL
SELECT hi, q.level + 1 AS level, breadcrumb || id
FROM q
JOIN comment hi
ON hi.parent_id = (q.h).id
)
SELECT (q.h).id,
(q.h).parent_id,
level,
breadcrumb::VARCHAR AS path,
text2ltree('0.' || array_to_string(breadcrumb, '.')) as ltree_path
FROM q
ORDER BY
breadcrumb;
-- Add the ltree column
update comment c
set path = ct.ltree_path
from comment_temp ct
where c.id = ct.id;
-- Update the child counts
update comment_aggregates ca set child_count = c2.child_count
from (
select c.id, c.path, count(c2.id) as child_count from comment c
left join comment c2 on c2.path <@ c.path and c2.path != c.path
group by c.id
) as c2
where ca.comment_id = c2.id;
-- Create the index
create index idx_path_gist on comment using gist (path);
-- Drop the parent_id column
alter table comment drop column parent_id cascade;

View file

@ -0,0 +1,54 @@
##!/bin/sh
set -e
## This script upgrades the postgres from version 12 to 14
## Make sure everything is started
sudo docker-compose start
# Export the DB
echo "Exporting the Database to 12_14.dump.sql ..."
sudo docker-compose exec -T postgres pg_dumpall -c -U lemmy > 12_14_dump.sql
echo "Done."
# Stop everything
sudo docker-compose stop
sleep 10s
# Delete the folder
echo "Removing the old postgres folder"
sudo rm -rf volumes/postgres
# Change the version in your docker-compose.yml
echo "Updating docker-compose to use postgres version 14."
sed -i "s/postgres:12-alpine/postgres:14-alpine/" ./docker-compose.yml
# Start up postgres
echo "Starting up new postgres..."
sudo docker-compose up -d postgres
# Sleep for a bit so it can start up, build the new folders
sleep 20s
# Import the DB
echo "Importing the database...."
cat 12_14_dump.sql | sudo docker-compose exec -T postgres psql -U lemmy
echo "Done."
POSTGRES_PASSWORD=$(grep "POSTGRES_PASSWORD" ./docker-compose.yml | cut -d"=" -f2)
# Fix weird password issue with postgres 14
echo "Fixing a weird password issue with postgres 14"
sudo docker-compose exec -T postgres psql -U lemmy -c "alter user lemmy with password '$POSTGRES_PASSWORD'"
sudo docker-compose restart postgres
# Just in case
sudo chown -R 991:991 volumes/pictrs
# Start up the rest of lemmy
echo "Starting up lemmy"
sudo docker-compose up -d
# Delete the DB Dump? Probably safe to keep it
echo "A copy of your old database is at 12_14.dump.sql . You can delete this file if the upgrade went smoothly."

View file

@ -10,3 +10,4 @@ export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
export LEMMY_CONFIG_LOCATION=../../config/config.hjson export LEMMY_CONFIG_LOCATION=../../config/config.hjson
RUST_BACKTRACE=1 \ RUST_BACKTRACE=1 \
cargo test --workspace --no-fail-fast cargo test --workspace --no-fail-fast
# Add this to do printlns: -- --nocapture

View file

@ -119,7 +119,7 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route("/remove", web::post().to(route_post_crud::<RemoveComment>)) .route("/remove", web::post().to(route_post_crud::<RemoveComment>))
.route( .route(
"/mark_as_read", "/mark_as_read",
web::post().to(route_post::<MarkCommentAsRead>), web::post().to(route_post::<MarkCommentReplyAsRead>),
) )
.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>))