Merge remote-tracking branch 'origin/main' into remove_success_responses

This commit is contained in:
Dessalines 2024-10-28 10:40:35 -04:00
commit f921494cb3
115 changed files with 3023 additions and 2394 deletions

View file

@ -42,14 +42,14 @@ steps:
- event: [pull_request, tag]
prettier_check:
image: tmknom/prettier:3.0.0
image: tmknom/prettier:3.2.5
commands:
- prettier -c . '!**/volumes' '!**/dist' '!target' '!**/translations' '!api_tests/pnpm-lock.yaml'
when:
- event: pull_request
toml_fmt:
image: tamasfe/taplo:0.8.1
image: tamasfe/taplo:0.9.3
commands:
- taplo format --check
when:

176
Cargo.lock generated
View file

@ -42,7 +42,7 @@ dependencies = [
"pin-project-lite",
"rand",
"regex",
"reqwest 0.12.7",
"reqwest 0.12.8",
"reqwest-middleware",
"rsa",
"serde",
@ -298,9 +298,9 @@ dependencies = [
[[package]]
name = "actix-web-prom"
version = "0.8.0"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76743e67d4e7efa9fc2ac7123de0dd7b2ca592668e19334f1d81a3b077afc6ac"
checksum = "56a34f1825c3ae06567a9d632466809bbf34963c86002e8921b64f32d48d289d"
dependencies = [
"actix-web",
"futures-core",
@ -839,9 +839,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.17"
version = "4.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
checksum = "7be5744db7978a28d9df86a214130d106a89ce49644cbc4e3f0c22c3fba30615"
dependencies = [
"clap_builder",
"clap_derive",
@ -849,9 +849,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.17"
version = "4.5.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
checksum = "a5fbc17d3ef8278f55b282b2a2e75ae6f6c7d4bb70ed3d0382375104bfafdb4b"
dependencies = [
"anstream",
"anstyle",
@ -861,9 +861,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.13"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -1645,9 +1645,9 @@ dependencies = [
[[package]]
name = "futures"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
@ -1660,9 +1660,9 @@ dependencies = [
[[package]]
name = "futures-channel"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
@ -1670,15 +1670,15 @@ dependencies = [
[[package]]
name = "futures-core"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
@ -1687,15 +1687,15 @@ dependencies = [
[[package]]
name = "futures-io"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
@ -1704,21 +1704,21 @@ dependencies = [
[[package]]
name = "futures-sink"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.30"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
@ -1875,9 +1875,9 @@ dependencies = [
[[package]]
name = "html2text"
version = "0.12.5"
version = "0.12.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c66ee488a63a92237d5b48875b7e05bb293be8fb2894641c8118b60c08ab5ef"
checksum = "042a9677c258ac2952dd026bb0cd21972f00f644a5a38f5a215cb22cdaf6834e"
dependencies = [
"html5ever 0.27.0",
"markup5ever 0.12.1",
@ -1989,7 +1989,7 @@ dependencies = [
"base64 0.22.1",
"http-signature-normalization",
"httpdate",
"reqwest 0.12.7",
"reqwest 0.12.8",
"reqwest-middleware",
"sha2",
"thiserror",
@ -2075,7 +2075,7 @@ dependencies = [
"http 1.1.0",
"hyper 1.4.1",
"hyper-util",
"rustls 0.23.13",
"rustls 0.23.14",
"rustls-pki-types",
"tokio",
"tokio-rustls 0.26.0",
@ -2514,7 +2514,7 @@ dependencies = [
"moka",
"pretty_assertions",
"regex",
"reqwest 0.12.7",
"reqwest 0.12.8",
"reqwest-middleware",
"rosetta-i18n",
"serde",
@ -2579,7 +2579,7 @@ dependencies = [
"lemmy_utils",
"moka",
"pretty_assertions",
"reqwest 0.12.7",
"reqwest 0.12.8",
"serde",
"serde_json",
"serde_with",
@ -2630,7 +2630,7 @@ dependencies = [
"moka",
"pretty_assertions",
"regex",
"rustls 0.23.13",
"rustls 0.23.14",
"serde",
"serde_json",
"serde_with",
@ -2718,7 +2718,7 @@ dependencies = [
"lemmy_utils",
"mockall",
"moka",
"reqwest 0.12.7",
"reqwest 0.12.8",
"serde_json",
"serial_test",
"test-context",
@ -2745,7 +2745,7 @@ dependencies = [
"lemmy_db_views",
"lemmy_db_views_actor",
"lemmy_utils",
"reqwest 0.12.7",
"reqwest 0.12.8",
"reqwest-middleware",
"rss",
"serde",
@ -2778,10 +2778,10 @@ dependencies = [
"lemmy_utils",
"pretty_assertions",
"prometheus",
"reqwest 0.12.7",
"reqwest 0.12.8",
"reqwest-middleware",
"reqwest-tracing",
"rustls 0.23.13",
"rustls 0.23.14",
"serde_json",
"serial_test",
"tokio",
@ -2809,9 +2809,13 @@ dependencies = [
"itertools 0.13.0",
"lettre",
"markdown-it",
"markdown-it-block-spoiler",
"markdown-it-ruby",
"markdown-it-sub",
"markdown-it-sup",
"pretty_assertions",
"regex",
"reqwest 0.12.7",
"reqwest 0.12.8",
"reqwest-middleware",
"rosetta-build",
"rosetta-i18n",
@ -2847,7 +2851,7 @@ dependencies = [
"nom",
"percent-encoding",
"quoted_printable",
"rustls 0.23.13",
"rustls 0.23.14",
"rustls-pemfile 2.1.3",
"rustls-pki-types",
"socket2",
@ -2980,6 +2984,44 @@ dependencies = [
"unicode-general-category",
]
[[package]]
name = "markdown-it-block-spoiler"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008a8e4184fd08b5dca0f2b5b2ef8f126c1e83ca797c44ee41f8d7765951360c"
dependencies = [
"itertools 0.13.0",
"markdown-it",
]
[[package]]
name = "markdown-it-ruby"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3505f4ada7c372e7f5eb4b07850bf5921193bc0bd43cb18991233999c9134d4"
dependencies = [
"itertools 0.13.0",
"markdown-it",
]
[[package]]
name = "markdown-it-sub"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8abe3aa8927af2314644b3aae37393241a229e869ff9c95ac640749e08357d2a"
dependencies = [
"markdown-it",
]
[[package]]
name = "markdown-it-sup"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ae949e78c7a615f88a47019d51b65962bfc5c4cbc65fa81eae8b9b2506d1cb1"
dependencies = [
"markdown-it",
]
[[package]]
name = "markup5ever"
version = "0.11.0"
@ -3762,7 +3804,7 @@ dependencies = [
"quinn-proto",
"quinn-udp",
"rustc-hash 2.0.0",
"rustls 0.23.13",
"rustls 0.23.14",
"socket2",
"thiserror",
"tokio",
@ -3779,7 +3821,7 @@ dependencies = [
"rand",
"ring",
"rustc-hash 2.0.0",
"rustls 0.23.13",
"rustls 0.23.14",
"slab",
"thiserror",
"tinyvec",
@ -3875,14 +3917,14 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.6"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata 0.4.7",
"regex-syntax 0.8.4",
"regex-automata 0.4.8",
"regex-syntax 0.8.5",
]
[[package]]
@ -3896,13 +3938,13 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.7"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax 0.8.4",
"regex-syntax 0.8.5",
]
[[package]]
@ -3919,9 +3961,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1"
[[package]]
name = "regex-syntax"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "reqwest"
@ -3966,9 +4008,9 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.12.7"
version = "0.12.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
checksum = "f713147fbe92361e52392c73b8c9e48c04c6625bce969ef54dc901e58e042a7b"
dependencies = [
"async-compression",
"base64 0.22.1",
@ -3990,7 +4032,7 @@ dependencies = [
"percent-encoding",
"pin-project-lite",
"quinn",
"rustls 0.23.13",
"rustls 0.23.14",
"rustls-pemfile 2.1.3",
"rustls-pki-types",
"serde",
@ -4019,7 +4061,7 @@ dependencies = [
"anyhow",
"async-trait",
"http 1.1.0",
"reqwest 0.12.7",
"reqwest 0.12.8",
"serde",
"thiserror",
"tower-service",
@ -4036,7 +4078,7 @@ dependencies = [
"getrandom",
"http 1.1.0",
"matchit",
"reqwest 0.12.7",
"reqwest 0.12.8",
"reqwest-middleware",
"tracing",
]
@ -4177,9 +4219,9 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.23.13"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8"
checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
dependencies = [
"aws-lc-rs",
"log",
@ -4212,9 +4254,9 @@ dependencies = [
[[package]]
name = "rustls-pki-types"
version = "1.8.0"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0"
checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55"
[[package]]
name = "rustls-webpki"
@ -4373,9 +4415,9 @@ dependencies = [
[[package]]
name = "serde_with"
version = "3.9.0"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857"
checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817"
dependencies = [
"base64 0.22.1",
"chrono",
@ -4391,9 +4433,9 @@ dependencies = [
[[package]]
name = "serde_with_macros"
version = "3.9.0"
version = "3.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350"
checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d"
dependencies = [
"darling 0.20.10",
"proc-macro2",
@ -4737,7 +4779,7 @@ dependencies = [
"fnv",
"once_cell",
"plist",
"regex-syntax 0.8.4",
"regex-syntax 0.8.5",
"serde",
"serde_derive",
"serde_json",
@ -4974,7 +5016,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04fb792ccd6bbcd4bba408eb8a292f70fc4a3589e5d793626f45190e6454b6ab"
dependencies = [
"ring",
"rustls 0.23.13",
"rustls 0.23.14",
"tokio",
"tokio-postgres",
"tokio-rustls 0.26.0",
@ -4997,7 +5039,7 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4"
dependencies = [
"rustls 0.23.13",
"rustls 0.23.14",
"rustls-pki-types",
"tokio",
]

View file

@ -188,7 +188,7 @@ chrono = { workspace = true }
prometheus = { version = "0.13.4", features = ["process"] }
serial_test = { workspace = true }
clap = { workspace = true }
actix-web-prom = "0.8.0"
actix-web-prom = "0.9.0"
[dev-dependencies]
pretty_assertions = { workspace = true }

View file

@ -6,7 +6,7 @@
"repository": "https://github.com/LemmyNet/lemmy",
"author": "Dessalines",
"license": "AGPL-3.0",
"packageManager": "pnpm@9.9.0",
"packageManager": "pnpm@9.12.0",
"scripts": {
"lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'",
"fix": "prettier --write src && eslint --fix src",

File diff suppressed because it is too large Load diff

View file

@ -158,16 +158,16 @@ test("Delete a comment", async () => {
expect(deleteCommentRes.comment_view.comment.deleted).toBe(true);
expect(deleteCommentRes.comment_view.comment.content).toBe("");
// Make sure that comment is undefined on beta
// Make sure that comment is deleted on beta
await waitUntil(
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e),
e => e.message == "not_found",
() => resolveComment(beta, commentRes.comment_view.comment),
c => c.comment?.comment.deleted === true,
);
// Make sure that comment is undefined on gamma after delete
// Make sure that comment is deleted on gamma after delete
await waitUntil(
() => resolveComment(gamma, commentRes.comment_view.comment).catch(e => e),
e => e.message === "not_found",
() => resolveComment(gamma, commentRes.comment_view.comment),
c => c.comment?.comment.deleted === true,
);
// Test undeleting the comment
@ -181,11 +181,10 @@ test("Delete a comment", async () => {
// Make sure that comment is undeleted on beta
let betaComment2 = (
await waitUntil(
() => resolveComment(beta, commentRes.comment_view.comment).catch(e => e),
e => e.message !== "not_found",
() => resolveComment(beta, commentRes.comment_view.comment),
c => c.comment?.comment.deleted === false,
)
).comment;
expect(betaComment2?.comment.deleted).toBe(false);
assertCommentFederation(betaComment2, undeleteCommentRes.comment_view);
});

View file

@ -794,3 +794,29 @@ test("Fetch post with redirect", async () => {
let gammaPost2 = await gamma.resolveObject(form);
expect(gammaPost2.post).toBeDefined();
});
test("Rewrite markdown links", async () => {
const community = (await resolveBetaCommunity(beta)).community!;
// create a post
let postRes1 = await createPost(beta, community.community.id);
// link to this post in markdown
let postRes2 = await createPost(
beta,
community.community.id,
"https://example.com/",
`[link](${postRes1.post_view.post.ap_id})`,
);
console.log(postRes2.post_view.post.body);
expect(postRes2.post_view.post).toBeDefined();
// fetch both posts from another instance
const alphaPost1 = await resolvePost(alpha, postRes1.post_view.post);
const alphaPost2 = await resolvePost(alpha, postRes2.post_view.post);
// remote markdown link is replaced with local link
expect(alphaPost2.post?.post.body).toBe(
`[link](http://lemmy-alpha:8541/post/${alphaPost1.post?.post.id})`,
);
});

View file

@ -75,6 +75,8 @@
"ProxyAllImages"
# Timeout for uploading images to pictrs (in seconds)
upload_timeout: 30
# Resize post thumbnails to this maximum width/height.
max_thumbnail_size: 256
}
# Email sending configuration. All options except login/password are mandatory
email: {

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
comment::{CommentResponse, CreateCommentLike},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_bot_account, check_community_user_action, check_downvotes_enabled},
utils::{check_bot_account, check_community_user_action, check_local_vote_mode, VoteItem},
};
use lemmy_db_schema::{
newtypes::LocalUserId,
@ -27,14 +27,20 @@ pub async fn like_comment(
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let comment_id = data.comment_id;
let mut recipient_ids = Vec::<LocalUserId>::new();
// Don't do a downvote if site has downvotes disabled
check_downvotes_enabled(data.score, &local_site)?;
check_local_vote_mode(
data.score,
VoteItem::Comment(comment_id),
&local_site,
local_user_view.person.id,
&mut context.pool(),
)
.await?;
check_bot_account(&local_user_view.person)?;
let comment_id = data.comment_id;
let orig_comment = CommentView::read(
&mut context.pool(),
comment_id,
@ -61,7 +67,6 @@ pub async fn like_comment(
let like_form = CommentLikeForm {
comment_id: data.comment_id,
post_id: orig_comment.post.id,
person_id: local_user_view.person.id,
score: data.score,
};

View file

@ -92,8 +92,10 @@ pub async fn ban_from_community(
let remove_data = data.ban;
remove_or_restore_user_data_in_community(
data.community_id,
local_user_view.person.id,
banned_person_id,
remove_data,
&data.reason,
&mut context.pool(),
)
.await?;

View file

@ -3,4 +3,5 @@ pub mod ban;
pub mod block;
pub mod follow;
pub mod hide;
pub mod random;
pub mod transfer;

View file

@ -0,0 +1,55 @@
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
use lemmy_api_common::{
community::{CommunityResponse, GetRandomCommunity},
context::LemmyContext,
utils::{check_private_instance, is_mod_or_admin_opt},
};
use lemmy_db_schema::source::{
actor_language::CommunityLanguage,
community::Community,
local_site::LocalSite,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityView;
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn get_random_community(
data: Query<GetRandomCommunity>,
context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<CommunityResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site)?;
let local_user = local_user_view.as_ref().map(|u| &u.local_user);
let random_community_id =
Community::get_random_community_id(&mut context.pool(), &data.type_).await?;
let is_mod_or_admin = is_mod_or_admin_opt(
&mut context.pool(),
local_user_view.as_ref(),
Some(random_community_id),
)
.await
.is_ok();
let community_view = CommunityView::read(
&mut context.pool(),
random_community_id,
local_user,
is_mod_or_admin,
)
.await?;
let discussion_languages =
CommunityLanguage::read(&mut context.pool(), random_community_id).await?;
Ok(Json(CommunityResponse {
community_view,
discussion_languages,
}))
}

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
context::LemmyContext,
person::{BanPerson, BanPersonResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_expire_time, is_admin, remove_user_data, restore_user_data},
utils::{check_expire_time, is_admin, remove_or_restore_user_data},
};
use lemmy_db_schema::{
source::{
@ -66,11 +66,15 @@ pub async fn ban_from_site(
// Remove their data if that's desired
if data.remove_or_restore_data.unwrap_or(false) {
if data.ban {
remove_user_data(person.id, &context).await?;
} else {
restore_user_data(person.id, &context).await?;
}
let removed = data.ban;
remove_or_restore_user_data(
local_user_view.person.id,
person.id,
removed,
&data.reason,
&context,
)
.await?;
};
// Mod tables

View file

@ -8,8 +8,9 @@ use lemmy_api_common::{
utils::{
check_bot_account,
check_community_user_action,
check_downvotes_enabled,
check_local_vote_mode,
mark_post_as_read,
VoteItem,
},
};
use lemmy_db_schema::{
@ -31,13 +32,19 @@ pub async fn like_post(
local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let post_id = data.post_id;
// Don't do a downvote if site has downvotes disabled
check_downvotes_enabled(data.score, &local_site)?;
check_local_vote_mode(
data.score,
VoteItem::Post(post_id),
&local_site,
local_user_view.person.id,
&mut context.pool(),
)
.await?;
check_bot_account(&local_user_view.person)?;
// Check for a community ban
let post_id = data.post_id;
let post = Post::read(&mut context.pool(), post_id).await?;
check_community_user_action(

View file

@ -63,7 +63,7 @@ pub async fn leave_admin(
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let oauth_providers = OAuthProvider::get_all_public(&mut context.pool()).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await.ok();
Ok(Json(GetSiteResponse {
site_view,

View file

@ -34,13 +34,10 @@ use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType, CACHE_DURATION_API};
use serial_test::serial;
#[expect(clippy::unwrap_used)]
async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance, LocalUserView)> {
let pool = &mut context.pool();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.expect("Create test instance");
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let admin_person = Person::create(
pool,
@ -57,7 +54,7 @@ async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance
let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id).await?;
let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id);
let site = Site::create(pool, &site_form).await.unwrap();
let site = Site::create(pool, &site_form).await?;
// Create a local site, since this is necessary for determining if email verification is
// required
@ -68,14 +65,12 @@ async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance
site_setup: Some(true),
..LocalSiteInsertForm::new(site.id)
};
let local_site = LocalSite::create(pool, &local_site_form).await.unwrap();
let local_site = LocalSite::create(pool, &local_site_form).await?;
// Required to have a working local SiteView when updating the site to change email verification
// requirement or registration mode
let rate_limit_form = LocalSiteRateLimitInsertForm::new(local_site.id);
LocalSiteRateLimit::create(pool, &rate_limit_form)
.await
.unwrap();
LocalSiteRateLimit::create(pool, &rate_limit_form).await?;
Ok((inserted_instance, admin_local_user_view))
}
@ -109,7 +104,6 @@ async fn signup(
Ok((local_user, application))
}
#[expect(clippy::unwrap_used)]
async fn get_application_statuses(
context: &Data<LemmyContext>,
admin: LocalUserView,
@ -122,14 +116,14 @@ async fn get_application_statuses(
get_unread_registration_application_count(context.reset_request_count(), admin.clone()).await?;
let unread_applications = list_registration_applications(
Query::from_query("unread_only=true").unwrap(),
Query::from_query("unread_only=true")?,
context.reset_request_count(),
admin.clone(),
)
.await?;
let all_applications = list_registration_applications(
Query::from_query("unread_only=false").unwrap(),
Query::from_query("unread_only=false")?,
context.reset_request_count(),
admin,
)

View file

@ -42,44 +42,40 @@ pub async fn get_sitemap(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
pub(crate) mod tests {
use crate::sitemap::generate_urlset;
use chrono::{DateTime, NaiveDate, Utc};
use elementtree::Element;
use lemmy_db_schema::newtypes::DbUrl;
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use url::Url;
#[tokio::test]
async fn test_generate_urlset() {
async fn test_generate_urlset() -> LemmyResult<()> {
let posts: Vec<(DbUrl, DateTime<Utc>)> = vec![
(
Url::parse("https://example.com").unwrap().into(),
Url::parse("https://example.com")?.into(),
NaiveDate::from_ymd_opt(2022, 12, 1)
.unwrap()
.unwrap_or_default()
.and_hms_opt(9, 10, 11)
.unwrap()
.unwrap_or_default()
.and_utc(),
),
(
Url::parse("https://lemmy.ml").unwrap().into(),
Url::parse("https://lemmy.ml")?.into(),
NaiveDate::from_ymd_opt(2023, 1, 1)
.unwrap()
.unwrap_or_default()
.and_hms_opt(1, 2, 3)
.unwrap()
.unwrap_or_default()
.and_utc(),
),
];
let mut buf = Vec::<u8>::new();
generate_urlset(posts)
.await
.unwrap()
.write(&mut buf)
.unwrap();
let root = Element::from_reader(buf.as_slice()).unwrap();
generate_urlset(posts).await?.write(&mut buf)?;
let root = Element::from_reader(buf.as_slice())?;
assert_eq!(root.tag().name(), "urlset");
assert_eq!(root.child_count(), 2);
@ -99,45 +95,43 @@ pub(crate) mod tests {
root
.children()
.next()
.unwrap()
.children()
.find(|element| element.tag().name() == "loc")
.unwrap()
.text(),
.and_then(|n| n.children().find(|element| element.tag().name() == "loc"))
.map(Element::text)
.unwrap_or_default(),
"https://example.com/"
);
assert_eq!(
root
.children()
.next()
.unwrap()
.and_then(|n| n
.children()
.find(|element| element.tag().name() == "lastmod")
.unwrap()
.text(),
.find(|element| element.tag().name() == "lastmod"))
.map(Element::text)
.unwrap_or_default(),
"2022-12-01T09:10:11+00:00"
);
assert_eq!(
root
.children()
.nth(1)
.unwrap()
.children()
.find(|element| element.tag().name() == "loc")
.unwrap()
.text(),
.and_then(|n| n.children().find(|element| element.tag().name() == "loc"))
.map(Element::text)
.unwrap_or_default(),
"https://lemmy.ml/"
);
assert_eq!(
root
.children()
.nth(1)
.unwrap()
.and_then(|n| n
.children()
.find(|element| element.tag().name() == "lastmod")
.unwrap()
.text(),
.find(|element| element.tag().name() == "lastmod"))
.map(Element::text)
.unwrap_or_default(),
"2023-01-01T01:02:03+00:00"
);
Ok(())
}
}

View file

@ -69,7 +69,6 @@ impl Claims {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{claims::Claims, context::LemmyContext};
@ -84,7 +83,7 @@ mod tests {
traits::Crud,
utils::build_db_pool_for_tests,
};
use lemmy_utils::rate_limit::RateLimitCell;
use lemmy_utils::{error::LemmyResult, rate_limit::RateLimitCell};
use pretty_assertions::assert_eq;
use reqwest::Client;
use reqwest_middleware::ClientBuilder;
@ -92,10 +91,10 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_should_not_validate_user_token_after_password_change() {
async fn test_should_not_validate_user_token_after_password_change() -> LemmyResult<()> {
let pool_ = build_db_pool_for_tests().await;
let pool = &mut (&pool_).into();
let secret = Secret::init(pool).await.unwrap().unwrap();
let secret = Secret::init(pool).await?;
let context = LemmyContext::create(
pool_.clone(),
ClientBuilder::new(Client::default()).build(),
@ -103,29 +102,25 @@ mod tests {
RateLimitCell::with_test_config(),
);
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "Gerry9812");
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let inserted_person = Person::create(pool, &new_person).await?;
let local_user_form = LocalUserInsertForm::test_form(inserted_person.id);
let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![])
.await
.unwrap();
let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?;
let req = TestRequest::default().to_http_request();
let jwt = Claims::generate(inserted_local_user.id, req, &context)
.await
.unwrap();
let jwt = Claims::generate(inserted_local_user.id, req, &context).await?;
let valid = Claims::validate(&jwt, &context).await;
assert!(valid.is_ok());
let num_deleted = Person::delete(pool, inserted_person.id).await.unwrap();
let num_deleted = Person::delete(pool, inserted_person.id).await?;
assert_eq!(1, num_deleted);
Ok(())
}
}

View file

@ -3,9 +3,13 @@ use lemmy_db_schema::{
source::site::Site,
CommunityVisibility,
ListingType,
PostSortType,
};
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView, PersonView};
use lemmy_db_views_actor::structs::{
CommunityModeratorView,
CommunitySortType,
CommunityView,
PersonView,
};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
@ -74,7 +78,7 @@ pub struct CommunityResponse {
/// Fetches a list of communities.
pub struct ListCommunities {
pub type_: Option<ListingType>,
pub sort: Option<PostSortType>,
pub sort: Option<CommunitySortType>,
pub show_nsfw: Option<bool>,
pub page: Option<i64>,
pub limit: Option<i64>,
@ -225,3 +229,12 @@ pub struct TransferCommunity {
pub community_id: CommunityId,
pub person_id: PersonId,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Fetches a random community
pub struct GetRandomCommunity {
pub type_: Option<ListingType>,
}

View file

@ -354,9 +354,10 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
// fetch remote non-pictrs images for persistent thumbnail link
// TODO: should limit size once supported by pictrs
let fetch_url = format!(
"{}image/download?url={}",
"{}image/download?url={}&resize={}",
pictrs_config.url,
encode(image_url.as_str())
encode(image_url.as_str()),
context.settings().pictrs_config()?.max_thumbnail_size
);
let res = context
@ -471,13 +472,13 @@ pub async fn replace_image(
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
context::LemmyContext,
request::{extract_opengraph_data, fetch_link_metadata},
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
use url::Url;
@ -485,10 +486,10 @@ mod tests {
// These helped with testing
#[tokio::test]
#[serial]
async fn test_link_metadata() {
async fn test_link_metadata() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap();
let sample_res = fetch_link_metadata(&sample_url, &context).await.unwrap();
let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ")?;
let sample_res = fetch_link_metadata(&sample_url, &context).await?;
assert_eq!(
Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()),
sample_res.opengraph_data.title
@ -499,8 +500,7 @@ mod tests {
);
assert_eq!(
Some(
Url::parse("https://gitlab.com/uploads/-/system/project/avatar/4877469/iod_logo.png")
.unwrap()
Url::parse("https://gitlab.com/uploads/-/system/project/avatar/4877469/iod_logo.png")?
.into()
),
sample_res.opengraph_data.image
@ -510,19 +510,21 @@ mod tests {
Some(mime::TEXT_HTML_UTF_8.to_string()),
sample_res.content_type
);
Ok(())
}
#[test]
fn test_resolve_image_url() {
fn test_resolve_image_url() -> LemmyResult<()> {
// url that lists the opengraph fields
let url = Url::parse("https://example.com/one/two.html").unwrap();
let url = Url::parse("https://example.com/one/two.html")?;
// root relative url
let html_bytes = b"<!DOCTYPE html><html><head><meta property='og:image' content='/image.jpg'></head><body></body></html>";
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
assert_eq!(
metadata.image,
Some(Url::parse("https://example.com/image.jpg").unwrap().into())
Some(Url::parse("https://example.com/image.jpg")?.into())
);
// base relative url
@ -530,11 +532,7 @@ mod tests {
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
assert_eq!(
metadata.image,
Some(
Url::parse("https://example.com/one/image.jpg")
.unwrap()
.into()
)
Some(Url::parse("https://example.com/one/image.jpg")?.into())
);
// absolute url
@ -542,7 +540,7 @@ mod tests {
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
assert_eq!(
metadata.image,
Some(Url::parse("https://cdn.host.com/image.jpg").unwrap().into())
Some(Url::parse("https://cdn.host.com/image.jpg")?.into())
);
// protocol relative url
@ -550,7 +548,9 @@ mod tests {
let metadata = extract_opengraph_data(html_bytes, &url).expect("Unable to parse metadata");
assert_eq!(
metadata.image,
Some(Url::parse("https://example.com/image.jpg").unwrap().into())
Some(Url::parse("https://example.com/image.jpg")?.into())
);
Ok(())
}
}

View file

@ -21,6 +21,7 @@ use lemmy_db_schema::{
tagline::Tagline,
},
CommentSortType,
FederationMode,
ListingType,
ModlogActionType,
PostListingMode,
@ -170,7 +171,6 @@ pub struct CreateSite {
pub description: Option<String>,
pub icon: Option<String>,
pub banner: Option<String>,
pub enable_downvotes: Option<bool>,
pub enable_nsfw: Option<bool>,
pub community_creation_admin_only: Option<bool>,
pub require_email_verification: Option<bool>,
@ -208,6 +208,10 @@ pub struct CreateSite {
pub registration_mode: Option<RegistrationMode>,
pub oauth_registration: Option<bool>,
pub content_warning: Option<String>,
pub post_upvotes: Option<FederationMode>,
pub post_downvotes: Option<FederationMode>,
pub comment_upvotes: Option<FederationMode>,
pub comment_downvotes: Option<FederationMode>,
}
#[skip_serializing_none]
@ -224,8 +228,6 @@ pub struct EditSite {
pub icon: Option<String>,
/// A url for your site's banner.
pub banner: Option<String>,
/// Whether to enable downvotes.
pub enable_downvotes: Option<bool>,
/// Whether to enable NSFW.
pub enable_nsfw: Option<bool>,
/// Limits community creation to admins only.
@ -291,13 +293,21 @@ pub struct EditSite {
/// A list of blocked URLs
pub blocked_urls: Option<Vec<String>>,
pub registration_mode: Option<RegistrationMode>,
/// Whether or not external auth methods can auto-register users.
pub oauth_registration: Option<bool>,
/// Whether to email admins for new reports.
pub reports_email_admins: Option<bool>,
/// If present, nsfw content is visible by default. Should be displayed by frontends/clients
/// when the site is first opened by a user.
pub content_warning: Option<String>,
/// Whether or not external auth methods can auto-register users.
pub oauth_registration: Option<bool>,
/// What kind of post upvotes your site allows.
pub post_upvotes: Option<FederationMode>,
/// What kind of post downvotes your site allows.
pub post_downvotes: Option<FederationMode>,
/// What kind of comment upvotes your site allows.
pub comment_upvotes: Option<FederationMode>,
/// What kind of comment downvotes your site allows.
pub comment_downvotes: Option<FederationMode>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]

View file

@ -11,9 +11,9 @@ use chrono::{DateTime, Days, Local, TimeZone, Utc};
use enum_map::{enum_map, EnumMap};
use lemmy_db_schema::{
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId},
newtypes::{CommentId, CommunityId, DbUrl, InstanceId, PersonId, PostId},
source::{
comment::{Comment, CommentUpdateForm},
comment::{Comment, CommentLike, CommentUpdateForm},
community::{Community, CommunityModerator, CommunityUpdateForm},
community_block::CommunityBlock,
email_verification::{EmailVerification, EmailVerificationForm},
@ -23,16 +23,18 @@ use lemmy_db_schema::{
local_site::LocalSite,
local_site_rate_limit::LocalSiteRateLimit,
local_site_url_blocklist::LocalSiteUrlBlocklist,
moderator::{ModRemoveComment, ModRemoveCommentForm, ModRemovePost, ModRemovePostForm},
oauth_account::OAuthAccount,
password_reset_request::PasswordResetRequest,
person::{Person, PersonUpdateForm},
person_block::PersonBlock,
post::{Post, PostRead},
post::{Post, PostLike, PostRead},
registration_application::RegistrationApplication,
site::Site,
},
traits::Crud,
traits::{Crud, Likeable},
utils::DbPool,
FederationMode,
RegistrationMode,
};
use lemmy_db_views::{
@ -48,9 +50,12 @@ use lemmy_utils::{
email::{send_email, translations::Lang},
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
rate_limit::{ActionType, BucketConfig},
settings::structs::{PictrsImageMode, Settings},
settings::{
structs::{PictrsImageMode, Settings},
SETTINGS,
},
utils::{
markdown::{markdown_check_for_blocked_urls, markdown_rewrite_image_links},
markdown::{image_links::markdown_rewrite_image_links, markdown_check_for_blocked_urls},
slurs::{build_slur_regex, remove_slurs},
validation::clean_urls_in_text,
},
@ -296,13 +301,36 @@ pub async fn check_person_instance_community_block(
Ok(())
}
/// A vote item type used to check the vote mode.
pub enum VoteItem {
Post(PostId),
Comment(CommentId),
}
#[tracing::instrument(skip_all)]
pub fn check_downvotes_enabled(score: i16, local_site: &LocalSite) -> LemmyResult<()> {
if score == -1 && !local_site.enable_downvotes {
Err(LemmyErrorType::DownvotesAreDisabled)?
} else {
Ok(())
pub async fn check_local_vote_mode(
score: i16,
vote_item: VoteItem,
local_site: &LocalSite,
person_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let (downvote_setting, upvote_setting) = match vote_item {
VoteItem::Post(_) => (local_site.post_downvotes, local_site.post_upvotes),
VoteItem::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes),
};
let downvote_fail = score == -1 && downvote_setting == FederationMode::Disable;
let upvote_fail = score == 1 && upvote_setting == FederationMode::Disable;
// Undo previous vote for item if new vote fails
if downvote_fail || upvote_fail {
match vote_item {
VoteItem::Post(post_id) => PostLike::remove(pool, person_id, post_id).await?,
VoteItem::Comment(comment_id) => CommentLike::remove(pool, person_id, comment_id).await?,
};
}
Ok(())
}
/// Dont allow bots to do certain actions, like voting
@ -667,11 +695,18 @@ pub async fn purge_image_posts_for_community(
Ok(())
}
pub async fn remove_user_data(
/// Removes or restores user data.
pub async fn remove_or_restore_user_data(
mod_person_id: PersonId,
banned_person_id: PersonId,
removed: bool,
reason: &Option<String>,
context: &LemmyContext,
) -> LemmyResult<()> {
let pool = &mut context.pool();
// Only these actions are possible when removing, not restoring
if removed {
// Purge user images
let person = Person::read(pool, banned_person_id).await?;
if let Some(avatar) = person.avatar {
@ -694,9 +729,6 @@ pub async fn remove_user_data(
)
.await?;
// Posts
Post::update_removed_for_creator(pool, banned_person_id, None, true).await?;
// Purge image posts
purge_image_posts_for_person(banned_person_id, context).await?;
@ -717,7 +749,7 @@ pub async fn remove_user_data(
pool,
community_id,
&CommunityUpdateForm {
removed: Some(true),
removed: Some(removed),
..Default::default()
},
)
@ -742,37 +774,100 @@ pub async fn remove_user_data(
)
.await?;
}
}
// Posts
let removed_or_restored_posts =
Post::update_removed_for_creator(pool, banned_person_id, None, removed).await?;
create_modlog_entries_for_removed_or_restored_posts(
pool,
mod_person_id,
removed_or_restored_posts.iter().map(|r| r.id).collect(),
removed,
reason,
)
.await?;
// Comments
Comment::update_removed_for_creator(pool, banned_person_id, true).await?;
let removed_or_restored_comments =
Comment::update_removed_for_creator(pool, banned_person_id, removed).await?;
create_modlog_entries_for_removed_or_restored_comments(
pool,
mod_person_id,
removed_or_restored_comments.iter().map(|r| r.id).collect(),
removed,
reason,
)
.await?;
Ok(())
}
/// We can't restore their images, but we can unremove their posts and comments
pub async fn restore_user_data(
banned_person_id: PersonId,
context: &LemmyContext,
async fn create_modlog_entries_for_removed_or_restored_posts(
pool: &mut DbPool<'_>,
mod_person_id: PersonId,
post_ids: Vec<PostId>,
removed: bool,
reason: &Option<String>,
) -> LemmyResult<()> {
let pool = &mut context.pool();
// Build the forms
let forms = post_ids
.iter()
.map(|&post_id| ModRemovePostForm {
mod_person_id,
post_id,
removed: Some(removed),
reason: reason.clone(),
})
.collect();
// Posts
Post::update_removed_for_creator(pool, banned_person_id, None, false).await?;
ModRemovePost::create_multiple(pool, &forms).await?;
// Comments
Comment::update_removed_for_creator(pool, banned_person_id, false).await?;
Ok(())
}
async fn create_modlog_entries_for_removed_or_restored_comments(
pool: &mut DbPool<'_>,
mod_person_id: PersonId,
comment_ids: Vec<CommentId>,
removed: bool,
reason: &Option<String>,
) -> LemmyResult<()> {
// Build the forms
let forms = comment_ids
.iter()
.map(|&comment_id| ModRemoveCommentForm {
mod_person_id,
comment_id,
removed: Some(removed),
reason: reason.clone(),
})
.collect();
ModRemoveComment::create_multiple(pool, &forms).await?;
Ok(())
}
pub async fn remove_or_restore_user_data_in_community(
community_id: CommunityId,
mod_person_id: PersonId,
banned_person_id: PersonId,
remove: bool,
reason: &Option<String>,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
// Posts
let posts =
Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), remove).await?;
create_modlog_entries_for_removed_or_restored_posts(
pool,
mod_person_id,
posts.iter().map(|r| r.id).collect(),
remove,
reason,
)
.await?;
// Comments
// TODO Diesel doesn't allow updates with joins, so this has to be a loop
@ -798,6 +893,15 @@ pub async fn remove_or_restore_user_data_in_community(
.await?;
}
create_modlog_entries_for_removed_or_restored_comments(
pool,
mod_person_id,
comments.iter().map(|r| r.comment.id).collect(),
remove,
reason,
)
.await?;
Ok(())
}
@ -872,12 +976,8 @@ pub fn generate_followers_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{actor_id}/followers"))?.into())
}
pub fn generate_inbox_url(actor_id: &DbUrl) -> Result<DbUrl, ParseError> {
Ok(Url::parse(&format!("{actor_id}/inbox"))?.into())
}
pub fn generate_shared_inbox_url(settings: &Settings) -> LemmyResult<DbUrl> {
let url = format!("{}/inbox", settings.get_protocol_and_hostname());
pub fn generate_inbox_url() -> LemmyResult<DbUrl> {
let url = format!("{}/inbox", SETTINGS.get_protocol_and_hostname());
Ok(Url::parse(&url)?.into())
}
@ -1067,10 +1167,20 @@ fn build_proxied_image_url(
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;
use lemmy_db_schema::source::{
comment::CommentInsertForm,
community::CommunityInsertForm,
person::PersonInsertForm,
post::PostInsertForm,
};
use lemmy_db_views_moderator::structs::{
ModRemoveCommentView,
ModRemovePostView,
ModlogListParams,
};
use pretty_assertions::assert_eq;
use serial_test::serial;
@ -1092,48 +1202,42 @@ mod tests {
}
#[test]
fn test_limit_ban_term() {
fn test_limit_ban_term() -> LemmyResult<()> {
// Ban expires in past, should throw error
assert!(limit_expire_time(Utc::now() - Days::new(5)).is_err());
// Legitimate ban term, return same value
let fourteen_days = Utc::now() + Days::new(14);
assert_eq!(
limit_expire_time(fourteen_days).unwrap(),
Some(fourteen_days)
);
assert_eq!(limit_expire_time(fourteen_days)?, Some(fourteen_days));
let nine_years = Utc::now() + Days::new(365 * 9);
assert_eq!(limit_expire_time(nine_years).unwrap(), Some(nine_years));
assert_eq!(limit_expire_time(nine_years)?, Some(nine_years));
// Too long ban term, changes to None (permanent ban)
assert_eq!(
limit_expire_time(Utc::now() + Days::new(365 * 11)).unwrap(),
None
);
assert_eq!(limit_expire_time(Utc::now() + Days::new(365 * 11))?, None);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_proxy_image_link() {
async fn test_proxy_image_link() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
// image from local domain is unchanged
let local_url = Url::parse("http://lemmy-alpha/image.png").unwrap();
let local_url = Url::parse("http://lemmy-alpha/image.png")?;
let proxied =
proxy_image_link_internal(local_url.clone(), PictrsImageMode::ProxyAllImages, &context)
.await
.unwrap();
.await?;
assert_eq!(&local_url, proxied.inner());
// image from remote domain is proxied
let remote_image = Url::parse("http://lemmy-beta/image.png").unwrap();
let remote_image = Url::parse("http://lemmy-beta/image.png")?;
let proxied = proxy_image_link_internal(
remote_image.clone(),
PictrsImageMode::ProxyAllImages,
&context,
)
.await
.unwrap();
.await?;
assert_eq!(
"https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png",
proxied.as_str()
@ -1146,5 +1250,159 @@ mod tests {
.await
.is_ok()
);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_mod_remove_or_restore_data() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_mod = PersonInsertForm::test_form(inserted_instance.id, "modder");
let inserted_mod = Person::create(pool, &new_mod).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "chrimbus");
let inserted_person = Person::create(pool, &new_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
"mod_community crepes".to_string(),
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await?;
let post_form_1 = PostInsertForm::new(
"A test post tubular".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post_1 = Post::create(pool, &post_form_1).await?;
let post_form_2 = PostInsertForm::new(
"A test post radical".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post_2 = Post::create(pool, &post_form_2).await?;
let comment_form_1 = CommentInsertForm::new(
inserted_person.id,
inserted_post_1.id,
"A test comment tubular".into(),
);
let _inserted_comment_1 = Comment::create(pool, &comment_form_1, None).await?;
let comment_form_2 = CommentInsertForm::new(
inserted_person.id,
inserted_post_2.id,
"A test comment radical".into(),
);
let _inserted_comment_2 = Comment::create(pool, &comment_form_2, None).await?;
// Remove the user data
remove_or_restore_user_data(
inserted_mod.id,
inserted_person.id,
true,
&Some("a remove reason".to_string()),
&context,
)
.await?;
// Verify that their posts and comments are removed.
let params = ModlogListParams {
community_id: None,
mod_person_id: None,
other_person_id: None,
post_id: None,
comment_id: None,
page: None,
limit: None,
hide_modlog_names: false,
};
// Posts
let post_modlog = ModRemovePostView::list(pool, params).await?;
assert_eq!(2, post_modlog.len());
let mod_removed_posts = post_modlog
.iter()
.map(|p| p.mod_remove_post.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![true, true], mod_removed_posts);
let removed_posts = post_modlog
.iter()
.map(|p| p.post.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![true, true], removed_posts);
// Comments
let comment_modlog = ModRemoveCommentView::list(pool, params).await?;
assert_eq!(2, comment_modlog.len());
let mod_removed_comments = comment_modlog
.iter()
.map(|p| p.mod_remove_comment.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![true, true], mod_removed_comments);
let removed_comments = comment_modlog
.iter()
.map(|p| p.comment.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![true, true], removed_comments);
// Now restore the content, and make sure it got appended
remove_or_restore_user_data(
inserted_mod.id,
inserted_person.id,
false,
&Some("a restore reason".to_string()),
&context,
)
.await?;
// Posts
let post_modlog = ModRemovePostView::list(pool, params).await?;
assert_eq!(4, post_modlog.len());
let mod_restored_posts = post_modlog
.iter()
.map(|p| p.mod_remove_post.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![false, false, true, true], mod_restored_posts);
let restored_posts = post_modlog
.iter()
.map(|p| p.post.removed)
.collect::<Vec<bool>>();
// All of these will be false, cause its the current state of the post
assert_eq!(vec![false, false, false, false], restored_posts);
// Comments
let comment_modlog = ModRemoveCommentView::list(pool, params).await?;
assert_eq!(4, comment_modlog.len());
let mod_restored_comments = comment_modlog
.iter()
.map(|p| p.mod_remove_comment.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![false, false, true, true], mod_restored_comments);
let restored_comments = comment_modlog
.iter()
.map(|p| p.comment.removed)
.collect::<Vec<bool>>();
assert_eq!(vec![false, false, false, false], restored_comments);
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
}

View file

@ -88,16 +88,9 @@ pub async fn create_comment(
check_comment_depth(parent)?;
}
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
data.language_id,
community_id,
)
.await?;
// attempt to set default language if none was provided
let language_id = match data.language_id {
Some(lid) => Some(lid),
Some(lid) => lid,
None => {
default_post_language(
&mut context.pool(),
@ -108,8 +101,11 @@ pub async fn create_comment(
}
};
CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id)
.await?;
let comment_form = CommentInsertForm {
language_id,
language_id: Some(language_id),
..CommentInsertForm::new(local_user_view.person.id, data.post_id, content.clone())
};
@ -136,7 +132,6 @@ pub async fn create_comment(
// You like your own comment by default
let like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: post.id,
person_id: local_user_view.person.id,
score: 1,
};

View file

@ -55,13 +55,14 @@ pub async fn update_comment(
Err(LemmyErrorType::NoCommentEditAllowed)?
}
let language_id = data.language_id;
if let Some(language_id) = data.language_id {
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
language_id,
orig_comment.community.id,
)
.await?;
}
let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?;

View file

@ -8,7 +8,6 @@ use lemmy_api_common::{
generate_followers_url,
generate_inbox_url,
generate_local_apub_endpoint,
generate_shared_inbox_url,
get_url_blocklist,
is_admin,
local_site_to_slur_regex,
@ -96,8 +95,7 @@ pub async fn create_community(
actor_id: Some(community_actor_id.clone()),
private_key: Some(keypair.private_key),
followers_url: Some(generate_followers_url(&community_actor_id)?),
inbox_url: Some(generate_inbox_url(&community_actor_id)?),
shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?),
inbox_url: Some(generate_inbox_url()?),
posting_restricted_to_mods: data.posting_restricted_to_mods,
visibility: data.visibility,
..CommunityInsertForm::new(

View file

@ -104,18 +104,9 @@ pub async fn create_post(
.await?;
}
// Only need to check if language is allowed in case user set it explicitly. When using default
// language, it already only returns allowed languages.
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
data.language_id,
community_id,
)
.await?;
// attempt to set default language if none was provided
let language_id = match data.language_id {
Some(lid) => Some(lid),
Some(lid) => lid,
None => {
default_post_language(
&mut context.pool(),
@ -126,6 +117,11 @@ pub async fn create_post(
}
};
// Only need to check if language is allowed in case user set it explicitly. When using default
// language, it already only returns allowed languages.
CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id)
.await?;
let scheduled_publish_time =
convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?;
let post_form = PostInsertForm {
@ -133,7 +129,7 @@ pub async fn create_post(
body,
alt_text: data.alt_text.clone(),
nsfw: data.nsfw,
language_id,
language_id: Some(language_id),
scheduled_publish_time,
..PostInsertForm::new(
data.name.trim().to_string(),

View file

@ -101,13 +101,14 @@ pub async fn update_post(
Err(LemmyErrorType::NoPostEditAllowed)?
}
let language_id = data.language_id;
if let Some(language_id) = data.language_id {
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
language_id,
orig_post.community_id,
)
.await?;
}
// handle changes to scheduled_publish_time
let scheduled_publish_time = match (

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
context::LemmyContext,
site::{CreateSite, SiteResponse},
utils::{
generate_shared_inbox_url,
generate_inbox_url,
get_url_blocklist,
is_admin,
local_site_rate_limit_to_rate_limit_config,
@ -55,7 +55,7 @@ pub async fn create_site(
validate_create_payload(&local_site, &data)?;
let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
let inbox_url = Some(generate_shared_inbox_url(context.settings())?);
let inbox_url = Some(generate_inbox_url()?);
let keypair = generate_actor_keypair()?;
let slur_regex = local_site_to_slur_regex(&local_site);
@ -90,7 +90,6 @@ pub async fn create_site(
let local_site_form = LocalSiteUpdateForm {
// Set the site setup to true
site_setup: Some(true),
enable_downvotes: data.enable_downvotes,
registration_mode: data.registration_mode,
community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification,
@ -110,6 +109,10 @@ pub async fn create_site(
captcha_enabled: data.captcha_enabled,
captcha_difficulty: data.captcha_difficulty.clone(),
default_post_listing_mode: data.default_post_listing_mode,
post_upvotes: data.post_upvotes,
post_downvotes: data.post_downvotes,
comment_upvotes: data.comment_upvotes,
comment_downvotes: data.comment_downvotes,
..Default::default()
};

View file

@ -43,7 +43,7 @@ pub async fn get_site(
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await?;
let tagline = Tagline::get_random(&mut context.pool()).await.ok();
let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?;
let oauth_providers =
OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone());

View file

@ -99,7 +99,6 @@ pub async fn update_site(
.ok();
let local_site_form = LocalSiteUpdateForm {
enable_downvotes: data.enable_downvotes,
registration_mode: data.registration_mode,
community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification,
@ -121,6 +120,10 @@ pub async fn update_site(
reports_email_admins: data.reports_email_admins,
default_post_listing_mode: data.default_post_listing_mode,
oauth_registration: data.oauth_registration,
post_upvotes: data.post_upvotes,
post_downvotes: data.post_downvotes,
comment_upvotes: data.comment_upvotes,
comment_downvotes: data.comment_downvotes,
..Default::default()
};

View file

@ -11,7 +11,6 @@ use lemmy_api_common::{
check_user_valid,
generate_inbox_url,
generate_local_apub_endpoint,
generate_shared_inbox_url,
honeypot_check,
local_site_to_slur_regex,
password_length_check,
@ -418,8 +417,7 @@ async fn create_person(
// Register the new person
let person_form = PersonInsertForm {
actor_id: Some(actor_id.clone()),
inbox_url: Some(generate_inbox_url(&actor_id)?),
shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?),
inbox_url: Some(generate_inbox_url()?),
private_key: Some(actor_keypair.private_key),
..PersonInsertForm::new(username.clone(), actor_keypair.public_key, instance_id)
};

View file

@ -3,9 +3,9 @@
"type": "Group",
"preferredUsername": "tenforward",
"name": "Ten Forward",
"summary": "<p>Lounge and recreation facility</p>\n<hr />\n<p>Welcome to the <a href=\"https://memory-alpha.fandom.com/wiki/USS_Enterprise_(NCC-1701-D)\">Enterprise</a>!.</p>\n",
"summary": "<p>Lounge and recreation facility</p>\n<hr />\n<p>Welcome to the Enterprise!.</p>\n",
"source": {
"content": "Lounge and recreation facility\n\n---\n\nWelcome to the [Enterprise](https://memory-alpha.fandom.com/wiki/USS_Enterprise_(NCC-1701-D))!.",
"content": "Lounge and recreation facility\n\n---\n\nWelcome to the Enterprise!",
"mediaType": "text/markdown"
},
"sensitive": false,

View file

@ -10,7 +10,7 @@
"attachment": [],
"attributedTo": "https://queer.hacktivis.me/users/lanodan",
"cc": ["https://www.w3.org/ns/activitystreams#Public"],
"content": "<span class=\"h-card\"><a class=\"u-url mention\" data-user=\"9zkUX4o3WxGM8vGPfU\" href=\"https://pleroma.popolon.org/users/popolon\" rel=\"ugc\">@<span>popolon</span></a></span> Have what?",
"content": "Have what?",
"context": "https://queer.hacktivis.me/contexts/34cba3d2-2f35-4169-aeff-56af9bfeb753",
"conversation": "https://queer.hacktivis.me/contexts/34cba3d2-2f35-4169-aeff-56af9bfeb753",
"id": "https://queer.hacktivis.me/objects/8d4973f4-53de-49cd-8c27-df160e16a9c2",

View file

@ -41,7 +41,7 @@
"owner": "https://queer.hacktivis.me/users/lanodan",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsWOgdjSMc010qvxC3njI\nXJlFWMJ5gJ8QXCW/PajYdsHPM6d+jxBNJ6zp9/tIRa2m7bWHTSkuHQ7QthOpt6vu\n+dAWpKRLS607SPLItn/qUcyXvgN+H8shfyhMxvkVs9jXdtlBsLUVE7UNpN0dxzqe\nI79QWbf7o4amgaIWGRYB+OYMnIxKt+GzIkivZdSVSYjfxNnBYkMCeUxm5EpPIxKS\nP5bBHAVRRambD5NUmyKILuC60/rYuc/C+vmgpY2HCWFS2q6o34dPr9enwL6t4b3m\nS1t/EJHk9rGaaDqSGkDEfyQI83/7SDebWKuETMKKFLZi1vMgQIFuOYCIhN6bIiZm\npQIDAQAB\n-----END PUBLIC KEY-----\n\n"
},
"summary": "---<br/>Website: <a href=\"https://hacktivis.me/\">https://hacktivis.me/</a><br/>Lang: Français(natif), English(fluent), LSF(🤏~👌), русский (еле-еле), <br/>Politics: Anarchist as in DIY/DIWO, freedom of association, anti-authoritarian, anti-identitarianism<br/><br/>Pronouns: meh, pick any, have fun<br/>Timezone: Let&#39;s say Mars, I have a non-24h cycle<br/>```<br/>🦊🦄⚧🂡ⓥ :anarchy: 👿🐧 :gentoo:<br/>Pleroma maintainer (mostly backend)<br/>BadWolf developer<br/>Gentoo contributor<br/><br/>Dayjob: yogoko.fr<br/><br/>That person which uses HJKL in games<br/><br/>Just because computer bad: X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*<br/><br/>banner from: <a href=\"https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db\">https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db</a><br/>Federation-bots: <a class=\"hashtag\" data-tag=\"nobot\" href=\"https://queer.hacktivis.me/tag/nobot\">#nobot</a>",
"summary": "---Lang: Français(natif), English(fluent), LSF(🤏~👌), русский (еле-еле), <br/>Politics: Anarchist as in DIY/DIWO, freedom of association, anti-authoritarian, anti-identitarianism<br/><br/>Pronouns: meh, pick any, have fun<br/>Timezone: Let&#39;s say Mars, I have a non-24h cycle<br/>```<br/>🦊🦄⚧🂡ⓥ :anarchy: 👿🐧 :gentoo:<br/>Pleroma maintainer (mostly backend)<br/>BadWolf developer<br/>Gentoo contributor<br/><br/>Dayjob: yogoko.fr<br/><br/>That person which uses HJKL in games<br/><br/>Just because computer bad: X5O!P%@AP[4\\PZX54(P^)7CC)7}$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*<br/><br/>banner from: <a href=\"https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db\">https://soc.flyingcube.tech/objects/56f79be2-9013-4559-9826-f7dc392417db</a><br/>Federation-bots: <a class=\"hashtag\" data-tag=\"nobot\" href=\"https://queer.hacktivis.me/tag/nobot\">#nobot</a>",
"tag": [
{
"icon": {

View file

@ -23,7 +23,7 @@ use anyhow::anyhow;
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{remove_or_restore_user_data_in_community, remove_user_data},
utils::{remove_or_restore_user_data, remove_or_restore_user_data_in_community},
};
use lemmy_db_schema::{
source::{
@ -160,6 +160,7 @@ impl ActivityHandler for BlockUser {
let mod_person = self.actor.dereference(context).await?;
let blocked_person = self.object.dereference(context).await?;
let target = self.target.dereference(context).await?;
let reason = self.summary;
match target {
SiteOrCommunity::Site(_site) => {
let blocked_person = Person::update(
@ -173,14 +174,15 @@ impl ActivityHandler for BlockUser {
)
.await?;
if self.remove_data.unwrap_or(false) {
remove_user_data(blocked_person.id, context).await?;
remove_or_restore_user_data(mod_person.id, blocked_person.id, true, &reason, context)
.await?;
}
// write mod log
let form = ModBanForm {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
reason: self.summary,
reason,
banned: Some(true),
expires,
};
@ -207,8 +209,10 @@ impl ActivityHandler for BlockUser {
if self.remove_data.unwrap_or(false) {
remove_or_restore_user_data_in_community(
community.id,
mod_person.id,
blocked_person.id,
true,
&reason,
&mut context.pool(),
)
.await?;
@ -219,7 +223,7 @@ impl ActivityHandler for BlockUser {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
community_id: community.id,
reason: self.summary,
reason,
banned: Some(true),
expires,
};

View file

@ -19,7 +19,7 @@ use activitypub_federation::{
};
use lemmy_api_common::{
context::LemmyContext,
utils::{remove_or_restore_user_data_in_community, restore_user_data},
utils::{remove_or_restore_user_data, remove_or_restore_user_data_in_community},
};
use lemmy_db_schema::{
source::{
@ -120,7 +120,8 @@ impl ActivityHandler for UndoBlockUser {
.await?;
if self.restore_data.unwrap_or(false) {
restore_user_data(blocked_person.id, context).await?;
remove_or_restore_user_data(mod_person.id, blocked_person.id, false, &None, context)
.await?;
}
// write mod log
@ -144,8 +145,10 @@ impl ActivityHandler for UndoBlockUser {
if self.restore_data.unwrap_or(false) {
remove_or_restore_user_data_in_community(
community.id,
mod_person.id,
blocked_person.id,
false,
&None,
&mut context.pool(),
)
.await?;

View file

@ -106,8 +106,14 @@ impl ActivityHandler for UpdateCommunity {
icon: Some(self.object.icon.map(|i| i.url.into())),
banner: Some(self.object.image.map(|i| i.url.into())),
followers_url: self.object.followers.map(Into::into),
inbox_url: Some(self.object.inbox.into()),
shared_inbox_url: Some(self.object.endpoints.map(|e| e.shared_inbox.into())),
inbox_url: Some(
self
.object
.endpoints
.map(|e| e.shared_inbox)
.unwrap_or(self.object.inbox)
.into(),
),
moderators_url: self.object.attributed_to.map(Into::into),
posting_restricted_to_mods: self.object.posting_restricted_to_mods,
featured_url: self.object.featured.map(Into::into),

View file

@ -153,7 +153,6 @@ impl ActivityHandler for CreateOrUpdateNote {
// author likes their own comment by default
let like_form = CommentLikeForm {
comment_id: comment.id,
post_id: comment.post_id,
person_id: comment.creator_id,
score: 1,
};

View file

@ -62,7 +62,6 @@ async fn vote_comment(
let comment_id = comment.id;
let like_form = CommentLikeForm {
comment_id,
post_id: comment.post_id,
person_id: actor.id,
score: vote_type.into(),
};

View file

@ -18,7 +18,7 @@ use activitypub_federation::{
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::{context::LemmyContext, utils::check_bot_account};
use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_db_schema::{source::local_site::LocalSite, FederationMode};
use lemmy_utils::error::{LemmyError, LemmyResult};
use url::Url;
@ -68,12 +68,22 @@ impl ActivityHandler for Vote {
check_bot_account(&actor.0)?;
let enable_downvotes = LocalSite::read(&mut context.pool())
// Check for enabled federation votes
let local_site = LocalSite::read(&mut context.pool())
.await
.map(|l| l.enable_downvotes)
.unwrap_or(true);
if self.kind == VoteType::Dislike && !enable_downvotes {
// If this is a downvote but downvotes are ignored, only undo any existing vote
.unwrap_or_default();
let (downvote_setting, upvote_setting) = match object {
PostOrComment::Post(_) => (local_site.post_downvotes, local_site.post_upvotes),
PostOrComment::Comment(_) => (local_site.comment_downvotes, local_site.comment_upvotes),
};
// Don't allow dislikes for either disabled, or local only votes
let downvote_fail = self.kind == VoteType::Dislike && downvote_setting != FederationMode::All;
let upvote_fail = self.kind == VoteType::Like && upvote_setting != FederationMode::All;
if downvote_fail || upvote_fail {
// If this is a rejection, undo the vote
match object {
PostOrComment::Post(p) => undo_vote_post(actor, &p, context).await,
PostOrComment::Comment(c) => undo_vote_comment(actor, &c, context).await,

View file

@ -1,10 +1,10 @@
use crate::fetcher::{
post_or_comment::PostOrComment,
search::{search_query_to_object_id, search_query_to_object_id_local, SearchableObjects},
user_or_community::UserOrCommunity,
};
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
use diesel::NotFound;
use lemmy_api_common::{
context::LemmyContext,
site::{ResolveObject, ResolveObjectResponse},
@ -46,35 +46,145 @@ async fn convert_response(
local_user_view: Option<LocalUserView>,
pool: &mut DbPool<'_>,
) -> LemmyResult<Json<ResolveObjectResponse>> {
use SearchableObjects::*;
let removed_or_deleted;
let mut res = ResolveObjectResponse::default();
let local_user = local_user_view.map(|l| l.local_user);
let is_admin = local_user.clone().map(|l| l.admin).unwrap_or_default();
match object {
Post(p) => {
removed_or_deleted = p.deleted || p.removed;
res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), false).await?)
SearchableObjects::PostOrComment(pc) => match *pc {
PostOrComment::Post(p) => {
res.post = Some(PostView::read(pool, p.id, local_user.as_ref(), is_admin).await?)
}
Comment(c) => {
removed_or_deleted = c.deleted || c.removed;
PostOrComment::Comment(c) => {
res.comment = Some(CommentView::read(pool, c.id, local_user.as_ref()).await?)
}
PersonOrCommunity(p) => match *p {
UserOrCommunity::User(u) => {
removed_or_deleted = u.deleted;
res.person = Some(PersonView::read(pool, u.id).await?)
}
},
SearchableObjects::PersonOrCommunity(pc) => match *pc {
UserOrCommunity::User(u) => res.person = Some(PersonView::read(pool, u.id).await?),
UserOrCommunity::Community(c) => {
removed_or_deleted = c.deleted || c.removed;
res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), false).await?)
res.community = Some(CommunityView::read(pool, c.id, local_user.as_ref(), is_admin).await?)
}
},
};
// if the object was deleted from database, dont return it
if removed_or_deleted {
Err(NotFound {}.into())
} else {
Ok(Json(res))
}
#[cfg(test)]
mod tests {
use crate::api::resolve_object::resolve_object;
use actix_web::web::Query;
use lemmy_api_common::{context::LemmyContext, site::ResolveObject};
use lemmy_db_schema::{
source::{
community::{Community, CommunityInsertForm},
instance::Instance,
local_site::{LocalSite, LocalSiteInsertForm},
post::{Post, PostInsertForm, PostUpdateForm},
site::{Site, SiteInsertForm},
},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use serial_test::serial;
#[tokio::test]
#[serial]
#[expect(clippy::unwrap_used)]
async fn test_object_visibility() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let name = "test_local_user_name";
let bio = "test_local_user_bio";
let creator = LocalUserView::create_test_user(pool, name, bio, false).await?;
let regular_user = LocalUserView::create_test_user(pool, name, bio, false).await?;
let admin_user = LocalUserView::create_test_user(pool, name, bio, true).await?;
let instance_id = creator.person.instance_id;
let site_form = SiteInsertForm::new("test site".to_string(), instance_id);
let site = Site::create(pool, &site_form).await?;
let local_site_form = LocalSiteInsertForm {
site_setup: Some(true),
private_instance: Some(false),
..LocalSiteInsertForm::new(site.id)
};
LocalSite::create(pool, &local_site_form).await?;
let community = Community::create(
pool,
&CommunityInsertForm::new(
instance_id,
"test".to_string(),
"test".to_string(),
"pubkey".to_string(),
),
)
.await?;
let post_insert_form = PostInsertForm::new("Test".to_string(), creator.person.id, community.id);
let post = Post::create(pool, &post_insert_form).await?;
let query = format!("q={}", post.ap_id).to_string();
let query: Query<ResolveObject> = Query::from_query(&query)?;
// Objects should be resolvable without authentication
let res = resolve_object(query.clone(), context.reset_request_count(), None).await?;
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
// Objects should be resolvable by regular users
let res = resolve_object(
query.clone(),
context.reset_request_count(),
Some(regular_user.clone()),
)
.await?;
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
// Objects should be resolvable by admins
let res = resolve_object(
query.clone(),
context.reset_request_count(),
Some(admin_user.clone()),
)
.await?;
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
Post::update(
pool,
post.id,
&PostUpdateForm {
deleted: Some(true),
..Default::default()
},
)
.await?;
// Deleted objects should not be resolvable without authentication
let res = resolve_object(query.clone(), context.reset_request_count(), None).await;
assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound));
// Deleted objects should not be resolvable by regular users
let res = resolve_object(
query.clone(),
context.reset_request_count(),
Some(regular_user.clone()),
)
.await;
assert!(res.is_err_and(|e| e.error_type == LemmyErrorType::NotFound));
// Deleted objects should be resolvable by admins
let res = resolve_object(
query.clone(),
context.reset_request_count(),
Some(admin_user.clone()),
)
.await?;
assert_eq!(res.post.as_ref().unwrap().post.ap_id, post.ap_id);
LocalSite::delete(pool).await?;
Site::delete(pool, site.id).await?;
Instance::delete(pool, instance_id).await?;
Ok(())
}
}

View file

@ -12,7 +12,11 @@ use lemmy_db_views::{
post_view::PostQuery,
structs::{LocalUserView, SiteView},
};
use lemmy_db_views_actor::{community_view::CommunityQuery, person_view::PersonQuery};
use lemmy_db_views_actor::{
community_view::CommunityQuery,
person_view::PersonQuery,
structs::CommunitySortType,
};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
@ -102,7 +106,7 @@ pub async fn search(
};
let community_query = CommunityQuery {
sort,
sort: sort.map(CommunitySortType::from),
listing_type,
search_term: Some(q.clone()),
title_only,

View file

@ -313,18 +313,14 @@ where
#[cfg(test)]
#[expect(clippy::indexing_slicing)]
mod tests {
pub(crate) mod tests {
use crate::api::user_settings_backup::{export_settings, import_settings};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm},
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
local_user::LocalUser,
},
traits::{Crud, Followable},
};
@ -336,32 +332,13 @@ mod tests {
use std::time::Duration;
use tokio::time::sleep;
async fn create_user(
name: String,
bio: Option<String>,
context: &Data<LemmyContext>,
) -> LemmyResult<LocalUserView> {
let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?;
let person_form = PersonInsertForm {
display_name: Some(name.clone()),
bio,
..PersonInsertForm::test_form(instance.id, &name)
};
let person = Person::create(&mut context.pool(), &person_form).await?;
let user_form = LocalUserInsertForm::test_form(person.id);
let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?;
Ok(LocalUserView::read(&mut context.pool(), local_user.id).await?)
}
#[tokio::test]
#[serial]
async fn test_settings_export_import() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let export_user =
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
let export_user = LocalUserView::create_test_user(pool, "hanna", "my bio", false).await?;
let community_form = CommunityInsertForm::new(
export_user.person.instance_id,
@ -369,25 +346,25 @@ mod tests {
"testcom".to_string(),
"pubkey".to_string(),
);
let community = Community::create(&mut context.pool(), &community_form).await?;
let community = Community::create(pool, &community_form).await?;
let follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: export_user.person.id,
pending: false,
};
CommunityFollower::follow(&mut context.pool(), &follower_form).await?;
CommunityFollower::follow(pool, &follower_form).await?;
let backup = export_settings(export_user.clone(), context.reset_request_count()).await?;
let import_user = create_user("charles".to_string(), None, &context).await?;
let import_user =
LocalUserView::create_test_user(pool, "charles", "charles bio", false).await?;
import_settings(backup, import_user.clone(), context.reset_request_count()).await?;
// wait for background task to finish
sleep(Duration::from_millis(1000)).await;
let import_user_updated =
LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?;
let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?;
assert_eq!(
export_user.person.display_name,
@ -395,13 +372,12 @@ mod tests {
);
assert_eq!(export_user.person.bio, import_user_updated.person.bio);
let follows =
CommunityFollowerView::for_person(&mut context.pool(), import_user.person.id).await?;
let follows = CommunityFollowerView::for_person(pool, import_user.person.id).await?;
assert_eq!(follows.len(), 1);
assert_eq!(follows[0].community.actor_id, community.actor_id);
LocalUser::delete(&mut context.pool(), export_user.local_user.id).await?;
LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?;
LocalUser::delete(pool, export_user.local_user.id).await?;
LocalUser::delete(pool, import_user.local_user.id).await?;
Ok(())
}
@ -409,9 +385,9 @@ mod tests {
#[serial]
async fn disallow_large_backup() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let export_user =
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
let export_user = LocalUserView::create_test_user(pool, "harry", "harry bio", false).await?;
let mut backup = export_settings(export_user.clone(), context.reset_request_count()).await?;
@ -426,7 +402,7 @@ mod tests {
backup.saved_comments.push("http://example4.com".parse()?);
}
let import_user = create_user("charles".to_string(), None, &context).await?;
let import_user = LocalUserView::create_test_user(pool, "sally", "sally bio", false).await?;
let imported =
import_settings(backup, import_user.clone(), context.reset_request_count()).await;
@ -436,8 +412,8 @@ mod tests {
Some(LemmyErrorType::TooManyItems)
);
LocalUser::delete(&mut context.pool(), export_user.local_user.id).await?;
LocalUser::delete(&mut context.pool(), import_user.local_user.id).await?;
LocalUser::delete(pool, export_user.local_user.id).await?;
LocalUser::delete(pool, import_user.local_user.id).await?;
Ok(())
}
@ -445,9 +421,9 @@ mod tests {
#[serial]
async fn import_partial_backup() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();
let import_user =
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
let import_user = LocalUserView::create_test_user(pool, "larry", "larry bio", false).await?;
let backup =
serde_json::from_str("{\"bot_account\": true, \"settings\": {\"theme\": \"my_theme\"}}")?;
@ -458,8 +434,7 @@ mod tests {
)
.await?;
let import_user_updated =
LocalUserView::read(&mut context.pool(), import_user.local_user.id).await?;
let import_user_updated = LocalUserView::read(pool, import_user.local_user.id).await?;
// mark as bot account
assert!(import_user_updated.person.bot_account);
// dont remove existing bio

View file

@ -0,0 +1,192 @@
use super::{search::SearchableObjects, user_or_community::UserOrCommunity};
use crate::fetcher::post_or_comment::PostOrComment;
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use lemmy_api_common::{
context::LemmyContext,
utils::{generate_local_apub_endpoint, EndpointType},
};
use lemmy_db_schema::{newtypes::InstanceId, source::instance::Instance};
use lemmy_utils::{
error::LemmyResult,
utils::markdown::image_links::{markdown_find_links, markdown_handle_title},
};
use url::Url;
pub async fn markdown_rewrite_remote_links_opt(
src: Option<String>,
context: &Data<LemmyContext>,
) -> Option<String> {
match src {
Some(t) => Some(markdown_rewrite_remote_links(t, context).await),
None => None,
}
}
/// Goes through all remote markdown links and attempts to resolve them as Activitypub objects.
/// If successful, the link is rewritten to a local link, so it can be viewed without leaving the
/// local instance.
///
/// As it relies on ObjectId::dereference, it can only be used for incoming federated objects, not
/// for the API.
pub async fn markdown_rewrite_remote_links(
mut src: String,
context: &Data<LemmyContext>,
) -> String {
let links_offsets = markdown_find_links(&src);
// Go through the collected links in reverse order
for (start, end) in links_offsets.into_iter().rev() {
let (url, extra) = markdown_handle_title(&src, start, end);
if let Some(local_url) = to_local_url(url, context).await {
let mut local_url = local_url.to_string();
// restore title
if let Some(extra) = extra {
local_url = format!("{local_url} {extra}");
}
src.replace_range(start..end, local_url.as_str());
}
}
src
}
pub(crate) async fn to_local_url(url: &str, context: &Data<LemmyContext>) -> Option<Url> {
let local_domain = &context.settings().get_protocol_and_hostname();
let object_id = ObjectId::<SearchableObjects>::parse(url).ok()?;
if object_id.inner().domain() == Some(local_domain) {
return None;
}
let dereferenced = object_id.dereference(context).await.ok()?;
match dereferenced {
SearchableObjects::PostOrComment(pc) => match *pc {
PostOrComment::Post(post) => {
generate_local_apub_endpoint(EndpointType::Post, &post.id.to_string(), local_domain)
}
PostOrComment::Comment(comment) => {
generate_local_apub_endpoint(EndpointType::Comment, &comment.id.to_string(), local_domain)
}
}
.ok()
.map(Into::into),
SearchableObjects::PersonOrCommunity(pc) => match *pc {
UserOrCommunity::User(user) => {
format_actor_url(&user.name, "u", user.instance_id, context).await
}
UserOrCommunity::Community(community) => {
format_actor_url(&community.name, "c", community.instance_id, context).await
}
}
.ok(),
}
}
async fn format_actor_url(
name: &str,
kind: &str,
instance_id: InstanceId,
context: &LemmyContext,
) -> LemmyResult<Url> {
let local_protocol_and_hostname = context.settings().get_protocol_and_hostname();
let local_hostname = &context.settings().hostname;
let instance = Instance::read(&mut context.pool(), instance_id).await?;
let url = if &instance.domain != local_hostname {
format!(
"{local_protocol_and_hostname}/{kind}/{name}@{}",
instance.domain
)
} else {
format!("{local_protocol_and_hostname}/{kind}/{name}")
};
Ok(Url::parse(&url)?)
}
#[cfg(test)]
mod tests {
use super::*;
use lemmy_db_schema::{
source::{
community::{Community, CommunityInsertForm},
post::{Post, PostInsertForm},
},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[serial]
#[tokio::test]
async fn test_markdown_rewrite_remote_links() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?;
let community = Community::create(
&mut context.pool(),
&CommunityInsertForm::new(
instance.id,
"my_community".to_string(),
"My Community".to_string(),
"pubkey".to_string(),
),
)
.await?;
let user =
LocalUserView::create_test_user(&mut context.pool(), "garda", "garda bio", false).await?;
// insert a remote post which is already fetched
let post_form = PostInsertForm {
ap_id: Some(Url::parse("https://example.com/post/123")?.into()),
..PostInsertForm::new("My post".to_string(), user.person.id, community.id)
};
let post = Post::create(&mut context.pool(), &post_form).await?;
let markdown_local_post_url = format!("[link](https://lemmy-alpha/post/{})", post.id);
let tests: Vec<_> = vec![
(
"rewrite remote post link",
format!("[link]({})", post.ap_id),
markdown_local_post_url.as_ref(),
),
(
"rewrite community link",
format!("[link]({})", community.actor_id),
"[link](https://lemmy-alpha/c/my_community@example.com)",
),
(
"dont rewrite local post link",
"[link](https://lemmy-alpha/post/2)".to_string(),
"[link](https://lemmy-alpha/post/2)",
),
(
"dont rewrite local community link",
"[link](https://lemmy-alpha/c/test)".to_string(),
"[link](https://lemmy-alpha/c/test)",
),
(
"dont rewrite non-fediverse link",
"[link](https://example.com/)".to_string(),
"[link](https://example.com/)",
),
(
"dont rewrite invalid url",
"[link](example-com)".to_string(),
"[link](example-com)",
),
];
let context = LemmyContext::init_test_context().await;
for (msg, input, expected) in &tests {
let result = markdown_rewrite_remote_links(input.to_string(), &context).await;
assert_eq!(
&result, expected,
"Testing {}, with original input '{}'",
msg, input
);
}
Instance::delete(&mut context.pool(), instance.id).await?;
Ok(())
}
}

View file

@ -10,6 +10,7 @@ use lemmy_db_schema::traits::ApubActor;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyError, LemmyResult};
pub(crate) mod markdown_links;
pub mod post_or_comment;
pub mod search;
pub mod site_or_community_or_user;

View file

@ -1,8 +1,5 @@
use crate::{
fetcher::user_or_community::{PersonOrGroup, UserOrCommunity},
objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::objects::{note::Note, page::Page},
};
use super::post_or_comment::{PageOrNote, PostOrComment};
use crate::fetcher::user_or_community::{PersonOrGroup, UserOrCommunity};
use activitypub_federation::{
config::Data,
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
@ -54,16 +51,14 @@ pub(crate) async fn search_query_to_object_id_local(
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
#[derive(Debug)]
pub(crate) enum SearchableObjects {
Post(ApubPost),
Comment(ApubComment),
PostOrComment(Box<PostOrComment>),
PersonOrCommunity(Box<UserOrCommunity>),
}
#[derive(Deserialize)]
#[serde(untagged)]
pub(crate) enum SearchableKinds {
Page(Box<Page>),
Note(Note),
PageOrNote(Box<PageOrNote>),
PersonOrGroup(Box<PersonOrGroup>),
}
@ -75,8 +70,7 @@ impl Object for SearchableObjects {
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
match self {
SearchableObjects::Post(p) => p.last_refreshed_at(),
SearchableObjects::Comment(c) => c.last_refreshed_at(),
SearchableObjects::PostOrComment(p) => p.last_refreshed_at(),
SearchableObjects::PersonOrCommunity(p) => p.last_refreshed_at(),
}
}
@ -95,13 +89,9 @@ impl Object for SearchableObjects {
if let Some(uc) = uc {
return Ok(Some(SearchableObjects::PersonOrCommunity(Box::new(uc))));
}
let p = ApubPost::read_from_id(object_id.clone(), context).await?;
if let Some(p) = p {
return Ok(Some(SearchableObjects::Post(p)));
}
let c = ApubComment::read_from_id(object_id, context).await?;
if let Some(c) = c {
return Ok(Some(SearchableObjects::Comment(c)));
let pc = PostOrComment::read_from_id(object_id.clone(), context).await?;
if let Some(pc) = pc {
return Ok(Some(SearchableObjects::PostOrComment(Box::new(pc))));
}
Ok(None)
}
@ -109,25 +99,16 @@ impl Object for SearchableObjects {
#[tracing::instrument(skip_all)]
async fn delete(self, data: &Data<Self::DataType>) -> LemmyResult<()> {
match self {
SearchableObjects::Post(p) => p.delete(data).await,
SearchableObjects::Comment(c) => c.delete(data).await,
SearchableObjects::PersonOrCommunity(pc) => match *pc {
UserOrCommunity::User(p) => p.delete(data).await,
UserOrCommunity::Community(c) => c.delete(data).await,
},
SearchableObjects::PostOrComment(pc) => pc.delete(data).await,
SearchableObjects::PersonOrCommunity(pc) => pc.delete(data).await,
}
}
async fn into_json(self, data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
use SearchableObjects::*;
Ok(match self {
SearchableObjects::Post(p) => SearchableKinds::Page(Box::new(p.into_json(data).await?)),
SearchableObjects::Comment(c) => SearchableKinds::Note(c.into_json(data).await?),
SearchableObjects::PersonOrCommunity(pc) => {
SearchableKinds::PersonOrGroup(Box::new(match *pc {
UserOrCommunity::User(p) => PersonOrGroup::Person(p.into_json(data).await?),
UserOrCommunity::Community(c) => PersonOrGroup::Group(c.into_json(data).await?),
}))
}
PostOrComment(pc) => SearchableKinds::PageOrNote(Box::new(pc.into_json(data).await?)),
PersonOrCommunity(pc) => SearchableKinds::PersonOrGroup(Box::new(pc.into_json(data).await?)),
})
}
@ -137,24 +118,20 @@ impl Object for SearchableObjects {
expected_domain: &Url,
data: &Data<Self::DataType>,
) -> LemmyResult<()> {
use SearchableKinds::*;
match apub {
SearchableKinds::Page(a) => ApubPost::verify(a, expected_domain, data).await,
SearchableKinds::Note(a) => ApubComment::verify(a, expected_domain, data).await,
SearchableKinds::PersonOrGroup(pg) => match pg.as_ref() {
PersonOrGroup::Person(a) => ApubPerson::verify(a, expected_domain, data).await,
PersonOrGroup::Group(a) => ApubCommunity::verify(a, expected_domain, data).await,
},
PageOrNote(pn) => PostOrComment::verify(pn, expected_domain, data).await,
PersonOrGroup(pg) => UserOrCommunity::verify(pg, expected_domain, data).await,
}
}
#[tracing::instrument(skip_all)]
async fn from_json(apub: Self::Kind, context: &Data<LemmyContext>) -> LemmyResult<Self> {
use SearchableKinds as SAT;
use SearchableKinds::*;
use SearchableObjects as SO;
Ok(match apub {
SAT::Page(p) => SO::Post(ApubPost::from_json(*p, context).await?),
SAT::Note(n) => SO::Comment(ApubComment::from_json(n, context).await?),
SAT::PersonOrGroup(pg) => {
PageOrNote(pg) => SO::PostOrComment(Box::new(PostOrComment::from_json(*pg, context).await?)),
PersonOrGroup(pg) => {
SO::PersonOrCommunity(Box::new(UserOrCommunity::from_json(*pg, context).await?))
}
})

View file

@ -1,5 +1,4 @@
use crate::{
activity_lists::GroupInboxActivities,
collections::{
community_featured::ApubCommunityFeatured,
community_follower::ApubCommunityFollower,
@ -7,15 +6,13 @@ use crate::{
community_outbox::ApubCommunityOutbox,
},
http::{check_community_public, create_apub_response, create_apub_tombstone_response},
objects::{community::ApubCommunity, person::ApubPerson},
objects::community::ApubCommunity,
};
use activitypub_federation::{
actix_web::inbox::receive_activity,
config::Data,
protocol::context::WithContext,
traits::{Collection, Object},
};
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
use actix_web::{web, HttpResponse};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
@ -47,19 +44,6 @@ pub(crate) async fn get_apub_community_http(
create_apub_response(&apub)
}
/// Handler for all incoming receive to community inboxes.
#[tracing::instrument(skip_all)]
pub async fn community_inbox(
request: HttpRequest,
body: Bytes,
data: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> {
receive_activity::<WithContext<GroupInboxActivities>, ApubPerson, LemmyContext>(
request, body, &data,
)
.await
}
/// Returns an empty followers collection, only populating the size (for privacy).
pub(crate) async fn get_apub_community_followers(
info: web::Path<CommunityQuery>,
@ -120,7 +104,6 @@ pub(crate) async fn get_apub_community_featured(
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
pub(crate) mod tests {
use super::*;
@ -182,7 +165,7 @@ pub(crate) mod tests {
}
async fn decode_response<T: DeserializeOwned>(res: HttpResponse) -> LemmyResult<T> {
let body = to_bytes(res.into_body()).await.unwrap();
let body = to_bytes(res.into_body()).await.unwrap_or_default();
let body = std::str::from_utf8(&body)?;
Ok(serde_json::from_str(body)?)
}

View file

@ -1,17 +1,10 @@
use crate::{
activity_lists::PersonInboxActivities,
fetcher::user_or_community::UserOrCommunity,
http::{create_apub_response, create_apub_tombstone_response},
objects::person::ApubPerson,
protocol::collections::empty_outbox::EmptyOutbox,
};
use activitypub_federation::{
actix_web::inbox::receive_activity,
config::Data,
protocol::context::WithContext,
traits::Object,
};
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
use activitypub_federation::{config::Data, traits::Object};
use actix_web::{web, HttpResponse};
use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url};
use lemmy_db_schema::{source::person::Person, traits::ApubActor};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
@ -44,18 +37,6 @@ pub(crate) async fn get_apub_person_http(
}
}
#[tracing::instrument(skip_all)]
pub async fn person_inbox(
request: HttpRequest,
body: Bytes,
data: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> {
receive_activity::<WithContext<PersonInboxActivities>, UserOrCommunity, LemmyContext>(
request, body, &data,
)
.await
}
#[tracing::instrument(skip_all)]
pub(crate) async fn get_apub_person_outbox(
info: web::Path<PersonQuery>,

View file

@ -1,7 +1,6 @@
use crate::http::{
comment::get_apub_comment,
community::{
community_inbox,
get_apub_community_featured,
get_apub_community_followers,
get_apub_community_http,
@ -9,7 +8,7 @@ use crate::http::{
get_apub_community_outbox,
},
get_activity,
person::{get_apub_person_http, get_apub_person_outbox, person_inbox},
person::{get_apub_person_http, get_apub_person_outbox},
post::get_apub_post,
shared_inbox,
site::{get_apub_site_http, get_apub_site_outbox},
@ -56,8 +55,6 @@ pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(
web::scope("")
.guard(InboxRequestGuard)
.route("/c/{community_name}/inbox", web::post().to(community_inbox))
.route("/u/{user_name}/inbox", web::post().to(person_inbox))
.route("/inbox", web::post().to(shared_inbox)),
);
}

View file

@ -1,6 +1,7 @@
use crate::{
activities::{verify_is_public, verify_person_in_community},
check_apub_id_valid_with_strictness,
fetcher::markdown_links::markdown_rewrite_remote_links,
mentions::collect_non_local_mentions,
objects::{read_from_string_or_source, verify_is_remote_object},
protocol::{
@ -104,7 +105,7 @@ impl Object for ApubComment {
} else {
post.ap_id.into()
};
let language = LanguageTag::new_single(self.language_id, &mut context.pool()).await?;
let language = Some(LanguageTag::new_single(self.language_id, &mut context.pool()).await?);
let maa = collect_non_local_mentions(&self, community.actor_id.clone().into(), context).await?;
let note = Note {
@ -128,6 +129,8 @@ impl Object for ApubComment {
Ok(note)
}
/// Recursively fetches all parent comments. This can lead to a stack overflow so we need to
/// Box::pin all large futures on the heap.
#[tracing::instrument(skip_all)]
async fn verify(
note: &Note,
@ -137,14 +140,24 @@ impl Object for ApubComment {
verify_domains_match(note.id.inner(), expected_domain)?;
verify_domains_match(note.attributed_to.inner(), note.id.inner())?;
verify_is_public(&note.to, &note.cc)?;
let community = note.community(context).await?;
let community = Box::pin(note.community(context)).await?;
check_apub_id_valid_with_strictness(note.id.inner(), community.local, context).await?;
Box::pin(check_apub_id_valid_with_strictness(
note.id.inner(),
community.local,
context,
))
.await?;
verify_is_remote_object(&note.id, context)?;
verify_person_in_community(&note.attributed_to, &community, context).await?;
Box::pin(verify_person_in_community(
&note.attributed_to,
&community,
context,
))
.await?;
let (post, _) = note.get_parents(context).await?;
let creator = note.attributed_to.dereference(context).await?;
let (post, _) = Box::pin(note.get_parents(context)).await?;
let creator = Box::pin(note.attributed_to.dereference(context)).await?;
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &creator, community.id)
.await
.is_ok();
@ -169,8 +182,11 @@ impl Object for ApubComment {
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(context).await?;
let content = process_markdown(&content, slur_regex, &url_blocklist, context).await?;
let language_id =
LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?;
let content = markdown_rewrite_remote_links(content, context).await;
let language_id = Some(
LanguageTag::to_language_id_single(note.language.unwrap_or_default(), &mut context.pool())
.await?,
);
let form = CommentInsertForm {
creator_id: creator.id,
@ -284,7 +300,7 @@ pub(crate) mod tests {
let comment = ApubComment::from_json(json, &context).await?;
assert_eq!(comment.ap_id, pleroma_url.into());
assert_eq!(comment.content.len(), 64);
assert_eq!(comment.content.len(), 10);
assert!(!comment.local);
assert_eq!(context.request_count(), 1);

View file

@ -1,10 +1,11 @@
use crate::{
activities::GetActorType,
check_apub_id_valid,
fetcher::markdown_links::markdown_rewrite_remote_links_opt,
local_site_data_cached,
objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt},
protocol::{
objects::{group::Group, Endpoints, LanguageTag},
objects::{group::Group, LanguageTag},
ImageObject,
Source,
},
@ -115,9 +116,7 @@ impl Object for ApubCommunity {
inbox: self.inbox_url.clone().into(),
outbox: generate_outbox_url(&self.actor_id)?.into(),
followers: self.followers_url.clone().map(Into::into),
endpoints: self.shared_inbox_url.clone().map(|s| Endpoints {
shared_inbox: s.into(),
}),
endpoints: None,
public_key: self.public_key(),
language,
published: Some(self.published),
@ -148,6 +147,7 @@ impl Object for ApubCommunity {
let description = read_from_string_or_source_opt(&group.summary, &None, &group.source);
let description =
process_markdown_opt(&description, slur_regex, &url_blocklist, context).await?;
let description = markdown_rewrite_remote_links_opt(description, context).await;
let icon = proxy_image_link_opt_apub(group.icon.map(|i| i.url), context).await?;
let banner = proxy_image_link_opt_apub(group.image.map(|i| i.url), context).await?;
@ -163,8 +163,13 @@ impl Object for ApubCommunity {
banner,
description,
followers_url: group.followers.clone().map(Into::into),
inbox_url: Some(group.inbox.into()),
shared_inbox_url: group.endpoints.map(|e| e.shared_inbox.into()),
inbox_url: Some(
group
.endpoints
.map(|e| e.shared_inbox)
.unwrap_or(group.inbox)
.into(),
),
moderators_url: group.attributed_to.clone().map(Into::into),
posting_restricted_to_mods: group.posting_restricted_to_mods,
featured_url: group.featured.clone().map(Into::into),
@ -223,7 +228,7 @@ impl Actor for ApubCommunity {
}
fn shared_inbox(&self) -> Option<Url> {
self.shared_inbox_url.clone().map(Into::into)
None
}
}
@ -296,7 +301,7 @@ pub(crate) mod tests {
assert!(!community.local);
assert_eq!(
community.description.as_ref().map(std::string::String::len),
Some(132)
Some(63)
);
Community::delete(&mut context.pool(), community.id).await?;

View file

@ -2,6 +2,7 @@ use super::verify_is_remote_object;
use crate::{
activities::GetActorType,
check_apub_id_valid_with_strictness,
fetcher::markdown_links::markdown_rewrite_remote_links_opt,
local_site_data_cached,
objects::read_from_string_or_source_opt,
protocol::{
@ -151,6 +152,7 @@ impl Object for ApubSite {
let url_blocklist = get_url_blocklist(context).await?;
let sidebar = read_from_string_or_source_opt(&apub.content, &None, &apub.source);
let sidebar = process_markdown_opt(&sidebar, slur_regex, &url_blocklist, context).await?;
let sidebar = markdown_rewrite_remote_links_opt(sidebar, context).await;
let icon = proxy_image_link_opt_apub(apub.icon.map(|i| i.url), context).await?;
let banner = proxy_image_link_opt_apub(apub.image.map(|i| i.url), context).await?;

View file

@ -2,13 +2,11 @@ use super::verify_is_remote_object;
use crate::{
activities::GetActorType,
check_apub_id_valid_with_strictness,
fetcher::markdown_links::markdown_rewrite_remote_links_opt,
local_site_data_cached,
objects::{instance::fetch_instance_actor_for_object, read_from_string_or_source_opt},
protocol::{
objects::{
person::{Person, UserTypes},
Endpoints,
},
objects::person::{Person, UserTypes},
ImageObject,
Source,
},
@ -117,9 +115,7 @@ impl Object for ApubPerson {
matrix_user_id: self.matrix_user_id.clone(),
published: Some(self.published),
outbox: generate_outbox_url(&self.actor_id)?.into(),
endpoints: self.shared_inbox_url.clone().map(|s| Endpoints {
shared_inbox: s.into(),
}),
endpoints: None,
public_key: self.public_key(),
updated: self.updated,
inbox: self.inbox_url.clone().into(),
@ -156,6 +152,7 @@ impl Object for ApubPerson {
let url_blocklist = get_url_blocklist(context).await?;
let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source);
let bio = process_markdown_opt(&bio, slur_regex, &url_blocklist, context).await?;
let bio = markdown_rewrite_remote_links_opt(bio, context).await;
let avatar = proxy_image_link_opt_apub(person.icon.map(|i| i.url), context).await?;
let banner = proxy_image_link_opt_apub(person.image.map(|i| i.url), context).await?;
@ -180,8 +177,13 @@ impl Object for ApubPerson {
private_key: None,
public_key: person.public_key.public_key_pem,
last_refreshed_at: Some(naive_now()),
inbox_url: Some(person.inbox.into()),
shared_inbox_url: person.endpoints.map(|e| e.shared_inbox.into()),
inbox_url: Some(
person
.endpoints
.map(|e| e.shared_inbox)
.unwrap_or(person.inbox)
.into(),
),
matrix_user_id: person.matrix_user_id,
instance_id,
};
@ -209,7 +211,7 @@ impl Actor for ApubPerson {
}
fn shared_inbox(&self) -> Option<Url> {
self.shared_inbox_url.clone().map(Into::into)
None
}
}
@ -277,7 +279,7 @@ pub(crate) mod tests {
assert_eq!(person.name, "lanodan");
assert!(!person.local);
assert_eq!(context.request_count(), 0);
assert_eq!(person.bio.as_ref().map(std::string::String::len), Some(873));
assert_eq!(person.bio.as_ref().map(std::string::String::len), Some(812));
cleanup((person, site), &context).await?;
Ok(())

View file

@ -1,6 +1,7 @@
use crate::{
activities::{verify_is_public, verify_person_in_community},
check_apub_id_valid_with_strictness,
fetcher::markdown_links::{markdown_rewrite_remote_links_opt, to_local_url},
local_site_data_cached,
objects::{read_from_string_or_source_opt, verify_is_remote_object},
protocol::{
@ -110,7 +111,7 @@ impl Object for ApubPost {
let creator = Person::read(&mut context.pool(), creator_id).await?;
let community_id = self.community_id;
let community = Community::read(&mut context.pool(), community_id).await?;
let language = LanguageTag::new_single(self.language_id, &mut context.pool()).await?;
let language = Some(LanguageTag::new_single(self.language_id, &mut context.pool()).await?);
let attachment = self
.url
@ -226,10 +227,13 @@ impl Object for ApubPost {
let url_blocklist = get_url_blocklist(context).await?;
if let Some(url) = &url {
is_url_blocked(url, &url_blocklist)?;
is_valid_url(url)?;
}
let url = if let Some(url) = url {
is_url_blocked(&url, &url_blocklist)?;
is_valid_url(&url)?;
to_local_url(url.as_str(), context).await.or(Some(url))
} else {
None
};
let alt_text = first_attachment.cloned().and_then(Attachment::alt_text);
@ -237,8 +241,11 @@ impl Object for ApubPost {
let body = read_from_string_or_source_opt(&page.content, &page.media_type, &page.source);
let body = process_markdown_opt(&body, slur_regex, &url_blocklist, context).await?;
let language_id =
LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?;
let body = markdown_rewrite_remote_links_opt(body, context).await;
let language_id = Some(
LanguageTag::to_language_id_single(page.language.unwrap_or_default(), &mut context.pool())
.await?,
);
let form = PostInsertForm {
url: url.map(Into::into),
@ -301,7 +308,7 @@ mod tests {
assert_eq!(post.body.as_ref().map(std::string::String::len), Some(45));
assert!(!post.locked);
assert!(!post.featured_community);
assert_eq!(context.request_count(), 0);
assert_eq!(context.request_count(), 1);
Post::delete(&mut context.pool(), post.id).await?;
Person::delete(&mut context.pool(), person.id).await?;

View file

@ -1,6 +1,7 @@
use super::verify_is_remote_object;
use crate::{
check_apub_id_valid_with_strictness,
fetcher::markdown_links::markdown_rewrite_remote_links,
objects::read_from_string_or_source,
protocol::{
objects::chat_message::{ChatMessage, ChatMessageType},
@ -134,6 +135,7 @@ impl Object for ApubPrivateMessage {
let url_blocklist = get_url_blocklist(context).await?;
let content = read_from_string_or_source(&note.content, &None, &note.source);
let content = process_markdown(&content, slur_regex, &url_blocklist, context).await?;
let content = markdown_rewrite_remote_links(content, context).await;
let form = PrivateMessageInsertForm {
creator_id: creator.id,

View file

@ -30,21 +30,30 @@ pub(crate) struct LanguageTag {
pub(crate) name: String,
}
impl Default for LanguageTag {
fn default() -> Self {
LanguageTag {
identifier: "und".to_string(),
name: "Undetermined".to_string(),
}
}
}
impl LanguageTag {
pub(crate) async fn new_single(
lang: LanguageId,
pool: &mut DbPool<'_>,
) -> LemmyResult<Option<LanguageTag>> {
) -> LemmyResult<LanguageTag> {
let lang = Language::read_from_id(pool, lang).await?;
// undetermined
if lang.id == UNDETERMINED_ID {
Ok(None)
Ok(LanguageTag::default())
} else {
Ok(Some(LanguageTag {
Ok(LanguageTag {
identifier: lang.code,
name: lang.name,
}))
})
}
}
@ -69,13 +78,10 @@ impl LanguageTag {
}
pub(crate) async fn to_language_id_single(
lang: Option<Self>,
lang: Self,
pool: &mut DbPool<'_>,
) -> LemmyResult<Option<LanguageId>> {
let identifier = lang.map(|l| l.identifier);
let language = Language::read_id_from_code(pool, identifier.as_deref()).await?;
Ok(language)
) -> LemmyResult<LanguageId> {
Ok(Language::read_id_from_code(pool, &lang.identifier).await?)
}
pub(crate) async fn to_language_id_multiple(
@ -86,10 +92,10 @@ impl LanguageTag {
for l in langs {
let id = l.identifier;
language_ids.push(Language::read_id_from_code(pool, Some(&id)).await?);
language_ids.push(Language::read_id_from_code(pool, &id).await?);
}
Ok(language_ids.into_iter().flatten().collect())
Ok(language_ids.into_iter().collect())
}
}

View file

@ -30,7 +30,6 @@ impl CommentAggregates {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
@ -45,26 +44,25 @@ mod tests {
traits::{Crud, Likeable},
utils::build_db_pool_for_tests,
};
use diesel::result::Error;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_comment_agg");
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let inserted_person = Person::create(pool, &new_person).await?;
let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_comment_agg");
let another_inserted_person = Person::create(pool, &another_person).await.unwrap();
let another_inserted_person = Person::create(pool, &another_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -72,21 +70,21 @@ mod tests {
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let inserted_community = Community::create(pool, &new_community).await?;
let new_post = PostInsertForm::new(
"A test post".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
let comment_form = CommentInsertForm::new(
inserted_person.id,
inserted_post.id,
"A test comment".into(),
);
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
let child_comment_form = CommentInsertForm::new(
inserted_person.id,
@ -94,22 +92,17 @@ mod tests {
"A test comment".into(),
);
let _inserted_child_comment =
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path))
.await
.unwrap();
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?;
let comment_like = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
CommentLike::like(pool, &comment_like).await.unwrap();
CommentLike::like(pool, &comment_like).await?;
let comment_aggs_before_delete = CommentAggregates::read(pool, inserted_comment.id)
.await
.unwrap();
let comment_aggs_before_delete = CommentAggregates::read(pool, inserted_comment.id).await?;
assert_eq!(1, comment_aggs_before_delete.score);
assert_eq!(1, comment_aggs_before_delete.upvotes);
@ -118,52 +111,43 @@ mod tests {
// Add a post dislike from the other person
let comment_dislike = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
person_id: another_inserted_person.id,
score: -1,
};
CommentLike::like(pool, &comment_dislike).await.unwrap();
CommentLike::like(pool, &comment_dislike).await?;
let comment_aggs_after_dislike = CommentAggregates::read(pool, inserted_comment.id)
.await
.unwrap();
let comment_aggs_after_dislike = CommentAggregates::read(pool, inserted_comment.id).await?;
assert_eq!(0, comment_aggs_after_dislike.score);
assert_eq!(1, comment_aggs_after_dislike.upvotes);
assert_eq!(1, comment_aggs_after_dislike.downvotes);
// Remove the first comment like
CommentLike::remove(pool, inserted_person.id, inserted_comment.id)
.await
.unwrap();
let after_like_remove = CommentAggregates::read(pool, inserted_comment.id)
.await
.unwrap();
CommentLike::remove(pool, inserted_person.id, inserted_comment.id).await?;
let after_like_remove = CommentAggregates::read(pool, inserted_comment.id).await?;
assert_eq!(-1, after_like_remove.score);
assert_eq!(0, after_like_remove.upvotes);
assert_eq!(1, after_like_remove.downvotes);
// Remove the parent post
Post::delete(pool, inserted_post.id).await.unwrap();
Post::delete(pool, inserted_post.id).await?;
// Should be none found, since the post was deleted
let after_delete = CommentAggregates::read(pool, inserted_comment.id).await;
assert!(after_delete.is_err());
// This should delete all the associated rows, and fire triggers
Person::delete(pool, another_inserted_person.id)
.await
.unwrap();
let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap();
Person::delete(pool, another_inserted_person.id).await?;
let person_num_deleted = Person::delete(pool, inserted_person.id).await?;
assert_eq!(1, person_num_deleted);
// Delete the community
let community_num_deleted = Community::delete(pool, inserted_community.id)
.await
.unwrap();
let community_num_deleted = Community::delete(pool, inserted_community.id).await?;
assert_eq!(1, community_num_deleted);
Instance::delete(pool, inserted_instance.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
}

View file

@ -1,6 +1,5 @@
use crate::{
aggregates::structs::CommunityAggregates,
diesel::OptionalExtension,
newtypes::CommunityId,
schema::{community_aggregates, community_aggregates::subscribers},
utils::{get_conn, DbPool},
@ -9,16 +8,12 @@ use diesel::{result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
impl CommunityAggregates {
pub async fn read(
pool: &mut DbPool<'_>,
for_community_id: CommunityId,
) -> Result<Option<Self>, Error> {
pub async fn read(pool: &mut DbPool<'_>, for_community_id: CommunityId) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
community_aggregates::table
.find(for_community_id)
.first(conn)
.await
.optional()
}
pub async fn update_federated_followers(
@ -36,7 +31,6 @@ impl CommunityAggregates {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
@ -51,26 +45,25 @@ mod tests {
traits::{Crud, Followable},
utils::build_db_pool_for_tests,
};
use diesel::result::Error;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg");
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let inserted_person = Person::create(pool, &new_person).await?;
let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg");
let another_inserted_person = Person::create(pool, &another_person).await.unwrap();
let another_inserted_person = Person::create(pool, &another_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -78,7 +71,7 @@ mod tests {
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let inserted_community = Community::create(pool, &new_community).await?;
let another_community = CommunityInsertForm::new(
inserted_instance.id,
@ -86,7 +79,7 @@ mod tests {
"nada".to_owned(),
"pubkey".to_string(),
);
let another_inserted_community = Community::create(pool, &another_community).await.unwrap();
let another_inserted_community = Community::create(pool, &another_community).await?;
let first_person_follow = CommunityFollowerForm {
community_id: inserted_community.id,
@ -94,9 +87,7 @@ mod tests {
pending: false,
};
CommunityFollower::follow(pool, &first_person_follow)
.await
.unwrap();
CommunityFollower::follow(pool, &first_person_follow).await?;
let second_person_follow = CommunityFollowerForm {
community_id: inserted_community.id,
@ -104,9 +95,7 @@ mod tests {
pending: false,
};
CommunityFollower::follow(pool, &second_person_follow)
.await
.unwrap();
CommunityFollower::follow(pool, &second_person_follow).await?;
let another_community_follow = CommunityFollowerForm {
community_id: another_inserted_community.id,
@ -114,23 +103,21 @@ mod tests {
pending: false,
};
CommunityFollower::follow(pool, &another_community_follow)
.await
.unwrap();
CommunityFollower::follow(pool, &another_community_follow).await?;
let new_post = PostInsertForm::new(
"A test post".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
let comment_form = CommentInsertForm::new(
inserted_person.id,
inserted_post.id,
"A test comment".into(),
);
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
let child_comment_form = CommentInsertForm::new(
inserted_person.id,
@ -138,14 +125,10 @@ mod tests {
"A test comment".into(),
);
let _inserted_child_comment =
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path))
.await
.unwrap();
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?;
let community_aggregates_before_delete = CommunityAggregates::read(pool, inserted_community.id)
.await
.unwrap()
.unwrap();
let community_aggregates_before_delete =
CommunityAggregates::read(pool, inserted_community.id).await?;
assert_eq!(2, community_aggregates_before_delete.subscribers);
assert_eq!(2, community_aggregates_before_delete.subscribers_local);
@ -153,76 +136,53 @@ mod tests {
assert_eq!(2, community_aggregates_before_delete.comments);
// Test the other community
let another_community_aggs = CommunityAggregates::read(pool, another_inserted_community.id)
.await
.unwrap()
.unwrap();
let another_community_aggs =
CommunityAggregates::read(pool, another_inserted_community.id).await?;
assert_eq!(1, another_community_aggs.subscribers);
assert_eq!(1, another_community_aggs.subscribers_local);
assert_eq!(0, another_community_aggs.posts);
assert_eq!(0, another_community_aggs.comments);
// Unfollow test
CommunityFollower::unfollow(pool, &second_person_follow)
.await
.unwrap();
let after_unfollow = CommunityAggregates::read(pool, inserted_community.id)
.await
.unwrap()
.unwrap();
CommunityFollower::unfollow(pool, &second_person_follow).await?;
let after_unfollow = CommunityAggregates::read(pool, inserted_community.id).await?;
assert_eq!(1, after_unfollow.subscribers);
assert_eq!(1, after_unfollow.subscribers_local);
// Follow again just for the later tests
CommunityFollower::follow(pool, &second_person_follow)
.await
.unwrap();
let after_follow_again = CommunityAggregates::read(pool, inserted_community.id)
.await
.unwrap()
.unwrap();
CommunityFollower::follow(pool, &second_person_follow).await?;
let after_follow_again = CommunityAggregates::read(pool, inserted_community.id).await?;
assert_eq!(2, after_follow_again.subscribers);
assert_eq!(2, after_follow_again.subscribers_local);
// Remove a parent post (the comment count should also be 0)
Post::delete(pool, inserted_post.id).await.unwrap();
let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id)
.await
.unwrap()
.unwrap();
Post::delete(pool, inserted_post.id).await?;
let after_parent_post_delete = CommunityAggregates::read(pool, inserted_community.id).await?;
assert_eq!(0, after_parent_post_delete.comments);
assert_eq!(0, after_parent_post_delete.posts);
// Remove the 2nd person
Person::delete(pool, another_inserted_person.id)
.await
.unwrap();
let after_person_delete = CommunityAggregates::read(pool, inserted_community.id)
.await
.unwrap()
.unwrap();
Person::delete(pool, another_inserted_person.id).await?;
let after_person_delete = CommunityAggregates::read(pool, inserted_community.id).await?;
assert_eq!(1, after_person_delete.subscribers);
assert_eq!(1, after_person_delete.subscribers_local);
// This should delete all the associated rows, and fire triggers
let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap();
let person_num_deleted = Person::delete(pool, inserted_person.id).await?;
assert_eq!(1, person_num_deleted);
// Delete the community
let community_num_deleted = Community::delete(pool, inserted_community.id)
.await
.unwrap();
let community_num_deleted = Community::delete(pool, inserted_community.id).await?;
assert_eq!(1, community_num_deleted);
let another_community_num_deleted = Community::delete(pool, another_inserted_community.id)
.await
.unwrap();
let another_community_num_deleted =
Community::delete(pool, another_inserted_community.id).await?;
assert_eq!(1, another_community_num_deleted);
// Should be none found, since the creator was deleted
let after_delete = CommunityAggregates::read(pool, inserted_community.id)
.await
.unwrap();
assert!(after_delete.is_none());
let after_delete = CommunityAggregates::read(pool, inserted_community.id).await;
assert!(after_delete.is_err());
Ok(())
}
}

View file

@ -1,4 +1,3 @@
pub(crate) use crate::diesel::OptionalExtension;
use crate::{
aggregates::structs::PersonAggregates,
newtypes::PersonId,
@ -9,18 +8,13 @@ use diesel::{result::Error, QueryDsl};
use diesel_async::RunQueryDsl;
impl PersonAggregates {
pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result<Option<Self>, Error> {
pub async fn read(pool: &mut DbPool<'_>, person_id: PersonId) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
person_aggregates::table
.find(person_id)
.first(conn)
.await
.optional()
person_aggregates::table.find(person_id).first(conn).await
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
@ -35,26 +29,25 @@ mod tests {
traits::{Crud, Likeable},
utils::build_db_pool_for_tests,
};
use diesel::result::Error;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_user_agg");
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let inserted_person = Person::create(pool, &new_person).await?;
let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_user_agg");
let another_inserted_person = Person::create(pool, &another_person).await.unwrap();
let another_inserted_person = Person::create(pool, &another_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -63,37 +56,36 @@ mod tests {
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let inserted_community = Community::create(pool, &new_community).await?;
let new_post = PostInsertForm::new(
"A test post".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
let post_like = PostLikeForm {
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
let _inserted_post_like = PostLike::like(pool, &post_like).await.unwrap();
let _inserted_post_like = PostLike::like(pool, &post_like).await?;
let comment_form = CommentInsertForm::new(
inserted_person.id,
inserted_post.id,
"A test comment".into(),
);
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
let mut comment_like = CommentLikeForm {
comment_id: inserted_comment.id,
person_id: inserted_person.id,
post_id: inserted_post.id,
score: 1,
};
let _inserted_comment_like = CommentLike::like(pool, &comment_like).await.unwrap();
let _inserted_comment_like = CommentLike::like(pool, &comment_like).await?;
let child_comment_form = CommentInsertForm::new(
inserted_person.id,
@ -101,23 +93,17 @@ mod tests {
"A test comment".into(),
);
let inserted_child_comment =
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path))
.await
.unwrap();
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?;
let child_comment_like = CommentLikeForm {
comment_id: inserted_child_comment.id,
person_id: another_inserted_person.id,
post_id: inserted_post.id,
score: 1,
};
let _inserted_child_comment_like = CommentLike::like(pool, &child_comment_like).await.unwrap();
let _inserted_child_comment_like = CommentLike::like(pool, &child_comment_like).await?;
let person_aggregates_before_delete = PersonAggregates::read(pool, inserted_person.id)
.await
.unwrap()
.unwrap();
let person_aggregates_before_delete = PersonAggregates::read(pool, inserted_person.id).await?;
assert_eq!(1, person_aggregates_before_delete.post_count);
assert_eq!(1, person_aggregates_before_delete.post_score);
@ -125,13 +111,8 @@ mod tests {
assert_eq!(2, person_aggregates_before_delete.comment_score);
// Remove a post like
PostLike::remove(pool, inserted_person.id, inserted_post.id)
.await
.unwrap();
let after_post_like_remove = PersonAggregates::read(pool, inserted_person.id)
.await
.unwrap()
.unwrap();
PostLike::remove(pool, inserted_person.id, inserted_post.id).await?;
let after_post_like_remove = PersonAggregates::read(pool, inserted_person.id).await?;
assert_eq!(0, after_post_like_remove.post_score);
Comment::update(
@ -142,8 +123,7 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
Comment::update(
pool,
inserted_child_comment.id,
@ -152,51 +132,34 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
let after_parent_comment_removed = PersonAggregates::read(pool, inserted_person.id)
.await
.unwrap()
.unwrap();
let after_parent_comment_removed = PersonAggregates::read(pool, inserted_person.id).await?;
assert_eq!(0, after_parent_comment_removed.comment_count);
// TODO: fix person aggregate comment score calculation
// assert_eq!(0, after_parent_comment_removed.comment_score);
// Remove a parent comment (the scores should also be removed)
Comment::delete(pool, inserted_comment.id).await.unwrap();
Comment::delete(pool, inserted_child_comment.id)
.await
.unwrap();
let after_parent_comment_delete = PersonAggregates::read(pool, inserted_person.id)
.await
.unwrap()
.unwrap();
Comment::delete(pool, inserted_comment.id).await?;
Comment::delete(pool, inserted_child_comment.id).await?;
let after_parent_comment_delete = PersonAggregates::read(pool, inserted_person.id).await?;
assert_eq!(0, after_parent_comment_delete.comment_count);
// TODO: fix person aggregate comment score calculation
// assert_eq!(0, after_parent_comment_delete.comment_score);
// Add in the two comments again, then delete the post.
let new_parent_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let new_parent_comment = Comment::create(pool, &comment_form, None).await?;
let _new_child_comment =
Comment::create(pool, &child_comment_form, Some(&new_parent_comment.path))
.await
.unwrap();
Comment::create(pool, &child_comment_form, Some(&new_parent_comment.path)).await?;
comment_like.comment_id = new_parent_comment.id;
CommentLike::like(pool, &comment_like).await.unwrap();
let after_comment_add = PersonAggregates::read(pool, inserted_person.id)
.await
.unwrap()
.unwrap();
CommentLike::like(pool, &comment_like).await?;
let after_comment_add = PersonAggregates::read(pool, inserted_person.id).await?;
assert_eq!(2, after_comment_add.comment_count);
// TODO: fix person aggregate comment score calculation
// assert_eq!(1, after_comment_add.comment_score);
Post::delete(pool, inserted_post.id).await.unwrap();
let after_post_delete = PersonAggregates::read(pool, inserted_person.id)
.await
.unwrap()
.unwrap();
Post::delete(pool, inserted_post.id).await?;
let after_post_delete = PersonAggregates::read(pool, inserted_person.id).await?;
// TODO: fix person aggregate comment score calculation
// assert_eq!(0, after_post_delete.comment_score);
assert_eq!(0, after_post_delete.comment_count);
@ -204,24 +167,20 @@ mod tests {
assert_eq!(0, after_post_delete.post_count);
// This should delete all the associated rows, and fire triggers
let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap();
let person_num_deleted = Person::delete(pool, inserted_person.id).await?;
assert_eq!(1, person_num_deleted);
Person::delete(pool, another_inserted_person.id)
.await
.unwrap();
Person::delete(pool, another_inserted_person.id).await?;
// Delete the community
let community_num_deleted = Community::delete(pool, inserted_community.id)
.await
.unwrap();
let community_num_deleted = Community::delete(pool, inserted_community.id).await?;
assert_eq!(1, community_num_deleted);
// Should be none found
let after_delete = PersonAggregates::read(pool, inserted_person.id)
.await
.unwrap();
assert!(after_delete.is_none());
let after_delete = PersonAggregates::read(pool, inserted_person.id).await;
assert!(after_delete.is_err());
Instance::delete(pool, inserted_instance.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
}

View file

@ -49,8 +49,6 @@ impl PostAggregates {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
@ -65,26 +63,25 @@ mod tests {
traits::{Crud, Likeable},
utils::build_db_pool_for_tests,
};
use diesel::result::Error;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg");
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let inserted_person = Person::create(pool, &new_person).await?;
let another_person = PersonInsertForm::test_form(inserted_instance.id, "jerry_community_agg");
let another_inserted_person = Person::create(pool, &another_person).await.unwrap();
let another_inserted_person = Person::create(pool, &another_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -92,21 +89,21 @@ mod tests {
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let inserted_community = Community::create(pool, &new_community).await?;
let new_post = PostInsertForm::new(
"A test post".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
let comment_form = CommentInsertForm::new(
inserted_person.id,
inserted_post.id,
"A test comment".into(),
);
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
let child_comment_form = CommentInsertForm::new(
inserted_person.id,
@ -114,9 +111,7 @@ mod tests {
"A test comment".into(),
);
let inserted_child_comment =
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path))
.await
.unwrap();
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?;
let post_like = PostLikeForm {
post_id: inserted_post.id,
@ -124,9 +119,9 @@ mod tests {
score: 1,
};
PostLike::like(pool, &post_like).await.unwrap();
PostLike::like(pool, &post_like).await?;
let post_aggs_before_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap();
let post_aggs_before_delete = PostAggregates::read(pool, inserted_post.id).await?;
assert_eq!(2, post_aggs_before_delete.comments);
assert_eq!(1, post_aggs_before_delete.score);
@ -140,9 +135,9 @@ mod tests {
score: -1,
};
PostLike::like(pool, &post_dislike).await.unwrap();
PostLike::like(pool, &post_dislike).await?;
let post_aggs_after_dislike = PostAggregates::read(pool, inserted_post.id).await.unwrap();
let post_aggs_after_dislike = PostAggregates::read(pool, inserted_post.id).await?;
assert_eq!(2, post_aggs_after_dislike.comments);
assert_eq!(0, post_aggs_after_dislike.score);
@ -150,59 +145,51 @@ mod tests {
assert_eq!(1, post_aggs_after_dislike.downvotes);
// Remove the comments
Comment::delete(pool, inserted_comment.id).await.unwrap();
Comment::delete(pool, inserted_child_comment.id)
.await
.unwrap();
let after_comment_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap();
Comment::delete(pool, inserted_comment.id).await?;
Comment::delete(pool, inserted_child_comment.id).await?;
let after_comment_delete = PostAggregates::read(pool, inserted_post.id).await?;
assert_eq!(0, after_comment_delete.comments);
assert_eq!(0, after_comment_delete.score);
assert_eq!(1, after_comment_delete.upvotes);
assert_eq!(1, after_comment_delete.downvotes);
// Remove the first post like
PostLike::remove(pool, inserted_person.id, inserted_post.id)
.await
.unwrap();
let after_like_remove = PostAggregates::read(pool, inserted_post.id).await.unwrap();
PostLike::remove(pool, inserted_person.id, inserted_post.id).await?;
let after_like_remove = PostAggregates::read(pool, inserted_post.id).await?;
assert_eq!(0, after_like_remove.comments);
assert_eq!(-1, after_like_remove.score);
assert_eq!(0, after_like_remove.upvotes);
assert_eq!(1, after_like_remove.downvotes);
// This should delete all the associated rows, and fire triggers
Person::delete(pool, another_inserted_person.id)
.await
.unwrap();
let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap();
Person::delete(pool, another_inserted_person.id).await?;
let person_num_deleted = Person::delete(pool, inserted_person.id).await?;
assert_eq!(1, person_num_deleted);
// Delete the community
let community_num_deleted = Community::delete(pool, inserted_community.id)
.await
.unwrap();
let community_num_deleted = Community::delete(pool, inserted_community.id).await?;
assert_eq!(1, community_num_deleted);
// Should be none found, since the creator was deleted
let after_delete = PostAggregates::read(pool, inserted_post.id).await;
assert!(after_delete.is_err());
Instance::delete(pool, inserted_instance.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_soft_delete() {
async fn test_soft_delete() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_community_agg");
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let inserted_person = Person::create(pool, &new_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -210,14 +197,14 @@ mod tests {
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let inserted_community = Community::create(pool, &new_community).await?;
let new_post = PostInsertForm::new(
"A test post".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
let comment_form = CommentInsertForm::new(
inserted_person.id,
@ -225,9 +212,9 @@ mod tests {
"A test comment".into(),
);
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
let post_aggregates_before = PostAggregates::read(pool, inserted_post.id).await.unwrap();
let post_aggregates_before = PostAggregates::read(pool, inserted_post.id).await?;
assert_eq!(1, post_aggregates_before.comments);
Comment::update(
@ -238,10 +225,9 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
let post_aggregates_after_remove = PostAggregates::read(pool, inserted_post.id).await.unwrap();
let post_aggregates_after_remove = PostAggregates::read(pool, inserted_post.id).await?;
assert_eq!(0, post_aggregates_after_remove.comments);
Comment::update(
@ -252,8 +238,7 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
Comment::update(
pool,
@ -263,10 +248,9 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
let post_aggregates_after_delete = PostAggregates::read(pool, inserted_post.id).await.unwrap();
let post_aggregates_after_delete = PostAggregates::read(pool, inserted_post.id).await?;
assert_eq!(0, post_aggregates_after_delete.comments);
Comment::update(
@ -277,19 +261,17 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
let post_aggregates_after_delete_remove =
PostAggregates::read(pool, inserted_post.id).await.unwrap();
let post_aggregates_after_delete_remove = PostAggregates::read(pool, inserted_post.id).await?;
assert_eq!(0, post_aggregates_after_delete_remove.comments);
Comment::delete(pool, inserted_comment.id).await.unwrap();
Post::delete(pool, inserted_post.id).await.unwrap();
Person::delete(pool, inserted_person.id).await.unwrap();
Community::delete(pool, inserted_community.id)
.await
.unwrap();
Instance::delete(pool, inserted_instance.id).await.unwrap();
Comment::delete(pool, inserted_comment.id).await?;
Post::delete(pool, inserted_post.id).await?;
Person::delete(pool, inserted_person.id).await?;
Community::delete(pool, inserted_community.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
}

View file

@ -1,6 +1,5 @@
use crate::{
aggregates::structs::SiteAggregates,
diesel::OptionalExtension,
schema::site_aggregates,
utils::{get_conn, DbPool},
};
@ -8,15 +7,13 @@ use diesel::result::Error;
use diesel_async::RunQueryDsl;
impl SiteAggregates {
pub async fn read(pool: &mut DbPool<'_>) -> Result<Option<Self>, Error> {
pub async fn read(pool: &mut DbPool<'_>) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
site_aggregates::table.first(conn).await.optional()
site_aggregates::table.first(conn).await
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
@ -32,22 +29,21 @@ mod tests {
traits::Crud,
utils::{build_db_pool_for_tests, DbPool},
};
use diesel::result::Error;
use pretty_assertions::assert_eq;
use serial_test::serial;
async fn prepare_site_with_community(
pool: &mut DbPool<'_>,
) -> (Instance, Person, Site, Community) {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
) -> Result<(Instance, Person, Site, Community), Error> {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy_site_agg");
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let inserted_person = Person::create(pool, &new_person).await?;
let site_form = SiteInsertForm::new("test_site".into(), inserted_instance.id);
let inserted_site = Site::create(pool, &site_form).await.unwrap();
let inserted_site = Site::create(pool, &site_form).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -56,23 +52,24 @@ mod tests {
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
(
let inserted_community = Community::create(pool, &new_community).await?;
Ok((
inserted_instance,
inserted_person,
inserted_site,
inserted_community,
)
))
}
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let (inserted_instance, inserted_person, inserted_site, inserted_community) =
prepare_site_with_community(pool).await;
prepare_site_with_community(pool).await?;
let new_post = PostInsertForm::new(
"A test post".into(),
@ -81,8 +78,8 @@ mod tests {
);
// Insert two of those posts
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let _inserted_post_again = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
let _inserted_post_again = Post::create(pool, &new_post).await?;
let comment_form = CommentInsertForm::new(
inserted_person.id,
@ -91,7 +88,7 @@ mod tests {
);
// Insert two of those comments
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
let child_comment_form = CommentInsertForm::new(
inserted_person.id,
@ -99,11 +96,9 @@ mod tests {
"A test comment".into(),
);
let _inserted_child_comment =
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path))
.await
.unwrap();
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?;
let site_aggregates_before_delete = SiteAggregates::read(pool).await.unwrap().unwrap();
let site_aggregates_before_delete = SiteAggregates::read(pool).await?;
// TODO: this is unstable, sometimes it returns 0 users, sometimes 1
//assert_eq!(0, site_aggregates_before_delete.users);
@ -112,42 +107,42 @@ mod tests {
assert_eq!(2, site_aggregates_before_delete.comments);
// Try a post delete
Post::delete(pool, inserted_post.id).await.unwrap();
let site_aggregates_after_post_delete = SiteAggregates::read(pool).await.unwrap().unwrap();
Post::delete(pool, inserted_post.id).await?;
let site_aggregates_after_post_delete = SiteAggregates::read(pool).await?;
assert_eq!(1, site_aggregates_after_post_delete.posts);
assert_eq!(0, site_aggregates_after_post_delete.comments);
// This shouuld delete all the associated rows, and fire triggers
let person_num_deleted = Person::delete(pool, inserted_person.id).await.unwrap();
let person_num_deleted = Person::delete(pool, inserted_person.id).await?;
assert_eq!(1, person_num_deleted);
// Delete the community
let community_num_deleted = Community::delete(pool, inserted_community.id)
.await
.unwrap();
let community_num_deleted = Community::delete(pool, inserted_community.id).await?;
assert_eq!(1, community_num_deleted);
// Site should still exist, it can without a site creator.
let after_delete_creator = SiteAggregates::read(pool).await;
assert!(after_delete_creator.is_ok());
Site::delete(pool, inserted_site.id).await.unwrap();
let after_delete_site = SiteAggregates::read(pool).await.unwrap();
assert!(after_delete_site.is_none());
Site::delete(pool, inserted_site.id).await?;
let after_delete_site = SiteAggregates::read(pool).await;
assert!(after_delete_site.is_err());
Instance::delete(pool, inserted_instance.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_soft_delete() {
async fn test_soft_delete() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let (inserted_instance, inserted_person, inserted_site, inserted_community) =
prepare_site_with_community(pool).await;
prepare_site_with_community(pool).await?;
let site_aggregates_before = SiteAggregates::read(pool).await.unwrap().unwrap();
let site_aggregates_before = SiteAggregates::read(pool).await?;
assert_eq!(1, site_aggregates_before.communities);
Community::update(
@ -158,10 +153,9 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
let site_aggregates_after_delete = SiteAggregates::read(pool).await.unwrap().unwrap();
let site_aggregates_after_delete = SiteAggregates::read(pool).await?;
assert_eq!(0, site_aggregates_after_delete.communities);
Community::update(
@ -172,8 +166,7 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
Community::update(
pool,
@ -183,10 +176,9 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
let site_aggregates_after_remove = SiteAggregates::read(pool).await.unwrap().unwrap();
let site_aggregates_after_remove = SiteAggregates::read(pool).await?;
assert_eq!(0, site_aggregates_after_remove.communities);
Community::update(
@ -197,17 +189,16 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
let site_aggregates_after_remove_delete = SiteAggregates::read(pool).await.unwrap().unwrap();
let site_aggregates_after_remove_delete = SiteAggregates::read(pool).await?;
assert_eq!(0, site_aggregates_after_remove_delete.communities);
Community::delete(pool, inserted_community.id)
.await
.unwrap();
Site::delete(pool, inserted_site.id).await.unwrap();
Person::delete(pool, inserted_person.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await.unwrap();
Community::delete(pool, inserted_community.id).await?;
Site::delete(pool, inserted_site.id).await?;
Person::delete(pool, inserted_person.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
}

View file

@ -58,11 +58,11 @@ impl ReceivedActivity {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;
use crate::{source::activity::ActorType, utils::build_db_pool_for_tests};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serde_json::json;
use serial_test::serial;
@ -70,26 +70,25 @@ mod tests {
#[tokio::test]
#[serial]
async fn receive_activity_duplicate() {
async fn receive_activity_duplicate() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let ap_id: DbUrl = Url::parse("http://example.com/activity/531")
.unwrap()
.into();
let ap_id: DbUrl = Url::parse("http://example.com/activity/531")?.into();
// inserting activity should only work once
ReceivedActivity::create(pool, &ap_id).await.unwrap();
ReceivedActivity::create(pool, &ap_id).await.unwrap_err();
ReceivedActivity::create(pool, &ap_id).await?;
let second = ReceivedActivity::create(pool, &ap_id).await;
assert!(second.is_err());
Ok(())
}
#[tokio::test]
#[serial]
async fn sent_activity_write_read() {
async fn sent_activity_write_read() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let ap_id: DbUrl = Url::parse("http://example.com/activity/412")
.unwrap()
.into();
let ap_id: DbUrl = Url::parse("http://example.com/activity/412")?.into();
let data = json!({
"key1": "0xF9BA143B95FF6D82",
"key2": "42",
@ -100,20 +99,20 @@ mod tests {
ap_id: ap_id.clone(),
data: data.clone(),
sensitive,
actor_apub_id: Url::parse("http://example.com/u/exampleuser")
.unwrap()
.into(),
actor_apub_id: Url::parse("http://example.com/u/exampleuser")?.into(),
actor_type: ActorType::Person,
send_all_instances: false,
send_community_followers_of: None,
send_inboxes: vec![],
};
SentActivity::create(pool, form).await.unwrap();
SentActivity::create(pool, form).await?;
let res = SentActivity::read_from_apub_id(pool, &ap_id).await.unwrap();
let res = SentActivity::read_from_apub_id(pool, &ap_id).await?;
assert_eq!(res.ap_id, ap_id);
assert_eq!(res.data, data);
assert_eq!(res.sensitive, sensitive);
Ok(())
}
}

View file

@ -199,13 +199,12 @@ impl CommunityLanguage {
/// Returns true if the given language is one of configured languages for given community
pub async fn is_allowed_community_language(
pool: &mut DbPool<'_>,
for_language_id: Option<LanguageId>,
for_language_id: LanguageId,
for_community_id: CommunityId,
) -> LemmyResult<()> {
use crate::schema::community_language::dsl::community_language;
let conn = &mut get_conn(pool).await?;
if let Some(for_language_id) = for_language_id {
let is_allowed = select(exists(
community_language.find((for_community_id, for_language_id)),
))
@ -217,9 +216,6 @@ impl CommunityLanguage {
} else {
Err(LemmyErrorType::LanguageNotAllowed)?
}
} else {
Ok(())
}
}
/// When site languages are updated, delete all languages of local communities which are not
@ -327,7 +323,7 @@ pub async fn default_post_language(
pool: &mut DbPool<'_>,
community_id: CommunityId,
local_user_id: LocalUserId,
) -> Result<Option<LanguageId>, Error> {
) -> Result<LanguageId, Error> {
use crate::schema::{community_language::dsl as cl, local_user_language::dsl as ul};
let conn = &mut get_conn(pool).await?;
let mut intersection = ul::local_user_language
@ -339,12 +335,12 @@ pub async fn default_post_language(
.await?;
if intersection.len() == 1 {
Ok(intersection.pop())
Ok(intersection.pop().unwrap_or(UNDETERMINED_ID))
} else if intersection.len() == 2 && intersection.contains(&UNDETERMINED_ID) {
intersection.retain(|i| i != &UNDETERMINED_ID);
Ok(intersection.pop())
Ok(intersection.pop().unwrap_or(UNDETERMINED_ID))
} else {
Ok(None)
Ok(UNDETERMINED_ID)
}
}
@ -392,7 +388,6 @@ async fn convert_read_languages(
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
#[expect(clippy::indexing_slicing)]
mod tests {
@ -409,168 +404,148 @@ mod tests {
traits::Crud,
utils::build_db_pool_for_tests,
};
use diesel::result::Error;
use pretty_assertions::assert_eq;
use serial_test::serial;
async fn test_langs1(pool: &mut DbPool<'_>) -> Vec<LanguageId> {
vec![
Language::read_id_from_code(pool, Some("en"))
.await
.unwrap()
.unwrap(),
Language::read_id_from_code(pool, Some("fr"))
.await
.unwrap()
.unwrap(),
Language::read_id_from_code(pool, Some("ru"))
.await
.unwrap()
.unwrap(),
]
async fn test_langs1(pool: &mut DbPool<'_>) -> Result<Vec<LanguageId>, Error> {
Ok(vec![
Language::read_id_from_code(pool, "en").await?,
Language::read_id_from_code(pool, "fr").await?,
Language::read_id_from_code(pool, "ru").await?,
])
}
async fn test_langs2(pool: &mut DbPool<'_>) -> Vec<LanguageId> {
vec![
Language::read_id_from_code(pool, Some("fi"))
.await
.unwrap()
.unwrap(),
Language::read_id_from_code(pool, Some("se"))
.await
.unwrap()
.unwrap(),
]
async fn test_langs2(pool: &mut DbPool<'_>) -> Result<Vec<LanguageId>, Error> {
Ok(vec![
Language::read_id_from_code(pool, "fi").await?,
Language::read_id_from_code(pool, "se").await?,
])
}
async fn create_test_site(pool: &mut DbPool<'_>) -> (Site, Instance) {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
async fn create_test_site(pool: &mut DbPool<'_>) -> Result<(Site, Instance), Error> {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id);
let site = Site::create(pool, &site_form).await.unwrap();
let site = Site::create(pool, &site_form).await?;
// Create a local site, since this is necessary for local languages
let local_site_form = LocalSiteInsertForm::new(site.id);
LocalSite::create(pool, &local_site_form).await.unwrap();
LocalSite::create(pool, &local_site_form).await?;
(site, inserted_instance)
Ok((site, inserted_instance))
}
#[tokio::test]
#[serial]
async fn test_convert_update_languages() {
async fn test_convert_update_languages() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
// call with empty vec, returns all languages
let conn = &mut get_conn(pool).await.unwrap();
let converted1 = convert_update_languages(conn, vec![]).await.unwrap();
let conn = &mut get_conn(pool).await?;
let converted1 = convert_update_languages(conn, vec![]).await?;
assert_eq!(184, converted1.len());
// call with nonempty vec, returns same vec
let test_langs = test_langs1(&mut conn.into()).await;
let converted2 = convert_update_languages(conn, test_langs.clone())
.await
.unwrap();
let test_langs = test_langs1(&mut conn.into()).await?;
let converted2 = convert_update_languages(conn, test_langs.clone()).await?;
assert_eq!(test_langs, converted2);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_convert_read_languages() {
async fn test_convert_read_languages() -> Result<(), Error> {
use crate::schema::language::dsl::{id, language};
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
// call with all languages, returns empty vec
let conn = &mut get_conn(pool).await.unwrap();
let all_langs = language.select(id).get_results(conn).await.unwrap();
let converted1: Vec<LanguageId> = convert_read_languages(conn, all_langs).await.unwrap();
let conn = &mut get_conn(pool).await?;
let all_langs = language.select(id).get_results(conn).await?;
let converted1: Vec<LanguageId> = convert_read_languages(conn, all_langs).await?;
assert_eq!(0, converted1.len());
// call with nonempty vec, returns same vec
let test_langs = test_langs1(&mut conn.into()).await;
let converted2 = convert_read_languages(conn, test_langs.clone())
.await
.unwrap();
let test_langs = test_langs1(&mut conn.into()).await?;
let converted2 = convert_read_languages(conn, test_langs.clone()).await?;
assert_eq!(test_langs, converted2);
Ok(())
}
#[tokio::test]
#[serial]
async fn test_site_languages() {
async fn test_site_languages() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await;
let site_languages1 = SiteLanguage::read_local_raw(pool).await.unwrap();
let (site, instance) = create_test_site(pool).await?;
let site_languages1 = SiteLanguage::read_local_raw(pool).await?;
// site is created with all languages
assert_eq!(184, site_languages1.len());
let test_langs = test_langs1(pool).await;
SiteLanguage::update(pool, test_langs.clone(), &site)
.await
.unwrap();
let test_langs = test_langs1(pool).await?;
SiteLanguage::update(pool, test_langs.clone(), &site).await?;
let site_languages2 = SiteLanguage::read_local_raw(pool).await.unwrap();
let site_languages2 = SiteLanguage::read_local_raw(pool).await?;
// after update, site only has new languages
assert_eq!(test_langs, site_languages2);
Site::delete(pool, site.id).await.unwrap();
Instance::delete(pool, instance.id).await.unwrap();
LocalSite::delete(pool).await.unwrap();
Site::delete(pool, site.id).await?;
Instance::delete(pool, instance.id).await?;
LocalSite::delete(pool).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_user_languages() {
async fn test_user_languages() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await;
let (site, instance) = create_test_site(pool).await?;
let person_form = PersonInsertForm::test_form(instance.id, "my test person");
let person = Person::create(pool, &person_form).await.unwrap();
let person = Person::create(pool, &person_form).await?;
let local_user_form = LocalUserInsertForm::test_form(person.id);
let local_user = LocalUser::create(pool, &local_user_form, vec![])
.await
.unwrap();
let local_user_langs1 = LocalUserLanguage::read(pool, local_user.id).await.unwrap();
let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?;
let local_user_langs1 = LocalUserLanguage::read(pool, local_user.id).await?;
// new user should be initialized with all languages
assert_eq!(0, local_user_langs1.len());
// update user languages
let test_langs2 = test_langs2(pool).await;
LocalUserLanguage::update(pool, test_langs2, local_user.id)
.await
.unwrap();
let local_user_langs2 = LocalUserLanguage::read(pool, local_user.id).await.unwrap();
let test_langs2 = test_langs2(pool).await?;
LocalUserLanguage::update(pool, test_langs2, local_user.id).await?;
let local_user_langs2 = LocalUserLanguage::read(pool, local_user.id).await?;
assert_eq!(3, local_user_langs2.len());
Person::delete(pool, person.id).await.unwrap();
LocalUser::delete(pool, local_user.id).await.unwrap();
Site::delete(pool, site.id).await.unwrap();
LocalSite::delete(pool).await.unwrap();
Instance::delete(pool, instance.id).await.unwrap();
Person::delete(pool, person.id).await?;
LocalUser::delete(pool, local_user.id).await?;
Site::delete(pool, site.id).await?;
LocalSite::delete(pool).await?;
Instance::delete(pool, instance.id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_community_languages() {
async fn test_community_languages() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await;
let test_langs = test_langs1(pool).await;
SiteLanguage::update(pool, test_langs.clone(), &site)
.await
.unwrap();
let (site, instance) = create_test_site(pool).await?;
let test_langs = test_langs1(pool).await?;
SiteLanguage::update(pool, test_langs.clone(), &site).await?;
let read_site_langs = SiteLanguage::read(pool, site.id).await.unwrap();
let read_site_langs = SiteLanguage::read(pool, site.id).await?;
assert_eq!(test_langs, read_site_langs);
// Test the local ones are the same
let read_local_site_langs = SiteLanguage::read_local_raw(pool).await.unwrap();
let read_local_site_langs = SiteLanguage::read_local_raw(pool).await?;
assert_eq!(test_langs, read_local_site_langs);
let community_form = CommunityInsertForm::new(
@ -579,52 +554,48 @@ mod tests {
"test community".to_string(),
"pubkey".to_string(),
);
let community = Community::create(pool, &community_form).await.unwrap();
let community_langs1 = CommunityLanguage::read(pool, community.id).await.unwrap();
let community = Community::create(pool, &community_form).await?;
let community_langs1 = CommunityLanguage::read(pool, community.id).await?;
// community is initialized with site languages
assert_eq!(test_langs, community_langs1);
let allowed_lang1 =
CommunityLanguage::is_allowed_community_language(pool, Some(test_langs[0]), community.id)
.await;
CommunityLanguage::is_allowed_community_language(pool, test_langs[0], community.id).await;
assert!(allowed_lang1.is_ok());
let test_langs2 = test_langs2(pool).await;
let test_langs2 = test_langs2(pool).await?;
let allowed_lang2 =
CommunityLanguage::is_allowed_community_language(pool, Some(test_langs2[0]), community.id)
.await;
CommunityLanguage::is_allowed_community_language(pool, test_langs2[0], community.id).await;
assert!(allowed_lang2.is_err());
// limit site languages to en, fi. after this, community languages should be updated to
// intersection of old languages (en, fr, ru) and (en, fi), which is only fi.
SiteLanguage::update(pool, vec![test_langs[0], test_langs2[0]], &site)
.await
.unwrap();
let community_langs2 = CommunityLanguage::read(pool, community.id).await.unwrap();
SiteLanguage::update(pool, vec![test_langs[0], test_langs2[0]], &site).await?;
let community_langs2 = CommunityLanguage::read(pool, community.id).await?;
assert_eq!(vec![test_langs[0]], community_langs2);
// update community languages to different ones
CommunityLanguage::update(pool, test_langs2.clone(), community.id)
.await
.unwrap();
let community_langs3 = CommunityLanguage::read(pool, community.id).await.unwrap();
CommunityLanguage::update(pool, test_langs2.clone(), community.id).await?;
let community_langs3 = CommunityLanguage::read(pool, community.id).await?;
assert_eq!(test_langs2, community_langs3);
Community::delete(pool, community.id).await.unwrap();
Site::delete(pool, site.id).await.unwrap();
LocalSite::delete(pool).await.unwrap();
Instance::delete(pool, instance.id).await.unwrap();
Community::delete(pool, community.id).await?;
Site::delete(pool, site.id).await?;
LocalSite::delete(pool).await?;
Instance::delete(pool, instance.id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_default_post_language() {
async fn test_default_post_language() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await;
let test_langs = test_langs1(pool).await;
let test_langs2 = test_langs2(pool).await;
let (site, instance) = create_test_site(pool).await?;
let test_langs = test_langs1(pool).await?;
let test_langs2 = test_langs2(pool).await?;
let community_form = CommunityInsertForm::new(
instance.id,
@ -632,58 +603,39 @@ mod tests {
"test community".to_string(),
"pubkey".to_string(),
);
let community = Community::create(pool, &community_form).await.unwrap();
CommunityLanguage::update(pool, test_langs, community.id)
.await
.unwrap();
let community = Community::create(pool, &community_form).await?;
CommunityLanguage::update(pool, test_langs, community.id).await?;
let person_form = PersonInsertForm::test_form(instance.id, "my test person");
let person = Person::create(pool, &person_form).await.unwrap();
let person = Person::create(pool, &person_form).await?;
let local_user_form = LocalUserInsertForm::test_form(person.id);
let local_user = LocalUser::create(pool, &local_user_form, vec![])
.await
.unwrap();
LocalUserLanguage::update(pool, test_langs2, local_user.id)
.await
.unwrap();
let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?;
LocalUserLanguage::update(pool, test_langs2, local_user.id).await?;
// no overlap in user/community languages, so defaults to undetermined
let def1 = default_post_language(pool, community.id, local_user.id)
.await
.unwrap();
assert_eq!(None, def1);
let def1 = default_post_language(pool, community.id, local_user.id).await?;
assert_eq!(UNDETERMINED_ID, def1);
let ru = Language::read_id_from_code(pool, Some("ru"))
.await
.unwrap()
.unwrap();
let ru = Language::read_id_from_code(pool, "ru").await?;
let test_langs3 = vec![
ru,
Language::read_id_from_code(pool, Some("fi"))
.await
.unwrap()
.unwrap(),
Language::read_id_from_code(pool, Some("se"))
.await
.unwrap()
.unwrap(),
Language::read_id_from_code(pool, "fi").await?,
Language::read_id_from_code(pool, "se").await?,
UNDETERMINED_ID,
];
LocalUserLanguage::update(pool, test_langs3, local_user.id)
.await
.unwrap();
LocalUserLanguage::update(pool, test_langs3, local_user.id).await?;
// this time, both have ru as common lang
let def2 = default_post_language(pool, community.id, local_user.id)
.await
.unwrap();
assert_eq!(Some(ru), def2);
let def2 = default_post_language(pool, community.id, local_user.id).await?;
assert_eq!(ru, def2);
Person::delete(pool, person.id).await.unwrap();
Community::delete(pool, community.id).await.unwrap();
LocalUser::delete(pool, local_user.id).await.unwrap();
Site::delete(pool, site.id).await.unwrap();
LocalSite::delete(pool).await.unwrap();
Instance::delete(pool, instance.id).await.unwrap();
Person::delete(pool, person.id).await?;
Community::delete(pool, community.id).await?;
LocalUser::delete(pool, local_user.id).await?;
Site::delete(pool, site.id).await?;
LocalSite::delete(pool).await?;
Instance::delete(pool, instance.id).await?;
Ok(())
}
}

View file

@ -40,12 +40,12 @@ impl Comment {
pub async fn update_removed_for_creator(
pool: &mut DbPool<'_>,
for_creator_id: PersonId,
new_removed: bool,
removed: bool,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(comment::table.filter(comment::creator_id.eq(for_creator_id)))
.set((
comment::removed.eq(new_removed),
comment::removed.eq(removed),
comment::updated.eq(naive_now()),
))
.get_results::<Self>(conn)
@ -196,7 +196,6 @@ impl Saveable for CommentSaved {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
@ -220,23 +219,22 @@ mod tests {
utils::build_db_pool_for_tests,
};
use diesel_ltree::Ltree;
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
use url::Url;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "terry");
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let inserted_person = Person::create(pool, &new_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -244,21 +242,21 @@ mod tests {
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let inserted_community = Community::create(pool, &new_community).await?;
let new_post = PostInsertForm::new(
"A test post".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
let comment_form = CommentInsertForm::new(
inserted_person.id,
inserted_post.id,
"A test comment".into(),
);
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
let expected_comment = Comment {
id: inserted_comment.id,
@ -273,8 +271,7 @@ mod tests {
ap_id: Url::parse(&format!(
"https://lemmy-alpha/comment/{}",
inserted_comment.id
))
.unwrap()
))?
.into(),
distinguished: false,
local: true,
@ -287,23 +284,19 @@ mod tests {
"A child comment".into(),
);
let inserted_child_comment =
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path))
.await
.unwrap();
Comment::create(pool, &child_comment_form, Some(&inserted_comment.path)).await?;
// Comment Like
let comment_like_form = CommentLikeForm {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
person_id: inserted_person.id,
score: 1,
};
let inserted_comment_like = CommentLike::like(pool, &comment_like_form).await.unwrap();
let inserted_comment_like = CommentLike::like(pool, &comment_like_form).await?;
let expected_comment_like = CommentLike {
comment_id: inserted_comment.id,
post_id: inserted_post.id,
person_id: inserted_person.id,
published: inserted_comment_like.published,
score: 1,
@ -315,7 +308,7 @@ mod tests {
person_id: inserted_person.id,
};
let inserted_comment_saved = CommentSaved::save(pool, &comment_saved_form).await.unwrap();
let inserted_comment_saved = CommentSaved::save(pool, &comment_saved_form).await?;
let expected_comment_saved = CommentSaved {
comment_id: inserted_comment.id,
@ -328,27 +321,17 @@ mod tests {
..Default::default()
};
let updated_comment = Comment::update(pool, inserted_comment.id, &comment_update_form)
.await
.unwrap();
let updated_comment = Comment::update(pool, inserted_comment.id, &comment_update_form).await?;
let read_comment = Comment::read(pool, inserted_comment.id).await.unwrap();
let like_removed = CommentLike::remove(pool, inserted_person.id, inserted_comment.id)
.await
.unwrap();
let saved_removed = CommentSaved::unsave(pool, &comment_saved_form)
.await
.unwrap();
let num_deleted = Comment::delete(pool, inserted_comment.id).await.unwrap();
Comment::delete(pool, inserted_child_comment.id)
.await
.unwrap();
Post::delete(pool, inserted_post.id).await.unwrap();
Community::delete(pool, inserted_community.id)
.await
.unwrap();
Person::delete(pool, inserted_person.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await.unwrap();
let read_comment = Comment::read(pool, inserted_comment.id).await?;
let like_removed = CommentLike::remove(pool, inserted_person.id, inserted_comment.id).await?;
let saved_removed = CommentSaved::unsave(pool, &comment_saved_form).await?;
let num_deleted = Comment::delete(pool, inserted_comment.id).await?;
Comment::delete(pool, inserted_child_comment.id).await?;
Post::delete(pool, inserted_post.id).await?;
Community::delete(pool, inserted_community.id).await?;
Person::delete(pool, inserted_person.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
assert_eq!(expected_comment, read_comment);
assert_eq!(expected_comment, inserted_comment);
@ -362,5 +345,7 @@ mod tests {
assert_eq!(1, like_removed);
assert_eq!(1, saved_removed);
assert_eq!(1, num_deleted);
Ok(())
}
}

View file

@ -30,12 +30,13 @@ use crate::{
get_conn,
DbPool,
},
ListingType,
SubscribedType,
};
use chrono::{DateTime, Utc};
use diesel::{
deserialize,
dsl::{self, exists, insert_into},
dsl::{self, exists, insert_into, not},
pg::Pg,
result::Error,
select,
@ -193,6 +194,30 @@ impl Community {
.await?;
Ok(())
}
pub async fn get_random_community_id(
pool: &mut DbPool<'_>,
type_: &Option<ListingType>,
) -> Result<CommunityId, Error> {
let conn = &mut get_conn(pool).await?;
sql_function!(fn random() -> Text);
let mut query = community::table
.filter(not(community::deleted))
.filter(not(community::removed))
.into_boxed();
if let Some(ListingType::Local) = type_ {
query = query.filter(community::local);
}
query
.select(community::id)
.order(random())
.limit(1)
.first::<CommunityId>(conn)
.await
}
}
impl CommunityModerator {
@ -498,7 +523,6 @@ mod tests {
banner: None,
followers_url: inserted_community.followers_url.clone(),
inbox_url: inserted_community.inbox_url.clone(),
shared_inbox_url: None,
moderators_url: None,
featured_url: None,
hidden: false,

View file

@ -48,19 +48,19 @@ impl FederationAllowList {
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
source::{federation_allowlist::FederationAllowList, instance::Instance},
utils::build_db_pool_for_tests,
};
use diesel::result::Error;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_allowlist_insert_and_clear() {
async fn test_allowlist_insert_and_clear() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let domains = vec![
@ -71,9 +71,9 @@ mod tests {
let allowed = Some(domains.clone());
FederationAllowList::replace(pool, allowed).await.unwrap();
FederationAllowList::replace(pool, allowed).await?;
let allows = Instance::allowlist(pool).await.unwrap();
let allows = Instance::allowlist(pool).await?;
let allows_domains = allows
.iter()
.map(|i| i.domain.clone())
@ -85,13 +85,13 @@ mod tests {
// Now test clearing them via Some(empty vec)
let clear_allows = Some(Vec::new());
FederationAllowList::replace(pool, clear_allows)
.await
.unwrap();
let allows = Instance::allowlist(pool).await.unwrap();
FederationAllowList::replace(pool, clear_allows).await?;
let allows = Instance::allowlist(pool).await?;
assert_eq!(0, allows.len());
Instance::delete_all(pool).await.unwrap();
Instance::delete_all(pool).await?;
Ok(())
}
}

View file

@ -67,6 +67,11 @@ impl Instance {
}
}
}
pub async fn read(pool: &mut DbPool<'_>, instance_id: InstanceId) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
instance::table.find(instance_id).first(conn).await
}
pub async fn update(
pool: &mut DbPool<'_>,
instance_id: InstanceId,

View file

@ -1,3 +1,4 @@
use super::actor_language::UNDETERMINED_ID;
use crate::{
diesel::ExpressionMethods,
newtypes::LanguageId,
@ -19,47 +20,42 @@ impl Language {
language::table.find(id_).first(conn).await
}
/// Attempts to find the given language code and return its ID. If not found, returns none.
pub async fn read_id_from_code(
pool: &mut DbPool<'_>,
code_: Option<&str>,
) -> Result<Option<LanguageId>, Error> {
if let Some(code_) = code_ {
/// Attempts to find the given language code and return its ID.
pub async fn read_id_from_code(pool: &mut DbPool<'_>, code_: &str) -> Result<LanguageId, Error> {
let conn = &mut get_conn(pool).await?;
Ok(
language::table
let res = language::table
.filter(language::code.eq(code_))
.first::<Self>(conn)
.await
.map(|l| l.id)
.ok(),
)
} else {
Ok(None)
}
.map(|l| l.id);
// Return undetermined by default
Ok(res.unwrap_or(UNDETERMINED_ID))
}
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
#[expect(clippy::indexing_slicing)]
mod tests {
use crate::{source::language::Language, utils::build_db_pool_for_tests};
use diesel::result::Error;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_languages() {
async fn test_languages() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let all = Language::read_all(pool).await.unwrap();
let all = Language::read_all(pool).await?;
assert_eq!(184, all.len());
assert_eq!("ak", all[5].code);
assert_eq!("lv", all[99].code);
assert_eq!("yi", all[179].code);
Ok(())
}
}

View file

@ -66,6 +66,20 @@ impl Crud for ModRemovePost {
}
}
impl ModRemovePost {
pub async fn create_multiple(
pool: &mut DbPool<'_>,
forms: &Vec<ModRemovePostForm>,
) -> Result<usize, Error> {
use crate::schema::mod_remove_post::dsl::mod_remove_post;
let conn = &mut get_conn(pool).await?;
insert_into(mod_remove_post)
.values(forms)
.execute(conn)
.await
}
}
#[async_trait]
impl Crud for ModLockPost {
type InsertForm = ModLockPostForm;
@ -153,6 +167,20 @@ impl Crud for ModRemoveComment {
}
}
impl ModRemoveComment {
pub async fn create_multiple(
pool: &mut DbPool<'_>,
forms: &Vec<ModRemoveCommentForm>,
) -> Result<usize, Error> {
use crate::schema::mod_remove_comment::dsl::mod_remove_comment;
let conn = &mut get_conn(pool).await?;
insert_into(mod_remove_comment)
.values(forms)
.execute(conn)
.await
}
}
#[async_trait]
impl Crud for ModRemoveCommunity {
type InsertForm = ModRemoveCommunityForm;
@ -465,7 +493,6 @@ impl Crud for AdminPurgeComment {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
@ -499,26 +526,25 @@ mod tests {
traits::Crud,
utils::build_db_pool_for_tests,
};
use diesel::result::Error;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_mod = PersonInsertForm::test_form(inserted_instance.id, "the mod");
let inserted_mod = Person::create(pool, &new_mod).await.unwrap();
let inserted_mod = Person::create(pool, &new_mod).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "jim2");
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let inserted_person = Person::create(pool, &new_person).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -527,21 +553,21 @@ mod tests {
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let inserted_community = Community::create(pool, &new_community).await?;
let new_post = PostInsertForm::new(
"A test post thweep".into(),
inserted_person.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
let comment_form = CommentInsertForm::new(
inserted_person.id,
inserted_post.id,
"A test comment".into(),
);
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
// Now the actual tests
@ -552,12 +578,8 @@ mod tests {
reason: None,
removed: None,
};
let inserted_mod_remove_post = ModRemovePost::create(pool, &mod_remove_post_form)
.await
.unwrap();
let read_mod_remove_post = ModRemovePost::read(pool, inserted_mod_remove_post.id)
.await
.unwrap();
let inserted_mod_remove_post = ModRemovePost::create(pool, &mod_remove_post_form).await?;
let read_mod_remove_post = ModRemovePost::read(pool, inserted_mod_remove_post.id).await?;
let expected_mod_remove_post = ModRemovePost {
id: inserted_mod_remove_post.id,
post_id: inserted_post.id,
@ -574,12 +596,8 @@ mod tests {
post_id: inserted_post.id,
locked: None,
};
let inserted_mod_lock_post = ModLockPost::create(pool, &mod_lock_post_form)
.await
.unwrap();
let read_mod_lock_post = ModLockPost::read(pool, inserted_mod_lock_post.id)
.await
.unwrap();
let inserted_mod_lock_post = ModLockPost::create(pool, &mod_lock_post_form).await?;
let read_mod_lock_post = ModLockPost::read(pool, inserted_mod_lock_post.id).await?;
let expected_mod_lock_post = ModLockPost {
id: inserted_mod_lock_post.id,
post_id: inserted_post.id,
@ -596,12 +614,8 @@ mod tests {
featured: false,
is_featured_community: true,
};
let inserted_mod_feature_post = ModFeaturePost::create(pool, &mod_feature_post_form)
.await
.unwrap();
let read_mod_feature_post = ModFeaturePost::read(pool, inserted_mod_feature_post.id)
.await
.unwrap();
let inserted_mod_feature_post = ModFeaturePost::create(pool, &mod_feature_post_form).await?;
let read_mod_feature_post = ModFeaturePost::read(pool, inserted_mod_feature_post.id).await?;
let expected_mod_feature_post = ModFeaturePost {
id: inserted_mod_feature_post.id,
post_id: inserted_post.id,
@ -619,12 +633,10 @@ mod tests {
reason: None,
removed: None,
};
let inserted_mod_remove_comment = ModRemoveComment::create(pool, &mod_remove_comment_form)
.await
.unwrap();
let read_mod_remove_comment = ModRemoveComment::read(pool, inserted_mod_remove_comment.id)
.await
.unwrap();
let inserted_mod_remove_comment =
ModRemoveComment::create(pool, &mod_remove_comment_form).await?;
let read_mod_remove_comment =
ModRemoveComment::read(pool, inserted_mod_remove_comment.id).await?;
let expected_mod_remove_comment = ModRemoveComment {
id: inserted_mod_remove_comment.id,
comment_id: inserted_comment.id,
@ -643,13 +655,9 @@ mod tests {
removed: None,
};
let inserted_mod_remove_community =
ModRemoveCommunity::create(pool, &mod_remove_community_form)
.await
.unwrap();
ModRemoveCommunity::create(pool, &mod_remove_community_form).await?;
let read_mod_remove_community =
ModRemoveCommunity::read(pool, inserted_mod_remove_community.id)
.await
.unwrap();
ModRemoveCommunity::read(pool, inserted_mod_remove_community.id).await?;
let expected_mod_remove_community = ModRemoveCommunity {
id: inserted_mod_remove_community.id,
community_id: inserted_community.id,
@ -670,13 +678,9 @@ mod tests {
expires: None,
};
let inserted_mod_ban_from_community =
ModBanFromCommunity::create(pool, &mod_ban_from_community_form)
.await
.unwrap();
ModBanFromCommunity::create(pool, &mod_ban_from_community_form).await?;
let read_mod_ban_from_community =
ModBanFromCommunity::read(pool, inserted_mod_ban_from_community.id)
.await
.unwrap();
ModBanFromCommunity::read(pool, inserted_mod_ban_from_community.id).await?;
let expected_mod_ban_from_community = ModBanFromCommunity {
id: inserted_mod_ban_from_community.id,
community_id: inserted_community.id,
@ -697,8 +701,8 @@ mod tests {
banned: None,
expires: None,
};
let inserted_mod_ban = ModBan::create(pool, &mod_ban_form).await.unwrap();
let read_mod_ban = ModBan::read(pool, inserted_mod_ban.id).await.unwrap();
let inserted_mod_ban = ModBan::create(pool, &mod_ban_form).await?;
let read_mod_ban = ModBan::read(pool, inserted_mod_ban.id).await?;
let expected_mod_ban = ModBan {
id: inserted_mod_ban.id,
mod_person_id: inserted_mod.id,
@ -717,12 +721,8 @@ mod tests {
community_id: inserted_community.id,
removed: None,
};
let inserted_mod_add_community = ModAddCommunity::create(pool, &mod_add_community_form)
.await
.unwrap();
let read_mod_add_community = ModAddCommunity::read(pool, inserted_mod_add_community.id)
.await
.unwrap();
let inserted_mod_add_community = ModAddCommunity::create(pool, &mod_add_community_form).await?;
let read_mod_add_community = ModAddCommunity::read(pool, inserted_mod_add_community.id).await?;
let expected_mod_add_community = ModAddCommunity {
id: inserted_mod_add_community.id,
community_id: inserted_community.id,
@ -739,8 +739,8 @@ mod tests {
other_person_id: inserted_person.id,
removed: None,
};
let inserted_mod_add = ModAdd::create(pool, &mod_add_form).await.unwrap();
let read_mod_add = ModAdd::read(pool, inserted_mod_add.id).await.unwrap();
let inserted_mod_add = ModAdd::create(pool, &mod_add_form).await?;
let read_mod_add = ModAdd::read(pool, inserted_mod_add.id).await?;
let expected_mod_add = ModAdd {
id: inserted_mod_add.id,
mod_person_id: inserted_mod.id,
@ -749,14 +749,12 @@ mod tests {
when_: inserted_mod_add.when_,
};
Comment::delete(pool, inserted_comment.id).await.unwrap();
Post::delete(pool, inserted_post.id).await.unwrap();
Community::delete(pool, inserted_community.id)
.await
.unwrap();
Person::delete(pool, inserted_person.id).await.unwrap();
Person::delete(pool, inserted_mod.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await.unwrap();
Comment::delete(pool, inserted_comment.id).await?;
Post::delete(pool, inserted_post.id).await?;
Community::delete(pool, inserted_community.id).await?;
Person::delete(pool, inserted_person.id).await?;
Person::delete(pool, inserted_mod.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
assert_eq!(expected_mod_remove_post, read_mod_remove_post);
assert_eq!(expected_mod_lock_post, read_mod_lock_post);
@ -767,5 +765,7 @@ mod tests {
assert_eq!(expected_mod_ban, read_mod_ban);
assert_eq!(expected_mod_add_community, read_mod_add_community);
assert_eq!(expected_mod_add, read_mod_add);
Ok(())
}
}

View file

@ -279,7 +279,6 @@ mod tests {
public_key: "pubkey".to_owned(),
last_refreshed_at: inserted_person.published,
inbox_url: inserted_person.inbox_url.clone(),
shared_inbox_url: None,
matrix_user_id: None,
ban_expires: None,
instance_id: inserted_instance.id,

View file

@ -144,7 +144,7 @@ impl Post {
pool: &mut DbPool<'_>,
for_creator_id: PersonId,
for_community_id: Option<CommunityId>,
new_removed: bool,
removed: bool,
) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
@ -156,7 +156,7 @@ impl Post {
}
update
.set((post::removed.eq(new_removed), post::updated.eq(naive_now())))
.set((post::removed.eq(removed), post::updated.eq(naive_now())))
.get_results::<Self>(conn)
.await
}
@ -543,7 +543,6 @@ mod tests {
assert_eq!(3, num_deleted);
Community::delete(pool, inserted_community.id).await?;
Person::delete(pool, inserted_person.id).await?;
Instance::delete(pool, inserted_instance.id).await?;

View file

@ -80,7 +80,6 @@ impl Reportable for PostReport {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::*;
@ -94,14 +93,13 @@ mod tests {
traits::Crud,
utils::build_db_pool_for_tests,
};
use diesel::result::Error;
use serial_test::serial;
async fn init(pool: &mut DbPool<'_>) -> (Person, PostReport) {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
async fn init(pool: &mut DbPool<'_>) -> Result<(Person, PostReport), Error> {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let person_form = PersonInsertForm::test_form(inserted_instance.id, "jim");
let person = Person::create(pool, &person_form).await.unwrap();
let person = Person::create(pool, &person_form).await?;
let community_form = CommunityInsertForm::new(
inserted_instance.id,
@ -109,10 +107,10 @@ mod tests {
"nada".to_owned(),
"pubkey".to_string(),
);
let community = Community::create(pool, &community_form).await.unwrap();
let community = Community::create(pool, &community_form).await?;
let form = PostInsertForm::new("A test post".into(), person.id, community.id);
let post = Post::create(pool, &form).await.unwrap();
let post = Post::create(pool, &form).await?;
let report_form = PostReportForm {
post_id: post.id,
@ -120,46 +118,46 @@ mod tests {
reason: "my reason".to_string(),
..Default::default()
};
let report = PostReport::report(pool, &report_form).await.unwrap();
(person, report)
let report = PostReport::report(pool, &report_form).await?;
Ok((person, report))
}
#[tokio::test]
#[serial]
async fn test_resolve_post_report() {
async fn test_resolve_post_report() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let (person, report) = init(pool).await;
let (person, report) = init(pool).await?;
let resolved_count = PostReport::resolve(pool, report.id, person.id)
.await
.unwrap();
let resolved_count = PostReport::resolve(pool, report.id, person.id).await?;
assert_eq!(resolved_count, 1);
let unresolved_count = PostReport::unresolve(pool, report.id, person.id)
.await
.unwrap();
let unresolved_count = PostReport::unresolve(pool, report.id, person.id).await?;
assert_eq!(unresolved_count, 1);
Person::delete(pool, person.id).await.unwrap();
Post::delete(pool, report.post_id).await.unwrap();
Person::delete(pool, person.id).await?;
Post::delete(pool, report.post_id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn test_resolve_all_post_reports() {
async fn test_resolve_all_post_reports() -> Result<(), Error> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let (person, report) = init(pool).await;
let (person, report) = init(pool).await?;
let resolved_count = PostReport::resolve_all_for_object(pool, report.post_id, person.id)
.await
.unwrap();
let resolved_count =
PostReport::resolve_all_for_object(pool, report.post_id, person.id).await?;
assert_eq!(resolved_count, 1);
Person::delete(pool, person.id).await.unwrap();
Post::delete(pool, report.post_id).await.unwrap();
Person::delete(pool, person.id).await?;
Post::delete(pool, report.post_id).await?;
Ok(())
}
}

View file

@ -85,7 +85,6 @@ impl PrivateMessage {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
@ -97,27 +96,26 @@ mod tests {
traits::Crud,
utils::build_db_pool_for_tests,
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
use url::Url;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let creator_form = PersonInsertForm::test_form(inserted_instance.id, "creator_pm");
let inserted_creator = Person::create(pool, &creator_form).await.unwrap();
let inserted_creator = Person::create(pool, &creator_form).await?;
let recipient_form = PersonInsertForm::test_form(inserted_instance.id, "recipient_pm");
let inserted_recipient = Person::create(pool, &recipient_form).await.unwrap();
let inserted_recipient = Person::create(pool, &recipient_form).await?;
let private_message_form = PrivateMessageInsertForm::new(
inserted_creator.id,
@ -125,9 +123,7 @@ mod tests {
"A test private message".into(),
);
let inserted_private_message = PrivateMessage::create(pool, &private_message_form)
.await
.unwrap();
let inserted_private_message = PrivateMessage::create(pool, &private_message_form).await?;
let expected_private_message = PrivateMessage {
id: inserted_private_message.id,
@ -141,15 +137,12 @@ mod tests {
ap_id: Url::parse(&format!(
"https://lemmy-alpha/private_message/{}",
inserted_private_message.id
))
.unwrap()
))?
.into(),
local: true,
};
let read_private_message = PrivateMessage::read(pool, inserted_private_message.id)
.await
.unwrap();
let read_private_message = PrivateMessage::read(pool, inserted_private_message.id).await?;
let private_message_update_form = PrivateMessageUpdateForm {
content: Some("A test private message".into()),
@ -160,8 +153,7 @@ mod tests {
inserted_private_message.id,
&private_message_update_form,
)
.await
.unwrap();
.await?;
let deleted_private_message = PrivateMessage::update(
pool,
@ -171,8 +163,7 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
let marked_read_private_message = PrivateMessage::update(
pool,
inserted_private_message.id,
@ -181,16 +172,17 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
Person::delete(pool, inserted_creator.id).await.unwrap();
Person::delete(pool, inserted_recipient.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await.unwrap();
.await?;
Person::delete(pool, inserted_creator.id).await?;
Person::delete(pool, inserted_recipient.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
assert_eq!(expected_private_message, read_private_message);
assert_eq!(expected_private_message, updated_private_message);
assert_eq!(expected_private_message, inserted_private_message);
assert!(deleted_private_message.deleted);
assert!(marked_read_private_message.read);
Ok(())
}
}

View file

@ -1,5 +1,4 @@
use crate::{
diesel::OptionalExtension,
schema::secret::dsl::secret,
source::secret::Secret,
utils::{get_conn, DbPool},
@ -10,12 +9,12 @@ use diesel_async::RunQueryDsl;
impl Secret {
/// Initialize the Secrets from the DB.
/// Warning: You should only call this once.
pub async fn init(pool: &mut DbPool<'_>) -> Result<Option<Secret>, Error> {
pub async fn init(pool: &mut DbPool<'_>) -> Result<Secret, Error> {
Self::read_secrets(pool).await
}
async fn read_secrets(pool: &mut DbPool<'_>) -> Result<Option<Self>, Error> {
async fn read_secrets(pool: &mut DbPool<'_>) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
secret.first(conn).await.optional()
secret.first(conn).await
}
}

View file

@ -5,7 +5,7 @@ use crate::{
traits::Crud,
utils::{get_conn, limit_and_offset, DbPool},
};
use diesel::{insert_into, result::Error, ExpressionMethods, OptionalExtension, QueryDsl};
use diesel::{insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel_async::RunQueryDsl;
#[async_trait]
@ -51,14 +51,9 @@ impl Tagline {
.await
}
pub async fn get_random(pool: &mut DbPool<'_>) -> Result<Option<Self>, Error> {
pub async fn get_random(pool: &mut DbPool<'_>) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
sql_function!(fn random() -> Text);
tagline
.order(random())
.limit(1)
.first::<Self>(conn)
.await
.optional()
tagline.order(random()).limit(1).first::<Self>(conn).await
}
}

View file

@ -251,6 +251,27 @@ pub enum CommunityVisibility {
LocalOnly,
}
#[derive(
EnumString, Display, Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Default, Hash,
)]
#[cfg_attr(feature = "full", derive(DbEnum, TS))]
#[cfg_attr(
feature = "full",
ExistingTypePath = "crate::schema::sql_types::FederationModeEnum"
)]
#[cfg_attr(feature = "full", DbValueStyle = "verbatim")]
#[cfg_attr(feature = "full", ts(export))]
/// The federation mode for an item
pub enum FederationMode {
#[default]
/// Allows all
All,
/// Allows only local
Local,
/// Disables
Disable,
}
/// Wrapper for assert_eq! macro. Checks that vec matches the given length, and prints the
/// vec on failure.
#[macro_export]

View file

@ -13,6 +13,10 @@ pub mod sql_types {
#[diesel(postgres_type(name = "community_visibility"))]
pub struct CommunityVisibility;
#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "federation_mode_enum"))]
pub struct FederationModeEnum;
#[derive(diesel::sql_types::SqlType)]
#[diesel(postgres_type(name = "listing_type_enum"))]
pub struct ListingTypeEnum;
@ -119,7 +123,6 @@ diesel::table! {
comment_like (person_id, comment_id) {
person_id -> Int4,
comment_id -> Int4,
post_id -> Int4,
score -> Int2,
published -> Timestamptz,
}
@ -185,8 +188,6 @@ diesel::table! {
followers_url -> Nullable<Varchar>,
#[max_length = 255]
inbox_url -> Varchar,
#[max_length = 255]
shared_inbox_url -> Nullable<Varchar>,
hidden -> Bool,
posting_restricted_to_mods -> Bool,
instance_id -> Int4,
@ -368,12 +369,12 @@ diesel::table! {
use super::sql_types::PostListingModeEnum;
use super::sql_types::PostSortTypeEnum;
use super::sql_types::CommentSortTypeEnum;
use super::sql_types::FederationModeEnum;
local_site (id) {
id -> Int4,
site_id -> Int4,
site_setup -> Bool,
enable_downvotes -> Bool,
community_creation_admin_only -> Bool,
require_email_verification -> Bool,
application_question -> Nullable<Text>,
@ -398,6 +399,10 @@ diesel::table! {
default_post_sort_type -> PostSortTypeEnum,
default_comment_sort_type -> CommentSortTypeEnum,
oauth_registration -> Bool,
post_upvotes -> FederationModeEnum,
post_downvotes -> FederationModeEnum,
comment_upvotes -> FederationModeEnum,
comment_downvotes -> FederationModeEnum,
}
}
@ -679,8 +684,6 @@ diesel::table! {
deleted -> Bool,
#[max_length = 255]
inbox_url -> Varchar,
#[max_length = 255]
shared_inbox_url -> Nullable<Varchar>,
matrix_user_id -> Nullable<Text>,
bot_account -> Bool,
ban_expires -> Nullable<Timestamptz>,
@ -991,7 +994,6 @@ diesel::joinable!(comment -> post (post_id));
diesel::joinable!(comment_aggregates -> comment (comment_id));
diesel::joinable!(comment_like -> comment (comment_id));
diesel::joinable!(comment_like -> person (person_id));
diesel::joinable!(comment_like -> post (post_id));
diesel::joinable!(comment_reply -> comment (comment_id));
diesel::joinable!(comment_reply -> person (recipient_id));
diesel::joinable!(comment_report -> comment (comment_id));

View file

@ -102,7 +102,6 @@ pub struct CommentUpdateForm {
pub struct CommentLike {
pub person_id: PersonId,
pub comment_id: CommentId,
pub post_id: PostId, // TODO this is redundant
pub score: i16,
pub published: DateTime<Utc>,
}
@ -113,7 +112,6 @@ pub struct CommentLike {
pub struct CommentLikeForm {
pub person_id: PersonId,
pub comment_id: CommentId,
pub post_id: PostId, // TODO this is redundant
pub score: i16,
}

View file

@ -54,8 +54,6 @@ pub struct Community {
#[cfg_attr(feature = "full", ts(skip))]
#[serde(skip, default = "placeholder_apub_url")]
pub inbox_url: DbUrl,
#[serde(skip)]
pub shared_inbox_url: Option<DbUrl>,
/// Whether the community is hidden.
pub hidden: bool,
/// Whether posting is restricted to mods only.
@ -107,8 +105,6 @@ pub struct CommunityInsertForm {
#[new(default)]
pub inbox_url: Option<DbUrl>,
#[new(default)]
pub shared_inbox_url: Option<DbUrl>,
#[new(default)]
pub moderators_url: Option<DbUrl>,
#[new(default)]
pub featured_url: Option<DbUrl>,
@ -140,7 +136,6 @@ pub struct CommunityUpdateForm {
pub banner: Option<Option<DbUrl>>,
pub followers_url: Option<DbUrl>,
pub inbox_url: Option<DbUrl>,
pub shared_inbox_url: Option<Option<DbUrl>>,
pub moderators_url: Option<DbUrl>,
pub featured_url: Option<DbUrl>,
pub hidden: Option<bool>,

View file

@ -3,6 +3,7 @@ use crate::schema::local_site;
use crate::{
newtypes::{LocalSiteId, SiteId},
CommentSortType,
FederationMode,
ListingType,
PostListingMode,
PostSortType,
@ -27,8 +28,6 @@ pub struct LocalSite {
pub site_id: SiteId,
/// True if the site is set up.
pub site_setup: bool,
/// Whether downvotes are enabled.
pub enable_downvotes: bool,
/// Whether only admins can create communities.
pub community_creation_admin_only: bool,
/// Whether emails are required.
@ -72,6 +71,14 @@ pub struct LocalSite {
pub default_comment_sort_type: CommentSortType,
/// Whether or not external auth methods can auto-register users.
pub oauth_registration: bool,
/// What kind of post upvotes your site allows.
pub post_upvotes: FederationMode,
/// What kind of post downvotes your site allows.
pub post_downvotes: FederationMode,
/// What kind of comment upvotes your site allows.
pub comment_upvotes: FederationMode,
/// What kind of comment downvotes your site allows.
pub comment_downvotes: FederationMode,
}
#[derive(Clone, derive_new::new)]
@ -82,8 +89,6 @@ pub struct LocalSiteInsertForm {
#[new(default)]
pub site_setup: Option<bool>,
#[new(default)]
pub enable_downvotes: Option<bool>,
#[new(default)]
pub community_creation_admin_only: Option<bool>,
#[new(default)]
pub require_email_verification: Option<bool>,
@ -114,8 +119,6 @@ pub struct LocalSiteInsertForm {
#[new(default)]
pub registration_mode: Option<RegistrationMode>,
#[new(default)]
pub oauth_registration: Option<bool>,
#[new(default)]
pub reports_email_admins: Option<bool>,
#[new(default)]
pub federation_signed_fetch: Option<bool>,
@ -125,6 +128,16 @@ pub struct LocalSiteInsertForm {
pub default_post_sort_type: Option<PostSortType>,
#[new(default)]
pub default_comment_sort_type: Option<CommentSortType>,
#[new(default)]
pub oauth_registration: Option<bool>,
#[new(default)]
pub post_upvotes: Option<FederationMode>,
#[new(default)]
pub post_downvotes: Option<FederationMode>,
#[new(default)]
pub comment_upvotes: Option<FederationMode>,
#[new(default)]
pub comment_downvotes: Option<FederationMode>,
}
#[derive(Clone, Default)]
@ -132,7 +145,6 @@ pub struct LocalSiteInsertForm {
#[cfg_attr(feature = "full", diesel(table_name = local_site))]
pub struct LocalSiteUpdateForm {
pub site_setup: Option<bool>,
pub enable_downvotes: Option<bool>,
pub community_creation_admin_only: Option<bool>,
pub require_email_verification: Option<bool>,
pub application_question: Option<Option<String>>,
@ -148,11 +160,15 @@ pub struct LocalSiteUpdateForm {
pub captcha_enabled: Option<bool>,
pub captcha_difficulty: Option<String>,
pub registration_mode: Option<RegistrationMode>,
pub oauth_registration: Option<bool>,
pub reports_email_admins: Option<bool>,
pub updated: Option<Option<DateTime<Utc>>>,
pub federation_signed_fetch: Option<bool>,
pub default_post_listing_mode: Option<PostListingMode>,
pub default_post_sort_type: Option<PostSortType>,
pub default_comment_sort_type: Option<CommentSortType>,
pub oauth_registration: Option<bool>,
pub post_upvotes: Option<FederationMode>,
pub post_downvotes: Option<FederationMode>,
pub comment_upvotes: Option<FederationMode>,
pub comment_downvotes: Option<FederationMode>,
}

View file

@ -48,8 +48,6 @@ pub struct Person {
#[cfg_attr(feature = "full", ts(skip))]
#[serde(skip, default = "placeholder_apub_url")]
pub inbox_url: DbUrl,
#[serde(skip)]
pub shared_inbox_url: Option<DbUrl>,
/// A matrix id, usually given an @person:matrix.org
pub matrix_user_id: Option<String>,
/// Whether the person is a bot account.
@ -93,8 +91,6 @@ pub struct PersonInsertForm {
#[new(default)]
pub inbox_url: Option<DbUrl>,
#[new(default)]
pub shared_inbox_url: Option<DbUrl>,
#[new(default)]
pub matrix_user_id: Option<String>,
#[new(default)]
pub bot_account: Option<bool>,
@ -119,7 +115,6 @@ pub struct PersonUpdateForm {
pub banner: Option<Option<DbUrl>>,
pub deleted: Option<bool>,
pub inbox_url: Option<DbUrl>,
pub shared_inbox_url: Option<Option<DbUrl>>,
pub matrix_user_id: Option<Option<String>>,
pub bot_account: Option<bool>,
pub ban_expires: Option<Option<DateTime<Utc>>>,

View file

@ -259,7 +259,6 @@ impl CommentReportQuery {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
#[expect(clippy::indexing_slicing)]
mod tests {
@ -284,27 +283,24 @@ mod tests {
CommunityVisibility,
SubscribedType,
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_crv");
let inserted_timmy = Person::create(pool, &new_person).await.unwrap();
let inserted_timmy = Person::create(pool, &new_person).await?;
let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id);
let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![])
.await
.unwrap();
let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?;
let timmy_view = LocalUserView {
local_user: timmy_local_user,
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
@ -314,12 +310,12 @@ mod tests {
let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_crv");
let inserted_sara = Person::create(pool, &new_person_2).await.unwrap();
let inserted_sara = Person::create(pool, &new_person_2).await?;
// Add a third person, since new ppl can only report something once.
let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_crv");
let inserted_jessica = Person::create(pool, &new_person_3).await.unwrap();
let inserted_jessica = Person::create(pool, &new_person_3).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -327,7 +323,7 @@ mod tests {
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let inserted_community = Community::create(pool, &new_community).await?;
// Make timmy a mod
let timmy_moderator_form = CommunityModeratorForm {
@ -335,9 +331,7 @@ mod tests {
person_id: inserted_timmy.id,
};
let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form)
.await
.unwrap();
let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?;
let new_post = PostInsertForm::new(
"A test post crv".into(),
@ -345,14 +339,14 @@ mod tests {
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
let comment_form = CommentInsertForm::new(
inserted_timmy.id,
inserted_post.id,
"A test comment 32".into(),
);
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
// sara reports
let sara_report_form = CommentReportForm {
@ -362,9 +356,7 @@ mod tests {
reason: "from sara".into(),
};
let inserted_sara_report = CommentReport::report(pool, &sara_report_form)
.await
.unwrap();
let inserted_sara_report = CommentReport::report(pool, &sara_report_form).await?;
// jessica reports
let jessica_report_form = CommentReportForm {
@ -374,18 +366,12 @@ mod tests {
reason: "from jessica".into(),
};
let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form)
.await
.unwrap();
let inserted_jessica_report = CommentReport::report(pool, &jessica_report_form).await?;
let agg = CommentAggregates::read(pool, inserted_comment.id)
.await
.unwrap();
let agg = CommentAggregates::read(pool, inserted_comment.id).await?;
let read_jessica_report_view =
CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id)
.await
.unwrap();
CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?;
let expected_jessica_report_view = CommentReportView {
comment_report: inserted_jessica_report.clone(),
comment: inserted_comment.clone(),
@ -416,7 +402,6 @@ mod tests {
last_refreshed_at: inserted_community.last_refreshed_at,
followers_url: inserted_community.followers_url,
inbox_url: inserted_community.inbox_url,
shared_inbox_url: inserted_community.shared_inbox_url,
moderators_url: inserted_community.moderators_url,
featured_url: inserted_community.featured_url,
instance_id: inserted_instance.id,
@ -437,7 +422,6 @@ mod tests {
banner: None,
updated: None,
inbox_url: inserted_jessica.inbox_url.clone(),
shared_inbox_url: None,
matrix_user_id: None,
ban_expires: None,
instance_id: inserted_instance.id,
@ -460,7 +444,6 @@ mod tests {
banner: None,
updated: None,
inbox_url: inserted_timmy.inbox_url.clone(),
shared_inbox_url: None,
matrix_user_id: None,
ban_expires: None,
instance_id: inserted_instance.id,
@ -502,7 +485,6 @@ mod tests {
banner: None,
updated: None,
inbox_url: inserted_sara.inbox_url.clone(),
shared_inbox_url: None,
matrix_user_id: None,
ban_expires: None,
instance_id: inserted_instance.id,
@ -514,8 +496,7 @@ mod tests {
// Do a batch read of timmys reports
let reports = CommentReportQuery::default()
.list(pool, &timmy_view)
.await
.unwrap();
.await?;
assert_eq!(
reports,
@ -526,19 +507,14 @@ mod tests {
);
// Make sure the counts are correct
let report_count = CommentReportView::get_report_count(pool, inserted_timmy.id, false, None)
.await
.unwrap();
let report_count =
CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?;
assert_eq!(2, report_count);
// Try to resolve the report
CommentReport::resolve(pool, inserted_jessica_report.id, inserted_timmy.id)
.await
.unwrap();
CommentReport::resolve(pool, inserted_jessica_report.id, inserted_timmy.id).await?;
let read_jessica_report_view_after_resolve =
CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id)
.await
.unwrap();
CommentReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?;
let mut expected_jessica_report_view_after_resolve = expected_jessica_report_view;
expected_jessica_report_view_after_resolve
@ -570,7 +546,6 @@ mod tests {
private_key: inserted_timmy.private_key.clone(),
public_key: inserted_timmy.public_key.clone(),
last_refreshed_at: inserted_timmy.last_refreshed_at,
shared_inbox_url: None,
matrix_user_id: None,
ban_expires: None,
instance_id: inserted_instance.id,
@ -588,24 +563,21 @@ mod tests {
..Default::default()
}
.list(pool, &timmy_view)
.await
.unwrap();
.await?;
assert_eq!(reports_after_resolve[0], expected_sara_report_view);
assert_eq!(reports_after_resolve.len(), 1);
// Make sure the counts are correct
let report_count_after_resolved =
CommentReportView::get_report_count(pool, inserted_timmy.id, false, None)
.await
.unwrap();
CommentReportView::get_report_count(pool, inserted_timmy.id, false, None).await?;
assert_eq!(1, report_count_after_resolved);
Person::delete(pool, inserted_timmy.id).await.unwrap();
Person::delete(pool, inserted_sara.id).await.unwrap();
Person::delete(pool, inserted_jessica.id).await.unwrap();
Community::delete(pool, inserted_community.id)
.await
.unwrap();
Instance::delete(pool, inserted_instance.id).await.unwrap();
Person::delete(pool, inserted_timmy.id).await?;
Person::delete(pool, inserted_sara.id).await?;
Person::delete(pool, inserted_jessica.id).await?;
Community::delete(pool, inserted_community.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
}

View file

@ -216,7 +216,6 @@ fn queries<'a>() -> Queries<
query = query.filter(post::community_id.eq(community_id));
}
if let Some(listing_type) = options.listing_type {
let is_subscribed = exists(
community_follower::table.filter(
post::community_id
@ -225,7 +224,7 @@ fn queries<'a>() -> Queries<
),
);
match listing_type {
match options.listing_type.unwrap_or_default() {
ListingType::Subscribed => query = query.filter(is_subscribed), /* TODO could be this: and(community_follower::person_id.eq(person_id_join)), */
ListingType::Local => {
query = query
@ -243,7 +242,6 @@ fn queries<'a>() -> Queries<
));
}
}
}
// If its saved only, then filter, and order by the saved time, not the comment creation time.
if options.saved_only.unwrap_or_default() {
@ -423,7 +421,6 @@ impl<'a> CommentQuery<'a> {
#[cfg(test)]
#[expect(clippy::indexing_slicing)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
@ -511,7 +508,7 @@ mod tests {
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await?;
let english_id = Language::read_id_from_code(pool, Some("en")).await?;
let english_id = Language::read_id_from_code(pool, "en").await?;
// Create a comment tree with this hierarchy
// 0
@ -522,7 +519,7 @@ mod tests {
// \
// 5
let comment_form_0 = CommentInsertForm {
language_id: english_id,
language_id: Some(english_id),
..CommentInsertForm::new(
inserted_timmy_person.id,
inserted_post.id,
@ -533,7 +530,7 @@ mod tests {
let inserted_comment_0 = Comment::create(pool, &comment_form_0, None).await?;
let comment_form_1 = CommentInsertForm {
language_id: english_id,
language_id: Some(english_id),
..CommentInsertForm::new(
inserted_sara_person.id,
inserted_post.id,
@ -543,9 +540,9 @@ mod tests {
let inserted_comment_1 =
Comment::create(pool, &comment_form_1, Some(&inserted_comment_0.path)).await?;
let finnish_id = Language::read_id_from_code(pool, Some("fi")).await?;
let finnish_id = Language::read_id_from_code(pool, "fi").await?;
let comment_form_2 = CommentInsertForm {
language_id: finnish_id,
language_id: Some(finnish_id),
..CommentInsertForm::new(
inserted_timmy_person.id,
inserted_post.id,
@ -557,7 +554,7 @@ mod tests {
Comment::create(pool, &comment_form_2, Some(&inserted_comment_0.path)).await?;
let comment_form_3 = CommentInsertForm {
language_id: english_id,
language_id: Some(english_id),
..CommentInsertForm::new(
inserted_timmy_person.id,
inserted_post.id,
@ -567,9 +564,7 @@ mod tests {
let _inserted_comment_3 =
Comment::create(pool, &comment_form_3, Some(&inserted_comment_1.path)).await?;
let polish_id = Language::read_id_from_code(pool, Some("pl"))
.await?
.unwrap();
let polish_id = Language::read_id_from_code(pool, "pl").await?;
let comment_form_4 = CommentInsertForm {
language_id: Some(polish_id),
..CommentInsertForm::new(
@ -606,7 +601,6 @@ mod tests {
let comment_like_form = CommentLikeForm {
comment_id: inserted_comment_0.id,
post_id: inserted_post.id,
person_id: inserted_timmy_person.id,
score: 1,
};
@ -655,8 +649,8 @@ mod tests {
.await?;
assert_eq!(
&expected_comment_view_no_person,
read_comment_views_no_person.first().unwrap()
Some(&expected_comment_view_no_person),
read_comment_views_no_person.first()
);
let read_comment_views_with_person = CommentQuery {
@ -706,7 +700,6 @@ mod tests {
// Like a new comment
let comment_like_form = CommentLikeForm {
comment_id: data.inserted_comment_1.id,
post_id: data.inserted_post.id,
person_id: data.timmy_local_user_view.person.id,
score: 1,
};
@ -832,9 +825,7 @@ mod tests {
assert_length!(5, all_languages);
// change user lang to finnish, should only show one post in finnish and one undetermined
let finnish_id = Language::read_id_from_code(pool, Some("fi"))
.await?
.unwrap();
let finnish_id = Language::read_id_from_code(pool, "fi").await?;
LocalUserLanguage::update(
pool,
vec![finnish_id],
@ -853,8 +844,8 @@ mod tests {
.find(|c| c.comment.language_id == finnish_id);
assert!(finnish_comment.is_some());
assert_eq!(
data.inserted_comment_2.content,
finnish_comment.unwrap().comment.content
Some(&data.inserted_comment_2.content),
finnish_comment.map(|c| &c.comment.content)
);
// now show all comments with undetermined language (which is the default value)
@ -1058,7 +1049,6 @@ mod tests {
banner: None,
updated: None,
inbox_url: data.timmy_local_user_view.person.inbox_url.clone(),
shared_inbox_url: None,
matrix_user_id: None,
ban_expires: None,
instance_id: data.inserted_instance.id,
@ -1114,7 +1104,6 @@ mod tests {
last_refreshed_at: data.inserted_community.last_refreshed_at,
followers_url: data.inserted_community.followers_url.clone(),
inbox_url: data.inserted_community.inbox_url.clone(),
shared_inbox_url: data.inserted_community.shared_inbox_url.clone(),
moderators_url: data.inserted_community.moderators_url.clone(),
featured_url: data.inserted_community.featured_url.clone(),
visibility: CommunityVisibility::Public,

View file

@ -5,6 +5,12 @@ use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
newtypes::{LocalUserId, OAuthProviderId, PersonId},
schema::{local_user, local_user_vote_display_mode, oauth_account, person, person_aggregates},
source::{
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
},
traits::Crud,
utils::{
functions::{coalesce, lower},
DbConn,
@ -134,6 +140,31 @@ impl LocalUserView {
pub async fn list_admins_with_emails(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {
queries().list(pool, ListMode::AdminsWithEmails).await
}
pub async fn create_test_user(
pool: &mut DbPool<'_>,
name: &str,
bio: &str,
admin: bool,
) -> Result<Self, Error> {
let instance_id = Instance::read_or_create(pool, "example.com".to_string())
.await?
.id;
let person_form = PersonInsertForm {
display_name: Some(name.to_owned()),
bio: Some(bio.to_owned()),
..PersonInsertForm::test_form(instance_id, name)
};
let person = Person::create(pool, &person_form).await?;
let user_form = match admin {
true => LocalUserInsertForm::test_form_admin(person.id),
false => LocalUserInsertForm::test_form(person.id),
};
let local_user = LocalUser::create(pool, &user_form, vec![]).await?;
LocalUserView::read(pool, local_user.id).await
}
}
impl FromRequest for LocalUserView {

View file

@ -284,7 +284,6 @@ impl PostReportQuery {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
#[expect(clippy::indexing_slicing)]
mod tests {
@ -306,27 +305,24 @@ mod tests {
traits::{Crud, Joinable, Reportable},
utils::build_db_pool_for_tests,
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_prv");
let inserted_timmy = Person::create(pool, &new_person).await.unwrap();
let inserted_timmy = Person::create(pool, &new_person).await?;
let new_local_user = LocalUserInsertForm::test_form(inserted_timmy.id);
let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![])
.await
.unwrap();
let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?;
let timmy_view = LocalUserView {
local_user: timmy_local_user,
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
@ -336,12 +332,12 @@ mod tests {
let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_prv");
let inserted_sara = Person::create(pool, &new_person_2).await.unwrap();
let inserted_sara = Person::create(pool, &new_person_2).await?;
// Add a third person, since new ppl can only report something once.
let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "jessica_prv");
let inserted_jessica = Person::create(pool, &new_person_3).await.unwrap();
let inserted_jessica = Person::create(pool, &new_person_3).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -349,7 +345,7 @@ mod tests {
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let inserted_community = Community::create(pool, &new_community).await?;
// Make timmy a mod
let timmy_moderator_form = CommunityModeratorForm {
@ -357,16 +353,14 @@ mod tests {
person_id: inserted_timmy.id,
};
let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form)
.await
.unwrap();
let _inserted_moderator = CommunityModerator::join(pool, &timmy_moderator_form).await?;
let new_post = PostInsertForm::new(
"A test post crv".into(),
inserted_timmy.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
// sara reports
let sara_report_form = PostReportForm {
@ -378,14 +372,14 @@ mod tests {
reason: "from sara".into(),
};
PostReport::report(pool, &sara_report_form).await.unwrap();
PostReport::report(pool, &sara_report_form).await?;
let new_post_2 = PostInsertForm::new(
"A test post crv 2".into(),
inserted_timmy.id,
inserted_community.id,
);
let inserted_post_2 = Post::create(pool, &new_post_2).await.unwrap();
let inserted_post_2 = Post::create(pool, &new_post_2).await?;
// jessica reports
let jessica_report_form = PostReportForm {
@ -397,14 +391,10 @@ mod tests {
reason: "from jessica".into(),
};
let inserted_jessica_report = PostReport::report(pool, &jessica_report_form)
.await
.unwrap();
let inserted_jessica_report = PostReport::report(pool, &jessica_report_form).await?;
let read_jessica_report_view =
PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id)
.await
.unwrap();
PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?;
assert_eq!(
read_jessica_report_view.post_report,
@ -418,30 +408,23 @@ mod tests {
assert_eq!(read_jessica_report_view.resolver, None);
// Do a batch read of timmys reports
let reports = PostReportQuery::default()
.list(pool, &timmy_view)
.await
.unwrap();
let reports = PostReportQuery::default().list(pool, &timmy_view).await?;
assert_eq!(reports[1].creator.id, inserted_sara.id);
assert_eq!(reports[0].creator.id, inserted_jessica.id);
// Make sure the counts are correct
let report_count = PostReportView::get_report_count(pool, inserted_timmy.id, false, None)
.await
.unwrap();
let report_count =
PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?;
assert_eq!(2, report_count);
// Pretend the post was removed, and resolve all reports for that object.
// This is called manually in the API for post removals
PostReport::resolve_all_for_object(pool, inserted_jessica_report.post_id, inserted_timmy.id)
.await
.unwrap();
.await?;
let read_jessica_report_view_after_resolve =
PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id)
.await
.unwrap();
PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?;
assert!(read_jessica_report_view_after_resolve.post_report.resolved);
assert_eq!(
read_jessica_report_view_after_resolve
@ -450,8 +433,10 @@ mod tests {
Some(inserted_timmy.id)
);
assert_eq!(
read_jessica_report_view_after_resolve.resolver.unwrap().id,
inserted_timmy.id
read_jessica_report_view_after_resolve
.resolver
.map(|r| r.id),
Some(inserted_timmy.id)
);
// Do a batch read of timmys reports
@ -461,24 +446,21 @@ mod tests {
..Default::default()
}
.list(pool, &timmy_view)
.await
.unwrap();
.await?;
assert_length!(1, reports_after_resolve);
assert_eq!(reports_after_resolve[0].creator.id, inserted_sara.id);
// Make sure the counts are correct
let report_count_after_resolved =
PostReportView::get_report_count(pool, inserted_timmy.id, false, None)
.await
.unwrap();
PostReportView::get_report_count(pool, inserted_timmy.id, false, None).await?;
assert_eq!(1, report_count_after_resolved);
Person::delete(pool, inserted_timmy.id).await.unwrap();
Person::delete(pool, inserted_sara.id).await.unwrap();
Person::delete(pool, inserted_jessica.id).await.unwrap();
Community::delete(pool, inserted_community.id)
.await
.unwrap();
Instance::delete(pool, inserted_instance.id).await.unwrap();
Person::delete(pool, inserted_timmy.id).await?;
Person::delete(pool, inserted_sara.id).await?;
Person::delete(pool, inserted_jessica.id).await?;
Community::delete(pool, inserted_community.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
}

View file

@ -346,16 +346,14 @@ fn queries<'a>() -> Queries<
query = query.filter(post_aggregates::creator_id.eq(creator_id));
}
if let Some(listing_type) = options.listing_type {
if let Some(person_id) = options.local_user.person_id() {
let is_subscribed = exists(
community_follower::table.filter(
post_aggregates::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(person_id)),
.and(community_follower::person_id.eq(person_id_join)),
),
);
match listing_type {
match options.listing_type.unwrap_or_default() {
ListingType::Subscribed => query = query.filter(is_subscribed),
ListingType::Local => {
query = query
@ -368,26 +366,11 @@ fn queries<'a>() -> Queries<
community_moderator::table.filter(
post::community_id
.eq(community_moderator::community_id)
.and(community_moderator::person_id.eq(person_id)),
.and(community_moderator::person_id.eq(person_id_join)),
),
));
}
}
}
// If your person_id is missing, only show local
else {
match listing_type {
ListingType::Local => {
query = query
.filter(community::local.eq(true))
.filter(community::hidden.eq(false));
}
_ => query = query.filter(community::hidden.eq(false)),
}
}
} else {
query = query.filter(community::hidden.eq(false));
}
if let Some(search_term) = &options.search_term {
if options.url_only.unwrap_or_default() {
@ -739,7 +722,6 @@ impl<'a> PostQuery<'a> {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{
post_view::{PaginationCursorData, PostQuery, PostView},
@ -755,6 +737,8 @@ mod tests {
comment::{Comment, CommentInsertForm},
community::{
Community,
CommunityFollower,
CommunityFollowerForm,
CommunityInsertForm,
CommunityModerator,
CommunityModeratorForm,
@ -783,7 +767,7 @@ mod tests {
},
site::Site,
},
traits::{Bannable, Blockable, Crud, Joinable, Likeable, Saveable},
traits::{Bannable, Blockable, Crud, Followable, Joinable, Likeable, Saveable},
utils::{build_db_pool, build_db_pool_for_tests, DbPool, RANK_DEFAULT},
CommunityVisibility,
PostSortType,
@ -1305,13 +1289,9 @@ mod tests {
let pool = &mut pool.into();
let data = init_data(pool).await?;
let spanish_id = Language::read_id_from_code(pool, Some("es"))
.await?
.expect("spanish should exist");
let spanish_id = Language::read_id_from_code(pool, "es").await?;
let french_id = Language::read_id_from_code(pool, Some("fr"))
.await?
.expect("french should exist");
let french_id = Language::read_id_from_code(pool, "fr").await?;
let post_spanish = PostInsertForm {
language_id: Some(spanish_id),
@ -1437,6 +1417,43 @@ mod tests {
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn post_listings_hidden_community() -> LemmyResult<()> {
let pool = &build_db_pool().await?;
let pool = &mut pool.into();
let data = init_data(pool).await?;
Community::update(
pool,
data.inserted_community.id,
&CommunityUpdateForm {
hidden: Some(true),
..Default::default()
},
)
.await?;
let posts = PostQuery::default().list(&data.site, pool).await?;
assert!(posts.is_empty());
let posts = data.default_post_query().list(&data.site, pool).await?;
assert!(posts.is_empty());
// Follow the community
let form = CommunityFollowerForm {
community_id: data.inserted_community.id,
person_id: data.local_user_view.person.id,
pending: false,
};
CommunityFollower::follow(pool, &form).await?;
let posts = data.default_post_query().list(&data.site, pool).await?;
assert!(!posts.is_empty());
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn post_listing_instance_block() -> LemmyResult<()> {
@ -1690,7 +1707,7 @@ mod tests {
assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_hidden));
// Make sure that hidden field is true.
assert!(&post_listings_show_hidden.first().unwrap().hidden);
assert!(&post_listings_show_hidden.first().is_some_and(|p| p.hidden));
cleanup(data, pool).await
}
@ -1726,7 +1743,7 @@ mod tests {
assert_eq!(vec![POST_BY_BOT, POST], names(&post_listings_show_nsfw));
// Make sure that nsfw field is true.
assert!(&post_listings_show_nsfw.first().unwrap().post.nsfw);
assert!(&post_listings_show_nsfw.first().is_some_and(|p| p.post.nsfw));
cleanup(data, pool).await
}
@ -1795,7 +1812,6 @@ mod tests {
banner: None,
updated: None,
inbox_url: inserted_person.inbox_url.clone(),
shared_inbox_url: None,
matrix_user_id: None,
ban_expires: None,
instance_id: data.inserted_instance.id,
@ -1830,7 +1846,6 @@ mod tests {
last_refreshed_at: inserted_community.last_refreshed_at,
followers_url: inserted_community.followers_url.clone(),
inbox_url: inserted_community.inbox_url.clone(),
shared_inbox_url: inserted_community.shared_inbox_url.clone(),
moderators_url: inserted_community.moderators_url.clone(),
featured_url: inserted_community.featured_url.clone(),
visibility: CommunityVisibility::Public,

View file

@ -111,7 +111,6 @@ impl PrivateMessageReportQuery {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
#[expect(clippy::indexing_slicing)]
mod tests {
@ -127,24 +126,23 @@ mod tests {
traits::{Crud, Reportable},
utils::build_db_pool_for_tests,
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person_1 = PersonInsertForm::test_form(inserted_instance.id, "timmy_mrv");
let inserted_timmy = Person::create(pool, &new_person_1).await.unwrap();
let inserted_timmy = Person::create(pool, &new_person_1).await?;
let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "jessica_mrv");
let inserted_jessica = Person::create(pool, &new_person_2).await.unwrap();
let inserted_jessica = Person::create(pool, &new_person_2).await?;
// timmy sends private message to jessica
let pm_form = PrivateMessageInsertForm::new(
@ -152,7 +150,7 @@ mod tests {
inserted_jessica.id,
"something offensive".to_string(),
);
let pm = PrivateMessage::create(pool, &pm_form).await.unwrap();
let pm = PrivateMessage::create(pool, &pm_form).await?;
// jessica reports private message
let pm_report_form = PrivateMessageReportForm {
@ -161,14 +159,9 @@ mod tests {
private_message_id: pm.id,
reason: "its offensive".to_string(),
};
let pm_report = PrivateMessageReport::report(pool, &pm_report_form)
.await
.unwrap();
let pm_report = PrivateMessageReport::report(pool, &pm_report_form).await?;
let reports = PrivateMessageReportQuery::default()
.list(pool)
.await
.unwrap();
let reports = PrivateMessageReportQuery::default().list(pool).await?;
assert_length!(1, reports);
assert!(!reports[0].private_message_report.resolved);
assert_eq!(inserted_timmy.name, reports[0].private_message_creator.name);
@ -177,28 +170,27 @@ mod tests {
assert_eq!(pm.content, reports[0].private_message.content);
let new_person_3 = PersonInsertForm::test_form(inserted_instance.id, "admin_mrv");
let inserted_admin = Person::create(pool, &new_person_3).await.unwrap();
let inserted_admin = Person::create(pool, &new_person_3).await?;
// admin resolves the report (after taking appropriate action)
PrivateMessageReport::resolve(pool, pm_report.id, inserted_admin.id)
.await
.unwrap();
PrivateMessageReport::resolve(pool, pm_report.id, inserted_admin.id).await?;
let reports = PrivateMessageReportQuery {
unresolved_only: (false),
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_length!(1, reports);
assert!(reports[0].private_message_report.resolved);
assert!(reports[0].resolver.is_some());
assert_eq!(
inserted_admin.name,
reports[0].resolver.as_ref().unwrap().name
Some(&inserted_admin.name),
reports[0].resolver.as_ref().map(|r| &r.name)
);
Instance::delete(pool, inserted_instance.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
}

View file

@ -173,7 +173,6 @@ impl PrivateMessageQuery {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
#[expect(clippy::indexing_slicing)]
mod tests {
@ -205,45 +204,35 @@ mod tests {
async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult<Data> {
let message_content = String::new();
let instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let timmy_form = PersonInsertForm::test_form(instance.id, "timmy_rav");
let timmy = Person::create(pool, &timmy_form).await.unwrap();
let timmy = Person::create(pool, &timmy_form).await?;
let sara_form = PersonInsertForm::test_form(instance.id, "sara_rav");
let sara = Person::create(pool, &sara_form).await.unwrap();
let sara = Person::create(pool, &sara_form).await?;
let jess_form = PersonInsertForm::test_form(instance.id, "jess_rav");
let jess = Person::create(pool, &jess_form).await.unwrap();
let jess = Person::create(pool, &jess_form).await?;
let sara_timmy_message_form =
PrivateMessageInsertForm::new(sara.id, timmy.id, message_content.clone());
PrivateMessage::create(pool, &sara_timmy_message_form)
.await
.unwrap();
PrivateMessage::create(pool, &sara_timmy_message_form).await?;
let sara_jess_message_form =
PrivateMessageInsertForm::new(sara.id, jess.id, message_content.clone());
PrivateMessage::create(pool, &sara_jess_message_form)
.await
.unwrap();
PrivateMessage::create(pool, &sara_jess_message_form).await?;
let timmy_sara_message_form =
PrivateMessageInsertForm::new(timmy.id, sara.id, message_content.clone());
PrivateMessage::create(pool, &timmy_sara_message_form)
.await
.unwrap();
PrivateMessage::create(pool, &timmy_sara_message_form).await?;
let jess_timmy_message_form =
PrivateMessageInsertForm::new(jess.id, timmy.id, message_content.clone());
PrivateMessage::create(pool, &jess_timmy_message_form)
.await
.unwrap();
PrivateMessage::create(pool, &jess_timmy_message_form).await?;
Ok(Data {
instance,
@ -255,7 +244,7 @@ mod tests {
async fn cleanup(instance_id: InstanceId, pool: &mut DbPool<'_>) -> LemmyResult<()> {
// This also deletes all persons and private messages thanks to sql `on delete cascade`
Instance::delete(pool, instance_id).await.unwrap();
Instance::delete(pool, instance_id).await?;
Ok(())
}
@ -277,8 +266,7 @@ mod tests {
..Default::default()
}
.list(pool, timmy.id)
.await
.unwrap();
.await?;
assert_length!(3, &timmy_messages);
assert_eq!(timmy_messages[0].creator.id, jess.id);
@ -294,8 +282,7 @@ mod tests {
..Default::default()
}
.list(pool, timmy.id)
.await
.unwrap();
.await?;
assert_length!(2, &timmy_unread_messages);
assert_eq!(timmy_unread_messages[0].creator.id, jess.id);
@ -309,8 +296,7 @@ mod tests {
..Default::default()
}
.list(pool, timmy.id)
.await
.unwrap();
.await?;
assert_length!(2, &timmy_sara_messages);
assert_eq!(timmy_sara_messages[0].creator.id, timmy.id);
@ -324,8 +310,7 @@ mod tests {
..Default::default()
}
.list(pool, timmy.id)
.await
.unwrap();
.await?;
assert_length!(1, &timmy_sara_unread_messages);
assert_eq!(timmy_sara_unread_messages[0].creator.id, sara.id);
@ -352,9 +337,7 @@ mod tests {
target_id: sara.id,
};
let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form)
.await
.unwrap();
let inserted_block = PersonBlock::block(pool, &timmy_blocks_sara_form).await?;
let expected_block = PersonBlock {
person_id: timmy.id,
@ -369,14 +352,11 @@ mod tests {
..Default::default()
}
.list(pool, timmy.id)
.await
.unwrap();
.await?;
assert_length!(1, &timmy_messages);
let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id)
.await
.unwrap();
let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?;
assert_eq!(timmy_unread_messages, 1);
cleanup(instance.id, pool).await
@ -399,9 +379,7 @@ mod tests {
instance_id: sara.instance_id,
};
let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form)
.await
.unwrap();
let inserted_instance_block = InstanceBlock::block(pool, &timmy_blocks_instance_form).await?;
let expected_instance_block = InstanceBlock {
person_id: timmy.id,
@ -416,14 +394,11 @@ mod tests {
..Default::default()
}
.list(pool, timmy.id)
.await
.unwrap();
.await?;
assert_length!(0, &timmy_messages);
let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id)
.await
.unwrap();
let timmy_unread_messages = PrivateMessageView::get_unread_messages(pool, timmy.id).await?;
assert_eq!(timmy_unread_messages, 0);
cleanup(instance.id, pool).await
}

View file

@ -135,7 +135,6 @@ impl RegistrationApplicationQuery {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::registration_application_view::{
@ -156,38 +155,34 @@ mod tests {
traits::Crud,
utils::build_db_pool_for_tests,
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let timmy_person_form = PersonInsertForm::test_form(inserted_instance.id, "timmy_rav");
let inserted_timmy_person = Person::create(pool, &timmy_person_form).await.unwrap();
let inserted_timmy_person = Person::create(pool, &timmy_person_form).await?;
let timmy_local_user_form = LocalUserInsertForm::test_form_admin(inserted_timmy_person.id);
let _inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![])
.await
.unwrap();
let _inserted_timmy_local_user =
LocalUser::create(pool, &timmy_local_user_form, vec![]).await?;
let sara_person_form = PersonInsertForm::test_form(inserted_instance.id, "sara_rav");
let inserted_sara_person = Person::create(pool, &sara_person_form).await.unwrap();
let inserted_sara_person = Person::create(pool, &sara_person_form).await?;
let sara_local_user_form = LocalUserInsertForm::test_form(inserted_sara_person.id);
let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![])
.await
.unwrap();
let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![]).await?;
// Sara creates an application
let sara_app_form = RegistrationApplicationInsertForm {
@ -195,23 +190,17 @@ mod tests {
answer: "LET ME IIIIINN".to_string(),
};
let sara_app = RegistrationApplication::create(pool, &sara_app_form)
.await
.unwrap();
let sara_app = RegistrationApplication::create(pool, &sara_app_form).await?;
let read_sara_app_view = RegistrationApplicationView::read(pool, sara_app.id)
.await
.unwrap();
let read_sara_app_view = RegistrationApplicationView::read(pool, sara_app.id).await?;
let jess_person_form = PersonInsertForm::test_form(inserted_instance.id, "jess_rav");
let inserted_jess_person = Person::create(pool, &jess_person_form).await.unwrap();
let inserted_jess_person = Person::create(pool, &jess_person_form).await?;
let jess_local_user_form = LocalUserInsertForm::test_form(inserted_jess_person.id);
let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![])
.await
.unwrap();
let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![]).await?;
// Sara creates an application
let jess_app_form = RegistrationApplicationInsertForm {
@ -219,13 +208,9 @@ mod tests {
answer: "LET ME IIIIINN".to_string(),
};
let jess_app = RegistrationApplication::create(pool, &jess_app_form)
.await
.unwrap();
let jess_app = RegistrationApplication::create(pool, &jess_app_form).await?;
let read_jess_app_view = RegistrationApplicationView::read(pool, jess_app.id)
.await
.unwrap();
let read_jess_app_view = RegistrationApplicationView::read(pool, jess_app.id).await?;
let mut expected_sara_app_view = RegistrationApplicationView {
registration_application: sara_app.clone(),
@ -273,7 +258,6 @@ mod tests {
banner: None,
updated: None,
inbox_url: inserted_sara_person.inbox_url.clone(),
shared_inbox_url: None,
matrix_user_id: None,
instance_id: inserted_instance.id,
private_key: inserted_sara_person.private_key,
@ -291,8 +275,7 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_eq!(
apps,
@ -300,9 +283,7 @@ mod tests {
);
// Make sure the counts are correct
let unread_count = RegistrationApplicationView::get_unread_count(pool, false)
.await
.unwrap();
let unread_count = RegistrationApplicationView::get_unread_count(pool, false).await?;
assert_eq!(unread_count, 2);
// Approve the application
@ -311,9 +292,7 @@ mod tests {
deny_reason: None,
};
RegistrationApplication::update(pool, sara_app.id, &approve_form)
.await
.unwrap();
RegistrationApplication::update(pool, sara_app.id, &approve_form).await?;
// Update the local_user row
let approve_local_user_form = LocalUserUpdateForm {
@ -321,13 +300,10 @@ mod tests {
..Default::default()
};
LocalUser::update(pool, inserted_sara_local_user.id, &approve_local_user_form)
.await
.unwrap();
LocalUser::update(pool, inserted_sara_local_user.id, &approve_local_user_form).await?;
let read_sara_app_view_after_approve = RegistrationApplicationView::read(pool, sara_app.id)
.await
.unwrap();
let read_sara_app_view_after_approve =
RegistrationApplicationView::read(pool, sara_app.id).await?;
// Make sure the columns changed
expected_sara_app_view
@ -351,7 +327,6 @@ mod tests {
banner: None,
updated: None,
inbox_url: inserted_timmy_person.inbox_url.clone(),
shared_inbox_url: None,
matrix_user_id: None,
instance_id: inserted_instance.id,
private_key: inserted_timmy_person.private_key,
@ -367,28 +342,23 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_eq!(apps_after_resolve, vec![read_jess_app_view]);
// Make sure the counts are correct
let unread_count_after_approve = RegistrationApplicationView::get_unread_count(pool, false)
.await
.unwrap();
let unread_count_after_approve =
RegistrationApplicationView::get_unread_count(pool, false).await?;
assert_eq!(unread_count_after_approve, 1);
// Make sure the not undenied_only has all the apps
let all_apps = RegistrationApplicationQuery::default()
.list(pool)
.await
.unwrap();
let all_apps = RegistrationApplicationQuery::default().list(pool).await?;
assert_eq!(all_apps.len(), 2);
Person::delete(pool, inserted_timmy_person.id)
.await
.unwrap();
Person::delete(pool, inserted_sara_person.id).await.unwrap();
Person::delete(pool, inserted_jess_person.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await.unwrap();
Person::delete(pool, inserted_timmy_person.id).await?;
Person::delete(pool, inserted_sara_person.id).await?;
Person::delete(pool, inserted_jess_person.id).await?;
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
}

View file

@ -10,7 +10,7 @@ use diesel::{
use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
newtypes::{CommentId, PostId},
schema::{comment_like, community_person_ban, person, post, post_like},
schema::{comment, comment_like, community_person_ban, person, post, post_like},
utils::{get_conn, limit_and_offset, DbPool},
};
@ -59,7 +59,8 @@ impl VoteView {
comment_like::table
.inner_join(person::table)
.inner_join(post::table)
.inner_join(comment::table)
.inner_join(post::table.on(comment::post_id.eq(post::id)))
// Join to community_person_ban to get creator_banned_from_community
.left_join(
community_person_ban::table.on(
@ -83,7 +84,6 @@ impl VoteView {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::structs::VoteView;
@ -98,26 +98,25 @@ mod tests {
traits::{Bannable, Crud, Likeable},
utils::build_db_pool_for_tests,
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
#[tokio::test]
#[serial]
async fn post_and_comment_vote_views() {
async fn post_and_comment_vote_views() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let new_person = PersonInsertForm::test_form(inserted_instance.id, "timmy_vv");
let inserted_timmy = Person::create(pool, &new_person).await.unwrap();
let inserted_timmy = Person::create(pool, &new_person).await?;
let new_person_2 = PersonInsertForm::test_form(inserted_instance.id, "sara_vv");
let inserted_sara = Person::create(pool, &new_person_2).await.unwrap();
let inserted_sara = Person::create(pool, &new_person_2).await?;
let new_community = CommunityInsertForm::new(
inserted_instance.id,
@ -125,21 +124,21 @@ mod tests {
"nada".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
let inserted_community = Community::create(pool, &new_community).await?;
let new_post = PostInsertForm::new(
"A test post vv".into(),
inserted_timmy.id,
inserted_community.id,
);
let inserted_post = Post::create(pool, &new_post).await.unwrap();
let inserted_post = Post::create(pool, &new_post).await?;
let comment_form = CommentInsertForm::new(
inserted_timmy.id,
inserted_post.id,
"A test comment vv".into(),
);
let inserted_comment = Comment::create(pool, &comment_form, None).await.unwrap();
let inserted_comment = Comment::create(pool, &comment_form, None).await?;
// Timmy upvotes his own post
let timmy_post_vote_form = PostLikeForm {
@ -147,7 +146,7 @@ mod tests {
person_id: inserted_timmy.id,
score: 1,
};
PostLike::like(pool, &timmy_post_vote_form).await.unwrap();
PostLike::like(pool, &timmy_post_vote_form).await?;
// Sara downvotes timmy's post
let sara_post_vote_form = PostLikeForm {
@ -155,7 +154,7 @@ mod tests {
person_id: inserted_sara.id,
score: -1,
};
PostLike::like(pool, &sara_post_vote_form).await.unwrap();
PostLike::like(pool, &sara_post_vote_form).await?;
let expected_post_vote_views = [
VoteView {
@ -170,32 +169,24 @@ mod tests {
},
];
let read_post_vote_views = VoteView::list_for_post(pool, inserted_post.id, None, None)
.await
.unwrap();
let read_post_vote_views = VoteView::list_for_post(pool, inserted_post.id, None, None).await?;
assert_eq!(read_post_vote_views, expected_post_vote_views);
// Timothy votes down his own comment
let timmy_comment_vote_form = CommentLikeForm {
post_id: inserted_post.id,
comment_id: inserted_comment.id,
person_id: inserted_timmy.id,
score: -1,
};
CommentLike::like(pool, &timmy_comment_vote_form)
.await
.unwrap();
CommentLike::like(pool, &timmy_comment_vote_form).await?;
// Sara upvotes timmy's comment
let sara_comment_vote_form = CommentLikeForm {
post_id: inserted_post.id,
comment_id: inserted_comment.id,
person_id: inserted_sara.id,
score: 1,
};
CommentLike::like(pool, &sara_comment_vote_form)
.await
.unwrap();
CommentLike::like(pool, &sara_comment_vote_form).await?;
let expected_comment_vote_views = [
VoteView {
@ -210,9 +201,8 @@ mod tests {
},
];
let read_comment_vote_views = VoteView::list_for_comment(pool, inserted_comment.id, None, None)
.await
.unwrap();
let read_comment_vote_views =
VoteView::list_for_comment(pool, inserted_comment.id, None, None).await?;
assert_eq!(read_comment_vote_views, expected_comment_vote_views);
// Ban timmy from that community
@ -221,36 +211,26 @@ mod tests {
person_id: inserted_timmy.id,
expires: None,
};
CommunityPersonBan::ban(pool, &ban_timmy_form)
.await
.unwrap();
CommunityPersonBan::ban(pool, &ban_timmy_form).await?;
// Make sure creator_banned_from_community is true
let read_comment_vote_views_after_ban =
VoteView::list_for_comment(pool, inserted_comment.id, None, None)
.await
.unwrap();
VoteView::list_for_comment(pool, inserted_comment.id, None, None).await?;
assert!(
read_comment_vote_views_after_ban
assert!(read_comment_vote_views_after_ban
.first()
.unwrap()
.creator_banned_from_community
);
.is_some_and(|c| c.creator_banned_from_community));
let read_post_vote_views_after_ban =
VoteView::list_for_post(pool, inserted_post.id, None, None)
.await
.unwrap();
VoteView::list_for_post(pool, inserted_post.id, None, None).await?;
assert!(
read_post_vote_views_after_ban
assert!(read_post_vote_views_after_ban
.get(1)
.unwrap()
.creator_banned_from_community
);
.is_some_and(|p| p.creator_banned_from_community));
// Cleanup
Instance::delete(pool, inserted_instance.id).await.unwrap();
Instance::delete(pool, inserted_instance.id).await?;
Ok(())
}
}

View file

@ -10,7 +10,7 @@ use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
newtypes::{CommunityId, DbUrl, InstanceId, PersonId},
schema::{community, community_follower, person},
utils::{functions::coalesce, get_conn, DbPool},
utils::{get_conn, DbPool},
};
impl CommunityFollowerView {
@ -37,10 +37,7 @@ impl CommunityFollowerView {
// local-person+remote-community or remote-person+local-community
.filter(not(person::local))
.filter(community_follower::published.gt(published_since.naive_utc()))
.select((
community::id,
coalesce(person::shared_inbox_url, person::inbox_url),
))
.select((community::id, person::inbox_url))
.distinct() // only need each community_id, inbox combination once
.load::<(CommunityId, DbUrl)>(conn)
.await
@ -54,7 +51,7 @@ impl CommunityFollowerView {
.filter(community_follower::community_id.eq(community_id))
.filter(not(person::local))
.inner_join(person::table)
.select(coalesce(person::shared_inbox_url, person::inbox_url))
.select(person::inbox_url)
.distinct()
.load::<DbUrl>(conn)
.await?;

View file

@ -1,4 +1,4 @@
use crate::structs::{CommunityModeratorView, CommunityView, PersonView};
use crate::structs::{CommunityModeratorView, CommunitySortType, CommunityView, PersonView};
use diesel::{
pg::Pg,
result::Error,
@ -22,7 +22,16 @@ use lemmy_db_schema::{
instance_block,
},
source::{community::CommunityFollower, local_user::LocalUser, site::Site},
utils::{fuzzy_search, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
utils::{
functions::lower,
fuzzy_search,
limit_and_offset,
DbConn,
DbPool,
ListFn,
Queries,
ReadFn,
},
ListingType,
PostSortType,
};
@ -103,7 +112,7 @@ fn queries<'a>() -> Queries<
};
let list = move |mut conn: DbConn<'a>, (options, site): (CommunityQuery<'a>, &'a Site)| async move {
use PostSortType::*;
use CommunitySortType::*;
// The left join below will return None in this case
let person_id_join = options.local_user.person_id().unwrap_or(PersonId(-1));
@ -148,6 +157,8 @@ fn queries<'a>() -> Queries<
}
TopMonth => query = query.order_by(community_aggregates::users_active_month.desc()),
TopWeek => query = query.order_by(community_aggregates::users_active_week.desc()),
NameAsc => query = query.order_by(lower(community::name).asc()),
NameDesc => query = query.order_by(lower(community::name).desc()),
};
if let Some(listing_type) = options.listing_type {
@ -228,10 +239,36 @@ impl CommunityView {
}
}
impl From<PostSortType> for CommunitySortType {
fn from(value: PostSortType) -> Self {
match value {
PostSortType::Active => Self::Active,
PostSortType::Hot => Self::Hot,
PostSortType::New => Self::New,
PostSortType::Old => Self::Old,
PostSortType::TopDay => Self::TopDay,
PostSortType::TopWeek => Self::TopWeek,
PostSortType::TopMonth => Self::TopMonth,
PostSortType::TopYear => Self::TopYear,
PostSortType::TopAll => Self::TopAll,
PostSortType::MostComments => Self::MostComments,
PostSortType::NewComments => Self::NewComments,
PostSortType::TopHour => Self::TopHour,
PostSortType::TopSixHour => Self::TopSixHour,
PostSortType::TopTwelveHour => Self::TopTwelveHour,
PostSortType::TopThreeMonths => Self::TopThreeMonths,
PostSortType::TopSixMonths => Self::TopSixMonths,
PostSortType::TopNineMonths => Self::TopNineMonths,
PostSortType::Controversial => Self::Controversial,
PostSortType::Scaled => Self::Scaled,
}
}
}
#[derive(Default)]
pub struct CommunityQuery<'a> {
pub listing_type: Option<ListingType>,
pub sort: Option<PostSortType>,
pub sort: Option<CommunitySortType>,
pub local_user: Option<&'a LocalUser>,
pub search_term: Option<String>,
pub title_only: Option<bool>,
@ -248,10 +285,12 @@ impl<'a> CommunityQuery<'a> {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use crate::{community_view::CommunityQuery, structs::CommunityView};
use crate::{
community_view::CommunityQuery,
structs::{CommunitySortType, CommunityView},
};
use lemmy_db_schema::{
source::{
community::{Community, CommunityInsertForm, CommunityUpdateForm},
@ -264,41 +303,63 @@ mod tests {
utils::{build_db_pool_for_tests, DbPool},
CommunityVisibility,
};
use lemmy_utils::error::LemmyResult;
use serial_test::serial;
use url::Url;
struct Data {
inserted_instance: Instance,
local_user: LocalUser,
inserted_community: Community,
inserted_communities: [Community; 3],
site: Site,
}
async fn init_data(pool: &mut DbPool<'_>) -> Data {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
.await
.unwrap();
async fn init_data(pool: &mut DbPool<'_>) -> LemmyResult<Data> {
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
let person_name = "tegan".to_string();
let new_person = PersonInsertForm::test_form(inserted_instance.id, &person_name);
let inserted_person = Person::create(pool, &new_person).await.unwrap();
let inserted_person = Person::create(pool, &new_person).await?;
let local_user_form = LocalUserInsertForm::test_form(inserted_person.id);
let local_user = LocalUser::create(pool, &local_user_form, vec![])
.await
.unwrap();
let local_user = LocalUser::create(pool, &local_user_form, vec![]).await?;
let new_community = CommunityInsertForm::new(
let inserted_communities = [
Community::create(
pool,
&CommunityInsertForm::new(
inserted_instance.id,
"test_community_1".to_string(),
"nada1".to_owned(),
"pubkey".to_string(),
),
)
.await?,
Community::create(
pool,
&CommunityInsertForm::new(
inserted_instance.id,
"test_community_2".to_string(),
"nada2".to_owned(),
"pubkey".to_string(),
),
)
.await?,
Community::create(
pool,
&CommunityInsertForm::new(
inserted_instance.id,
"test_community_3".to_string(),
"nada".to_owned(),
"nada3".to_owned(),
"pubkey".to_string(),
);
let inserted_community = Community::create(pool, &new_community).await.unwrap();
),
)
.await?,
];
let url = Url::parse("http://example.com").unwrap();
let url = Url::parse("http://example.com")?;
let site = Site {
id: Default::default(),
name: String::new(),
@ -317,74 +378,102 @@ mod tests {
content_warning: None,
};
Data {
Ok(Data {
inserted_instance,
local_user,
inserted_community,
inserted_communities,
site,
}
})
}
async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
Community::delete(pool, data.inserted_community.id)
.await
.unwrap();
Person::delete(pool, data.local_user.person_id)
.await
.unwrap();
Instance::delete(pool, data.inserted_instance.id)
.await
.unwrap();
async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> {
for Community { id, .. } in data.inserted_communities {
Community::delete(pool, id).await?;
}
Person::delete(pool, data.local_user.person_id).await?;
Instance::delete(pool, data.inserted_instance.id).await?;
Ok(())
}
#[tokio::test]
#[serial]
async fn local_only_community() {
async fn local_only_community() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
let data = init_data(pool).await?;
Community::update(
pool,
data.inserted_community.id,
data.inserted_communities[0].id,
&CommunityUpdateForm {
visibility: Some(CommunityVisibility::LocalOnly),
..Default::default()
},
)
.await
.unwrap();
.await?;
let unauthenticated_query = CommunityQuery {
..Default::default()
}
.list(&data.site, pool)
.await
.unwrap();
assert_eq!(0, unauthenticated_query.len());
.await?;
assert_eq!(
data.inserted_communities.len() - 1,
unauthenticated_query.len()
);
let authenticated_query = CommunityQuery {
local_user: Some(&data.local_user),
..Default::default()
}
.list(&data.site, pool)
.await
.unwrap();
assert_eq!(1, authenticated_query.len());
.await?;
assert_eq!(data.inserted_communities.len(), authenticated_query.len());
let unauthenticated_community =
CommunityView::read(pool, data.inserted_community.id, None, false).await;
CommunityView::read(pool, data.inserted_communities[0].id, None, false).await;
assert!(unauthenticated_community.is_err());
let authenticated_community = CommunityView::read(
pool,
data.inserted_community.id,
data.inserted_communities[0].id,
Some(&data.local_user),
false,
)
.await;
assert!(authenticated_community.is_ok());
cleanup(data, pool).await;
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn community_sort_name() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await?;
let query = CommunityQuery {
sort: Some(CommunitySortType::NameAsc),
..Default::default()
};
let communities = query.list(&data.site, pool).await?;
for (i, c) in communities.iter().enumerate().skip(1) {
let prev = communities.get(i - 1).expect("No previous community?");
assert!(c.community.title.cmp(&prev.community.title).is_ge());
}
let query = CommunityQuery {
sort: Some(CommunitySortType::NameDesc),
..Default::default()
};
let communities = query.list(&data.site, pool).await?;
for (i, c) in communities.iter().enumerate().skip(1) {
let prev = communities.get(i - 1).expect("No previous community?");
assert!(c.community.title.cmp(&prev.community.title).is_le());
}
cleanup(data, pool).await
}
}

View file

@ -59,6 +59,35 @@ pub struct CommunityView {
pub banned_from_community: bool,
}
/// The community sort types. See here for descriptions: https://join-lemmy.org/docs/en/users/03-votes-and-ranking.html
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub enum CommunitySortType {
#[default]
Active,
Hot,
New,
Old,
TopDay,
TopWeek,
TopMonth,
TopYear,
TopAll,
MostComments,
NewComments,
TopHour,
TopSixHour,
TopTwelveHour,
TopThreeMonths,
TopSixMonths,
TopNineMonths,
Controversial,
Scaled,
NameAsc,
NameDesc,
}
#[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]

View file

@ -222,7 +222,6 @@ impl<T: DataSource> CommunityInboxCollector<T> {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
#[expect(clippy::indexing_slicing)]
mod tests {
use super::*;
@ -230,6 +229,7 @@ mod tests {
newtypes::{ActivityId, CommunityId, InstanceId, SiteId},
source::activity::{ActorType, SentActivity},
};
use lemmy_utils::error::LemmyResult;
use mockall::{mock, predicate::*};
use serde_json::json;
mock! {
@ -253,13 +253,11 @@ mod tests {
}
#[tokio::test]
async fn test_get_inbox_urls_empty() {
async fn test_get_inbox_urls_empty() -> LemmyResult<()> {
let mut collector = setup_collector();
let activity = SentActivity {
id: ActivityId(1),
ap_id: Url::parse("https://example.com/activities/1")
.unwrap()
.into(),
ap_id: Url::parse("https://example.com/activities/1")?.into(),
data: json!({}),
sensitive: false,
published: Utc::now(),
@ -270,14 +268,16 @@ mod tests {
actor_apub_id: None,
};
let result = collector.get_inbox_urls(&activity).await.unwrap();
let result = collector.get_inbox_urls(&activity).await?;
assert!(result.is_empty());
Ok(())
}
#[tokio::test]
async fn test_get_inbox_urls_send_all_instances() {
async fn test_get_inbox_urls_send_all_instances() -> LemmyResult<()> {
let mut collector = setup_collector();
let site_inbox = Url::parse("https://example.com/inbox").unwrap();
let site_inbox = Url::parse("https://example.com/inbox")?;
let site = Site {
id: SiteId(1),
name: "Test Site".to_string(),
@ -287,7 +287,7 @@ mod tests {
icon: None,
banner: None,
description: None,
actor_id: Url::parse("https://example.com/site").unwrap().into(),
actor_id: Url::parse("https://example.com/site")?.into(),
last_refreshed_at: Utc::now(),
inbox_url: site_inbox.clone().into(),
private_key: None,
@ -303,9 +303,7 @@ mod tests {
let activity = SentActivity {
id: ActivityId(1),
ap_id: Url::parse("https://example.com/activities/1")
.unwrap()
.into(),
ap_id: Url::parse("https://example.com/activities/1")?.into(),
data: json!({}),
sensitive: false,
published: Utc::now(),
@ -316,13 +314,15 @@ mod tests {
actor_apub_id: None,
};
let result = collector.get_inbox_urls(&activity).await.unwrap();
let result = collector.get_inbox_urls(&activity).await?;
assert_eq!(result.len(), 1);
assert_eq!(result[0], site_inbox);
Ok(())
}
#[tokio::test]
async fn test_get_inbox_urls_community_followers() {
async fn test_get_inbox_urls_community_followers() -> LemmyResult<()> {
let mut collector = setup_collector();
let community_id = CommunityId(1);
let url1 = "https://follower1.example.com/inbox";
@ -333,18 +333,22 @@ mod tests {
.expect_get_instance_followed_community_inboxes()
.return_once(move |_, _| {
Ok(vec![
(community_id, Url::parse(url1).unwrap().into()),
(community_id, Url::parse(url2).unwrap().into()),
(
community_id,
Url::parse(url1).map_err(|_| diesel::NotFound)?.into(),
),
(
community_id,
Url::parse(url2).map_err(|_| diesel::NotFound)?.into(),
),
])
});
collector.update_communities().await.unwrap();
collector.update_communities().await?;
let activity = SentActivity {
id: ActivityId(1),
ap_id: Url::parse("https://example.com/activities/1")
.unwrap()
.into(),
ap_id: Url::parse("https://example.com/activities/1")?.into(),
data: json!({}),
sensitive: false,
published: Utc::now(),
@ -355,24 +359,24 @@ mod tests {
actor_apub_id: None,
};
let result = collector.get_inbox_urls(&activity).await.unwrap();
let result = collector.get_inbox_urls(&activity).await?;
assert_eq!(result.len(), 2);
assert!(result.contains(&Url::parse(url1).unwrap()));
assert!(result.contains(&Url::parse(url2).unwrap()));
assert!(result.contains(&Url::parse(url1)?));
assert!(result.contains(&Url::parse(url2)?));
Ok(())
}
#[tokio::test]
async fn test_get_inbox_urls_send_inboxes() {
async fn test_get_inbox_urls_send_inboxes() -> LemmyResult<()> {
let mut collector = setup_collector();
collector.domain = "example.com".to_string();
let inbox_user_1 = Url::parse("https://example.com/user1/inbox").unwrap();
let inbox_user_2 = Url::parse("https://example.com/user2/inbox").unwrap();
let other_domain_inbox = Url::parse("https://other-domain.com/user3/inbox").unwrap();
let inbox_user_1 = Url::parse("https://example.com/user1/inbox")?;
let inbox_user_2 = Url::parse("https://example.com/user2/inbox")?;
let other_domain_inbox = Url::parse("https://other-domain.com/user3/inbox")?;
let activity = SentActivity {
id: ActivityId(1),
ap_id: Url::parse("https://example.com/activities/1")
.unwrap()
.into(),
ap_id: Url::parse("https://example.com/activities/1")?.into(),
data: json!({}),
sensitive: false,
published: Utc::now(),
@ -387,20 +391,22 @@ mod tests {
actor_apub_id: None,
};
let result = collector.get_inbox_urls(&activity).await.unwrap();
let result = collector.get_inbox_urls(&activity).await?;
assert_eq!(result.len(), 2);
assert!(result.contains(&inbox_user_1));
assert!(result.contains(&inbox_user_2));
assert!(!result.contains(&other_domain_inbox));
Ok(())
}
#[tokio::test]
async fn test_get_inbox_urls_combined() {
async fn test_get_inbox_urls_combined() -> LemmyResult<()> {
let mut collector = setup_collector();
collector.domain = "example.com".to_string();
let community_id = CommunityId(1);
let site_inbox = Url::parse("https://example.com/site_inbox").unwrap();
let site_inbox = Url::parse("https://example.com/site_inbox")?;
let site = Site {
id: SiteId(1),
name: "Test Site".to_string(),
@ -410,7 +416,7 @@ mod tests {
icon: None,
banner: None,
description: None,
actor_id: Url::parse("https://example.com/site").unwrap().into(),
actor_id: Url::parse("https://example.com/site")?.into(),
last_refreshed_at: Utc::now(),
inbox_url: site_inbox.clone().into(),
private_key: None,
@ -431,18 +437,18 @@ mod tests {
.return_once(move |_, _| {
Ok(vec![(
community_id,
Url::parse(subdomain_inbox).unwrap().into(),
Url::parse(subdomain_inbox)
.map_err(|_| diesel::NotFound)?
.into(),
)])
});
collector.update_communities().await.unwrap();
let user1_inbox = Url::parse("https://example.com/user1/inbox").unwrap();
let user2_inbox = Url::parse("https://other-domain.com/user2/inbox").unwrap();
collector.update_communities().await?;
let user1_inbox = Url::parse("https://example.com/user1/inbox")?;
let user2_inbox = Url::parse("https://other-domain.com/user2/inbox")?;
let activity = SentActivity {
id: ActivityId(1),
ap_id: Url::parse("https://example.com/activities/1")
.unwrap()
.into(),
ap_id: Url::parse("https://example.com/activities/1")?.into(),
data: json!({}),
sensitive: false,
published: Utc::now(),
@ -456,27 +462,29 @@ mod tests {
actor_apub_id: None,
};
let result = collector.get_inbox_urls(&activity).await.unwrap();
let result = collector.get_inbox_urls(&activity).await?;
assert_eq!(result.len(), 3);
assert!(result.contains(&site_inbox));
assert!(result.contains(&Url::parse(subdomain_inbox).unwrap()));
assert!(result.contains(&Url::parse(subdomain_inbox)?));
assert!(result.contains(&user1_inbox));
assert!(!result.contains(&user2_inbox));
Ok(())
}
#[tokio::test]
async fn test_update_communities() {
async fn test_update_communities() -> LemmyResult<()> {
let mut collector = setup_collector();
let community_id1 = CommunityId(1);
let community_id2 = CommunityId(2);
let community_id3 = CommunityId(3);
let user1_inbox_str = "https://follower1.example.com/inbox";
let user1_inbox = Url::parse(user1_inbox_str).unwrap();
let user1_inbox = Url::parse(user1_inbox_str)?;
let user2_inbox_str = "https://follower2.example.com/inbox";
let user2_inbox = Url::parse(user2_inbox_str).unwrap();
let user2_inbox = Url::parse(user2_inbox_str)?;
let user3_inbox_str = "https://follower3.example.com/inbox";
let user3_inbox = Url::parse(user3_inbox_str).unwrap();
let user3_inbox = Url::parse(user3_inbox_str)?;
collector
.data_source
@ -485,42 +493,57 @@ mod tests {
.returning(move |_, last_fetch| {
if last_fetch == Utc.timestamp_nanos(0) {
Ok(vec![
(community_id1, Url::parse(user1_inbox_str).unwrap().into()),
(community_id2, Url::parse(user2_inbox_str).unwrap().into()),
(
community_id1,
Url::parse(user1_inbox_str)
.map_err(|_| diesel::NotFound)?
.into(),
),
(
community_id2,
Url::parse(user2_inbox_str)
.map_err(|_| diesel::NotFound)?
.into(),
),
])
} else {
Ok(vec![(
community_id3,
Url::parse(user3_inbox_str).unwrap().into(),
Url::parse(user3_inbox_str)
.map_err(|_| diesel::NotFound)?
.into(),
)])
}
});
// First update
collector.update_communities().await.unwrap();
collector.update_communities().await?;
assert_eq!(collector.followed_communities.len(), 2);
assert!(collector.followed_communities[&community_id1].contains(&user1_inbox));
assert!(collector.followed_communities[&community_id2].contains(&user2_inbox));
// Simulate time passing
collector.last_full_communities_fetch = Utc::now() - chrono::TimeDelta::try_minutes(3).unwrap();
collector.last_full_communities_fetch =
Utc::now() - chrono::TimeDelta::try_minutes(3).expect("TimeDelta out of bounds");
collector.last_incremental_communities_fetch =
Utc::now() - chrono::TimeDelta::try_minutes(3).unwrap();
Utc::now() - chrono::TimeDelta::try_minutes(3).expect("TimeDelta out of bounds");
// Second update (incremental)
collector.update_communities().await.unwrap();
collector.update_communities().await?;
assert_eq!(collector.followed_communities.len(), 3);
assert!(collector.followed_communities[&community_id1].contains(&user1_inbox));
assert!(collector.followed_communities[&community_id3].contains(&user3_inbox));
assert!(collector.followed_communities[&community_id2].contains(&user2_inbox));
Ok(())
}
#[tokio::test]
async fn test_get_inbox_urls_no_duplicates() {
async fn test_get_inbox_urls_no_duplicates() -> LemmyResult<()> {
let mut collector = setup_collector();
collector.domain = "example.com".to_string();
let community_id = CommunityId(1);
let site_inbox = Url::parse("https://example.com/site_inbox").unwrap();
let site_inbox = Url::parse("https://example.com/site_inbox")?;
let site_inbox_clone = site_inbox.clone();
let site = Site {
id: SiteId(1),
@ -531,7 +554,7 @@ mod tests {
icon: None,
banner: None,
description: None,
actor_id: Url::parse("https://example.com/site").unwrap().into(),
actor_id: Url::parse("https://example.com/site")?.into(),
last_refreshed_at: Utc::now(),
inbox_url: site_inbox.clone().into(),
private_key: None,
@ -550,13 +573,11 @@ mod tests {
.expect_get_instance_followed_community_inboxes()
.return_once(move |_, _| Ok(vec![(community_id, site_inbox_clone.into())]));
collector.update_communities().await.unwrap();
collector.update_communities().await?;
let activity = SentActivity {
id: ActivityId(1),
ap_id: Url::parse("https://example.com/activities/1")
.unwrap()
.into(),
ap_id: Url::parse("https://example.com/activities/1")?.into(),
data: json!({}),
sensitive: false,
published: Utc::now(),
@ -567,8 +588,10 @@ mod tests {
actor_apub_id: None,
};
let result = collector.get_inbox_urls(&activity).await.unwrap();
let result = collector.get_inbox_urls(&activity).await?;
assert_eq!(result.len(), 1);
assert!(result.contains(&Url::parse("https://example.com/site_inbox").unwrap()));
assert!(result.contains(&Url::parse("https://example.com/site_inbox")?));
Ok(())
}
}

View file

@ -449,7 +449,7 @@ mod test {
protocol::context::WithContext,
};
use actix_web::{dev::ServerHandle, web, App, HttpResponse, HttpServer};
use lemmy_api_common::utils::{generate_inbox_url, generate_shared_inbox_url};
use lemmy_api_common::utils::generate_inbox_url;
use lemmy_db_schema::{
newtypes::DbUrl,
source::{
@ -491,8 +491,7 @@ mod test {
let person_form = PersonInsertForm {
actor_id: Some(actor_id.clone()),
private_key: (Some(actor_keypair.private_key)),
inbox_url: Some(generate_inbox_url(&actor_id)?),
shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?),
inbox_url: Some(generate_inbox_url()?),
..PersonInsertForm::new("alice".to_string(), actor_keypair.public_key, instance.id)
};
let person = Person::create(&mut context.pool(), &person_form).await?;

View file

@ -82,6 +82,10 @@ ts-rs = { workspace = true, optional = true }
enum-map = { workspace = true, optional = true }
cfg-if = "1"
clearurls = { version = "0.0.4", features = ["linkify"] }
markdown-it-block-spoiler = "1.0.0"
markdown-it-sub = "1.0.0"
markdown-it-sup = "1.0.0"
markdown-it-ruby = "1.0.0"
[dev-dependencies]
reqwest = { workspace = true }

View file

@ -46,7 +46,7 @@ pub enum LemmyErrorType {
PersonIsBlocked,
CommunityIsBlocked,
InstanceIsBlocked,
DownvotesAreDisabled,
VoteNotAllowed,
InstanceIsPrivate,
/// Password must be between 10 and 60 characters
InvalidPassword,
@ -288,7 +288,6 @@ cfg_if! {
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use actix_web::{body::MessageBody, ResponseError};
@ -297,21 +296,25 @@ cfg_if! {
use strum::IntoEnumIterator;
#[test]
fn deserializes_no_message() {
fn deserializes_no_message() -> LemmyResult<()> {
let err = LemmyError::from(LemmyErrorType::Banned).error_response();
let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
assert_eq!(&json, "{\"error\":\"banned\"}")
let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?;
assert_eq!(&json, "{\"error\":\"banned\"}");
Ok(())
}
#[test]
fn deserializes_with_message() {
fn deserializes_with_message() -> LemmyResult<()> {
let reg_banned = LemmyErrorType::PersonIsBannedFromSite(String::from("reason"));
let err = LemmyError::from(reg_banned).error_response();
let json = String::from_utf8(err.into_body().try_into_bytes().unwrap().to_vec()).unwrap();
let json = String::from_utf8(err.into_body().try_into_bytes().unwrap_or_default().to_vec())?;
assert_eq!(
&json,
"{\"error\":\"person_is_banned_from_site\",\"message\":\"reason\"}"
)
);
Ok(())
}
#[test]
@ -328,19 +331,22 @@ cfg_if! {
/// Check if errors match translations. Disabled because many are not translated at all.
#[test]
#[ignore]
fn test_translations_match() {
fn test_translations_match() -> LemmyResult<()> {
#[derive(Deserialize)]
struct Err {
error: String,
}
let translations = read_to_string("translations/translations/en.json").unwrap();
LemmyErrorType::iter().for_each(|e| {
let msg = serde_json::to_string(&e).unwrap();
let msg: Err = serde_json::from_str(&msg).unwrap();
let translations = read_to_string("translations/translations/en.json")?;
for e in LemmyErrorType::iter() {
let msg = serde_json::to_string(&e)?;
let msg: Err = serde_json::from_str(&msg)?;
let msg = msg.error;
assert!(translations.contains(&format!("\"{msg}\"")), "{msg}");
});
}
Ok(())
}
}
}

View file

@ -308,10 +308,10 @@ fn split_ipv6(ip: Ipv6Addr) -> ([u8; 6], u8, u8) {
}
#[cfg(test)]
#[expect(clippy::unwrap_used)]
mod tests {
use super::{ActionType, BucketConfig, InstantSecs, RateLimitState, RateLimitedGroup};
use crate::error::LemmyResult;
use pretty_assertions::assert_eq;
#[test]
@ -326,7 +326,7 @@ mod tests {
}
#[test]
fn test_rate_limiter() {
fn test_rate_limiter() -> LemmyResult<()> {
let bucket_configs = enum_map::enum_map! {
ActionType::Message => BucketConfig {
capacity: 2,
@ -350,7 +350,7 @@ mod tests {
"1:2:3:0405:6::",
];
for ip in ips {
let ip = ip.parse().unwrap();
let ip = ip.parse()?;
let message_passed = rate_limiter.check(ActionType::Message, ip, now);
let post_passed = rate_limiter.check(ActionType::Post, ip, now);
assert!(message_passed);
@ -407,7 +407,7 @@ mod tests {
// Do 2 `Message` actions for 1 IP address and expect only the 2nd one to fail
for expected_to_pass in [true, false] {
let ip = "1:2:3:0400::".parse().unwrap();
let ip = "1:2:3:0400::".parse()?;
let passed = rate_limiter.check(ActionType::Message, ip, now);
assert_eq!(passed, expected_to_pass);
}
@ -419,7 +419,7 @@ mod tests {
assert!(rate_limiter.ipv6_buckets.is_empty());
// `remove full buckets` should not remove empty buckets
let ip = "1.1.1.1".parse().unwrap();
let ip = "1.1.1.1".parse()?;
// empty the bucket with 2 requests
assert!(rate_limiter.check(ActionType::Post, ip, now));
assert!(rate_limiter.check(ActionType::Post, ip, now));
@ -429,11 +429,13 @@ mod tests {
// `remove full buckets` should not remove partial buckets
now.secs += 2;
let ip = "1.1.1.1".parse().unwrap();
let ip = "1.1.1.1".parse()?;
// Only make one request, so bucket still has 1 token
assert!(rate_limiter.check(ActionType::Post, ip, now));
rate_limiter.remove_full_buckets(now);
assert!(!rate_limiter.ipv4_buckets.is_empty());
Ok(())
}
}

View file

@ -90,6 +90,10 @@ pub struct PictrsConfig {
/// Timeout for uploading images to pictrs (in seconds)
#[default(30)]
pub upload_timeout: u64,
/// Resize post thumbnails to this maximum width/height.
#[default(256)]
pub max_thumbnail_size: u32,
}
#[derive(Debug, Deserialize, Serialize, Clone, SmartDefault, Document, PartialEq)]

View file

@ -0,0 +1,168 @@
use super::{link_rule::Link, MARKDOWN_PARSER};
use crate::settings::SETTINGS;
use markdown_it::{plugins::cmark::inline::image::Image, NodeValue};
use url::Url;
use urlencoding::encode;
/// Rewrites all links to remote domains in markdown, so they go through `/api/v3/image_proxy`.
pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec<Url>) {
let links_offsets = find_urls::<Image>(&src);
let mut links = vec![];
// Go through the collected links in reverse order
for (start, end) in links_offsets.into_iter().rev() {
let (url, extra) = markdown_handle_title(&src, start, end);
match Url::parse(url) {
Ok(parsed) => {
links.push(parsed.clone());
// If link points to remote domain, replace with proxied link
if parsed.domain() != Some(&SETTINGS.hostname) {
let mut proxied = format!(
"{}/api/v3/image_proxy?url={}",
SETTINGS.get_protocol_and_hostname(),
encode(url),
);
// restore custom emoji format
if let Some(extra) = extra {
proxied = format!("{proxied} {extra}");
}
src.replace_range(start..end, &proxied);
}
}
Err(_) => {
// If its not a valid url, replace with empty text
src.replace_range(start..end, "");
}
}
}
(src, links)
}
pub fn markdown_handle_title(src: &str, start: usize, end: usize) -> (&str, Option<&str>) {
let content = src.get(start..end).unwrap_or_default();
// necessary for custom emojis which look like `![name](url "title")`
let (url, extra) = if content.contains(' ') {
let split = content.split_once(' ').expect("split is valid");
(split.0, Some(split.1))
} else {
(content, None)
};
(url, extra)
}
pub fn markdown_find_links(src: &str) -> Vec<(usize, usize)> {
find_urls::<Link>(src)
}
// Walk the syntax tree to find positions of image or link urls
fn find_urls<T: NodeValue + UrlAndTitle>(src: &str) -> Vec<(usize, usize)> {
let ast = MARKDOWN_PARSER.parse(src);
let mut links_offsets = vec![];
ast.walk(|node, _depth| {
if let Some(image) = node.cast::<T>() {
let node_offsets = node.srcmap.expect("srcmap is none").get_byte_offsets();
let start_offset = node_offsets.1 - image.url_len() - 1 - image.title_len();
let end_offset = node_offsets.1 - 1;
links_offsets.push((start_offset, end_offset));
}
});
links_offsets
}
pub trait UrlAndTitle {
fn url_len(&self) -> usize;
fn title_len(&self) -> usize;
}
impl UrlAndTitle for Image {
fn url_len(&self) -> usize {
self.url.len()
}
fn title_len(&self) -> usize {
self.title.as_ref().map(|t| t.len() + 3).unwrap_or_default()
}
}
impl UrlAndTitle for Link {
fn url_len(&self) -> usize {
self.url.len()
}
fn title_len(&self) -> usize {
self.title.as_ref().map(|t| t.len() + 3).unwrap_or_default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_find_links() {
let links = markdown_find_links("[test](https://example.com)");
assert_eq!(vec![(7, 26)], links);
let links = find_urls::<Image>("![test](https://example.com)");
assert_eq!(vec![(8, 27)], links);
}
#[test]
fn test_markdown_proxy_images() {
let tests: Vec<_> =
vec![
(
"remote image proxied",
"![link](http://example.com/image.jpg)",
"![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)",
),
(
"local image unproxied",
"![link](http://lemmy-alpha/image.jpg)",
"![link](http://lemmy-alpha/image.jpg)",
),
(
"multiple image links",
"![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)",
"![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)",
),
(
"empty link handled",
"![image]()",
"![image]()"
),
(
"empty label handled",
"![](http://example.com/image.jpg)",
"![](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)"
),
(
"invalid image link removed",
"![image](http-not-a-link)",
"![image]()"
),
(
"label with nested markdown handled",
"![a *b* c](http://example.com/image.jpg)",
"![a *b* c](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)"
),
(
"custom emoji support",
r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#,
r#"![party-blob](https://lemmy-alpha/api/v3/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#
)
];
tests.iter().for_each(|&(msg, input, expected)| {
let result = markdown_rewrite_image_links(input.to_string());
assert_eq!(
result.0, expected,
"Testing {}, with original input '{}'",
msg, input
);
});
}
}

Some files were not shown because too many files have changed in this diff Show more