Merge branch 'main' into apub-remove-expires

apub-remove-expires
Felix Ableitner 1 month ago
commit e84e25d8fa

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

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

960
Cargo.lock generated

File diff suppressed because it is too large Load Diff

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

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

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

@ -18,6 +18,7 @@ import {
resolveBetaCommunity, resolveBetaCommunity,
createComment, createComment,
deletePost, deletePost,
delay,
removePost, removePost,
getPost, getPost,
unfollowRemotes, unfollowRemotes,
@ -219,9 +220,10 @@ test("Sticky a post", async () => {
if (!gammaPost) { if (!gammaPost) {
throw "Missing gamma post"; 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; 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); 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); expect(gammaPost.post?.post.ap_id).toBe(alphaPost.post_view.post.ap_id);
await unfollowRemotes(alpha); 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) { if (!site.my_user) {
throw "Missing site 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 () => { 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); let user = await registerUser(alpha, alphaUrl);
// make a local post and comment // 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; .community;
if (!alphaCommunity) { if (!alphaCommunity) {
throw "Missing alpha community"; throw "Missing alpha community";
@ -134,8 +134,28 @@ test("Create user with Arabic name", async () => {
if (!site.my_user) { if (!site.my_user) {
throw "Missing site 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; let alphaPerson = (await resolvePerson(alpha, apShortname)).person;
expect(alphaPerson).toBeDefined(); 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"]);
});

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

@ -259,9 +259,9 @@ pub async fn local_user_view_from_jwt(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*; use super::*;

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

@ -3,6 +3,7 @@ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::SaveUserSettings, person::SaveUserSettings,
utils::{ utils::{
get_url_blocklist,
local_site_to_slur_regex, local_site_to_slur_regex,
process_markdown_opt, process_markdown_opt,
proxy_image_link_opt_api, proxy_image_link_opt_api,
@ -35,7 +36,10 @@ pub async fn save_user_settings(
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool()).await?;
let slur_regex = local_site_to_slur_regex(&site_view.local_site); 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 avatar = proxy_image_link_opt_api(&data.avatar, &context).await?;
let banner = proxy_image_link_opt_api(&data.banner, &context).await?; let banner = proxy_image_link_opt_api(&data.banner, &context).await?;

@ -4,10 +4,7 @@ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::{UpdateTotp, UpdateTotpResponse}, person::{UpdateTotp, UpdateTotpResponse},
}; };
use lemmy_db_schema::{ use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
source::local_user::{LocalUser, LocalUserUpdateForm},
traits::Crud,
};
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;

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

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

@ -59,6 +59,8 @@ uuid = { workspace = true, optional = true }
tokio = { workspace = true, optional = true } tokio = { workspace = true, optional = true }
reqwest = { workspace = true, optional = true } reqwest = { workspace = true, optional = true }
ts-rs = { workspace = true, optional = true } ts-rs = { workspace = true, optional = true }
moka.workspace = true
anyhow.workspace = true
once_cell = { workspace = true, optional = true } once_cell = { workspace = true, optional = true }
actix-web = { workspace = true, optional = true } actix-web = { workspace = true, optional = true }
enum-map = { workspace = true } enum-map = { workspace = true }

@ -72,9 +72,9 @@ impl Claims {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{claims::Claims, context::LemmyContext}; use crate::{claims::Claims, context::LemmyContext};
use actix_web::test::TestRequest; use actix_web::test::TestRequest;
@ -124,7 +124,9 @@ mod tests {
.password_encrypted("123456".to_string()) .password_encrypted("123456".to_string())
.build(); .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 req = TestRequest::default().to_http_request();
let jwt = Claims::generate(inserted_local_user.id, req, &context) let jwt = Claims::generate(inserted_local_user.id, req, &context)

@ -59,14 +59,8 @@ pub async fn fetch_link_metadata(
let opengraph_data = extract_opengraph_data(&html_bytes, url) let opengraph_data = extract_opengraph_data(&html_bytes, url)
.map_err(|e| info!("{e}")) .map_err(|e| info!("{e}"))
.unwrap_or_default(); .unwrap_or_default();
let thumbnail = extract_thumbnail_from_opengraph_data( let thumbnail =
url, extract_thumbnail_from_opengraph_data(url, &opengraph_data, generate_thumbnail, context).await;
&opengraph_data,
&content_type,
generate_thumbnail,
context,
)
.await;
Ok(LinkMetadata { Ok(LinkMetadata {
opengraph_data, opengraph_data,
@ -158,23 +152,21 @@ fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> Result<OpenGraphData,
pub async fn extract_thumbnail_from_opengraph_data( pub async fn extract_thumbnail_from_opengraph_data(
url: &Url, url: &Url,
opengraph_data: &OpenGraphData, opengraph_data: &OpenGraphData,
content_type: &Option<Mime>,
generate_thumbnail: bool, generate_thumbnail: bool,
context: &LemmyContext, context: &LemmyContext,
) -> Option<DbUrl> { ) -> Option<DbUrl> {
let is_image = content_type.as_ref().unwrap_or(&mime::TEXT_PLAIN).type_() == mime::IMAGE; if generate_thumbnail {
if generate_thumbnail && is_image {
let image_url = opengraph_data let image_url = opengraph_data
.image .image
.as_ref() .as_ref()
.map(lemmy_db_schema::newtypes::DbUrl::inner) .map(DbUrl::inner)
.unwrap_or(url); .unwrap_or(url);
generate_pictrs_thumbnail(image_url, context) generate_pictrs_thumbnail(image_url, context)
.await .await
.ok() .ok()
.map(Into::into) .map(Into::into)
} else { } else {
None opengraph_data.image.clone()
} }
} }
@ -321,9 +313,9 @@ async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> Resu
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{ use crate::{
context::LemmyContext, context::LemmyContext,
@ -363,7 +355,7 @@ mod tests {
Some(mime::TEXT_HTML_UTF_8.to_string()), Some(mime::TEXT_HTML_UTF_8.to_string()),
sample_res.content_type sample_res.content_type
); );
assert_eq!(None, sample_res.thumbnail); assert!(sample_res.thumbnail.is_some());
} }
// #[test] // #[test]

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

@ -17,6 +17,7 @@ use lemmy_db_schema::{
instance_block::InstanceBlock, instance_block::InstanceBlock,
local_site::LocalSite, local_site::LocalSite,
local_site_rate_limit::LocalSiteRateLimit, local_site_rate_limit::LocalSiteRateLimit,
local_site_url_blocklist::LocalSiteUrlBlocklist,
password_reset_request::PasswordResetRequest, password_reset_request::PasswordResetRequest,
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},
person_block::PersonBlock, person_block::PersonBlock,
@ -38,18 +39,24 @@ use lemmy_utils::{
rate_limit::{ActionType, BucketConfig}, rate_limit::{ActionType, BucketConfig},
settings::structs::{PictrsImageMode, Settings}, settings::structs::{PictrsImageMode, Settings},
utils::{ utils::{
markdown::markdown_rewrite_image_links, markdown::{markdown_check_for_blocked_urls, markdown_rewrite_image_links},
slurs::{build_slur_regex, remove_slurs}, 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 rosetta_i18n::{Language, LanguageId};
use std::collections::HashSet; use std::{collections::HashSet, time::Duration};
use tracing::warn; use tracing::warn;
use url::{ParseError, Url}; use url::{ParseError, Url};
use urlencoding::encode; use urlencoding::encode;
pub static AUTH_COOKIE_NAME: &str = "jwt"; 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)] #[tracing::instrument(skip_all)]
pub async fn is_mod_or_admin( 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) .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( pub async fn send_application_approved_email(
user: &LocalUserView, user: &LocalUserView,
settings: &Settings, settings: &Settings,
@ -867,9 +915,13 @@ fn limit_expire_time(expires: DateTime<Utc>) -> LemmyResult<Option<DateTime<Utc>
pub async fn process_markdown( pub async fn process_markdown(
text: &str, text: &str,
slur_regex: &Option<Regex>, slur_regex: &Option<Regex>,
url_blocklist: &RegexSet,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<String> { ) -> LemmyResult<String> {
let text = remove_slurs(text, slur_regex); let text = remove_slurs(text, slur_regex);
markdown_check_for_blocked_urls(&text, url_blocklist)?;
if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages { if context.settings().pictrs_config()?.image_mode() == PictrsImageMode::ProxyAllImages {
let (text, links) = markdown_rewrite_image_links(text); let (text, links) = markdown_rewrite_image_links(text);
RemoteImage::create(&mut context.pool(), links).await?; RemoteImage::create(&mut context.pool(), links).await?;
@ -882,10 +934,13 @@ pub async fn process_markdown(
pub async fn process_markdown_opt( pub async fn process_markdown_opt(
text: &Option<String>, text: &Option<String>,
slur_regex: &Option<Regex>, slur_regex: &Option<Regex>,
url_blocklist: &RegexSet,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<Option<String>> { ) -> LemmyResult<Option<String>> {
match text { 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), None => Ok(None),
} }
} }
@ -964,9 +1019,9 @@ pub async fn proxy_image_link_opt_apub(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*; use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

@ -10,6 +10,7 @@ use lemmy_api_common::{
check_post_deleted_or_removed, check_post_deleted_or_removed,
generate_local_apub_endpoint, generate_local_apub_endpoint,
get_post, get_post,
get_url_blocklist,
is_mod_or_admin, is_mod_or_admin,
local_site_to_slur_regex, local_site_to_slur_regex,
process_markdown, process_markdown,
@ -44,7 +45,8 @@ pub async fn create_comment(
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let slur_regex = local_site_to_slur_regex(&local_site); 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)?; is_valid_body_field(&Some(content.clone()), false)?;
// Check for a community ban // Check for a community ban
@ -162,10 +164,15 @@ pub async fn create_comment(
) )
.await?; .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 { if let Some(parent) = parent_opt {
let person_id = local_user_view.person.id;
let parent_id = parent.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 { if let Ok(reply) = comment_reply {
CommentReply::update( CommentReply::update(
&mut context.pool(), &mut context.pool(),
@ -177,7 +184,6 @@ pub async fn create_comment(
} }
// If the parent has PersonMentions mark them as read too // If the parent has PersonMentions mark them as read too
let person_id = local_user_view.person.id;
let person_mention = let person_mention =
PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await; PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
if let Ok(mention) = person_mention { if let Ok(mention) = person_mention {

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

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

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

@ -9,6 +9,7 @@ use lemmy_api_common::{
utils::{ utils::{
check_community_user_action, check_community_user_action,
generate_local_apub_endpoint, generate_local_apub_endpoint,
get_url_blocklist,
honeypot_check, honeypot_check,
local_site_to_slur_regex, local_site_to_slur_regex,
mark_post_as_read, mark_post_as_read,
@ -38,6 +39,7 @@ use lemmy_utils::{
validation::{ validation::{
check_url_scheme, check_url_scheme,
clean_url_params, clean_url_params,
is_url_blocked,
is_valid_alt_text_field, is_valid_alt_text_field,
is_valid_body_field, is_valid_body_field,
is_valid_post_title, is_valid_post_title,
@ -60,8 +62,9 @@ pub async fn create_post(
let slur_regex = local_site_to_slur_regex(&local_site); let slur_regex = local_site_to_slur_regex(&local_site);
check_slurs(&data.name, &slur_regex)?; 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 data_url = data.url.as_ref();
let url = data_url.map(clean_url_params); // TODO no good way to handle a "clear" 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); 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_post_title(&data.name)?;
is_valid_body_field(&body, true)?; is_valid_body_field(&body, true)?;
is_valid_alt_text_field(&data.alt_text)?; is_valid_alt_text_field(&data.alt_text)?;
is_url_blocked(&url, &url_blocklist)?;
check_url_scheme(&url)?; check_url_scheme(&url)?;
check_url_scheme(&custom_thumbnail)?; check_url_scheme(&custom_thumbnail)?;

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

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

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

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

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

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

@ -4,6 +4,7 @@ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
site::{EditSite, SiteResponse}, site::{EditSite, SiteResponse},
utils::{ utils::{
get_url_blocklist,
is_admin, is_admin,
local_site_rate_limit_to_rate_limit_config, local_site_rate_limit_to_rate_limit_config,
local_site_to_slur_regex, local_site_to_slur_regex,
@ -18,6 +19,7 @@ use lemmy_db_schema::{
federation_blocklist::FederationBlockList, federation_blocklist::FederationBlockList,
local_site::{LocalSite, LocalSiteUpdateForm}, local_site::{LocalSite, LocalSiteUpdateForm},
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
local_site_url_blocklist::LocalSiteUrlBlocklist,
local_user::LocalUser, local_user::LocalUser,
site::{Site, SiteUpdateForm}, site::{Site, SiteUpdateForm},
tagline::Tagline, tagline::Tagline,
@ -34,6 +36,7 @@ use lemmy_utils::{
validation::{ validation::{
build_and_check_regex, build_and_check_regex,
check_site_visibility_valid, check_site_visibility_valid,
check_urls_are_valid,
is_valid_body_field, is_valid_body_field,
site_description_length_check, site_description_length_check,
site_name_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 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 icon = proxy_image_link_opt_api(&data.icon, &context).await?;
let banner = proxy_image_link_opt_api(&data.banner, &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(); let blocked = data.blocked_instances.clone();
FederationBlockList::replace(&mut context.pool(), blocked).await?; 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. // TODO can't think of a better way to do this.
// If the server suddenly requires email verification, or required applications, no old users // 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. // 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)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::site::update::validate_update_payload; use crate::site::update::validate_update_payload;
use lemmy_api_common::site::EditSite; use lemmy_api_common::site::EditSite;
@ -578,6 +587,7 @@ mod tests {
captcha_difficulty: None, captcha_difficulty: None,
allowed_instances: None, allowed_instances: None,
blocked_instances: None, blocked_instances: None,
blocked_urls: None,
taglines: None, taglines: None,
registration_mode: site_registration_mode, registration_mode: site_registration_mode,
reports_email_admins: None, reports_email_admins: None,

@ -20,6 +20,7 @@ use lemmy_db_schema::{
aggregates::structs::PersonAggregates, aggregates::structs::PersonAggregates,
source::{ source::{
captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer},
language::Language,
local_user::{LocalUser, LocalUserInsertForm}, local_user::{LocalUser, LocalUserInsertForm},
local_user_vote_display_mode::LocalUserVoteDisplayMode, local_user_vote_display_mode::LocalUserVoteDisplayMode,
person::{Person, PersonInsertForm}, person::{Person, PersonInsertForm},
@ -36,6 +37,7 @@ use lemmy_utils::{
validation::is_valid_actor_name, validation::is_valid_actor_name,
}, },
}; };
use std::collections::HashSet;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn register( pub async fn register(
@ -128,12 +130,15 @@ pub async fn register(
let accepted_application = Some(!require_registration_application); let accepted_application = Some(!require_registration_application);
// Get the user's preferred language using the Accept-Language header // Get the user's preferred language using the Accept-Language header
let language_tag = req.headers().get("Accept-Language").and_then(|hdr| { let language_tags: Vec<String> = req
accept_language::parse(hdr.to_str().unwrap_or_default()) .headers()
.first() .get("Accept-Language")
// Remove the optional region code .map(|hdr| accept_language::parse(hdr.to_str().unwrap_or_default()))
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string()) .iter()
}); .flatten()
// Remove the optional region code
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string())
.collect();
// Create the local user // Create the local user
let local_user_form = LocalUserInsertForm::builder() let local_user_form = LocalUserInsertForm::builder()
@ -144,12 +149,23 @@ pub async fn register(
.accepted_application(accepted_application) .accepted_application(accepted_application)
.default_listing_type(Some(local_site.default_post_listing_type)) .default_listing_type(Some(local_site.default_post_listing_type))
.post_listing_mode(Some(local_site.default_post_listing_mode)) .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 // If its the initial site setup, they are an admin
.admin(Some(!local_site.site_setup)) .admin(Some(!local_site.site_setup))
.build(); .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 { if local_site.site_setup && require_registration_application {
// Create the registration application // Create the registration application

@ -8,6 +8,6 @@
"type": "Block", "type": "Block",
"removeData": true, "removeData": true,
"summary": "spam post", "summary": "spam post",
"expires": "2021-11-01T12:23:50.151874Z", "endTime": "2021-11-01T12:23:50.151874Z",
"id": "http://enterprise.lemmy.ml/activities/block/5d42fffb-0903-4625-86d4-0b39bb344fc2" "id": "http://enterprise.lemmy.ml/activities/block/5d42fffb-0903-4625-86d4-0b39bb344fc2"
} }

@ -11,7 +11,7 @@
"type": "Block", "type": "Block",
"removeData": true, "removeData": true,
"summary": "spam post", "summary": "spam post",
"expires": "2021-11-01T12:23:50.151874Z", "endTime": "2021-11-01T12:23:50.151874Z",
"id": "http://enterprise.lemmy.ml/activities/block/726f43ab-bd0e-4ab3-89c8-627e976f553c" "id": "http://enterprise.lemmy.ml/activities/block/726f43ab-bd0e-4ab3-89c8-627e976f553c"
}, },
"cc": ["http://enterprise.lemmy.ml/c/main"], "cc": ["http://enterprise.lemmy.ml/c/main"],

@ -3,5 +3,6 @@
"to": ["https://www.w3.org/ns/activitystreams#Public"], "to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "http://ds9.lemmy.ml/u/lemmy_alpha", "object": "http://ds9.lemmy.ml/u/lemmy_alpha",
"type": "Delete", "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,7 +2,7 @@ use crate::{
activities::{ activities::{
generate_activity_id, generate_activity_id,
verify_person_in_community, verify_person_in_community,
voting::{vote_comment, vote_post}, voting::{undo_vote_comment, undo_vote_post, vote_comment, vote_post},
}, },
insert_received_activity, insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson}, objects::{community::ApubCommunity, person::ApubPerson},
@ -17,7 +17,6 @@ use activitypub_federation::{
fetch::object_id::ObjectId, fetch::object_id::ObjectId,
traits::{ActivityHandler, Actor}, traits::{ActivityHandler, Actor},
}; };
use anyhow::anyhow;
use lemmy_api_common::{context::LemmyContext, utils::check_bot_account}; use lemmy_api_common::{context::LemmyContext, utils::check_bot_account};
use lemmy_db_schema::source::local_site::LocalSite; use lemmy_db_schema::source::local_site::LocalSite;
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
@ -58,15 +57,7 @@ impl ActivityHandler for Vote {
async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> { async fn verify(&self, context: &Data<LemmyContext>) -> Result<(), LemmyError> {
let community = self.community(context).await?; let community = self.community(context).await?;
verify_person_in_community(&self.actor, &community, context).await?; verify_person_in_community(&self.actor, &community, context).await?;
let enable_downvotes = LocalSite::read(&mut context.pool()) Ok(())
.await
.map(|l| l.enable_downvotes)
.unwrap_or(true);
if self.kind == VoteType::Dislike && !enable_downvotes {
Err(anyhow!("Downvotes disabled").into())
} else {
Ok(())
}
} }
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
@ -77,9 +68,22 @@ impl ActivityHandler for Vote {
check_bot_account(&actor.0)?; check_bot_account(&actor.0)?;
match object { let enable_downvotes = LocalSite::read(&mut context.pool())
PostOrComment::Post(p) => vote_post(&self.kind, actor, &p, context).await, .await
PostOrComment::Comment(c) => vote_comment(&self.kind, actor, &c, context).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)] #[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::indexing_slicing)]
use crate::{ use crate::{
activity_lists::{GroupInboxActivities, PersonInboxActivities, SharedInboxActivities}, activity_lists::{GroupInboxActivities, PersonInboxActivities, SharedInboxActivities},

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

@ -319,8 +319,8 @@ pub async fn import_settings(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::indexing_slicing)]
use crate::api::user_settings_backup::{export_settings, import_settings}; use crate::api::user_settings_backup::{export_settings, import_settings};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
@ -361,7 +361,7 @@ mod tests {
.person_id(person.id) .person_id(person.id)
.password_encrypted("pass".to_string()) .password_encrypted("pass".to_string())
.build(); .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?) Ok(LocalUserView::read(&mut context.pool(), local_user.id).await?)
} }

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

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

@ -115,9 +115,9 @@ pub(crate) async fn get_apub_community_featured(
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
pub(crate) mod tests { pub(crate) mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*; use super::*;
use crate::protocol::objects::{group::Group, tombstone::Tombstone}; use crate::protocol::objects::{group::Group, tombstone::Tombstone};

@ -18,7 +18,7 @@ use activitypub_federation::{
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, 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::{ use lemmy_db_schema::{
source::{ source::{
@ -165,7 +165,8 @@ impl Object for ApubComment {
let local_site = LocalSite::read(&mut context.pool()).await.ok(); let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site); 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 = let language_id =
LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?; LanguageTag::to_language_id_single(note.language, &mut context.pool()).await?;

@ -21,6 +21,7 @@ use lemmy_api_common::{
generate_featured_url, generate_featured_url,
generate_moderators_url, generate_moderators_url,
generate_outbox_url, generate_outbox_url,
get_url_blocklist,
local_site_opt_to_slur_regex, local_site_opt_to_slur_regex,
process_markdown_opt, process_markdown_opt,
proxy_image_link_opt_apub, proxy_image_link_opt_apub,
@ -141,8 +142,10 @@ impl Object for ApubCommunity {
let local_site = LocalSite::read(&mut context.pool()).await.ok(); let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site); 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 = 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 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?; let banner = proxy_image_link_opt_apub(group.image.map(|i| i.url), context).await?;

@ -19,7 +19,12 @@ use activitypub_federation::{
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, 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::{ use lemmy_db_schema::{
newtypes::InstanceId, newtypes::InstanceId,
@ -138,8 +143,9 @@ impl Object for ApubSite {
let local_site = LocalSite::read(&mut context.pool()).await.ok(); let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site); 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 = 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 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?; 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, context::LemmyContext,
utils::{ utils::{
generate_outbox_url, generate_outbox_url,
get_url_blocklist,
local_site_opt_to_slur_regex, local_site_opt_to_slur_regex,
process_markdown_opt, process_markdown_opt,
proxy_image_link_opt_apub, proxy_image_link_opt_apub,
@ -152,8 +153,9 @@ impl Object for ApubPerson {
let local_site = LocalSite::read(&mut context.pool()).await.ok(); let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site); 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 = 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 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?; let banner = proxy_image_link_opt_apub(person.image.map(|i| i.url), context).await?;

@ -26,6 +26,7 @@ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
request::fetch_link_metadata_opt, request::fetch_link_metadata_opt,
utils::{ utils::{
get_url_blocklist,
local_site_opt_to_sensitive, local_site_opt_to_sensitive,
local_site_opt_to_slur_regex, local_site_opt_to_slur_regex,
process_markdown_opt, process_markdown_opt,
@ -246,9 +247,10 @@ impl Object for ApubPost {
let thumbnail_url = proxy_image_link_opt_apub(thumbnail_url, context).await?; let thumbnail_url = proxy_image_link_opt_apub(thumbnail_url, context).await?;
let slur_regex = &local_site_opt_to_slur_regex(&local_site); 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 = 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 = let language_id =
LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?; LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?;

@ -14,7 +14,7 @@ use activitypub_federation::{
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, 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::{ use lemmy_db_schema::{
source::{ source::{
@ -127,8 +127,9 @@ impl Object for ApubPrivateMessage {
let local_site = LocalSite::read(&mut context.pool()).await.ok(); let local_site = LocalSite::read(&mut context.pool()).await.ok();
let slur_regex = &local_site_opt_to_slur_regex(&local_site); 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 = 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 { let form = PrivateMessageInsertForm {
creator_id: creator.id, creator_id: creator.id,

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

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

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

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

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

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

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

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

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

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

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

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

@ -41,9 +41,9 @@ impl Language {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{source::language::Language, utils::build_db_pool_for_tests}; use crate::{source::language::Language, utils::build_db_pool_for_tests};
use pretty_assertions::assert_eq; 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,12 +1,11 @@
use crate::{ use crate::{
newtypes::{DbUrl, LocalUserId, PersonId}, newtypes::{DbUrl, LanguageId, LocalUserId, PersonId},
schema::{local_user, person, registration_application}, schema::{local_user, person, registration_application},
source::{ source::{
actor_language::{LocalUserLanguage, SiteLanguage}, actor_language::LocalUserLanguage,
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeInsertForm}, local_user_vote_display_mode::{LocalUserVoteDisplayMode, LocalUserVoteDisplayModeInsertForm},
}, },
traits::Crud,
utils::{ utils::{
functions::{coalesce, lower}, functions::{coalesce, lower},
get_conn, get_conn,
@ -25,6 +24,52 @@ use diesel::{
use diesel_async::RunQueryDsl; use diesel_async::RunQueryDsl;
impl LocalUser { 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( pub async fn update_password(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
local_user_id: LocalUserId, local_user_id: LocalUserId,
@ -183,52 +228,3 @@ pub struct UserBackupLists {
pub blocked_users: Vec<DbUrl>, pub blocked_users: Vec<DbUrl>,
pub blocked_instances: Vec<String>, 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?;
}
// 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_)
}
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
}
}

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

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

@ -81,9 +81,9 @@ impl PasswordResetRequest {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{ use crate::{
source::{ source::{
@ -121,7 +121,9 @@ mod tests {
.password_encrypted("pass".to_string()) .password_encrypted("pass".to_string())
.build(); .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"; let token = "nope";

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

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

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

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

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

@ -409,6 +409,15 @@ diesel::table! {
} }
} }
diesel::table! {
local_site_url_blocklist (id) {
id -> Int4,
url -> Text,
published -> Timestamptz,
updated -> Nullable<Timestamptz>,
}
}
diesel::table! { diesel::table! {
use diesel::sql_types::*; use diesel::sql_types::*;
use super::sql_types::SortTypeEnum; use super::sql_types::SortTypeEnum;
@ -1052,6 +1061,7 @@ diesel::allow_tables_to_appear_in_same_query!(
local_image, local_image,
local_site, local_site,
local_site_rate_limit, local_site_rate_limit,
local_site_url_blocklist,
local_user, local_user,
local_user_language, local_user_language,
local_user_vote_display_mode, local_user_vote_display_mode,

@ -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>>,
}

@ -22,6 +22,7 @@ pub mod instance_block;
pub mod language; pub mod language;
pub mod local_site; pub mod local_site;
pub mod local_site_rate_limit; pub mod local_site_rate_limit;
pub mod local_site_url_blocklist;
pub mod local_user; pub mod local_user;
pub mod local_user_vote_display_mode; pub mod local_user_vote_display_mode;
pub mod login_token; pub mod login_token;

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

@ -258,9 +258,9 @@ impl CommentReportQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{ use crate::{
comment_report_view::{CommentReportQuery, CommentReportView}, comment_report_view::{CommentReportQuery, CommentReportView},
@ -308,7 +308,9 @@ mod tests {
.person_id(inserted_timmy.id) .person_id(inserted_timmy.id)
.password_encrypted("123".to_string()) .password_encrypted("123".to_string())
.build(); .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 { let timmy_view = LocalUserView {
local_user: timmy_local_user, local_user: timmy_local_user,
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),

@ -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| { let is_saved = |person_id| {
comment_saved::table comment_saved::table
.filter( .filter(
@ -113,6 +123,14 @@ fn queries<'a>() -> Queries<
); );
let all_joins = move |query: comment::BoxedQuery<'a, Pg>, my_person_id: Option<PersonId>| { 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< let score_selection: Box<
dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::SmallInt>>, dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::SmallInt>>,
> = if let Some(person_id) = my_person_id { > = if let Some(person_id) = my_person_id {
@ -156,6 +174,7 @@ fn queries<'a>() -> Queries<
community::all_columns, community::all_columns,
comment_aggregates::all_columns, comment_aggregates::all_columns,
is_creator_banned_from_community, is_creator_banned_from_community,
is_local_user_banned_from_community_selection,
creator_is_moderator, creator_is_moderator,
creator_is_admin, creator_is_admin,
subscribed_type_selection, subscribed_type_selection,
@ -407,9 +426,9 @@ impl<'a> CommentQuery<'a> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{ use crate::{
comment_view::{CommentQuery, CommentSortType, CommentView, DbPool}, comment_view::{CommentQuery, CommentSortType, CommentView, DbPool},
@ -436,6 +455,8 @@ mod tests {
CommunityInsertForm, CommunityInsertForm,
CommunityModerator, CommunityModerator,
CommunityModeratorForm, CommunityModeratorForm,
CommunityPersonBan,
CommunityPersonBanForm,
CommunityUpdateForm, CommunityUpdateForm,
}, },
instance::Instance, instance::Instance,
@ -446,11 +467,12 @@ mod tests {
person_block::{PersonBlock, PersonBlockForm}, person_block::{PersonBlock, PersonBlockForm},
post::{Post, PostInsertForm}, post::{Post, PostInsertForm},
}, },
traits::{Blockable, Crud, Joinable, Likeable, Saveable}, traits::{Bannable, Blockable, Crud, Joinable, Likeable, Saveable},
utils::{build_db_pool_for_tests, RANK_DEFAULT}, utils::{build_db_pool_for_tests, RANK_DEFAULT},
CommunityVisibility, CommunityVisibility,
SubscribedType, SubscribedType,
}; };
use lemmy_utils::error::LemmyResult;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serial_test::serial; use serial_test::serial;
@ -481,7 +503,7 @@ mod tests {
.admin(Some(true)) .admin(Some(true))
.password_encrypted(String::new()) .password_encrypted(String::new())
.build(); .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 .await
.unwrap(); .unwrap();
@ -633,12 +655,12 @@ mod tests {
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_crud() { async fn test_crud() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await; let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into(); let pool = &mut pool.into();
let data = init_data(pool).await; 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(); let mut expected_comment_view_with_person = expected_comment_view_no_person.clone();
expected_comment_view_with_person.my_vote = Some(1); expected_comment_view_with_person.my_vote = Some(1);
@ -649,8 +671,7 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert_eq!( assert_eq!(
expected_comment_view_no_person, expected_comment_view_no_person,
@ -664,8 +685,7 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert_eq!( assert_eq!(
expected_comment_view_with_person, expected_comment_view_with_person,
@ -680,8 +700,7 @@ mod tests {
data.inserted_comment_1.id, data.inserted_comment_1.id,
Some(data.timmy_local_user_view.person.id), Some(data.timmy_local_user_view.person.id),
) )
.await .await?;
.unwrap();
// Make sure block set the creator blocked // Make sure block set the creator blocked
assert!(read_comment_from_blocked_person.creator_blocked); assert!(read_comment_from_blocked_person.creator_blocked);
@ -692,8 +711,7 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert_eq!( assert_eq!(
expected_comment_view_with_person, expected_comment_view_with_person,
@ -708,17 +726,16 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert!(read_disliked_comment_views.is_empty()); assert!(read_disliked_comment_views.is_empty());
cleanup(data, pool).await; cleanup(data, pool).await
} }
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_comment_tree() { async fn test_comment_tree() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await; let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into(); let pool = &mut pool.into();
let data = init_data(pool).await; let data = init_data(pool).await;
@ -730,8 +747,7 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
let child_path = data.inserted_comment_1.path.clone(); let child_path = data.inserted_comment_1.path.clone();
let read_comment_views_child_path = CommentQuery { let read_comment_views_child_path = CommentQuery {
@ -740,8 +756,7 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
// Make sure the comment parent-limited fetch is correct // Make sure the comment parent-limited fetch is correct
assert_length!(6, read_comment_views_top_path); assert_length!(6, read_comment_views_top_path);
@ -761,12 +776,11 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
// Make sure a depth limited one only has the top comment // Make sure a depth limited one only has the top comment
assert_eq!( assert_eq!(
expected_comment_view(&data, pool).await, expected_comment_view(&data, pool).await?,
read_comment_views_top_max_depth[0] read_comment_views_top_max_depth[0]
); );
assert_length!(1, read_comment_views_top_max_depth); assert_length!(1, read_comment_views_top_max_depth);
@ -780,8 +794,7 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
// Make sure a depth limited one, and given child comment 1, has 3 // Make sure a depth limited one, and given child comment 1, has 3
assert!(read_comment_views_parent_max_depth[2] assert!(read_comment_views_parent_max_depth[2]
@ -790,12 +803,12 @@ mod tests {
.eq("Comment 3")); .eq("Comment 3"));
assert_length!(3, read_comment_views_parent_max_depth); assert_length!(3, read_comment_views_parent_max_depth);
cleanup(data, pool).await; cleanup(data, pool).await
} }
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_languages() { async fn test_languages() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await; let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into(); let pool = &mut pool.into();
let data = init_data(pool).await; let data = init_data(pool).await;
@ -807,29 +820,25 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert_length!(5, all_languages); assert_length!(5, all_languages);
// change user lang to finnish, should only show one post in finnish and one undetermined // 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")) let finnish_id = Language::read_id_from_code(pool, Some("fi"))
.await .await?
.unwrap()
.unwrap(); .unwrap();
LocalUserLanguage::update( LocalUserLanguage::update(
pool, pool,
vec![finnish_id], vec![finnish_id],
data.timmy_local_user_view.local_user.id, data.timmy_local_user_view.local_user.id,
) )
.await .await?;
.unwrap();
let finnish_comments = CommentQuery { let finnish_comments = CommentQuery {
local_user: (Some(&data.timmy_local_user_view)), local_user: (Some(&data.timmy_local_user_view)),
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert_length!(2, finnish_comments); assert_length!(2, finnish_comments);
let finnish_comment = finnish_comments let finnish_comment = finnish_comments
.iter() .iter()
@ -846,23 +855,21 @@ mod tests {
vec![UNDETERMINED_ID], vec![UNDETERMINED_ID],
data.timmy_local_user_view.local_user.id, data.timmy_local_user_view.local_user.id,
) )
.await .await?;
.unwrap();
let undetermined_comment = CommentQuery { let undetermined_comment = CommentQuery {
local_user: (Some(&data.timmy_local_user_view)), local_user: (Some(&data.timmy_local_user_view)),
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert_length!(1, undetermined_comment); assert_length!(1, undetermined_comment);
cleanup(data, pool).await; cleanup(data, pool).await
} }
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_distinguished_first() { async fn test_distinguished_first() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await; let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into(); let pool = &mut pool.into();
let data = init_data(pool).await; let data = init_data(pool).await;
@ -871,26 +878,23 @@ mod tests {
distinguished: Some(true), distinguished: Some(true),
..Default::default() ..Default::default()
}; };
Comment::update(pool, data.inserted_comment_2.id, &form) Comment::update(pool, data.inserted_comment_2.id, &form).await?;
.await
.unwrap();
let comments = CommentQuery { let comments = CommentQuery {
post_id: Some(data.inserted_comment_2.post_id), post_id: Some(data.inserted_comment_2.post_id),
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert_eq!(comments[0].comment.id, data.inserted_comment_2.id); assert_eq!(comments[0].comment.id, data.inserted_comment_2.id);
assert!(comments[0].comment.distinguished); assert!(comments[0].comment.distinguished);
cleanup(data, pool).await; cleanup(data, pool).await
} }
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_creator_is_moderator() { async fn test_creator_is_moderator() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await; let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into(); let pool = &mut pool.into();
let data = init_data(pool).await; let data = init_data(pool).await;
@ -902,7 +906,7 @@ mod tests {
community_id, community_id,
person_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 // Make sure that they come back as a mod in the list
let comments = CommentQuery { let comments = CommentQuery {
@ -910,19 +914,18 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert_eq!(comments[1].creator.name, "sara"); assert_eq!(comments[1].creator.name, "sara");
assert!(comments[1].creator_is_moderator); assert!(comments[1].creator_is_moderator);
assert!(!comments[0].creator_is_moderator); assert!(!comments[0].creator_is_moderator);
cleanup(data, pool).await; cleanup(data, pool).await
} }
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_creator_is_admin() { async fn test_creator_is_admin() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await; let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into(); let pool = &mut pool.into();
let data = init_data(pool).await; let data = init_data(pool).await;
@ -932,8 +935,7 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
// Timmy is an admin, and make sure that field is true // Timmy is an admin, and make sure that field is true
assert_eq!(comments[0].creator.name, "timmy"); assert_eq!(comments[0].creator.name, "timmy");
@ -943,12 +945,12 @@ mod tests {
assert_eq!(comments[1].creator.name, "sara"); assert_eq!(comments[1].creator.name, "sara");
assert!(!comments[1].creator_is_admin); assert!(!comments[1].creator_is_admin);
cleanup(data, pool).await; cleanup(data, pool).await
} }
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn test_saved_order() { async fn test_saved_order() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await; let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into(); let pool = &mut pool.into();
let data = init_data(pool).await; let data = init_data(pool).await;
@ -958,17 +960,13 @@ mod tests {
person_id: data.timmy_local_user_view.person.id, person_id: data.timmy_local_user_view.person.id,
comment_id: data.inserted_comment_0.id, comment_id: data.inserted_comment_0.id,
}; };
CommentSaved::save(pool, &save_comment_0_form) CommentSaved::save(pool, &save_comment_0_form).await?;
.await
.unwrap();
let save_comment_2_form = CommentSavedForm { let save_comment_2_form = CommentSavedForm {
person_id: data.timmy_local_user_view.person.id, person_id: data.timmy_local_user_view.person.id,
comment_id: data.inserted_comment_2.id, comment_id: data.inserted_comment_2.id,
}; };
CommentSaved::save(pool, &save_comment_2_form) CommentSaved::save(pool, &save_comment_2_form).await?;
.await
.unwrap();
// Fetch the saved comments // Fetch the saved comments
let comments = CommentQuery { let comments = CommentQuery {
@ -977,8 +975,7 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
// There should only be two comments // There should only be two comments
assert_eq!(2, comments.len()); assert_eq!(2, comments.len());
@ -989,47 +986,33 @@ mod tests {
// The second comment, should be the first one saved // The second comment, should be the first one saved
assert_eq!(comments[1].comment.id, data.inserted_comment_0.id); 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( CommentLike::remove(
pool, pool,
data.timmy_local_user_view.person.id, data.timmy_local_user_view.person.id,
data.inserted_comment_0.id, data.inserted_comment_0.id,
) )
.await .await?;
.unwrap(); Comment::delete(pool, data.inserted_comment_0.id).await?;
Comment::delete(pool, data.inserted_comment_0.id) Comment::delete(pool, data.inserted_comment_1.id).await?;
.await Post::delete(pool, data.inserted_post.id).await?;
.unwrap(); Community::delete(pool, data.inserted_community.id).await?;
Comment::delete(pool, data.inserted_comment_1.id) Person::delete(pool, data.timmy_local_user_view.person.id).await?;
.await LocalUser::delete(pool, data.timmy_local_user_view.local_user.id).await?;
.unwrap(); Person::delete(pool, data.inserted_sara_person.id).await?;
Post::delete(pool, data.inserted_post.id).await.unwrap(); Instance::delete(pool, data.inserted_instance.id).await?;
Community::delete(pool, data.inserted_community.id)
.await Ok(())
.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();
} }
async fn expected_comment_view(data: &Data, pool: &mut DbPool<'_>) -> CommentView { async fn expected_comment_view(data: &Data, pool: &mut DbPool<'_>) -> LemmyResult<CommentView> {
let agg = CommentAggregates::read(pool, data.inserted_comment_0.id) let agg = CommentAggregates::read(pool, data.inserted_comment_0.id).await?;
.await Ok(CommentView {
.unwrap();
CommentView {
creator_banned_from_community: false, creator_banned_from_community: false,
banned_from_community: false,
creator_is_moderator: false, creator_is_moderator: false,
creator_is_admin: true, creator_is_admin: true,
my_vote: None, my_vote: None,
@ -1136,12 +1119,12 @@ mod tests {
hot_rank: RANK_DEFAULT, hot_rank: RANK_DEFAULT,
controversy_rank: 0.0, controversy_rank: 0.0,
}, },
} })
} }
#[tokio::test] #[tokio::test]
#[serial] #[serial]
async fn local_only_instance() { async fn local_only_instance() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests().await; let pool = &build_db_pool_for_tests().await;
let pool = &mut pool.into(); let pool = &mut pool.into();
let data = init_data(pool).await; let data = init_data(pool).await;
@ -1154,15 +1137,13 @@ mod tests {
..Default::default() ..Default::default()
}, },
) )
.await .await?;
.unwrap();
let unauthenticated_query = CommentQuery { let unauthenticated_query = CommentQuery {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert_eq!(0, unauthenticated_query.len()); assert_eq!(0, unauthenticated_query.len());
let authenticated_query = CommentQuery { let authenticated_query = CommentQuery {
@ -1170,8 +1151,7 @@ mod tests {
..Default::default() ..Default::default()
} }
.list(pool) .list(pool)
.await .await?;
.unwrap();
assert_eq!(5, authenticated_query.len()); assert_eq!(5, authenticated_query.len());
let unauthenticated_comment = CommentView::read(pool, data.inserted_comment_0.id, None).await; let unauthenticated_comment = CommentView::read(pool, data.inserted_comment_0.id, None).await;
@ -1185,6 +1165,67 @@ mod tests {
.await; .await;
assert!(authenticated_comment.is_ok()); 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 { for emoji in &mut result {
if let Some(keywords) = hash.get_mut(&emoji.custom_emoji.id) { if let Some(keywords) = hash.get_mut(&emoji.custom_emoji.id) {
emoji.keywords = keywords.clone(); emoji.keywords.clone_from(keywords);
} }
} }
result result

@ -283,9 +283,9 @@ impl PostReportQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{ use crate::{
post_report_view::{PostReportQuery, PostReportView}, post_report_view::{PostReportQuery, PostReportView},
@ -330,7 +330,9 @@ mod tests {
.person_id(inserted_timmy.id) .person_id(inserted_timmy.id)
.password_encrypted("123".to_string()) .password_encrypted("123".to_string())
.build(); .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 { let timmy_view = LocalUserView {
local_user: timmy_local_user, local_user: timmy_local_user,
local_user_vote_display_mode: LocalUserVoteDisplayMode::default(), local_user_vote_display_mode: LocalUserVoteDisplayMode::default(),

@ -73,6 +73,17 @@ fn queries<'a>() -> Queries<
.and(community_person_ban::person_id.eq(post_aggregates::creator_id)), .and(community_person_ban::person_id.eq(post_aggregates::creator_id)),
), ),
); );
let is_local_user_banned_from_community = |person_id| {
exists(
community_person_ban::table.filter(
post_aggregates::community_id
.eq(community_person_ban::community_id)
.and(community_person_ban::person_id.eq(person_id)),
),
)
};
let creator_is_moderator = exists( let creator_is_moderator = exists(
community_moderator::table.filter( community_moderator::table.filter(
post_aggregates::community_id post_aggregates::community_id
@ -143,6 +154,14 @@ fn queries<'a>() -> Queries<
let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>, let all_joins = move |query: post_aggregates::BoxedQuery<'a, Pg>,
my_person_id: Option<PersonId>| { 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 is_saved_selection: Box< let is_saved_selection: Box<
dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::Timestamptz>>, dyn BoxableExpression<_, Pg, SqlType = sql_types::Nullable<sql_types::Timestamptz>>,
> = if let Some(person_id) = my_person_id { > = if let Some(person_id) = my_person_id {
@ -223,6 +242,7 @@ fn queries<'a>() -> Queries<
person::all_columns, person::all_columns,
community::all_columns, community::all_columns,
is_creator_banned_from_community, is_creator_banned_from_community,
is_local_user_banned_from_community_selection,
creator_is_moderator, creator_is_moderator,
creator_is_admin, creator_is_admin,
post_aggregates::all_columns, post_aggregates::all_columns,
@ -742,6 +762,8 @@ mod tests {
CommunityInsertForm, CommunityInsertForm,
CommunityModerator, CommunityModerator,
CommunityModeratorForm, CommunityModeratorForm,
CommunityPersonBan,
CommunityPersonBanForm,
CommunityUpdateForm, CommunityUpdateForm,
}, },
community_block::{CommunityBlock, CommunityBlockForm}, community_block::{CommunityBlock, CommunityBlockForm},
@ -755,7 +777,7 @@ mod tests {
post::{Post, PostHide, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm}, post::{Post, PostHide, PostInsertForm, PostLike, PostLikeForm, PostRead, PostUpdateForm},
site::Site, site::Site,
}, },
traits::{Blockable, Crud, Joinable, Likeable}, traits::{Bannable, Blockable, Crud, Joinable, Likeable},
utils::{build_db_pool, build_db_pool_for_tests, DbPool, RANK_DEFAULT}, utils::{build_db_pool, build_db_pool_for_tests, DbPool, RANK_DEFAULT},
CommunityVisibility, CommunityVisibility,
SortType, SortType,
@ -807,7 +829,7 @@ mod tests {
admin: Some(true), admin: Some(true),
..LocalUserInsertForm::test_form(inserted_person.id) ..LocalUserInsertForm::test_form(inserted_person.id)
}; };
let inserted_local_user = LocalUser::create(pool, &local_user_form).await?; let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![]).await?;
let new_bot = PersonInsertForm { let new_bot = PersonInsertForm {
bot_account: Some(true), bot_account: Some(true),
@ -833,6 +855,7 @@ mod tests {
let inserted_blocked_local_user = LocalUser::create( let inserted_blocked_local_user = LocalUser::create(
pool, pool,
&LocalUserInsertForm::test_form(inserted_blocked_person.id), &LocalUserInsertForm::test_form(inserted_blocked_person.id),
vec![],
) )
.await?; .await?;
@ -1604,6 +1627,7 @@ mod tests {
last_refreshed_at: inserted_person.last_refreshed_at, last_refreshed_at: inserted_person.last_refreshed_at,
}, },
creator_banned_from_community: false, creator_banned_from_community: false,
banned_from_community: false,
creator_is_moderator: false, creator_is_moderator: false,
creator_is_admin: true, creator_is_admin: true,
community: Community { community: Community {
@ -1707,4 +1731,67 @@ mod tests {
cleanup(data, pool).await?; cleanup(data, pool).await?;
Ok(()) Ok(())
} }
#[tokio::test]
#[serial]
async fn post_listing_local_user_banned_from_community() -> LemmyResult<()> {
let pool = &build_db_pool().await?;
let pool = &mut pool.into();
let data = init_data(pool).await?;
// Test that post 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 post_view = PostView::read(
pool,
data.inserted_post.id,
Some(inserted_banned_from_comm_local_user.person_id),
false,
)
.await?;
assert!(post_view.banned_from_community);
Person::delete(pool, inserted_banned_from_comm_person.id).await?;
cleanup(data, pool).await
}
#[tokio::test]
#[serial]
async fn post_listing_local_user_not_banned_from_community() -> LemmyResult<()> {
let pool = &build_db_pool().await?;
let pool = &mut pool.into();
let data = init_data(pool).await?;
let post_view = PostView::read(
pool,
data.inserted_post.id,
Some(data.local_user_view.person.id),
false,
)
.await?;
assert!(!post_view.banned_from_community);
cleanup(data, pool).await
}
} }

@ -106,9 +106,9 @@ impl PrivateMessageReportQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::private_message_report_view::PrivateMessageReportQuery; use crate::private_message_report_view::PrivateMessageReportQuery;
use lemmy_db_schema::{ use lemmy_db_schema::{

@ -173,9 +173,9 @@ impl PrivateMessageQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView}; use crate::{private_message_view::PrivateMessageQuery, structs::PrivateMessageView};
use lemmy_db_schema::{ use lemmy_db_schema::{

@ -127,9 +127,9 @@ impl RegistrationApplicationQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::registration_application_view::{ use crate::registration_application_view::{
RegistrationApplicationQuery, RegistrationApplicationQuery,
@ -176,7 +176,7 @@ mod tests {
.admin(Some(true)) .admin(Some(true))
.build(); .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 .await
.unwrap(); .unwrap();
@ -193,7 +193,7 @@ mod tests {
.password_encrypted("nada".to_string()) .password_encrypted("nada".to_string())
.build(); .build();
let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form) let inserted_sara_local_user = LocalUser::create(pool, &sara_local_user_form, vec![])
.await .await
.unwrap(); .unwrap();
@ -224,7 +224,7 @@ mod tests {
.password_encrypted("nada".to_string()) .password_encrypted("nada".to_string())
.build(); .build();
let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form) let inserted_jess_local_user = LocalUser::create(pool, &jess_local_user_form, vec![])
.await .await
.unwrap(); .unwrap();

@ -64,6 +64,7 @@ pub struct CommentView {
pub community: Community, pub community: Community,
pub counts: CommentAggregates, pub counts: CommentAggregates,
pub creator_banned_from_community: bool, pub creator_banned_from_community: bool,
pub banned_from_community: bool,
pub creator_is_moderator: bool, pub creator_is_moderator: bool,
pub creator_is_admin: bool, pub creator_is_admin: bool,
pub subscribed: SubscribedType, pub subscribed: SubscribedType,
@ -129,6 +130,7 @@ pub struct PostView {
pub creator: Person, pub creator: Person,
pub community: Community, pub community: Community,
pub creator_banned_from_community: bool, pub creator_banned_from_community: bool,
pub banned_from_community: bool,
pub creator_is_moderator: bool, pub creator_is_moderator: bool,
pub creator_is_admin: bool, pub creator_is_admin: bool,
pub counts: PostAggregates, pub counts: PostAggregates,

@ -50,9 +50,9 @@ impl VoteView {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::structs::VoteView; use crate::structs::VoteView;
use lemmy_db_schema::{ use lemmy_db_schema::{

@ -250,9 +250,9 @@ impl<'a> CommunityQuery<'a> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{community_view::CommunityQuery, structs::CommunityView}; use crate::{community_view::CommunityQuery, structs::CommunityView};
use lemmy_db_schema::{ use lemmy_db_schema::{
@ -296,7 +296,9 @@ mod tests {
.person_id(inserted_person.id) .person_id(inserted_person.id)
.password_encrypted(String::new()) .password_encrypted(String::new())
.build(); .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 new_community = CommunityInsertForm::builder() let new_community = CommunityInsertForm::builder()
.name("test_community_3".to_string()) .name("test_community_3".to_string())

@ -163,9 +163,9 @@ impl PersonQuery {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*; use super::*;
use diesel::NotFound; use diesel::NotFound;
@ -204,7 +204,7 @@ mod tests {
.person_id(alice.id) .person_id(alice.id)
.password_encrypted(String::new()) .password_encrypted(String::new())
.build(); .build();
let alice_local_user = LocalUser::create(pool, &alice_local_user_form).await?; let alice_local_user = LocalUser::create(pool, &alice_local_user_form, vec![]).await?;
let bob_form = PersonInsertForm::builder() let bob_form = PersonInsertForm::builder()
.name("bob".to_string()) .name("bob".to_string())
@ -218,7 +218,7 @@ mod tests {
.person_id(bob.id) .person_id(bob.id)
.password_encrypted(String::new()) .password_encrypted(String::new())
.build(); .build();
let bob_local_user = LocalUser::create(pool, &bob_local_user_form).await?; let bob_local_user = LocalUser::create(pool, &bob_local_user_form, vec![]).await?;
Ok(Data { Ok(Data {
alice, alice,

@ -46,17 +46,17 @@ static SAVE_STATE_EVERY_TIME: Duration = Duration::from_secs(60);
/// this delay limits the maximum time until the follow actually results in activities from that community id being sent to that inbox url. /// this delay limits the maximum time until the follow actually results in activities from that community id being sent to that inbox url.
/// This delay currently needs to not be too small because the DB load is currently fairly high because of the current structure of storing inboxes for every person, not having a separate list of shared_inboxes, and the architecture of having every instance queue be fully separate. /// This delay currently needs to not be too small because the DB load is currently fairly high because of the current structure of storing inboxes for every person, not having a separate list of shared_inboxes, and the architecture of having every instance queue be fully separate.
/// (see https://github.com/LemmyNet/lemmy/issues/3958) /// (see https://github.com/LemmyNet/lemmy/issues/3958)
static FOLLOW_ADDITIONS_RECHECK_DELAY: Lazy<chrono::Duration> = Lazy::new(|| { static FOLLOW_ADDITIONS_RECHECK_DELAY: Lazy<chrono::TimeDelta> = Lazy::new(|| {
if *LEMMY_TEST_FAST_FEDERATION { if *LEMMY_TEST_FAST_FEDERATION {
chrono::Duration::seconds(1) chrono::TimeDelta::try_seconds(1).expect("TimeDelta out of bounds")
} else { } else {
chrono::Duration::minutes(2) chrono::TimeDelta::try_minutes(2).expect("TimeDelta out of bounds")
} }
}); });
/// The same as FOLLOW_ADDITIONS_RECHECK_DELAY, but triggering when the last person on an instance unfollows a specific remote community. /// The same as FOLLOW_ADDITIONS_RECHECK_DELAY, but triggering when the last person on an instance unfollows a specific remote community.
/// This is expected to happen pretty rarely and updating it in a timely manner is not too important. /// This is expected to happen pretty rarely and updating it in a timely manner is not too important.
static FOLLOW_REMOVALS_RECHECK_DELAY: Lazy<chrono::Duration> = static FOLLOW_REMOVALS_RECHECK_DELAY: Lazy<chrono::TimeDelta> =
Lazy::new(|| chrono::Duration::hours(1)); Lazy::new(|| chrono::TimeDelta::try_hours(1).expect("TimeDelta out of bounds"));
pub(crate) struct InstanceWorker { pub(crate) struct InstanceWorker {
instance: Instance, instance: Instance,
// load site lazily because if an instance is first seen due to being on allowlist, // load site lazily because if an instance is first seen due to being on allowlist,
@ -332,7 +332,8 @@ impl InstanceWorker {
instance_id: InstanceId, instance_id: InstanceId,
last_fetch: DateTime<Utc>, last_fetch: DateTime<Utc>,
) -> Result<(HashMap<CommunityId, HashSet<Url>>, DateTime<Utc>)> { ) -> Result<(HashMap<CommunityId, HashSet<Url>>, DateTime<Utc>)> {
let new_last_fetch = Utc::now() - chrono::Duration::seconds(10); // update to time before fetch to ensure overlap. subtract 10s to ensure overlap even if published date is not exact let new_last_fetch =
Utc::now() - chrono::TimeDelta::try_seconds(10).expect("TimeDelta out of bounds"); // update to time before fetch to ensure overlap. subtract 10s to ensure overlap even if published date is not exact
Ok(( Ok((
CommunityFollowerView::get_instance_followed_community_inboxes(pool, instance_id, last_fetch) CommunityFollowerView::get_instance_followed_community_inboxes(pool, instance_id, last_fetch)
.await? .await?

@ -74,11 +74,11 @@ uuid = { workspace = true, features = ["serde", "v4"], optional = true }
rosetta-i18n = { workspace = true, optional = true } rosetta-i18n = { workspace = true, optional = true }
tokio = { workspace = true, optional = true } tokio = { workspace = true, optional = true }
urlencoding = { workspace = true, optional = true } urlencoding = { workspace = true, optional = true }
openssl = { version = "0.10.63", optional = true } openssl = { version = "0.10.64", optional = true }
html2text = { version = "0.6.0", optional = true } html2text = { version = "0.6.0", optional = true }
deser-hjson = { version = "2.2.4", optional = true } deser-hjson = { version = "2.2.4", optional = true }
smart-default = { version = "0.7.1", optional = true } smart-default = { version = "0.7.1", optional = true }
lettre = { version = "0.11.3", features = [ lettre = { version = "0.11.4", features = [
"tokio1", "tokio1",
"tokio1-native-tls", "tokio1-native-tls",
], optional = true } ], optional = true }

@ -135,6 +135,7 @@ pub enum LemmyErrorType {
CouldntSetAllRegistrationsAccepted, CouldntSetAllRegistrationsAccepted,
CouldntSetAllEmailVerified, CouldntSetAllEmailVerified,
Banned, Banned,
BlockedUrl,
CouldntGetComments, CouldntGetComments,
CouldntGetPosts, CouldntGetPosts,
InvalidUrl, InvalidUrl,

@ -221,9 +221,9 @@ fn parse_ip(addr: &str) -> Option<IpAddr> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
#[test] #[test]
fn test_parse_ip() { fn test_parse_ip() {

@ -158,7 +158,7 @@ impl<K: Eq + Hash, C: MapLevel> MapLevel for Map<K, C> {
// Evaluated if `some_children_remaining` is false // Evaluated if `some_children_remaining` is false
let total_has_refill_in_future = || { let total_has_refill_in_future = || {
group.total.into_iter().all(|(action_type, bucket)| { group.total.into_iter().any(|(action_type, bucket)| {
#[allow(clippy::indexing_slicing)] #[allow(clippy::indexing_slicing)]
let config = configs[action_type]; let config = configs[action_type];
bucket.update(now, config).tokens != config.capacity bucket.update(now, config).tokens != config.capacity
@ -306,9 +306,9 @@ fn split_ipv6(ip: Ipv6Addr) -> ([u8; 6], u8, u8) {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::{ActionType, BucketConfig, InstantSecs, RateLimitState, RateLimitedGroup}; use super::{ActionType, BucketConfig, InstantSecs, RateLimitState, RateLimitedGroup};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@ -416,5 +416,23 @@ mod tests {
rate_limiter.remove_full_buckets(now); rate_limiter.remove_full_buckets(now);
assert!(rate_limiter.ipv4_buckets.is_empty()); assert!(rate_limiter.ipv4_buckets.is_empty());
assert!(rate_limiter.ipv6_buckets.is_empty()); assert!(rate_limiter.ipv6_buckets.is_empty());
// `remove full buckets` should not remove empty buckets
let ip = "1.1.1.1".parse().unwrap();
// empty the bucket with 2 requests
assert!(rate_limiter.check(ActionType::Post, ip, now));
assert!(rate_limiter.check(ActionType::Post, ip, now));
rate_limiter.remove_full_buckets(now);
assert!(!rate_limiter.ipv4_buckets.is_empty());
// `remove full buckets` should not remove partial buckets
now.secs += 2;
let ip = "1.1.1.1".parse().unwrap();
// Only make one request, so bucket still has 1 token
assert!(rate_limiter.check(ActionType::Post, ip, now));
rate_limiter.remove_full_buckets(now);
assert!(!rate_limiter.ipv4_buckets.is_empty());
} }
} }

@ -1,6 +1,7 @@
use crate::settings::SETTINGS; use crate::{error::LemmyResult, settings::SETTINGS, LemmyErrorType};
use markdown_it::{plugins::cmark::inline::image::Image, MarkdownIt}; use markdown_it::{plugins::cmark::inline::image::Image, MarkdownIt};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::RegexSet;
use url::Url; use url::Url;
use urlencoding::encode; use urlencoding::encode;
@ -98,10 +99,17 @@ pub fn markdown_rewrite_image_links(mut src: String) -> (String, Vec<Url>) {
(src, links) (src, links)
} }
pub fn markdown_check_for_blocked_urls(text: &str, blocklist: &RegexSet) -> LemmyResult<()> {
if blocklist.is_match(text) {
Err(LemmyErrorType::BlockedUrl)?
}
Ok(())
}
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use super::*; use super::*;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@ -109,65 +117,65 @@ mod tests {
#[test] #[test]
fn test_basic_markdown() { fn test_basic_markdown() {
let tests: Vec<_> = vec![ let tests: Vec<_> = vec![
( (
"headings", "headings",
"# h1\n## h2\n### h3\n#### h4\n##### h5\n###### h6", "# h1\n## h2\n### h3\n#### h4\n##### h5\n###### h6",
"<h1>h1</h1>\n<h2>h2</h2>\n<h3>h3</h3>\n<h4>h4</h4>\n<h5>h5</h5>\n<h6>h6</h6>\n" "<h1>h1</h1>\n<h2>h2</h2>\n<h3>h3</h3>\n<h4>h4</h4>\n<h5>h5</h5>\n<h6>h6</h6>\n"
), ),
( (
"line breaks", "line breaks",
"First\rSecond", "First\rSecond",
"<p>First\nSecond</p>\n"), "<p>First\nSecond</p>\n"),
( (
"emphasis", "emphasis",
"__bold__ **bold** *italic* ***bold+italic***", "__bold__ **bold** *italic* ***bold+italic***",
"<p><strong>bold</strong> <strong>bold</strong> <em>italic</em> <em><strong>bold+italic</strong></em></p>\n" "<p><strong>bold</strong> <strong>bold</strong> <em>italic</em> <em><strong>bold+italic</strong></em></p>\n"
), ),
( (
"blockquotes", "blockquotes",
"> #### Hello\n > \n > - Hola\n > - 안영 \n>> Goodbye\n", "> #### Hello\n > \n > - Hola\n > - 안영 \n>> Goodbye\n",
"<blockquote>\n<h4>Hello</h4>\n<ul>\n<li>Hola</li>\n<li>안영</li>\n</ul>\n<blockquote>\n<p>Goodbye</p>\n</blockquote>\n</blockquote>\n" "<blockquote>\n<h4>Hello</h4>\n<ul>\n<li>Hola</li>\n<li>안영</li>\n</ul>\n<blockquote>\n<p>Goodbye</p>\n</blockquote>\n</blockquote>\n"
), ),
( (
"lists (ordered, unordered)", "lists (ordered, unordered)",
"1. pen\n2. apple\n3. apple pen\n- pen\n- pineapple\n- pineapple pen", "1. pen\n2. apple\n3. apple pen\n- pen\n- pineapple\n- pineapple pen",
"<ol>\n<li>pen</li>\n<li>apple</li>\n<li>apple pen</li>\n</ol>\n<ul>\n<li>pen</li>\n<li>pineapple</li>\n<li>pineapple pen</li>\n</ul>\n" "<ol>\n<li>pen</li>\n<li>apple</li>\n<li>apple pen</li>\n</ol>\n<ul>\n<li>pen</li>\n<li>pineapple</li>\n<li>pineapple pen</li>\n</ul>\n"
), ),
( (
"code and code blocks", "code and code blocks",
"this is my amazing `code snippet` and my amazing ```code block```", "this is my amazing `code snippet` and my amazing ```code block```",
"<p>this is my amazing <code>code snippet</code> and my amazing <code>code block</code></p>\n" "<p>this is my amazing <code>code snippet</code> and my amazing <code>code block</code></p>\n"
), ),
// Links with added nofollow attribute // Links with added nofollow attribute
( (
"links", "links",
"[Lemmy](https://join-lemmy.org/ \"Join Lemmy!\")", "[Lemmy](https://join-lemmy.org/ \"Join Lemmy!\")",
"<p><a href=\"https://join-lemmy.org/\" rel=\"nofollow\" title=\"Join Lemmy!\">Lemmy</a></p>\n" "<p><a href=\"https://join-lemmy.org/\" rel=\"nofollow\" title=\"Join Lemmy!\">Lemmy</a></p>\n"
), ),
// Remote images with proxy // Remote images with proxy
( (
"images", "images",
"![My linked image](https://example.com/image.png \"image alt text\")", "![My linked image](https://example.com/image.png \"image alt text\")",
"<p><img src=\"https://example.com/image.png\" alt=\"My linked image\" title=\"image alt text\" /></p>\n" "<p><img src=\"https://example.com/image.png\" alt=\"My linked image\" title=\"image alt text\" /></p>\n"
), ),
// Local images without proxy // Local images without proxy
( (
"images", "images",
"![My linked image](https://lemmy-alpha/image.png \"image alt text\")", "![My linked image](https://lemmy-alpha/image.png \"image alt text\")",
"<p><img src=\"https://lemmy-alpha/image.png\" alt=\"My linked image\" title=\"image alt text\" /></p>\n" "<p><img src=\"https://lemmy-alpha/image.png\" alt=\"My linked image\" title=\"image alt text\" /></p>\n"
), ),
// Ensure spoiler plugin is added // Ensure spoiler plugin is added
( (
"basic spoiler", "basic spoiler",
"::: spoiler click to see more\nhow spicy!\n:::\n", "::: spoiler click to see more\nhow spicy!\n:::\n",
"<details><summary>click to see more</summary><p>how spicy!\n</p></details>\n" "<details><summary>click to see more</summary><p>how spicy!\n</p></details>\n"
), ),
( (
"escape html special chars", "escape html special chars",
"<script>alert('xss');</script> hello &\"", "<script>alert('xss');</script> hello &\"",
"<p>&lt;script&gt;alert(xss);&lt;/script&gt; hello &amp;&quot;</p>\n" "<p>&lt;script&gt;alert(xss);&lt;/script&gt; hello &amp;&quot;</p>\n"
) )
]; ];
tests.iter().for_each(|&(msg, input, expected)| { tests.iter().for_each(|&(msg, input, expected)| {
let result = markdown_to_html(input); let result = markdown_to_html(input);
@ -184,46 +192,46 @@ mod tests {
fn test_markdown_proxy_images() { fn test_markdown_proxy_images() {
let tests: Vec<_> = let tests: Vec<_> =
vec![ vec![
( (
"remote image proxied", "remote image proxied",
"![link](http://example.com/image.jpg)", "![link](http://example.com/image.jpg)",
"![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)", "![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)",
), ),
( (
"local image unproxied", "local image unproxied",
"![link](http://lemmy-alpha/image.jpg)", "![link](http://lemmy-alpha/image.jpg)",
"![link](http://lemmy-alpha/image.jpg)", "![link](http://lemmy-alpha/image.jpg)",
), ),
( (
"multiple image links", "multiple image links",
"![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)", "![link](http://example.com/image1.jpg) ![link](http://example.com/image2.jpg)",
"![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)", "![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage1.jpg) ![link](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage2.jpg)",
), ),
( (
"empty link handled", "empty link handled",
"![image]()", "![image]()",
"![image]()" "![image]()"
), ),
( (
"empty label handled", "empty label handled",
"![](http://example.com/image.jpg)", "![](http://example.com/image.jpg)",
"![](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" "![](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)"
), ),
( (
"invalid image link removed", "invalid image link removed",
"![image](http-not-a-link)", "![image](http-not-a-link)",
"![image]()" "![image]()"
), ),
( (
"label with nested markdown handled", "label with nested markdown handled",
"![a *b* c](http://example.com/image.jpg)", "![a *b* c](http://example.com/image.jpg)",
"![a *b* c](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)" "![a *b* c](https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Fexample.com%2Fimage.jpg)"
), ),
( (
"custom emoji support", "custom emoji support",
r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#, r#"![party-blob](https://www.hexbear.net/pictrs/image/83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#,
r#"![party-blob](https://lemmy-alpha/api/v3/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"# r#"![party-blob](https://lemmy-alpha/api/v3/image_proxy?url=https%3A%2F%2Fwww.hexbear.net%2Fpictrs%2Fimage%2F83405746-0620-4728-9358-5f51b040ffee.gif "emoji party-blob")"#
) )
]; ];
tests.iter().for_each(|&(msg, input, expected)| { tests.iter().for_each(|&(msg, input, expected)| {
@ -237,6 +245,69 @@ mod tests {
}); });
} }
#[test]
fn test_url_blocking() {
let set = RegexSet::new(vec![r"(https://)?example\.com/?"]).unwrap();
assert!(
markdown_check_for_blocked_urls(&String::from("[](https://example.com)"), &set).is_err()
);
assert!(markdown_check_for_blocked_urls(
&String::from("Go to https://example.com to get free Robux"),
&set
)
.is_err());
assert!(
markdown_check_for_blocked_urls(&String::from("[](https://example.blog)"), &set).is_ok()
);
assert!(markdown_check_for_blocked_urls(&String::from("example.com"), &set).is_err());
assert!(markdown_check_for_blocked_urls(
"Odio exercitationem culpa sed sunt
et. Sit et similique tempora deserunt doloremque. Cupiditate iusto
repellat et quis qui. Cum veritatis facere quasi repellendus sunt
eveniet nemo sint. Cumque sit unde est. https://example.com Alias
repellendus at quos.",
&set
)
.is_err());
let set = RegexSet::new(vec![r"(https://)?example\.com/spam\.jpg"]).unwrap();
assert!(markdown_check_for_blocked_urls(
&String::from("![](https://example.com/spam.jpg)"),
&set
)
.is_err());
let set = RegexSet::new(vec![
r"(https://)?quo\.example\.com/?",
r"(https://)?foo\.example\.com/?",
r"(https://)?bar\.example\.com/?",
])
.unwrap();
assert!(
markdown_check_for_blocked_urls(&String::from("https://baz.example.com"), &set).is_ok()
);
assert!(
markdown_check_for_blocked_urls(&String::from("https://bar.example.com"), &set).is_err()
);
let set = RegexSet::new(vec![r"(https://)?example\.com/banned_page"]).unwrap();
assert!(
markdown_check_for_blocked_urls(&String::from("https://example.com/page"), &set).is_ok()
);
let set = RegexSet::new(vec![r"(https://)?ex\.mple\.com/?"]).unwrap();
assert!(markdown_check_for_blocked_urls("example.com", &set).is_ok());
}
#[test] #[test]
fn test_sanitize_html() { fn test_sanitize_html() {
let sanitized = sanitize_html("<script>alert('xss');</script> hello &\"'"); let sanitized = sanitize_html("<script>alert('xss');</script> hello &\"'");

@ -134,9 +134,9 @@ pub fn add(markdown_parser: &mut MarkdownIt) {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::utils::markdown::spoiler_rule::add; use crate::utils::markdown::spoiler_rule::add;
use markdown_it::MarkdownIt; use markdown_it::MarkdownIt;

@ -34,9 +34,9 @@ pub fn scrape_text_for_mentions(text: &str) -> Vec<MentionData> {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod test { mod test {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::utils::mention::scrape_text_for_mentions; use crate::utils::mention::scrape_text_for_mentions;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

@ -64,9 +64,9 @@ pub(crate) fn slurs_vec_to_str(slurs: &[&str]) -> String {
} }
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod test { mod test {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str}; use crate::utils::slurs::{remove_slurs, slur_check, slurs_vec_to_str};
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;

@ -1,8 +1,8 @@
use crate::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use crate::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
use itertools::Itertools; use itertools::Itertools;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::{Regex, RegexBuilder}; use regex::{Regex, RegexBuilder, RegexSet};
use url::Url; use url::{ParseError, Url};
// From here: https://github.com/vector-im/element-android/blob/develop/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt#L35 // From here: https://github.com/vector-im/element-android/blob/develop/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/MatrixPatterns.kt#L35
static VALID_MATRIX_ID_REGEX: Lazy<Regex> = Lazy::new(|| { static VALID_MATRIX_ID_REGEX: Lazy<Regex> = Lazy::new(|| {
@ -299,10 +299,37 @@ pub fn check_url_scheme(url: &Option<Url>) -> LemmyResult<()> {
} }
} }
pub fn is_url_blocked(url: &Option<Url>, blocklist: &RegexSet) -> LemmyResult<()> {
if let Some(url) = url {
if blocklist.is_match(url.as_str()) {
Err(LemmyErrorType::BlockedUrl)?
}
}
Ok(())
}
pub fn check_urls_are_valid(urls: &Vec<String>) -> LemmyResult<Vec<String>> {
let mut parsed_urls = vec![];
for url in urls {
let url = Url::parse(url).or_else(|e| {
if e == ParseError::RelativeUrlWithoutBase {
Url::parse(&format!("https://{url}"))
} else {
Err(e)
}
})?;
parsed_urls.push(url.to_string());
}
Ok(parsed_urls)
}
#[cfg(test)] #[cfg(test)]
#[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)]
mod tests { mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::indexing_slicing)]
use crate::{ use crate::{
error::LemmyErrorType, error::LemmyErrorType,
@ -310,7 +337,9 @@ mod tests {
build_and_check_regex, build_and_check_regex,
check_site_visibility_valid, check_site_visibility_valid,
check_url_scheme, check_url_scheme,
check_urls_are_valid,
clean_url_params, clean_url_params,
is_url_blocked,
is_valid_actor_name, is_valid_actor_name,
is_valid_bio_field, is_valid_bio_field,
is_valid_display_name, is_valid_display_name,
@ -550,4 +579,38 @@ mod tests {
let magnet_link="magnet:?xt=urn:btih:4b390af3891e323778959d5abfff4b726510f14c&dn=Ravel%20Complete%20Piano%20Sheet%20Music%20-%20Public%20Domain&tr=udp%3A%2F%2Fopen.tracker.cl%3A1337%2Fannounce"; let magnet_link="magnet:?xt=urn:btih:4b390af3891e323778959d5abfff4b726510f14c&dn=Ravel%20Complete%20Piano%20Sheet%20Music%20-%20Public%20Domain&tr=udp%3A%2F%2Fopen.tracker.cl%3A1337%2Fannounce";
assert!(check_url_scheme(&Some(Url::parse(magnet_link).unwrap())).is_ok()); assert!(check_url_scheme(&Some(Url::parse(magnet_link).unwrap())).is_ok());
} }
#[test]
fn test_url_block() {
let set = regex::RegexSet::new(vec![
r"(https://)?example\.org/page/to/article",
r"(https://)?example\.net/?",
r"(https://)?example\.com/?",
])
.unwrap();
assert!(is_url_blocked(&Some(Url::parse("https://example.blog").unwrap()), &set).is_ok());
assert!(is_url_blocked(&Some(Url::parse("https://example.org").unwrap()), &set).is_ok());
assert!(is_url_blocked(&None, &set).is_ok());
assert!(is_url_blocked(&Some(Url::parse("https://example.com").unwrap()), &set).is_err());
}
#[test]
fn test_url_parsed() {
assert_eq!(
vec![String::from("https://example.com/")],
check_urls_are_valid(&vec![String::from("example.com")]).unwrap()
);
assert!(check_urls_are_valid(&vec![
String::from("example.com"),
String::from("https://example.blog")
])
.is_ok());
assert!(check_urls_are_valid(&vec![String::from("https://example .com"),]).is_err());
}
} }

@ -19,6 +19,7 @@ FROM --platform=${BUILDPLATFORM} ${AMD_BUILDER_IMAGE} AS build-amd64
ARG CARGO_BUILD_FEATURES ARG CARGO_BUILD_FEATURES
ARG RUST_RELEASE_MODE ARG RUST_RELEASE_MODE
ARG RUSTFLAGS
WORKDIR /lemmy WORKDIR /lemmy
@ -48,6 +49,7 @@ FROM --platform=linux/amd64 ${ARM_BUILDER_IMAGE} AS build-arm64
ARG RUST_RELEASE_MODE ARG RUST_RELEASE_MODE
ARG CARGO_BUILD_FEATURES ARG CARGO_BUILD_FEATURES
ARG RUSTFLAGS
WORKDIR /home/lemmy/src WORKDIR /home/lemmy/src
USER 10001:10001 USER 10001:10001

@ -0,0 +1,3 @@
-- This file should undo anything in `up.sql`
DROP TABLE local_site_url_blocklist;

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

Loading…
Cancel
Save