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

delete_local_image_on_delete_account
Dessalines 2 months ago
commit 8c99a5598d

@ -1,3 +1,3 @@
* @Nutomic @dessalines @phiresky
* @Nutomic @dessalines @phiresky @dullbananas @SleeplessOne1917
crates/apub/ @Nutomic
migrations/ @dessalines @phiresky
migrations/ @dessalines @phiresky @dullbananas

@ -20,6 +20,8 @@ body:
required: true
- label: Is this only a single bug? Do not put multiple bugs in one issue.
required: true
- label: Do you agree to follow the rules in our [Code of Conduct](https://join-lemmy.org/docs/code_of_conduct.html)?
required: true
- label: Is this a backend issue? Use the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui) repo for UI / frontend issues.
required: true
- type: textarea

@ -20,6 +20,8 @@ body:
required: true
- label: Is this a backend issue? Use the [lemmy-ui](https://github.com/LemmyNet/lemmy-ui) repo for UI / frontend issues.
required: true
- label: Do you agree to follow the rules in our [Code of Conduct](https://join-lemmy.org/docs/code_of_conduct.html)?
required: true
- type: textarea
id: problem
attributes:

@ -6,21 +6,23 @@ variables:
- &install_pnpm "corepack enable pnpm"
- &slow_check_paths
- path:
# rust source code
- "crates/**"
- "src/**"
- "**/Cargo.toml"
- "Cargo.lock"
# database migrations
- "migrations/**"
# typescript tests
- "api_tests/**"
# config files and scripts used by ci
- ".woodpecker.yml"
- ".rustfmt.toml"
- "scripts/update_config_defaults.sh"
- "diesel.toml"
- ".gitmodules"
include: [
# rust source code
"crates/**",
"src/**",
"**/Cargo.toml",
"Cargo.lock",
# database migrations
"migrations/**",
# typescript tests
"api_tests/**",
# config files and scripts used by ci
".woodpecker.yml",
".rustfmt.toml",
"scripts/update_config_defaults.sh",
"diesel.toml",
".gitmodules",
]
# Broken for cron jobs currently, see
# https://github.com/woodpecker-ci/woodpecker/issues/1716
@ -198,7 +200,7 @@ steps:
- cat target/log/lemmy_*.out || true
- "# If you can't see all output, then use the download button"
when:
status: [failure]
- status: [failure]
publish_release_docker:
image: woodpeckerci/plugin-docker-buildx
@ -211,7 +213,7 @@ steps:
- RUST_RELEASE_MODE=release
tag: ${CI_COMMIT_TAG}
when:
event: tag
- event: tag
nightly_build:
image: woodpeckerci/plugin-docker-buildx
@ -224,7 +226,7 @@ steps:
- RUST_RELEASE_MODE=release
tag: dev
when:
event: cron
- event: cron
# using https://github.com/pksunkara/cargo-workspaces
publish_to_crates_io:
@ -237,7 +239,7 @@ steps:
- cargo workspaces publish --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
secrets: [cargo_api_token]
when:
event: tag
- event: tag
notify_on_failure:
image: alpine:3
@ -245,7 +247,7 @@ steps:
- apk add curl
- "curl -d'Lemmy CI build failed: ${CI_PIPELINE_URL}' ntfy.sh/lemmy_drone_ci"
when:
status: [failure]
- status: [failure]
notify_on_tag_deploy:
image: alpine:3
@ -253,7 +255,7 @@ steps:
- apk add curl
- "curl -d'lemmy:${CI_COMMIT_TAG} deployed' ntfy.sh/lemmy_drone_ci"
when:
event: tag
- event: tag
services:
database:

1040
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -37,6 +37,8 @@ debug = 0
[features]
embed-pictrs = ["pict-rs"]
# This feature requires building with `tokio_unstable` flag, see documentation:
# https://docs.rs/tokio/latest/tokio/#unstable-features
console = [
"console-subscriber",
"opentelemetry",
@ -96,15 +98,15 @@ lemmy_routes = { version = "=0.19.3", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.3", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.3", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.3", path = "./crates/db_views_moderator" }
activitypub_federation = { version = "0.5.1-beta.1", default-features = false, features = [
activitypub_federation = { version = "0.5.2", default-features = false, features = [
"actix-web",
] }
diesel = "2.1.4"
diesel_migrations = "2.1.0"
diesel-async = "0.4.1"
serde = { version = "1.0.195", features = ["derive"] }
serde_with = "3.5.1"
actix-web = { version = "4.4.1", default-features = false, features = [
serde = { version = "1.0.197", features = ["derive"] }
serde_with = "3.7.0"
actix-web = { version = "4.5.1", default-features = false, features = [
"macros",
"rustls",
"compress-brotli",
@ -113,39 +115,39 @@ actix-web = { version = "4.4.1", default-features = false, features = [
"cookies",
] }
tracing = "0.1.40"
tracing-actix-web = { version = "0.7.9", default-features = false }
tracing-actix-web = { version = "0.7.10", default-features = false }
tracing-error = "0.2.0"
tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = { version = "2.5.0", features = ["serde"] }
reqwest = { version = "0.11.23", features = ["json", "blocking", "gzip"] }
reqwest = { version = "0.11.26", features = ["json", "blocking", "gzip"] }
reqwest-middleware = "0.2.4"
reqwest-tracing = "0.4.7"
clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.15.0"
chrono = { version = "0.4.32", features = ["serde"], default-features = false }
serde_json = { version = "1.0.111", features = ["preserve_order"] }
chrono = { version = "0.4.35", features = ["serde"], default-features = false }
serde_json = { version = "1.0.114", features = ["preserve_order"] }
base64 = "0.21.7"
uuid = { version = "1.7.0", features = ["serde", "v4"] }
async-trait = "0.1.77"
captcha = "0.0.9"
anyhow = { version = "1.0.79", features = [
anyhow = { version = "1.0.81", features = [
"backtrace",
] } # backtrace is on by default on nightly, but not stable rust
diesel_ltree = "0.3.1"
typed-builder = "0.18.1"
serial_test = "2.0.0"
tokio = { version = "1.35.1", features = ["full"] }
tokio = { version = "1.36.0", features = ["full"] }
regex = "1.10.3"
once_cell = "1.19.0"
diesel-derive-newtype = "2.1.0"
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = "0.25.0"
strum_macros = "0.25.3"
itertools = "0.12.0"
itertools = "0.12.1"
futures = "0.3.30"
http = "0.2.11"
http = "0.2.12"
rosetta-i18n = "0.1.3"
opentelemetry = { version = "0.19.0", features = ["rt-tokio"] }
tracing-opentelemetry = { version = "0.19.0" }
@ -160,9 +162,9 @@ tokio-postgres = "0.7.10"
tokio-postgres-rustls = "0.10.0"
urlencoding = "2.1.3"
enum-map = "2.7"
moka = { version = "0.12.4", features = ["future"] }
moka = { version = "0.12.5", features = ["future"] }
i-love-jesus = { version = "0.1.0" }
clap = { version = "4.4.18", features = ["derive"] }
clap = { version = "4.5.2", features = ["derive"] }
pretty_assertions = "1.4.0"
[dependencies]
@ -193,7 +195,7 @@ tracing-opentelemetry = { workspace = true, optional = true }
opentelemetry = { workspace = true, optional = true }
console-subscriber = { version = "0.1.10", optional = true }
opentelemetry-otlp = { version = "0.12.0", optional = true }
pict-rs = { version = "0.5.1", optional = true }
pict-rs = { version = "0.5.9", optional = true }
tokio.workspace = true
actix-cors = "0.6.5"
futures-util = { workspace = true }

@ -20,16 +20,16 @@
},
"devDependencies": {
"@types/jest": "^29.5.12",
"@types/node": "^20.11.22",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@types/node": "^20.11.27",
"@typescript-eslint/eslint-plugin": "^7.2.0",
"@typescript-eslint/parser": "^7.2.0",
"download-file-sync": "^1.0.4",
"eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.0.1",
"eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0",
"lemmy-js-client": "0.19.4-alpha.6",
"lemmy-js-client": "0.19.4-alpha.8",
"prettier": "^3.2.5",
"ts-jest": "^29.1.0",
"typescript": "^5.3.3"
"typescript": "^5.4.2"
}
}

@ -9,14 +9,14 @@ devDependencies:
specifier: ^29.5.12
version: 29.5.12
'@types/node':
specifier: ^20.11.22
version: 20.11.22
specifier: ^20.11.27
version: 20.11.27
'@typescript-eslint/eslint-plugin':
specifier: ^7.1.0
version: 7.1.0(@typescript-eslint/parser@7.1.0)(eslint@8.57.0)(typescript@5.3.3)
specifier: ^7.2.0
version: 7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/parser':
specifier: ^7.1.0
version: 7.1.0(eslint@8.57.0)(typescript@5.3.3)
specifier: ^7.2.0
version: 7.2.0(eslint@8.57.0)(typescript@5.4.2)
download-file-sync:
specifier: ^1.0.4
version: 1.0.4
@ -24,23 +24,23 @@ devDependencies:
specifier: ^8.57.0
version: 8.57.0
eslint-plugin-prettier:
specifier: ^5.0.1
specifier: ^5.1.3
version: 5.1.3(eslint@8.57.0)(prettier@3.2.5)
jest:
specifier: ^29.5.0
version: 29.7.0(@types/node@20.11.22)
version: 29.7.0(@types/node@20.11.27)
lemmy-js-client:
specifier: 0.19.4-alpha.6
version: 0.19.4-alpha.6
specifier: 0.19.4-alpha.8
version: 0.19.4-alpha.8
prettier:
specifier: ^3.2.5
version: 3.2.5
ts-jest:
specifier: ^29.1.0
version: 29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.3.3)
version: 29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.4.2)
typescript:
specifier: ^5.3.3
version: 5.3.3
specifier: ^5.4.2
version: 5.4.2
packages:
@ -464,7 +464,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
chalk: 4.1.2
jest-message-util: 29.7.0
jest-util: 29.7.0
@ -485,14 +485,14 @@ packages:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
ansi-escapes: 4.3.2
chalk: 4.1.2
ci-info: 3.9.0
exit: 0.1.2
graceful-fs: 4.2.11
jest-changed-files: 29.7.0
jest-config: 29.7.0(@types/node@20.11.22)
jest-config: 29.7.0(@types/node@20.11.27)
jest-haste-map: 29.7.0
jest-message-util: 29.7.0
jest-regex-util: 29.6.3
@ -520,7 +520,7 @@ packages:
dependencies:
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
jest-mock: 29.7.0
dev: true
@ -547,7 +547,7 @@ packages:
dependencies:
'@jest/types': 29.6.3
'@sinonjs/fake-timers': 10.3.0
'@types/node': 20.11.22
'@types/node': 20.11.27
jest-message-util: 29.7.0
jest-mock: 29.7.0
jest-util: 29.7.0
@ -580,7 +580,7 @@ packages:
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@jridgewell/trace-mapping': 0.3.22
'@types/node': 20.11.22
'@types/node': 20.11.27
chalk: 4.1.2
collect-v8-coverage: 1.0.2
exit: 0.1.2
@ -668,7 +668,7 @@ packages:
'@jest/schemas': 29.6.3
'@types/istanbul-lib-coverage': 2.0.6
'@types/istanbul-reports': 3.0.4
'@types/node': 20.11.22
'@types/node': 20.11.27
'@types/yargs': 17.0.32
chalk: 4.1.2
dev: true
@ -777,7 +777,7 @@ packages:
/@types/graceful-fs@4.1.9:
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
dependencies:
'@types/node': 20.11.22
'@types/node': 20.11.27
dev: true
/@types/istanbul-lib-coverage@2.0.6:
@ -807,8 +807,8 @@ packages:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: true
/@types/node@20.11.22:
resolution: {integrity: sha512-/G+IxWxma6V3E+pqK1tSl2Fo1kl41pK1yeCyDsgkF9WlVAme4j5ISYM2zR11bgLFJGLN5sVK40T4RJNuiZbEjA==}
/@types/node@20.11.27:
resolution: {integrity: sha512-qyUZfMnCg1KEz57r7pzFtSGt49f6RPkPBis3Vo4PbS7roQEDn22hiHzl/Lo1q4i4hDEgBJmBF/NTNg2XR0HbFg==}
dependencies:
undici-types: 5.26.5
dev: true
@ -831,8 +831,8 @@ packages:
'@types/yargs-parser': 21.0.3
dev: true
/@typescript-eslint/eslint-plugin@7.1.0(@typescript-eslint/parser@7.1.0)(eslint@8.57.0)(typescript@5.3.3):
resolution: {integrity: sha512-j6vT/kCulhG5wBmGtstKeiVr1rdXE4nk+DT1k6trYkwlrvW9eOF5ZbgKnd/YR6PcM4uTEXa0h6Fcvf6X7Dxl0w==}
/@typescript-eslint/eslint-plugin@7.2.0(@typescript-eslint/parser@7.2.0)(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-mdekAHOqS9UjlmyF/LSs6AIEvfceV749GFxoBAjwAv0nkevfKHWQFDMcBZWUiIC5ft6ePWivXoS36aKQ0Cy3sw==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
'@typescript-eslint/parser': ^7.0.0
@ -843,25 +843,25 @@ packages:
optional: true
dependencies:
'@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
'@typescript-eslint/scope-manager': 7.1.0
'@typescript-eslint/type-utils': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
'@typescript-eslint/utils': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 7.1.0
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/scope-manager': 7.2.0
'@typescript-eslint/type-utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2)
'@typescript-eslint/visitor-keys': 7.2.0
debug: 4.3.4
eslint: 8.57.0
graphemer: 1.4.0
ignore: 5.3.1
natural-compare: 1.4.0
semver: 7.6.0
ts-api-utils: 1.2.1(typescript@5.3.3)
typescript: 5.3.3
ts-api-utils: 1.3.0(typescript@5.4.2)
typescript: 5.4.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/parser@7.1.0(eslint@8.57.0)(typescript@5.3.3):
resolution: {integrity: sha512-V1EknKUubZ1gWFjiOZhDSNToOjs63/9O0puCgGS8aDOgpZY326fzFu15QAUjwaXzRZjf/qdsdBrckYdv9YxB8w==}
/@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-5FKsVcHTk6TafQKQbuIVkXq58Fnbkd2wDL4LB7AURN7RUOu1utVP+G8+6u3ZhEroW3DF6hyo3ZEXxgKgp4KeCg==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^8.56.0
@ -870,27 +870,27 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/scope-manager': 7.1.0
'@typescript-eslint/types': 7.1.0
'@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 7.1.0
'@typescript-eslint/scope-manager': 7.2.0
'@typescript-eslint/types': 7.2.0
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2)
'@typescript-eslint/visitor-keys': 7.2.0
debug: 4.3.4
eslint: 8.57.0
typescript: 5.3.3
typescript: 5.4.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/scope-manager@7.1.0:
resolution: {integrity: sha512-6TmN4OJiohHfoOdGZ3huuLhpiUgOGTpgXNUPJgeZOZR3DnIpdSgtt83RS35OYNNXxM4TScVlpVKC9jyQSETR1A==}
/@typescript-eslint/scope-manager@7.2.0:
resolution: {integrity: sha512-Qh976RbQM/fYtjx9hs4XkayYujB/aPwglw2choHmf3zBjB4qOywWSdt9+KLRdHubGcoSwBnXUH2sR3hkyaERRg==}
engines: {node: ^16.0.0 || >=18.0.0}
dependencies:
'@typescript-eslint/types': 7.1.0
'@typescript-eslint/visitor-keys': 7.1.0
'@typescript-eslint/types': 7.2.0
'@typescript-eslint/visitor-keys': 7.2.0
dev: true
/@typescript-eslint/type-utils@7.1.0(eslint@8.57.0)(typescript@5.3.3):
resolution: {integrity: sha512-UZIhv8G+5b5skkcuhgvxYWHjk7FW7/JP5lPASMEUoliAPwIH/rxoUSQPia2cuOj9AmDZmwUl1usKm85t5VUMew==}
/@typescript-eslint/type-utils@7.2.0(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-xHi51adBHo9O9330J8GQYQwrKBqbIPJGZZVQTHHmy200hvkLZFWJIFtAG/7IYTWUyun6DE6w5InDReePJYJlJA==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^8.56.0
@ -899,23 +899,23 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3)
'@typescript-eslint/utils': 7.1.0(eslint@8.57.0)(typescript@5.3.3)
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2)
'@typescript-eslint/utils': 7.2.0(eslint@8.57.0)(typescript@5.4.2)
debug: 4.3.4
eslint: 8.57.0
ts-api-utils: 1.2.1(typescript@5.3.3)
typescript: 5.3.3
ts-api-utils: 1.3.0(typescript@5.4.2)
typescript: 5.4.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/types@7.1.0:
resolution: {integrity: sha512-qTWjWieJ1tRJkxgZYXx6WUYtWlBc48YRxgY2JN1aGeVpkhmnopq+SUC8UEVGNXIvWH7XyuTjwALfG6bFEgCkQA==}
/@typescript-eslint/types@7.2.0:
resolution: {integrity: sha512-XFtUHPI/abFhm4cbCDc5Ykc8npOKBSJePY3a3s+lwumt7XWJuzP5cZcfZ610MIPHjQjNsOLlYK8ASPaNG8UiyA==}
engines: {node: ^16.0.0 || >=18.0.0}
dev: true
/@typescript-eslint/typescript-estree@7.1.0(typescript@5.3.3):
resolution: {integrity: sha512-k7MyrbD6E463CBbSpcOnwa8oXRdHzH1WiVzOipK3L5KSML92ZKgUBrTlehdi7PEIMT8k0bQixHUGXggPAlKnOQ==}
/@typescript-eslint/typescript-estree@7.2.0(typescript@5.4.2):
resolution: {integrity: sha512-cyxS5WQQCoBwSakpMrvMXuMDEbhOo9bNHHrNcEWis6XHx6KF518tkF1wBvKIn/tpq5ZpUYK7Bdklu8qY0MsFIA==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
typescript: '*'
@ -923,21 +923,21 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 7.1.0
'@typescript-eslint/visitor-keys': 7.1.0
'@typescript-eslint/types': 7.2.0
'@typescript-eslint/visitor-keys': 7.2.0
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
semver: 7.6.0
ts-api-utils: 1.2.1(typescript@5.3.3)
typescript: 5.3.3
ts-api-utils: 1.3.0(typescript@5.4.2)
typescript: 5.4.2
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/utils@7.1.0(eslint@8.57.0)(typescript@5.3.3):
resolution: {integrity: sha512-WUFba6PZC5OCGEmbweGpnNJytJiLG7ZvDBJJoUcX4qZYf1mGZ97mO2Mps6O2efxJcJdRNpqweCistDbZMwIVHw==}
/@typescript-eslint/utils@7.2.0(eslint@8.57.0)(typescript@5.4.2):
resolution: {integrity: sha512-YfHpnMAGb1Eekpm3XRK8hcMwGLGsnT6L+7b2XyRv6ouDuJU1tZir1GS2i0+VXRatMwSI1/UfcyPe53ADkU+IuA==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^8.56.0
@ -945,9 +945,9 @@ packages:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
'@types/json-schema': 7.0.15
'@types/semver': 7.5.8
'@typescript-eslint/scope-manager': 7.1.0
'@typescript-eslint/types': 7.1.0
'@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3)
'@typescript-eslint/scope-manager': 7.2.0
'@typescript-eslint/types': 7.2.0
'@typescript-eslint/typescript-estree': 7.2.0(typescript@5.4.2)
eslint: 8.57.0
semver: 7.6.0
transitivePeerDependencies:
@ -955,11 +955,11 @@ packages:
- typescript
dev: true
/@typescript-eslint/visitor-keys@7.1.0:
resolution: {integrity: sha512-FhUqNWluiGNzlvnDZiXad4mZRhtghdoKW6e98GoEOYSu5cND+E39rG5KwJMUzeENwm1ztYBRqof8wMLP+wNPIA==}
/@typescript-eslint/visitor-keys@7.2.0:
resolution: {integrity: sha512-c6EIQRHhcpl6+tO8EMR+kjkkV+ugUNXOmeASA1rlzkd8EPIriavpWoiEz1HR/VLhbVIdhqnV6E7JZm00cBDx2A==}
engines: {node: ^16.0.0 || >=18.0.0}
dependencies:
'@typescript-eslint/types': 7.1.0
'@typescript-eslint/types': 7.2.0
eslint-visitor-keys: 3.4.3
dev: true
@ -1276,7 +1276,7 @@ packages:
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
dev: true
/create-jest@29.7.0(@types/node@20.11.22):
/create-jest@29.7.0(@types/node@20.11.27):
resolution: {integrity: sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
@ -1285,7 +1285,7 @@ packages:
chalk: 4.1.2
exit: 0.1.2
graceful-fs: 4.2.11
jest-config: 29.7.0(@types/node@20.11.22)
jest-config: 29.7.0(@types/node@20.11.27)
jest-util: 29.7.0
prompts: 2.4.2
transitivePeerDependencies:
@ -1939,7 +1939,7 @@ packages:
'@jest/expect': 29.7.0
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
chalk: 4.1.2
co: 4.6.0
dedent: 1.5.1
@ -1960,7 +1960,7 @@ packages:
- supports-color
dev: true
/jest-cli@29.7.0(@types/node@20.11.22):
/jest-cli@29.7.0(@types/node@20.11.27):
resolution: {integrity: sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
@ -1974,10 +1974,10 @@ packages:
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
chalk: 4.1.2
create-jest: 29.7.0(@types/node@20.11.22)
create-jest: 29.7.0(@types/node@20.11.27)
exit: 0.1.2
import-local: 3.1.0
jest-config: 29.7.0(@types/node@20.11.22)
jest-config: 29.7.0(@types/node@20.11.27)
jest-util: 29.7.0
jest-validate: 29.7.0
yargs: 17.7.2
@ -1988,7 +1988,7 @@ packages:
- ts-node
dev: true
/jest-config@29.7.0(@types/node@20.11.22):
/jest-config@29.7.0(@types/node@20.11.27):
resolution: {integrity: sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
peerDependencies:
@ -2003,7 +2003,7 @@ packages:
'@babel/core': 7.23.9
'@jest/test-sequencer': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
babel-jest: 29.7.0(@babel/core@7.23.9)
chalk: 4.1.2
ci-info: 3.9.0
@ -2063,7 +2063,7 @@ packages:
'@jest/environment': 29.7.0
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
jest-mock: 29.7.0
jest-util: 29.7.0
dev: true
@ -2079,7 +2079,7 @@ packages:
dependencies:
'@jest/types': 29.6.3
'@types/graceful-fs': 4.1.9
'@types/node': 20.11.22
'@types/node': 20.11.27
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.11
@ -2130,7 +2130,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
jest-util: 29.7.0
dev: true
@ -2185,7 +2185,7 @@ packages:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
chalk: 4.1.2
emittery: 0.13.1
graceful-fs: 4.2.11
@ -2216,7 +2216,7 @@ packages:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
chalk: 4.1.2
cjs-module-lexer: 1.2.3
collect-v8-coverage: 1.0.2
@ -2268,7 +2268,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
chalk: 4.1.2
ci-info: 3.9.0
graceful-fs: 4.2.11
@ -2293,7 +2293,7 @@ packages:
dependencies:
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.11.22
'@types/node': 20.11.27
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
@ -2305,13 +2305,13 @@ packages:
resolution: {integrity: sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@types/node': 20.11.22
'@types/node': 20.11.27
jest-util: 29.7.0
merge-stream: 2.0.0
supports-color: 8.1.1
dev: true
/jest@29.7.0(@types/node@20.11.22):
/jest@29.7.0(@types/node@20.11.27):
resolution: {integrity: sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==}
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
hasBin: true
@ -2324,7 +2324,7 @@ packages:
'@jest/core': 29.7.0
'@jest/types': 29.6.3
import-local: 3.1.0
jest-cli: 29.7.0(@types/node@20.11.22)
jest-cli: 29.7.0(@types/node@20.11.27)
transitivePeerDependencies:
- '@types/node'
- babel-plugin-macros
@ -2390,8 +2390,8 @@ packages:
engines: {node: '>=6'}
dev: true
/lemmy-js-client@0.19.4-alpha.6:
resolution: {integrity: sha512-x4htMlpoZ7hzrhrIk82aompVxbpu2ZDWtmWNGraM0+27nUCDf6gYxJH5nb5R/o39BQe5KSHq6zoBdliBwAY40w==}
/lemmy-js-client@0.19.4-alpha.8:
resolution: {integrity: sha512-8vjqUYVOhyUTcmG9FvPLjrWziVwNa2/Zi+kSflTrajJsK0V+5DclJ5dhdVMUQ4DEA70gb0OuNMDlipPG2FoS5A==}
dependencies:
cross-fetch: 4.0.0
form-data: 4.0.0
@ -2956,16 +2956,16 @@ packages:
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
dev: true
/ts-api-utils@1.2.1(typescript@5.3.3):
resolution: {integrity: sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==}
/ts-api-utils@1.3.0(typescript@5.4.2):
resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==}
engines: {node: '>=16'}
peerDependencies:
typescript: '>=4.2.0'
dependencies:
typescript: 5.3.3
typescript: 5.4.2
dev: true
/ts-jest@29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.3.3):
/ts-jest@29.1.2(@babel/core@7.23.9)(jest@29.7.0)(typescript@5.4.2):
resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==}
engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0}
hasBin: true
@ -2989,13 +2989,13 @@ packages:
'@babel/core': 7.23.9
bs-logger: 0.2.6
fast-json-stable-stringify: 2.1.0
jest: 29.7.0(@types/node@20.11.22)
jest: 29.7.0(@types/node@20.11.27)
jest-util: 29.7.0
json5: 2.2.3
lodash.memoize: 4.1.2
make-error: 1.3.6
semver: 7.5.4
typescript: 5.3.3
typescript: 5.4.2
yargs-parser: 21.1.1
dev: true
@ -3025,8 +3025,8 @@ packages:
engines: {node: '>=10'}
dev: true
/typescript@5.3.3:
resolution: {integrity: sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==}
/typescript@5.4.2:
resolution: {integrity: sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==}
engines: {node: '>=14.17'}
hasBin: true
dev: true

@ -18,6 +18,7 @@ import {
resolveBetaCommunity,
createComment,
deletePost,
delay,
removePost,
getPost,
unfollowRemotes,
@ -219,9 +220,10 @@ test("Sticky a post", async () => {
if (!gammaPost) {
throw "Missing gamma post";
}
let gammaTrySticky = await featurePost(gamma, true, gammaPost.post);
// This has been failing occasionally
await featurePost(gamma, true, gammaPost.post);
let betaPost3 = (await resolvePost(beta, postRes.post_view.post)).post;
expect(gammaTrySticky.post_view.post.featured_community).toBe(true);
// expect(gammaTrySticky.post_view.post.featured_community).toBe(true);
expect(betaPost3?.post.featured_community).toBe(false);
});
@ -710,3 +712,25 @@ test("Fetch post via redirect", async () => {
expect(gammaPost.post?.post.ap_id).toBe(alphaPost.post_view.post.ap_id);
await unfollowRemotes(alpha);
});
test("Block post that contains banned URL", async () => {
let editSiteForm: EditSite = {
blocked_urls: ["https://evil.com/"],
};
await epsilon.editSite(editSiteForm);
await delay(500);
if (!betaCommunity) {
throw "Missing beta community";
}
expect(
createPost(epsilon, betaCommunity.community.id, "https://evil.com"),
).rejects.toStrictEqual(Error("blocked_url"));
// Later tests need this to be empty
editSiteForm.blocked_urls = [];
await epsilon.editSite(editSiteForm);
});

@ -45,7 +45,7 @@ test("Create user", async () => {
if (!site.my_user) {
throw "Missing site user";
}
apShortname = `@${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
});
test("Set some user settings, check that they are federated", async () => {
@ -68,7 +68,7 @@ test("Delete user", async () => {
let user = await registerUser(alpha, alphaUrl);
// make a local post and comment
let alphaCommunity = (await resolveCommunity(user, "!main@lemmy-alpha:8541"))
let alphaCommunity = (await resolveCommunity(user, "main@lemmy-alpha:8541"))
.community;
if (!alphaCommunity) {
throw "Missing alpha community";
@ -134,8 +134,28 @@ test("Create user with Arabic name", async () => {
if (!site.my_user) {
throw "Missing site user";
}
apShortname = `@${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
apShortname = `${site.my_user.local_user_view.person.name}@lemmy-alpha:8541`;
let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
expect(alphaPerson).toBeDefined();
});
test("Create user with accept-language", async () => {
let lemmy_http = new LemmyHttp(alphaUrl, {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language#syntax
headers: { "Accept-Language": "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5" },
});
let user = await registerUser(lemmy_http, alphaUrl);
let site = await getSite(user);
expect(site.my_user).toBeDefined();
expect(site.my_user?.local_user_view.local_user.interface_language).toBe(
"fr",
);
let langs = site.all_languages
.filter(a => site.my_user?.discussion_languages.includes(a.id))
.map(l => l.code);
// should have languages from accept header, as well as "undetermined"
// which is automatically enabled by backend
expect(langs).toStrictEqual(["und", "de", "en", "fr"]);
});

@ -0,0 +1,86 @@
# git-cliff ~ configuration file
# https://git-cliff.org/docs/configuration
[remote.github]
owner = "LemmyNet"
repo = "lemmy"
# token = ""
[changelog]
# template for the changelog body
# https://keats.github.io/tera/docs/#introduction
body = """
## What's Changed
{%- if version %} in {{ version }}{%- endif -%}
{% for commit in commits %}
{% if commit.github.pr_title -%}
{%- set commit_message = commit.github.pr_title -%}
{%- else -%}
{%- set commit_message = commit.message -%}
{%- endif -%}
* {{ commit_message | split(pat="\n") | first | trim }}\
{% if commit.github.username %} by @{{ commit.github.username }}{%- endif -%}
{% if commit.github.pr_number %} in \
[#{{ commit.github.pr_number }}]({{ self::remote_url() }}/pull/{{ commit.github.pr_number }}) \
{%- endif %}
{%- endfor -%}
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
{% raw %}\n{% endraw -%}
## New Contributors
{%- endif %}\
{% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %}
* @{{ contributor.username }} made their first contribution
{%- if contributor.pr_number %} in \
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
{%- endif %}
{%- endfor -%}
{% if version %}
{% if previous.version %}
**Full Changelog**: {{ self::remote_url() }}/compare/{{ previous.version }}...{{ version }}
{% endif %}
{% else -%}
{% raw %}\n{% endraw %}
{% endif %}
{%- macro remote_url() -%}
https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}
{%- endmacro -%}
"""
# remove the leading and trailing whitespace from the template
trim = true
# changelog footer
footer = """
<!-- generated by git-cliff -->
"""
# postprocessors
postprocessors = []
[git]
# parse the commits based on https://www.conventionalcommits.org
conventional_commits = false
# filter out the commits that are not conventional
filter_unconventional = true
# process each line of a commit as an individual commit
split_commits = false
# regex for preprocessing the commit messages
commit_preprocessors = [
# remove issue numbers from commits
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
]
# protect breaking changes from being skipped due to matching a skipping commit_parser
protect_breaking_commits = false
# filter out the commits that are not matched by commit parsers
filter_commits = false
# regex for matching git tags
tag_pattern = "[0-9].*"
# regex for skipping tags
skip_tags = "beta|alpha"
# regex for ignoring tags
ignore_tags = "rc"
# sort the tags topologically
topo_order = false
# sort the commits inside sections by oldest/newest order
sort_commits = "newest"

@ -34,7 +34,7 @@ tracing = { workspace = true }
chrono = { workspace = true }
url = { workspace = true }
wav = "1.0.0"
sitemap-rs = "0.2.0"
sitemap-rs = "0.2.1"
totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] }
actix-web-httpauth = "0.8.1"

@ -135,11 +135,7 @@ pub(crate) fn generate_totp_2fa_secret() -> String {
Secret::generate_secret().to_string()
}
pub(crate) fn build_totp_2fa(
site_name: &str,
username: &str,
secret: &str,
) -> Result<TOTP, LemmyError> {
fn build_totp_2fa(hostname: &str, username: &str, secret: &str) -> Result<TOTP, LemmyError> {
let sec = Secret::Raw(secret.as_bytes().to_vec());
let sec_bytes = sec
.to_bytes()
@ -151,7 +147,7 @@ pub(crate) fn build_totp_2fa(
1,
30,
sec_bytes,
Some(site_name.to_string()),
Some(hostname.to_string()),
username.to_string(),
)
.with_lemmy_type(LemmyErrorType::CouldntGenerateTotp)
@ -263,16 +259,16 @@ pub async fn local_user_view_from_jwt(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
#[test]
fn test_build_totp() {
let generated_secret = generate_totp_2fa_secret();
let totp = build_totp_2fa("lemmy", "my_name", &generated_secret);
let totp = build_totp_2fa("lemmy.ml", "my_name", &generated_secret);
assert!(totp.is_ok());
}
}

@ -6,10 +6,7 @@ use lemmy_api_common::{
person::GenerateTotpSecretResponse,
sensitive::Sensitive,
};
use lemmy_db_schema::{
source::local_user::{LocalUser, LocalUserUpdateForm},
traits::Crud,
};
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyError, LemmyErrorType};

@ -50,7 +50,11 @@ pub async fn login(
// Check the totp if enabled
if local_user_view.local_user.totp_2fa_enabled {
check_totp_2fa_valid(&local_user_view, &data.totp_2fa_token, &site_view.site.name)?;
check_totp_2fa_valid(
&local_user_view,
&data.totp_2fa_token,
&context.settings().hostname,
)?;
}
let jwt = Claims::generate(local_user_view.local_user.id, req, &context).await?;

@ -3,6 +3,7 @@ use lemmy_api_common::{
context::LemmyContext,
person::SaveUserSettings,
utils::{
get_url_blocklist,
local_site_to_slur_regex,
process_markdown_opt,
proxy_image_link_opt_api,
@ -14,6 +15,7 @@ use lemmy_db_schema::{
source::{
actor_language::LocalUserLanguage,
local_user::{LocalUser, LocalUserUpdateForm},
local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeUpdateForm},
person::{Person, PersonUpdateForm},
},
traits::Crud,
@ -34,7 +36,10 @@ pub async fn save_user_settings(
let site_view = SiteView::read_local(&mut context.pool()).await?;
let slur_regex = local_site_to_slur_regex(&site_view.local_site);
let bio = diesel_option_overwrite(process_markdown_opt(&data.bio, &slur_regex, &context).await?);
let url_blocklist = get_url_blocklist(&context).await?;
let bio = diesel_option_overwrite(
process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context).await?,
);
let avatar = proxy_image_link_opt_api(&data.avatar, &context).await?;
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;
@ -136,5 +141,15 @@ pub async fn save_user_settings(
.await
.ok();
// Update the vote display modes
let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm {
score: data.show_scores,
upvotes: data.show_upvotes,
downvotes: data.show_downvotes,
upvote_percentage: data.show_upvote_percentage,
};
LocalUserVoteDisplayMode::update(&mut context.pool(), local_user_id, &vote_display_modes_form)
.await?;
Ok(Json(SuccessResponse::default()))
}

@ -4,11 +4,8 @@ use lemmy_api_common::{
context::LemmyContext,
person::{UpdateTotp, UpdateTotpResponse},
};
use lemmy_db_schema::{
source::local_user::{LocalUser, LocalUserUpdateForm},
traits::Crud,
};
use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError;
/// Enable or disable two-factor-authentication. The current setting is determined from
@ -25,12 +22,10 @@ pub async fn update_totp(
local_user_view: LocalUserView,
context: Data<LemmyContext>,
) -> Result<Json<UpdateTotpResponse>, LemmyError> {
let site_view = SiteView::read_local(&mut context.pool()).await?;
check_totp_2fa_valid(
&local_user_view,
&Some(data.totp_token.clone()),
&site_view.site.name,
&context.settings().hostname,
)?;
// toggle the 2fa setting

@ -4,6 +4,7 @@ use lemmy_db_schema::{
source::{
actor_language::SiteLanguage,
language::Language,
local_site_url_blocklist::LocalSiteUrlBlocklist,
local_user::{LocalUser, LocalUserUpdateForm},
moderator::{ModAdd, ModAddForm},
tagline::Tagline,
@ -62,6 +63,7 @@ pub async fn leave_admin(
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
let custom_emojis =
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
Ok(Json(GetSiteResponse {
site_view,
@ -72,5 +74,6 @@ pub async fn leave_admin(
discussion_languages,
taglines,
custom_emojis,
blocked_urls,
}))
}

@ -42,8 +42,8 @@ pub async fn get_sitemap(context: Data<LemmyContext>) -> LemmyResult<HttpRespons
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
pub(crate) mod tests {
#![allow(clippy::unwrap_used)]
use crate::sitemap::generate_urlset;
use chrono::{DateTime, NaiveDate, Utc};

@ -54,17 +54,17 @@ tracing = { workspace = true, optional = true }
reqwest-middleware = { workspace = true, optional = true }
regex = { workspace = true }
rosetta-i18n = { workspace = true, optional = true }
anyhow = { workspace = true }
futures = { workspace = true, optional = true }
uuid = { workspace = true, optional = true }
tokio = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true }
ts-rs = { workspace = true, optional = true }
moka.workspace = true
anyhow.workspace = true
once_cell = { workspace = true, optional = true }
actix-web = { workspace = true, optional = true }
enum-map = { workspace = true }
urlencoding = { workspace = true }
async-trait = { workspace = true }
mime = { version = "0.3.17", optional = true }
webpage = { version = "1.6", default-features = false, features = [
"serde",
@ -73,7 +73,6 @@ encoding = { version = "0.2.33", optional = true }
jsonwebtoken = { version = "8.3.0", optional = true }
# necessary for wasmt compilation
getrandom = { version = "0.2.12", features = ["js"] }
task-local-extensions = "0.1.4"
[package.metadata.cargo-machete]
ignored = ["getrandom"]

@ -87,7 +87,7 @@ pub async fn build_post_response(
Ok(Json(PostResponse { post_view }))
}
// TODO: this function is a mess and should be split up to handle email seperately
// TODO: this function is a mess and should be split up to handle email separately
#[tracing::instrument(skip_all)]
pub async fn send_local_notifs(
mentions: Vec<MentionData>,

@ -72,9 +72,9 @@ impl Claims {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{claims::Claims, context::LemmyContext};
use actix_web::test::TestRequest;
@ -124,7 +124,9 @@ mod tests {
.password_encrypted("123456".to_string())
.build();
let inserted_local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![])
.await
.unwrap();
let req = TestRequest::default().to_http_request();
let jwt = Claims::generate(inserted_local_user.id, req, &context)

@ -1,6 +1,5 @@
use crate::request::client_builder;
use activitypub_federation::config::{Data, FederationConfig};
use anyhow::anyhow;
use lemmy_db_schema::{
source::secret::Secret,
utils::{build_db_pool_for_tests, ActualDbPool, DbPool},
@ -9,10 +8,8 @@ use lemmy_utils::{
rate_limit::RateLimitCell,
settings::{structs::Settings, SETTINGS},
};
use reqwest::{Request, Response};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware, Middleware, Next};
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use std::sync::Arc;
use task_local_extensions::Extensions;
#[derive(Clone)]
pub struct LemmyContext {
@ -55,32 +52,16 @@ impl LemmyContext {
&self.rate_limit_cell
}
/// Initialize a context for use in tests, optionally blocks network requests.
/// Initialize a context for use in tests which blocks federation network calls.
///
/// Do not use this in production code.
pub async fn init_test_context() -> Data<LemmyContext> {
Self::build_test_context(true).await
}
/// Initialize a context for use in tests, with network requests allowed.
/// TODO: get rid of this if possible.
///
/// Do not use this in production code.
pub async fn init_test_context_with_networking() -> Data<LemmyContext> {
Self::build_test_context(false).await
}
async fn build_test_context(block_networking: bool) -> Data<LemmyContext> {
// call this to run migrations
let pool = build_db_pool_for_tests().await;
let client = client_builder(&SETTINGS).build().expect("build client");
let mut client = ClientBuilder::new(client);
if block_networking {
client = client.with(BlockedMiddleware);
}
let client = client.build();
let client = ClientBuilder::new(client).build();
let secret = Secret {
id: 0,
jwt_secret: String::new(),
@ -92,24 +73,11 @@ impl LemmyContext {
let config = FederationConfig::builder()
.domain(context.settings().hostname.clone())
.app_data(context)
// Dont allow any network fetches
.http_fetch_limit(0)
.build()
.await
.expect("build federation config");
config.to_request_data()
}
}
struct BlockedMiddleware;
/// A reqwest middleware which blocks all requests
#[async_trait::async_trait]
impl Middleware for BlockedMiddleware {
async fn handle(
&self,
_req: Request,
_extensions: &mut Extensions,
_next: Next<'_>,
) -> reqwest_middleware::Result<Response> {
Err(anyhow!("Network requests not allowed").into())
}
}

@ -86,8 +86,6 @@ pub struct SaveUserSettings {
pub show_nsfw: Option<bool>,
pub blur_nsfw: Option<bool>,
pub auto_expand: Option<bool>,
/// Show post and comment scores.
pub show_scores: Option<bool>,
/// Your user's theme.
pub theme: Option<String>,
pub default_sort_type: Option<SortType>,
@ -122,6 +120,7 @@ pub struct SaveUserSettings {
pub open_links_in_new_tab: Option<bool>,
/// Enable infinite scroll
pub infinite_scroll_enabled: Option<bool>,
/// A post-view mode that changes how multiple post listings look.
pub post_listing_mode: Option<PostListingMode>,
/// Whether to allow keyboard navigation (for browsing and interacting with posts and comments).
pub enable_keyboard_navigation: Option<bool>,
@ -129,6 +128,11 @@ pub struct SaveUserSettings {
pub enable_animated_images: Option<bool>,
/// Whether to auto-collapse bot comments.
pub collapse_bot_comments: Option<bool>,
/// Some vote display mode settings
pub show_scores: Option<bool>,
pub show_upvotes: Option<bool>,
pub show_downvotes: Option<bool>,
pub show_upvote_percentage: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]

@ -4,7 +4,10 @@ use crate::{
utils::proxy_image_link,
};
use encoding::{all::encodings, DecoderTrap};
use lemmy_db_schema::newtypes::DbUrl;
use lemmy_db_schema::{
newtypes::DbUrl,
source::images::{LocalImage, LocalImageForm},
};
use lemmy_utils::{
error::{LemmyError, LemmyErrorType},
settings::structs::{PictrsImageMode, Settings},
@ -56,14 +59,8 @@ pub async fn fetch_link_metadata(
let opengraph_data = extract_opengraph_data(&html_bytes, url)
.map_err(|e| info!("{e}"))
.unwrap_or_default();
let thumbnail = extract_thumbnail_from_opengraph_data(
url,
&opengraph_data,
&content_type,
generate_thumbnail,
context,
)
.await;
let thumbnail =
extract_thumbnail_from_opengraph_data(url, &opengraph_data, generate_thumbnail, context).await;
Ok(LinkMetadata {
opengraph_data,
@ -155,23 +152,21 @@ fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> Result<OpenGraphData,
pub async fn extract_thumbnail_from_opengraph_data(
url: &Url,
opengraph_data: &OpenGraphData,
content_type: &Option<Mime>,
generate_thumbnail: bool,
context: &LemmyContext,
) -> Option<DbUrl> {
let is_image = content_type.as_ref().unwrap_or(&mime::TEXT_PLAIN).type_() == mime::IMAGE;
if generate_thumbnail && is_image {
if generate_thumbnail {
let image_url = opengraph_data
.image
.as_ref()
.map(lemmy_db_schema::newtypes::DbUrl::inner)
.map(DbUrl::inner)
.unwrap_or(url);
generate_pictrs_thumbnail(image_url, context)
.await
.ok()
.map(Into::into)
} else {
None
opengraph_data.image.clone()
}
}
@ -184,7 +179,6 @@ struct PictrsResponse {
#[derive(Deserialize, Debug)]
struct PictrsFile {
file: String,
#[allow(dead_code)]
delete_token: String,
}
@ -287,6 +281,14 @@ async fn generate_pictrs_thumbnail(
context.settings().get_protocol_and_hostname(),
response.files.first().expect("missing pictrs file").file
))?;
for uploaded_image in response.files {
let form = LocalImageForm {
local_user_id: None,
pictrs_alias: uploaded_image.file.to_string(),
pictrs_delete_token: uploaded_image.delete_token.to_string(),
};
LocalImage::create(&mut context.pool(), &form).await?;
}
Ok(thumbnail_url)
} else {
Err(LemmyErrorType::PictrsResponseError(response.msg))?
@ -311,9 +313,9 @@ async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> Resu
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
context::LemmyContext,
@ -327,7 +329,7 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_link_metadata() {
let context = LemmyContext::init_test_context_with_networking().await;
let context = LemmyContext::init_test_context().await;
let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap();
let sample_res = fetch_link_metadata(&sample_url, false, &context)
.await
@ -353,7 +355,7 @@ mod tests {
Some(mime::TEXT_HTML_UTF_8.to_string()),
sample_res.content_type
);
assert_eq!(None, sample_res.thumbnail);
assert!(sample_res.thumbnail.is_some());
}
// #[test]

@ -6,6 +6,7 @@ use lemmy_db_schema::{
federation_queue_state::FederationQueueState,
instance::Instance,
language::Language,
local_site_url_blocklist::LocalSiteUrlBlocklist,
tagline::Tagline,
},
ListingType,
@ -268,6 +269,8 @@ pub struct EditSite {
pub allowed_instances: Option<Vec<String>>,
/// A list of blocked instances.
pub blocked_instances: Option<Vec<String>>,
/// A list of blocked URLs
pub blocked_urls: Option<Vec<String>>,
/// A list of taglines shown at the top of the front page.
pub taglines: Option<Vec<String>>,
pub registration_mode: Option<RegistrationMode>,
@ -305,6 +308,7 @@ pub struct GetSiteResponse {
pub taglines: Vec<Tagline>,
/// A list of custom emojis your site supports.
pub custom_emojis: Vec<CustomEmojiView>,
pub blocked_urls: Vec<LocalSiteUrlBlocklist>,
}
#[skip_serializing_none]

@ -17,6 +17,7 @@ use lemmy_db_schema::{
instance_block::InstanceBlock,
local_site::LocalSite,
local_site_rate_limit::LocalSiteRateLimit,
local_site_url_blocklist::LocalSiteUrlBlocklist,
password_reset_request::PasswordResetRequest,
person::{Person, PersonUpdateForm},
person_block::PersonBlock,
@ -38,18 +39,24 @@ use lemmy_utils::{
rate_limit::{ActionType, BucketConfig},
settings::structs::{PictrsImageMode, Settings},
utils::{
markdown::markdown_rewrite_image_links,
markdown::{markdown_check_for_blocked_urls, markdown_rewrite_image_links},
slurs::{build_slur_regex, remove_slurs},
},
};
use regex::Regex;
use moka::future::Cache;
use once_cell::sync::Lazy;
use regex::{escape, Regex, RegexSet};
use rosetta_i18n::{Language, LanguageId};
use std::collections::HashSet;
use std::{collections::HashSet, time::Duration};
use tracing::warn;
use url::{ParseError, Url};
use urlencoding::encode;
pub static AUTH_COOKIE_NAME: &str = "jwt";
#[cfg(debug_assertions)]
static URL_BLOCKLIST_RECHECK_DELAY: Duration = Duration::from_millis(500);
#[cfg(not(debug_assertions))]
static URL_BLOCKLIST_RECHECK_DELAY: Duration = Duration::from_secs(60);
#[tracing::instrument(skip_all)]
pub async fn is_mod_or_admin(
@ -516,6 +523,47 @@ pub fn local_site_opt_to_sensitive(local_site: &Option<LocalSite>) -> bool {
.unwrap_or(false)
}
pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet> {
static URL_BLOCKLIST: Lazy<Cache<(), RegexSet>> = Lazy::new(|| {
Cache::builder()
.max_capacity(1)
.time_to_live(URL_BLOCKLIST_RECHECK_DELAY)
.build()
});
Ok(
URL_BLOCKLIST
.try_get_with::<_, LemmyError>((), async {
let urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
let regexes = urls.iter().map(|url| {
let url = &url.url;
let parsed = Url::parse(url).expect("Coundln't parse URL.");
if url.ends_with('/') {
format!(
"({}://)?{}{}?",
parsed.scheme(),
escape(parsed.domain().expect("No domain.")),
escape(parsed.path())
)
} else {
format!(
"({}://)?{}{}",
parsed.scheme(),
escape(parsed.domain().expect("No domain.")),
escape(parsed.path())
)
}
});
let set = RegexSet::new(regexes)?;
Ok(set)
})
.await
.map_err(|e| anyhow::anyhow!("Failed to build URL blocklist due to `{}`", e))?,
)
}
pub async fn send_application_approved_email(
user: &LocalUserView,
settings: &Settings,
@ -890,9 +938,13 @@ fn limit_expire_time(expires: DateTime<Utc>) -> LemmyResult<Option<DateTime<Utc>
pub async fn process_markdown(
text: &str,
slur_regex: &Option<Regex>,
url_blocklist: &RegexSet,
context: &LemmyContext,
) -> LemmyResult<String> {
let text = remove_slurs(text, slur_regex);
markdown_check_for_blocked_urls(&text, url_blocklist)?;
if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages {
let (text, links) = markdown_rewrite_image_links(text);
RemoteImage::create(&mut context.pool(), links).await?;
@ -905,10 +957,13 @@ pub async fn process_markdown(
pub async fn process_markdown_opt(
text: &Option<String>,
slur_regex: &Option<Regex>,
url_blocklist: &RegexSet,
context: &LemmyContext,
) -> LemmyResult<Option<String>> {
match text {
Some(t) => process_markdown(t, slur_regex, context).await.map(Some),
Some(t) => process_markdown(t, slur_regex, url_blocklist, context)
.await
.map(Some),
None => Ok(None),
}
}
@ -987,9 +1042,9 @@ pub async fn proxy_image_link_opt_apub(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use pretty_assertions::assert_eq;

@ -10,6 +10,7 @@ use lemmy_api_common::{
check_post_deleted_or_removed,
generate_local_apub_endpoint,
get_post,
get_url_blocklist,
is_mod_or_admin,
local_site_to_slur_regex,
process_markdown,
@ -44,7 +45,8 @@ pub async fn create_comment(
let local_site = LocalSite::read(&mut context.pool()).await?;
let slur_regex = local_site_to_slur_regex(&local_site);
let content = process_markdown(&data.content, &slur_regex, &context).await?;
let url_blocklist = get_url_blocklist(&context).await?;
let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&Some(content.clone()), false)?;
// Check for a community ban
@ -162,10 +164,15 @@ pub async fn create_comment(
)
.await?;
// If its a reply, mark the parent as read
// If we're responding to a comment where we're the recipient,
// (ie we're the grandparent, or the recipient of the parent comment_reply),
// then mark the parent as read.
// Then we don't have to do it manually after we respond to a comment.
if let Some(parent) = parent_opt {
let person_id = local_user_view.person.id;
let parent_id = parent.id;
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), parent_id).await;
let comment_reply =
CommentReply::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
if let Ok(reply) = comment_reply {
CommentReply::update(
&mut context.pool(),
@ -177,7 +184,6 @@ pub async fn create_comment(
}
// If the parent has PersonMentions mark them as read too
let person_id = local_user_view.person.id;
let person_mention =
PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
if let Ok(mention) = person_mention {

@ -5,7 +5,12 @@ use lemmy_api_common::{
comment::{CommentResponse, EditComment},
context::LemmyContext,
send_activity::{ActivityChannel, SendActivityData},
utils::{check_community_user_action, local_site_to_slur_regex, process_markdown_opt},
utils::{
check_community_user_action,
get_url_blocklist,
local_site_to_slur_regex,
process_markdown_opt,
},
};
use lemmy_db_schema::{
source::{
@ -54,7 +59,8 @@ pub async fn update_comment(
.await?;
let slur_regex = local_site_to_slur_regex(&local_site);
let content = process_markdown_opt(&data.content, &slur_regex, &context).await?;
let url_blocklist = get_url_blocklist(&context).await?;
let content = process_markdown_opt(&data.content, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&content, false)?;
let comment_id = data.comment_id;

@ -9,6 +9,7 @@ use lemmy_api_common::{
generate_inbox_url,
generate_local_apub_endpoint,
generate_shared_inbox_url,
get_url_blocklist,
is_admin,
local_site_to_slur_regex,
process_markdown_opt,
@ -53,9 +54,11 @@ pub async fn create_community(
}
let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?;
check_slurs(&data.name, &slur_regex)?;
check_slurs(&data.title, &slur_regex)?;
let description = process_markdown_opt(&data.description, &slur_regex, &context).await?;
let description =
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?;
let icon = proxy_image_link_api(&data.icon, &context).await?;
let banner = proxy_image_link_api(&data.banner, &context).await?;

@ -7,6 +7,7 @@ use lemmy_api_common::{
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_community_mod_action,
get_url_blocklist,
local_site_to_slur_regex,
process_markdown_opt,
proxy_image_link_opt_api,
@ -36,8 +37,10 @@ pub async fn update_community(
let local_site = LocalSite::read(&mut context.pool()).await?;
let slur_regex = local_site_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(&context).await?;
check_slurs_opt(&data.title, &slur_regex)?;
let description = process_markdown_opt(&data.description, &slur_regex, &context).await?;
let description =
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&data.description, false)?;
let description = diesel_option_overwrite(description);

@ -9,6 +9,7 @@ use lemmy_api_common::{
utils::{
check_community_user_action,
generate_local_apub_endpoint,
get_url_blocklist,
honeypot_check,
local_site_to_slur_regex,
mark_post_as_read,
@ -29,7 +30,7 @@ use lemmy_db_schema::{
CommunityVisibility,
};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_actor::structs::CommunityView;
use lemmy_db_views_actor::structs::CommunityModeratorView;
use lemmy_utils::{
error::{LemmyError, LemmyErrorExt, LemmyErrorType},
spawn_try_task,
@ -38,6 +39,7 @@ use lemmy_utils::{
validation::{
check_url_scheme,
clean_url_params,
is_url_blocked,
is_valid_alt_text_field,
is_valid_body_field,
is_valid_post_title,
@ -60,8 +62,9 @@ pub async fn create_post(
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&data.name, &slur_regex)?;
let url_blocklist = get_url_blocklist(&context).await?;
let body = process_markdown_opt(&data.body, &slur_regex, &context).await?;
let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?;
let data_url = data.url.as_ref();
let url = data_url.map(clean_url_params); // TODO no good way to handle a "clear"
let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params);
@ -69,6 +72,7 @@ pub async fn create_post(
is_valid_post_title(&data.name)?;
is_valid_body_field(&body, true)?;
is_valid_alt_text_field(&data.alt_text)?;
is_url_blocked(&url, &url_blocklist)?;
check_url_scheme(&url)?;
check_url_scheme(&custom_thumbnail)?;
@ -83,10 +87,10 @@ pub async fn create_post(
let community = Community::read(&mut context.pool(), community_id).await?;
if community.posting_restricted_to_mods {
let community_id = data.community_id;
let is_mod = CommunityView::is_mod_or_admin(
let is_mod = CommunityModeratorView::is_community_moderator(
&mut context.pool(),
local_user_view.local_user.person_id,
community_id,
local_user_view.local_user.person_id,
)
.await?;
if !is_mod {

@ -8,6 +8,7 @@ use lemmy_api_common::{
send_activity::{ActivityChannel, SendActivityData},
utils::{
check_community_user_action,
get_url_blocklist,
local_site_to_slur_regex,
process_markdown_opt,
proxy_image_link_opt_apub,
@ -30,6 +31,7 @@ use lemmy_utils::{
validation::{
check_url_scheme,
clean_url_params,
is_url_blocked,
is_valid_alt_text_field,
is_valid_body_field,
is_valid_post_title,
@ -51,9 +53,11 @@ pub async fn update_post(
let url = data.url.as_ref().map(clean_url_params);
let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params);
let url_blocklist = get_url_blocklist(&context).await?;
let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs_opt(&data.name, &slur_regex)?;
let body = process_markdown_opt(&data.body, &slur_regex, &context).await?;
let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?;
if let Some(name) = &data.name {
is_valid_post_title(name)?;
@ -61,6 +65,7 @@ pub async fn update_post(
is_valid_body_field(&body, true)?;
is_valid_alt_text_field(&data.alt_text)?;
is_url_blocked(&url, &url_blocklist)?;
check_url_scheme(&url)?;
check_url_scheme(&custom_thumbnail)?;

@ -8,6 +8,7 @@ use lemmy_api_common::{
check_person_block,
generate_local_apub_endpoint,
get_interface_language,
get_url_blocklist,
local_site_to_slur_regex,
process_markdown,
send_email_to_user,
@ -36,7 +37,8 @@ pub async fn create_private_message(
let local_site = LocalSite::read(&mut context.pool()).await?;
let slur_regex = local_site_to_slur_regex(&local_site);
let content = process_markdown(&data.content, &slur_regex, &context).await?;
let url_blocklist = get_url_blocklist(&context).await?;
let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&Some(content.clone()), false)?;
check_person_block(

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

@ -6,6 +6,7 @@ use lemmy_api_common::{
site::{CreateSite, SiteResponse},
utils::{
generate_shared_inbox_url,
get_url_blocklist,
is_admin,
local_site_rate_limit_to_rate_limit_config,
local_site_to_slur_regex,
@ -58,7 +59,8 @@ pub async fn create_site(
let keypair = generate_actor_keypair()?;
let slur_regex = local_site_to_slur_regex(&local_site);
let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &context).await?;
let url_blocklist = get_url_blocklist(&context).await?;
let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?;
let icon = proxy_image_link_opt_api(&data.icon, &context).await?;
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;
@ -187,9 +189,9 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) ->
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::site::create::validate_create_payload;
use lemmy_api_common::site::CreateSite;

@ -41,9 +41,9 @@ pub fn application_question_check(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::site::{application_question_check, site_default_post_listing_type_check};
use lemmy_db_schema::{ListingType, RegistrationMode};

@ -6,6 +6,7 @@ use lemmy_api_common::{
use lemmy_db_schema::source::{
actor_language::{LocalUserLanguage, SiteLanguage},
language::Language,
local_site_url_blocklist::LocalSiteUrlBlocklist,
tagline::Tagline,
};
use lemmy_db_views::structs::{CustomEmojiView, LocalUserView, SiteView};
@ -47,6 +48,7 @@ pub async fn get_site(
let taglines = Tagline::get_all(&mut context.pool(), site_view.local_site.id).await?;
let custom_emojis =
CustomEmojiView::get_all(&mut context.pool(), site_view.local_site.id).await?;
let blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
Ok(GetSiteResponse {
site_view,
admins,
@ -56,6 +58,7 @@ pub async fn get_site(
discussion_languages,
taglines,
custom_emojis,
blocked_urls,
})
})
.await

@ -4,6 +4,7 @@ use lemmy_api_common::{
context::LemmyContext,
site::{EditSite, SiteResponse},
utils::{
get_url_blocklist,
is_admin,
local_site_rate_limit_to_rate_limit_config,
local_site_to_slur_regex,
@ -18,6 +19,7 @@ use lemmy_db_schema::{
federation_blocklist::FederationBlockList,
local_site::{LocalSite, LocalSiteUpdateForm},
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
local_site_url_blocklist::LocalSiteUrlBlocklist,
local_user::LocalUser,
site::{Site, SiteUpdateForm},
tagline::Tagline,
@ -34,6 +36,7 @@ use lemmy_utils::{
validation::{
build_and_check_regex,
check_site_visibility_valid,
check_urls_are_valid,
is_valid_body_field,
site_description_length_check,
site_name_length_check,
@ -61,7 +64,8 @@ pub async fn update_site(
}
let slur_regex = local_site_to_slur_regex(&local_site);
let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &context).await?;
let url_blocklist = get_url_blocklist(&context).await?;
let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?;
let icon = proxy_image_link_opt_api(&data.icon, &context).await?;
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;
@ -137,6 +141,11 @@ pub async fn update_site(
let blocked = data.blocked_instances.clone();
FederationBlockList::replace(&mut context.pool(), blocked).await?;
if let Some(url_blocklist) = data.blocked_urls.clone() {
let parsed_urls = check_urls_are_valid(&url_blocklist)?;
LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?;
}
// TODO can't think of a better way to do this.
// If the server suddenly requires email verification, or required applications, no old users
// will be able to log in. It really only wants this to be a requirement for NEW signups.
@ -222,9 +231,9 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::site::update::validate_update_payload;
use lemmy_api_common::site::EditSite;
@ -578,6 +587,7 @@ mod tests {
captcha_difficulty: None,
allowed_instances: None,
blocked_instances: None,
blocked_urls: None,
taglines: None,
registration_mode: site_registration_mode,
reports_email_admins: None,

@ -20,7 +20,9 @@ use lemmy_db_schema::{
aggregates::structs::PersonAggregates,
source::{
captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer},
language::Language,
local_user::{LocalUser, LocalUserInsertForm},
local_user_vote_display_mode::LocalUserVoteDisplayMode,
person::{Person, PersonInsertForm},
registration_application::{RegistrationApplication, RegistrationApplicationInsertForm},
},
@ -35,6 +37,7 @@ use lemmy_utils::{
validation::is_valid_actor_name,
},
};
use std::collections::HashSet;
#[tracing::instrument(skip(context))]
pub async fn register(
@ -127,12 +130,15 @@ pub async fn register(
let accepted_application = Some(!require_registration_application);
// Get the user's preferred language using the Accept-Language header
let language_tag = req.headers().get("Accept-Language").and_then(|hdr| {
accept_language::parse(hdr.to_str().unwrap_or_default())
.first()
// Remove the optional region code
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string())
});
let language_tags: Vec<String> = req
.headers()
.get("Accept-Language")
.map(|hdr| accept_language::parse(hdr.to_str().unwrap_or_default()))
.iter()
.flatten()
// Remove the optional region code
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string())
.collect();
// Create the local user
let local_user_form = LocalUserInsertForm::builder()
@ -143,12 +149,23 @@ pub async fn register(
.accepted_application(accepted_application)
.default_listing_type(Some(local_site.default_post_listing_type))
.post_listing_mode(Some(local_site.default_post_listing_mode))
.interface_language(language_tag)
.interface_language(language_tags.first().cloned())
// If its the initial site setup, they are an admin
.admin(Some(!local_site.site_setup))
.build();
let inserted_local_user = LocalUser::create(&mut context.pool(), &local_user_form).await?;
let all_languages = Language::read_all(&mut context.pool()).await?;
// use hashset to avoid duplicates
let mut language_ids = HashSet::new();
for l in language_tags {
if let Some(found) = all_languages.iter().find(|all| all.code == l) {
language_ids.insert(found.id);
}
}
let language_ids = language_ids.into_iter().collect();
let inserted_local_user =
LocalUser::create(&mut context.pool(), &local_user_form, language_ids).await?;
if local_site.site_setup && require_registration_application {
// Create the registration application
@ -183,6 +200,7 @@ pub async fn register(
if local_site.require_email_verification {
let local_user_view = LocalUserView {
local_user: inserted_local_user,
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
person: inserted_person,
counts: PersonAggregates::default(),
};

@ -3,5 +3,6 @@
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "http://ds9.lemmy.ml/u/lemmy_alpha",
"type": "Delete",
"id": "http://ds9.lemmy.ml/activities/delete/f2abee48-c7bb-41d5-9e27-8775ff32db12"
"id": "http://ds9.lemmy.ml/activities/delete/f2abee48-c7bb-41d5-9e27-8775ff32db12",
"removeData": true
}

@ -2,6 +2,7 @@
"type": "Application",
"id": "https://enterprise.lemmy.ml/",
"name": "Enterprise",
"preferredUsername": "enterprise.lemmy.ml",
"summary": "A test instance",
"content": "<p>Enterprise sidebar</p>\\n",
"mediaType": "text/html",

@ -11,21 +11,21 @@
"votersCount": "toot:votersCount"
}
],
"id": "https://dice.camp/users/thekernelinyellow/statuses/110830743680706519",
"id": "https://masto.qa.urbanwildlife.biz/users/mastodon/statuses/110830743680706519",
"type": "Note",
"summary": null,
"inReplyTo": null,
"published": "2023-08-04T09:55:39Z",
"url": "https://dice.camp/@thekernelinyellow/110830743680706519",
"attributedTo": "https://dice.camp/users/thekernelinyellow",
"url": "https://masto.qa.urbanwildlife.biz/110830743680706519",
"attributedTo": "https://masto.qa.urbanwildlife.biz/users/mastodon",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": [
"https://dice.camp/users/thekernelinyellow/followers",
"https://masto.qa.urbanwildlife.biz/users/mastodon/followers",
"https://enterprise.lemmy.ml/c/tenforward",
"https://enterprise.lemmy.ml/c/tenforward/followers"
],
"sensitive": false,
"atomUri": "https://dice.camp/users/thekernelinyellow/statuses/110830743680706519",
"atomUri": "https://masto.qa.urbanwildlife.biz/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>",
@ -41,12 +41,12 @@
}
],
"replies": {
"id": "https://dice.camp/users/thekernelinyellow/statuses/110830743680706519/replies",
"id": "https://masto.qa.urbanwildlife.biz/users/mastodon/statuses/110830743680706519/replies",
"type": "Collection",
"first": {
"type": "CollectionPage",
"next": "https://dice.camp/users/thekernelinyellow/statuses/110830743680706519/replies?only_other_accounts=true&page=true",
"partOf": "https://dice.camp/users/thekernelinyellow/statuses/110830743680706519/replies",
"next": "https://masto.qa.urbanwildlife.biz/users/mastodon/statuses/110830743680706519/replies?only_other_accounts=true&page=true",
"partOf": "https://masto.qa.urbanwildlife.biz/users/mastodon/statuses/110830743680706519/replies",
"items": []
}
}

@ -72,6 +72,7 @@ impl BlockUser {
)?,
audience,
expires,
end_time: expires,
})
}
@ -149,7 +150,7 @@ impl ActivityHandler for BlockUser {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
let expires = self.expires.map(Into::into);
let expires = self.expires.or(self.end_time).map(Into::into);
let mod_person = self.actor.dereference(context).await?;
let blocked_person = self.object.dereference(context).await?;
let target = self.target.dereference(context).await?;

@ -98,7 +98,7 @@ impl ActivityHandler for UndoBlockUser {
#[tracing::instrument(skip_all)]
async fn receive(self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
insert_received_activity(&self.id, context).await?;
let expires = self.object.expires.map(Into::into);
let expires = self.object.expires.or(self.object.end_time).map(Into::into);
let mod_person = self.actor.dereference(context).await?;
let blocked_person = self.object.object.dereference(context).await?;
match self.object.target.dereference(context).await? {

@ -2,7 +2,7 @@ use crate::{
activities::{
generate_activity_id,
verify_person_in_community,
voting::{vote_comment, vote_post},
voting::{undo_vote_comment, undo_vote_post, vote_comment, vote_post},
},
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson},
@ -17,7 +17,6 @@ use activitypub_federation::{
fetch::object_id::ObjectId,
traits::{ActivityHandler, Actor},
};
use anyhow::anyhow;
use lemmy_api_common::{context::LemmyContext, utils::check_bot_account};
use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_utils::error::LemmyError;
@ -58,15 +57,7 @@ impl ActivityHandler for Vote {
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?;
let enable_downvotes = LocalSite::read(&mut context.pool())
.await
.map(|l| l.enable_downvotes)
.unwrap_or(true);
if self.kind == VoteType::Dislike && !enable_downvotes {
Err(anyhow!("Downvotes disabled").into())
} else {
Ok(())
}
Ok(())
}
#[tracing::instrument(skip_all)]
@ -77,9 +68,22 @@ impl ActivityHandler for Vote {
check_bot_account(&actor.0)?;
match object {
PostOrComment::Post(p) => vote_post(&self.kind, actor, &p, context).await,
PostOrComment::Comment(c) => vote_comment(&self.kind, actor, &c, context).await,
let enable_downvotes = LocalSite::read(&mut context.pool())
.await
.map(|l| l.enable_downvotes)
.unwrap_or(true);
if self.kind == VoteType::Dislike && !enable_downvotes {
// If this is a downvote but downvotes are ignored, only undo any existing vote
match object {
PostOrComment::Post(p) => undo_vote_post(actor, &p, context).await,
PostOrComment::Comment(c) => undo_vote_comment(actor, &c, context).await,
}
} else {
// Otherwise apply the vote normally
match object {
PostOrComment::Post(p) => vote_post(&self.kind, actor, &p, context).await,
PostOrComment::Comment(c) => vote_comment(&self.kind, actor, &c, context).await,
}
}
}
}

@ -123,8 +123,8 @@ impl InCommunity for AnnouncableActivities {
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::indexing_slicing)]
use crate::{
activity_lists::{GroupInboxActivities, PersonInboxActivities, SharedInboxActivities},

@ -27,8 +27,11 @@ pub async fn list_comments(
check_private_instance(&local_user_view, &local_site)?;
let community_id = if let Some(name) = &data.community_name {
Some(resolve_actor_identifier::<ApubCommunity, Community>(name, &context, &None, true).await?)
.map(|c| c.id)
Some(
resolve_actor_identifier::<ApubCommunity, Community>(name, &context, &local_user_view, true)
.await?,
)
.map(|c| c.id)
} else {
data.community_id
};

@ -30,8 +30,11 @@ pub async fn list_posts(
let page = data.page;
let limit = data.limit;
let community_id = if let Some(name) = &data.community_name {
Some(resolve_actor_identifier::<ApubCommunity, Community>(name, &context, &None, true).await?)
.map(|c| c.id)
Some(
resolve_actor_identifier::<ApubCommunity, Community>(name, &context, &local_user_view, true)
.await?,
)
.map(|c| c.id)
} else {
data.community_id
};

@ -1,7 +1,6 @@
use crate::fetcher::search::{
search_query_to_object_id,
search_query_to_object_id_local,
SearchableObjects,
use crate::fetcher::{
search::{search_query_to_object_id, search_query_to_object_id_local, SearchableObjects},
user_or_community::UserOrCommunity,
};
use activitypub_federation::config::Data;
use actix_web::web::{Json, Query};
@ -31,7 +30,7 @@ pub async fn resolve_object(
let res = if is_authenticated {
// user is fully authenticated; allow remote lookups as well.
search_query_to_object_id(&data.q, &context).await
search_query_to_object_id(data.q.clone(), &context).await
} else {
// user isn't authenticated only allow a local search.
search_query_to_object_id_local(&data.q, &context).await
@ -52,14 +51,6 @@ async fn convert_response(
let removed_or_deleted;
let mut res = ResolveObjectResponse::default();
match object {
Person(p) => {
removed_or_deleted = p.deleted;
res.person = Some(PersonView::read(pool, p.id).await?)
}
Community(c) => {
removed_or_deleted = c.deleted || c.removed;
res.community = Some(CommunityView::read(pool, c.id, user_id, false).await?)
}
Post(p) => {
removed_or_deleted = p.deleted || p.removed;
res.post = Some(PostView::read(pool, p.id, user_id, false).await?)
@ -68,6 +59,16 @@ async fn convert_response(
removed_or_deleted = c.deleted || c.removed;
res.comment = Some(CommentView::read(pool, c.id, user_id).await?)
}
PersonOrCommunity(p) => match *p {
UserOrCommunity::User(u) => {
removed_or_deleted = u.deleted;
res.person = Some(PersonView::read(pool, u.id).await?)
}
UserOrCommunity::Community(c) => {
removed_or_deleted = c.deleted || c.removed;
res.community = Some(CommunityView::read(pool, c.id, user_id, false).await?)
}
},
};
// if the object was deleted from database, dont return it
if removed_or_deleted {

@ -17,6 +17,7 @@ use lemmy_db_schema::{
instance::Instance,
instance_block::{InstanceBlock, InstanceBlockForm},
local_user::{LocalUser, LocalUserUpdateForm},
local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeUpdateForm},
person::{Person, PersonUpdateForm},
person_block::{PersonBlock, PersonBlockForm},
post::{PostSaved, PostSavedForm},
@ -50,6 +51,7 @@ pub struct UserSettingsBackup {
// 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>,
pub vote_display_mode_settings: Option<LocalUserVoteDisplayMode>,
#[serde(default)]
pub followed_communities: Vec<ObjectId<ApubCommunity>>,
#[serde(default)]
@ -80,6 +82,7 @@ pub async fn export_settings(
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),
vote_display_mode_settings: Some(local_user_view.local_user_vote_display_mode),
followed_communities: vec_into(lists.followed_communities),
blocked_communities: vec_into(lists.blocked_communities),
blocked_instances: lists.blocked_instances,
@ -132,6 +135,27 @@ pub async fn import_settings(
)
.await?;
// Update the vote display mode settings
let vote_display_mode_form = LocalUserVoteDisplayModeUpdateForm {
score: data.vote_display_mode_settings.as_ref().map(|s| s.score),
upvotes: data.vote_display_mode_settings.as_ref().map(|s| s.upvotes),
downvotes: data
.vote_display_mode_settings
.as_ref()
.map(|s| s.downvotes),
upvote_percentage: data
.vote_display_mode_settings
.as_ref()
.map(|s| s.upvote_percentage),
};
LocalUserVoteDisplayMode::update(
&mut context.pool(),
local_user_view.local_user.id,
&vote_display_mode_form,
)
.await?;
let url_count = data.followed_communities.len()
+ data.blocked_communities.len()
+ data.blocked_users.len()
@ -295,8 +319,8 @@ pub async fn import_settings(
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::indexing_slicing)]
use crate::api::user_settings_backup::{export_settings, import_settings};
use activitypub_federation::config::Data;
@ -337,7 +361,7 @@ mod tests {
.person_id(person.id)
.password_encrypted("pass".to_string())
.build();
let local_user = LocalUser::create(&mut context.pool(), &user_form).await?;
let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?;
Ok(LocalUserView::read(&mut context.pool(), local_user.id).await?)
}

@ -101,8 +101,8 @@ impl Collection for ApubCommunityModerators {
}
#[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{

@ -1,6 +1,7 @@
use crate::{
fetcher::user_or_community::{PersonOrGroup, UserOrCommunity},
objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::objects::{group::Group, note::Note, page::Page, person::Person},
protocol::objects::{note::Note, page::Page},
};
use activitypub_federation::{
config::Data,
@ -9,7 +10,7 @@ use activitypub_federation::{
};
use chrono::{DateTime, Utc};
use lemmy_api_common::context::LemmyContext;
use lemmy_utils::error::{LemmyError, LemmyErrorType};
use lemmy_utils::error::LemmyError;
use serde::Deserialize;
use url::Url;
@ -18,28 +19,22 @@ use url::Url;
/// which gets resolved to an URL.
#[tracing::instrument(skip_all)]
pub(crate) async fn search_query_to_object_id(
query: &str,
mut query: String,
context: &Data<LemmyContext>,
) -> Result<SearchableObjects, LemmyError> {
Ok(match Url::parse(query) {
Ok(match Url::parse(&query) {
Ok(url) => {
// its already an url, just go with it
ObjectId::from(url).dereference(context).await?
}
Err(_) => {
// not an url, try to resolve via webfinger
let mut chars = query.chars();
let kind = chars.next();
let identifier = chars.as_str();
match kind {
Some('@') => SearchableObjects::Person(
webfinger_resolve_actor::<LemmyContext, ApubPerson>(identifier, context).await?,
),
Some('!') => SearchableObjects::Community(
webfinger_resolve_actor::<LemmyContext, ApubCommunity>(identifier, context).await?,
),
_ => return Err(LemmyErrorType::InvalidQuery)?,
if query.starts_with('!') || query.starts_with('@') {
query.remove(0);
}
SearchableObjects::PersonOrCommunity(Box::new(
webfinger_resolve_actor::<LemmyContext, UserOrCommunity>(&query, context).await?,
))
}
})
}
@ -59,19 +54,17 @@ pub(crate) async fn search_query_to_object_id_local(
/// The types of ActivityPub objects that can be fetched directly by searching for their ID.
#[derive(Debug)]
pub(crate) enum SearchableObjects {
Person(ApubPerson),
Community(ApubCommunity),
Post(ApubPost),
Comment(ApubComment),
PersonOrCommunity(Box<UserOrCommunity>),
}
#[derive(Deserialize)]
#[serde(untagged)]
pub(crate) enum SearchableKinds {
Group(Group),
Person(Person),
Page(Page),
Page(Box<Page>),
Note(Note),
PersonOrGroup(Box<PersonOrGroup>),
}
#[async_trait::async_trait]
@ -82,10 +75,9 @@ impl Object for SearchableObjects {
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
match self {
SearchableObjects::Person(p) => p.last_refreshed_at(),
SearchableObjects::Community(c) => c.last_refreshed_at(),
SearchableObjects::Post(p) => p.last_refreshed_at(),
SearchableObjects::Comment(c) => c.last_refreshed_at(),
SearchableObjects::PersonOrCommunity(p) => p.last_refreshed_at(),
}
}
@ -99,13 +91,9 @@ impl Object for SearchableObjects {
object_id: Url,
context: &Data<Self::DataType>,
) -> Result<Option<Self>, LemmyError> {
let c = ApubCommunity::read_from_id(object_id.clone(), context).await?;
if let Some(c) = c {
return Ok(Some(SearchableObjects::Community(c)));
}
let p = ApubPerson::read_from_id(object_id.clone(), context).await?;
if let Some(p) = p {
return Ok(Some(SearchableObjects::Person(p)));
let uc = UserOrCommunity::read_from_id(object_id.clone(), context).await?;
if let Some(uc) = uc {
return Ok(Some(SearchableObjects::PersonOrCommunity(Box::new(uc))));
}
let p = ApubPost::read_from_id(object_id.clone(), context).await?;
if let Some(p) = p {
@ -121,10 +109,12 @@ impl Object for SearchableObjects {
#[tracing::instrument(skip_all)]
async fn delete(self, data: &Data<Self::DataType>) -> Result<(), LemmyError> {
match self {
SearchableObjects::Person(p) => p.delete(data).await,
SearchableObjects::Community(c) => c.delete(data).await,
SearchableObjects::Post(p) => p.delete(data).await,
SearchableObjects::Comment(c) => c.delete(data).await,
SearchableObjects::PersonOrCommunity(pc) => match *pc {
UserOrCommunity::User(p) => p.delete(data).await,
UserOrCommunity::Community(c) => c.delete(data).await,
},
}
}
@ -139,10 +129,12 @@ impl Object for SearchableObjects {
data: &Data<Self::DataType>,
) -> Result<(), LemmyError> {
match apub {
SearchableKinds::Group(a) => ApubCommunity::verify(a, expected_domain, data).await,
SearchableKinds::Person(a) => ApubPerson::verify(a, expected_domain, data).await,
SearchableKinds::Page(a) => ApubPost::verify(a, expected_domain, data).await,
SearchableKinds::Note(a) => ApubComment::verify(a, expected_domain, data).await,
SearchableKinds::PersonOrGroup(pg) => match pg.as_ref() {
PersonOrGroup::Person(a) => ApubPerson::verify(a, expected_domain, data).await,
PersonOrGroup::Group(a) => ApubCommunity::verify(a, expected_domain, data).await,
},
}
}
@ -151,10 +143,11 @@ impl Object for SearchableObjects {
use SearchableKinds as SAT;
use SearchableObjects as SO;
Ok(match apub {
SAT::Group(g) => SO::Community(ApubCommunity::from_json(g, context).await?),
SAT::Person(p) => SO::Person(ApubPerson::from_json(p, context).await?),
SAT::Page(p) => SO::Post(ApubPost::from_json(p, context).await?),
SAT::Page(p) => SO::Post(ApubPost::from_json(*p, context).await?),
SAT::Note(n) => SO::Comment(ApubComment::from_json(n, context).await?),
SAT::PersonOrGroup(pg) => {
SO::PersonOrCommunity(Box::new(UserOrCommunity::from_json(*pg, context).await?))
}
})
}
}

@ -72,7 +72,7 @@ pub(crate) async fn get_apub_community_followers(
}
/// Returns the community outbox, which is populated by a maximum of 20 posts (but no other
/// activites like votes or comments).
/// activities like votes or comments).
pub(crate) async fn get_apub_community_outbox(
info: web::Path<CommunityQuery>,
context: Data<LemmyContext>,
@ -115,9 +115,9 @@ pub(crate) async fn get_apub_community_featured(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
pub(crate) mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::protocol::objects::{group::Group, tombstone::Tombstone};

@ -18,7 +18,7 @@ use activitypub_federation::{
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{is_mod_or_admin, local_site_opt_to_slur_regex, process_markdown},
utils::{get_url_blocklist, is_mod_or_admin, local_site_opt_to_slur_regex, process_markdown},
};
use lemmy_db_schema::{
source::{
@ -165,7 +165,8 @@ 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 = process_markdown(&content, slur_regex, context).await?;
let url_blocklist = get_url_blocklist(context).await?;
let content = process_markdown(&content, slur_regex, &url_blocklist, context).await?;
let language_id =
LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?;

@ -21,6 +21,7 @@ use lemmy_api_common::{
generate_featured_url,
generate_moderators_url,
generate_outbox_url,
get_url_blocklist,
local_site_opt_to_slur_regex,
process_markdown_opt,
proxy_image_link_opt_apub,
@ -141,8 +142,10 @@ impl Object for ApubCommunity {
let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(context).await?;
let description = read_from_string_or_source_opt(&group.summary, &None, &group.source);
let description = process_markdown_opt(&description, slur_regex, context).await?;
let description =
process_markdown_opt(&description, slur_regex, &url_blocklist, context).await?;
let icon = proxy_image_link_opt_apub(group.icon.map(|i| i.url), context).await?;
let banner = proxy_image_link_opt_apub(group.image.map(|i| i.url), context).await?;
@ -177,18 +180,21 @@ impl Object for ApubCommunity {
let community: ApubCommunity = community.into();
// 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.
// These collections are not necessary for Lemmy to work, so ignore errors.
let community_ = community.clone();
let context_ = context.reset_request_count();
spawn_try_task(async move {
group.outbox.dereference(&community_, &context_).await?;
group.followers.dereference(&community_, &context_).await?;
group.outbox.dereference(&community_, &context_).await.ok();
group
.followers
.dereference(&community_, &context_)
.await
.ok();
if let Some(featured) = group.featured {
featured.dereference(&community_, &context_).await?;
featured.dereference(&community_, &context_).await.ok();
}
if let Some(moderators) = group.attributed_to {
moderators.dereference(&community_, &context_).await?;
moderators.dereference(&community_, &context_).await.ok();
}
Ok(())
});

@ -19,7 +19,12 @@ use activitypub_federation::{
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{local_site_opt_to_slur_regex, process_markdown_opt, proxy_image_link_opt_apub},
utils::{
get_url_blocklist,
local_site_opt_to_slur_regex,
process_markdown_opt,
proxy_image_link_opt_apub,
},
};
use lemmy_db_schema::{
newtypes::InstanceId,
@ -96,6 +101,7 @@ impl Object for ApubSite {
kind: ApplicationType::Application,
id: self.id().into(),
name: self.name.clone(),
preferred_username: data.domain().to_string(),
content: self.sidebar.as_ref().map(|d| markdown_to_html(d)),
source: self.sidebar.clone().map(Source::new),
summary: self.description.clone(),
@ -137,8 +143,9 @@ impl Object for ApubSite {
let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(context).await?;
let sidebar = read_from_string_or_source_opt(&apub.content, &None, &apub.source);
let sidebar = process_markdown_opt(&sidebar, slur_regex, context).await?;
let sidebar = process_markdown_opt(&sidebar, slur_regex, &url_blocklist, context).await?;
let icon = proxy_image_link_opt_apub(apub.icon.map(|i| i.url), context).await?;
let banner = proxy_image_link_opt_apub(apub.image.map(|i| i.url), context).await?;

@ -22,6 +22,7 @@ use lemmy_api_common::{
context::LemmyContext,
utils::{
generate_outbox_url,
get_url_blocklist,
local_site_opt_to_slur_regex,
process_markdown_opt,
proxy_image_link_opt_apub,
@ -152,8 +153,9 @@ impl Object for ApubPerson {
let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(context).await?;
let bio = read_from_string_or_source_opt(&person.summary, &None, &person.source);
let bio = process_markdown_opt(&bio, slur_regex, context).await?;
let bio = process_markdown_opt(&bio, slur_regex, &url_blocklist, context).await?;
let avatar = proxy_image_link_opt_apub(person.icon.map(|i| i.url), context).await?;
let banner = proxy_image_link_opt_apub(person.image.map(|i| i.url), context).await?;

@ -5,7 +5,7 @@ use crate::{
objects::{read_from_string_or_source_opt, verify_is_remote_object},
protocol::{
objects::{
page::{Attachment, AttributedTo, Page, PageType},
page::{Attachment, AttributedTo, Hashtag, HashtagType, Page, PageType},
LanguageTag,
},
ImageObject,
@ -26,7 +26,7 @@ use lemmy_api_common::{
context::LemmyContext,
request::fetch_link_metadata_opt,
utils::{
is_mod_or_admin,
get_url_blocklist,
local_site_opt_to_sensitive,
local_site_opt_to_slur_regex,
process_markdown_opt,
@ -43,6 +43,7 @@ use lemmy_db_schema::{
},
traits::Crud,
};
use lemmy_db_views_actor::structs::CommunityModeratorView;
use lemmy_utils::{
error::LemmyError,
utils::{markdown::markdown_to_html, slurs::check_slurs_opt, validation::check_url_scheme},
@ -124,6 +125,11 @@ impl Object for ApubPost {
})
.into_iter()
.collect();
let hashtag = Hashtag {
href: self.ap_id.clone().into(),
name: format!("#{}", &community.name),
kind: HashtagType::Hashtag,
};
let page = Page {
kind: PageType::Page,
@ -144,6 +150,7 @@ impl Object for ApubPost {
updated: self.updated,
audience: Some(community.actor_id.into()),
in_reply_to: None,
tag: vec![hashtag],
};
Ok(page)
}
@ -179,7 +186,8 @@ 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, community.id).await?;
CommunityModeratorView::is_community_moderator(&mut context.pool(), community.id, creator.id)
.await?;
}
let mut name = page
.name
@ -239,9 +247,10 @@ impl Object for ApubPost {
let thumbnail_url = proxy_image_link_opt_apub(thumbnail_url, context).await?;
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(context).await?;
let body = read_from_string_or_source_opt(&page.content, &page.media_type, &page.source);
let body = process_markdown_opt(&body, slur_regex, context).await?;
let body = process_markdown_opt(&body, slur_regex, &url_blocklist, context).await?;
let language_id =
LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?;
@ -302,8 +311,7 @@ mod tests {
use super::*;
use crate::{
objects::{
community::{tests::parse_lemmy_community, ApubCommunity},
instance::ApubSite,
community::tests::parse_lemmy_community,
person::{tests::parse_lemmy_person, ApubPerson},
},
protocol::tests::file_to_json_object,
@ -333,7 +341,10 @@ mod tests {
assert!(!post.featured_community);
assert_eq!(context.request_count(), 0);
cleanup(&context, person, site, community, post).await?;
Post::delete(&mut context.pool(), post.id).await?;
Person::delete(&mut context.pool(), person.id).await?;
Community::delete(&mut context.pool(), community.id).await?;
Site::delete(&mut context.pool(), site.id).await?;
Ok(())
}
@ -341,29 +352,19 @@ mod tests {
#[serial]
async fn test_convert_mastodon_post_title() -> LemmyResult<()> {
let context = LemmyContext::init_test_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/person.json")?;
let person = ApubPerson::from_json(json, &context).await?;
let json = file_to_json_object("assets/mastodon/objects/page.json")?;
let post = ApubPost::from_json(json, &context).await?;
assert_eq!(post.name, "Variable never resetting at refresh");
cleanup(&context, person, site, community, post).await?;
Ok(())
}
async fn cleanup(
context: &Data<LemmyContext>,
person: ApubPerson,
site: ApubSite,
community: ApubCommunity,
post: ApubPost,
) -> LemmyResult<()> {
Post::delete(&mut context.pool(), post.id).await?;
Person::delete(&mut context.pool(), person.id).await?;
Community::delete(&mut context.pool(), community.id).await?;
Site::delete(&mut context.pool(), site.id).await?;
Ok(())
}
}

@ -14,7 +14,7 @@ use activitypub_federation::{
use chrono::{DateTime, Utc};
use lemmy_api_common::{
context::LemmyContext,
utils::{check_person_block, local_site_opt_to_slur_regex, process_markdown},
utils::{check_person_block, get_url_blocklist, local_site_opt_to_slur_regex, process_markdown},
};
use lemmy_db_schema::{
source::{
@ -127,8 +127,9 @@ impl Object for ApubPrivateMessage {
let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
let url_blocklist = get_url_blocklist(context).await?;
let content = read_from_string_or_source(&note.content, &None, &note.source);
let content = process_markdown(&content, slur_regex, context).await?;
let content = process_markdown(&content, slur_regex, &url_blocklist, context).await?;
let form = PrivateMessageInsertForm {
creator_id: creator.id,

@ -38,7 +38,9 @@ pub struct BlockUser {
pub(crate) remove_data: Option<bool>,
/// block reason, written to mod log
pub(crate) summary: Option<String>,
/// TODO: deprecated
pub(crate) expires: Option<DateTime<Utc>>,
pub(crate) end_time: Option<DateTime<Utc>>,
}
#[async_trait::async_trait]

@ -19,8 +19,10 @@ pub struct Instance {
#[serde(rename = "type")]
pub(crate) kind: ApplicationType,
pub(crate) id: ObjectId<ApubSite>,
// site name
/// site name
pub(crate) name: String,
/// instance domain, necessary for mastodon authorized fetch
pub(crate) preferred_username: String,
pub(crate) inbox: Url,
/// mandatory field in activitypub, lemmy currently serves an empty outbox
pub(crate) outbox: Url,

@ -66,6 +66,8 @@ pub struct Page {
pub(crate) updated: Option<DateTime<Utc>>,
pub(crate) language: Option<LanguageTag>,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
#[serde(deserialize_with = "deserialize_skip_error", default)]
pub(crate) tag: Vec<Hashtag>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@ -140,6 +142,19 @@ pub(crate) struct AttributedToPeertube {
pub id: ObjectId<UserOrCommunity>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Hashtag {
pub(crate) href: Url,
pub(crate) name: String,
#[serde(rename = "type")]
pub(crate) kind: HashtagType,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum HashtagType {
Hashtag,
}
impl Page {
/// Only mods can change the post's locked status. So if it is changed from the default value,
/// it is a mod action and needs to be verified as such.

@ -33,9 +33,9 @@ impl CommentAggregates {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
aggregates::comment_aggregates::CommentAggregates,

@ -31,9 +31,9 @@ impl CommunityAggregates {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
aggregates::community_aggregates::CommunityAggregates,

@ -18,9 +18,9 @@ impl PersonAggregates {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
aggregates::person_aggregates::PersonAggregates,

@ -52,9 +52,9 @@ impl PostAggregates {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
aggregates::post_aggregates::PostAggregates,

@ -14,9 +14,9 @@ impl SiteAggregates {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
aggregates::site_aggregates::SiteAggregates,

@ -61,9 +61,9 @@ impl ReceivedActivity {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{source::activity::ActorType, utils::build_db_pool_for_tests};

@ -385,9 +385,9 @@ async fn convert_read_languages(
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{
@ -523,10 +523,6 @@ mod tests {
let pool = &mut pool.into();
let (site, instance) = create_test_site(pool).await;
let mut test_langs = test_langs1(pool).await;
SiteLanguage::update(pool, test_langs.clone(), &site)
.await
.unwrap();
let person_form = PersonInsertForm::builder()
.name("my test person".to_string())
@ -539,14 +535,13 @@ mod tests {
.password_encrypted("my_pw".to_string())
.build();
let local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
let local_user = LocalUser::create(pool, &local_user_form, vec![])
.await
.unwrap();
let local_user_langs1 = LocalUserLanguage::read(pool, local_user.id).await.unwrap();
// new user should be initialized with site languages and undetermined
//test_langs.push(UNDETERMINED_ID);
//test_langs.sort();
test_langs.insert(0, UNDETERMINED_ID);
assert_eq!(test_langs, local_user_langs1);
// new user should be initialized with all languages
assert_eq!(0, local_user_langs1.len());
// update user languages
let test_langs2 = test_langs2(pool).await;
@ -655,7 +650,9 @@ mod tests {
.person_id(person.id)
.password_encrypted("my_pw".to_string())
.build();
let local_user = LocalUser::create(pool, &local_user_form).await.unwrap();
let local_user = LocalUser::create(pool, &local_user_form, vec![])
.await
.unwrap();
LocalUserLanguage::update(pool, test_langs2, local_user.id)
.await
.unwrap();

@ -48,9 +48,9 @@ impl CaptchaAnswer {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::captcha_answer::{CaptchaAnswer, CaptchaAnswerForm, CheckCaptchaAnswer},

@ -241,9 +241,9 @@ impl Saveable for CommentSaved {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
newtypes::LanguageId,

@ -1,6 +1,6 @@
use crate::{
newtypes::{CommentId, CommentReplyId, PersonId},
schema::comment_reply::dsl::{comment_id, comment_reply, read, recipient_id},
schema::comment_reply,
source::comment_reply::{CommentReply, CommentReplyInsertForm, CommentReplyUpdateForm},
traits::Crud,
utils::{get_conn, DbPool},
@ -22,9 +22,9 @@ impl Crud for CommentReply {
// since the return here isnt utilized, we dont need to do an update
// but get_result doesnt return the existing row here
insert_into(comment_reply)
insert_into(comment_reply::table)
.values(comment_reply_form)
.on_conflict((recipient_id, comment_id))
.on_conflict((comment_reply::recipient_id, comment_reply::comment_id))
.do_update()
.set(comment_reply_form)
.get_result::<Self>(conn)
@ -37,7 +37,7 @@ impl Crud for CommentReply {
comment_reply_form: &Self::UpdateForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(comment_reply.find(comment_reply_id))
diesel::update(comment_reply::table.find(comment_reply_id))
.set(comment_reply_form)
.get_result::<Self>(conn)
.await
@ -51,11 +51,11 @@ impl CommentReply {
) -> Result<Vec<CommentReply>, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(
comment_reply
.filter(recipient_id.eq(for_recipient_id))
.filter(read.eq(false)),
comment_reply::table
.filter(comment_reply::recipient_id.eq(for_recipient_id))
.filter(comment_reply::read.eq(false)),
)
.set(read.eq(true))
.set(comment_reply::read.eq(true))
.get_results::<Self>(conn)
.await
}
@ -65,17 +65,30 @@ impl CommentReply {
for_comment_id: CommentId,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
comment_reply
.filter(comment_id.eq(for_comment_id))
comment_reply::table
.filter(comment_reply::comment_id.eq(for_comment_id))
.first::<Self>(conn)
.await
}
pub async fn read_by_comment_and_person(
pool: &mut DbPool<'_>,
for_comment_id: CommentId,
for_recipient_id: PersonId,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
comment_reply::table
.filter(comment_reply::comment_id.eq(for_comment_id))
.filter(comment_reply::recipient_id.eq(for_recipient_id))
.first::<Self>(conn)
.await
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::{

@ -381,9 +381,9 @@ impl ApubActor for Community {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::{

@ -48,9 +48,9 @@ impl FederationAllowList {
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::{federation_allowlist::FederationAllowList, instance::Instance},

@ -41,9 +41,9 @@ impl Language {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{source::language::Language, utils::build_db_pool_for_tests};
use pretty_assertions::assert_eq;

@ -0,0 +1,49 @@
use crate::{
schema::local_site_url_blocklist,
source::local_site_url_blocklist::{LocalSiteUrlBlocklist, LocalSiteUrlBlocklistForm},
utils::{get_conn, DbPool},
};
use diesel::{dsl::insert_into, result::Error};
use diesel_async::{AsyncPgConnection, RunQueryDsl};
impl LocalSiteUrlBlocklist {
pub async fn replace(pool: &mut DbPool<'_>, url_blocklist: Vec<String>) -> Result<(), Error> {
let conn = &mut get_conn(pool).await?;
conn
.build_transaction()
.run(|conn| {
Box::pin(async move {
use crate::schema::local_site_url_blocklist::dsl::local_site_url_blocklist;
Self::clear(conn).await?;
let forms = url_blocklist
.into_iter()
.map(|url| LocalSiteUrlBlocklistForm { url, updated: None })
.collect::<Vec<_>>();
insert_into(local_site_url_blocklist)
.values(forms)
.execute(conn)
.await?;
Ok(())
}) as _
})
.await
}
async fn clear(conn: &mut AsyncPgConnection) -> Result<usize, Error> {
diesel::delete(local_site_url_blocklist::table)
.execute(conn)
.await
}
pub async fn get_all(pool: &mut DbPool<'_>) -> Result<Vec<Self>, Error> {
let conn = &mut get_conn(pool).await?;
local_site_url_blocklist::table
.get_results::<Self>(conn)
.await
}
}

@ -1,11 +1,11 @@
use crate::{
newtypes::{DbUrl, LocalUserId, PersonId},
newtypes::{DbUrl, LanguageId, LocalUserId, PersonId},
schema::{local_user, person, registration_application},
source::{
actor_language::{LocalUserLanguage, SiteLanguage},
actor_language::LocalUserLanguage,
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeInsertForm},
},
traits::Crud,
utils::{
functions::{coalesce, lower},
get_conn,
@ -24,6 +24,52 @@ use diesel::{
use diesel_async::RunQueryDsl;
impl LocalUser {
pub async fn create(
pool: &mut DbPool<'_>,
form: &LocalUserInsertForm,
languages: Vec<LanguageId>,
) -> Result<LocalUser, Error> {
let conn = &mut get_conn(pool).await?;
let mut form_with_encrypted_password = form.clone();
let password_hash =
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
form_with_encrypted_password.password_encrypted = password_hash;
let local_user_ = insert_into(local_user::table)
.values(form_with_encrypted_password)
.get_result::<Self>(conn)
.await?;
LocalUserLanguage::update(pool, languages, local_user_.id).await?;
// Create their vote_display_modes
let vote_display_mode_form = LocalUserVoteDisplayModeInsertForm::builder()
.local_user_id(local_user_.id)
.build();
LocalUserVoteDisplayMode::create(pool, &vote_display_mode_form).await?;
Ok(local_user_)
}
pub async fn update(
pool: &mut DbPool<'_>,
local_user_id: LocalUserId,
form: &LocalUserUpdateForm,
) -> Result<LocalUser, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(local_user::table.find(local_user_id))
.set(form)
.get_result::<Self>(conn)
.await
}
pub async fn delete(pool: &mut DbPool<'_>, id: LocalUserId) -> Result<usize, Error> {
let conn = &mut *get_conn(pool).await?;
diesel::delete(local_user::table.find(id))
.execute(conn)
.await
}
pub async fn update_password(
pool: &mut DbPool<'_>,
local_user_id: LocalUserId,
@ -182,46 +228,3 @@ pub struct UserBackupLists {
pub blocked_users: Vec<DbUrl>,
pub blocked_instances: Vec<String>,
}
#[async_trait]
impl Crud for LocalUser {
type InsertForm = LocalUserInsertForm;
type UpdateForm = LocalUserUpdateForm;
type IdType = LocalUserId;
async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
let mut form_with_encrypted_password = form.clone();
let password_hash =
hash(&form.password_encrypted, DEFAULT_COST).expect("Couldn't hash password");
form_with_encrypted_password.password_encrypted = password_hash;
let local_user_ = insert_into(local_user::table)
.values(form_with_encrypted_password)
.get_result::<Self>(conn)
.await?;
let site_languages = SiteLanguage::read_local_raw(pool).await;
if let Ok(langs) = site_languages {
// if site exists, init user with site languages
LocalUserLanguage::update(pool, langs, local_user_.id).await?;
} else {
// otherwise, init with all languages (this only happens during tests and
// for first admin user, which is created before site)
LocalUserLanguage::update(pool, vec![], local_user_.id).await?;
}
Ok(local_user_)
}
async fn update(
pool: &mut DbPool<'_>,
local_user_id: LocalUserId,
form: &Self::UpdateForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(local_user::table.find(local_user_id))
.set(form)
.get_result::<Self>(conn)
.await
}
}

@ -0,0 +1,57 @@
use crate::{
newtypes::LocalUserId,
schema::local_user_vote_display_mode,
source::local_user_vote_display_mode::{
LocalUserVoteDisplayMode,
LocalUserVoteDisplayModeInsertForm,
LocalUserVoteDisplayModeUpdateForm,
},
utils::{get_conn, DbPool},
};
use diesel::{dsl::insert_into, result::Error, QueryDsl};
use diesel_async::RunQueryDsl;
impl LocalUserVoteDisplayMode {
pub async fn read(pool: &mut DbPool<'_>) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
local_user_vote_display_mode::table
.first::<Self>(conn)
.await
}
pub async fn create(
pool: &mut DbPool<'_>,
form: &LocalUserVoteDisplayModeInsertForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
insert_into(local_user_vote_display_mode::table)
.values(form)
.get_result::<Self>(conn)
.await
}
pub async fn update(
pool: &mut DbPool<'_>,
local_user_id: LocalUserId,
form: &LocalUserVoteDisplayModeUpdateForm,
) -> Result<(), Error> {
// avoid error "There are no changes to save. This query cannot be built"
if form.is_empty() {
return Ok(());
}
let conn = &mut get_conn(pool).await?;
diesel::update(local_user_vote_display_mode::table.find(local_user_id))
.set(form)
.get_result::<Self>(conn)
.await?;
Ok(())
}
}
impl LocalUserVoteDisplayModeUpdateForm {
fn is_empty(&self) -> bool {
self.score.is_none()
&& self.upvotes.is_none()
&& self.downvotes.is_none()
&& self.upvote_percentage.is_none()
}
}

@ -17,7 +17,9 @@ pub mod instance_block;
pub mod language;
pub mod local_site;
pub mod local_site_rate_limit;
pub mod local_site_url_blocklist;
pub mod local_user;
pub mod local_user_vote_display_mode;
pub mod login_token;
pub mod moderator;
pub mod password_reset_request;

@ -465,9 +465,9 @@ impl Crud for AdminPurgeComment {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::{

@ -81,9 +81,9 @@ impl PasswordResetRequest {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::{
@ -121,7 +121,9 @@ mod tests {
.password_encrypted("pass".to_string())
.build();
let inserted_local_user = LocalUser::create(pool, &new_local_user).await.unwrap();
let inserted_local_user = LocalUser::create(pool, &new_local_user, vec![])
.await
.unwrap();
let token = "nope";

@ -212,9 +212,9 @@ impl PersonFollower {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::{

@ -1,6 +1,6 @@
use crate::{
newtypes::{CommentId, PersonId, PersonMentionId},
schema::person_mention::dsl::{comment_id, person_mention, read, recipient_id},
schema::person_mention,
source::person_mention::{PersonMention, PersonMentionInsertForm, PersonMentionUpdateForm},
traits::Crud,
utils::{get_conn, DbPool},
@ -21,9 +21,9 @@ impl Crud for PersonMention {
let conn = &mut get_conn(pool).await?;
// since the return here isnt utilized, we dont need to do an update
// but get_result doesnt return the existing row here
insert_into(person_mention)
insert_into(person_mention::table)
.values(person_mention_form)
.on_conflict((recipient_id, comment_id))
.on_conflict((person_mention::recipient_id, person_mention::comment_id))
.do_update()
.set(person_mention_form)
.get_result::<Self>(conn)
@ -36,7 +36,7 @@ impl Crud for PersonMention {
person_mention_form: &Self::UpdateForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(person_mention.find(person_mention_id))
diesel::update(person_mention::table.find(person_mention_id))
.set(person_mention_form)
.get_result::<Self>(conn)
.await
@ -50,11 +50,11 @@ impl PersonMention {
) -> Result<Vec<PersonMention>, Error> {
let conn = &mut get_conn(pool).await?;
diesel::update(
person_mention
.filter(recipient_id.eq(for_recipient_id))
.filter(read.eq(false)),
person_mention::table
.filter(person_mention::recipient_id.eq(for_recipient_id))
.filter(person_mention::read.eq(false)),
)
.set(read.eq(true))
.set(person_mention::read.eq(true))
.get_results::<Self>(conn)
.await
}
@ -65,18 +65,18 @@ impl PersonMention {
for_recipient_id: PersonId,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
person_mention
.filter(comment_id.eq(for_comment_id))
.filter(recipient_id.eq(for_recipient_id))
person_mention::table
.filter(person_mention::comment_id.eq(for_comment_id))
.filter(person_mention::recipient_id.eq(for_recipient_id))
.first::<Self>(conn)
.await
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::{

@ -27,7 +27,7 @@ use crate::{
},
};
use ::url::Url;
use chrono::{Duration, Utc};
use chrono::Utc;
use diesel::{dsl::insert_into, result::Error, ExpressionMethods, QueryDsl, TextExpressionMethods};
use diesel_async::RunQueryDsl;
use std::collections::HashSet;
@ -104,7 +104,9 @@ impl Post {
.filter(post::local.eq(true))
.filter(post::deleted.eq(false))
.filter(post::removed.eq(false))
.filter(post::published.ge(Utc::now().naive_utc() - Duration::days(SITEMAP_DAYS)))
.filter(
post::published.ge(Utc::now().naive_utc() - SITEMAP_DAYS.expect("TimeDelta out of bounds")),
)
.order(post::published.desc())
.limit(SITEMAP_LIMIT)
.load::<(DbUrl, chrono::DateTime<Utc>)>(conn)
@ -360,9 +362,9 @@ impl PostHide {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::{

@ -80,9 +80,9 @@ impl Reportable for PostReport {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use crate::{

@ -74,9 +74,9 @@ impl PrivateMessage {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
source::{

@ -342,7 +342,7 @@ diesel::table! {
diesel::table! {
local_image (pictrs_alias) {
local_user_id -> Int4,
local_user_id -> Nullable<Int4>,
pictrs_alias -> Text,
pictrs_delete_token -> Text,
published -> Timestamptz,
@ -409,6 +409,15 @@ diesel::table! {
}
}
diesel::table! {
local_site_url_blocklist (id) {
id -> Int4,
url -> Text,
published -> Timestamptz,
updated -> Nullable<Timestamptz>,
}
}
diesel::table! {
use diesel::sql_types::*;
use super::sql_types::SortTypeEnum;
@ -454,6 +463,16 @@ diesel::table! {
}
}
diesel::table! {
local_user_vote_display_mode (local_user_id) {
local_user_id -> Int4,
score -> Bool,
upvotes -> Bool,
downvotes -> Bool,
upvote_percentage -> Bool,
}
}
diesel::table! {
login_token (token) {
token -> Text,
@ -961,6 +980,7 @@ diesel::joinable!(local_site_rate_limit -> local_site (local_site_id));
diesel::joinable!(local_user -> person (person_id));
diesel::joinable!(local_user_language -> language (language_id));
diesel::joinable!(local_user_language -> local_user (local_user_id));
diesel::joinable!(local_user_vote_display_mode -> local_user (local_user_id));
diesel::joinable!(login_token -> local_user (user_id));
diesel::joinable!(mod_add_community -> community (community_id));
diesel::joinable!(mod_ban_from_community -> community (community_id));
@ -1041,8 +1061,10 @@ diesel::allow_tables_to_appear_in_same_query!(
local_image,
local_site,
local_site_rate_limit,
local_site_url_blocklist,
local_user,
local_user_language,
local_user_vote_display_mode,
login_token,
mod_add,
mod_add_community,

@ -2,11 +2,9 @@ use crate::newtypes::{DbUrl, LocalUserId};
#[cfg(feature = "full")]
use crate::schema::{local_image, remote_image};
use chrono::{DateTime, Utc};
use serde_with::skip_serializing_none;
use std::fmt::Debug;
use typed_builder::TypedBuilder;
#[skip_serializing_none]
#[derive(PartialEq, Eq, Debug, Clone)]
#[cfg_attr(feature = "full", derive(Queryable, Associations))]
#[cfg_attr(feature = "full", diesel(table_name = local_image))]
@ -15,7 +13,7 @@ use typed_builder::TypedBuilder;
diesel(belongs_to(crate::source::local_user::LocalUser))
)]
pub struct LocalImage {
pub local_user_id: LocalUserId,
pub local_user_id: Option<LocalUserId>,
pub pictrs_alias: String,
pub pictrs_delete_token: String,
pub published: DateTime<Utc>,
@ -25,14 +23,13 @@ pub struct LocalImage {
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = local_image))]
pub struct LocalImageForm {
pub local_user_id: LocalUserId,
pub local_user_id: Option<LocalUserId>,
pub pictrs_alias: String,
pub pictrs_delete_token: String,
}
/// Stores all images which are hosted on remote domains. When attempting to proxy an image, it
/// is checked against this table to avoid Lemmy being used as a general purpose proxy.
#[skip_serializing_none]
#[derive(PartialEq, Eq, Debug, Clone)]
#[cfg_attr(feature = "full", derive(Queryable, Identifiable))]
#[cfg_attr(feature = "full", diesel(table_name = remote_image))]

@ -0,0 +1,28 @@
#[cfg(feature = "full")]
use crate::schema::local_site_url_blocklist;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
#[skip_serializing_none]
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))]
#[cfg_attr(feature = "full", diesel(table_name = local_site_url_blocklist))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
pub struct LocalSiteUrlBlocklist {
pub id: i32,
pub url: String,
pub published: DateTime<Utc>,
pub updated: Option<DateTime<Utc>>,
}
#[derive(Default, Clone)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = local_site_url_blocklist))]
pub struct LocalSiteUrlBlocklistForm {
pub url: String,
pub updated: Option<DateTime<Utc>>,
}

@ -36,6 +36,7 @@ pub struct LocalUser {
pub show_avatars: bool,
pub send_notifications_to_email: bool,
/// Whether to show comment / post scores.
// TODO now that there is a vote_display_mode, this can be gotten rid of in future releases.
pub show_scores: bool,
/// Whether to show bot accounts.
pub show_bot_accounts: bool,
@ -55,6 +56,7 @@ pub struct LocalUser {
pub infinite_scroll_enabled: bool,
/// Whether the person is an admin.
pub admin: bool,
/// A post-view mode that changes how multiple post listings look.
pub post_listing_mode: PostListingMode,
pub totp_2fa_enabled: bool,
/// Whether to allow keyboard navigation (for browsing and interacting with posts and comments).

@ -0,0 +1,51 @@
use crate::newtypes::LocalUserId;
#[cfg(feature = "full")]
use crate::schema::local_user_vote_display_mode;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
use typed_builder::TypedBuilder;
#[skip_serializing_none]
#[derive(PartialEq, Eq, Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))]
#[cfg_attr(feature = "full", diesel(table_name = local_user_vote_display_mode))]
#[cfg_attr(feature = "full", diesel(primary_key(local_user_id)))]
#[cfg_attr(
feature = "full",
diesel(belongs_to(crate::source::local_site::LocalUser))
)]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
/// The vote display settings for your user.
pub struct LocalUserVoteDisplayMode {
pub local_user_id: LocalUserId,
pub score: bool,
pub upvotes: bool,
pub downvotes: bool,
pub upvote_percentage: bool,
}
#[derive(Clone, TypedBuilder)]
#[builder(field_defaults(default))]
#[cfg_attr(feature = "full", derive(Insertable))]
#[cfg_attr(feature = "full", diesel(table_name = local_user_vote_display_mode))]
pub struct LocalUserVoteDisplayModeInsertForm {
#[builder(!default)]
pub local_user_id: LocalUserId,
pub score: Option<bool>,
pub upvotes: Option<bool>,
pub downvotes: Option<bool>,
pub upvote_percentage: Option<bool>,
}
#[derive(Clone, Default)]
#[cfg_attr(feature = "full", derive(AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = local_user_vote_display_mode))]
pub struct LocalUserVoteDisplayModeUpdateForm {
pub score: Option<bool>,
pub upvotes: Option<bool>,
pub downvotes: Option<bool>,
pub upvote_percentage: Option<bool>,
}

@ -22,7 +22,9 @@ pub mod instance_block;
pub mod language;
pub mod local_site;
pub mod local_site_rate_limit;
pub mod local_site_url_blocklist;
pub mod local_user;
pub mod local_user_vote_display_mode;
pub mod login_token;
pub mod moderator;
pub mod password_reset_request;
@ -44,6 +46,6 @@ pub mod tagline;
/// value is not sent by Lemmy. Necessary for crates which rely on Rust API such as lemmy-stats-crawler.
fn placeholder_apub_url() -> DbUrl {
DbUrl(Box::new(
Url::parse("http://example.com").expect("parse placeholer url"),
Url::parse("http://example.com").expect("parse placeholder url"),
))
}

@ -34,7 +34,9 @@ pub struct Site {
pub last_refreshed_at: DateTime<Utc>,
/// The site inbox
pub inbox_url: DbUrl,
#[serde(skip)]
pub private_key: Option<String>,
#[serde(skip)]
pub public_key: String,
pub instance_id: InstanceId,
/// If present, nsfw content is visible by default. Should be displayed by frontends/clients

@ -6,7 +6,7 @@ use crate::{
SortType,
};
use anyhow::Context;
use chrono::{DateTime, Utc};
use chrono::{DateTime, TimeDelta, Utc};
use deadpool::Runtime;
use diesel::{
helper_types::AsExprOf,
@ -51,7 +51,7 @@ use url::Url;
const FETCH_LIMIT_DEFAULT: i64 = 10;
pub const FETCH_LIMIT_MAX: i64 = 50;
pub const SITEMAP_LIMIT: i64 = 50000;
pub const SITEMAP_DAYS: i64 = 31;
pub const SITEMAP_DAYS: Option<TimeDelta> = TimeDelta::try_days(31);
pub const RANK_DEFAULT: f64 = 0.0001;
pub type ActualDbPool = Pool<AsyncPgConnection>;
@ -536,9 +536,9 @@ impl<RF, LF> Queries<RF, LF> {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*;
use pretty_assertions::assert_eq;

@ -18,10 +18,14 @@ use lemmy_db_schema::{
comment_aggregates,
comment_like,
comment_report,
comment_saved,
community,
community_follower,
community_moderator,
community_person_ban,
local_user,
person,
person_block,
post,
},
utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
@ -64,6 +68,45 @@ fn queries<'a>() -> Queries<
),
),
)
.left_join(
aliases::community_moderator1.on(
community::id
.eq(aliases::community_moderator1.field(community_moderator::community_id))
.and(
aliases::community_moderator1
.field(community_moderator::person_id)
.eq(comment::creator_id),
),
),
)
.left_join(
local_user::table.on(
comment::creator_id
.eq(local_user::person_id)
.and(local_user::admin.eq(true)),
),
)
.left_join(
person_block::table.on(
comment::creator_id
.eq(person_block::target_id)
.and(person_block::person_id.eq(my_person_id)),
),
)
.left_join(
community_follower::table.on(
post::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(my_person_id)),
),
)
.left_join(
comment_saved::table.on(
comment::id
.eq(comment_saved::comment_id)
.and(comment_saved::person_id.eq(my_person_id)),
),
)
.select((
comment_report::all_columns,
comment::all_columns,
@ -73,6 +116,14 @@ fn queries<'a>() -> Queries<
aliases::person1.fields(person::all_columns),
comment_aggregates::all_columns,
community_person_ban::community_id.nullable().is_not_null(),
aliases::community_moderator1
.field(community_moderator::community_id)
.nullable()
.is_not_null(),
local_user::admin.nullable().is_not_null(),
person_block::target_id.nullable().is_not_null(),
community_follower::pending.nullable(),
comment_saved::published.nullable().is_not_null(),
comment_like::score.nullable(),
aliases::person2.fields(person::all_columns).nullable(),
))
@ -207,9 +258,9 @@ impl CommentReportQuery {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
comment_report_view::{CommentReportQuery, CommentReportView},
@ -223,12 +274,14 @@ mod tests {
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
local_user_vote_display_mode::LocalUserVoteDisplayMode,
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm},
},
traits::{Crud, Joinable, Reportable},
utils::{build_db_pool_for_tests, RANK_DEFAULT},
CommunityVisibility,
SubscribedType,
};
use pretty_assertions::assert_eq;
use serial_test::serial;
@ -255,9 +308,12 @@ mod tests {
.person_id(inserted_timmy.id)
.password_encrypted("123".to_string())
.build();
let timmy_local_user = LocalUser::create(pool, &new_local_user).await.unwrap();
let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![])
.await
.unwrap();
let timmy_view = LocalUserView {
local_user: timmy_local_user,
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
person: inserted_timmy.clone(),
counts: Default::default(),
};
@ -350,6 +406,11 @@ mod tests {
comment_report: inserted_jessica_report.clone(),
comment: inserted_comment.clone(),
post: inserted_post,
creator_is_moderator: true,
creator_is_admin: false,
creator_blocked: false,
subscribed: SubscribedType::NotSubscribed,
saved: false,
community: Community {
id: inserted_community.id,
name: inserted_community.name,

@ -53,6 +53,16 @@ fn queries<'a>() -> Queries<
),
);
let is_local_user_banned_from_community = |person_id| {
exists(
community_person_ban::table.filter(
community::id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(person_id)),
),
)
};
let is_saved = |person_id| {
comment_saved::table
.filter(
@ -113,6 +123,14 @@ fn queries<'a>() -> Queries<
);
let all_joins = move |query: comment::BoxedQuery<'a, Pg>, my_person_id: Option<PersonId>| {
let is_local_user_banned_from_community_selection: Box<
dyn BoxableExpression<_, Pg, SqlType = sql_types::Bool>,
> = if let Some(person_id) = my_person_id {
Box::new(is_local_user_banned_from_community(person_id))
} else {
Box::new(false.into_sql::<sql_types::Bool>())
};
let score_selection: Box<
dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::SmallInt>>,
> = if let Some(person_id) = my_person_id {
@ -156,6 +174,7 @@ fn queries<'a>() -> Queries<
community::all_columns,
comment_aggregates::all_columns,
is_creator_banned_from_community,
is_local_user_banned_from_community_selection,
creator_is_moderator,
creator_is_admin,
subscribed_type_selection,
@ -407,9 +426,9 @@ impl<'a> CommentQuery<'a> {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
comment_view::{CommentQuery, CommentSortType, CommentView, DbPool},
@ -436,20 +455,24 @@ mod tests {
CommunityInsertForm,
CommunityModerator,
CommunityModeratorForm,
CommunityPersonBan,
CommunityPersonBanForm,
CommunityUpdateForm,
},
instance::Instance,
language::Language,
local_user::{LocalUser, LocalUserInsertForm},
local_user_vote_display_mode::LocalUserVoteDisplayMode,
person::{Person, PersonInsertForm},
person_block::{PersonBlock, PersonBlockForm},
post::{Post, PostInsertForm},
},
traits::{Blockable, Crud, Joinable, Likeable, Saveable},
traits::{Bannable, Blockable, Crud, Joinable, Likeable, Saveable},
utils::{build_db_pool_for_tests, RANK_DEFAULT},
CommunityVisibility,
SubscribedType,
};
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq;
use serial_test::serial;
@ -480,7 +503,7 @@ mod tests {
.admin(Some(true))
.password_encrypted(String::new())
.build();
let inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form)
let inserted_timmy_local_user = LocalUser::create(pool, &timmy_local_user_form, vec![])
.await
.unwrap();
@ -614,6 +637,7 @@ mod tests {
let timmy_local_user_view = LocalUserView {
local_user: inserted_timmy_local_user.clone(),
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
person: inserted_timmy_person.clone(),
counts: Default::default(),
};
@ -631,12 +655,12 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_crud() {
async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
let expected_comment_view_no_person = expected_comment_view(&data, pool).await;
let expected_comment_view_no_person = expected_comment_view(&data, pool).await?;
let mut expected_comment_view_with_person = expected_comment_view_no_person.clone();
expected_comment_view_with_person.my_vote = Some(1);
@ -647,8 +671,7 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_eq!(
expected_comment_view_no_person,
@ -662,8 +685,7 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_eq!(
expected_comment_view_with_person,
@ -678,8 +700,7 @@ mod tests {
data.inserted_comment_1.id,
Some(data.timmy_local_user_view.person.id),
)
.await
.unwrap();
.await?;
// Make sure block set the creator blocked
assert!(read_comment_from_blocked_person.creator_blocked);
@ -690,8 +711,7 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_eq!(
expected_comment_view_with_person,
@ -706,17 +726,16 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert!(read_disliked_comment_views.is_empty());
cleanup(data, pool).await;
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn test_comment_tree() {
async fn test_comment_tree() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
@ -728,8 +747,7 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
let child_path = data.inserted_comment_1.path.clone();
let read_comment_views_child_path = CommentQuery {
@ -738,8 +756,7 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
// Make sure the comment parent-limited fetch is correct
assert_length!(6, read_comment_views_top_path);
@ -759,12 +776,11 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
// Make sure a depth limited one only has the top comment
assert_eq!(
expected_comment_view(&data, pool).await,
expected_comment_view(&data, pool).await?,
read_comment_views_top_max_depth[0]
);
assert_length!(1, read_comment_views_top_max_depth);
@ -778,8 +794,7 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
// Make sure a depth limited one, and given child comment 1, has 3
assert!(read_comment_views_parent_max_depth[2]
@ -788,12 +803,12 @@ mod tests {
.eq("Comment 3"));
assert_length!(3, read_comment_views_parent_max_depth);
cleanup(data, pool).await;
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn test_languages() {
async fn test_languages() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
@ -805,29 +820,25 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_length!(5, all_languages);
// change user lang to finnish, should only show one post in finnish and one undetermined
let finnish_id = Language::read_id_from_code(pool, Some("fi"))
.await
.unwrap()
.await?
.unwrap();
LocalUserLanguage::update(
pool,
vec![finnish_id],
data.timmy_local_user_view.local_user.id,
)
.await
.unwrap();
.await?;
let finnish_comments = CommentQuery {
local_user: (Some(&data.timmy_local_user_view)),
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_length!(2, finnish_comments);
let finnish_comment = finnish_comments
.iter()
@ -844,23 +855,21 @@ mod tests {
vec![UNDETERMINED_ID],
data.timmy_local_user_view.local_user.id,
)
.await
.unwrap();
.await?;
let undetermined_comment = CommentQuery {
local_user: (Some(&data.timmy_local_user_view)),
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_length!(1, undetermined_comment);
cleanup(data, pool).await;
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn test_distinguished_first() {
async fn test_distinguished_first() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
@ -869,26 +878,23 @@ mod tests {
distinguished: Some(true),
..Default::default()
};
Comment::update(pool, data.inserted_comment_2.id, &form)
.await
.unwrap();
Comment::update(pool, data.inserted_comment_2.id, &form).await?;
let comments = CommentQuery {
post_id: Some(data.inserted_comment_2.post_id),
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_eq!(comments[0].comment.id, data.inserted_comment_2.id);
assert!(comments[0].comment.distinguished);
cleanup(data, pool).await;
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn test_creator_is_moderator() {
async fn test_creator_is_moderator() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
@ -900,7 +906,7 @@ mod tests {
community_id,
person_id,
};
CommunityModerator::join(pool, &form).await.unwrap();
CommunityModerator::join(pool, &form).await?;
// Make sure that they come back as a mod in the list
let comments = CommentQuery {
@ -908,19 +914,18 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_eq!(comments[1].creator.name, "sara");
assert!(comments[1].creator_is_moderator);
assert!(!comments[0].creator_is_moderator);
cleanup(data, pool).await;
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn test_creator_is_admin() {
async fn test_creator_is_admin() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
@ -930,8 +935,7 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
// Timmy is an admin, and make sure that field is true
assert_eq!(comments[0].creator.name, "timmy");
@ -941,12 +945,12 @@ mod tests {
assert_eq!(comments[1].creator.name, "sara");
assert!(!comments[1].creator_is_admin);
cleanup(data, pool).await;
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn test_saved_order() {
async fn test_saved_order() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
@ -956,17 +960,13 @@ mod tests {
person_id: data.timmy_local_user_view.person.id,
comment_id: data.inserted_comment_0.id,
};
CommentSaved::save(pool, &save_comment_0_form)
.await
.unwrap();
CommentSaved::save(pool, &save_comment_0_form).await?;
let save_comment_2_form = CommentSavedForm {
person_id: data.timmy_local_user_view.person.id,
comment_id: data.inserted_comment_2.id,
};
CommentSaved::save(pool, &save_comment_2_form)
.await
.unwrap();
CommentSaved::save(pool, &save_comment_2_form).await?;
// Fetch the saved comments
let comments = CommentQuery {
@ -975,8 +975,7 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
// There should only be two comments
assert_eq!(2, comments.len());
@ -987,47 +986,33 @@ mod tests {
// The second comment, should be the first one saved
assert_eq!(comments[1].comment.id, data.inserted_comment_0.id);
cleanup(data, pool).await;
cleanup(data, pool).await
}
async fn cleanup(data: Data, pool: &mut DbPool<'_>) {
async fn cleanup(data: Data, pool: &mut DbPool<'_>) -> LemmyResult<()> {
CommentLike::remove(
pool,
data.timmy_local_user_view.person.id,
data.inserted_comment_0.id,
)
.await
.unwrap();
Comment::delete(pool, data.inserted_comment_0.id)
.await
.unwrap();
Comment::delete(pool, data.inserted_comment_1.id)
.await
.unwrap();
Post::delete(pool, data.inserted_post.id).await.unwrap();
Community::delete(pool, data.inserted_community.id)
.await
.unwrap();
Person::delete(pool, data.timmy_local_user_view.person.id)
.await
.unwrap();
LocalUser::delete(pool, data.timmy_local_user_view.local_user.id)
.await
.unwrap();
Person::delete(pool, data.inserted_sara_person.id)
.await
.unwrap();
Instance::delete(pool, data.inserted_instance.id)
.await
.unwrap();
.await?;
Comment::delete(pool, data.inserted_comment_0.id).await?;
Comment::delete(pool, data.inserted_comment_1.id).await?;
Post::delete(pool, data.inserted_post.id).await?;
Community::delete(pool, data.inserted_community.id).await?;
Person::delete(pool, data.timmy_local_user_view.person.id).await?;
LocalUser::delete(pool, data.timmy_local_user_view.local_user.id).await?;
Person::delete(pool, data.inserted_sara_person.id).await?;
Instance::delete(pool, data.inserted_instance.id).await?;
Ok(())
}
async fn expected_comment_view(data: &Data, pool: &mut DbPool<'_>) -> CommentView {
let agg = CommentAggregates::read(pool, data.inserted_comment_0.id)
.await
.unwrap();
CommentView {
async fn expected_comment_view(data: &Data, pool: &mut DbPool<'_>) -> LemmyResult<CommentView> {
let agg = CommentAggregates::read(pool, data.inserted_comment_0.id).await?;
Ok(CommentView {
creator_banned_from_community: false,
banned_from_community: false,
creator_is_moderator: false,
creator_is_admin: true,
my_vote: None,
@ -1134,12 +1119,12 @@ mod tests {
hot_rank: RANK_DEFAULT,
controversy_rank: 0.0,
},
}
})
}
#[tokio::test]
#[serial]
async fn local_only_instance() {
async fn local_only_instance() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
@ -1152,15 +1137,13 @@ mod tests {
..Default::default()
},
)
.await
.unwrap();
.await?;
let unauthenticated_query = CommentQuery {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_eq!(0, unauthenticated_query.len());
let authenticated_query = CommentQuery {
@ -1168,8 +1151,7 @@ mod tests {
..Default::default()
}
.list(pool)
.await
.unwrap();
.await?;
assert_eq!(5, authenticated_query.len());
let unauthenticated_comment = CommentView::read(pool, data.inserted_comment_0.id, None).await;
@ -1183,6 +1165,67 @@ mod tests {
.await;
assert!(authenticated_comment.is_ok());
cleanup(data, pool).await;
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn comment_listing_local_user_banned_from_community() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
// Test that comment view shows if local user is blocked from community
let banned_from_comm_person = PersonInsertForm::test_form(data.inserted_instance.id, "jill");
let inserted_banned_from_comm_person = Person::create(pool, &banned_from_comm_person).await?;
let inserted_banned_from_comm_local_user = LocalUser::create(
pool,
&LocalUserInsertForm::test_form(inserted_banned_from_comm_person.id),
vec![],
)
.await?;
CommunityPersonBan::ban(
pool,
&CommunityPersonBanForm {
community_id: data.inserted_community.id,
person_id: inserted_banned_from_comm_person.id,
expires: None,
},
)
.await?;
let comment_view = CommentView::read(
pool,
data.inserted_comment_0.id,
Some(inserted_banned_from_comm_local_user.person_id),
)
.await?;
assert!(comment_view.banned_from_community);
Person::delete(pool, inserted_banned_from_comm_person.id).await?;
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn comment_listing_local_user_not_banned_from_community() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into();
let data = init_data(pool).await;
let comment_view = CommentView::read(
pool,
data.inserted_comment_0.id,
Some(data.timmy_local_user_view.person.id),
)
.await?;
assert!(!comment_view.banned_from_community);
cleanup(data, pool).await
}
}

@ -77,7 +77,7 @@ impl CustomEmojiView {
}
for emoji in &mut result {
if let Some(keywords) = hash.get_mut(&emoji.custom_emoji.id) {
emoji.keywords = keywords.clone();
emoji.keywords.clone_from(keywords);
}
}
result

@ -4,7 +4,7 @@ use diesel::{result::Error, BoolExpressionMethods, ExpressionMethods, JoinOnDsl,
use diesel_async::RunQueryDsl;
use lemmy_db_schema::{
newtypes::{LocalUserId, PersonId},
schema::{local_user, person, person_aggregates},
schema::{local_user, local_user_vote_display_mode, person, person_aggregates},
utils::{
functions::{coalesce, lower},
DbConn,
@ -33,6 +33,7 @@ fn queries<'a>(
) -> Queries<impl ReadFn<'a, LocalUserView, ReadBy<'a>>, impl ListFn<'a, LocalUserView, ListMode>> {
let selection = (
local_user::all_columns,
local_user_vote_display_mode::all_columns,
person::all_columns,
person_aggregates::all_columns,
);
@ -58,6 +59,7 @@ fn queries<'a>(
_ => query,
};
query
.inner_join(local_user_vote_display_mode::table)
.inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id)))
.select(selection)
.first::<LocalUserView>(&mut conn)
@ -68,10 +70,11 @@ fn queries<'a>(
match mode {
ListMode::AdminsWithEmails => {
local_user::table
.filter(local_user::email.is_not_null())
.filter(local_user::admin.eq(true))
.inner_join(local_user_vote_display_mode::table)
.inner_join(person::table)
.inner_join(person_aggregates::table.on(person::id.eq(person_aggregates::person_id)))
.filter(local_user::email.is_not_null())
.filter(local_user::admin.eq(true))
.select(selection)
.load::<LocalUserView>(&mut conn)
.await

@ -14,15 +14,31 @@ use lemmy_db_schema::{
newtypes::{CommunityId, PersonId, PostId, PostReportId},
schema::{
community,
community_follower,
community_moderator,
community_person_ban,
local_user,
person,
person_block,
person_post_aggregates,
post,
post_aggregates,
post_hide,
post_like,
post_read,
post_report,
post_saved,
},
utils::{
functions::coalesce,
get_conn,
limit_and_offset,
DbConn,
DbPool,
ListFn,
Queries,
ReadFn,
},
utils::{get_conn, limit_and_offset, DbConn, DbPool, ListFn, Queries, ReadFn},
};
fn queries<'a>() -> Queries<
@ -42,6 +58,67 @@ fn queries<'a>() -> Queries<
.and(community_person_ban::person_id.eq(post::creator_id)),
),
)
.left_join(
aliases::community_moderator1.on(
aliases::community_moderator1
.field(community_moderator::community_id)
.eq(post::community_id)
.and(
aliases::community_moderator1
.field(community_moderator::person_id)
.eq(my_person_id),
),
),
)
.left_join(
local_user::table.on(
post::creator_id
.eq(local_user::person_id)
.and(local_user::admin.eq(true)),
),
)
.left_join(
post_saved::table.on(
post::id
.eq(post_saved::post_id)
.and(post_saved::person_id.eq(my_person_id)),
),
)
.left_join(
post_read::table.on(
post::id
.eq(post_read::post_id)
.and(post_read::person_id.eq(my_person_id)),
),
)
.left_join(
post_hide::table.on(
post::id
.eq(post_hide::post_id)
.and(post_hide::person_id.eq(my_person_id)),
),
)
.left_join(
person_block::table.on(
post::creator_id
.eq(person_block::target_id)
.and(person_block::person_id.eq(my_person_id)),
),
)
.left_join(
person_post_aggregates::table.on(
post::id
.eq(person_post_aggregates::post_id)
.and(person_post_aggregates::person_id.eq(my_person_id)),
),
)
.left_join(
community_follower::table.on(
post::community_id
.eq(community_follower::community_id)
.and(community_follower::person_id.eq(my_person_id)),
),
)
.left_join(
post_like::table.on(
post::id
@ -61,7 +138,21 @@ fn queries<'a>() -> Queries<
person::all_columns,
aliases::person1.fields(person::all_columns),
community_person_ban::community_id.nullable().is_not_null(),
aliases::community_moderator1
.field(community_moderator::community_id)
.nullable()
.is_not_null(),
local_user::admin.nullable().is_not_null(),
community_follower::pending.nullable(),
post_saved::post_id.nullable().is_not_null(),
post_read::post_id.nullable().is_not_null(),
post_hide::post_id.nullable().is_not_null(),
person_block::target_id.nullable().is_not_null(),
post_like::score.nullable(),
coalesce(
post_aggregates::comments.nullable() - person_post_aggregates::read_comments.nullable(),
post_aggregates::comments,
),
post_aggregates::all_columns,
aliases::person2.fields(person::all_columns.nullable()),
))
@ -192,9 +283,9 @@ impl PostReportQuery {
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{
post_report_view::{PostReportQuery, PostReportView},
@ -206,6 +297,7 @@ mod tests {
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
local_user_vote_display_mode::LocalUserVoteDisplayMode,
person::{Person, PersonInsertForm},
post::{Post, PostInsertForm},
post_report::{PostReport, PostReportForm},
@ -238,9 +330,12 @@ mod tests {
.person_id(inserted_timmy.id)
.password_encrypted("123".to_string())
.build();
let timmy_local_user = LocalUser::create(pool, &new_local_user).await.unwrap();
let timmy_local_user = LocalUser::create(pool, &new_local_user, vec![])
.await
.unwrap();
let timmy_view = LocalUserView {
local_user: timmy_local_user,
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),
person: inserted_timmy.clone(),
counts: Default::default(),
};

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

Loading…
Cancel
Save