Merge branch 'main' into markdown-link-rule

cleanup-request-rs
Felix Ableitner 2023-10-23 15:40:04 +02:00
commit 650e3a71d7
148 changed files with 3294 additions and 2239 deletions

View File

@ -2,7 +2,7 @@
# See https://github.com/woodpecker-ci/woodpecker/issues/1677
variables:
- &muslrust_image "clux/muslrust:1.70.0"
- &rust_image "rust:1.72.1"
- &slow_check_paths
- path:
# rust source code
@ -57,15 +57,13 @@ steps:
cargo_fmt:
group: format
image: *muslrust_image
image: rustlang/rust:nightly
environment:
# store cargo data in repo folder so that it gets cached between steps
CARGO_HOME: .cargo
commands:
# need make existing toolchain available
- cp -n ~/.cargo . -r
- rustup toolchain install nightly-2023-07-10 --no-self-update --profile minimal --component rustfmt
- cargo +nightly-2023-07-10 fmt -- --check
- cargo +nightly fmt -- --check
restore-cache:
image: meltwater/drone-cache:v1
@ -93,7 +91,7 @@ steps:
# make sure api builds with default features (used by other crates relying on lemmy api)
check_api_common_default_features:
image: *muslrust_image
image: *rust_image
environment:
CARGO_HOME: .cargo
commands:
@ -101,7 +99,7 @@ steps:
when: *slow_check_paths
lemmy_api_common_doesnt_depend_on_diesel:
image: *muslrust_image
image: *rust_image
environment:
CARGO_HOME: .cargo
commands:
@ -109,7 +107,7 @@ steps:
when: *slow_check_paths
lemmy_api_common_works_with_wasm:
image: *muslrust_image
image: *rust_image
environment:
CARGO_HOME: .cargo
commands:
@ -118,7 +116,7 @@ steps:
when: *slow_check_paths
check_defaults_hjson_updated:
image: *muslrust_image
image: *rust_image
environment:
CARGO_HOME: .cargo
commands:
@ -149,7 +147,7 @@ steps:
when: *slow_check_paths
cargo_clippy:
image: *muslrust_image
image: *rust_image
environment:
CARGO_HOME: .cargo
commands:
@ -173,17 +171,17 @@ steps:
when: *slow_check_paths
cargo_build:
image: *muslrust_image
image: *rust_image
environment:
CARGO_HOME: .cargo
commands:
- cargo build
- mv target/x86_64-unknown-linux-musl/debug/lemmy_server target/lemmy_server
- mv target/debug/lemmy_server target/lemmy_server
when: *slow_check_paths
cargo_test:
group: tests
image: *muslrust_image
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
@ -195,12 +193,12 @@ steps:
run_federation_tests:
group: tests
image: node:alpine
image: node:20-bookworm-slim
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432
DO_WRITE_HOSTS_FILE: "1"
commands:
- apk add bash curl postgresql-client
- apt update && apt install -y bash curl postgresql-client
- bash api_tests/prepare-drone-federation-test.sh
- cd api_tests/
- yarn
@ -239,7 +237,9 @@ steps:
settings:
repo: dessalines/lemmy
dockerfile: docker/Dockerfile
platforms: linux/amd64,linux/arm64
# TODO fix arm build: see: https://woodpecker.join-lemmy.org/repos/129/pipeline/2888/20
# platforms: linux/amd64,linux/arm64
platforms: linux/amd64
build_args:
- RUST_RELEASE_MODE=release
tag: ${CI_COMMIT_TAG}

35
Cargo.lock generated
View File

@ -2621,7 +2621,7 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "lemmy_api"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"activitypub_federation",
"actix-web",
@ -2652,13 +2652,14 @@ dependencies = [
[[package]]
name = "lemmy_api_common"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"activitypub_federation",
"actix-web",
"anyhow",
"chrono",
"encoding",
"enum-map",
"futures",
"getrandom",
"jsonwebtoken",
@ -2686,7 +2687,7 @@ dependencies = [
[[package]]
name = "lemmy_api_crud"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"activitypub_federation",
"actix-web",
@ -2707,7 +2708,7 @@ dependencies = [
[[package]]
name = "lemmy_apub"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"activitypub_federation",
"actix-web",
@ -2719,6 +2720,7 @@ dependencies = [
"enum_delegate",
"futures",
"html2md",
"html2text",
"http",
"itertools 0.11.0",
"lemmy_api_common",
@ -2734,6 +2736,7 @@ dependencies = [
"serde_json",
"serde_with",
"serial_test",
"stringreader",
"strum_macros",
"task-local-extensions",
"tokio",
@ -2744,7 +2747,7 @@ dependencies = [
[[package]]
name = "lemmy_db_schema"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"activitypub_federation",
"async-trait",
@ -2780,7 +2783,7 @@ dependencies = [
[[package]]
name = "lemmy_db_views"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"actix-web",
"diesel",
@ -2798,7 +2801,7 @@ dependencies = [
[[package]]
name = "lemmy_db_views_actor"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"chrono",
"diesel",
@ -2806,12 +2809,14 @@ dependencies = [
"lemmy_db_schema",
"serde",
"serde_with",
"strum",
"strum_macros",
"ts-rs",
]
[[package]]
name = "lemmy_db_views_moderator"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"diesel",
"diesel-async",
@ -2823,7 +2828,7 @@ dependencies = [
[[package]]
name = "lemmy_federate"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"activitypub_federation",
"anyhow",
@ -2855,7 +2860,7 @@ dependencies = [
[[package]]
name = "lemmy_routes"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"activitypub_federation",
"actix-web",
@ -2881,7 +2886,7 @@ dependencies = [
[[package]]
name = "lemmy_server"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"activitypub_federation",
"actix-cors",
@ -2929,7 +2934,7 @@ dependencies = [
[[package]]
name = "lemmy_utils"
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
dependencies = [
"actix-web",
"anyhow",
@ -4952,6 +4957,12 @@ dependencies = [
"unicode-normalization",
]
[[package]]
name = "stringreader"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "913e7b03d63752f6cdd2df77da36749d82669904798fe8944b9ec3d23f159905"
[[package]]
name = "strsim"
version = "0.10.0"

View File

@ -1,5 +1,5 @@
[workspace.package]
version = "0.19.0-beta.7"
version = "0.19.0-rc.3"
edition = "2021"
description = "A link aggregator for the fediverse"
license = "AGPL-3.0"
@ -23,6 +23,8 @@ doctest = false
[profile.release]
debug = 0
lto = "thin"
strip = true # Automatically strip symbols from the binary.
opt-level = "z" # Optimize for size.
# 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.
@ -58,16 +60,16 @@ members = [
]
[workspace.dependencies]
lemmy_api = { version = "=0.19.0-beta.7", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.0-beta.7", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.0-beta.7", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.0-beta.7", path = "./crates/utils" }
lemmy_db_schema = { version = "=0.19.0-beta.7", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.0-beta.7", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.0-beta.7", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.0-beta.7", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.0-beta.7", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.0-beta.7", path = "./crates/db_views_moderator" }
lemmy_api = { version = "=0.19.0-rc.3", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.0-rc.3", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.0-rc.3", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.0-rc.3", path = "./crates/utils" }
lemmy_db_schema = { version = "=0.19.0-rc.3", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.0-rc.3", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.0-rc.3", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.0-rc.3", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.0-rc.3", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.0-rc.3", path = "./crates/db_views_moderator" }
activitypub_federation = { version = "0.5.0-beta.3", default-features = false, features = [
"actix-web",
] }
@ -128,6 +130,7 @@ futures-util = "0.3.28"
tokio-postgres = "0.7.8"
tokio-postgres-rustls = "0.10.0"
urlencoding = "2.1.3"
enum-map = "2.6"
[dependencies]
lemmy_api = { workspace = true }
@ -137,7 +140,7 @@ lemmy_utils = { workspace = true }
lemmy_db_schema = { workspace = true }
lemmy_api_common = { workspace = true }
lemmy_routes = { workspace = true }
lemmy_federate = { version = "0.19.0-beta.7", path = "crates/federate" }
lemmy_federate = { version = "0.19.0-rc.3", path = "crates/federate" }
activitypub_federation = { workspace = true }
diesel = { workspace = true }
diesel-async = { workspace = true }

View File

@ -13,11 +13,11 @@
},
"devDependencies": {
"@types/jest": "^29.5.1",
"@types/node": "^20.1.2",
"@typescript-eslint/eslint-plugin": "^5.59.5",
"@typescript-eslint/parser": "^5.59.5",
"eslint": "^8.40.0",
"eslint-plugin-prettier": "^4.0.0",
"@types/node": "^20.8.6",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"eslint": "^8.51.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "^29.5.0",
"lemmy-js-client": "0.19.0-rc.12",
"prettier": "^3.0.0",

View File

@ -32,9 +32,9 @@ import {
getReplies,
getUnreadCount,
waitUntil,
delay,
waitForPost,
alphaUrl,
followCommunity,
} from "./shared";
import { CommentView } from "lemmy-js-client/dist/types/CommentView";
import { CommunityView } from "lemmy-js-client";
@ -500,6 +500,13 @@ test("A and G subscribe to B (center) A posts, G mentions B, it gets announced t
throw "Missing alpha community";
}
// follow community from beta so that it accepts the mention
let betaCommunity = await resolveCommunity(
beta,
alphaCommunity.community.actor_id,
);
await followCommunity(beta, true, betaCommunity.community!.community.id);
let alphaPost = await createPost(alpha, alphaCommunity.community.id);
expect(alphaPost.post_view.community.local).toBe(true);

View File

@ -26,10 +26,14 @@ import {
blockInstance,
waitUntil,
delay,
waitForPost,
alphaUrl,
delta,
betaAllowedInstances,
searchPostLocal,
resolveBetaCommunity,
longDelay,
} from "./shared";
import { LemmyHttp } from "lemmy-js-client";
import { EditSite, LemmyHttp } from "lemmy-js-client";
beforeAll(async () => {
await setupLogins();
@ -376,3 +380,109 @@ test("User blocks instance, communities are hidden", async () => {
let listing_ids3 = listing3.posts.map(p => p.post.ap_id);
expect(listing_ids3).toContain(postRes.post_view.post.ap_id);
});
test("Community follower count is federated", async () => {
// Follow the beta community from alpha
let resolved = await resolveBetaCommunity(alpha);
if (!resolved.community) {
throw "Missing beta community";
}
await followCommunity(alpha, true, resolved.community.community.id);
let followed = (
await waitUntil(
() => resolveBetaCommunity(alpha),
c => c.community?.subscribed === "Subscribed",
)
).community;
// Make sure there is 1 subscriber
expect(followed?.counts.subscribers).toBe(1);
// Follow the community from gamma
resolved = await resolveBetaCommunity(gamma);
if (!resolved.community) {
throw "Missing beta community";
}
await followCommunity(gamma, true, resolved.community.community.id);
followed = (
await waitUntil(
() => resolveBetaCommunity(gamma),
c => c.community?.subscribed === "Subscribed",
)
).community;
// Make sure there are 2 subscribers
expect(followed?.counts?.subscribers).toBe(2);
// Follow the community from delta
resolved = await resolveBetaCommunity(delta);
if (!resolved.community) {
throw "Missing beta community";
}
await followCommunity(delta, true, resolved.community.community.id);
followed = (
await waitUntil(
() => resolveBetaCommunity(delta),
c => c.community?.subscribed === "Subscribed",
)
).community;
// Make sure there are 3 subscribers
expect(followed?.counts?.subscribers).toBe(3);
});
test("Dont receive community activities after unsubscribe", async () => {
let communityRes = await createCommunity(alpha);
expect(communityRes.community_view.community.name).toBeDefined();
expect(communityRes.community_view.counts.subscribers).toBe(1);
let betaCommunity = (
await resolveCommunity(beta, communityRes.community_view.community.actor_id)
).community;
assertCommunityFederation(betaCommunity, communityRes.community_view);
// follow alpha community from beta
await followCommunity(beta, true, betaCommunity!.community.id);
// ensure that follower count was updated
let communityRes1 = await getCommunity(
alpha,
communityRes.community_view.community.id,
);
expect(communityRes1.community_view.counts.subscribers).toBe(2);
// temporarily block alpha, so that it doesnt know about unfollow
let editSiteForm: EditSite = {};
editSiteForm.allowed_instances = ["lemmy-epsilon"];
await beta.editSite(editSiteForm);
await longDelay();
// unfollow
await followCommunity(beta, false, betaCommunity!.community.id);
// ensure that alpha still sees beta as follower
let communityRes2 = await getCommunity(
alpha,
communityRes.community_view.community.id,
);
expect(communityRes2.community_view.counts.subscribers).toBe(2);
// unblock alpha
editSiteForm.allowed_instances = betaAllowedInstances;
await beta.editSite(editSiteForm);
await longDelay();
// create a post, it shouldnt reach beta
let postRes = await createPost(
alpha,
communityRes.community_view.community.id,
);
expect(postRes.post_view.post.id).toBeDefined();
// await longDelay();
let postResBeta = searchPostLocal(beta, postRes.post_view.post);
expect((await postResBeta).posts.length).toBe(0);
});

View File

@ -39,8 +39,7 @@ import {
loginUser,
} from "./shared";
import { PostView } from "lemmy-js-client/dist/types/PostView";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
import { LemmyHttp, Login } from "lemmy-js-client";
import { LemmyHttp } from "lemmy-js-client";
let betaCommunity: CommunityView | undefined;
@ -426,7 +425,7 @@ test("Enforce site ban for federated user", async () => {
expect(alphaUserOnBeta1.person?.person.banned).toBe(true);
// existing alpha post should be removed on beta
let searchBeta2 = await waitUntil(
await waitUntil(
() => getPost(beta, searchBeta1.post.id),
s => s.post_view.post.removed,
);
@ -441,13 +440,16 @@ test("Enforce site ban for federated user", async () => {
expect(unBanAlpha.banned).toBe(false);
// Login gets invalidated by ban, need to login again
let newAlphaUserJwt = await loginUser(alpha, alphaUserPerson?.name!);
if (!alphaUserPerson) {
throw "Missing alpha person";
}
let newAlphaUserJwt = await loginUser(alpha, alphaUserPerson.name);
alpha_user.setHeaders({
Authorization: "Bearer " + newAlphaUserJwt.jwt ?? "",
});
// alpha makes new post in beta community, it federates
let postRes2 = await createPost(alpha_user, betaCommunity!.community.id);
let searchBeta3 = await waitForPost(beta, postRes2.post_view.post);
await waitForPost(beta, postRes2.post_view.post);
let alphaUserOnBeta2 = await resolvePerson(beta, alphaUserActorId!);
expect(alphaUserOnBeta2.person?.person.banned).toBe(false);
@ -554,29 +556,3 @@ test("Report a post", async () => {
expect(betaReport.original_post_body).toBe(alphaReport.original_post_body);
expect(betaReport.reason).toBe(alphaReport.reason);
});
test("Sanitize HTML", async () => {
let betaCommunity = (await resolveBetaCommunity(beta)).community;
if (!betaCommunity) {
throw "Missing beta community";
}
let name = randomString(5);
let body = "<script>alert('xss');</script> hello &\"'";
let form: CreatePost = {
name,
body,
community_id: betaCommunity.community.id,
};
let post = await beta.createPost(form);
// first escaping for the api
expect(post.post_view.post.body).toBe(
"&lt;script>alert(&#x27;xss&#x27;);&lt;/script> hello &amp;&quot;&#x27;",
);
let alphaPost = (await resolvePost(alpha, post.post_view.post)).post;
// second escaping over federation, avoid double escape of &
expect(alphaPost?.post.body).toBe(
"&lt;script>alert(&#x27;xss&#x27;);&lt;/script> hello &amp;&quot;&#x27;",
);
});

View File

@ -84,6 +84,13 @@ export let gamma = new LemmyHttp(gammaUrl);
export let delta = new LemmyHttp(deltaUrl);
export let epsilon = new LemmyHttp(epsilonUrl);
export let betaAllowedInstances = [
"lemmy-alpha",
"lemmy-gamma",
"lemmy-delta",
"lemmy-epsilon",
];
const password = "lemmylemmy";
export async function setupLogins() {
@ -150,12 +157,7 @@ export async function setupLogins() {
];
await alpha.editSite(editSiteForm);
editSiteForm.allowed_instances = [
"lemmy-alpha",
"lemmy-gamma",
"lemmy-delta",
"lemmy-epsilon",
];
editSiteForm.allowed_instances = betaAllowedInstances;
await beta.editSite(editSiteForm);
editSiteForm.allowed_instances = [

File diff suppressed because it is too large Load Diff

View File

@ -2,7 +2,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
comment::{CommentResponse, DistinguishComment},
context::LemmyContext,
utils::{check_community_ban, is_mod_or_admin},
utils::{check_community_mod_action, check_community_user_action},
};
use lemmy_db_schema::{
source::comment::{Comment, CommentUpdateForm},
@ -19,18 +19,19 @@ pub async fn distinguish_comment(
) -> Result<Json<CommentResponse>, LemmyError> {
let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None).await?;
check_community_ban(
local_user_view.person.id,
check_community_user_action(
&local_user_view.person,
orig_comment.community.id,
&mut context.pool(),
)
.await?;
// Verify that only a mod or admin can distinguish a comment
is_mod_or_admin(
&mut context.pool(),
local_user_view.person.id,
check_community_mod_action(
&local_user_view.person,
orig_comment.community.id,
false,
&mut context.pool(),
)
.await?;

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
comment::{CommentResponse, CreateCommentLike},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, check_downvotes_enabled},
utils::{check_community_user_action, check_downvotes_enabled},
};
use lemmy_db_schema::{
newtypes::LocalUserId,
@ -36,8 +36,8 @@ pub async fn like_comment(
let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?;
check_community_ban(
local_user_view.person.id,
check_community_user_action(
&local_user_view.person,
orig_comment.community.id,
&mut context.pool(),
)

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
comment::{CommentReportResponse, CreateCommentReport},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, sanitize_html_api, send_new_report_email_to_admins},
utils::{check_community_user_action, send_new_report_email_to_admins},
};
use lemmy_db_schema::{
source::{
@ -26,14 +26,19 @@ pub async fn create_comment_report(
) -> Result<Json<CommentReportResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = sanitize_html_api(data.reason.trim());
let reason = data.reason.trim().to_string();
check_report_reason(&reason, &local_site)?;
let person_id = local_user_view.person.id;
let comment_id = data.comment_id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?;
check_community_ban(person_id, comment_view.community.id, &mut context.pool()).await?;
check_community_user_action(
&local_user_view.person,
comment_view.community.id,
&mut context.pool(),
)
.await?;
let report_form = CommentReportForm {
creator_id: person_id,

View File

@ -2,6 +2,7 @@ use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
comment::{ListCommentReports, ListCommentReportsResponse},
context::LemmyContext,
utils::check_community_mod_action_opt,
};
use lemmy_db_views::{comment_report_view::CommentReportQuery, structs::LocalUserView};
use lemmy_utils::error::LemmyError;
@ -17,6 +18,8 @@ pub async fn list_comment_reports(
let community_id = data.community_id;
let unresolved_only = data.unresolved_only.unwrap_or_default();
check_community_mod_action_opt(&local_user_view, community_id, &mut context.pool()).await?;
let page = data.page;
let limit = data.limit;
let comment_reports = CommentReportQuery {

View File

@ -2,7 +2,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
comment::{CommentReportResponse, ResolveCommentReport},
context::LemmyContext,
utils::is_mod_or_admin,
utils::check_community_mod_action,
};
use lemmy_db_schema::{source::comment_report::CommentReport, traits::Reportable};
use lemmy_db_views::structs::{CommentReportView, LocalUserView};
@ -20,7 +20,13 @@ pub async fn resolve_comment_report(
let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?;
let person_id = local_user_view.person.id;
is_mod_or_admin(&mut context.pool(), person_id, report.community.id).await?;
check_community_mod_action(
&local_user_view.person,
report.community.id,
false,
&mut context.pool(),
)
.await?;
if data.resolved {
CommentReport::resolve(&mut context.pool(), report_id, person_id)

View File

@ -4,7 +4,7 @@ use lemmy_api_common::{
community::{AddModToCommunity, AddModToCommunityResponse},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::is_mod_or_admin,
utils::check_community_mod_action,
};
use lemmy_db_schema::{
source::{
@ -26,7 +26,13 @@ pub async fn add_mod_to_community(
let community_id = data.community_id;
// Verify that only mods or admins can add mod
is_mod_or_admin(&mut context.pool(), local_user_view.person.id, community_id).await?;
check_community_mod_action(
&local_user_view.person,
community_id,
false,
&mut context.pool(),
)
.await?;
let community = Community::read(&mut context.pool(), community_id).await?;
if local_user_view.local_user.admin && !community.local {
Err(LemmyErrorType::NotAModerator)?

View File

@ -4,7 +4,7 @@ use lemmy_api_common::{
community::{BanFromCommunity, BanFromCommunityResponse},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{is_mod_or_admin, remove_user_data_in_community, sanitize_html_api_opt},
utils::{check_community_mod_action, check_expire_time, remove_user_data_in_community},
};
use lemmy_db_schema::{
source::{
@ -22,7 +22,7 @@ use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::PersonView;
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
utils::{time::naive_from_unix, validation::is_valid_body_field},
utils::validation::is_valid_body_field,
};
#[tracing::instrument(skip(context))]
@ -33,13 +33,14 @@ pub async fn ban_from_community(
) -> Result<Json<BanFromCommunityResponse>, LemmyError> {
let banned_person_id = data.person_id;
let remove_data = data.remove_data.unwrap_or(false);
let expires = data.expires.map(naive_from_unix);
let expires = check_expire_time(data.expires)?;
// Verify that only mods or admins can ban
is_mod_or_admin(
&mut context.pool(),
local_user_view.person.id,
check_community_mod_action(
&local_user_view.person,
data.community_id,
false,
&mut context.pool(),
)
.await?;
is_valid_body_field(&data.reason, false)?;
@ -81,7 +82,7 @@ pub async fn ban_from_community(
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
community_id: data.community_id,
reason: sanitize_html_api_opt(&data.reason),
reason: data.reason.clone(),
banned: Some(data.ban),
expires,
};

View File

@ -4,7 +4,7 @@ use lemmy_api_common::{
community::{CommunityResponse, FollowCommunity},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, check_community_deleted_or_removed},
utils::check_community_user_action,
};
use lemmy_db_schema::{
source::{
@ -32,8 +32,8 @@ pub async fn follow_community(
if data.follow {
if community.local {
check_community_ban(local_user_view.person.id, community.id, &mut context.pool()).await?;
check_community_deleted_or_removed(community.id, &mut context.pool()).await?;
check_community_user_action(&local_user_view.person, community.id, &mut context.pool())
.await?;
CommunityFollower::follow(&mut context.pool(), &community_follower_form)
.await

View File

@ -1,11 +1,11 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
build_response::build_community_response,
community::{CommunityResponse, HideCommunity},
community::HideCommunity,
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{is_admin, sanitize_html_api_opt},
utils::is_admin,
SuccessResponse,
};
use lemmy_db_schema::{
source::{
@ -22,7 +22,7 @@ pub async fn hide_community(
data: Json<HideCommunity>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<CommunityResponse>, LemmyError> {
) -> Result<Json<SuccessResponse>, LemmyError> {
// Verify its a admin (only admin can hide or unhide it)
is_admin(&local_user_view)?;
@ -34,7 +34,7 @@ pub async fn hide_community(
let mod_hide_community_form = ModHideCommunityForm {
community_id: data.community_id,
mod_person_id: local_user_view.person.id,
reason: sanitize_html_api_opt(&data.reason),
reason: data.reason.clone(),
hidden: Some(data.hidden),
};
@ -51,5 +51,5 @@ pub async fn hide_community(
)
.await?;
build_community_response(&context, local_user_view, community_id).await
Ok(Json(SuccessResponse::default()))
}

View File

@ -3,7 +3,7 @@ use anyhow::Context;
use lemmy_api_common::{
community::{GetCommunityResponse, TransferCommunity},
context::LemmyContext,
utils::{is_admin, is_top_mod},
utils::{check_community_user_action, is_admin, is_top_mod},
};
use lemmy_db_schema::{
source::{
@ -27,11 +27,12 @@ pub async fn transfer_community(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<GetCommunityResponse>, LemmyError> {
// Fetch the community mods
let community_id = data.community_id;
let mut community_mods =
CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?;
// Make sure transferrer is either the top community mod, or an admin
if !(is_top_mod(&local_user_view, &community_mods).is_ok() || is_admin(&local_user_view).is_ok())
{

View File

@ -2,11 +2,15 @@ use actix_web::{http::header::Header, HttpRequest};
use actix_web_httpauth::headers::authorization::{Authorization, Bearer};
use base64::{engine::general_purpose::STANDARD_NO_PAD as base64, Engine};
use captcha::Captcha;
use lemmy_api_common::utils::{local_site_to_slur_regex, AUTH_COOKIE_NAME};
use lemmy_api_common::{
claims::Claims,
context::LemmyContext,
utils::{check_user_valid, local_site_to_slur_regex, AUTH_COOKIE_NAME},
};
use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
error::{LemmyError, LemmyErrorExt, LemmyErrorExt2, LemmyErrorType, LemmyResult},
utils::slurs::check_slurs,
};
use std::io::Cursor;
@ -144,6 +148,20 @@ pub(crate) fn build_totp_2fa(
.with_lemmy_type(LemmyErrorType::CouldntGenerateTotp)
}
#[tracing::instrument(skip_all)]
pub async fn local_user_view_from_jwt(
jwt: &str,
context: &LemmyContext,
) -> Result<LocalUserView, LemmyError> {
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?;
check_user_valid(&local_user_view.person)?;
Ok(local_user_view)
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]

View File

@ -4,7 +4,7 @@ use lemmy_api_common::{
context::LemmyContext,
person::{BanPerson, BanPersonResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{is_admin, remove_user_data, sanitize_html_api_opt},
utils::{check_expire_time, is_admin, remove_user_data},
};
use lemmy_db_schema::{
source::{
@ -18,8 +18,9 @@ use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::PersonView;
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
utils::{time::naive_from_unix, validation::is_valid_body_field},
utils::validation::is_valid_body_field,
};
#[tracing::instrument(skip(context))]
pub async fn ban_from_site(
data: Json<BanPerson>,
@ -31,7 +32,7 @@ pub async fn ban_from_site(
is_valid_body_field(&data.reason, false)?;
let expires = data.expires.map(naive_from_unix);
let expires = check_expire_time(data.expires)?;
let person = Person::update(
&mut context.pool(),
@ -61,7 +62,7 @@ pub async fn ban_from_site(
let form = ModBanForm {
mod_person_id: local_user_view.person.id,
other_person_id: data.person_id,
reason: sanitize_html_api_opt(&data.reason),
reason: data.reason.clone(),
banned: Some(data.ban),
expires,
};

View File

@ -1,8 +1,9 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::{LoginResponse, PasswordChangeAfterReset},
person::PasswordChangeAfterReset,
utils::password_length_check,
SuccessResponse,
};
use lemmy_db_schema::source::{
local_user::LocalUser,
@ -15,7 +16,7 @@ use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
pub async fn change_password_after_reset(
data: Json<PasswordChangeAfterReset>,
context: Data<LemmyContext>,
) -> Result<Json<LoginResponse>, LemmyError> {
) -> Result<Json<SuccessResponse>, LemmyError> {
// Fetch the user_id from the token
let token = data.token.clone();
let local_user_id = PasswordResetRequest::read_from_token(&mut context.pool(), &token)
@ -37,9 +38,5 @@ pub async fn change_password_after_reset(
LoginToken::invalidate_all(&mut context.pool(), local_user_id).await?;
Ok(Json(LoginResponse {
jwt: None,
verify_email_sent: false,
registration_created: false,
}))
Ok(Json(SuccessResponse::default()))
}

View File

@ -44,11 +44,7 @@ pub async fn login(
if !valid {
Err(LemmyErrorType::IncorrectLogin)?
}
check_user_valid(
local_user_view.person.banned,
local_user_view.person.ban_expires,
local_user_view.person.deleted,
)?;
check_user_valid(&local_user_view.person)?;
// Check if the user's email is verified if email verification is turned on
// However, skip checking verification if the user is an admin

View File

@ -14,4 +14,5 @@ pub mod report_count;
pub mod reset_password;
pub mod save_settings;
pub mod update_totp;
pub mod validate_auth;
pub mod verify_email;

View File

@ -2,6 +2,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::{GetReportCount, GetReportCountResponse},
utils::check_community_mod_action_opt,
};
use lemmy_db_views::structs::{
CommentReportView,
@ -21,6 +22,8 @@ pub async fn report_count(
let admin = local_user_view.local_user.admin;
let community_id = data.community_id;
check_community_mod_action_opt(&local_user_view, community_id, &mut context.pool()).await?;
let comment_reports =
CommentReportView::get_report_count(&mut context.pool(), person_id, admin, community_id)
.await?;

View File

@ -1,18 +1,19 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::{PasswordReset, PasswordResetResponse},
person::PasswordReset,
utils::send_password_reset_email,
SuccessResponse,
};
use lemmy_db_schema::source::password_reset_request::PasswordResetRequest;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn reset_password(
data: Json<PasswordReset>,
context: Data<LemmyContext>,
) -> Result<Json<PasswordResetResponse>, LemmyError> {
) -> LemmyResult<Json<SuccessResponse>> {
// Fetch that email
let email = data.email.to_lowercase();
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email)
@ -31,5 +32,5 @@ pub async fn reset_password(
// Email the pure token to the user.
send_password_reset_email(&local_user_view, &mut context.pool(), context.settings()).await?;
Ok(Json(PasswordResetResponse {}))
Ok(Json(SuccessResponse::default()))
}

View File

@ -2,7 +2,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::SaveUserSettings,
utils::{sanitize_html_api_opt, send_verification_email},
utils::send_verification_email,
SuccessResponse,
};
use lemmy_db_schema::{
@ -28,13 +28,10 @@ pub async fn save_user_settings(
) -> Result<Json<SuccessResponse>, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
let bio = sanitize_html_api_opt(&data.bio);
let display_name = sanitize_html_api_opt(&data.display_name);
let avatar = diesel_option_overwrite_to_url(&data.avatar)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let bio = diesel_option_overwrite(bio);
let display_name = diesel_option_overwrite(display_name);
let bio = diesel_option_overwrite(data.bio.clone());
let display_name = diesel_option_overwrite(data.display_name.clone());
let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone());
let email_deref = data.email.as_deref().map(str::to_lowercase);
let email = diesel_option_overwrite(email_deref.clone());
@ -82,7 +79,6 @@ pub async fn save_user_settings(
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 theme = sanitize_html_api_opt(&data.theme);
let person_form = PersonUpdateForm {
display_name,
@ -106,7 +102,6 @@ pub async fn save_user_settings(
email,
show_avatars: data.show_avatars,
show_read_posts: data.show_read_posts,
show_new_post_notifs: data.show_new_post_notifs,
send_notifications_to_email: data.send_notifications_to_email,
show_nsfw: data.show_nsfw,
blur_nsfw: data.blur_nsfw,
@ -115,10 +110,13 @@ pub async fn save_user_settings(
show_scores: data.show_scores,
default_sort_type,
default_listing_type,
theme,
theme: data.theme.clone(),
interface_language: data.interface_language.clone(),
open_links_in_new_tab: data.open_links_in_new_tab,
infinite_scroll_enabled: data.infinite_scroll_enabled,
post_listing_mode: data.post_listing_mode,
enable_keyboard_navigation: data.enable_keyboard_navigation,
enable_animated_images: data.enable_animated_images,
..Default::default()
};

View File

@ -0,0 +1,23 @@
use crate::{local_user_view_from_jwt, read_auth_token};
use actix_web::{
web::{Data, Json},
HttpRequest,
};
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
use lemmy_utils::error::{LemmyError, LemmyErrorType};
/// Returns an error message if the auth token is invalid for any reason. Necessary because other
/// endpoints silently treat any call with invalid auth as unauthenticated.
#[tracing::instrument(skip(context))]
pub async fn validate_auth(
req: HttpRequest,
context: Data<LemmyContext>,
) -> Result<Json<SuccessResponse>, LemmyError> {
let jwt = read_auth_token(&req)?;
if let Some(jwt) = jwt {
local_user_view_from_jwt(&jwt, &context).await?;
} else {
Err(LemmyErrorType::NotLoggedIn)?;
}
Ok(Json(SuccessResponse::default()))
}

View File

@ -1,8 +1,9 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
person::{VerifyEmail, VerifyEmailResponse},
person::VerifyEmail,
utils::send_new_applicant_email_to_admins,
SuccessResponse,
};
use lemmy_db_schema::{
source::{
@ -14,12 +15,12 @@ use lemmy_db_schema::{
RegistrationMode,
};
use lemmy_db_views::structs::SiteView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
pub async fn verify_email(
data: Json<VerifyEmail>,
context: Data<LemmyContext>,
) -> Result<Json<VerifyEmailResponse>, LemmyError> {
) -> LemmyResult<Json<SuccessResponse>> {
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)
@ -48,5 +49,5 @@ pub async fn verify_email(
.await?;
}
Ok(Json(VerifyEmailResponse {}))
Ok(Json(SuccessResponse::default()))
}

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
context::LemmyContext,
post::{FeaturePost, PostResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, check_community_deleted_or_removed, is_admin, is_mod_or_admin},
utils::{check_community_mod_action, is_admin},
};
use lemmy_db_schema::{
source::{
@ -27,23 +27,15 @@ pub async fn feature_post(
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
check_community_ban(
local_user_view.person.id,
check_community_mod_action(
&local_user_view.person,
orig_post.community_id,
false,
&mut context.pool(),
)
.await?;
check_community_deleted_or_removed(orig_post.community_id, &mut context.pool()).await?;
if data.feature_type == PostFeatureType::Community {
// Verify that only the mods can feature in community
is_mod_or_admin(
&mut context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
} else {
if data.feature_type == PostFeatureType::Local {
is_admin(&local_user_view)?;
}
@ -72,12 +64,17 @@ pub async fn feature_post(
ModFeaturePost::create(&mut context.pool(), &form).await?;
let person_id = local_user_view.person.id;
ActivityChannel::submit_activity(
SendActivityData::FeaturePost(post, local_user_view.person, data.featured),
SendActivityData::FeaturePost(post, local_user_view.person.clone(), data.featured),
&context,
)
.await?;
build_post_response(&context, orig_post.community_id, person_id, post_id).await
build_post_response(
&context,
orig_post.community_id,
&local_user_view.person,
post_id,
)
.await
}

View File

@ -5,12 +5,7 @@ use lemmy_api_common::{
context::LemmyContext,
post::{CreatePostLike, PostResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_community_ban,
check_community_deleted_or_removed,
check_downvotes_enabled,
mark_post_as_read,
},
utils::{check_community_user_action, check_downvotes_enabled, mark_post_as_read},
};
use lemmy_db_schema::{
source::{
@ -39,13 +34,12 @@ pub async fn like_post(
let post_id = data.post_id;
let post = Post::read(&mut context.pool(), post_id).await?;
check_community_ban(
local_user_view.person.id,
check_community_user_action(
&local_user_view.person,
post.community_id,
&mut context.pool(),
)
.await?;
check_community_deleted_or_removed(post.community_id, &mut context.pool()).await?;
let like_form = PostLikeForm {
post_id: data.post_id,
@ -83,7 +77,7 @@ pub async fn like_post(
build_post_response(
context.deref(),
post.community_id,
local_user_view.person.id,
&local_user_view.person,
post_id,
)
.await

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
context::LemmyContext,
post::{LockPost, PostResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, check_community_deleted_or_removed, is_mod_or_admin},
utils::check_community_mod_action,
};
use lemmy_db_schema::{
source::{
@ -26,21 +26,13 @@ pub async fn lock_post(
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
check_community_ban(
local_user_view.person.id,
check_community_mod_action(
&local_user_view.person,
orig_post.community_id,
false,
&mut context.pool(),
)
.await?;
check_community_deleted_or_removed(orig_post.community_id, &mut context.pool()).await?;
// Verify that only the mods can lock
is_mod_or_admin(
&mut context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let post_id = data.post_id;
@ -63,12 +55,17 @@ pub async fn lock_post(
};
ModLockPost::create(&mut context.pool(), &form).await?;
let person_id = local_user_view.person.id;
ActivityChannel::submit_activity(
SendActivityData::LockPost(post, local_user_view.person, data.locked),
SendActivityData::LockPost(post, local_user_view.person.clone(), data.locked),
&context,
)
.await?;
build_post_response(&context, orig_post.community_id, person_id, post_id).await
build_post_response(
&context,
orig_post.community_id,
&local_user_view.person,
post_id,
)
.await
}

View File

@ -1,30 +1,41 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
post::{MarkPostAsRead, PostResponse},
utils,
};
use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::error::LemmyError;
use lemmy_api_common::{context::LemmyContext, post::MarkPostAsRead, SuccessResponse};
use lemmy_db_schema::source::post::PostRead;
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, MAX_API_PARAM_ELEMENTS};
use std::collections::HashSet;
#[tracing::instrument(skip(context))]
pub async fn mark_post_as_read(
data: Json<MarkPostAsRead>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<PostResponse>, LemmyError> {
let post_id = data.post_id;
) -> Result<Json<SuccessResponse>, LemmyError> {
let mut post_ids = HashSet::new();
if let Some(post_ids_) = &data.post_ids {
post_ids.extend(post_ids_.iter().cloned());
}
if let Some(post_id) = data.post_id {
post_ids.insert(post_id);
}
if post_ids.len() > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
let person_id = local_user_view.person.id;
// Mark the post as read / unread
if data.read {
utils::mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
PostRead::mark_as_read(&mut context.pool(), post_ids, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
} else {
utils::mark_post_as_unread(person_id, post_id, &mut context.pool()).await?;
PostRead::mark_as_unread(&mut context.pool(), post_ids, person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
}
// Fetch it
let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false).await?;
Ok(Json(PostResponse { post_view }))
Ok(Json(SuccessResponse::default()))
}

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
context::LemmyContext,
post::{CreatePostReport, PostReportResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, sanitize_html_api, send_new_report_email_to_admins},
utils::{check_community_user_action, send_new_report_email_to_admins},
};
use lemmy_db_schema::{
source::{
@ -26,14 +26,19 @@ pub async fn create_post_report(
) -> Result<Json<PostReportResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = sanitize_html_api(data.reason.trim());
let reason = data.reason.trim().to_string();
check_report_reason(&reason, &local_site)?;
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?;
check_community_ban(person_id, post_view.community.id, &mut context.pool()).await?;
check_community_user_action(
&local_user_view.person,
post_view.community.id,
&mut context.pool(),
)
.await?;
let report_form = PostReportForm {
creator_id: person_id,

View File

@ -2,6 +2,7 @@ use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
post::{ListPostReports, ListPostReportsResponse},
utils::check_community_mod_action_opt,
};
use lemmy_db_views::{post_report_view::PostReportQuery, structs::LocalUserView};
use lemmy_utils::error::LemmyError;
@ -17,6 +18,8 @@ pub async fn list_post_reports(
let community_id = data.community_id;
let unresolved_only = data.unresolved_only.unwrap_or_default();
check_community_mod_action_opt(&local_user_view, community_id, &mut context.pool()).await?;
let page = data.page;
let limit = data.limit;
let post_reports = PostReportQuery {

View File

@ -2,7 +2,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
post::{PostReportResponse, ResolvePostReport},
utils::is_mod_or_admin,
utils::check_community_mod_action,
};
use lemmy_db_schema::{source::post_report::PostReport, traits::Reportable};
use lemmy_db_views::structs::{LocalUserView, PostReportView};
@ -20,7 +20,13 @@ pub async fn resolve_post_report(
let report = PostReportView::read(&mut context.pool(), report_id, person_id).await?;
let person_id = local_user_view.person.id;
is_mod_or_admin(&mut context.pool(), person_id, report.community.id).await?;
check_community_mod_action(
&local_user_view.person,
report.community.id,
false,
&mut context.pool(),
)
.await?;
if data.resolved {
PostReport::resolve(&mut context.pool(), report_id, person_id)

View File

@ -3,7 +3,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
private_message::{CreatePrivateMessageReport, PrivateMessageReportResponse},
utils::{sanitize_html_api, send_new_report_email_to_admins},
utils::send_new_report_email_to_admins,
};
use lemmy_db_schema::{
source::{
@ -24,7 +24,7 @@ pub async fn create_pm_report(
) -> Result<Json<PrivateMessageReportResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let reason = sanitize_html_api(data.reason.trim());
let reason = data.reason.trim().to_string();
check_report_reason(&reason, &local_site)?;
let person_id = local_user_view.person.id;
@ -35,7 +35,7 @@ pub async fn create_pm_report(
creator_id: person_id,
private_message_id,
original_pm_text: private_message.content,
reason: reason.clone(),
reason,
};
let report = PrivateMessageReport::report(&mut context.pool(), &report_form)

View File

@ -2,13 +2,9 @@ use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{
context::LemmyContext,
site::{GetModlog, GetModlogResponse},
utils::{check_private_instance, is_admin, is_mod_or_admin},
};
use lemmy_db_schema::{
newtypes::{CommunityId, PersonId},
source::local_site::LocalSite,
ModlogActionType,
utils::{check_community_mod_action_opt, check_private_instance, is_admin},
};
use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_moderator::structs::{
AdminPurgeCommentView,
@ -44,19 +40,16 @@ pub async fn get_mod_log(
let type_ = data.type_.unwrap_or(All);
let community_id = data.community_id;
let (local_person_id, is_admin) = match local_user_view {
Some(s) => (s.person.id, is_admin(&s).is_ok()),
None => (PersonId(-1), false),
let is_mod_or_admin = if let Some(local_user_view) = local_user_view {
let is_mod = community_id.is_some()
&& check_community_mod_action_opt(&local_user_view, community_id, &mut context.pool())
.await
.is_ok();
is_mod || is_admin(&local_user_view).is_ok()
} else {
false
};
let community_id_value = match community_id {
Some(s) => s,
None => CommunityId(-1),
};
let is_mod_of_community = data.community_id.is_some()
&& is_mod_or_admin(&mut context.pool(), local_person_id, community_id_value)
.await
.is_ok();
let hide_modlog_names = local_site.hide_modlog_mod_names && !is_mod_of_community && !is_admin;
let hide_modlog_names = local_site.hide_modlog_mod_names && !is_mod_or_admin;
let mod_person_id = if hide_modlog_names {
None

View File

@ -1,8 +1,9 @@
use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
site::{PurgeComment, PurgeItemResponse},
utils::{is_admin, sanitize_html_api_opt},
site::PurgeComment,
utils::is_admin,
SuccessResponse,
};
use lemmy_db_schema::{
source::{
@ -19,7 +20,7 @@ pub async fn purge_comment(
data: Json<PurgeComment>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<PurgeItemResponse>, LemmyError> {
) -> Result<Json<SuccessResponse>, LemmyError> {
// Only let admin purge an item
is_admin(&local_user_view)?;
@ -35,14 +36,13 @@ pub async fn purge_comment(
Comment::delete(&mut context.pool(), comment_id).await?;
// Mod tables
let reason = sanitize_html_api_opt(&data.reason);
let form = AdminPurgeCommentForm {
admin_person_id: local_user_view.person.id,
reason,
reason: data.reason.clone(),
post_id,
};
AdminPurgeComment::create(&mut context.pool(), &form).await?;
Ok(Json(PurgeItemResponse { success: true }))
Ok(Json(SuccessResponse::default()))
}

View File

@ -2,8 +2,9 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
request::purge_image_from_pictrs,
site::{PurgeCommunity, PurgeItemResponse},
utils::{is_admin, purge_image_posts_for_community, sanitize_html_api_opt},
site::PurgeCommunity,
utils::{is_admin, purge_image_posts_for_community},
SuccessResponse,
};
use lemmy_db_schema::{
source::{
@ -20,7 +21,7 @@ pub async fn purge_community(
data: Json<PurgeCommunity>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<PurgeItemResponse>, LemmyError> {
) -> Result<Json<SuccessResponse>, LemmyError> {
// Only let admin purge an item
is_admin(&local_user_view)?;
@ -42,13 +43,12 @@ pub async fn purge_community(
Community::delete(&mut context.pool(), community_id).await?;
// Mod tables
let reason = sanitize_html_api_opt(&data.reason);
let form = AdminPurgeCommunityForm {
admin_person_id: local_user_view.person.id,
reason,
reason: data.reason.clone(),
};
AdminPurgeCommunity::create(&mut context.pool(), &form).await?;
Ok(Json(PurgeItemResponse { success: true }))
Ok(Json(SuccessResponse::default()))
}

View File

@ -2,8 +2,9 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
request::delete_image_from_pictrs,
site::{PurgeItemResponse, PurgePerson},
utils::{is_admin, sanitize_html_api_opt},
site::PurgePerson,
utils::is_admin,
SuccessResponse,
};
use lemmy_db_schema::{
source::{
@ -21,7 +22,7 @@ pub async fn purge_person(
data: Json<PurgePerson>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<PurgeItemResponse>, LemmyError> {
) -> Result<Json<SuccessResponse>, LemmyError> {
// Only let admin purge an item
is_admin(&local_user_view)?;
@ -41,13 +42,12 @@ pub async fn purge_person(
Person::delete(&mut context.pool(), person_id).await?;
// Mod tables
let reason = sanitize_html_api_opt(&data.reason);
let form = AdminPurgePersonForm {
admin_person_id: local_user_view.person.id,
reason,
reason: data.reason.clone(),
};
AdminPurgePerson::create(&mut context.pool(), &form).await?;
Ok(Json(PurgeItemResponse { success: true }))
Ok(Json(SuccessResponse::default()))
}

View File

@ -2,8 +2,9 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
request::purge_image_from_pictrs,
site::{PurgeItemResponse, PurgePost},
utils::{is_admin, sanitize_html_api_opt},
site::PurgePost,
utils::is_admin,
SuccessResponse,
};
use lemmy_db_schema::{
source::{
@ -20,7 +21,7 @@ pub async fn purge_post(
data: Json<PurgePost>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<PurgeItemResponse>, LemmyError> {
) -> Result<Json<SuccessResponse>, LemmyError> {
// Only let admin purge an item
is_admin(&local_user_view)?;
@ -43,14 +44,13 @@ pub async fn purge_post(
Post::delete(&mut context.pool(), post_id).await?;
// Mod tables
let reason = sanitize_html_api_opt(&data.reason);
let form = AdminPurgePostForm {
admin_person_id: local_user_view.person.id,
reason,
reason: data.reason.clone(),
community_id,
};
AdminPurgePost::create(&mut context.pool(), &form).await?;
Ok(Json(PurgeItemResponse { success: true }))
Ok(Json(SuccessResponse::default()))
}

View File

@ -68,6 +68,7 @@ actix-web = { workspace = true, optional = true }
jsonwebtoken = { version = "8.3.0", optional = true }
# necessary for wasmt compilation
getrandom = { version = "0.2.10", features = ["js"] }
enum-map = { workspace = true }
[dev-dependencies]
serial_test = { workspace = true }

View File

@ -7,7 +7,7 @@ use crate::{
};
use actix_web::web::Json;
use lemmy_db_schema::{
newtypes::{CommentId, CommunityId, LocalUserId, PersonId, PostId},
newtypes::{CommentId, CommunityId, LocalUserId, PostId},
source::{
actor_language::CommunityLanguage,
comment::Comment,
@ -20,7 +20,10 @@ use lemmy_db_schema::{
};
use lemmy_db_views::structs::{CommentView, LocalUserView, PostView};
use lemmy_db_views_actor::structs::CommunityView;
use lemmy_utils::{error::LemmyError, utils::mention::MentionData};
use lemmy_utils::{
error::LemmyError,
utils::{markdown::markdown_to_html, mention::MentionData},
};
pub async fn build_comment_response(
context: &LemmyContext,
@ -41,10 +44,9 @@ pub async fn build_community_response(
local_user_view: LocalUserView,
community_id: CommunityId,
) -> Result<Json<CommunityResponse>, LemmyError> {
let is_mod_or_admin =
is_mod_or_admin(&mut context.pool(), local_user_view.person.id, community_id)
.await
.is_ok();
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view.person, community_id)
.await
.is_ok();
let person_id = local_user_view.person.id;
let community_view = CommunityView::read(
&mut context.pool(),
@ -64,16 +66,16 @@ pub async fn build_community_response(
pub async fn build_post_response(
context: &LemmyContext,
community_id: CommunityId,
person_id: PersonId,
person: &Person,
post_id: PostId,
) -> Result<Json<PostResponse>, LemmyError> {
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), person_id, community_id)
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), person, community_id)
.await
.is_ok();
let post_view = PostView::read(
&mut context.pool(),
post_id,
Some(person_id),
Some(person.id),
is_mod_or_admin,
)
.await?;
@ -121,10 +123,11 @@ pub async fn send_local_notifs(
// Send an email to those local users that have notifications on
if do_send_email {
let lang = get_interface_language(&mention_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&mention_user_view,
&lang.notification_mentioned_by_subject(&person.name),
&lang.notification_mentioned_by_body(&comment.content, &inbox_link, &person.name),
&lang.notification_mentioned_by_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
@ -164,10 +167,11 @@ pub async fn send_local_notifs(
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_comment_reply_subject(&person.name),
&lang.notification_comment_reply_body(&comment.content, &inbox_link, &person.name),
&lang.notification_comment_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await
@ -201,10 +205,11 @@ pub async fn send_local_notifs(
if do_send_email {
let lang = get_interface_language(&parent_user_view);
let content = markdown_to_html(&comment.content);
send_email_to_user(
&parent_user_view,
&lang.notification_post_reply_subject(&person.name),
&lang.notification_post_reply_body(&comment.content, &inbox_link, &person.name),
&lang.notification_post_reply_body(&content, &inbox_link, &person.name),
context.settings(),
)
.await

View File

@ -88,7 +88,7 @@ mod tests {
traits::Crud,
utils::build_db_pool_for_tests,
};
use lemmy_utils::rate_limit::{RateLimitCell, RateLimitConfig};
use lemmy_utils::rate_limit::RateLimitCell;
use reqwest::Client;
use reqwest_middleware::ClientBuilder;
use serial_test::serial;
@ -103,9 +103,7 @@ mod tests {
pool_.clone(),
ClientBuilder::new(Client::default()).build(),
secret,
RateLimitCell::new(RateLimitConfig::builder().build())
.await
.clone(),
RateLimitCell::with_test_config(),
);
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())

View File

@ -154,7 +154,6 @@ pub struct EditCommunity {
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Hide a community from the main view.
// TODO this should really be a part of edit community. And why does it contain a reason, that should be in the mod tables.
pub struct HideCommunity {
pub community_id: CommunityId,
pub hidden: bool,
@ -180,7 +179,6 @@ pub struct RemoveCommunity {
pub community_id: CommunityId,
pub removed: bool,
pub reason: Option<String>,
pub expires: Option<i64>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]

View File

@ -46,7 +46,7 @@ impl LemmyContext {
pub fn secret(&self) -> &Secret {
&self.secret
}
pub fn settings_updated_channel(&self) -> &RateLimitCell {
pub fn rate_limit_cell(&self) -> &RateLimitCell {
&self.rate_limit_cell
}
}

View File

@ -39,15 +39,6 @@ pub struct DeleteCustomEmoji {
pub id: CustomEmojiId,
}
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The response for deleting a custom emoji.
pub struct DeleteCustomEmojiResponse {
pub id: CustomEmojiId,
pub success: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]

View File

@ -3,6 +3,7 @@ use lemmy_db_schema::{
newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId},
CommentSortType,
ListingType,
PostListingMode,
SortType,
};
use lemmy_db_views::structs::{CommentView, PostView};
@ -123,8 +124,11 @@ pub struct SaveUserSettings {
pub open_links_in_new_tab: Option<bool>,
/// Enable infinite scroll
pub infinite_scroll_enabled: Option<bool>,
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 should be paused
pub enable_animated_images: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
@ -331,12 +335,6 @@ pub struct DeleteAccount {
pub delete_content: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The response of deleting your account.
pub struct DeleteAccountResponse {}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
@ -345,12 +343,6 @@ pub struct PasswordReset {
pub email: Sensitive<String>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The response of a password reset.
pub struct PasswordResetResponse {}
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
@ -400,12 +392,6 @@ pub struct VerifyEmail {
pub token: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// A response to verifying your email.
pub struct VerifyEmailResponse {}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]

View File

@ -135,12 +135,15 @@ pub struct RemovePost {
pub reason: Option<String>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// Mark a post as read.
pub struct MarkPostAsRead {
pub post_id: PostId,
/// TODO: deprecated, send `post_ids` instead
pub post_id: Option<PostId>,
pub post_ids: Option<Vec<PostId>>,
pub read: bool,
}

View File

@ -361,14 +361,6 @@ pub struct PurgeComment {
pub reason: Option<String>,
}
#[derive(Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The response for purged items.
pub struct PurgeItemResponse {
pub success: bool,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
#[cfg_attr(feature = "full", derive(TS))]

View File

@ -6,9 +6,9 @@ use crate::{
};
use actix_web::cookie::{Cookie, SameSite};
use anyhow::Context;
use chrono::{DateTime, Utc};
use chrono::{DateTime, Days, Local, TimeZone, Utc};
use enum_map::{enum_map, EnumMap};
use lemmy_db_schema::{
impls::person::is_banned,
newtypes::{CommunityId, DbUrl, PersonId, PostId},
source::{
comment::{Comment, CommentUpdateForm},
@ -20,9 +20,9 @@ use lemmy_db_schema::{
password_reset_request::PasswordResetRequest,
person::{Person, PersonUpdateForm},
person_block::PersonBlock,
post::{Post, PostRead, PostReadForm},
post::{Post, PostRead},
},
traits::{Crud, Readable},
traits::Crud,
utils::DbPool,
};
use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView};
@ -33,14 +33,15 @@ use lemmy_db_views_actor::structs::{
};
use lemmy_utils::{
email::{send_email, translations::Lang},
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
location_info,
rate_limit::RateLimitConfig,
rate_limit::{ActionType, BucketConfig},
settings::structs::Settings,
utils::slurs::build_slur_regex,
};
use regex::Regex;
use rosetta_i18n::{Language, LanguageId};
use std::collections::HashSet;
use tracing::warn;
use url::{ParseError, Url};
@ -49,10 +50,12 @@ pub static AUTH_COOKIE_NAME: &str = "auth";
#[tracing::instrument(skip_all)]
pub async fn is_mod_or_admin(
pool: &mut DbPool<'_>,
person_id: PersonId,
person: &Person,
community_id: CommunityId,
) -> Result<(), LemmyError> {
let is_mod_or_admin = CommunityView::is_mod_or_admin(pool, person_id, community_id).await?;
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 {
@ -68,7 +71,7 @@ pub async fn is_mod_or_admin_opt(
) -> Result<(), LemmyError> {
if let Some(local_user_view) = local_user_view {
if let Some(community_id) = community_id {
is_mod_or_admin(pool, local_user_view.person.id, community_id).await
is_mod_or_admin(pool, &local_user_view.person, community_id).await
} else {
is_admin(local_user_view)
}
@ -78,8 +81,11 @@ pub async fn is_mod_or_admin_opt(
}
pub fn is_admin(local_user_view: &LocalUserView) -> Result<(), LemmyError> {
check_user_valid(&local_user_view.person)?;
if !local_user_view.local_user.admin {
Err(LemmyErrorType::NotAnAdmin)?
} else if local_user_view.person.banned {
Err(LemmyErrorType::Banned)?
} else {
Ok(())
}
@ -89,6 +95,7 @@ pub fn is_top_mod(
local_user_view: &LocalUserView,
community_mods: &[CommunityModeratorView],
) -> Result<(), LemmyError> {
check_user_valid(&local_user_view.person)?;
if local_user_view.person.id
!= community_mods
.first()
@ -113,73 +120,98 @@ pub async fn mark_post_as_read(
person_id: PersonId,
post_id: PostId,
pool: &mut DbPool<'_>,
) -> Result<PostRead, LemmyError> {
let post_read_form = PostReadForm { post_id, person_id };
PostRead::mark_as_read(pool, &post_read_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)
}
#[tracing::instrument(skip_all)]
pub async fn mark_post_as_unread(
person_id: PersonId,
post_id: PostId,
pool: &mut DbPool<'_>,
) -> Result<usize, LemmyError> {
let post_read_form = PostReadForm { post_id, person_id };
PostRead::mark_as_unread(pool, &post_read_form)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)
}
pub fn check_user_valid(
banned: bool,
ban_expires: Option<DateTime<Utc>>,
deleted: bool,
) -> Result<(), LemmyError> {
PostRead::mark_as_read(pool, HashSet::from([post_id]), person_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntMarkPostAsRead)?;
Ok(())
}
pub fn check_user_valid(person: &Person) -> Result<(), LemmyError> {
// Check for a site ban
if is_banned(banned, ban_expires) {
if person.banned {
Err(LemmyErrorType::SiteBan)?
}
// check for account deletion
else if deleted {
else if person.deleted {
Err(LemmyErrorType::Deleted)?
} else {
Ok(())
}
}
#[tracing::instrument(skip_all)]
pub async fn check_community_ban(
person_id: PersonId,
/// 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
/// the user isn't banned.
pub async fn check_community_user_action(
person: &Person,
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> Result<(), LemmyError> {
let is_banned = CommunityPersonBanView::get(pool, person_id, community_id)
.await
.is_ok();
if is_banned {
Err(LemmyErrorType::BannedFromCommunity)?
} else {
Ok(())
}
) -> LemmyResult<()> {
check_user_valid(person)?;
check_community_deleted_removed(community_id, pool).await?;
check_community_ban(person, community_id, pool).await?;
Ok(())
}
#[tracing::instrument(skip_all)]
pub async fn check_community_deleted_or_removed(
async fn check_community_deleted_removed(
community_id: CommunityId,
pool: &mut DbPool<'_>,
) -> Result<(), LemmyError> {
) -> LemmyResult<()> {
let community = Community::read(pool, community_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?;
if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)?
} else {
Ok(())
}
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
/// removed/deleted.
pub async fn check_community_mod_action(
person: &Person,
community_id: CommunityId,
allow_deleted: bool,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
is_mod_or_admin(pool, person, community_id).await?;
check_community_ban(person, community_id, pool).await?;
// it must be possible to restore deleted community
if !allow_deleted {
check_community_deleted_removed(community_id, pool).await?;
}
Ok(())
}
pub async fn check_community_mod_action_opt(
local_user_view: &LocalUserView,
community_id: Option<CommunityId>,
pool: &mut DbPool<'_>,
) -> LemmyResult<()> {
if let Some(community_id) = community_id {
check_community_mod_action(&local_user_view.person, community_id, false, pool).await?;
} else {
is_admin(local_user_view)?;
}
Ok(())
}
pub fn check_post_deleted_or_removed(post: &Post) -> Result<(), LemmyError> {
@ -359,23 +391,21 @@ fn lang_str_to_lang(lang: &str) -> Lang {
}
pub fn local_site_rate_limit_to_rate_limit_config(
local_site_rate_limit: &LocalSiteRateLimit,
) -> RateLimitConfig {
let l = local_site_rate_limit;
RateLimitConfig {
message: l.message,
message_per_second: l.message_per_second,
post: l.post,
post_per_second: l.post_per_second,
register: l.register,
register_per_second: l.register_per_second,
image: l.image,
image_per_second: l.image_per_second,
comment: l.comment,
comment_per_second: l.comment_per_second,
search: l.search,
search_per_second: l.search_per_second,
l: &LocalSiteRateLimit,
) -> EnumMap<ActionType, BucketConfig> {
enum_map! {
ActionType::Message => (l.message, l.message_per_second),
ActionType::Post => (l.post, l.post_per_second),
ActionType::Register => (l.register, l.register_per_second),
ActionType::Image => (l.image, l.image_per_second),
ActionType::Comment => (l.comment, l.comment_per_second),
ActionType::Search => (l.search, l.search_per_second),
ActionType::ImportUserSettings => (l.import_user_settings, l.import_user_settings_per_second),
}
.map(|_key, (capacity, secs_to_refill)| BucketConfig {
capacity: u32::try_from(capacity).unwrap_or(0),
secs_to_refill: u32::try_from(secs_to_refill).unwrap_or(0),
})
}
pub fn local_site_to_slur_regex(local_site: &LocalSite) -> Option<Regex> {
@ -721,37 +751,6 @@ pub fn generate_moderators_url(community_id: &DbUrl) -> Result<DbUrl, LemmyError
Ok(Url::parse(&format!("{community_id}/moderators"))?.into())
}
/// Replace special HTML characters in API parameters to prevent XSS attacks.
///
/// Taken from https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.md#output-encoding-for-html-contexts
///
/// `>` is left in place because it is interpreted as markdown quote.
pub fn sanitize_html_api(data: &str) -> String {
data
.replace('&', "&amp;")
.replace('<', "&lt;")
.replace('\"', "&quot;")
.replace('\'', "&#x27;")
}
pub fn sanitize_html_api_opt(data: &Option<String>) -> Option<String> {
data.as_ref().map(|d| sanitize_html_api(d))
}
/// Replace special HTML characters in federation parameters to prevent XSS attacks.
///
/// Unlike [sanitize_html_api()] it leaves `&` in place to avoid double escaping.
pub fn sanitize_html_federation(data: &str) -> String {
data
.replace('<', "&lt;")
.replace('\"', "&quot;")
.replace('\'', "&#x27;")
}
pub fn sanitize_html_federation_opt(data: &Option<String>) -> Option<String> {
data.as_ref().map(|d| sanitize_html_federation(d))
}
pub fn create_login_cookie(jwt: Sensitive<String>) -> Cookie<'static> {
let mut cookie = Cookie::new(AUTH_COOKIE_NAME, jwt.into_inner());
cookie.set_secure(true);
@ -760,12 +759,40 @@ pub fn create_login_cookie(jwt: Sensitive<String>) -> Cookie<'static> {
cookie
}
/// Ensure that ban/block expiry is in valid range. If its in past, throw error. If its more
/// than 10 years in future, convert to permanent ban. Otherwise return the same value.
pub fn check_expire_time(expires_unix_opt: Option<i64>) -> LemmyResult<Option<DateTime<Utc>>> {
if let Some(expires_unix) = expires_unix_opt {
let expires = Utc
.timestamp_opt(expires_unix, 0)
.single()
.ok_or(LemmyErrorType::InvalidUnixTime)?;
limit_expire_time(expires)
} else {
Ok(None)
}
}
fn limit_expire_time(expires: DateTime<Utc>) -> LemmyResult<Option<DateTime<Utc>>> {
const MAX_BAN_TERM: Days = Days::new(10 * 365);
if expires < Local::now() {
Err(LemmyErrorType::BanExpirationInPast)?
} else if expires > Local::now() + MAX_BAN_TERM {
Ok(None)
} else {
Ok(Some(expires))
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::utils::{honeypot_check, password_length_check};
use crate::utils::{honeypot_check, limit_expire_time, password_length_check};
use chrono::{Days, Utc};
#[test]
#[rustfmt::skip]
@ -783,4 +810,25 @@ mod tests {
assert!(honeypot_check(&Some("1".to_string())).is_err());
assert!(honeypot_check(&Some("message".to_string())).is_err());
}
#[test]
fn test_limit_ban_term() {
// 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)
);
let nine_years = Utc::now() + Days::new(365 * 9);
assert_eq!(limit_expire_time(nine_years).unwrap(), 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
);
}
}

View File

@ -6,13 +6,11 @@ use lemmy_api_common::{
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_community_ban,
check_community_deleted_or_removed,
check_community_user_action,
check_post_deleted_or_removed,
generate_local_apub_endpoint,
get_post,
local_site_to_slur_regex,
sanitize_html_api,
EndpointType,
},
};
@ -52,15 +50,13 @@ pub async fn create_comment(
&local_site_to_slur_regex(&local_site),
);
is_valid_body_field(&Some(content.clone()), false)?;
let content = sanitize_html_api(&content);
// Check for a community ban
let post_id = data.post_id;
let post = get_post(post_id, &mut context.pool()).await?;
let community_id = post.community_id;
check_community_ban(local_user_view.person.id, community_id, &mut context.pool()).await?;
check_community_deleted_or_removed(community_id, &mut context.pool()).await?;
check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?;
check_post_deleted_or_removed(&post)?;
// Check if post is locked, no new comments

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
comment::{CommentResponse, DeleteComment},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::check_community_ban,
utils::check_community_user_action,
};
use lemmy_db_schema::{
source::{
@ -31,8 +31,8 @@ pub async fn delete_comment(
Err(LemmyErrorType::CouldntUpdateComment)?
}
check_community_ban(
local_user_view.person.id,
check_community_user_action(
&local_user_view.person,
orig_comment.community.id,
&mut context.pool(),
)

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
comment::{CommentResponse, RemoveComment},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, is_mod_or_admin},
utils::check_community_mod_action,
};
use lemmy_db_schema::{
source::{
@ -27,21 +27,14 @@ pub async fn remove_comment(
let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?;
check_community_ban(
local_user_view.person.id,
check_community_mod_action(
&local_user_view.person,
orig_comment.community.id,
false,
&mut context.pool(),
)
.await?;
// Verify that only a mod or admin can remove
is_mod_or_admin(
&mut context.pool(),
local_user_view.person.id,
orig_comment.community.id,
)
.await?;
// Do the remove
let removed = data.removed;
let updated_comment = Comment::update(

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
comment::{CommentResponse, EditComment},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, local_site_to_slur_regex, sanitize_html_api_opt},
utils::{check_community_user_action, local_site_to_slur_regex},
};
use lemmy_db_schema::{
source::{
@ -37,8 +37,8 @@ pub async fn update_comment(
let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?;
check_community_ban(
local_user_view.person.id,
check_community_user_action(
&local_user_view.person,
orig_comment.community.id,
&mut context.pool(),
)
@ -63,7 +63,6 @@ pub async fn update_comment(
.as_ref()
.map(|c| remove_slurs(c, &local_site_to_slur_regex(&local_site)));
is_valid_body_field(&content, false)?;
let content = sanitize_html_api_opt(&content);
let comment_id = data.comment_id;
let form = CommentUpdateForm {

View File

@ -11,8 +11,6 @@ use lemmy_api_common::{
generate_shared_inbox_url,
is_admin,
local_site_to_slur_regex,
sanitize_html_api,
sanitize_html_api_opt,
EndpointType,
},
};
@ -57,14 +55,10 @@ pub async fn create_community(
let icon = diesel_option_overwrite_to_url_create(&data.icon)?;
let banner = diesel_option_overwrite_to_url_create(&data.banner)?;
let name = sanitize_html_api(&data.name);
let title = sanitize_html_api(&data.title);
let description = sanitize_html_api_opt(&data.description);
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&name, &slur_regex)?;
check_slurs(&title, &slur_regex)?;
check_slurs_opt(&description, &slur_regex)?;
check_slurs(&data.name, &slur_regex)?;
check_slurs(&data.title, &slur_regex)?;
check_slurs_opt(&data.description, &slur_regex)?;
is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?;
is_valid_body_field(&data.description, false)?;
@ -85,9 +79,9 @@ pub async fn create_community(
let keypair = generate_actor_keypair()?;
let community_form = CommunityInsertForm::builder()
.name(name)
.title(title)
.description(description)
.name(data.name.clone())
.title(data.title.clone())
.description(data.description.clone())
.icon(icon)
.banner(banner)
.nsfw(data.nsfw)

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
community::{CommunityResponse, DeleteCommunity},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::is_top_mod,
utils::{check_community_mod_action, is_top_mod},
};
use lemmy_db_schema::{
source::community::{Community, CommunityUpdateForm},
@ -26,6 +26,14 @@ pub async fn delete_community(
let community_mods =
CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
check_community_mod_action(
&local_user_view.person,
community_id,
true,
&mut context.pool(),
)
.await?;
// Make sure deleter is the top mod
is_top_mod(&local_user_view, &community_mods)?;

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
community::{CommunityResponse, RemoveCommunity},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::is_admin,
utils::{check_community_mod_action, is_admin},
};
use lemmy_db_schema::{
source::{
@ -15,10 +15,7 @@ use lemmy_db_schema::{
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
utils::time::naive_from_unix,
};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType};
#[tracing::instrument(skip(context))]
pub async fn remove_community(
@ -26,6 +23,14 @@ pub async fn remove_community(
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<CommunityResponse>, LemmyError> {
check_community_mod_action(
&local_user_view.person,
data.community_id,
true,
&mut context.pool(),
)
.await?;
// Verify its an admin (only an admin can remove a community)
is_admin(&local_user_view)?;
@ -44,13 +49,11 @@ pub async fn remove_community(
.with_lemmy_type(LemmyErrorType::CouldntUpdateCommunity)?;
// Mod tables
let expires = data.expires.map(naive_from_unix);
let form = ModRemoveCommunityForm {
mod_person_id: local_user_view.person.id,
community_id: data.community_id,
removed: Some(removed),
reason: data.reason.clone(),
expires,
};
ModRemoveCommunity::create(&mut context.pool(), &form).await?;

View File

@ -5,10 +5,9 @@ use lemmy_api_common::{
community::{CommunityResponse, EditCommunity},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{local_site_to_slur_regex, sanitize_html_api_opt},
utils::{check_community_mod_action, local_site_to_slur_regex},
};
use lemmy_db_schema::{
newtypes::PersonId,
source::{
actor_language::{CommunityLanguage, SiteLanguage},
community::{Community, CommunityUpdateForm},
@ -18,7 +17,6 @@ use lemmy_db_schema::{
utils::{diesel_option_overwrite, diesel_option_overwrite_to_url, naive_now},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityModeratorView;
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
utils::{slurs::check_slurs_opt, validation::is_valid_body_field},
@ -37,22 +35,18 @@ pub async fn update_community(
check_slurs_opt(&data.description, &slur_regex)?;
is_valid_body_field(&data.description, false)?;
let title = sanitize_html_api_opt(&data.title);
let description = sanitize_html_api_opt(&data.description);
let icon = diesel_option_overwrite_to_url(&data.icon)?;
let banner = diesel_option_overwrite_to_url(&data.banner)?;
let description = diesel_option_overwrite(description);
let description = diesel_option_overwrite(data.description.clone());
// Verify its a mod (only mods can edit it)
let community_id = data.community_id;
let mods: Vec<PersonId> =
CommunityModeratorView::for_community(&mut context.pool(), community_id)
.await
.map(|v| v.into_iter().map(|m| m.moderator.id).collect())?;
if !mods.contains(&local_user_view.person.id) {
Err(LemmyErrorType::NotAModerator)?
}
check_community_mod_action(
&local_user_view.person,
data.community_id,
false,
&mut context.pool(),
)
.await?;
let community_id = data.community_id;
if let Some(languages) = data.discussion_languages.clone() {
@ -67,7 +61,7 @@ pub async fn update_community(
}
let community_form = CommunityUpdateForm {
title,
title: data.title.clone(),
description,
icon,
banner,

View File

@ -3,7 +3,7 @@ use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
custom_emoji::{CreateCustomEmoji, CustomEmojiResponse},
utils::{is_admin, sanitize_html_api},
utils::is_admin,
};
use lemmy_db_schema::source::{
custom_emoji::{CustomEmoji, CustomEmojiInsertForm},
@ -23,15 +23,11 @@ pub async fn create_custom_emoji(
// Make sure user is an admin
is_admin(&local_user_view)?;
let shortcode = sanitize_html_api(data.shortcode.to_lowercase().trim());
let alt_text = sanitize_html_api(&data.alt_text);
let category = sanitize_html_api(&data.category);
let emoji_form = CustomEmojiInsertForm::builder()
.local_site_id(local_site.id)
.shortcode(shortcode)
.alt_text(alt_text)
.category(category)
.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 = CustomEmoji::create(&mut context.pool(), &emoji_form).await?;

View File

@ -2,8 +2,9 @@ use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
custom_emoji::{DeleteCustomEmoji, DeleteCustomEmojiResponse},
custom_emoji::DeleteCustomEmoji,
utils::is_admin,
SuccessResponse,
};
use lemmy_db_schema::source::custom_emoji::CustomEmoji;
use lemmy_db_views::structs::LocalUserView;
@ -14,12 +15,11 @@ pub async fn delete_custom_emoji(
data: Json<DeleteCustomEmoji>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<DeleteCustomEmojiResponse>, LemmyError> {
) -> Result<Json<SuccessResponse>, LemmyError> {
// Make sure user is an admin
is_admin(&local_user_view)?;
CustomEmoji::delete(&mut context.pool(), data.id).await?;
Ok(Json(DeleteCustomEmojiResponse {
id: data.id,
success: true,
}))
Ok(Json(SuccessResponse::default()))
}

View File

@ -3,7 +3,7 @@ use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
custom_emoji::{CustomEmojiResponse, EditCustomEmoji},
utils::{is_admin, sanitize_html_api},
utils::is_admin,
};
use lemmy_db_schema::source::{
custom_emoji::{CustomEmoji, CustomEmojiUpdateForm},
@ -23,13 +23,10 @@ pub async fn update_custom_emoji(
// Make sure user is an admin
is_admin(&local_user_view)?;
let alt_text = sanitize_html_api(&data.alt_text);
let category = sanitize_html_api(&data.category);
let emoji_form = CustomEmojiUpdateForm::builder()
.local_site_id(local_site.id)
.alt_text(alt_text)
.category(category)
.alt_text(data.alt_text.to_string())
.category(data.category.to_string())
.image_url(data.clone().image_url.into())
.build();
let emoji = CustomEmoji::update(&mut context.pool(), data.id, &emoji_form).await?;

View File

@ -7,14 +7,11 @@ use lemmy_api_common::{
request::fetch_site_data,
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_community_ban,
check_community_deleted_or_removed,
check_community_user_action,
generate_local_apub_endpoint,
honeypot_check,
local_site_to_slur_regex,
mark_post_as_read,
sanitize_html_api,
sanitize_html_api_opt,
EndpointType,
},
};
@ -62,13 +59,12 @@ pub async fn create_post(
is_valid_body_field(&data.body, true)?;
check_url_scheme(&data.url)?;
check_community_ban(
local_user_view.person.id,
check_community_user_action(
&local_user_view.person,
data.community_id,
&mut context.pool(),
)
.await?;
check_community_deleted_or_removed(data.community_id, &mut context.pool()).await?;
let community_id = data.community_id;
let community = Community::read(&mut context.pool(), community_id).await?;
@ -92,11 +88,6 @@ pub async fn create_post(
.map(|u| (u.title, u.description, u.embed_video_url))
.unwrap_or_default();
let name = sanitize_html_api(data.name.trim());
let body = sanitize_html_api_opt(&data.body);
let embed_title = sanitize_html_api_opt(&embed_title);
let embed_description = sanitize_html_api_opt(&embed_description);
// 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(
@ -120,9 +111,9 @@ pub async fn create_post(
};
let post_form = PostInsertForm::builder()
.name(name)
.name(data.name.trim().to_string())
.url(url)
.body(body)
.body(data.body.clone())
.community_id(data.community_id)
.creator_id(local_user_view.person.id)
.nsfw(data.nsfw)
@ -191,5 +182,5 @@ pub async fn create_post(
});
};
build_post_response(&context, community_id, person_id, post_id).await
build_post_response(&context, community_id, &local_user_view.person, post_id).await
}

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
context::LemmyContext,
post::{DeletePost, PostResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, check_community_deleted_or_removed},
utils::check_community_user_action,
};
use lemmy_db_schema::{
source::post::{Post, PostUpdateForm},
@ -28,13 +28,12 @@ pub async fn delete_post(
Err(LemmyErrorType::CouldntUpdatePost)?
}
check_community_ban(
local_user_view.person.id,
check_community_user_action(
&local_user_view.person,
orig_post.community_id,
&mut context.pool(),
)
.await?;
check_community_deleted_or_removed(orig_post.community_id, &mut context.pool()).await?;
// Verify that only the creator can delete
if !Post::is_post_creator(local_user_view.person.id, orig_post.creator_id) {
@ -52,12 +51,17 @@ pub async fn delete_post(
)
.await?;
let person_id = local_user_view.person.id;
ActivityChannel::submit_activity(
SendActivityData::DeletePost(post, local_user_view.person, data.0.clone()),
SendActivityData::DeletePost(post, local_user_view.person.clone(), data.0.clone()),
&context,
)
.await?;
build_post_response(&context, orig_post.community_id, person_id, data.post_id).await
build_post_response(
&context,
orig_post.community_id,
&local_user_view.person,
data.post_id,
)
.await
}

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
context::LemmyContext,
post::{PostResponse, RemovePost},
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, is_mod_or_admin},
utils::check_community_mod_action,
};
use lemmy_db_schema::{
source::{
@ -26,21 +26,14 @@ pub async fn remove_post(
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
check_community_ban(
local_user_view.person.id,
check_community_mod_action(
&local_user_view.person,
orig_post.community_id,
false,
&mut context.pool(),
)
.await?;
// Verify that only the mods can remove
is_mod_or_admin(
&mut context.pool(),
local_user_view.person.id,
orig_post.community_id,
)
.await?;
// Update the post
let post_id = data.post_id;
let removed = data.removed;
@ -63,12 +56,17 @@ pub async fn remove_post(
};
ModRemovePost::create(&mut context.pool(), &form).await?;
let person_id = local_user_view.person.id;
ActivityChannel::submit_activity(
SendActivityData::RemovePost(post, local_user_view.person, data.0),
SendActivityData::RemovePost(post, local_user_view.person.clone(), data.0),
&context,
)
.await?;
build_post_response(&context, orig_post.community_id, person_id, post_id).await
build_post_response(
&context,
orig_post.community_id,
&local_user_view.person,
post_id,
)
.await
}

View File

@ -6,7 +6,7 @@ use lemmy_api_common::{
post::{EditPost, PostResponse},
request::fetch_site_data,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_ban, local_site_to_slur_regex, sanitize_html_api_opt},
utils::{check_community_user_action, local_site_to_slur_regex},
};
use lemmy_db_schema::{
source::{
@ -55,8 +55,8 @@ pub async fn update_post(
let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?;
check_community_ban(
local_user_view.person.id,
check_community_user_action(
&local_user_view.person,
orig_post.community_id,
&mut context.pool(),
)
@ -75,12 +75,6 @@ pub async fn update_post(
.map(|u| (Some(u.title), Some(u.description), Some(u.embed_video_url)))
.unwrap_or_default();
let name = sanitize_html_api_opt(&data.name);
let body = sanitize_html_api_opt(&data.body);
let body = diesel_option_overwrite(body);
let embed_title = embed_title.map(|e| sanitize_html_api_opt(&e));
let embed_description = embed_description.map(|e| sanitize_html_api_opt(&e));
let language_id = data.language_id;
CommunityLanguage::is_allowed_community_language(
&mut context.pool(),
@ -90,9 +84,9 @@ pub async fn update_post(
.await?;
let post_form = PostUpdateForm {
name,
name: data.name.clone(),
url,
body,
body: diesel_option_overwrite(data.body.clone()),
nsfw: data.nsfw,
embed_title,
embed_description,
@ -113,7 +107,7 @@ pub async fn update_post(
build_post_response(
context.deref(),
orig_post.community_id,
local_user_view.person.id,
&local_user_view.person,
post_id,
)
.await

View File

@ -9,7 +9,6 @@ use lemmy_api_common::{
generate_local_apub_endpoint,
get_interface_language,
local_site_to_slur_regex,
sanitize_html_api,
send_email_to_user,
EndpointType,
},
@ -24,7 +23,7 @@ use lemmy_db_schema::{
use lemmy_db_views::structs::{LocalUserView, PrivateMessageView};
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
utils::{slurs::remove_slurs, validation::is_valid_body_field},
utils::{markdown::markdown_to_html, slurs::remove_slurs, validation::is_valid_body_field},
};
#[tracing::instrument(skip(context))]
@ -35,8 +34,7 @@ pub async fn create_private_message(
) -> Result<Json<PrivateMessageResponse>, LemmyError> {
let local_site = LocalSite::read(&mut context.pool()).await?;
let content = sanitize_html_api(&data.content);
let content = remove_slurs(&content, &local_site_to_slur_regex(&local_site));
let content = remove_slurs(&data.content, &local_site_to_slur_regex(&local_site));
is_valid_body_field(&Some(content.clone()), false)?;
check_person_block(
@ -83,6 +81,7 @@ pub async fn create_private_message(
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;
let content = markdown_to_html(&content);
send_email_to_user(
&local_recipient,
&lang.notification_private_message_subject(sender_name),

View File

@ -4,7 +4,7 @@ use lemmy_api_common::{
context::LemmyContext,
private_message::{EditPrivateMessage, PrivateMessageResponse},
send_activity::{ActivityChannel, SendActivityData},
utils::{local_site_to_slur_regex, sanitize_html_api},
utils::local_site_to_slur_regex,
};
use lemmy_db_schema::{
source::{
@ -36,8 +36,7 @@ pub async fn update_private_message(
}
// Doing the update
let content = sanitize_html_api(&data.content);
let content = remove_slurs(&content, &local_site_to_slur_regex(&local_site));
let content = remove_slurs(&data.content, &local_site_to_slur_regex(&local_site));
is_valid_body_field(&Some(content.clone()), false)?;
let private_message_id = data.private_message_id;

View File

@ -4,13 +4,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
site::{CreateSite, SiteResponse},
utils::{
generate_site_inbox_url,
is_admin,
local_site_rate_limit_to_rate_limit_config,
sanitize_html_api,
sanitize_html_api_opt,
},
utils::{generate_site_inbox_url, is_admin, local_site_rate_limit_to_rate_limit_config},
};
use lemmy_db_schema::{
newtypes::DbUrl,
@ -55,14 +49,11 @@ pub async fn create_site(
let actor_id: DbUrl = Url::parse(&context.settings().get_protocol_and_hostname())?.into();
let inbox_url = Some(generate_site_inbox_url(&actor_id)?);
let keypair = generate_actor_keypair()?;
let name = sanitize_html_api(&data.name);
let sidebar = sanitize_html_api_opt(&data.sidebar);
let description = sanitize_html_api_opt(&data.description);
let site_form = SiteUpdateForm {
name: Some(name),
sidebar: diesel_option_overwrite(sidebar),
description: diesel_option_overwrite(description),
name: Some(data.name.clone()),
sidebar: diesel_option_overwrite(data.sidebar.clone()),
description: diesel_option_overwrite(data.description.clone()),
icon: diesel_option_overwrite_to_url(&data.icon)?,
banner: diesel_option_overwrite_to_url(&data.banner)?,
actor_id: Some(actor_id),
@ -77,10 +68,6 @@ pub async fn create_site(
Site::update(&mut context.pool(), site_id, &site_form).await?;
let application_question = sanitize_html_api_opt(&data.application_question);
let default_theme = sanitize_html_api_opt(&data.default_theme);
let legal_information = sanitize_html_api_opt(&data.legal_information);
let local_site_form = LocalSiteUpdateForm {
// Set the site setup to true
site_setup: Some(true),
@ -89,11 +76,11 @@ pub async fn create_site(
enable_nsfw: data.enable_nsfw,
community_creation_admin_only: data.community_creation_admin_only,
require_email_verification: data.require_email_verification,
application_question: diesel_option_overwrite(application_question),
application_question: diesel_option_overwrite(data.application_question.clone()),
private_instance: data.private_instance,
default_theme,
default_theme: data.default_theme.clone(),
default_post_listing_type: data.default_post_listing_type,
legal_information: diesel_option_overwrite(legal_information),
legal_information: diesel_option_overwrite(data.legal_information.clone()),
application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names,
updated: Some(Some(naive_now())),
@ -132,10 +119,7 @@ pub async fn create_site(
let rate_limit_config =
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
context
.settings_updated_channel()
.send(rate_limit_config)
.await?;
context.rate_limit_cell().set_config(rate_limit_config);
Ok(Json(SiteResponse {
site_view,

View File

@ -3,7 +3,7 @@ use actix_web::web::{Data, Json};
use lemmy_api_common::{
context::LemmyContext,
site::{EditSite, SiteResponse},
utils::{is_admin, local_site_rate_limit_to_rate_limit_config, sanitize_html_api_opt},
utils::{is_admin, local_site_rate_limit_to_rate_limit_config},
};
use lemmy_db_schema::{
source::{
@ -54,14 +54,10 @@ pub async fn update_site(
SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
}
let name = sanitize_html_api_opt(&data.name);
let sidebar = sanitize_html_api_opt(&data.sidebar);
let description = sanitize_html_api_opt(&data.description);
let site_form = SiteUpdateForm {
name,
sidebar: diesel_option_overwrite(sidebar),
description: diesel_option_overwrite(description),
name: data.name.clone(),
sidebar: diesel_option_overwrite(data.sidebar.clone()),
description: diesel_option_overwrite(data.description.clone()),
icon: diesel_option_overwrite_to_url(&data.icon)?,
banner: diesel_option_overwrite_to_url(&data.banner)?,
updated: Some(Some(naive_now())),
@ -74,21 +70,17 @@ pub async fn update_site(
// Diesel will throw an error for empty update forms
.ok();
let application_question = sanitize_html_api_opt(&data.application_question);
let default_theme = sanitize_html_api_opt(&data.default_theme);
let legal_information = sanitize_html_api_opt(&data.legal_information);
let local_site_form = LocalSiteUpdateForm {
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_option_overwrite(application_question),
application_question: diesel_option_overwrite(data.application_question.clone()),
private_instance: data.private_instance,
default_theme,
default_theme: data.default_theme.clone(),
default_post_listing_type: data.default_post_listing_type,
legal_information: diesel_option_overwrite(legal_information),
legal_information: diesel_option_overwrite(data.legal_information.clone()),
application_email_admins: data.application_email_admins,
hide_modlog_mod_names: data.hide_modlog_mod_names,
updated: Some(Some(naive_now())),
@ -165,10 +157,7 @@ pub async fn update_site(
let rate_limit_config =
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);
context
.settings_updated_channel()
.send(rate_limit_config)
.await?;
context.rate_limit_cell().set_config(rate_limit_config);
Ok(Json(SiteResponse {
site_view,

View File

@ -12,8 +12,6 @@ use lemmy_api_common::{
honeypot_check,
local_site_to_slur_regex,
password_length_check,
sanitize_html_api,
sanitize_html_api_opt,
send_new_applicant_email_to_admins,
send_verification_email,
EndpointType,
@ -93,12 +91,6 @@ pub async fn register(
check_slurs(&data.username, &slur_regex)?;
check_slurs_opt(&data.answer, &slur_regex)?;
if sanitize_html_api(&data.username) != data.username {
Err(LemmyErrorType::InvalidName)?;
}
let answer = sanitize_html_api_opt(&data.answer);
let actor_keypair = generate_actor_keypair()?;
is_valid_actor_name(&data.username, local_site.actor_name_max_length as usize)?;
let actor_id = generate_local_apub_endpoint(
@ -154,7 +146,7 @@ pub async fn register(
let form = RegistrationApplicationInsertForm {
local_user_id: inserted_local_user.id,
// We already made sure answer was not null above
answer: answer.expect("must have an answer"),
answer: data.answer.clone().expect("must have an answer"),
};
RegistrationApplication::create(&mut context.pool(), &form).await?;

View File

@ -3,20 +3,21 @@ use actix_web::web::Json;
use bcrypt::verify;
use lemmy_api_common::{
context::LemmyContext,
person::{DeleteAccount, DeleteAccountResponse},
person::DeleteAccount,
send_activity::{ActivityChannel, SendActivityData},
utils::purge_user_account,
SuccessResponse,
};
use lemmy_db_schema::source::{login_token::LoginToken, person::Person};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))]
pub async fn delete_account(
data: Json<DeleteAccount>,
context: Data<LemmyContext>,
local_user_view: LocalUserView,
) -> Result<Json<DeleteAccountResponse>, LemmyError> {
) -> LemmyResult<Json<SuccessResponse>> {
// Verify the password
let valid: bool = verify(
&data.password,
@ -41,5 +42,5 @@ pub async fn delete_account(
)
.await?;
Ok(Json(DeleteAccountResponse {}))
Ok(Json(SuccessResponse::default()))
}

View File

@ -38,6 +38,8 @@ anyhow = { workspace = true }
reqwest = { workspace = true }
once_cell = { workspace = true }
html2md = "0.2.14"
html2text = "0.6.0"
stringreader = "0.1.1"
serde_with = { workspace = true }
enum_delegate = "0.2.0"
moka = { version = "0.11", features = ["future"] }

View File

@ -1,7 +1,7 @@
{
"id": "https://enterprise.lemmy.ml/c/tenforward",
"type": "Group",
"preferredUsername": "main",
"preferredUsername": "tenforward",
"name": "Ten Forward",
"summary": "<p>Lounge and recreation facility</p>\n<hr />\n<p>Welcome to the <a href=\"https://memory-alpha.fandom.com/wiki/USS_Enterprise_(NCC-1701-D)\">Enterprise</a>!.</p>\n",
"source": {

View File

@ -11,40 +11,42 @@
"votersCount": "toot:votersCount"
}
],
"id": "https://mastodon.madrid/users/felix/statuses/107224289116410645",
"id": "https://dice.camp/users/thekernelinyellow/statuses/110830743680706519",
"type": "Note",
"summary": null,
"published": "2021-11-05T11:46:50Z",
"url": "https://mastodon.madrid/@felix/107224289116410645",
"attributedTo": "https://mastodon.madrid/users/felix",
"to": ["https://mastodon.madrid/users/felix/followers"],
"inReplyTo": null,
"published": "2023-08-04T09:55:39Z",
"url": "https://dice.camp/@thekernelinyellow/110830743680706519",
"attributedTo": "https://dice.camp/users/thekernelinyellow",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": [
"https://www.w3.org/ns/activitystreams#Public",
"https://mamot.fr/users/retiolus"
"https://dice.camp/users/thekernelinyellow/followers",
"https://enterprise.lemmy.ml/c/tenforward",
"https://enterprise.lemmy.ml/c/tenforward/followers"
],
"sensitive": false,
"atomUri": "https://mastodon.madrid/users/felix/statuses/107224289116410645",
"inReplyToAtomUri": "https://mamot.fr/users/retiolus/statuses/107224244380204526",
"conversation": "tag:mamot.fr,2021-11-05:objectId=64635960:objectType=Conversation",
"content": "<p><span class=\"h-card\"><a href=\"https://mamot.fr/@retiolus\" class=\"u-url mention\">@<span>retiolus</span></a></span> i have never been disappointed by a thinkpad. if you want to save money, get a model from a few years ago, there isnt a huge difference anyway.</p>",
"atomUri": "https://dice.camp/users/thekernelinyellow/statuses/110830743680706519",
"inReplyToAtomUri": null,
"conversation": "tag:dice.camp,2023-08-04:objectId=29969291:objectType=Conversation",
"content": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://enterprise.lemmy.ml/c/tenforward\" class=\"u-url mention\">@<span>tenforward</span></a></span> Variable never resetting at refresh</p><p>Hi! I&#39;m using a variable to count elements in my generator but every time I generate a new character, the counter&#39;s value carries on from the previous one. Is there a function to reset it (I set it to 0 at the beginning of the file)</p>",
"contentMap": {
"en": "<p><span class=\"h-card\"><a href=\"https://mamot.fr/@retiolus\" class=\"u-url mention\">@<span>retiolus</span></a></span> i have never been disappointed by a thinkpad. if you want to save money, get a model from a few years ago, there isnt a huge difference anyway.</p>"
"it": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://enterprise.lemmy.ml/c/tenforward\" class=\"u-url mention\">@<span>tenforward</span></a></span>Variable never resetting at refresh</p><p>Hi! I&#39;m using a variable to count elements in my generator but every time I generate a new character, the counter&#39;s value carries on from the previous one. Is there a function to reset it (I set it to 0 at the beginning of the file)</p>"
},
"attachment": [],
"tag": [
{
"type": "Mention",
"href": "https://mamot.fr/users/retiolus",
"name": "@retiolus@mamot.fr"
"href": "https://enterprise.lemmy.ml/c/tenforward",
"name": "@tenforward@enterprise.lemmy.ml"
}
],
"replies": {
"id": "https://mastodon.madrid/users/felix/statuses/107224289116410645/replies",
"id": "https://dice.camp/users/thekernelinyellow/statuses/110830743680706519/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://mastodon.madrid/users/felix/statuses/107224289116410645/replies?only_other_accounts=true&page=true",
"partOf": "https://mastodon.madrid/users/felix/statuses/107224289116410645/replies",
"next": "https://dice.camp/users/thekernelinyellow/statuses/110830743680706519/replies?only_other_accounts=true&page=true",
"partOf": "https://dice.camp/users/thekernelinyellow/statuses/110830743680706519/replies",
"items": []
}
}

View File

@ -23,7 +23,7 @@ use anyhow::anyhow;
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{remove_user_data, remove_user_data_in_community, sanitize_html_federation_opt},
utils::{remove_user_data, remove_user_data_in_community},
};
use lemmy_db_schema::{
source::{
@ -173,7 +173,7 @@ impl ActivityHandler for BlockUser {
let form = ModBanForm {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
reason: sanitize_html_federation_opt(&self.summary),
reason: self.summary,
banned: Some(true),
expires,
};
@ -207,7 +207,7 @@ impl ActivityHandler for BlockUser {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
community_id: community.id,
reason: sanitize_html_federation_opt(&self.summary),
reason: self.summary,
banned: Some(true),
expires,
};

View File

@ -11,7 +11,12 @@ use activitypub_federation::{
traits::{Actor, Object},
};
use chrono::{DateTime, Utc};
use lemmy_api_common::{community::BanFromCommunity, context::LemmyContext, person::BanPerson};
use lemmy_api_common::{
community::BanFromCommunity,
context::LemmyContext,
person::BanPerson,
utils::check_expire_time,
};
use lemmy_db_schema::{
newtypes::CommunityId,
source::{community::Community, person::Person, site::Site},
@ -19,10 +24,7 @@ use lemmy_db_schema::{
utils::DbPool,
};
use lemmy_db_views::structs::SiteView;
use lemmy_utils::{
error::{LemmyError, LemmyResult},
utils::time::naive_from_unix,
};
use lemmy_utils::error::{LemmyError, LemmyResult};
use serde::Deserialize;
use url::Url;
@ -137,7 +139,7 @@ pub(crate) async fn send_ban_from_site(
context: Data<LemmyContext>,
) -> Result<(), LemmyError> {
let site = SiteOrCommunity::Site(SiteView::read_local(&mut context.pool()).await?.site.into());
let expires = data.expires.map(naive_from_unix);
let expires = check_expire_time(data.expires)?;
// if the action affects a local user, federate to other instances
if banned_user.local {
@ -177,7 +179,7 @@ pub(crate) async fn send_ban_from_community(
let community: ApubCommunity = Community::read(&mut context.pool(), community_id)
.await?
.into();
let expires = data.expires.map(naive_from_unix);
let expires = check_expire_time(data.expires)?;
if data.ban {
BlockUser::send(

View File

@ -17,7 +17,7 @@ use activitypub_federation::{
protocol::verification::verify_domains_match,
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::{context::LemmyContext, utils::sanitize_html_federation_opt};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
@ -118,7 +118,7 @@ impl ActivityHandler for UndoBlockUser {
let form = ModBanForm {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
reason: sanitize_html_federation_opt(&self.object.summary),
reason: self.object.summary,
banned: Some(false),
expires,
};
@ -137,7 +137,7 @@ impl ActivityHandler for UndoBlockUser {
mod_person_id: mod_person.id,
other_person_id: blocked_person.id,
community_id: community.id,
reason: sanitize_html_federation_opt(&self.object.summary),
reason: self.object.summary,
banned: Some(false),
expires,
};

View File

@ -22,8 +22,8 @@ use activitypub_federation::{
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::source::activity::ActivitySendTargets;
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use lemmy_db_schema::source::{activity::ActivitySendTargets, community::CommunityFollower};
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
use serde_json::Value;
use url::Url;
@ -46,24 +46,28 @@ impl ActivityHandler for RawAnnouncableActivities {
}
#[tracing::instrument(skip_all)]
async fn receive(self, data: &Data<Self::DataType>) -> Result<(), Self::Error> {
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
let activity: AnnouncableActivities = self.clone().try_into()?;
// This is only for sending, not receiving so we reject it.
if let AnnouncableActivities::Page(_) = activity {
Err(LemmyErrorType::CannotReceivePage)?
}
// verify and receive activity
activity.verify(data).await?;
activity.clone().receive(data).await?;
// Need to treat community as optional here because `Delete/PrivateMessage` gets routed through
let community = activity.community(context).await.ok();
can_accept_activity_in_community(&community, context).await?;
// if activity is in a community, send to followers
let community = activity.community(data).await;
if let Ok(community) = community {
// verify and receive activity
activity.verify(context).await?;
activity.clone().receive(context).await?;
// if community is local, send activity to followers
if let Some(community) = community {
if community.local {
let actor_id = activity.actor().clone().into();
verify_person_in_community(&actor_id, &community, data).await?;
AnnounceActivity::send(self, &community, data).await?;
verify_person_in_community(&actor_id, &community, context).await?;
AnnounceActivity::send(self, &community, context).await?;
}
}
Ok(())
@ -150,11 +154,15 @@ impl ActivityHandler for AnnounceActivity {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), LemmyError> {
let object: AnnouncableActivities = self.object.object(context).await?.try_into()?;
// This is only for sending, not receiving so we reject it.
if let AnnouncableActivities::Page(_) = object {
Err(LemmyErrorType::CannotReceivePage)?
}
let community = object.community(context).await?;
can_accept_activity_in_community(&Some(community), context).await?;
// verify here in order to avoid fetching the object twice over http
object.verify(context).await?;
object.receive(context).await
@ -185,3 +193,23 @@ impl TryFrom<AnnouncableActivities> for RawAnnouncableActivities {
serde_json::from_value(serde_json::to_value(value)?)
}
}
/// Check if an activity in the given community can be accepted. To return true, the community must
/// either be local to this instance, or it must have at least one local follower.
///
/// TODO: This means mentions dont work if the community has no local followers. Can be fixed
/// by checking if any local user is in to/cc fields of activity. Anyway this is a minor
/// problem compared to receiving unsolicited posts.
async fn can_accept_activity_in_community(
community: &Option<ApubCommunity>,
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
if let Some(community) = community {
if !community.local
&& !CommunityFollower::has_local_followers(&mut context.pool(), community.id).await?
{
Err(LemmyErrorType::CommunityHasNoFollowers)?
}
}
Ok(())
}

View File

@ -11,7 +11,7 @@ use activitypub_federation::{
kinds::activity::FlagType,
traits::{ActivityHandler, Actor},
};
use lemmy_api_common::{context::LemmyContext, utils::sanitize_html_federation};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
activity::ActivitySendTargets,
@ -90,7 +90,7 @@ impl ActivityHandler for Report {
post_id: post.id,
original_post_name: post.name.clone(),
original_post_url: post.url.clone(),
reason: sanitize_html_federation(&self.summary),
reason: self.summary.clone(),
original_post_body: post.body.clone(),
};
PostReport::report(&mut context.pool(), &report_form).await?;
@ -100,7 +100,7 @@ impl ActivityHandler for Report {
creator_id: actor.id,
comment_id: comment.id,
original_comment_text: comment.content.clone(),
reason: sanitize_html_federation(&self.summary),
reason: self.summary.clone(),
};
CommentReport::report(&mut context.pool(), &report_form).await?;
}

View File

@ -140,7 +140,7 @@ impl ActivityHandler for CreateOrUpdateNote {
if distinguished != existing_comment.distinguished {
let creator = self.actor.dereference(context).await?;
let (post, _) = self.object.get_parents(context).await?;
is_mod_or_admin(&mut context.pool(), creator.id, post.community_id).await?;
is_mod_or_admin(&mut context.pool(), &creator, post.community_id).await?;
}
}

View File

@ -8,7 +8,7 @@ use crate::{
protocol::{activities::deletion::delete::Delete, IdOrNestedObject},
};
use activitypub_federation::{config::Data, kinds::activity::DeleteType, traits::ActivityHandler};
use lemmy_api_common::{context::LemmyContext, utils::sanitize_html_federation_opt};
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
comment::{Comment, CommentUpdateForm},
@ -105,8 +105,6 @@ pub(in crate::activities) async fn receive_remove_action(
reason: Option<String>,
context: &Data<LemmyContext>,
) -> Result<(), LemmyError> {
let reason = sanitize_html_federation_opt(&reason);
match DeletableObjects::read_from_db(object, context).await? {
DeletableObjects::Community(community) => {
if community.local {
@ -117,7 +115,6 @@ pub(in crate::activities) async fn receive_remove_action(
community_id: community.id,
removed: Some(true),
reason,
expires: None,
};
ModRemoveCommunity::create(&mut context.pool(), &form).await?;
Community::update(

View File

@ -107,7 +107,6 @@ impl UndoDelete {
community_id: community.id,
removed: Some(false),
reason: None,
expires: None,
};
ModRemoveCommunity::create(&mut context.pool(), &form).await?;
Community::update(

View File

@ -44,7 +44,7 @@ use lemmy_db_schema::source::{
use lemmy_db_views_actor::structs::{CommunityPersonBanView, CommunityView};
use lemmy_utils::error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult};
use serde::Serialize;
use std::{ops::Deref, time::Duration};
use std::ops::Deref;
use tracing::info;
use url::{ParseError, Url};
use uuid::Uuid;
@ -56,10 +56,6 @@ pub mod deletion;
pub mod following;
pub mod voting;
/// Amount of time that the list of dead instances is cached. This is only updated once a day,
/// so there is no harm in caching it for a longer time.
pub static DEAD_INSTANCE_LIST_CACHE_DURATION: Duration = Duration::from_secs(30 * 60);
/// Checks that the specified Url actually identifies a Person (by fetching it), and that the person
/// doesn't have a site ban.
#[tracing::instrument(skip_all)]
@ -92,9 +88,7 @@ pub(crate) async fn verify_person_in_community(
}
let person_id = person.id;
let community_id = community.id;
let is_banned = CommunityPersonBanView::get(&mut context.pool(), person_id, community_id)
.await
.is_ok();
let is_banned = CommunityPersonBanView::get(&mut context.pool(), person_id, community_id).await?;
if is_banned {
Err(LemmyErrorType::PersonIsBannedFromCommunity)?
} else {

View File

@ -7,6 +7,7 @@ pub mod read_community;
pub mod read_person;
pub mod resolve_object;
pub mod search;
pub mod user_settings_backup;
/// Returns default listing type, depending if the query is for frontpage or community.
fn listing_type_with_default(

View File

@ -8,7 +8,7 @@ use lemmy_api_common::{
};
use lemmy_db_schema::{
source::{community::Community, local_site::LocalSite},
utils::{post_to_comment_sort_type, post_to_person_sort_type},
utils::post_to_comment_sort_type,
SearchType,
};
use lemmy_db_views::{comment_view::CommentQuery, post_view::PostQuery, structs::LocalUserView};
@ -101,7 +101,7 @@ pub async fn search(
}
SearchType::Users => {
users = PersonQuery {
sort: (sort.map(post_to_person_sort_type)),
sort,
search_term: (Some(q)),
page: (page),
limit: (limit),
@ -171,7 +171,7 @@ pub async fn search(
vec![]
} else {
PersonQuery {
sort: (sort.map(post_to_person_sort_type)),
sort,
search_term: (Some(q)),
page: (page),
limit: (limit),

View File

@ -0,0 +1,441 @@
use crate::objects::{
comment::ApubComment,
community::ApubCommunity,
person::ApubPerson,
post::ApubPost,
};
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
use actix_web::web::Json;
use futures::{future::try_join_all, StreamExt};
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
use lemmy_db_schema::{
newtypes::DbUrl,
source::{
comment::{CommentSaved, CommentSavedForm},
community::{CommunityFollower, CommunityFollowerForm},
community_block::{CommunityBlock, CommunityBlockForm},
local_user::{LocalUser, LocalUserUpdateForm},
person::{Person, PersonUpdateForm},
person_block::{PersonBlock, PersonBlockForm},
post::{PostSaved, PostSavedForm},
},
traits::{Blockable, Crud, Followable, Saveable},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::{
error::{LemmyError, LemmyErrorType, LemmyResult, MAX_API_PARAM_ELEMENTS},
spawn_try_task,
};
use serde::{Deserialize, Serialize};
use tracing::info;
/// Backup of user data. This struct should never be changed so that the data can be used as a
/// long-term backup in case the instance goes down unexpectedly. All fields are optional to allow
/// importing partial backups.
///
/// This data should not be parsed by apps/clients, but directly downloaded as a file.
///
/// Be careful with any changes to this struct, to avoid breaking changes which could prevent
/// importing older backups.
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UserSettingsBackup {
pub display_name: Option<String>,
pub bio: Option<String>,
pub avatar: Option<DbUrl>,
pub banner: Option<DbUrl>,
pub matrix_id: Option<String>,
pub bot_account: Option<bool>,
// TODO: might be worth making a separate struct for settings backup, to avoid breakage in case
// fields are renamed, and to avoid storing unnecessary fields like person_id or email
pub settings: Option<LocalUser>,
#[serde(default)]
pub followed_communities: Vec<ObjectId<ApubCommunity>>,
#[serde(default)]
pub saved_posts: Vec<ObjectId<ApubPost>>,
#[serde(default)]
pub saved_comments: Vec<ObjectId<ApubComment>>,
#[serde(default)]
pub blocked_communities: Vec<ObjectId<ApubCommunity>>,
#[serde(default)]
pub blocked_users: Vec<ObjectId<ApubPerson>>,
}
#[tracing::instrument(skip(context))]
pub async fn export_settings(
local_user_view: LocalUserView,
context: Data<LemmyContext>,
) -> Result<Json<UserSettingsBackup>, LemmyError> {
let lists = LocalUser::export_backup(&mut context.pool(), local_user_view.person.id).await?;
let vec_into = |vec: Vec<_>| vec.into_iter().map(Into::into).collect();
Ok(Json(UserSettingsBackup {
display_name: local_user_view.person.display_name,
bio: local_user_view.person.bio,
avatar: local_user_view.person.avatar,
banner: local_user_view.person.banner,
matrix_id: local_user_view.person.matrix_user_id,
bot_account: local_user_view.person.bot_account.into(),
settings: Some(local_user_view.local_user),
followed_communities: vec_into(lists.followed_communities),
blocked_communities: vec_into(lists.blocked_communities),
blocked_users: lists.blocked_users.into_iter().map(Into::into).collect(),
saved_posts: lists.saved_posts.into_iter().map(Into::into).collect(),
saved_comments: lists.saved_comments.into_iter().map(Into::into).collect(),
}))
}
#[tracing::instrument(skip(context))]
pub async fn import_settings(
data: Json<UserSettingsBackup>,
local_user_view: LocalUserView,
context: Data<LemmyContext>,
) -> Result<Json<SuccessResponse>, LemmyError> {
let person_form = PersonUpdateForm {
display_name: Some(data.display_name.clone()),
bio: Some(data.bio.clone()),
matrix_user_id: Some(data.matrix_id.clone()),
bot_account: data.bot_account,
..Default::default()
};
Person::update(&mut context.pool(), local_user_view.person.id, &person_form).await?;
let local_user_form = LocalUserUpdateForm {
show_nsfw: data.settings.as_ref().map(|s| s.show_nsfw),
theme: data.settings.as_ref().map(|s| s.theme.clone()),
default_sort_type: data.settings.as_ref().map(|s| s.default_sort_type),
default_listing_type: data.settings.as_ref().map(|s| s.default_listing_type),
interface_language: data.settings.as_ref().map(|s| s.interface_language.clone()),
show_avatars: data.settings.as_ref().map(|s| s.show_avatars),
send_notifications_to_email: data
.settings
.as_ref()
.map(|s| s.send_notifications_to_email),
show_scores: data.settings.as_ref().map(|s| s.show_scores),
show_bot_accounts: data.settings.as_ref().map(|s| s.show_bot_accounts),
show_read_posts: data.settings.as_ref().map(|s| s.show_read_posts),
open_links_in_new_tab: data.settings.as_ref().map(|s| s.open_links_in_new_tab),
blur_nsfw: data.settings.as_ref().map(|s| s.blur_nsfw),
auto_expand: data.settings.as_ref().map(|s| s.auto_expand),
infinite_scroll_enabled: data.settings.as_ref().map(|s| s.infinite_scroll_enabled),
post_listing_mode: data.settings.as_ref().map(|s| s.post_listing_mode),
..Default::default()
};
LocalUser::update(
&mut context.pool(),
local_user_view.local_user.id,
&local_user_form,
)
.await?;
let url_count = data.followed_communities.len()
+ data.blocked_communities.len()
+ data.blocked_users.len()
+ data.saved_posts.len()
+ data.saved_comments.len();
if url_count > MAX_API_PARAM_ELEMENTS {
Err(LemmyErrorType::TooManyItems)?;
}
spawn_try_task(async move {
const PARALLELISM: usize = 10;
let person_id = local_user_view.person.id;
// These tasks fetch objects from remote instances which might be down.
// TODO: Would be nice if we could send a list of failed items with api response, but then
// the request would likely timeout.
let mut failed_items = vec![];
info!(
"Starting settings backup for {}",
local_user_view.person.name
);
futures::stream::iter(
data
.followed_communities
.clone()
.into_iter()
// reset_request_count works like clone, and is necessary to avoid running into request limit
.map(|f| (f, context.reset_request_count()))
.map(|(followed, context)| async move {
// need to reset outgoing request count to avoid running into limit
let community = followed.dereference(&context).await?;
let form = CommunityFollowerForm {
person_id,
community_id: community.id,
pending: true,
};
CommunityFollower::follow(&mut context.pool(), &form).await?;
LemmyResult::Ok(())
}),
)
.buffer_unordered(PARALLELISM)
.collect::<Vec<_>>()
.await
.into_iter()
.enumerate()
.for_each(|(i, r)| {
if let Err(e) = r {
failed_items.push(data.followed_communities.get(i).map(|u| u.inner().clone()));
info!("Failed to import followed community: {e}");
}
});
futures::stream::iter(
data
.saved_posts
.clone()
.into_iter()
.map(|s| (s, context.reset_request_count()))
.map(|(saved, context)| async move {
let post = saved.dereference(&context).await?;
let form = PostSavedForm {
person_id,
post_id: post.id,
};
PostSaved::save(&mut context.pool(), &form).await?;
LemmyResult::Ok(())
}),
)
.buffer_unordered(PARALLELISM)
.collect::<Vec<_>>()
.await
.into_iter()
.enumerate()
.for_each(|(i, r)| {
if let Err(e) = r {
failed_items.push(data.followed_communities.get(i).map(|u| u.inner().clone()));
info!("Failed to import saved post community: {e}");
}
});
futures::stream::iter(
data
.saved_comments
.clone()
.into_iter()
.map(|s| (s, context.reset_request_count()))
.map(|(saved, context)| async move {
let comment = saved.dereference(&context).await?;
let form = CommentSavedForm {
person_id,
comment_id: comment.id,
};
CommentSaved::save(&mut context.pool(), &form).await?;
LemmyResult::Ok(())
}),
)
.buffer_unordered(PARALLELISM)
.collect::<Vec<_>>()
.await
.into_iter()
.enumerate()
.for_each(|(i, r)| {
if let Err(e) = r {
failed_items.push(data.followed_communities.get(i).map(|u| u.inner().clone()));
info!("Failed to import saved comment community: {e}");
}
});
let failed_items: Vec<_> = failed_items.into_iter().flatten().collect();
info!(
"Finished settings backup for {}, failed items: {:#?}",
local_user_view.person.name, failed_items
);
// These tasks don't connect to any remote instances but only insert directly in the database.
// That means the only error condition are db connection failures, so no extra error handling is
// needed.
try_join_all(data.blocked_communities.iter().map(|blocked| async {
// dont fetch unknown blocked objects from home server
let community = blocked.dereference_local(&context).await?;
let form = CommunityBlockForm {
person_id,
community_id: community.id,
};
CommunityBlock::block(&mut context.pool(), &form).await?;
LemmyResult::Ok(())
}))
.await?;
try_join_all(data.blocked_users.iter().map(|blocked| async {
// dont fetch unknown blocked objects from home server
let target = blocked.dereference_local(&context).await?;
let form = PersonBlockForm {
person_id,
target_id: target.id,
};
PersonBlock::block(&mut context.pool(), &form).await?;
LemmyResult::Ok(())
}))
.await?;
Ok(())
});
Ok(Json(Default::default()))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
api::user_settings_backup::{export_settings, import_settings},
objects::tests::init_context,
};
use activitypub_federation::config::Data;
use lemmy_api_common::context::LemmyContext;
use lemmy_db_schema::{
source::{
community::{Community, CommunityFollower, CommunityFollowerForm, CommunityInsertForm},
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
person::{Person, PersonInsertForm},
},
traits::{Crud, Followable},
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::LemmyErrorType;
use serial_test::serial;
use std::time::Duration;
use tokio::time::sleep;
async fn create_user(
name: String,
bio: Option<String>,
context: &Data<LemmyContext>,
) -> LocalUserView {
let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string())
.await
.unwrap();
let person_form = PersonInsertForm::builder()
.name(name.clone())
.display_name(Some(name.clone()))
.bio(bio)
.public_key("asd".to_string())
.instance_id(instance.id)
.build();
let person = Person::create(&mut context.pool(), &person_form)
.await
.unwrap();
let user_form = LocalUserInsertForm::builder()
.person_id(person.id)
.password_encrypted("pass".to_string())
.build();
let local_user = LocalUser::create(&mut context.pool(), &user_form)
.await
.unwrap();
LocalUserView::read(&mut context.pool(), local_user.id)
.await
.unwrap()
}
#[tokio::test]
#[serial]
async fn test_settings_export_import() {
let context = init_context().await;
let export_user = create_user("hanna".to_string(), Some("my bio".to_string()), &context).await;
let community_form = CommunityInsertForm::builder()
.name("testcom".to_string())
.title("testcom".to_string())
.instance_id(export_user.person.instance_id)
.build();
let community = Community::create(&mut context.pool(), &community_form)
.await
.unwrap();
let follower_form = CommunityFollowerForm {
community_id: community.id,
person_id: export_user.person.id,
pending: false,
};
CommunityFollower::follow(&mut context.pool(), &follower_form)
.await
.unwrap();
let backup = export_settings(export_user.clone(), context.reset_request_count())
.await
.unwrap();
let import_user = create_user("charles".to_string(), None, &context).await;
import_settings(backup, import_user.clone(), context.reset_request_count())
.await
.unwrap();
// wait for background task to finish
sleep(Duration::from_millis(1000)).await;
let import_user_updated = LocalUserView::read(&mut context.pool(), import_user.local_user.id)
.await
.unwrap();
assert_eq!(
export_user.person.display_name,
import_user_updated.person.display_name
);
assert_eq!(export_user.person.bio, import_user_updated.person.bio);
let follows = CommunityFollowerView::for_person(&mut context.pool(), import_user.person.id)
.await
.unwrap();
assert_eq!(follows.len(), 1);
assert_eq!(follows[0].community.actor_id, community.actor_id);
LocalUser::delete(&mut context.pool(), export_user.local_user.id)
.await
.unwrap();
LocalUser::delete(&mut context.pool(), import_user.local_user.id)
.await
.unwrap();
}
#[tokio::test]
#[serial]
async fn disallow_large_backup() {
let context = init_context().await;
let export_user = create_user("hanna".to_string(), Some("my bio".to_string()), &context).await;
let mut backup = export_settings(export_user.clone(), context.reset_request_count())
.await
.unwrap();
for _ in 0..251 {
backup
.followed_communities
.push("http://example.com".parse().unwrap());
backup
.blocked_communities
.push("http://example2.com".parse().unwrap());
backup
.saved_posts
.push("http://example3.com".parse().unwrap());
backup
.saved_comments
.push("http://example4.com".parse().unwrap());
}
let import_user = create_user("charles".to_string(), None, &context).await;
let imported =
import_settings(backup, import_user.clone(), context.reset_request_count()).await;
assert_eq!(
imported.err().unwrap().error_type,
LemmyErrorType::TooManyItems
);
LocalUser::delete(&mut context.pool(), export_user.local_user.id)
.await
.unwrap();
LocalUser::delete(&mut context.pool(), import_user.local_user.id)
.await
.unwrap();
}
}

View File

@ -0,0 +1,66 @@
use crate::{
objects::community::ApubCommunity,
protocol::collections::group_followers::GroupFollowers,
};
use activitypub_federation::{
config::Data,
kinds::collection::CollectionType,
protocol::verification::verify_domains_match,
traits::Collection,
};
use lemmy_api_common::{context::LemmyContext, utils::generate_followers_url};
use lemmy_db_schema::aggregates::structs::CommunityAggregates;
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::LemmyError;
use url::Url;
#[derive(Clone, Debug)]
pub(crate) struct ApubCommunityFollower(Vec<()>);
#[async_trait::async_trait]
impl Collection for ApubCommunityFollower {
type Owner = ApubCommunity;
type DataType = LemmyContext;
type Kind = GroupFollowers;
type Error = LemmyError;
async fn read_local(
community: &Self::Owner,
context: &Data<Self::DataType>,
) -> Result<Self::Kind, Self::Error> {
let community_id = community.id;
let community_followers =
CommunityFollowerView::count_community_followers(&mut context.pool(), community_id).await?;
Ok(GroupFollowers {
id: generate_followers_url(&community.actor_id)?.into(),
r#type: CollectionType::Collection,
total_items: community_followers as i32,
items: vec![],
})
}
async fn verify(
json: &Self::Kind,
expected_domain: &Url,
_data: &Data<Self::DataType>,
) -> Result<(), Self::Error> {
verify_domains_match(expected_domain, &json.id)?;
Ok(())
}
async fn from_json(
json: Self::Kind,
community: &Self::Owner,
context: &Data<Self::DataType>,
) -> Result<Self, Self::Error> {
CommunityAggregates::update_federated_followers(
&mut context.pool(),
community.id,
json.total_items,
)
.await?;
Ok(ApubCommunityFollower(Vec::new()))
}
}

View File

@ -1,3 +1,4 @@
pub(crate) mod community_featured;
pub(crate) mod community_follower;
pub(crate) mod community_moderators;
pub(crate) mod community_outbox;

View File

@ -2,12 +2,12 @@ use crate::{
activity_lists::GroupInboxActivities,
collections::{
community_featured::ApubCommunityFeatured,
community_follower::ApubCommunityFollower,
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
},
http::{create_apub_response, create_apub_tombstone_response},
objects::{community::ApubCommunity, person::ApubPerson},
protocol::collections::group_followers::GroupFollowers,
};
use activitypub_federation::{
actix_web::inbox::receive_activity,
@ -66,7 +66,7 @@ pub(crate) async fn get_apub_community_followers(
) -> Result<HttpResponse, LemmyError> {
let community =
Community::read_from_name(&mut context.pool(), &info.community_name, false).await?;
let followers = GroupFollowers::new(community, &context).await?;
let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?;
create_apub_response(&followers)
}

View File

@ -16,10 +16,7 @@ use activitypub_federation::{
traits::Object,
};
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{local_site_opt_to_slur_regex, sanitize_html_federation},
};
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_db_schema::{
source::{
comment::{Comment, CommentInsertForm, CommentUpdateForm},
@ -32,7 +29,7 @@ use lemmy_db_schema::{
};
use lemmy_utils::{
error::{LemmyError, LemmyErrorType},
utils::{markdown::markdown_to_html, slurs::remove_slurs, time::convert_datetime},
utils::{markdown::markdown_to_html, slurs::remove_slurs},
};
use std::ops::Deref;
use url::Url;
@ -116,8 +113,8 @@ impl Object for ApubComment {
media_type: Some(MediaTypeMarkdownOrHtml::Html),
source: Some(Source::new(self.content.clone())),
in_reply_to,
published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
published: Some(self.published),
updated: self.updated,
tag: maa.tags,
distinguished: Some(self.distinguished),
language,
@ -162,7 +159,6 @@ impl Object for ApubComment {
let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let content = remove_slurs(&content, slur_regex);
let content = sanitize_html_federation(&content);
let language_id =
LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?;

View File

@ -28,10 +28,7 @@ use lemmy_db_schema::{
traits::{ApubActor, Crud},
};
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::{
error::LemmyError,
utils::{markdown::markdown_to_html, time::convert_datetime},
};
use lemmy_utils::{error::LemmyError, utils::markdown::markdown_to_html};
use std::ops::Deref;
use tracing::debug;
use url::Url;
@ -109,8 +106,8 @@ impl Object for ApubCommunity {
}),
public_key: self.public_key(),
language,
published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
published: Some(self.published),
updated: self.updated,
posting_restricted_to_mods: Some(self.posting_restricted_to_mods),
attributed_to: Some(generate_moderators_url(&self.actor_id)?.into()),
};
@ -146,15 +143,19 @@ impl Object for ApubCommunity {
// Fetching mods and outbox is not necessary for Lemmy to work, so ignore errors. Besides,
// we need to ignore these errors so that tests can work entirely offline.
let fetch_outbox = group.outbox.dereference(&community, context);
let fetch_followers = group.followers.dereference(&community, context);
if let Some(moderators) = group.attributed_to {
let fetch_moderators = moderators.dereference(&community, context);
// Fetch mods and outbox in parallel
let res = tokio::join!(fetch_outbox, fetch_moderators);
// Fetch mods, outbox and followers in parallel
let res = tokio::join!(fetch_outbox, fetch_moderators, fetch_followers);
res.0.map_err(|e| debug!("{}", e)).ok();
res.1.map_err(|e| debug!("{}", e)).ok();
res.2.map_err(|e| debug!("{}", e)).ok();
} else {
fetch_outbox.await.map_err(|e| debug!("{}", e)).ok();
let res = tokio::join!(fetch_outbox, fetch_followers);
res.0.map_err(|e| debug!("{}", e)).ok();
res.1.map_err(|e| debug!("{}", e)).ok();
}
Ok(community)
@ -235,12 +236,14 @@ pub(crate) mod tests {
json.attributed_to = None;
json.outbox =
CollectionId::parse("https://enterprise.lemmy.ml/c/tenforward/not_outbox").unwrap();
json.followers =
CollectionId::parse("https://enterprise.lemmy.ml/c/tenforward/not_followers").unwrap();
let url = Url::parse("https://enterprise.lemmy.ml/c/tenforward").unwrap();
ApubCommunity::verify(&json, &url, &context2).await.unwrap();
let community = ApubCommunity::from_json(json, &context2).await.unwrap();
// this makes one requests to the (intentionally broken) outbox collection
assert_eq!(context2.request_count(), 1);
// this makes requests to the (intentionally broken) outbox and followers collections
assert_eq!(context2.request_count(), 2);
community
}

View File

@ -17,10 +17,7 @@ use activitypub_federation::{
traits::{Actor, Object},
};
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{local_site_opt_to_slur_regex, sanitize_html_federation_opt},
};
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_db_schema::{
newtypes::InstanceId,
source::{
@ -37,7 +34,6 @@ use lemmy_utils::{
utils::{
markdown::markdown_to_html,
slurs::{check_slurs, check_slurs_opt},
time::convert_datetime,
},
};
use std::ops::Deref;
@ -106,8 +102,8 @@ impl Object for ApubSite {
outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?,
public_key: self.public_key(),
language,
published: convert_datetime(self.published),
updated: self.updated.map(convert_datetime),
published: self.published,
updated: self.updated,
};
Ok(instance)
}
@ -135,8 +131,6 @@ impl Object for ApubSite {
let instance = DbInstance::read_or_create(&mut data.pool(), domain.to_string()).await?;
let sidebar = read_from_string_or_source_opt(&apub.content, &None, &apub.source);
let sidebar = sanitize_html_federation_opt(&sidebar);
let description = sanitize_html_federation_opt(&apub.summary);
let site_form = SiteInsertForm {
name: apub.name.clone(),
@ -144,7 +138,7 @@ impl Object for ApubSite {
updated: apub.updated,
icon: apub.icon.clone().map(|i| i.url.into()),
banner: apub.image.clone().map(|i| i.url.into()),
description,
description: apub.summary,
actor_id: Some(apub.id.clone().into()),
last_refreshed_at: Some(naive_now()),
inbox_url: Some(apub.inbox.clone().into()),

View File

@ -61,10 +61,7 @@ pub(crate) mod tests {
use anyhow::anyhow;
use lemmy_api_common::{context::LemmyContext, request::build_user_agent};
use lemmy_db_schema::{source::secret::Secret, utils::build_db_pool_for_tests};
use lemmy_utils::{
rate_limit::{RateLimitCell, RateLimitConfig},
settings::SETTINGS,
};
use lemmy_utils::{rate_limit::RateLimitCell, settings::SETTINGS};
use reqwest::{Client, Request, Response};
use reqwest_middleware::{ClientBuilder, Middleware, Next};
use task_local_extensions::Extensions;
@ -101,8 +98,7 @@ pub(crate) mod tests {
jwt_secret: String::new(),
};
let rate_limit_config = RateLimitConfig::builder().build();
let rate_limit_cell = RateLimitCell::new(rate_limit_config).await;
let rate_limit_cell = RateLimitCell::with_test_config();
let context = LemmyContext::create(pool, client, secret, rate_limit_cell.clone());
let config = FederationConfig::builder()

View File

@ -20,12 +20,7 @@ use activitypub_federation::{
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{
generate_outbox_url,
local_site_opt_to_slur_regex,
sanitize_html_federation,
sanitize_html_federation_opt,
},
utils::{generate_outbox_url, local_site_opt_to_slur_regex},
};
use lemmy_db_schema::{
source::{
@ -40,7 +35,6 @@ use lemmy_utils::{
utils::{
markdown::markdown_to_html,
slurs::{check_slurs, check_slurs_opt},
time::convert_datetime,
},
};
use std::ops::Deref;
@ -112,13 +106,13 @@ impl Object for ApubPerson {
icon: self.avatar.clone().map(ImageObject::new),
image: self.banner.clone().map(ImageObject::new),
matrix_user_id: self.matrix_user_id.clone(),
published: Some(convert_datetime(self.published)),
published: Some(self.published),
outbox: generate_outbox_url(&self.actor_id)?.into(),
endpoints: self.shared_inbox_url.clone().map(|s| Endpoints {
shared_inbox: s.into(),
}),
public_key: self.public_key(),
updated: self.updated.map(convert_datetime),
updated: self.updated,
inbox: self.inbox_url.clone().into(),
};
Ok(person)
@ -150,17 +144,14 @@ impl Object for ApubPerson {
) -> Result<ApubPerson, LemmyError> {
let instance_id = fetch_instance_actor_for_object(&person.id, context).await?;
let name = sanitize_html_federation(&person.preferred_username);
let display_name = sanitize_html_federation_opt(&person.name);
let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source);
let bio = sanitize_html_federation_opt(&bio);
// Some Mastodon users have `name: ""` (empty string), need to convert that to `None`
// https://github.com/mastodon/mastodon/issues/25233
let display_name = display_name.filter(|n| !n.is_empty());
let display_name = person.name.filter(|n| !n.is_empty());
let person_form = PersonInsertForm {
name,
name: person.preferred_username,
display_name,
banned: None,
ban_expires: None,
@ -275,7 +266,7 @@ pub(crate) mod tests {
assert_eq!(person.name, "lanodan");
assert!(!person.local);
assert_eq!(context.request_count(), 0);
assert_eq!(person.bio.as_ref().unwrap().len(), 878);
assert_eq!(person.bio.as_ref().unwrap().len(), 873);
cleanup((person, site), &context).await;
}

View File

@ -21,17 +21,11 @@ use activitypub_federation::{
};
use anyhow::anyhow;
use chrono::{DateTime, Utc};
use html2md::parse_html;
use html2text::{from_read_with_decorator, render::text_renderer::TrivialDecorator};
use lemmy_api_common::{
context::LemmyContext,
request::fetch_site_data,
utils::{
is_mod_or_admin,
local_site_opt_to_sensitive,
local_site_opt_to_slur_regex,
sanitize_html_federation,
sanitize_html_federation_opt,
},
utils::{is_mod_or_admin, local_site_opt_to_sensitive, local_site_opt_to_slur_regex},
};
use lemmy_db_schema::{
self,
@ -49,11 +43,11 @@ use lemmy_utils::{
utils::{
markdown::markdown_to_html,
slurs::{check_slurs_opt, remove_slurs},
time::convert_datetime,
validation::check_url_scheme,
},
};
use std::ops::Deref;
use stringreader::StringReader;
use url::Url;
const MAX_TITLE_LENGTH: usize = 200;
@ -132,8 +126,8 @@ impl Object for ApubPost {
comments_enabled: Some(!self.locked),
sensitive: Some(self.nsfw),
language,
published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
published: Some(self.published),
updated: self.updated,
audience: Some(community.actor_id.into()),
in_reply_to: None,
};
@ -171,17 +165,27 @@ impl Object for ApubPost {
let creator = page.creator()?.dereference(context).await?;
let community = page.community(context).await?;
if community.posting_restricted_to_mods {
is_mod_or_admin(&mut context.pool(), creator.id, community.id).await?;
is_mod_or_admin(&mut context.pool(), &creator, community.id).await?;
}
let mut name = page
.name
.clone()
.or_else(|| {
// Posts coming from Mastodon or similar platforms don't have a title. Instead we take the
// first line of the content and convert it from HTML to plaintext. We also remove mentions
// of the community name.
page
.content
.clone()
.as_ref()
.and_then(|c| parse_html(c).lines().next().map(ToString::to_string))
.as_deref()
.map(StringReader::new)
.map(|c| from_read_with_decorator(c, MAX_TITLE_LENGTH, TrivialDecorator::new()))
.and_then(|c| {
c.lines().next().map(|s| {
s.replace(&format!("@{}", community.name), "")
.trim()
.to_string()
})
})
})
.ok_or_else(|| anyhow!("Object must have name or content"))?;
if name.chars().count() > MAX_TITLE_LENGTH {
@ -231,17 +235,11 @@ impl Object for ApubPost {
.unwrap_or_default();
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let body_slurs_removed =
read_from_string_or_source_opt(&page.content, &page.media_type, &page.source)
.map(|s| remove_slurs(&s, slur_regex));
let body = read_from_string_or_source_opt(&page.content, &page.media_type, &page.source)
.map(|s| remove_slurs(&s, slur_regex));
let language_id =
LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?;
let name = sanitize_html_federation(&name);
let body = sanitize_html_federation_opt(&body_slurs_removed);
let embed_title = sanitize_html_federation_opt(&embed_title);
let embed_description = sanitize_html_federation_opt(&embed_description);
PostInsertForm {
name,
url: url.map(Into::into),
@ -300,8 +298,9 @@ mod tests {
use super::*;
use crate::{
objects::{
community::tests::parse_lemmy_community,
person::tests::parse_lemmy_person,
community::{tests::parse_lemmy_community, ApubCommunity},
instance::ApubSite,
person::{tests::parse_lemmy_person, ApubPerson},
post::ApubPost,
tests::init_context,
},
@ -330,6 +329,31 @@ mod tests {
assert!(!post.featured_community);
assert_eq!(context.request_count(), 0);
cleanup(&context, person, site, community, post).await;
}
#[tokio::test]
#[serial]
async fn test_convert_mastodon_post_title() {
let context = init_context().await;
let (person, site) = parse_lemmy_person(&context).await;
let community = parse_lemmy_community(&context).await;
let json = file_to_json_object("assets/mastodon/objects/page.json").unwrap();
let post = ApubPost::from_json(json, &context).await.unwrap();
assert_eq!(post.name, "Variable never resetting at refresh");
cleanup(&context, person, site, community, post).await;
}
async fn cleanup(
context: &Data<LemmyContext>,
person: ApubPerson,
site: ApubSite,
community: ApubCommunity,
post: ApubPost,
) {
Post::delete(&mut context.pool(), post.id).await.unwrap();
Person::delete(&mut context.pool(), person.id)
.await

View File

@ -12,10 +12,7 @@ use activitypub_federation::{
traits::Object,
};
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{check_person_block, sanitize_html_federation},
};
use lemmy_api_common::{context::LemmyContext, utils::check_person_block};
use lemmy_db_schema::{
source::{
person::Person,
@ -25,7 +22,7 @@ use lemmy_db_schema::{
};
use lemmy_utils::{
error::{LemmyError, LemmyErrorType},
utils::{markdown::markdown_to_html, time::convert_datetime},
utils::markdown::markdown_to_html,
};
use std::ops::Deref;
use url::Url;
@ -89,8 +86,8 @@ impl Object for ApubPrivateMessage {
content: markdown_to_html(&self.content),
media_type: Some(MediaTypeHtml::Html),
source: Some(Source::new(self.content.clone())),
published: Some(convert_datetime(self.published)),
updated: self.updated.map(convert_datetime),
published: Some(self.published),
updated: self.updated,
};
Ok(note)
}
@ -125,7 +122,6 @@ impl Object for ApubPrivateMessage {
check_person_block(creator.id, recipient.id, &mut context.pool()).await?;
let content = read_from_string_or_source(&note.content, &None, &note.source);
let content = sanitize_html_federation(&content);
let form = PrivateMessageInsertForm {
creator_id: creator.id,

View File

@ -1,34 +1,12 @@
use activitypub_federation::kinds::collection::CollectionType;
use lemmy_api_common::{context::LemmyContext, utils::generate_followers_url};
use lemmy_db_schema::source::community::Community;
use lemmy_db_views_actor::structs::CommunityFollowerView;
use lemmy_utils::error::LemmyError;
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct GroupFollowers {
id: Url,
r#type: CollectionType,
total_items: i32,
items: Vec<()>,
}
impl GroupFollowers {
pub(crate) async fn new(
community: Community,
context: &LemmyContext,
) -> Result<GroupFollowers, LemmyError> {
let community_id = community.id;
let community_followers =
CommunityFollowerView::count_community_followers(&mut context.pool(), community_id).await?;
Ok(GroupFollowers {
id: generate_followers_url(&community.actor_id)?.into(),
r#type: CollectionType::Collection,
total_items: community_followers as i32,
items: vec![],
})
}
pub(crate) id: Url,
pub(crate) r#type: CollectionType,
pub(crate) total_items: i32,
pub(crate) items: Vec<()>,
}

View File

@ -2,6 +2,7 @@ use crate::{
check_apub_id_valid_with_strictness,
collections::{
community_featured::ApubCommunityFeatured,
community_follower::ApubCommunityFollower,
community_moderators::ApubCommunityModerators,
community_outbox::ApubCommunityOutbox,
},
@ -23,10 +24,7 @@ use activitypub_federation::{
},
};
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{local_site_opt_to_slur_regex, sanitize_html_federation, sanitize_html_federation_opt},
};
use lemmy_api_common::{context::LemmyContext, utils::local_site_opt_to_slur_regex};
use lemmy_db_schema::{
newtypes::InstanceId,
source::community::{CommunityInsertForm, CommunityUpdateForm},
@ -51,7 +49,7 @@ pub struct Group {
/// username, set at account creation and usually fixed after that
pub(crate) preferred_username: String,
pub(crate) inbox: Url,
pub(crate) followers: Url,
pub(crate) followers: CollectionId<ApubCommunityFollower>,
pub(crate) public_key: PublicKey,
/// title
@ -97,14 +95,11 @@ impl Group {
}
pub(crate) fn into_insert_form(self, instance_id: InstanceId) -> CommunityInsertForm {
let name = sanitize_html_federation(&self.preferred_username);
let title = sanitize_html_federation(&self.name.unwrap_or(self.preferred_username));
let description = read_from_string_or_source_opt(&self.summary, &None, &self.source);
let description = sanitize_html_federation_opt(&description);
CommunityInsertForm {
name,
title,
name: self.preferred_username.clone(),
title: self.name.unwrap_or(self.preferred_username.clone()),
description,
removed: None,
published: self.published,

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