Merge remote-tracking branch 'origin/main' into post-tags

This commit is contained in:
phiresky 2024-11-08 13:54:53 +01:00
commit 3bebdf6610
321 changed files with 8429 additions and 8258 deletions

View file

@ -2,7 +2,7 @@
# See https://github.com/woodpecker-ci/woodpecker/issues/1677
variables:
- &rust_image "rust:1.80"
- &rust_image "rust:1.81"
- &rust_nightly_image "rustlang/rust:nightly"
- &install_pnpm "corepack enable pnpm"
- &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin"
@ -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:
@ -73,12 +73,12 @@ steps:
when:
- event: pull_request
cargo_machete:
cargo_shear:
image: *rust_nightly_image
commands:
- *install_binstall
- cargo binstall -y cargo-machete
- cargo machete
- cargo binstall -y cargo-shear
- cargo shear
when:
- event: pull_request
@ -157,7 +157,7 @@ steps:
CARGO_HOME: .cargo_home
commands:
- rustup component add clippy
- cargo clippy --workspace --tests --all-targets --features console -- -D warnings
- cargo clippy --workspace --tests --all-targets -- -D warnings
when: *slow_check_paths
cargo_build:
@ -240,10 +240,13 @@ steps:
publish_release_docker:
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
settings:
repo: dessalines/lemmy
dockerfile: docker/Dockerfile
username:
from_secret: docker_username
password:
from_secret: docker_password
platforms: linux/amd64, linux/arm64
build_args:
- RUST_RELEASE_MODE=release
@ -253,10 +256,13 @@ steps:
nightly_build:
image: woodpeckerci/plugin-docker-buildx
secrets: [docker_username, docker_password]
settings:
repo: dessalines/lemmy
dockerfile: docker/Dockerfile
username:
from_secret: docker_username
password:
from_secret: docker_password
platforms: linux/amd64,linux/arm64
build_args:
- RUST_RELEASE_MODE=release

2778
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -26,9 +26,10 @@ workspace = true
[profile.release]
debug = 0
lto = "thin"
strip = true # Automatically strip symbols from the binary.
opt-level = "z" # Optimize for size.
lto = "fat"
strip = true # Automatically strip symbols from the binary.
opt-level = 3 # Optimize for speed, not size.
codegen-units = 1 # Reduce parallel code generation.
# This profile significantly speeds up build time. If debug info is needed you can comment the line
# out temporarily, but make sure to leave this in the main branch.
@ -36,16 +37,6 @@ opt-level = "z" # Optimize for size.
debug = 0
[features]
embed-pictrs = ["pict-rs"]
# This feature requires building with `tokio_unstable` flag, see documentation:
# https://docs.rs/tokio/latest/tokio/#unstable-features
console = [
"console-subscriber",
"opentelemetry",
"opentelemetry-otlp",
"tracing-opentelemetry",
"reqwest-tracing/opentelemetry_0_16",
]
json-log = ["tracing-subscriber/json"]
default = []
@ -100,7 +91,7 @@ lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" }
lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" }
activitypub_federation = { version = "0.5.8", default-features = false, features = [
activitypub_federation = { version = "0.6.0-alpha2", default-features = false, features = [
"actix-web",
] }
diesel = "2.1.6"
@ -108,7 +99,7 @@ diesel_migrations = "2.1.0"
diesel-async = "0.4.1"
serde = { version = "1.0.204", features = ["derive"] }
serde_with = "3.9.0"
actix-web = { version = "4.8.0", default-features = false, features = [
actix-web = { version = "4.9.0", default-features = false, features = [
"macros",
"rustls-0_23",
"compress-brotli",
@ -117,19 +108,17 @@ actix-web = { version = "4.8.0", default-features = false, features = [
"cookies",
] }
tracing = "0.1.40"
tracing-actix-web = { version = "0.7.11", default-features = false }
tracing-error = "0.2.0"
tracing-log = "0.2.0"
tracing-actix-web = { version = "0.7.10", default-features = false }
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = { version = "2.5.2", features = ["serde"] }
reqwest = { version = "0.11.27", default-features = false, features = [
reqwest = { version = "0.12.7", default-features = false, features = [
"json",
"blocking",
"gzip",
"rustls-tls",
] }
reqwest-middleware = "0.2.5"
reqwest-tracing = "0.4.8"
reqwest-middleware = "0.3.3"
reqwest-tracing = "0.5.3"
clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.15.1"
@ -143,7 +132,6 @@ anyhow = { version = "1.0.86", features = [
"backtrace",
] } # backtrace is on by default on nightly, but not stable rust
diesel_ltree = "0.3.1"
typed-builder = "0.19.1"
serial_test = "3.1.1"
tokio = { version = "1.39.2", features = ["full"] }
regex = "1.10.5"
@ -152,10 +140,8 @@ diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = { version = "0.26.3", features = ["derive"] }
itertools = "0.13.0"
futures = "0.3.30"
http = "0.2.12"
http = "1.1"
rosetta-i18n = "0.1.3"
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
tracing-opentelemetry = { version = "0.19.0" }
ts-rs = { version = "7.1.1", features = [
"serde-compat",
"chrono-impl",
@ -188,8 +174,6 @@ diesel-async = { workspace = true }
actix-web = { workspace = true }
tracing = { workspace = true }
tracing-actix-web = { workspace = true }
tracing-error = { workspace = true }
tracing-log = { workspace = true }
tracing-subscriber = { workspace = true }
url = { workspace = true }
reqwest = { workspace = true }
@ -197,11 +181,6 @@ reqwest-middleware = { workspace = true }
reqwest-tracing = { workspace = true }
clokwerk = { workspace = true }
serde_json = { workspace = true }
tracing-opentelemetry = { workspace = true, optional = true }
opentelemetry = { workspace = true, optional = true }
console-subscriber = { version = "0.4.0", optional = true }
opentelemetry-otlp = { version = "0.12.0", optional = true }
pict-rs = { version = "0.5.16", optional = true }
rustls = { workspace = true }
tokio.workspace = true
actix-cors = "0.7.0"
@ -210,7 +189,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",
@ -21,16 +21,16 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^22.0.2",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.8.0",
"@types/node": "^22.3.0",
"@typescript-eslint/eslint-plugin": "^8.1.0",
"@typescript-eslint/parser": "^8.1.0",
"eslint": "^9.9.0",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0",
"lemmy-js-client": "0.19.5-alpha.1",
"lemmy-js-client": "0.20.0-alpha.11",
"prettier": "^3.2.5",
"ts-jest": "^29.1.0",
"typescript": "^5.5.4",
"typescript-eslint": "^8.0.0"
"typescript-eslint": "^8.1.0"
}
}

File diff suppressed because it is too large Load diff

View file

@ -92,7 +92,7 @@ test("Create a comment", async () => {
test("Create a comment in a non-existent post", async () => {
await expect(createComment(alpha, -1)).rejects.toStrictEqual(
Error("couldnt_find_post"),
Error("not_found"),
);
});
@ -143,7 +143,7 @@ test("Delete a comment", async () => {
await waitUntil(
() =>
resolveComment(gamma, commentRes.comment_view.comment).catch(e => e),
r => r.message !== "couldnt_find_object",
r => r.message !== "not_found",
)
).comment;
if (!gammaComment) {
@ -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 == "couldnt_find_object",
() => 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 === "couldnt_find_object",
() => 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 !== "couldnt_find_object",
() => resolveComment(beta, commentRes.comment_view.comment),
c => c.comment?.comment.deleted === false,
)
).comment;
expect(betaComment2?.comment.deleted).toBe(false);
assertCommentFederation(betaComment2, undeleteCommentRes.comment_view);
});
@ -858,3 +857,26 @@ test("Dont send a comment reply to a blocked community", async () => {
blockRes = await blockCommunity(beta, newCommunityId, false);
expect(blockRes.blocked).toBe(false);
});
/// Fetching a deeply nested comment can lead to stack overflow as all parent comments are also
/// fetched recursively. Ensure that it works properly.
test("Fetch a deeply nested comment", async () => {
let lastComment;
for (let i = 0; i < 50; i++) {
let commentRes = await createComment(
alpha,
postOnAlphaRes.post_view.post.id,
lastComment?.comment_view.comment.id,
);
expect(commentRes.comment_view.comment).toBeDefined();
lastComment = commentRes;
}
let betaComment = await resolveComment(
beta,
lastComment!.comment_view.comment,
);
expect(betaComment!.comment!.comment).toBeDefined();
expect(betaComment?.comment?.post).toBeDefined();
});

View file

@ -527,12 +527,12 @@ test("Content in local-only community doesn't federate", async () => {
// cant resolve the community from another instance
await expect(
resolveCommunity(beta, communityRes.actor_id),
).rejects.toStrictEqual(Error("couldnt_find_object"));
).rejects.toStrictEqual(Error("not_found"));
// create a post, also cant resolve it
let postRes = await createPost(alpha, communityRes.id);
await expect(resolvePost(beta, postRes.post_view.post)).rejects.toStrictEqual(
Error("couldnt_find_object"),
Error("not_found"),
);
});

View file

@ -125,12 +125,12 @@ test("Create a post", async () => {
// Delta only follows beta, so it should not see an alpha ap_id
await expect(
resolvePost(delta, postRes.post_view.post),
).rejects.toStrictEqual(Error("couldnt_find_object"));
).rejects.toStrictEqual(Error("not_found"));
// Epsilon has alpha blocked, it should not see the alpha post
await expect(
resolvePost(epsilon, postRes.post_view.post),
).rejects.toStrictEqual(Error("couldnt_find_object"));
).rejects.toStrictEqual(Error("not_found"));
// remove added allow/blocklists
editSiteForm.allowed_instances = [];
@ -140,9 +140,7 @@ test("Create a post", async () => {
});
test("Create a post in a non-existent community", async () => {
await expect(createPost(alpha, -2)).rejects.toStrictEqual(
Error("couldnt_find_community"),
);
await expect(createPost(alpha, -2)).rejects.toStrictEqual(Error("not_found"));
});
test("Unlike a post", async () => {
@ -502,10 +500,17 @@ test("Enforce site ban federation for local user", async () => {
alpha,
alphaPerson.person.id,
false,
false,
true,
);
expect(unBanAlpha.banned).toBe(false);
// existing alpha post should be restored on beta
betaBanRes = await waitUntil(
() => getPost(beta, searchBeta1.post.id),
s => !s.post_view.post.removed,
);
expect(betaBanRes.post_view.post.removed).toBe(false);
// Login gets invalidated by ban, need to login again
if (!alphaUserPerson) {
throw "Missing alpha person";
@ -623,7 +628,7 @@ test("Enforce community ban for federated user", async () => {
// Alpha tries to make post on beta, but it fails because of ban
await expect(
createPost(alpha, betaCommunity.community.id),
).rejects.toStrictEqual(Error("banned_from_community"));
).rejects.toStrictEqual(Error("person_is_banned_from_community"));
// Unban alpha
let unBanAlpha = await banPersonFromCommunity(
@ -789,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

@ -419,13 +419,13 @@ export async function banPersonFromSite(
api: LemmyHttp,
person_id: number,
ban: boolean,
remove_data: boolean,
remove_or_restore_data: boolean,
): Promise<BanPersonResponse> {
// Make sure lemmy-beta/c/main is cached on lemmy_alpha
let form: BanPerson = {
person_id,
ban,
remove_data,
remove_or_restore_data,
};
return api.banPerson(form);
}
@ -434,13 +434,13 @@ export async function banPersonFromCommunity(
api: LemmyHttp,
person_id: number,
community_id: number,
remove_data: boolean,
remove_or_restore_data: boolean,
ban: boolean,
): Promise<BanFromCommunityResponse> {
let form: BanFromCommunity = {
person_id,
community_id,
remove_data: remove_data,
remove_or_restore_data,
ban,
};
return api.banFromCommunity(form);
@ -690,7 +690,7 @@ export async function saveUserSettingsBio(
blur_nsfw: false,
auto_expand: true,
theme: "darkly",
default_sort_type: "Active",
default_post_sort_type: "Active",
default_listing_type: "All",
interface_language: "en",
show_avatars: true,
@ -710,7 +710,7 @@ export async function saveUserSettingsFederated(
show_nsfw: false,
blur_nsfw: true,
auto_expand: false,
default_sort_type: "Hot",
default_post_sort_type: "Hot",
default_listing_type: "All",
interface_language: "",
avatar,

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

@ -22,8 +22,7 @@ pub async fn distinguish_comment(
data.comment_id,
Some(&local_user_view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
check_community_user_action(
&local_user_view.person,
@ -60,8 +59,7 @@ pub async fn distinguish_comment(
data.comment_id,
Some(&local_user_view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
Ok(Json(CommentResponse {
comment_view,

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,21 +27,26 @@ 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,
Some(&local_user_view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
check_community_user_action(
&local_user_view.person,
@ -54,8 +59,7 @@ pub async fn like_comment(
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await;
if let Ok(Some(reply)) = comment_reply {
let recipient_id = reply.recipient_id;
if let Ok(Some(local_recipient)) =
LocalUserView::read_person(&mut context.pool(), recipient_id).await
if let Ok(local_recipient) = LocalUserView::read_person(&mut context.pool(), recipient_id).await
{
recipient_ids.push(local_recipient.local_user.id);
}
@ -63,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

@ -5,7 +5,7 @@ use lemmy_api_common::{
utils::is_mod_or_admin,
};
use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
/// Lists likes for a comment
#[tracing::instrument(skip(context))]
@ -19,8 +19,7 @@ pub async fn list_comment_likes(
data.comment_id,
Some(&local_user_view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
is_mod_or_admin(
&mut context.pool(),

View file

@ -37,8 +37,7 @@ pub async fn save_comment(
comment_id,
Some(&local_user_view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
Ok(Json(CommentResponse {
comment_view,

View file

@ -40,8 +40,7 @@ pub async fn create_comment_report(
comment_id,
Some(&local_user_view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
check_community_user_action(
&local_user_view.person,
@ -64,9 +63,8 @@ pub async fn create_comment_report(
.await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let comment_report_view = CommentReportView::read(&mut context.pool(), report.id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
let comment_report_view =
CommentReportView::read(&mut context.pool(), report.id, person_id).await?;
// Email the admins
if local_site.reports_email_admins {

View file

@ -17,9 +17,7 @@ pub async fn resolve_comment_report(
) -> LemmyResult<Json<CommentReportResponse>> {
let report_id = data.report_id;
let person_id = local_user_view.person.id;
let report = CommentReportView::read(&mut context.pool(), report_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
let person_id = local_user_view.person.id;
check_community_mod_action(
@ -41,9 +39,8 @@ pub async fn resolve_comment_report(
}
let report_id = data.report_id;
let comment_report_view = CommentReportView::read(&mut context.pool(), report_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
let comment_report_view =
CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
Ok(Json(CommentReportResponse {
comment_report_view,

View file

@ -46,23 +46,18 @@ pub async fn add_mod_to_community(
.await?;
}
let community = Community::read(&mut context.pool(), community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let community = Community::read(&mut context.pool(), community_id).await?;
// If user is admin and community is remote, explicitly check that he is a
// moderator. This is necessary because otherwise the action would be rejected
// by the community's home instance.
if local_user_view.local_user.admin && !community.local {
let is_mod = CommunityModeratorView::is_community_moderator(
CommunityModeratorView::check_is_community_moderator(
&mut context.pool(),
community.id,
local_user_view.person.id,
)
.await?;
if !is_mod {
Err(LemmyErrorType::NotAModerator)?
}
}
// Update in local database

View file

@ -4,7 +4,11 @@ use lemmy_api_common::{
community::{BanFromCommunity, BanFromCommunityResponse},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_mod_action, check_expire_time, remove_user_data_in_community},
utils::{
check_community_mod_action,
check_expire_time,
remove_or_restore_user_data_in_community,
},
};
use lemmy_db_schema::{
source::{
@ -33,7 +37,6 @@ pub async fn ban_from_community(
local_user_view: LocalUserView,
) -> LemmyResult<Json<BanFromCommunityResponse>> {
let banned_person_id = data.person_id;
let remove_data = data.remove_data.unwrap_or(false);
let expires = check_expire_time(data.expires)?;
// Verify that only mods or admins can ban
@ -85,9 +88,18 @@ pub async fn ban_from_community(
}
// Remove/Restore their data if that's desired
if remove_data {
remove_user_data_in_community(data.community_id, banned_person_id, &mut context.pool()).await?;
}
if data.remove_or_restore_data.unwrap_or(false) {
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?;
};
// Mod tables
let form = ModBanFromCommunityForm {
@ -101,9 +113,7 @@ pub async fn ban_from_community(
ModBanFromCommunity::create(&mut context.pool(), &form).await?;
let person_view = PersonView::read(&mut context.pool(), data.person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
let person_view = PersonView::read(&mut context.pool(), data.person_id).await?;
ActivityChannel::submit_activity(
SendActivityData::BanFromCommunity {

View file

@ -56,8 +56,7 @@ pub async fn block_community(
Some(&local_user_view.local_user),
false,
)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
.await?;
ActivityChannel::submit_activity(
SendActivityData::FollowCommunity(

View file

@ -23,9 +23,7 @@ pub async fn follow_community(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> {
let community = Community::read(&mut context.pool(), data.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let community = Community::read(&mut context.pool(), data.community_id).await?;
let mut community_follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: local_user_view.person.id,
@ -68,8 +66,7 @@ pub async fn follow_community(
Some(&local_user_view.local_user),
false,
)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
.await?;
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).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

@ -82,13 +82,10 @@ pub async fn transfer_community(
Some(&local_user_view.local_user),
false,
)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
.await?;
let community_id = data.community_id;
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
// Return the jwt
Ok(Json(GetCommunityResponse {

View file

@ -172,7 +172,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities(
target: &Person,
ban: bool,
reason: &Option<String>,
remove_data: &Option<bool>,
remove_or_restore_data: &Option<bool>,
expires: &Option<i64>,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
@ -230,7 +230,7 @@ pub(crate) async fn ban_nonlocal_user_from_local_communities(
person_id: target.id,
ban,
reason: reason.clone(),
remove_data: *remove_data,
remove_or_restore_data: *remove_or_restore_data,
expires: *expires,
};
@ -258,17 +258,13 @@ pub async fn local_user_view_from_jwt(
let local_user_id = Claims::validate(jwt, context)
.await
.with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id)
.await?
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?;
check_user_valid(&local_user_view.person)?;
Ok(local_user_view)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
use super::*;

View file

@ -36,8 +36,8 @@ pub async fn add_admin(
// Make sure that the person_id added is local
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
.await?
.ok_or(LemmyErrorType::ObjectNotLocal)?;
.await
.map_err(|_| LemmyErrorType::ObjectNotLocal)?;
LocalUser::update(
&mut context.pool(),

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},
utils::{check_expire_time, is_admin, remove_or_restore_user_data},
};
use lemmy_db_schema::{
source::{
@ -60,15 +60,22 @@ pub async fn ban_from_site(
// if its a local user, invalidate logins
let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await;
if let Ok(Some(local_user)) = local_user {
if let Ok(local_user) = local_user {
LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?;
}
// Remove their data if that's desired
let remove_data = data.remove_data.unwrap_or(false);
if remove_data {
remove_user_data(person.id, &context).await?;
}
if data.remove_or_restore_data.unwrap_or(false) {
let removed = data.ban;
remove_or_restore_user_data(
local_user_view.person.id,
person.id,
removed,
&data.reason,
&context,
)
.await?;
};
// Mod tables
let form = ModBanForm {
@ -81,16 +88,14 @@ pub async fn ban_from_site(
ModBan::create(&mut context.pool(), &form).await?;
let person_view = PersonView::read(&mut context.pool(), person.id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
let person_view = PersonView::read(&mut context.pool(), person.id).await?;
ban_nonlocal_user_from_local_communities(
&local_user_view,
&person,
data.ban,
&data.reason,
&data.remove_data,
&data.remove_or_restore_data,
&data.expires,
&context,
)
@ -101,7 +106,7 @@ pub async fn ban_from_site(
moderator: local_user_view.person,
banned_user: person_view.person.clone(),
reason: data.reason.clone(),
remove_data: data.remove_data,
remove_or_restore_data: data.remove_or_restore_data,
ban: data.ban,
expires: data.expires,
},

View file

@ -32,8 +32,7 @@ pub async fn block_person(
let target_user = LocalUserView::read_person(&mut context.pool(), target_id)
.await
.ok()
.flatten();
.ok();
if target_user.is_some_and(|t| t.local_user.admin) {
Err(LemmyErrorType::CantBlockAdmin)?
@ -49,9 +48,7 @@ pub async fn block_person(
.with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?;
}
let person_view = PersonView::read(&mut context.pool(), target_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
let person_view = PersonView::read(&mut context.pool(), target_id).await?;
Ok(Json(BlockPersonResponse {
person_view,
blocked: data.block,

View file

@ -28,11 +28,13 @@ pub async fn change_password(
}
// Check the old password
let valid: bool = verify(
&data.old_password,
&local_user_view.local_user.password_encrypted,
)
.unwrap_or(false);
let valid: bool = if let Some(password_encrypted) = &local_user_view.local_user.password_encrypted
{
verify(&data.old_password, password_encrypted).unwrap_or(false)
} else {
data.old_password.is_empty()
};
if !valid {
Err(LemmyErrorType::IncorrectLogin)?
}

View file

@ -21,7 +21,6 @@ pub async fn change_password_after_reset(
let token = data.token.clone();
let local_user_id = PasswordResetRequest::read_and_delete(&mut context.pool(), &token)
.await?
.ok_or(LemmyErrorType::TokenNotFound)?
.local_user_id;
password_length_check(&data.password)?;

View file

@ -2,8 +2,11 @@ use crate::{build_totp_2fa, generate_totp_2fa_secret};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{context::LemmyContext, person::GenerateTotpSecretResponse};
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_db_schema::source::{
local_user::{LocalUser, LocalUserUpdateForm},
site::Site,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
/// Generate a new secret for two-factor-authentication. Afterwards you need to call [toggle_totp]
@ -13,17 +16,14 @@ pub async fn generate_totp_secret(
local_user_view: LocalUserView,
context: Data<LemmyContext>,
) -> LemmyResult<Json<GenerateTotpSecretResponse>> {
let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let site = Site::read_local(&mut context.pool()).await?;
if local_user_view.local_user.totp_2fa_enabled {
return Err(LemmyErrorType::TotpAlreadyEnabled)?;
}
let secret = generate_totp_2fa_secret();
let secret_url =
build_totp_2fa(&site_view.site.name, &local_user_view.person.name, &secret)?.get_url();
let secret_url = build_totp_2fa(&site.name, &local_user_view.person.name, &secret)?.get_url();
let local_user_form = LocalUserUpdateForm {
totp_2fa_secret: Some(Some(secret)),

View file

@ -1,5 +1,5 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::context::LemmyContext;
use lemmy_api_common::{context::LemmyContext, person::ListLoginsResponse};
use lemmy_db_schema::source::login_token::LoginToken;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
@ -7,8 +7,8 @@ use lemmy_utils::error::LemmyResult;
pub async fn list_logins(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<Vec<LoginToken>>> {
) -> LemmyResult<Json<ListLoginsResponse>> {
let logins = LoginToken::list(&mut context.pool(), local_user_view.local_user.id).await?;
Ok(Json(logins))
Ok(Json(ListLoginsResponse { logins }))
}

View file

@ -1,4 +1,4 @@
use crate::{check_totp_2fa_valid, local_user::check_email_verified};
use crate::check_totp_2fa_valid;
use actix_web::{
web::{Data, Json},
HttpRequest,
@ -8,12 +8,7 @@ use lemmy_api_common::{
claims::Claims,
context::LemmyContext,
person::{Login, LoginResponse},
utils::check_user_valid,
};
use lemmy_db_schema::{
source::{local_site::LocalSite, registration_application::RegistrationApplication},
utils::DbPool,
RegistrationMode,
utils::{check_email_verified, check_registration_application, check_user_valid},
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
@ -24,23 +19,20 @@ pub async fn login(
req: HttpRequest,
context: Data<LemmyContext>,
) -> LemmyResult<Json<LoginResponse>> {
let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
// Fetch that username / email
let username_or_email = data.username_or_email.clone();
let local_user_view =
LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email)
.await?
.ok_or(LemmyErrorType::IncorrectLogin)?;
LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email).await?;
// Verify the password
let valid: bool = verify(
&data.password,
&local_user_view.local_user.password_encrypted,
)
.unwrap_or(false);
let valid: bool = local_user_view
.local_user
.password_encrypted
.as_ref()
.and_then(|password_encrypted| verify(&data.password, password_encrypted).ok())
.unwrap_or(false);
if !valid {
Err(LemmyErrorType::IncorrectLogin)?
}
@ -67,28 +59,3 @@ pub async fn login(
registration_created: false,
}))
}
async fn check_registration_application(
local_user_view: &LocalUserView,
local_site: &LocalSite,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
if (local_site.registration_mode == RegistrationMode::RequireApplication
|| local_site.registration_mode == RegistrationMode::Closed)
&& !local_user_view.local_user.accepted_application
&& !local_user_view.local_user.admin
{
// Fetch the registration application. If no admin id is present its still pending. Otherwise it
// was processed (either accepted or denied).
let local_user_id = local_user_view.local_user.id;
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id)
.await?
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
if registration.admin_id.is_some() {
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
} else {
Err(LemmyErrorType::RegistrationApplicationIsPending)?
}
}
Ok(())
}

View file

@ -1,6 +1,3 @@
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
pub mod add_admin;
pub mod ban_person;
pub mod block;
@ -20,15 +17,3 @@ pub mod save_settings;
pub mod update_totp;
pub mod validate_auth;
pub mod verify_email;
/// Check if the user's email is verified if email verification is turned on
/// However, skip checking verification if the user is an admin
fn check_email_verified(local_user_view: &LocalUserView, site_view: &SiteView) -> LemmyResult<()> {
if !local_user_view.local_user.admin
&& site_view.local_site.require_email_verification
&& !local_user_view.local_user.email_verified
{
Err(LemmyErrorType::EmailNotVerified)?
}
Ok(())
}

View file

@ -18,9 +18,7 @@ pub async fn mark_person_mention_as_read(
local_user_view: LocalUserView,
) -> LemmyResult<Json<PersonMentionResponse>> {
let person_mention_id = data.person_mention_id;
let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?;
if local_user_view.person.id != read_person_mention.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)?
@ -39,9 +37,7 @@ pub async fn mark_person_mention_as_read(
let person_mention_id = read_person_mention.id;
let person_id = local_user_view.person.id;
let person_mention_view =
PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id))
.await?
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)).await?;
Ok(Json(PersonMentionResponse {
person_mention_view,

View file

@ -18,9 +18,7 @@ pub async fn mark_reply_as_read(
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentReplyResponse>> {
let comment_reply_id = data.comment_reply_id;
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?;
if local_user_view.person.id != read_comment_reply.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)?
@ -40,9 +38,7 @@ pub async fn mark_reply_as_read(
let comment_reply_id = read_comment_reply.id;
let person_id = local_user_view.person.id;
let comment_reply_view =
CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id))
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?;
Ok(Json(CommentReplyResponse { comment_reply_view }))
}

View file

@ -1,9 +1,8 @@
use crate::local_user::check_email_verified;
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::PasswordReset,
utils::send_password_reset_email,
utils::{check_email_verified, send_password_reset_email},
SuccessResponse,
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
@ -17,12 +16,10 @@ pub async fn reset_password(
// Fetch that email
let email = data.email.to_lowercase();
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email)
.await?
.ok_or(LemmyErrorType::IncorrectLogin)?;
.await
.map_err(|_| LemmyErrorType::IncorrectLogin)?;
let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
check_email_verified(&local_user_view, &site_view)?;
// Email the pure token to the user.

View file

@ -36,9 +36,7 @@ pub async fn save_user_settings(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> {
let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let slur_regex = local_site_to_slur_regex(&site_view.local_site);
let url_blocklist = get_url_blocklist(&context).await?;
@ -65,9 +63,7 @@ pub async fn save_user_settings(
let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
// if email was changed, check that it is not taken and send verification mail
if previous_email.deref() != email {
if LocalUser::is_email_taken(&mut context.pool(), email).await? {
return Err(LemmyErrorType::EmailAlreadyExists)?;
}
LocalUser::check_is_email_taken(&mut context.pool(), email).await?;
send_verification_email(
&local_user_view,
email,
@ -104,7 +100,8 @@ pub async fn save_user_settings(
let local_user_id = local_user_view.local_user.id;
let person_id = local_user_view.person.id;
let default_listing_type = data.default_listing_type;
let default_sort_type = data.default_sort_type;
let default_post_sort_type = data.default_post_sort_type;
let default_comment_sort_type = data.default_comment_sort_type;
let person_form = PersonUpdateForm {
display_name,
@ -133,10 +130,9 @@ pub async fn save_user_settings(
send_notifications_to_email: data.send_notifications_to_email,
show_nsfw: data.show_nsfw,
blur_nsfw: data.blur_nsfw,
auto_expand: data.auto_expand,
show_bot_accounts: data.show_bot_accounts,
show_scores: data.show_scores,
default_sort_type,
default_post_sort_type,
default_comment_sort_type,
default_listing_type,
theme: data.theme.clone(),
interface_language: data.interface_language.clone(),

View file

@ -10,19 +10,15 @@ use lemmy_db_schema::source::{
local_user::{LocalUser, LocalUserUpdateForm},
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
use lemmy_utils::error::LemmyResult;
pub async fn verify_email(
data: Json<VerifyEmail>,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let token = data.token.clone();
let verification = EmailVerification::read_for_token(&mut context.pool(), &token)
.await?
.ok_or(LemmyErrorType::TokenNotFound)?;
let verification = EmailVerification::read_for_token(&mut context.pool(), &token).await?;
let form = LocalUserUpdateForm {
// necessary in case this is a new signup
@ -39,9 +35,7 @@ pub async fn verify_email(
// send out notification about registration application to admins if enabled
if site_view.local_site.application_email_admins {
let local_user = LocalUserView::read(&mut context.pool(), local_user_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
let local_user = LocalUserView::read(&mut context.pool(), local_user_id).await?;
send_new_applicant_email_to_admins(
&local_user.person.name,

View file

@ -16,7 +16,7 @@ use lemmy_db_schema::{
PostFeatureType,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn feature_post(
@ -25,9 +25,7 @@ pub async fn feature_post(
local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
check_community_mod_action(
&local_user_view.person,

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,16 +32,20 @@ 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?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let post = Post::read(&mut context.pool(), post_id).await?;
check_community_user_action(
&local_user_view.person,
@ -70,9 +75,7 @@ pub async fn like_post(
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
let community = Community::read(&mut context.pool(), post.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let community = Community::read(&mut context.pool(), post.community_id).await?;
ActivityChannel::submit_activity(
SendActivityData::LikePostOrComment {

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{source::post::Post, traits::Crud};
use lemmy_db_views::structs::{LocalUserView, VoteView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
/// Lists likes for a post
#[tracing::instrument(skip(context))]
@ -15,9 +15,7 @@ pub async fn list_post_likes(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<ListPostLikesResponse>> {
let post = Post::read(&mut context.pool(), data.post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let post = Post::read(&mut context.pool(), data.post_id).await?;
is_mod_or_admin(
&mut context.pool(),
&local_user_view.person,

View file

@ -15,7 +15,7 @@ use lemmy_db_schema::{
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn lock_post(
@ -24,9 +24,7 @@ pub async fn lock_post(
local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
check_community_mod_action(
&local_user_view.person,

View file

@ -40,8 +40,7 @@ pub async fn save_post(
Some(&local_user_view.local_user),
false,
)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
.await?;
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;

View file

@ -35,9 +35,7 @@ pub async fn create_post_report(
let person_id = local_user_view.person.id;
let post_id = data.post_id;
let post_view = PostView::read(&mut context.pool(), post_id, None, false)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let post_view = PostView::read(&mut context.pool(), post_id, None, false).await?;
check_community_user_action(
&local_user_view.person,
@ -61,9 +59,7 @@ pub async fn create_post_report(
.await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id).await?;
// Email the admins
if local_site.reports_email_admins {

View file

@ -17,9 +17,7 @@ pub async fn resolve_post_report(
) -> LemmyResult<Json<PostReportResponse>> {
let report_id = data.report_id;
let person_id = local_user_view.person.id;
let report = PostReportView::read(&mut context.pool(), report_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
let report = PostReportView::read(&mut context.pool(), report_id, person_id).await?;
let person_id = local_user_view.person.id;
check_community_mod_action(
@ -40,9 +38,7 @@ pub async fn resolve_post_report(
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
}
let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id).await?;
Ok(Json(PostReportResponse { post_report_view }))
}

View file

@ -18,9 +18,7 @@ pub async fn mark_pm_as_read(
) -> LemmyResult<Json<PrivateMessageResponse>> {
// Checking permissions
let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
if local_user_view.person.id != orig_private_message.recipient_id {
Err(LemmyErrorType::CouldntUpdatePrivateMessage)?
}
@ -39,9 +37,7 @@ pub async fn mark_pm_as_read(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
Ok(Json(PrivateMessageResponse {
private_message_view: view,
}))

View file

@ -29,9 +29,7 @@ pub async fn create_pm_report(
let person_id = local_user_view.person.id;
let private_message_id = data.private_message_id;
let private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
// Make sure that only the recipient of the private message can create a report
if person_id != private_message.recipient_id {
@ -49,9 +47,8 @@ pub async fn create_pm_report(
.await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report.id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?;
let private_message_report_view =
PrivateMessageReportView::read(&mut context.pool(), report.id).await?;
// Email the admins
if local_site.reports_email_admins {

View file

@ -28,9 +28,8 @@ pub async fn resolve_pm_report(
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
}
let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?;
let private_message_report_view =
PrivateMessageReportView::read(&mut context.pool(), report_id).await?;
Ok(Json(PrivateMessageReportResponse {
private_message_report_view,

View file

@ -5,15 +5,13 @@ use lemmy_api_common::{
utils::build_federated_instances,
};
use lemmy_db_views::structs::SiteView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn get_federated_instances(
context: Data<LemmyContext>,
) -> LemmyResult<Json<GetFederatedInstancesResponse>> {
let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let federated_instances =
build_federated_instances(&site_view.local_site, &mut context.pool()).await?;

View file

@ -7,11 +7,12 @@ use lemmy_db_schema::{
local_site_url_blocklist::LocalSiteUrlBlocklist,
local_user::{LocalUser, LocalUserUpdateForm},
moderator::{ModAdd, ModAddForm},
oauth_provider::OAuthProvider,
tagline::Tagline,
},
traits::Crud,
};
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_db_views_actor::structs::PersonView;
use lemmy_utils::{
error::{LemmyErrorType, LemmyResult},
@ -55,17 +56,14 @@ pub async fn leave_admin(
ModAdd::create(&mut context.pool(), &form).await?;
// Reread site and admins
let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let admins = PersonView::admins(&mut context.pool()).await?;
let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
let custom_emojis =
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).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.ok();
Ok(Json(GetSiteResponse {
site_view,
@ -74,8 +72,11 @@ pub async fn leave_admin(
my_user: None,
all_languages,
discussion_languages,
taglines,
custom_emojis,
oauth_providers: Some(oauth_providers),
admin_oauth_providers: None,
blocked_urls,
tagline,
taglines: vec![],
custom_emojis: vec![],
}))
}

View file

@ -16,7 +16,7 @@ use lemmy_db_schema::{
traits::Crud,
};
use lemmy_db_views::structs::{CommentView, LocalUserView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn purge_comment(
@ -35,8 +35,7 @@ pub async fn purge_comment(
comment_id,
Some(&local_user_view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
// Also check that you're a higher admin
LocalUser::is_higher_admin_check(

View file

@ -19,7 +19,7 @@ use lemmy_db_schema::{
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityModeratorView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn purge_community(
@ -31,9 +31,7 @@ pub async fn purge_community(
is_admin(&local_user_view)?;
// Read the community to get its images
let community = Community::read(&mut context.pool(), data.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let community = Community::read(&mut context.pool(), data.community_id).await?;
// Also check that you're a higher admin than all the mods
let community_mod_person_ids =

View file

@ -17,7 +17,7 @@ use lemmy_db_schema::{
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn purge_person(
@ -36,9 +36,7 @@ pub async fn purge_person(
)
.await?;
let person = Person::read(&mut context.pool(), data.person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
let person = Person::read(&mut context.pool(), data.person_id).await?;
ban_nonlocal_user_from_local_communities(
&local_user_view,
@ -77,7 +75,7 @@ pub async fn purge_person(
moderator: local_user_view.person,
banned_user: person,
reason: data.reason.clone(),
remove_data: Some(true),
remove_or_restore_data: Some(true),
ban: true,
expires: None,
},

View file

@ -17,7 +17,7 @@ use lemmy_db_schema::{
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn purge_post(
@ -29,9 +29,7 @@ pub async fn purge_post(
is_admin(&local_user_view)?;
// Read the post to get the community_id
let post = Post::read(&mut context.pool(), data.post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let post = Post::read(&mut context.pool(), data.post_id).await?;
// Also check that you're a higher admin
LocalUser::is_higher_admin_check(

View file

@ -14,10 +14,7 @@ use lemmy_db_schema::{
utils::{diesel_string_update, get_conn},
};
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
use lemmy_utils::{
error::{LemmyError, LemmyResult},
LemmyErrorType,
};
use lemmy_utils::error::{LemmyError, LemmyResult};
pub async fn approve_registration_application(
data: Json<ApproveRegistrationApplication>,
@ -61,9 +58,8 @@ pub async fn approve_registration_application(
.await?;
if data.approve {
let approved_local_user_view = LocalUserView::read(&mut context.pool(), approved_user_id)
.await?
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
let approved_local_user_view =
LocalUserView::read(&mut context.pool(), approved_user_id).await?;
if approved_local_user_view.local_user.email.is_some() {
// Email sending may fail, but this won't revert the application approval
send_application_approved_email(&approved_local_user_view, context.settings()).await?;
@ -71,9 +67,8 @@ pub async fn approve_registration_application(
};
// Read the view
let registration_application = RegistrationApplicationView::read(&mut context.pool(), app_id)
.await?
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
let registration_application =
RegistrationApplicationView::read(&mut context.pool(), app_id).await?;
Ok(Json(RegistrationApplicationResponse {
registration_application,

View file

@ -5,7 +5,7 @@ use lemmy_api_common::{
utils::is_admin,
};
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
/// Lists registration applications, filterable by undenied only.
pub async fn get_registration_application(
@ -18,9 +18,7 @@ pub async fn get_registration_application(
// Read the view
let registration_application =
RegistrationApplicationView::read_by_person(&mut context.pool(), data.person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
RegistrationApplicationView::read_by_person(&mut context.pool(), data.person_id).await?;
Ok(Json(RegistrationApplicationResponse {
registration_application,

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;
#[allow(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,
@ -54,35 +51,26 @@ async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance
)
.await?;
let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id)
.await?
.unwrap();
let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id).await?;
let site_form = SiteInsertForm::builder()
.name("test site".to_string())
.instance_id(inserted_instance.id)
.build();
let site = Site::create(pool, &site_form).await.unwrap();
let site_form = SiteInsertForm::new("test site".to_string(), inserted_instance.id);
let site = Site::create(pool, &site_form).await?;
// Create a local site, since this is necessary for determining if email verification is
// required
let local_site_form = LocalSiteInsertForm::builder()
.site_id(site.id)
.require_email_verification(Some(true))
.application_question(Some(".".to_string()))
.registration_mode(Some(RegistrationMode::RequireApplication))
.site_setup(Some(true))
.build();
let local_site = LocalSite::create(pool, &local_site_form).await.unwrap();
let local_site_form = LocalSiteInsertForm {
require_email_verification: Some(true),
application_question: Some(".".to_string()),
registration_mode: Some(RegistrationMode::RequireApplication),
site_setup: Some(true),
..LocalSiteInsertForm::new(site.id)
};
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::builder()
.local_site_id(local_site.id)
.build();
LocalSiteRateLimit::create(pool, &rate_limit_form)
.await
.unwrap();
let rate_limit_form = LocalSiteRateLimitInsertForm::new(local_site.id);
LocalSiteRateLimit::create(pool, &rate_limit_form).await?;
Ok((inserted_instance, admin_local_user_view))
}
@ -116,7 +104,6 @@ async fn signup(
Ok((local_user, application))
}
#[allow(clippy::unwrap_used)]
async fn get_application_statuses(
context: &Data<LemmyContext>,
admin: LocalUserView,
@ -129,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,
)
@ -145,10 +132,9 @@ async fn get_application_statuses(
Ok((application_count, unread_applications, all_applications))
}
#[allow(clippy::indexing_slicing)]
#[allow(clippy::unwrap_used)]
#[tokio::test]
#[serial]
#[tokio::test]
#[expect(clippy::indexing_slicing)]
async fn test_application_approval() -> LemmyResult<()> {
let context = LemmyContext::init_test_context().await;
let pool = &mut context.pool();

View file

@ -42,44 +42,40 @@ pub async fn get_sitemap(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
}
#[cfg(test)]
#[allow(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()
.children()
.find(|element| element.tag().name() == "lastmod")
.unwrap()
.text(),
.and_then(|n| n
.children()
.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()
.children()
.find(|element| element.tag().name() == "lastmod")
.unwrap()
.text(),
.and_then(|n| n
.children()
.find(|element| element.tag().name() == "lastmod"))
.map(Element::text)
.unwrap_or_default(),
"2023-01-01T01:02:03+00:00"
);
Ok(())
}
}

View file

@ -72,7 +72,7 @@ jsonwebtoken = { version = "9.3.0", optional = true }
# necessary for wasmt compilation
getrandom = { version = "0.2.15", features = ["js"] }
[package.metadata.cargo-machete]
[package.metadata.cargo-shear]
ignored = ["getrandom"]
[dev-dependencies]

View file

@ -27,7 +27,6 @@ use lemmy_db_views_actor::structs::CommunityView;
use lemmy_utils::{
error::LemmyResult,
utils::{markdown::markdown_to_html, mention::MentionData},
LemmyErrorType,
};
pub async fn build_comment_response(
@ -37,9 +36,8 @@ pub async fn build_comment_response(
recipient_ids: Vec<LocalUserId>,
) -> LemmyResult<CommentResponse> {
let local_user = local_user_view.map(|l| l.local_user);
let comment_view = CommentView::read(&mut context.pool(), comment_id, local_user.as_ref())
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
let comment_view =
CommentView::read(&mut context.pool(), comment_id, local_user.as_ref()).await?;
Ok(CommentResponse {
comment_view,
recipient_ids,
@ -61,8 +59,7 @@ pub async fn build_community_response(
Some(&local_user),
is_mod_or_admin,
)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
.await?;
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
Ok(Json(CommunityResponse {
@ -87,8 +84,7 @@ pub async fn build_post_response(
Some(&local_user),
is_mod_or_admin,
)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
.await?;
Ok(Json(PostResponse { post_view }))
}
@ -112,8 +108,7 @@ pub async fn send_local_notifs(
comment_id,
local_user_view.map(|view| &view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
let comment = comment_view.comment;
let post = comment_view.post;
let community = comment_view.community;
@ -125,7 +120,7 @@ pub async fn send_local_notifs(
{
let mention_name = mention.name.clone();
let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await;
if let Ok(Some(mention_user_view)) = user_view {
if let Ok(mention_user_view) = user_view {
// TODO
// At some point, make it so you can't tag the parent creator either
// Potential duplication of notifications, one for reply and the other for mention, is handled
@ -161,9 +156,7 @@ pub async fn send_local_notifs(
// Send comment_reply to the parent commenter / poster
if let Some(parent_comment_id) = comment.parent_comment_id() {
let parent_comment = Comment::read(&mut context.pool(), parent_comment_id)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?;
// Get the parent commenter local_user
let parent_creator_id = parent_comment.creator_id;
@ -182,7 +175,7 @@ pub async fn send_local_notifs(
// Don't send a notif to yourself
if parent_comment.creator_id != person.id && !check_blocks {
let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await;
if let Ok(Some(parent_user_view)) = user_view {
if let Ok(parent_user_view) = user_view {
// Don't duplicate notif if already mentioned by checking recipient ids
if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id);
@ -229,7 +222,7 @@ pub async fn send_local_notifs(
if post.creator_id != person.id && !check_blocks {
let creator_id = post.creator_id;
let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await;
if let Ok(Some(parent_user_view)) = parent_user {
if let Ok(parent_user_view) = parent_user {
if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id);

View file

@ -29,12 +29,8 @@ impl Claims {
let claims =
decode::<Claims>(jwt, &key, &validation).with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
let user_id = LocalUserId(claims.claims.sub.parse()?);
let is_valid = LoginToken::validate(&mut context.pool(), user_id, jwt).await?;
if !is_valid {
Err(LemmyErrorType::NotLoggedIn)?
} else {
Ok(user_id)
}
LoginToken::validate(&mut context.pool(), user_id, jwt).await?;
Ok(user_id)
}
pub async fn generate(
@ -73,8 +69,6 @@ impl Claims {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
use crate::{claims::Claims, context::LemmyContext};
@ -89,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;
@ -97,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(),
@ -108,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,
SortType,
};
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")]
@ -44,7 +48,9 @@ pub struct CreateCommunity {
pub name: String,
/// A longer title.
pub title: String,
/// A longer sidebar, or description of your community, in markdown.
/// A sidebar for the community in markdown.
pub sidebar: Option<String>,
/// A shorter, one line description of your community.
pub description: Option<String>,
/// An icon URL.
pub icon: Option<String>,
@ -74,7 +80,7 @@ pub struct CommunityResponse {
/// Fetches a list of communities.
pub struct ListCommunities {
pub type_: Option<ListingType>,
pub sort: Option<SortType>,
pub sort: Option<CommunitySortType>,
pub show_nsfw: Option<bool>,
pub page: Option<i64>,
pub limit: Option<i64>,
@ -97,7 +103,9 @@ pub struct BanFromCommunity {
pub community_id: CommunityId,
pub person_id: PersonId,
pub ban: bool,
pub remove_data: Option<bool>,
/// Optionally remove or restore all their data. Useful for new troll accounts.
/// If ban is true, then this means remove. If ban is false, it means restore.
pub remove_or_restore_data: Option<bool>,
pub reason: Option<String>,
/// A time that the ban will expire, in unix epoch seconds.
///
@ -141,7 +149,9 @@ pub struct EditCommunity {
pub community_id: CommunityId,
/// A longer title.
pub title: Option<String>,
/// A longer sidebar, or description of your community, in markdown.
/// A sidebar for the community in markdown.
pub sidebar: Option<String>,
/// A shorter, one line description of your community.
pub description: Option<String>,
/// An icon URL.
pub icon: Option<String>,
@ -223,3 +233,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

@ -1,6 +1,7 @@
use lemmy_db_schema::newtypes::CustomEmojiId;
use lemmy_db_views::structs::CustomEmojiView;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
use url::Url;
@ -46,3 +47,23 @@ pub struct DeleteCustomEmoji {
pub struct CustomEmojiResponse {
pub custom_emoji: CustomEmojiView,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// A response for custom emojis.
pub struct ListCustomEmojisResponse {
pub custom_emojis: Vec<CustomEmojiView>,
}
#[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 list of custom emojis.
pub struct ListCustomEmojis {
pub page: Option<i64>,
pub limit: Option<i64>,
pub category: Option<String>,
pub ignore_page_limits: Option<bool>,
}

View file

@ -7,6 +7,7 @@ pub mod community;
#[cfg(feature = "full")]
pub mod context;
pub mod custom_emoji;
pub mod oauth_provider;
pub mod person;
pub mod post;
pub mod private_message;
@ -15,6 +16,7 @@ pub mod request;
#[cfg(feature = "full")]
pub mod send_activity;
pub mod site;
pub mod tagline;
#[cfg(feature = "full")]
pub mod utils;

View file

@ -0,0 +1,71 @@
use lemmy_db_schema::newtypes::OAuthProviderId;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
use url::Url;
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Create an external auth method.
pub struct CreateOAuthProvider {
pub display_name: String,
pub issuer: String,
pub authorization_endpoint: String,
pub token_endpoint: String,
pub userinfo_endpoint: String,
pub id_claim: String,
pub client_id: String,
pub client_secret: String,
pub scopes: String,
pub auto_verify_email: Option<bool>,
pub account_linking_enabled: Option<bool>,
pub enabled: Option<bool>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Edit an external auth method.
pub struct EditOAuthProvider {
pub id: OAuthProviderId,
pub display_name: Option<String>,
pub authorization_endpoint: Option<String>,
pub token_endpoint: Option<String>,
pub userinfo_endpoint: Option<String>,
pub id_claim: Option<String>,
pub client_secret: Option<String>,
pub scopes: Option<String>,
pub auto_verify_email: Option<bool>,
pub account_linking_enabled: Option<bool>,
pub enabled: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Delete an external auth method.
pub struct DeleteOAuthProvider {
pub id: OAuthProviderId,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Logging in with an OAuth 2.0 authorization
pub struct AuthenticateWithOauth {
pub code: String,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub oauth_provider_id: OAuthProviderId,
#[cfg_attr(feature = "full", ts(type = "string"))]
pub redirect_uri: Url,
pub show_nsfw: Option<bool>,
/// Username is mandatory at registration time
pub username: Option<String>,
/// An answer is mandatory if require application is enabled on the server
pub answer: Option<String>,
}

View file

@ -1,11 +1,11 @@
use lemmy_db_schema::{
newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId},
sensitive::SensitiveString,
source::site::Site,
source::{login_token::LoginToken, site::Site},
CommentSortType,
ListingType,
PostListingMode,
SortType,
PostSortType,
};
use lemmy_db_views::structs::{CommentView, LocalImageView, PostView};
use lemmy_db_views_actor::structs::{
@ -84,12 +84,18 @@ pub struct CaptchaResponse {
pub struct SaveUserSettings {
/// Show nsfw posts.
pub show_nsfw: Option<bool>,
/// Blur nsfw posts.
pub blur_nsfw: Option<bool>,
pub auto_expand: Option<bool>,
/// Your user's theme.
pub theme: Option<String>,
pub default_sort_type: Option<SortType>,
/// The default post listing type, usually "local"
pub default_listing_type: Option<ListingType>,
/// A post-view mode that changes how multiple post listings look.
pub post_listing_mode: Option<PostListingMode>,
/// The default post sort, usually "active"
pub default_post_sort_type: Option<PostSortType>,
/// The default comment sort, usually "hot"
pub default_comment_sort_type: Option<CommentSortType>,
/// The language of the lemmy interface
pub interface_language: Option<String>,
/// A URL for your avatar.
@ -120,8 +126,6 @@ pub struct SaveUserSettings {
pub open_links_in_new_tab: Option<bool>,
/// Enable infinite scroll
pub infinite_scroll_enabled: Option<bool>,
/// A post-view mode that changes how multiple post listings look.
pub post_listing_mode: Option<PostListingMode>,
/// Whether to allow keyboard navigation (for browsing and interacting with posts and comments).
pub enable_keyboard_navigation: Option<bool>,
/// Whether user avatars or inline images in the UI that are gifs should be allowed to play or
@ -172,7 +176,7 @@ pub struct GetPersonDetails {
pub person_id: Option<PersonId>,
/// Example: dessalines , or dessalines@xyz.tld
pub username: Option<String>,
pub sort: Option<SortType>,
pub sort: Option<PostSortType>,
pub page: Option<i64>,
pub limit: Option<i64>,
pub community_id: Option<CommunityId>,
@ -217,8 +221,9 @@ pub struct AddAdminResponse {
pub struct BanPerson {
pub person_id: PersonId,
pub ban: bool,
/// Optionally remove all their data. Useful for new troll accounts.
pub remove_data: Option<bool>,
/// Optionally remove or restore all their data. Useful for new troll accounts.
/// If ban is true, then this means remove. If ban is false, it means restore.
pub remove_or_restore_data: Option<bool>,
pub reason: Option<String>,
/// A time that the ban will expire, in unix epoch seconds.
///
@ -441,3 +446,10 @@ pub struct ListMedia {
pub struct ListMediaResponse {
pub images: Vec<LocalImageView>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct ListLoginsResponse {
pub logins: Vec<LoginToken>,
}

View file

@ -2,7 +2,7 @@ use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, CommunityPostTagId, DbUrl, LanguageId, PostId, PostReportId},
ListingType,
PostFeatureType,
SortType,
PostSortType,
};
use lemmy_db_views::structs::{PaginationCursor, PostReportView, PostView, VoteView};
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
@ -31,6 +31,8 @@ pub struct CreatePost {
/// Instead of fetching a thumbnail, use a custom one.
pub custom_thumbnail: Option<String>,
pub community_post_tags: Option<Vec<CommunityPostTagId>>,
/// Time when this post should be scheduled. Null means publish immediately.
pub scheduled_publish_time: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -70,7 +72,7 @@ pub struct GetPostResponse {
/// Get a list of posts.
pub struct GetPosts {
pub type_: Option<ListingType>,
pub sort: Option<SortType>,
pub sort: Option<PostSortType>,
/// DEPRECATED, use page_cursor
pub page: Option<i64>,
pub limit: Option<i64>,
@ -126,6 +128,8 @@ pub struct EditPost {
/// Instead of fetching a thumbnail, use a custom one.
pub custom_thumbnail: Option<String>,
pub community_post_tags: Option<Vec<CommunityPostTagId>>,
/// Time when this post should be scheduled. Null means publish immediately.
pub scheduled_publish_time: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]

View file

@ -3,7 +3,7 @@ use crate::{
lemmy_db_schema::traits::Crud,
post::{LinkMetadata, OpenGraphData},
send_activity::{ActivityChannel, SendActivityData},
utils::{local_site_opt_to_sensitive, proxy_image_link},
utils::proxy_image_link,
};
use activitypub_federation::config::Data;
use chrono::{DateTime, Utc};
@ -13,8 +13,8 @@ use lemmy_db_schema::{
newtypes::DbUrl,
source::{
images::{ImageDetailsForm, LocalImage, LocalImageForm},
local_site::LocalSite,
post::{Post, PostUpdateForm},
site::Site,
},
};
use lemmy_utils::{
@ -44,6 +44,7 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder {
.user_agent(user_agent.clone())
.timeout(REQWEST_TIMEOUT)
.connect_timeout(REQWEST_TIMEOUT)
.use_rustls_tls()
}
/// Fetches metadata for the given link and optionally generates thumbnail.
@ -130,7 +131,6 @@ pub async fn generate_post_link_metadata(
post: Post,
custom_thumbnail: Option<Url>,
send_activity: impl FnOnce(Post) -> Option<SendActivityData> + Send + 'static,
local_site: Option<LocalSite>,
context: Data<LemmyContext>,
) -> LemmyResult<()> {
let metadata = match &post.url {
@ -144,7 +144,8 @@ pub async fn generate_post_link_metadata(
.is_some_and(|content_type| content_type.starts_with("image"));
// Decide if we are allowed to generate local thumbnail
let allow_sensitive = local_site_opt_to_sensitive(&local_site);
let site = Site::read_local(&mut context.pool()).await?;
let allow_sensitive = site.content_warning.is_some();
let allow_generate_thumbnail = allow_sensitive || !post.nsfw;
let image_url = if is_image_post {
@ -353,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
@ -470,14 +472,13 @@ pub async fn replace_image(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
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

@ -83,7 +83,7 @@ pub enum SendActivityData {
moderator: Person,
banned_user: Person,
reason: Option<String>,
remove_data: Option<bool>,
remove_or_restore_data: Option<bool>,
ban: bool,
expires: Option<i64>,
},

View file

@ -11,34 +11,35 @@ use lemmy_db_schema::{
RegistrationApplicationId,
},
source::{
community::Community,
federation_queue_state::FederationQueueState,
instance::Instance,
language::Language,
local_site_url_blocklist::LocalSiteUrlBlocklist,
oauth_provider::{OAuthProvider, PublicOAuthProvider},
person::Person,
tagline::Tagline,
},
CommentSortType,
FederationMode,
ListingType,
ModlogActionType,
PostListingMode,
PostSortType,
RegistrationMode,
SearchType,
SortType,
};
use lemmy_db_views::structs::{
CommentView,
CustomEmojiView,
LocalUserView,
PostView,
RegistrationApplicationView,
SiteView,
};
use lemmy_db_views_actor::structs::{
CommunityBlockView,
CommunityFollowerView,
CommunityModeratorView,
CommunityView,
InstanceBlockView,
PersonBlockView,
PersonView,
};
use lemmy_db_views_moderator::structs::{
@ -74,10 +75,15 @@ pub struct Search {
pub community_name: Option<String>,
pub creator_id: Option<PersonId>,
pub type_: Option<SearchType>,
pub sort: Option<SortType>,
pub sort: Option<PostSortType>,
pub listing_type: Option<ListingType>,
pub page: Option<i64>,
pub limit: Option<i64>,
pub title_only: Option<bool>,
pub post_url_only: Option<bool>,
pub saved_only: Option<bool>,
pub liked_only: Option<bool>,
pub disliked_only: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
@ -165,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>,
@ -173,7 +178,9 @@ pub struct CreateSite {
pub private_instance: Option<bool>,
pub default_theme: Option<String>,
pub default_post_listing_type: Option<ListingType>,
pub default_sort_type: Option<SortType>,
pub default_post_listing_mode: Option<PostListingMode>,
pub default_post_sort_type: Option<PostSortType>,
pub default_comment_sort_type: Option<CommentSortType>,
pub legal_information: Option<String>,
pub application_email_admins: Option<bool>,
pub hide_modlog_mod_names: Option<bool>,
@ -198,10 +205,13 @@ pub struct CreateSite {
pub captcha_difficulty: Option<String>,
pub allowed_instances: Option<Vec<String>>,
pub blocked_instances: Option<Vec<String>>,
pub taglines: Option<Vec<String>>,
pub registration_mode: Option<RegistrationMode>,
pub oauth_registration: Option<bool>,
pub content_warning: Option<String>,
pub default_post_listing_mode: Option<PostListingMode>,
pub post_upvotes: Option<FederationMode>,
pub post_downvotes: Option<FederationMode>,
pub comment_upvotes: Option<FederationMode>,
pub comment_downvotes: Option<FederationMode>,
}
#[skip_serializing_none]
@ -211,6 +221,7 @@ pub struct CreateSite {
/// Edits a site.
pub struct EditSite {
pub name: Option<String>,
/// A sidebar for the site, in markdown.
pub sidebar: Option<String>,
/// A shorter, one line description of your site.
pub description: Option<String>,
@ -218,8 +229,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.
@ -232,9 +241,14 @@ pub struct EditSite {
pub private_instance: Option<bool>,
/// The default theme. Usually "browser"
pub default_theme: Option<String>,
/// The default post listing type, usually "local"
pub default_post_listing_type: Option<ListingType>,
/// The default sort, usually "active"
pub default_sort_type: Option<SortType>,
/// Default value for listing mode, usually "list"
pub default_post_listing_mode: Option<PostListingMode>,
/// The default post sort, usually "active"
pub default_post_sort_type: Option<PostSortType>,
/// The default comment sort, usually "hot"
pub default_comment_sort_type: Option<CommentSortType>,
/// An optional page of legal information
pub legal_information: Option<String>,
/// Whether to email admins when receiving a new application.
@ -279,16 +293,22 @@ pub struct EditSite {
pub blocked_instances: Option<Vec<String>>,
/// A list of blocked URLs
pub blocked_urls: Option<Vec<String>>,
/// A list of taglines shown at the top of the front page.
pub taglines: Option<Vec<String>>,
pub registration_mode: Option<RegistrationMode>,
/// 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>,
/// Default value for [LocalUser.post_listing_mode]
pub default_post_listing_mode: Option<PostListingMode>,
/// 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)]
@ -297,7 +317,8 @@ pub struct EditSite {
/// The response for a site.
pub struct SiteResponse {
pub site_view: SiteView,
pub taglines: Vec<Tagline>,
/// deprecated, use field `tagline` or /api/v3/tagline/list
pub taglines: Vec<()>,
}
#[skip_serializing_none]
@ -312,10 +333,15 @@ pub struct GetSiteResponse {
pub my_user: Option<MyUserInfo>,
pub all_languages: Vec<Language>,
pub discussion_languages: Vec<LanguageId>,
/// A list of taglines shown at the top of the front page.
pub taglines: Vec<Tagline>,
/// A list of custom emojis your site supports.
pub custom_emojis: Vec<CustomEmojiView>,
/// deprecated, use field `tagline` or /api/v3/tagline/list
pub taglines: Vec<()>,
/// deprecated, use /api/v3/custom_emoji/list
pub custom_emojis: Vec<()>,
/// If the site has any taglines, a random one is included here for displaying
pub tagline: Option<Tagline>,
/// A list of external auth methods your site supports.
pub oauth_providers: Option<Vec<PublicOAuthProvider>>,
pub admin_oauth_providers: Option<Vec<OAuthProvider>>,
pub blocked_urls: Vec<LocalSiteUrlBlocklist>,
}
@ -337,9 +363,9 @@ pub struct MyUserInfo {
pub local_user_view: LocalUserView,
pub follows: Vec<CommunityFollowerView>,
pub moderates: Vec<CommunityModeratorView>,
pub community_blocks: Vec<CommunityBlockView>,
pub instance_blocks: Vec<InstanceBlockView>,
pub person_blocks: Vec<PersonBlockView>,
pub community_blocks: Vec<Community>,
pub instance_blocks: Vec<Instance>,
pub person_blocks: Vec<Person>,
pub discussion_languages: Vec<LanguageId>,
}

View file

@ -0,0 +1,55 @@
use lemmy_db_schema::{newtypes::TaglineId, source::tagline::Tagline};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Create a tagline
pub struct CreateTagline {
pub content: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Update a tagline
pub struct UpdateTagline {
pub id: TaglineId,
pub content: String,
}
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Delete a tagline
pub struct DeleteTagline {
pub id: TaglineId,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct TaglineResponse {
pub tagline: Tagline,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// A response for taglines.
pub struct ListTaglinesResponse {
pub taglines: Vec<Tagline>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Fetches a list of taglines.
pub struct ListTaglines {
pub page: Option<i64>,
pub limit: Option<i64>,
}

View file

@ -11,30 +11,35 @@ 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},
images::RemoteImage,
images::{ImageDetails, RemoteImage},
instance::Instance,
instance_block::InstanceBlock,
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::{
comment_view::CommentQuery,
structs::{LocalImageView, LocalUserView},
structs::{LocalImageView, LocalUserView, SiteView},
};
use lemmy_db_views_actor::structs::{
CommunityModeratorView,
@ -45,10 +50,14 @@ 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,
},
CACHE_DURATION_FEDERATION,
};
@ -69,13 +78,7 @@ pub async fn is_mod_or_admin(
community_id: CommunityId,
) -> LemmyResult<()> {
check_user_valid(person)?;
let is_mod_or_admin = CommunityView::is_mod_or_admin(pool, person.id, community_id).await?;
if !is_mod_or_admin {
Err(LemmyErrorType::NotAModOrAdmin)?
} else {
Ok(())
}
CommunityView::check_is_mod_or_admin(pool, person.id, community_id).await
}
#[tracing::instrument(skip_all)]
@ -106,13 +109,7 @@ pub async fn check_community_mod_of_any_or_admin_action(
let person = &local_user_view.person;
check_user_valid(person)?;
let is_mod_of_any_or_admin = CommunityView::is_mod_of_any_or_admin(pool, person.id).await?;
if !is_mod_of_any_or_admin {
Err(LemmyErrorType::NotAModOrAdmin)?
} else {
Ok(())
}
CommunityView::check_is_mod_of_any_or_admin(pool, person.id).await
}
pub fn is_admin(local_user_view: &LocalUserView) -> LemmyResult<()> {
@ -171,9 +168,7 @@ pub async fn update_read_comments(
..PersonPostAggregatesForm::default()
};
PersonPostAggregates::upsert(pool, &person_post_agg_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
PersonPostAggregates::upsert(pool, &person_post_agg_form).await?;
Ok(())
}
@ -191,6 +186,44 @@ pub fn check_user_valid(person: &Person) -> LemmyResult<()> {
}
}
/// Check if the user's email is verified if email verification is turned on
/// However, skip checking verification if the user is an admin
pub fn check_email_verified(
local_user_view: &LocalUserView,
site_view: &SiteView,
) -> LemmyResult<()> {
if !local_user_view.local_user.admin
&& site_view.local_site.require_email_verification
&& !local_user_view.local_user.email_verified
{
Err(LemmyErrorType::EmailNotVerified)?
}
Ok(())
}
pub async fn check_registration_application(
local_user_view: &LocalUserView,
local_site: &LocalSite,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
if (local_site.registration_mode == RegistrationMode::RequireApplication
|| local_site.registration_mode == RegistrationMode::Closed)
&& !local_user_view.local_user.accepted_application
&& !local_user_view.local_user.admin
{
// Fetch the registration application. If no admin id is present its still pending. Otherwise it
// was processed (either accepted or denied).
let local_user_id = local_user_view.local_user.id;
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id).await?;
if registration.admin_id.is_some() {
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
} else {
Err(LemmyErrorType::RegistrationApplicationIsPending)?
}
}
Ok(())
}
/// Checks that a normal user action (eg posting or voting) is allowed in a given community.
///
/// In particular it checks that neither the user nor community are banned or deleted, and that
@ -202,7 +235,7 @@ pub async fn check_community_user_action(
) -> LemmyResult<()> {
check_user_valid(person)?;
check_community_deleted_removed(community_id, pool).await?;
check_community_ban(person, community_id, pool).await?;
CommunityPersonBanView::check(pool, person.id, community_id).await?;
Ok(())
}
@ -210,28 +243,13 @@ async fn check_community_deleted_removed(
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let community = Community::read(pool, community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let community = Community::read(pool, community_id).await?;
if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)?
}
Ok(())
}
async fn check_community_ban(
person: &Person,
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
// check if user was banned from site or community
let is_banned = CommunityPersonBanView::get(pool, person.id, community_id).await?;
if is_banned {
Err(LemmyErrorType::BannedFromCommunity)?
}
Ok(())
}
/// Check that the given user can perform a mod action in the community.
///
/// In particular it checks that he is an admin or mod, wasn't banned and the community isn't
@ -243,7 +261,7 @@ pub async fn check_community_mod_action(
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
is_mod_or_admin(pool, person, community_id).await?;
check_community_ban(person, community_id, pool).await?;
CommunityPersonBanView::check(pool, person.id, community_id).await?;
// it must be possible to restore deleted community
if !allow_deleted {
@ -269,51 +287,6 @@ pub fn check_comment_deleted_or_removed(comment: &Comment) -> LemmyResult<()> {
}
}
/// Throws an error if a recipient has blocked a person.
#[tracing::instrument(skip_all)]
pub async fn check_person_block(
my_id: PersonId,
potential_blocker_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let is_blocked = PersonBlock::read(pool, potential_blocker_id, my_id).await?;
if is_blocked {
Err(LemmyErrorType::PersonIsBlocked)?
} else {
Ok(())
}
}
/// Throws an error if a recipient has blocked a community.
#[tracing::instrument(skip_all)]
async fn check_community_block(
community_id: CommunityId,
person_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let is_blocked = CommunityBlock::read(pool, person_id, community_id).await?;
if is_blocked {
Err(LemmyErrorType::CommunityIsBlocked)?
} else {
Ok(())
}
}
/// Throws an error if a recipient has blocked an instance.
#[tracing::instrument(skip_all)]
async fn check_instance_block(
instance_id: InstanceId,
person_id: PersonId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
let is_blocked = InstanceBlock::read(pool, person_id, instance_id).await?;
if is_blocked {
Err(LemmyErrorType::InstanceIsBlocked)?
} else {
Ok(())
}
}
#[tracing::instrument(skip_all)]
pub async fn check_person_instance_community_block(
my_id: PersonId,
@ -322,19 +295,42 @@ pub async fn check_person_instance_community_block(
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
check_person_block(my_id, potential_blocker_id, pool).await?;
check_instance_block(community_instance_id, potential_blocker_id, pool).await?;
check_community_block(community_id, potential_blocker_id, pool).await?;
PersonBlock::read(pool, potential_blocker_id, my_id).await?;
InstanceBlock::read(pool, potential_blocker_id, community_instance_id).await?;
CommunityBlock::read(pool, potential_blocker_id, community_id).await?;
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
@ -537,13 +533,6 @@ pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<Re
.unwrap_or(None)
}
pub fn local_site_opt_to_sensitive(local_site: &Option<LocalSite>) -> bool {
local_site
.as_ref()
.map(|site| site.enable_nsfw)
.unwrap_or(false)
}
pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet> {
static URL_BLOCKLIST: LazyLock<Cache<(), RegexSet>> = LazyLock::new(|| {
Cache::builder()
@ -667,7 +656,7 @@ pub async fn purge_image_posts_for_person(
/// Delete a local_user's images
async fn delete_local_user_images(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> {
if let Ok(Some(local_user)) = LocalUserView::read_person(&mut context.pool(), person_id).await {
if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), person_id).await {
let pictrs_uploads =
LocalImageView::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id)
.await?;
@ -706,106 +695,189 @@ 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();
// Purge user images
let person = Person::read(pool, banned_person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
if let Some(avatar) = person.avatar {
purge_image_from_pictrs(&avatar, context).await.ok();
}
if let Some(banner) = person.banner {
purge_image_from_pictrs(&banner, context).await.ok();
// 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 {
purge_image_from_pictrs(&avatar, context).await.ok();
}
if let Some(banner) = person.banner {
purge_image_from_pictrs(&banner, context).await.ok();
}
// Update the fields to None
Person::update(
pool,
banned_person_id,
&PersonUpdateForm {
avatar: Some(None),
banner: Some(None),
bio: Some(None),
..Default::default()
},
)
.await?;
// Purge image posts
purge_image_posts_for_person(banned_person_id, context).await?;
// Communities
// Remove all communities where they're the top mod
// for now, remove the communities manually
let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?;
// Filter to only this banned users top communities
let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
.into_iter()
.filter(|fmc| fmc.moderator.id == banned_person_id)
.collect();
for first_mod_community in banned_user_first_communities {
let community_id = first_mod_community.community.id;
Community::update(
pool,
community_id,
&CommunityUpdateForm {
removed: Some(removed),
..Default::default()
},
)
.await?;
// Delete the community images
if let Some(icon) = first_mod_community.community.icon {
purge_image_from_pictrs(&icon, context).await.ok();
}
if let Some(banner) = first_mod_community.community.banner {
purge_image_from_pictrs(&banner, context).await.ok();
}
// Update the fields to None
Community::update(
pool,
community_id,
&CommunityUpdateForm {
icon: Some(None),
banner: Some(None),
..Default::default()
},
)
.await?;
}
}
// Update the fields to None
Person::update(
// 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,
banned_person_id,
&PersonUpdateForm {
avatar: Some(None),
banner: Some(None),
bio: Some(None),
..Default::default()
},
mod_person_id,
removed_or_restored_posts.iter().map(|r| r.id).collect(),
removed,
reason,
)
.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?;
// Communities
// Remove all communities where they're the top mod
// for now, remove the communities manually
let first_mod_communities = CommunityModeratorView::get_community_first_mods(pool).await?;
// Filter to only this banned users top communities
let banned_user_first_communities: Vec<CommunityModeratorView> = first_mod_communities
.into_iter()
.filter(|fmc| fmc.moderator.id == banned_person_id)
.collect();
for first_mod_community in banned_user_first_communities {
let community_id = first_mod_community.community.id;
Community::update(
pool,
community_id,
&CommunityUpdateForm {
removed: Some(true),
..Default::default()
},
)
.await?;
// Delete the community images
if let Some(icon) = first_mod_community.community.icon {
purge_image_from_pictrs(&icon, context).await.ok();
}
if let Some(banner) = first_mod_community.community.banner {
purge_image_from_pictrs(&banner, context).await.ok();
}
// Update the fields to None
Community::update(
pool,
community_id,
&CommunityUpdateForm {
icon: Some(None),
banner: Some(None),
..Default::default()
},
)
.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(())
}
pub async fn remove_user_data_in_community(
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<()> {
// Build the forms
let forms = post_ids
.iter()
.map(|&post_id| ModRemovePostForm {
mod_person_id,
post_id,
removed: Some(removed),
reason: reason.clone(),
})
.collect();
ModRemovePost::create_multiple(pool, &forms).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
Post::update_removed_for_creator(pool, banned_person_id, Some(community_id), true).await?;
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
let site = Site::read_local(pool).await?;
let comments = CommentQuery {
creator_id: Some(banned_person_id),
community_id: Some(community_id),
..Default::default()
}
.list(pool)
.list(&site, pool)
.await?;
for comment_view in &comments {
@ -814,22 +886,29 @@ pub async fn remove_user_data_in_community(
pool,
comment_id,
&CommentUpdateForm {
removed: Some(true),
removed: Some(remove),
..Default::default()
},
)
.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(())
}
pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> {
let pool = &mut context.pool();
let person = Person::read(pool, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
let person = Person::read(pool, person_id).await?;
// Delete their local images, if they're a local user
delete_local_user_images(person_id, context).await.ok();
@ -858,6 +937,11 @@ pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) ->
// Leave communities they mod
CommunityModerator::leave_all_communities(pool, person_id).await?;
// Delete the oauth accounts linked to the local user
if let Ok(local_user) = LocalUserView::read_person(pool, person_id).await {
OAuthAccount::delete_user_accounts(pool, local_user.local_user.id).await?;
}
Person::delete_account(pool, person_id).await?;
Ok(())
@ -892,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())
}
@ -940,6 +1020,18 @@ fn limit_expire_time(expires: DateTime<Utc>) -> LemmyResult<Option<DateTime<Utc>
}
}
#[tracing::instrument(skip_all)]
pub fn check_conflicting_like_filters(
liked_only: Option<bool>,
disliked_only: Option<bool>,
) -> LemmyResult<()> {
if liked_only.unwrap_or_default() && disliked_only.unwrap_or_default() {
Err(LemmyErrorType::ContradictingFilters)?
} else {
Ok(())
}
}
pub async fn process_markdown(
text: &str,
slur_regex: &Option<Regex>,
@ -947,11 +1039,13 @@ pub async fn process_markdown(
context: &LemmyContext,
) -> LemmyResult<String> {
let text = remove_slurs(text, slur_regex);
let text = clean_urls_in_text(&text);
markdown_check_for_blocked_urls(&text, url_blocklist)?;
if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages {
let (text, links) = markdown_rewrite_image_links(text);
RemoteImage::create(&mut context.pool(), links.clone()).await?;
// Create images and image detail rows
for link in links {
@ -961,7 +1055,7 @@ pub async fn process_markdown(
let proxied =
build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
let details_form = details.build_image_details_form(&proxied);
RemoteImage::create(&mut context.pool(), &details_form).await?;
ImageDetails::create(&mut context.pool(), &details_form).await?;
}
}
Ok(text)
@ -997,13 +1091,15 @@ async fn proxy_image_link_internal(
if link.domain() == Some(&context.settings().hostname) {
Ok(link.into())
} else if image_mode == PictrsImageMode::ProxyAllImages {
RemoteImage::create(&mut context.pool(), vec![link.clone()]).await?;
let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
// This should fail softly, since pictrs might not even be running
let details_res = fetch_pictrs_proxied_image_details(&link, context).await;
if let Ok(details) = details_res {
let details_form = details.build_image_details_form(&proxied);
RemoteImage::create(&mut context.pool(), &details_form).await?;
ImageDetails::create(&mut context.pool(), &details_form).await?;
};
Ok(proxied.into())
@ -1071,11 +1167,20 @@ fn build_proxied_image_url(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
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;
@ -1097,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()
@ -1149,7 +1248,161 @@ mod tests {
assert!(
RemoteImage::validate(&mut context.pool(), remote_image.into())
.await
.is_err()
.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

@ -27,8 +27,12 @@ futures.workspace = true
uuid = { workspace = true }
moka.workspace = true
anyhow.workspace = true
webmention = "0.5.0"
chrono.workspace = true
webmention = "0.6.0"
accept-language = "3.1.0"
serde_json = { workspace = true }
serde = { workspace = true }
serde_with = { workspace = true }
[package.metadata.cargo-machete]
[package.metadata.cargo-shear]
ignored = ["futures"]

View file

@ -30,10 +30,9 @@ use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field},
MAX_COMMENT_DEPTH_LIMIT,
};
const MAX_COMMENT_DEPTH_LIMIT: usize = 100;
#[tracing::instrument(skip(context))]
pub async fn create_comment(
data: Json<CreateComment>,
@ -57,8 +56,7 @@ pub async fn create_comment(
Some(&local_user_view.local_user),
true,
)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
.await?;
let post = post_view.post;
let community_id = post_view.community.id;
@ -79,8 +77,7 @@ pub async fn create_comment(
Comment::read(&mut context.pool(), parent_id).await.ok()
} else {
None
}
.flatten();
};
// If there's a parent_id, check to make sure that comment is in that post
// Strange issue where sometimes the post ID of the parent comment is incorrect
@ -91,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(),
@ -111,12 +101,13 @@ pub async fn create_comment(
}
};
let comment_form = CommentInsertForm::builder()
.content(content.clone())
.post_id(data.post_id)
.creator_id(local_user_view.person.id)
.language_id(language_id)
.build();
CommunityLanguage::is_allowed_community_language(&mut context.pool(), language_id, community_id)
.await?;
let comment_form = CommentInsertForm {
language_id: Some(language_id),
..CommentInsertForm::new(local_user_view.person.id, data.post_id, content.clone())
};
// Create the comment
let parent_path = parent_opt.clone().map(|t| t.path);
@ -141,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

@ -26,8 +26,7 @@ pub async fn delete_comment(
comment_id,
Some(&local_user_view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
// Dont delete it if its already been deleted.
if orig_comment.comment.deleted == data.deleted {

View file

@ -31,8 +31,7 @@ pub async fn remove_comment(
comment_id,
Some(&local_user_view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
check_community_mod_action(
&local_user_view.person,

View file

@ -41,8 +41,7 @@ pub async fn update_comment(
comment_id,
Some(&local_user_view.local_user),
)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
.await?;
check_community_user_action(
&local_user_view.person,
@ -56,13 +55,14 @@ pub async fn update_comment(
Err(LemmyErrorType::NoCommentEditAllowed)?
}
let language_id = data.language_id;
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
language_id,
orig_comment.community.id,
)
.await?;
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,
@ -37,7 +36,11 @@ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{
slurs::check_slurs,
validation::{is_valid_actor_name, is_valid_body_field},
validation::{
is_valid_actor_name,
is_valid_body_field,
site_or_community_description_length_check,
},
},
};
@ -47,9 +50,7 @@ pub async fn create_community(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> {
let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let local_site = site_view.local_site;
if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() {
@ -60,8 +61,18 @@ pub async fn create_community(
let url_blocklist = get_url_blocklist(&context).await?;
check_slurs(&data.name, &slur_regex)?;
check_slurs(&data.title, &slur_regex)?;
let description =
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?;
let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?;
// Ensure that the sidebar has fewer than the max num characters...
if let Some(sidebar) = &sidebar {
is_valid_body_field(sidebar, false)?;
}
let description = data.description.clone();
if let Some(desc) = &description {
site_or_community_description_length_check(desc)?;
check_slurs(desc, &slur_regex)?;
}
let icon = diesel_url_create(data.icon.as_deref())?;
let icon = proxy_image_link_api(icon, &context).await?;
@ -71,10 +82,6 @@ pub async fn create_community(
is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?;
if let Some(desc) = &data.description {
is_valid_body_field(desc, false)?;
}
// Double check for duplicate community actor_ids
let community_actor_id = generate_local_apub_endpoint(
EndpointType::Community,
@ -90,23 +97,25 @@ pub async fn create_community(
// When you create a community, make sure the user becomes a moderator and a follower
let keypair = generate_actor_keypair()?;
let community_form = CommunityInsertForm::builder()
.name(data.name.clone())
.title(data.title.clone())
.description(description)
.icon(icon)
.banner(banner)
.nsfw(data.nsfw)
.actor_id(Some(community_actor_id.clone()))
.private_key(Some(keypair.private_key))
.public_key(keypair.public_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())?))
.posting_restricted_to_mods(data.posting_restricted_to_mods)
.instance_id(site_view.site.instance_id)
.visibility(data.visibility)
.build();
let community_form = CommunityInsertForm {
sidebar,
description,
icon,
banner,
nsfw: data.nsfw,
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()?),
posting_restricted_to_mods: data.posting_restricted_to_mods,
visibility: data.visibility,
..CommunityInsertForm::new(
site_view.site.instance_id,
data.name.clone(),
data.title.clone(),
keypair.public_key,
)
};
let inserted_community = Community::create(&mut context.pool(), &community_form)
.await

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_db_views_actor::community_view::CommunityQuery;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn list_communities(
@ -14,9 +14,7 @@ pub async fn list_communities(
context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<ListCommunitiesResponse>> {
let local_site = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let local_site = SiteView::read_local(&mut context.pool()).await?;
let is_admin = local_user_view
.as_ref()
.map(|luv| is_admin(luv).is_ok())

View file

@ -41,19 +41,19 @@ pub async fn update_community(
let url_blocklist = get_url_blocklist(&context).await?;
check_slurs_opt(&data.title, &slur_regex)?;
let description = diesel_string_update(
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context)
let sidebar = diesel_string_update(
process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context)
.await?
.as_deref(),
);
if let Some(Some(desc)) = &description {
is_valid_body_field(desc, false)?;
if let Some(Some(sidebar)) = &sidebar {
is_valid_body_field(sidebar, false)?;
}
let old_community = Community::read(&mut context.pool(), data.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let description = diesel_string_update(data.description.as_deref());
let old_community = Community::read(&mut context.pool(), data.community_id).await?;
let icon = diesel_url_update(data.icon.as_deref())?;
replace_image(&icon, &old_community.icon, &context).await?;
@ -86,6 +86,7 @@ pub async fn update_community(
let community_form = CommunityUpdateForm {
title: data.title.clone(),
sidebar,
description,
icon,
banner,

View file

@ -5,10 +5,12 @@ use lemmy_api_common::{
custom_emoji::{CreateCustomEmoji, CustomEmojiResponse},
utils::is_admin,
};
use lemmy_db_schema::source::{
custom_emoji::{CustomEmoji, CustomEmojiInsertForm},
custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm},
local_site::LocalSite,
use lemmy_db_schema::{
source::{
custom_emoji::{CustomEmoji, CustomEmojiInsertForm},
custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm},
},
traits::Crud,
};
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView};
use lemmy_utils::error::LemmyResult;
@ -19,24 +21,20 @@ pub async fn create_custom_emoji(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CustomEmojiResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?;
// Make sure user is an admin
is_admin(&local_user_view)?;
let emoji_form = CustomEmojiInsertForm::builder()
.local_site_id(local_site.id)
.shortcode(data.shortcode.to_lowercase().trim().to_string())
.alt_text(data.alt_text.to_string())
.category(data.category.to_string())
.image_url(data.clone().image_url.into())
.build();
let emoji_form = CustomEmojiInsertForm::new(
data.shortcode.to_lowercase().trim().to_string(),
data.clone().image_url.into(),
data.alt_text.to_string(),
data.category.to_string(),
);
let emoji = CustomEmoji::create(&mut context.pool(), &emoji_form).await?;
let mut keywords = vec![];
for keyword in &data.keywords {
let keyword_form = CustomEmojiKeywordInsertForm::builder()
.custom_emoji_id(emoji.id)
.keyword(keyword.to_lowercase().trim().to_string())
.build();
let keyword_form =
CustomEmojiKeywordInsertForm::new(emoji.id, keyword.to_lowercase().trim().to_string());
keywords.push(keyword_form);
}
CustomEmojiKeyword::create(&mut context.pool(), keywords).await?;

View file

@ -6,7 +6,7 @@ use lemmy_api_common::{
utils::is_admin,
SuccessResponse,
};
use lemmy_db_schema::source::custom_emoji::CustomEmoji;
use lemmy_db_schema::{source::custom_emoji::CustomEmoji, traits::Crud};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;

View file

@ -0,0 +1,25 @@
use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
custom_emoji::{ListCustomEmojis, ListCustomEmojisResponse},
};
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView};
use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))]
pub async fn list_custom_emojis(
data: Query<ListCustomEmojis>,
local_user_view: Option<LocalUserView>,
context: Data<LemmyContext>,
) -> Result<Json<ListCustomEmojisResponse>, LemmyError> {
let custom_emojis = CustomEmojiView::list(
&mut context.pool(),
&data.category,
data.page,
data.limit,
data.ignore_page_limits.unwrap_or(false),
)
.await?;
Ok(Json(ListCustomEmojisResponse { custom_emojis }))
}

View file

@ -1,3 +1,4 @@
pub mod create;
pub mod delete;
pub mod list;
pub mod update;

View file

@ -5,10 +5,12 @@ use lemmy_api_common::{
custom_emoji::{CustomEmojiResponse, EditCustomEmoji},
utils::is_admin,
};
use lemmy_db_schema::source::{
custom_emoji::{CustomEmoji, CustomEmojiUpdateForm},
custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm},
local_site::LocalSite,
use lemmy_db_schema::{
source::{
custom_emoji::{CustomEmoji, CustomEmojiUpdateForm},
custom_emoji_keyword::{CustomEmojiKeyword, CustomEmojiKeywordInsertForm},
},
traits::Crud,
};
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView};
use lemmy_utils::error::LemmyResult;
@ -19,24 +21,20 @@ pub async fn update_custom_emoji(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> LemmyResult<Json<CustomEmojiResponse>> {
let local_site = LocalSite::read(&mut context.pool()).await?;
// Make sure user is an admin
is_admin(&local_user_view)?;
let emoji_form = CustomEmojiUpdateForm::builder()
.local_site_id(local_site.id)
.alt_text(data.alt_text.to_string())
.category(data.category.to_string())
.image_url(data.clone().image_url.into())
.build();
let emoji_form = CustomEmojiUpdateForm::new(
data.clone().image_url.into(),
data.alt_text.to_string(),
data.category.to_string(),
);
let emoji = CustomEmoji::update(&mut context.pool(), data.id, &emoji_form).await?;
CustomEmojiKeyword::delete(&mut context.pool(), data.id).await?;
let mut keywords = vec![];
for keyword in &data.keywords {
let keyword_form = CustomEmojiKeywordInsertForm::builder()
.custom_emoji_id(emoji.id)
.keyword(keyword.to_lowercase().trim().to_string())
.build();
let keyword_form =
CustomEmojiKeywordInsertForm::new(emoji.id, keyword.to_lowercase().trim().to_string());
keywords.push(keyword_form);
}
CustomEmojiKeyword::create(&mut context.pool(), keywords).await?;

View file

@ -1,7 +1,9 @@
pub mod comment;
pub mod community;
pub mod custom_emoji;
pub mod oauth_provider;
pub mod post;
pub mod private_message;
pub mod site;
pub mod tagline;
pub mod user;

View file

@ -0,0 +1,42 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
oauth_provider::CreateOAuthProvider,
utils::is_admin,
};
use lemmy_db_schema::{
source::oauth_provider::{OAuthProvider, OAuthProviderInsertForm},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
use url::Url;
#[tracing::instrument(skip(context))]
pub async fn create_oauth_provider(
data: Json<CreateOAuthProvider>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<OAuthProvider>, LemmyError> {
// Make sure user is an admin
is_admin(&local_user_view)?;
let cloned_data = data.clone();
let oauth_provider_form = OAuthProviderInsertForm {
display_name: cloned_data.display_name,
issuer: Url::parse(&cloned_data.issuer)?.into(),
authorization_endpoint: Url::parse(&cloned_data.authorization_endpoint)?.into(),
token_endpoint: Url::parse(&cloned_data.token_endpoint)?.into(),
userinfo_endpoint: Url::parse(&cloned_data.userinfo_endpoint)?.into(),
id_claim: cloned_data.id_claim,
client_id: data.client_id.to_string(),
client_secret: data.client_secret.to_string(),
scopes: data.scopes.to_string(),
auto_verify_email: data.auto_verify_email,
account_linking_enabled: data.account_linking_enabled,
enabled: data.enabled,
};
let oauth_provider = OAuthProvider::create(&mut context.pool(), &oauth_provider_form).await?;
Ok(Json(oauth_provider))
}

View file

@ -0,0 +1,25 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
oauth_provider::DeleteOAuthProvider,
utils::is_admin,
SuccessResponse,
};
use lemmy_db_schema::{source::oauth_provider::OAuthProvider, traits::Crud};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))]
pub async fn delete_oauth_provider(
data: Json<DeleteOAuthProvider>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<SuccessResponse>, LemmyError> {
// Make sure user is an admin
is_admin(&local_user_view)?;
OAuthProvider::delete(&mut context.pool(), data.id)
.await
.with_lemmy_type(LemmyErrorType::CouldntDeleteOauthProvider)?;
Ok(Json(SuccessResponse::default()))
}

View file

@ -0,0 +1,3 @@
pub mod create;
pub mod delete;
pub mod update;

View file

@ -0,0 +1,42 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{context::LemmyContext, oauth_provider::EditOAuthProvider, utils::is_admin};
use lemmy_db_schema::{
source::oauth_provider::{OAuthProvider, OAuthProviderUpdateForm},
traits::Crud,
utils::{diesel_required_string_update, diesel_required_url_update, naive_now},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
#[tracing::instrument(skip(context))]
pub async fn update_oauth_provider(
data: Json<EditOAuthProvider>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<OAuthProvider>, LemmyError> {
// Make sure user is an admin
is_admin(&local_user_view)?;
let cloned_data = data.clone();
let oauth_provider_form = OAuthProviderUpdateForm {
display_name: diesel_required_string_update(cloned_data.display_name.as_deref()),
authorization_endpoint: diesel_required_url_update(
cloned_data.authorization_endpoint.as_deref(),
)?,
token_endpoint: diesel_required_url_update(cloned_data.token_endpoint.as_deref())?,
userinfo_endpoint: diesel_required_url_update(cloned_data.userinfo_endpoint.as_deref())?,
id_claim: diesel_required_string_update(data.id_claim.as_deref()),
client_secret: diesel_required_string_update(data.client_secret.as_deref()),
scopes: diesel_required_string_update(data.scopes.as_deref()),
auto_verify_email: data.auto_verify_email,
account_linking_enabled: data.account_linking_enabled,
enabled: data.enabled,
updated: Some(Some(naive_now())),
};
let update_result =
OAuthProvider::update(&mut context.pool(), data.id, &oauth_provider_form).await?;
let oauth_provider = OAuthProvider::read(&mut context.pool(), update_result.id).await?;
Ok(Json(oauth_provider))
}

View file

@ -1,3 +1,4 @@
use super::convert_published_time;
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
@ -92,34 +93,20 @@ pub async fn create_post(
.await?;
let community_id = data.community_id;
let community = Community::read(&mut context.pool(), community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let community = Community::read(&mut context.pool(), community_id).await?;
if community.posting_restricted_to_mods {
let community_id = data.community_id;
let is_mod = CommunityModeratorView::is_community_moderator(
CommunityModeratorView::check_is_community_moderator(
&mut context.pool(),
community_id,
local_user_view.local_user.person_id,
)
.await?;
if !is_mod {
Err(LemmyErrorType::OnlyModsCanPostInCommunity)?
}
}
// 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(),
@ -130,26 +117,41 @@ pub async fn create_post(
}
};
let post_form = PostInsertForm::builder()
.name(data.name.trim().to_string())
.url(url.map(Into::into))
.body(body)
.alt_text(data.alt_text.clone())
.community_id(data.community_id)
.creator_id(local_user_view.person.id)
.nsfw(data.nsfw)
.language_id(language_id)
.build();
// 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 {
url: url.map(Into::into),
body,
alt_text: data.alt_text.clone(),
nsfw: data.nsfw,
language_id: Some(language_id),
scheduled_publish_time,
..PostInsertForm::new(
data.name.trim().to_string(),
local_user_view.person.id,
data.community_id,
)
};
let inserted_post = Post::create(&mut context.pool(), &post_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
let federate_post = if scheduled_publish_time.is_none() {
send_webmention(inserted_post.clone(), community);
|post| Some(SendActivityData::CreatePost(post))
} else {
|_| None
};
generate_post_link_metadata(
inserted_post.clone(),
custom_thumbnail.map(Into::into),
|post| Some(SendActivityData::CreatePost(post)),
Some(local_site),
federate_post,
context.reset_request_count(),
)
.await?;
@ -169,11 +171,14 @@ pub async fn create_post(
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
if let Some(url) = inserted_post.url.clone() {
build_post_response(&context, community_id, local_user_view, post_id).await
}
pub fn send_webmention(post: Post, community: Community) {
if let Some(url) = post.url.clone() {
if community.visibility == CommunityVisibility::Public {
spawn_try_task(async move {
let mut webmention =
Webmention::new::<Url>(inserted_post.ap_id.clone().into(), url.clone().into())?;
let mut webmention = Webmention::new::<Url>(post.ap_id.clone().into(), url.clone().into())?;
webmention.set_checked(true);
match webmention
.send()
@ -187,6 +192,4 @@ pub async fn create_post(
});
}
};
build_post_response(&context, community_id, local_user_view, post_id).await
}

View file

@ -21,9 +21,7 @@ pub async fn delete_post(
local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
// Dont delete it if its already been deleted.
if orig_post.deleted == data.deleted {

View file

@ -1,5 +1,38 @@
use chrono::{DateTime, TimeZone, Utc};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::post::Post;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
pub mod create;
pub mod delete;
pub mod read;
pub mod remove;
pub mod update;
async fn convert_published_time(
scheduled_publish_time: Option<i64>,
local_user_view: &LocalUserView,
context: &LemmyContext,
) -> LemmyResult<Option<DateTime<Utc>>> {
const MAX_SCHEDULED_POSTS: i64 = 10;
if let Some(scheduled_publish_time) = scheduled_publish_time {
let converted = Utc
.timestamp_opt(scheduled_publish_time, 0)
.single()
.ok_or(LemmyErrorType::InvalidUnixTime)?;
if converted < Utc::now() {
Err(LemmyErrorType::PostScheduleTimeMustBeInFuture)?;
}
if !local_user_view.local_user.admin {
let count =
Post::user_scheduled_post_count(local_user_view.person.id, &mut context.pool()).await?;
if count >= MAX_SCHEDULED_POSTS {
Err(LemmyErrorType::TooManyScheduledPosts)?;
}
}
Ok(Some(converted))
} else {
Ok(None)
}
}

View file

@ -21,9 +21,7 @@ pub async fn get_post(
context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<GetPostResponse>> {
let local_site = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let local_site = SiteView::read_local(&mut context.pool()).await?;
check_private_instance(&local_user_view, &local_site.local_site)?;
@ -35,16 +33,14 @@ pub async fn get_post(
} else if let Some(comment_id) = data.comment_id {
Comment::read(&mut context.pool(), comment_id)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?
.post_id
} else {
Err(LemmyErrorType::CouldntFindPost)?
Err(LemmyErrorType::NotFound)?
};
// Check to see if the person is a mod or admin, to show deleted / removed
let community_id = Post::read(&mut context.pool(), post_id)
let community_id = Post::read_xx(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?
.community_id;
let is_mod_or_admin = is_mod_or_admin_opt(
@ -62,8 +58,7 @@ pub async fn get_post(
local_user.as_ref(),
is_mod_or_admin,
)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
.await?;
let post_id = post_view.post.id;
if let Some(person_id) = person_id {
@ -85,15 +80,15 @@ pub async fn get_post(
local_user.as_ref(),
is_mod_or_admin,
)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
.await?;
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
// Fetch the cross_posts
let cross_posts = if let Some(url) = &post_view.post.url {
let mut x_posts = PostQuery {
url_search: Some(url.inner().as_str().into()),
url_only: Some(true),
search_term: Some(url.inner().as_str().into()),
local_user: local_user.as_ref(),
..Default::default()
}

View file

@ -17,7 +17,7 @@ use lemmy_db_schema::{
traits::{Crud, Reportable},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn remove_post(
@ -26,9 +26,7 @@ pub async fn remove_post(
local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
check_community_mod_action(
&local_user_view.person,

View file

@ -1,3 +1,4 @@
use super::{convert_published_time, create::send_webmention};
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
@ -16,6 +17,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
actor_language::CommunityLanguage,
community::Community,
local_site::LocalSite,
post::{Post, PostUpdateForm},
},
@ -85,9 +87,7 @@ pub async fn update_post(
}
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
check_community_user_action(
&local_user_view.person,
@ -101,13 +101,29 @@ pub async fn update_post(
Err(LemmyErrorType::NoPostEditAllowed)?
}
let language_id = data.language_id;
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
language_id,
orig_post.community_id,
)
.await?;
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 (
orig_post.scheduled_publish_time,
data.scheduled_publish_time,
) {
// schedule time can be changed if post is still scheduled (and not published yet)
(Some(_), Some(_)) => {
Some(convert_published_time(data.scheduled_publish_time, &local_user_view, &context).await?)
}
// post was scheduled, gets changed to publish immediately
(Some(_), None) => Some(None),
// unchanged
(_, _) => None,
};
let post_form = PostUpdateForm {
name: data.name.clone(),
@ -117,6 +133,7 @@ pub async fn update_post(
nsfw: data.nsfw,
language_id: data.language_id,
updated: Some(Some(naive_now())),
scheduled_publish_time,
..Default::default()
};
@ -125,14 +142,36 @@ pub async fn update_post(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePost)?;
generate_post_link_metadata(
updated_post.clone(),
custom_thumbnail.flatten().map(Into::into),
|post| Some(SendActivityData::UpdatePost(post)),
Some(local_site),
context.reset_request_count(),
)
.await?;
// send out federation/webmention if necessary
match (
orig_post.scheduled_publish_time,
data.scheduled_publish_time,
) {
// schedule was removed, send create activity and webmention
(Some(_), None) => {
let community = Community::read(&mut context.pool(), orig_post.community_id).await?;
send_webmention(updated_post.clone(), community);
generate_post_link_metadata(
updated_post.clone(),
custom_thumbnail.flatten().map(Into::into),
|post| Some(SendActivityData::CreatePost(post)),
context.reset_request_count(),
)
.await?;
}
// post was already public, send update
(None, _) => {
generate_post_link_metadata(
updated_post.clone(),
custom_thumbnail.flatten().map(Into::into),
|post| Some(SendActivityData::UpdatePost(post)),
context.reset_request_count(),
)
.await?
}
// schedule was changed, do nothing
(Some(_), Some(_)) => {}
};
build_post_response(
context.deref(),

View file

@ -5,7 +5,6 @@ use lemmy_api_common::{
private_message::{CreatePrivateMessage, PrivateMessageResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_person_block,
get_interface_language,
get_url_blocklist,
local_site_to_slur_regex,
@ -16,6 +15,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
local_site::LocalSite,
person_block::PersonBlock,
private_message::{PrivateMessage, PrivateMessageInsertForm},
},
traits::Crud,
@ -39,33 +39,29 @@ pub async fn create_private_message(
let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&content, false)?;
check_person_block(
local_user_view.person.id,
data.recipient_id,
PersonBlock::read(
&mut context.pool(),
data.recipient_id,
local_user_view.person.id,
)
.await?;
let private_message_form = PrivateMessageInsertForm::builder()
.content(content.clone())
.creator_id(local_user_view.person.id)
.recipient_id(data.recipient_id)
.build();
let private_message_form = PrivateMessageInsertForm::new(
local_user_view.person.id,
data.recipient_id,
content.clone(),
);
let inserted_private_message = PrivateMessage::create(&mut context.pool(), &private_message_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id).await?;
// Send email to the local recipient, if one exists
if view.recipient.local {
let recipient_id = data.recipient_id;
let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id).await?;
let lang = get_interface_language(&local_recipient);
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
let sender_name = &local_user_view.person.name;

View file

@ -20,9 +20,7 @@ pub async fn delete_private_message(
) -> LemmyResult<Json<PrivateMessageResponse>> {
// Checking permissions
let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
if local_user_view.person.id != orig_private_message.creator_id {
Err(LemmyErrorType::EditPrivateMessageNotAllowed)?
}
@ -47,9 +45,7 @@ pub async fn delete_private_message(
)
.await?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
Ok(Json(PrivateMessageResponse {
private_message_view: view,
}))

View file

@ -30,9 +30,7 @@ pub async fn update_private_message(
// Checking permissions
let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?;
if local_user_view.person.id != orig_private_message.creator_id {
Err(LemmyErrorType::EditPrivateMessageNotAllowed)?
}
@ -56,9 +54,7 @@ pub async fn update_private_message(
.await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?;
ActivityChannel::submit_activity(
SendActivityData::UpdatePrivateMessage(view.clone()),

View file

@ -1,3 +1,4 @@
use super::not_zero;
use crate::site::{application_question_check, site_default_post_listing_type_check};
use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair};
use actix_web::web::Json;
@ -5,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,
@ -20,7 +21,6 @@ use lemmy_db_schema::{
local_site::{LocalSite, LocalSiteUpdateForm},
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
site::{Site, SiteUpdateForm},
tagline::Tagline,
},
traits::Crud,
utils::{diesel_string_update, diesel_url_create, naive_now},
@ -29,13 +29,13 @@ use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::{
error::{LemmyErrorType, LemmyResult},
utils::{
slurs::{check_slurs, check_slurs_opt},
slurs::check_slurs,
validation::{
build_and_check_regex,
check_site_visibility_valid,
is_valid_body_field,
site_description_length_check,
site_name_length_check,
site_or_community_description_length_check,
},
},
};
@ -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,16 +90,15 @@ 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,
enable_nsfw: data.enable_nsfw,
community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification,
application_question: diesel_string_update(data.application_question.as_deref()),
private_instance: data.private_instance,
default_theme: data.default_theme.clone(),
default_post_listing_type: data.default_post_listing_type,
default_sort_type: data.default_sort_type,
default_post_sort_type: data.default_post_sort_type,
default_comment_sort_type: data.default_comment_sort_type,
legal_information: diesel_string_update(data.legal_information.as_deref()),
application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names,
@ -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()
};
@ -117,28 +120,23 @@ pub async fn create_site(
let local_site_rate_limit_form = LocalSiteRateLimitUpdateForm {
message: data.rate_limit_message,
message_per_second: data.rate_limit_message_per_second,
message_per_second: not_zero(data.rate_limit_message_per_second),
post: data.rate_limit_post,
post_per_second: data.rate_limit_post_per_second,
post_per_second: not_zero(data.rate_limit_post_per_second),
register: data.rate_limit_register,
register_per_second: data.rate_limit_register_per_second,
register_per_second: not_zero(data.rate_limit_register_per_second),
image: data.rate_limit_image,
image_per_second: data.rate_limit_image_per_second,
image_per_second: not_zero(data.rate_limit_image_per_second),
comment: data.rate_limit_comment,
comment_per_second: data.rate_limit_comment_per_second,
comment_per_second: not_zero(data.rate_limit_comment_per_second),
search: data.rate_limit_search,
search_per_second: data.rate_limit_search_per_second,
search_per_second: not_zero(data.rate_limit_search_per_second),
..Default::default()
};
LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?;
let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let new_taglines = data.taglines.clone();
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?;
let rate_limit_config =
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
@ -146,7 +144,7 @@ pub async fn create_site(
Ok(Json(SiteResponse {
site_view,
taglines,
taglines: vec![],
}))
}
@ -169,8 +167,8 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) ->
check_slurs(&create_site.name, &slur_regex)?;
if let Some(desc) = &create_site.description {
site_description_length_check(desc)?;
check_slurs_opt(&create_site.description, &slur_regex)?;
site_or_community_description_length_check(desc)?;
check_slurs(desc, &slur_regex)?;
}
site_default_post_listing_type_check(&create_site.default_post_listing_type)?;
@ -197,13 +195,16 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) ->
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
use crate::site::create::validate_create_payload;
use lemmy_api_common::site::CreateSite;
use lemmy_db_schema::{source::local_site::LocalSite, ListingType, RegistrationMode, SortType};
use lemmy_db_schema::{
source::local_site::LocalSite,
ListingType,
PostSortType,
RegistrationMode,
};
use lemmy_utils::error::LemmyErrorType;
#[test]
@ -212,170 +213,114 @@ mod tests {
(
"CreateSite attempted on set up LocalSite",
LemmyErrorType::SiteAlreadyExists,
&generate_local_site(
true,
None::<String>,
true,
false,
None::<String>,
RegistrationMode::Open,
),
&generate_create_site(
String::from("site_name"),
None::<String>,
None::<String>,
None::<ListingType>,
None::<SortType>,
None::<String>,
None::<bool>,
None::<bool>,
None::<String>,
None::<RegistrationMode>,
),
&LocalSite {
site_setup: true,
private_instance: true,
federation_enabled: false,
registration_mode: RegistrationMode::Open,
..Default::default()
},
&CreateSite {
name: String::from("site_name"),
..Default::default()
},
),
(
"CreateSite name matches LocalSite slur filter",
LemmyErrorType::Slurs,
&generate_local_site(
false,
Some(String::from("(foo|bar)")),
true,
false,
None::<String>,
RegistrationMode::Open,
),
&generate_create_site(
String::from("foo site_name"),
None::<String>,
None::<String>,
None::<ListingType>,
None::<SortType>,
None::<String>,
None::<bool>,
None::<bool>,
None::<String>,
None::<RegistrationMode>,
),
&LocalSite {
site_setup: false,
private_instance: true,
slur_filter_regex: Some(String::from("(foo|bar)")),
federation_enabled: false,
registration_mode: RegistrationMode::Open,
..Default::default()
},
&CreateSite {
name: String::from("foo site_name"),
..Default::default()
},
),
(
"CreateSite name matches new slur filter",
LemmyErrorType::Slurs,
&generate_local_site(
false,
Some(String::from("(foo|bar)")),
true,
false,
None::<String>,
RegistrationMode::Open,
),
&generate_create_site(
String::from("zeta site_name"),
None::<String>,
None::<String>,
None::<ListingType>,
None::<SortType>,
Some(String::from("(zeta|alpha)")),
None::<bool>,
None::<bool>,
None::<String>,
None::<RegistrationMode>,
),
&LocalSite {
site_setup: false,
private_instance: true,
slur_filter_regex: Some(String::from("(foo|bar)")),
federation_enabled: false,
registration_mode: RegistrationMode::Open,
..Default::default()
},
&CreateSite {
name: String::from("zeta site_name"),
slur_filter_regex: Some(String::from("(zeta|alpha)")),
..Default::default()
},
),
(
"CreateSite listing type is Subscribed, which is invalid",
LemmyErrorType::InvalidDefaultPostListingType,
&generate_local_site(
false,
None::<String>,
true,
false,
None::<String>,
RegistrationMode::Open,
),
&generate_create_site(
String::from("site_name"),
None::<String>,
None::<String>,
Some(ListingType::Subscribed),
None::<SortType>,
None::<String>,
None::<bool>,
None::<bool>,
None::<String>,
None::<RegistrationMode>,
),
&LocalSite {
site_setup: false,
private_instance: true,
federation_enabled: false,
registration_mode: RegistrationMode::Open,
..Default::default()
},
&CreateSite {
name: String::from("site_name"),
default_post_listing_type: Some(ListingType::Subscribed),
..Default::default()
},
),
(
"CreateSite is both private and federated",
LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
&generate_local_site(
false,
None::<String>,
true,
false,
None::<String>,
RegistrationMode::Open,
),
&generate_create_site(
String::from("site_name"),
None::<String>,
None::<String>,
None::<ListingType>,
None::<SortType>,
None::<String>,
Some(true),
Some(true),
None::<String>,
None::<RegistrationMode>,
),
&LocalSite {
site_setup: false,
private_instance: true,
federation_enabled: false,
..Default::default()
},
&CreateSite {
name: String::from("site_name"),
private_instance: Some(true),
federation_enabled: Some(true),
..Default::default()
},
),
(
"LocalSite is private, but CreateSite also makes it federated",
LemmyErrorType::CantEnablePrivateInstanceAndFederationTogether,
&generate_local_site(
false,
None::<String>,
true,
false,
None::<String>,
RegistrationMode::Open,
),
&generate_create_site(
String::from("site_name"),
None::<String>,
None::<String>,
None::<ListingType>,
None::<SortType>,
None::<String>,
None::<bool>,
Some(true),
None::<String>,
None::<RegistrationMode>,
),
&LocalSite {
site_setup: false,
private_instance: true,
federation_enabled: false,
registration_mode: RegistrationMode::Open,
..Default::default()
},
&CreateSite {
name: String::from("site_name"),
federation_enabled: Some(true),
..Default::default()
},
),
(
"CreateSite requires application, but neither it nor LocalSite has an application question",
LemmyErrorType::ApplicationQuestionRequired,
&generate_local_site(
false,
None::<String>,
true,
false,
None::<String>,
RegistrationMode::Open,
),
&generate_create_site(
String::from("site_name"),
None::<String>,
None::<String>,
None::<ListingType>,
None::<SortType>,
None::<String>,
None::<bool>,
None::<bool>,
None::<String>,
Some(RegistrationMode::RequireApplication),
),
&LocalSite {
site_setup: false,
private_instance: true,
federation_enabled: false,
registration_mode: RegistrationMode::Open,
..Default::default()
},
&CreateSite {
name: String::from("site_name"),
registration_mode: Some(RegistrationMode::RequireApplication),
..Default::default()
},
),
];
@ -414,95 +359,72 @@ mod tests {
let valid_payloads = [
(
"No changes between LocalSite and CreateSite",
&generate_local_site(
false,
None::<String>,
true,
false,
None::<String>,
RegistrationMode::Open,
),
&generate_create_site(
String::from("site_name"),
None::<String>,
None::<String>,
None::<ListingType>,
None::<SortType>,
None::<String>,
None::<bool>,
None::<bool>,
None::<String>,
None::<RegistrationMode>,
),
&LocalSite {
site_setup: false,
private_instance: true,
federation_enabled: false,
registration_mode: RegistrationMode::Open,
..Default::default()
},
&CreateSite {
name: String::from("site_name"),
..Default::default()
},
),
(
"CreateSite allows clearing and changing values",
&generate_local_site(
false,
None::<String>,
true,
false,
None::<String>,
RegistrationMode::Open,
),
&generate_create_site(
String::from("site_name"),
Some(String::new()),
Some(String::new()),
Some(ListingType::All),
Some(SortType::Active),
Some(String::new()),
Some(false),
Some(true),
Some(String::new()),
Some(RegistrationMode::Open),
),
&LocalSite {
site_setup: false,
private_instance: true,
federation_enabled: false,
registration_mode: RegistrationMode::Open,
..Default::default()
},
&CreateSite {
name: String::from("site_name"),
sidebar: Some(String::new()),
description: Some(String::new()),
application_question: Some(String::new()),
private_instance: Some(false),
default_post_listing_type: Some(ListingType::All),
default_post_sort_type: Some(PostSortType::Active),
slur_filter_regex: Some(String::new()),
federation_enabled: Some(true),
registration_mode: Some(RegistrationMode::Open),
..Default::default()
},
),
(
"CreateSite clears existing slur filter regex",
&generate_local_site(
false,
Some(String::from("(foo|bar)")),
true,
false,
None::<String>,
RegistrationMode::Open,
),
&generate_create_site(
String::from("foo site_name"),
None::<String>,
None::<String>,
None::<ListingType>,
None::<SortType>,
Some(String::new()),
None::<bool>,
None::<bool>,
None::<String>,
None::<RegistrationMode>,
),
&LocalSite {
site_setup: false,
private_instance: true,
slur_filter_regex: Some(String::from("(foo|bar)")),
federation_enabled: false,
registration_mode: RegistrationMode::Open,
..Default::default()
},
&CreateSite {
name: String::from("foo site_name"),
slur_filter_regex: Some(String::new()),
..Default::default()
},
),
(
"LocalSite has application question and CreateSite now requires applications,",
&generate_local_site(
false,
None::<String>,
true,
false,
Some(String::from("question")),
RegistrationMode::Open,
),
&generate_create_site(
String::from("site_name"),
None::<String>,
None::<String>,
None::<ListingType>,
None::<SortType>,
None::<String>,
None::<bool>,
None::<bool>,
None::<String>,
Some(RegistrationMode::RequireApplication),
),
&LocalSite {
site_setup: false,
application_question: Some(String::from("question")),
private_instance: true,
federation_enabled: false,
registration_mode: RegistrationMode::Open,
..Default::default()
},
&CreateSite {
name: String::from("site_name"),
registration_mode: Some(RegistrationMode::RequireApplication),
..Default::default()
},
),
];
@ -518,84 +440,4 @@ mod tests {
);
})
}
fn generate_local_site(
site_setup: bool,
site_slur_filter_regex: Option<String>,
site_is_private: bool,
site_is_federated: bool,
site_application_question: Option<String>,
site_registration_mode: RegistrationMode,
) -> LocalSite {
LocalSite {
site_setup,
application_question: site_application_question,
private_instance: site_is_private,
slur_filter_regex: site_slur_filter_regex,
federation_enabled: site_is_federated,
registration_mode: site_registration_mode,
..Default::default()
}
}
// Allow the test helper function to have too many arguments.
// It's either this or generate the entire struct each time for testing.
#[allow(clippy::too_many_arguments)]
fn generate_create_site(
site_name: String,
site_description: Option<String>,
site_sidebar: Option<String>,
site_listing_type: Option<ListingType>,
site_sort_type: Option<SortType>,
site_slur_filter_regex: Option<String>,
site_is_private: Option<bool>,
site_is_federated: Option<bool>,
site_application_question: Option<String>,
site_registration_mode: Option<RegistrationMode>,
) -> CreateSite {
CreateSite {
name: site_name,
sidebar: site_sidebar,
description: site_description,
icon: None,
banner: None,
enable_downvotes: None,
enable_nsfw: None,
community_creation_admin_only: None,
require_email_verification: None,
application_question: site_application_question,
private_instance: site_is_private,
default_theme: None,
default_post_listing_type: site_listing_type,
default_sort_type: site_sort_type,
legal_information: None,
application_email_admins: None,
hide_modlog_mod_names: None,
discussion_languages: None,
slur_filter_regex: site_slur_filter_regex,
actor_name_max_length: None,
rate_limit_message: None,
rate_limit_message_per_second: None,
rate_limit_post: None,
rate_limit_post_per_second: None,
rate_limit_register: None,
rate_limit_register_per_second: None,
rate_limit_image: None,
rate_limit_image_per_second: None,
rate_limit_comment: None,
rate_limit_comment_per_second: None,
rate_limit_search: None,
rate_limit_search_per_second: None,
federation_enabled: site_is_federated,
federation_debug: None,
captcha_enabled: None,
captcha_difficulty: None,
allowed_instances: None,
blocked_instances: None,
taglines: None,
registration_mode: site_registration_mode,
content_warning: None,
default_post_listing_mode: None,
}
}
}

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