mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-25 13:51:19 +00:00
Merge branch 'main' into release/v0.19
This commit is contained in:
commit
33df7eaf18
166 changed files with 3142 additions and 1817 deletions
|
@ -2,9 +2,14 @@
|
||||||
# See https://github.com/woodpecker-ci/woodpecker/issues/1677
|
# See https://github.com/woodpecker-ci/woodpecker/issues/1677
|
||||||
|
|
||||||
variables:
|
variables:
|
||||||
- &rust_image "rust:1.78"
|
- &rust_image "rust:1.80"
|
||||||
- &rust_nightly_image "rustlang/rust:nightly"
|
- &rust_nightly_image "rustlang/rust:nightly"
|
||||||
- &install_pnpm "corepack enable pnpm"
|
- &install_pnpm "corepack enable pnpm"
|
||||||
|
- &install_binstall "wget -O- https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz | tar -xvz -C /usr/local/cargo/bin"
|
||||||
|
- install_diesel_cli: &install_diesel_cli
|
||||||
|
- apt-get update && apt-get install -y postgresql-client
|
||||||
|
- cargo install diesel_cli --no-default-features --features postgres
|
||||||
|
- export PATH="$CARGO_HOME/bin:$PATH"
|
||||||
- &slow_check_paths
|
- &slow_check_paths
|
||||||
- event: pull_request
|
- event: pull_request
|
||||||
path:
|
path:
|
||||||
|
@ -25,17 +30,6 @@ variables:
|
||||||
"diesel.toml",
|
"diesel.toml",
|
||||||
".gitmodules",
|
".gitmodules",
|
||||||
]
|
]
|
||||||
- install_binstall: &install_binstall
|
|
||||||
- wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
|
|
||||||
- tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
|
|
||||||
- cp cargo-binstall /usr/local/cargo/bin
|
|
||||||
- install_diesel_cli: &install_diesel_cli
|
|
||||||
- apt update && apt install -y lsb-release build-essential
|
|
||||||
- sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
|
||||||
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
|
||||||
- apt update && apt install -y postgresql-client-16
|
|
||||||
- cargo install diesel_cli --no-default-features --features postgres
|
|
||||||
- export PATH="$CARGO_HOME/bin:$PATH"
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
prepare_repo:
|
prepare_repo:
|
||||||
|
@ -82,7 +76,7 @@ steps:
|
||||||
cargo_machete:
|
cargo_machete:
|
||||||
image: *rust_nightly_image
|
image: *rust_nightly_image
|
||||||
commands:
|
commands:
|
||||||
- <<: *install_binstall
|
- *install_binstall
|
||||||
- cargo binstall -y cargo-machete
|
- cargo binstall -y cargo-machete
|
||||||
- cargo machete
|
- cargo machete
|
||||||
when:
|
when:
|
||||||
|
@ -204,11 +198,6 @@ steps:
|
||||||
- <<: *install_diesel_cli
|
- <<: *install_diesel_cli
|
||||||
# Run all migrations
|
# Run all migrations
|
||||||
- diesel migration run
|
- diesel migration run
|
||||||
# Dump schema to before.sqldump (PostgreSQL apt repo is used to prevent pg_dump version mismatch error)
|
|
||||||
- apt update && apt install -y lsb-release
|
|
||||||
- sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
|
||||||
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
|
|
||||||
- apt update && apt install -y postgresql-client-16
|
|
||||||
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
|
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
|
||||||
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f before.sqldump
|
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f before.sqldump
|
||||||
# Make sure that the newest migration is revertable without the `r` schema
|
# Make sure that the newest migration is revertable without the `r` schema
|
||||||
|
@ -232,7 +221,7 @@ steps:
|
||||||
DO_WRITE_HOSTS_FILE: "1"
|
DO_WRITE_HOSTS_FILE: "1"
|
||||||
commands:
|
commands:
|
||||||
- *install_pnpm
|
- *install_pnpm
|
||||||
- apt update && apt install -y bash curl postgresql-client
|
- apt-get update && apt-get install -y bash curl postgresql-client
|
||||||
- bash api_tests/prepare-drone-federation-test.sh
|
- bash api_tests/prepare-drone-federation-test.sh
|
||||||
- cd api_tests/
|
- cd api_tests/
|
||||||
- pnpm i
|
- pnpm i
|
||||||
|
@ -279,7 +268,7 @@ steps:
|
||||||
publish_to_crates_io:
|
publish_to_crates_io:
|
||||||
image: *rust_image
|
image: *rust_image
|
||||||
commands:
|
commands:
|
||||||
- <<: *install_binstall
|
- *install_binstall
|
||||||
# Install cargo-workspaces
|
# Install cargo-workspaces
|
||||||
- cargo binstall -y cargo-workspaces
|
- cargo binstall -y cargo-workspaces
|
||||||
- cp -r migrations crates/db_schema/
|
- cp -r migrations crates/db_schema/
|
||||||
|
@ -307,7 +296,8 @@ steps:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
database:
|
database:
|
||||||
image: postgres:16-alpine
|
# 15-alpine image necessary because of diesel tests
|
||||||
|
image: pgautoupgrade/pgautoupgrade:15-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: lemmy
|
POSTGRES_USER: lemmy
|
||||||
POSTGRES_PASSWORD: password
|
POSTGRES_PASSWORD: password
|
||||||
|
|
892
Cargo.lock
generated
892
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
73
Cargo.toml
73
Cargo.toml
|
@ -1,5 +1,5 @@
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.19.5"
|
version = "0.19.6-beta.7"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "A link aggregator for the fediverse"
|
description = "A link aggregator for the fediverse"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
|
@ -86,28 +86,29 @@ suspicious = { level = "deny", priority = -1 }
|
||||||
uninlined_format_args = "allow"
|
uninlined_format_args = "allow"
|
||||||
unused_self = "deny"
|
unused_self = "deny"
|
||||||
unwrap_used = "deny"
|
unwrap_used = "deny"
|
||||||
|
unimplemented = "deny"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
lemmy_api = { version = "=0.19.5", path = "./crates/api" }
|
lemmy_api = { version = "=0.19.6-beta.7", path = "./crates/api" }
|
||||||
lemmy_api_crud = { version = "=0.19.5", path = "./crates/api_crud" }
|
lemmy_api_crud = { version = "=0.19.6-beta.7", path = "./crates/api_crud" }
|
||||||
lemmy_apub = { version = "=0.19.5", path = "./crates/apub" }
|
lemmy_apub = { version = "=0.19.6-beta.7", path = "./crates/apub" }
|
||||||
lemmy_utils = { version = "=0.19.5", path = "./crates/utils", default-features = false }
|
lemmy_utils = { version = "=0.19.6-beta.7", path = "./crates/utils", default-features = false }
|
||||||
lemmy_db_schema = { version = "=0.19.5", path = "./crates/db_schema" }
|
lemmy_db_schema = { version = "=0.19.6-beta.7", path = "./crates/db_schema" }
|
||||||
lemmy_api_common = { version = "=0.19.5", path = "./crates/api_common" }
|
lemmy_api_common = { version = "=0.19.6-beta.7", path = "./crates/api_common" }
|
||||||
lemmy_routes = { version = "=0.19.5", path = "./crates/routes" }
|
lemmy_routes = { version = "=0.19.6-beta.7", path = "./crates/routes" }
|
||||||
lemmy_db_views = { version = "=0.19.5", path = "./crates/db_views" }
|
lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" }
|
||||||
lemmy_db_views_actor = { version = "=0.19.5", path = "./crates/db_views_actor" }
|
lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" }
|
||||||
lemmy_db_views_moderator = { version = "=0.19.5", path = "./crates/db_views_moderator" }
|
lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" }
|
||||||
lemmy_federate = { version = "=0.19.5", path = "./crates/federate" }
|
lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" }
|
||||||
activitypub_federation = { version = "0.5.6", default-features = false, features = [
|
activitypub_federation = { version = "0.5.8", default-features = false, features = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
] }
|
] }
|
||||||
diesel = "2.1.6"
|
diesel = "2.1.6"
|
||||||
diesel_migrations = "2.1.0"
|
diesel_migrations = "2.1.0"
|
||||||
diesel-async = "0.4.1"
|
diesel-async = "0.4.1"
|
||||||
serde = { version = "1.0.203", features = ["derive"] }
|
serde = { version = "1.0.204", features = ["derive"] }
|
||||||
serde_with = "3.8.1"
|
serde_with = "3.9.0"
|
||||||
actix-web = { version = "4.6.0", default-features = false, features = [
|
actix-web = { version = "4.8.0", default-features = false, features = [
|
||||||
"macros",
|
"macros",
|
||||||
"rustls-0_23",
|
"rustls-0_23",
|
||||||
"compress-brotli",
|
"compress-brotli",
|
||||||
|
@ -120,32 +121,35 @@ tracing-actix-web = { version = "0.7.11", 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.2", features = ["serde"] }
|
||||||
reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] }
|
reqwest = { version = "0.11.27", default-features = false, features = [
|
||||||
|
"json",
|
||||||
|
"blocking",
|
||||||
|
"gzip",
|
||||||
|
"rustls-tls",
|
||||||
|
] }
|
||||||
reqwest-middleware = "0.2.5"
|
reqwest-middleware = "0.2.5"
|
||||||
reqwest-tracing = "0.4.8"
|
reqwest-tracing = "0.4.8"
|
||||||
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.1"
|
bcrypt = "0.15.1"
|
||||||
chrono = { version = "0.4.38", features = ["serde"], default-features = false }
|
chrono = { version = "0.4.38", features = ["serde"], default-features = false }
|
||||||
serde_json = { version = "1.0.117", features = ["preserve_order"] }
|
serde_json = { version = "1.0.121", features = ["preserve_order"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
uuid = { version = "1.8.0", features = ["serde", "v4"] }
|
uuid = { version = "1.10.0", features = ["serde", "v4"] }
|
||||||
async-trait = "0.1.80"
|
async-trait = "0.1.81"
|
||||||
captcha = "0.0.9"
|
captcha = "0.0.9"
|
||||||
anyhow = { version = "1.0.86", features = [
|
anyhow = { version = "1.0.86", 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.2"
|
typed-builder = "0.19.1"
|
||||||
serial_test = "3.1.1"
|
serial_test = "3.1.1"
|
||||||
tokio = { version = "1.38.0", features = ["full"] }
|
tokio = { version = "1.39.2", features = ["full"] }
|
||||||
regex = "1.10.4"
|
regex = "1.10.5"
|
||||||
once_cell = "1.19.0"
|
|
||||||
diesel-derive-newtype = "2.1.2"
|
diesel-derive-newtype = "2.1.2"
|
||||||
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
||||||
strum = "0.26.2"
|
strum = { version = "0.26.3", features = ["derive"] }
|
||||||
strum_macros = "0.26.4"
|
|
||||||
itertools = "0.13.0"
|
itertools = "0.13.0"
|
||||||
futures = "0.3.30"
|
futures = "0.3.30"
|
||||||
http = "0.2.12"
|
http = "0.2.12"
|
||||||
|
@ -157,17 +161,17 @@ ts-rs = { version = "7.1.1", features = [
|
||||||
"chrono-impl",
|
"chrono-impl",
|
||||||
"no-serde-warnings",
|
"no-serde-warnings",
|
||||||
] }
|
] }
|
||||||
rustls = { version = "0.23.9", features = ["ring"] }
|
rustls = { version = "0.23.12", features = ["ring"] }
|
||||||
futures-util = "0.3.30"
|
futures-util = "0.3.30"
|
||||||
tokio-postgres = "0.7.10"
|
tokio-postgres = "0.7.11"
|
||||||
tokio-postgres-rustls = "0.12.0"
|
tokio-postgres-rustls = "0.12.0"
|
||||||
urlencoding = "2.1.3"
|
urlencoding = "2.1.3"
|
||||||
enum-map = "2.7"
|
enum-map = "2.7"
|
||||||
moka = { version = "0.12.7", features = ["future"] }
|
moka = { version = "0.12.8", features = ["future"] }
|
||||||
i-love-jesus = { version = "0.1.0" }
|
i-love-jesus = { version = "0.1.0" }
|
||||||
clap = { version = "4.5.6", features = ["derive", "env"] }
|
clap = { version = "4.5.13", features = ["derive", "env"] }
|
||||||
pretty_assertions = "1.4.0"
|
pretty_assertions = "1.4.0"
|
||||||
derive-new = "0.6.0"
|
derive-new = "0.7.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lemmy_api = { workspace = true }
|
lemmy_api = { workspace = true }
|
||||||
|
@ -195,9 +199,10 @@ clokwerk = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tracing-opentelemetry = { workspace = true, optional = true }
|
tracing-opentelemetry = { workspace = true, optional = true }
|
||||||
opentelemetry = { workspace = true, optional = true }
|
opentelemetry = { workspace = true, optional = true }
|
||||||
console-subscriber = { version = "0.3.0", optional = true }
|
console-subscriber = { version = "0.4.0", optional = true }
|
||||||
opentelemetry-otlp = { version = "0.12.0", optional = true }
|
opentelemetry-otlp = { version = "0.12.0", optional = true }
|
||||||
pict-rs = { version = "0.5.15", optional = true }
|
pict-rs = { version = "0.5.16", optional = true }
|
||||||
|
rustls = { workspace = true }
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
actix-cors = "0.7.0"
|
actix-cors = "0.7.0"
|
||||||
futures-util = { workspace = true }
|
futures-util = { workspace = true }
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
{
|
|
||||||
"root": true,
|
|
||||||
"env": {
|
|
||||||
"browser": true
|
|
||||||
},
|
|
||||||
"plugins": ["@typescript-eslint"],
|
|
||||||
"extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"project": "./tsconfig.json",
|
|
||||||
"warnOnUnsupportedTypeScriptVersion": false
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/ban-ts-comment": 0,
|
|
||||||
"@typescript-eslint/no-explicit-any": 0,
|
|
||||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
|
||||||
"@typescript-eslint/no-var-requires": 0,
|
|
||||||
"arrow-body-style": 0,
|
|
||||||
"curly": 0,
|
|
||||||
"eol-last": 0,
|
|
||||||
"eqeqeq": 0,
|
|
||||||
"func-style": 0,
|
|
||||||
"import/no-duplicates": 0,
|
|
||||||
"max-statements": 0,
|
|
||||||
"max-params": 0,
|
|
||||||
"new-cap": 0,
|
|
||||||
"no-console": 0,
|
|
||||||
"no-duplicate-imports": 0,
|
|
||||||
"no-extra-parens": 0,
|
|
||||||
"no-return-assign": 0,
|
|
||||||
"no-throw-literal": 0,
|
|
||||||
"no-trailing-spaces": 0,
|
|
||||||
"no-unused-expressions": 0,
|
|
||||||
"no-useless-constructor": 0,
|
|
||||||
"no-useless-escape": 0,
|
|
||||||
"no-var": 0,
|
|
||||||
"prefer-const": 0,
|
|
||||||
"prefer-rest-params": 0,
|
|
||||||
"quote-props": 0,
|
|
||||||
"unicorn/filename-case": 0
|
|
||||||
}
|
|
||||||
}
|
|
56
api_tests/eslint.config.mjs
Normal file
56
api_tests/eslint.config.mjs
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import pluginJs from "@eslint/js";
|
||||||
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
pluginJs.configs.recommended,
|
||||||
|
...tseslint.configs.recommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
parser: tseslint.parser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// For some reason this has to be in its own block
|
||||||
|
{
|
||||||
|
ignores: [
|
||||||
|
"putTypesInIndex.js",
|
||||||
|
"dist/*",
|
||||||
|
"docs/*",
|
||||||
|
".yalc",
|
||||||
|
"jest.config.js",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
files: ["src/**/*"],
|
||||||
|
rules: {
|
||||||
|
"@typescript-eslint/no-empty-interface": 0,
|
||||||
|
"@typescript-eslint/no-empty-function": 0,
|
||||||
|
"@typescript-eslint/ban-ts-comment": 0,
|
||||||
|
"@typescript-eslint/no-explicit-any": 0,
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||||
|
"@typescript-eslint/no-var-requires": 0,
|
||||||
|
"arrow-body-style": 0,
|
||||||
|
curly: 0,
|
||||||
|
"eol-last": 0,
|
||||||
|
eqeqeq: 0,
|
||||||
|
"func-style": 0,
|
||||||
|
"import/no-duplicates": 0,
|
||||||
|
"max-statements": 0,
|
||||||
|
"max-params": 0,
|
||||||
|
"new-cap": 0,
|
||||||
|
"no-console": 0,
|
||||||
|
"no-duplicate-imports": 0,
|
||||||
|
"no-extra-parens": 0,
|
||||||
|
"no-return-assign": 0,
|
||||||
|
"no-throw-literal": 0,
|
||||||
|
"no-trailing-spaces": 0,
|
||||||
|
"no-unused-expressions": 0,
|
||||||
|
"no-useless-constructor": 0,
|
||||||
|
"no-useless-escape": 0,
|
||||||
|
"no-var": 0,
|
||||||
|
"prefer-const": 0,
|
||||||
|
"prefer-rest-params": 0,
|
||||||
|
"quote-props": 0,
|
||||||
|
"unicorn/filename-case": 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
|
@ -6,9 +6,9 @@
|
||||||
"repository": "https://github.com/LemmyNet/lemmy",
|
"repository": "https://github.com/LemmyNet/lemmy",
|
||||||
"author": "Dessalines",
|
"author": "Dessalines",
|
||||||
"license": "AGPL-3.0",
|
"license": "AGPL-3.0",
|
||||||
"packageManager": "pnpm@9.4.0",
|
"packageManager": "pnpm@9.9.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'",
|
"lint": "tsc --noEmit && eslint --report-unused-disable-directives && prettier --check 'src/**/*.ts'",
|
||||||
"fix": "prettier --write src && eslint --fix src",
|
"fix": "prettier --write src && eslint --fix src",
|
||||||
"api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ",
|
"api-test": "jest -i follow.spec.ts && jest -i image.spec.ts && jest -i user.spec.ts && jest -i private_message.spec.ts && jest -i community.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts ",
|
||||||
"api-test-follow": "jest -i follow.spec.ts",
|
"api-test-follow": "jest -i follow.spec.ts",
|
||||||
|
@ -21,16 +21,16 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^20.12.4",
|
"@types/node": "^22.0.2",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^7.5.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"download-file-sync": "^1.0.4",
|
"eslint": "^9.8.0",
|
||||||
"eslint": "^9.0.0",
|
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"lemmy-js-client": "0.19.4",
|
"lemmy-js-client": "0.19.5-alpha.1",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"ts-jest": "^29.1.0",
|
"ts-jest": "^29.1.0",
|
||||||
"typescript": "^5.4.4"
|
"typescript": "^5.5.4",
|
||||||
|
"typescript-eslint": "^8.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -15,7 +15,7 @@ export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queu
|
||||||
|
|
||||||
# pictrs setup
|
# pictrs setup
|
||||||
if [ ! -f "api_tests/pict-rs" ]; then
|
if [ ! -f "api_tests/pict-rs" ]; then
|
||||||
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.13/pict-rs-linux-amd64" -o api_tests/pict-rs
|
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.16/pict-rs-linux-amd64" -o api_tests/pict-rs
|
||||||
chmod +x api_tests/pict-rs
|
chmod +x api_tests/pict-rs
|
||||||
fi
|
fi
|
||||||
./api_tests/pict-rs \
|
./api_tests/pict-rs \
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
|
import { AddModToCommunity } from "lemmy-js-client/dist/types/AddModToCommunity";
|
||||||
import { CommunityView } from "lemmy-js-client/dist/types/CommunityView";
|
import { CommunityView } from "lemmy-js-client/dist/types/CommunityView";
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
|
@ -9,6 +10,7 @@ import {
|
||||||
resolveCommunity,
|
resolveCommunity,
|
||||||
createCommunity,
|
createCommunity,
|
||||||
deleteCommunity,
|
deleteCommunity,
|
||||||
|
delay,
|
||||||
removeCommunity,
|
removeCommunity,
|
||||||
getCommunity,
|
getCommunity,
|
||||||
followCommunity,
|
followCommunity,
|
||||||
|
@ -533,3 +535,41 @@ test("Content in local-only community doesn't federate", async () => {
|
||||||
Error("couldnt_find_object"),
|
Error("couldnt_find_object"),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Remote mods can edit communities", async () => {
|
||||||
|
let communityRes = await createCommunity(alpha);
|
||||||
|
|
||||||
|
let betaCommunity = await resolveCommunity(
|
||||||
|
beta,
|
||||||
|
communityRes.community_view.community.actor_id,
|
||||||
|
);
|
||||||
|
if (!betaCommunity.community) {
|
||||||
|
throw "Missing beta community";
|
||||||
|
}
|
||||||
|
let betaOnAlpha = await resolvePerson(alpha, "lemmy_beta@lemmy-beta:8551");
|
||||||
|
|
||||||
|
let form: AddModToCommunity = {
|
||||||
|
community_id: communityRes.community_view.community.id,
|
||||||
|
person_id: betaOnAlpha.person?.person.id as number,
|
||||||
|
added: true,
|
||||||
|
};
|
||||||
|
alpha.addModToCommunity(form);
|
||||||
|
|
||||||
|
let form2: EditCommunity = {
|
||||||
|
community_id: betaCommunity.community?.community.id as number,
|
||||||
|
description: "Example description",
|
||||||
|
};
|
||||||
|
|
||||||
|
await editCommunity(beta, form2);
|
||||||
|
// give alpha time to get and process the edit
|
||||||
|
await delay(1000);
|
||||||
|
|
||||||
|
let alphaCommunity = await getCommunity(
|
||||||
|
alpha,
|
||||||
|
communityRes.community_view.community.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(alphaCommunity.community_view.community.description).toBe(
|
||||||
|
"Example description",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
|
@ -33,7 +33,6 @@ import {
|
||||||
sampleImage,
|
sampleImage,
|
||||||
sampleSite,
|
sampleSite,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
const downloadFileSync = require("download-file-sync");
|
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
beforeAll(setupLogins);
|
||||||
|
|
||||||
|
@ -57,7 +56,8 @@ test("Upload image and delete it", async () => {
|
||||||
expect(upload.delete_url).toBeDefined();
|
expect(upload.delete_url).toBeDefined();
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
// ensure that image download is working. theres probably a better way to do this
|
||||||
const content = downloadFileSync(upload.url);
|
const response = await fetch(upload.url ?? "");
|
||||||
|
const content = await response.text();
|
||||||
expect(content.length).toBeGreaterThan(0);
|
expect(content.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// Ensure that it comes back with the list_media endpoint
|
// Ensure that it comes back with the list_media endpoint
|
||||||
|
@ -92,7 +92,8 @@ test("Upload image and delete it", async () => {
|
||||||
expect(delete_).toBe(true);
|
expect(delete_).toBe(true);
|
||||||
|
|
||||||
// ensure that image is deleted
|
// ensure that image is deleted
|
||||||
const content2 = downloadFileSync(upload.url);
|
const response2 = await fetch(upload.url ?? "");
|
||||||
|
const content2 = await response2.text();
|
||||||
expect(content2).toBe("");
|
expect(content2).toBe("");
|
||||||
|
|
||||||
// Ensure that it shows the image is deleted
|
// Ensure that it shows the image is deleted
|
||||||
|
@ -120,7 +121,8 @@ test("Purge user, uploaded image removed", async () => {
|
||||||
expect(upload.delete_url).toBeDefined();
|
expect(upload.delete_url).toBeDefined();
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
// ensure that image download is working. theres probably a better way to do this
|
||||||
const content = downloadFileSync(upload.url);
|
const response = await fetch(upload.url ?? "");
|
||||||
|
const content = await response.text();
|
||||||
expect(content.length).toBeGreaterThan(0);
|
expect(content.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
// purge user
|
// purge user
|
||||||
|
@ -132,7 +134,8 @@ test("Purge user, uploaded image removed", async () => {
|
||||||
expect(delete_.success).toBe(true);
|
expect(delete_.success).toBe(true);
|
||||||
|
|
||||||
// ensure that image is deleted
|
// ensure that image is deleted
|
||||||
const content2 = downloadFileSync(upload.url);
|
const response2 = await fetch(upload.url ?? "");
|
||||||
|
const content2 = await response2.text();
|
||||||
expect(content2).toBe("");
|
expect(content2).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -150,7 +153,8 @@ test("Purge post, linked image removed", async () => {
|
||||||
expect(upload.delete_url).toBeDefined();
|
expect(upload.delete_url).toBeDefined();
|
||||||
|
|
||||||
// ensure that image download is working. theres probably a better way to do this
|
// ensure that image download is working. theres probably a better way to do this
|
||||||
const content = downloadFileSync(upload.url);
|
const response = await fetch(upload.url ?? "");
|
||||||
|
const content = await response.text();
|
||||||
expect(content.length).toBeGreaterThan(0);
|
expect(content.length).toBeGreaterThan(0);
|
||||||
|
|
||||||
let community = await resolveBetaCommunity(user);
|
let community = await resolveBetaCommunity(user);
|
||||||
|
@ -160,6 +164,7 @@ test("Purge post, linked image removed", async () => {
|
||||||
upload.url,
|
upload.url,
|
||||||
);
|
);
|
||||||
expect(post.post_view.post.url).toBe(upload.url);
|
expect(post.post_view.post.url).toBe(upload.url);
|
||||||
|
expect(post.post_view.image_details).toBeDefined();
|
||||||
|
|
||||||
// purge post
|
// purge post
|
||||||
const purgeForm: PurgePost = {
|
const purgeForm: PurgePost = {
|
||||||
|
@ -169,7 +174,8 @@ test("Purge post, linked image removed", async () => {
|
||||||
expect(delete_.success).toBe(true);
|
expect(delete_.success).toBe(true);
|
||||||
|
|
||||||
// ensure that image is deleted
|
// ensure that image is deleted
|
||||||
const content2 = downloadFileSync(upload.url);
|
const response2 = await fetch(upload.url ?? "");
|
||||||
|
const content2 = await response2.text();
|
||||||
expect(content2).toBe("");
|
expect(content2).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -184,6 +190,9 @@ test("Images in remote image post are proxied if setting enabled", async () => {
|
||||||
const post = postRes.post_view.post;
|
const post = postRes.post_view.post;
|
||||||
expect(post).toBeDefined();
|
expect(post).toBeDefined();
|
||||||
|
|
||||||
|
// Make sure it fetched the image details
|
||||||
|
expect(postRes.post_view.image_details).toBeDefined();
|
||||||
|
|
||||||
// remote image gets proxied after upload
|
// remote image gets proxied after upload
|
||||||
expect(
|
expect(
|
||||||
post.thumbnail_url?.startsWith(
|
post.thumbnail_url?.startsWith(
|
||||||
|
|
|
@ -512,7 +512,7 @@ test("Enforce site ban federation for local user", async () => {
|
||||||
}
|
}
|
||||||
let newAlphaUserJwt = await loginUser(alpha, alphaUserPerson.name);
|
let newAlphaUserJwt = await loginUser(alpha, alphaUserPerson.name);
|
||||||
alphaUserHttp.setHeaders({
|
alphaUserHttp.setHeaders({
|
||||||
Authorization: "Bearer " + newAlphaUserJwt.jwt ?? "",
|
Authorization: "Bearer " + newAlphaUserJwt.jwt,
|
||||||
});
|
});
|
||||||
// alpha makes new post in beta community, it federates
|
// alpha makes new post in beta community, it federates
|
||||||
let postRes2 = await createPost(alphaUserHttp, betaCommunity!.community.id);
|
let postRes2 = await createPost(alphaUserHttp, betaCommunity!.community.id);
|
||||||
|
|
|
@ -197,7 +197,7 @@ export async function setupLogins() {
|
||||||
// (because last_successful_id is set to current id when federation to an instance is first started)
|
// (because last_successful_id is set to current id when federation to an instance is first started)
|
||||||
// only needed the first time so do in this try
|
// only needed the first time so do in this try
|
||||||
await delay(10_000);
|
await delay(10_000);
|
||||||
} catch (_) {
|
} catch {
|
||||||
console.log("Communities already exist");
|
console.log("Communities already exist");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -899,7 +899,6 @@ export async function deleteAllImages(api: LemmyHttp) {
|
||||||
const imagesRes = await api.listAllMedia({
|
const imagesRes = await api.listAllMedia({
|
||||||
limit: imageFetchLimit,
|
limit: imageFetchLimit,
|
||||||
});
|
});
|
||||||
imagesRes.images;
|
|
||||||
Promise.all(
|
Promise.all(
|
||||||
imagesRes.images
|
imagesRes.images
|
||||||
.map(image => {
|
.map(image => {
|
||||||
|
|
|
@ -26,6 +26,7 @@ body = """
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
|
||||||
|
{%- if github -%}
|
||||||
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %}
|
||||||
{% raw %}\n{% endraw -%}
|
{% raw %}\n{% endraw -%}
|
||||||
## New Contributors
|
## New Contributors
|
||||||
|
@ -36,6 +37,7 @@ body = """
|
||||||
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
[#{{ contributor.pr_number }}]({{ self::remote_url() }}/pull/{{ contributor.pr_number }}) \
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
{%- endfor -%}
|
{%- endfor -%}
|
||||||
|
{%- endif -%}
|
||||||
|
|
||||||
{% if version %}
|
{% if version %}
|
||||||
{% if previous.version %}
|
{% if previous.version %}
|
||||||
|
@ -70,6 +72,7 @@ commit_preprocessors = [
|
||||||
# remove issue numbers from commits
|
# remove issue numbers from commits
|
||||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "" },
|
||||||
]
|
]
|
||||||
|
commit_parsers = [{ field = "author.name", pattern = "renovate", skip = true }]
|
||||||
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||||
protect_breaking_commits = false
|
protect_breaking_commits = false
|
||||||
# filter out the commits that are not matched by commit parsers
|
# filter out the commits that are not matched by commit parsers
|
||||||
|
|
|
@ -35,11 +35,12 @@ chrono = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
hound = "3.5.1"
|
hound = "3.5.1"
|
||||||
sitemap-rs = "0.2.1"
|
sitemap-rs = "0.2.1"
|
||||||
totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] }
|
totp-rs = { version = "5.6.0", features = ["gen_secret", "otpauth"] }
|
||||||
actix-web-httpauth = "0.8.1"
|
actix-web-httpauth = "0.8.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = { workspace = true }
|
serial_test = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
elementtree = "1.2.3"
|
elementtree = "1.2.3"
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
|
lemmy_api_crud = { workspace = true }
|
||||||
|
|
|
@ -17,9 +17,13 @@ pub async fn distinguish_comment(
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
local_user_view: LocalUserView,
|
local_user_view: LocalUserView,
|
||||||
) -> LemmyResult<Json<CommentResponse>> {
|
) -> LemmyResult<Json<CommentResponse>> {
|
||||||
let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None)
|
let orig_comment = CommentView::read(
|
||||||
.await?
|
&mut context.pool(),
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
data.comment_id,
|
||||||
|
Some(&local_user_view.local_user),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
|
|
||||||
check_community_user_action(
|
check_community_user_action(
|
||||||
&local_user_view.person,
|
&local_user_view.person,
|
||||||
|
@ -54,7 +58,7 @@ pub async fn distinguish_comment(
|
||||||
let comment_view = CommentView::read(
|
let comment_view = CommentView::read(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
data.comment_id,
|
data.comment_id,
|
||||||
Some(local_user_view.person.id),
|
Some(&local_user_view.local_user),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
|
|
|
@ -35,9 +35,13 @@ pub async fn like_comment(
|
||||||
check_bot_account(&local_user_view.person)?;
|
check_bot_account(&local_user_view.person)?;
|
||||||
|
|
||||||
let comment_id = data.comment_id;
|
let comment_id = data.comment_id;
|
||||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
|
let orig_comment = CommentView::read(
|
||||||
.await?
|
&mut context.pool(),
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
comment_id,
|
||||||
|
Some(&local_user_view.local_user),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
|
|
||||||
check_community_user_action(
|
check_community_user_action(
|
||||||
&local_user_view.person,
|
&local_user_view.person,
|
||||||
|
|
|
@ -17,7 +17,7 @@ pub async fn list_comment_likes(
|
||||||
let comment_view = CommentView::read(
|
let comment_view = CommentView::read(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
data.comment_id,
|
data.comment_id,
|
||||||
Some(local_user_view.person.id),
|
Some(&local_user_view.local_user),
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
|
|
|
@ -32,10 +32,13 @@ pub async fn save_comment(
|
||||||
}
|
}
|
||||||
|
|
||||||
let comment_id = data.comment_id;
|
let comment_id = data.comment_id;
|
||||||
let person_id = local_user_view.person.id;
|
let comment_view = CommentView::read(
|
||||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id))
|
&mut context.pool(),
|
||||||
.await?
|
comment_id,
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
Some(&local_user_view.local_user),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
|
|
||||||
Ok(Json(CommentResponse {
|
Ok(Json(CommentResponse {
|
||||||
comment_view,
|
comment_view,
|
||||||
|
|
|
@ -35,9 +35,13 @@ pub async fn create_comment_report(
|
||||||
|
|
||||||
let person_id = local_user_view.person.id;
|
let person_id = local_user_view.person.id;
|
||||||
let comment_id = data.comment_id;
|
let comment_id = data.comment_id;
|
||||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
|
let comment_view = CommentView::read(
|
||||||
.await?
|
&mut context.pool(),
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
comment_id,
|
||||||
|
Some(&local_user_view.local_user),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
|
|
||||||
check_community_user_action(
|
check_community_user_action(
|
||||||
&local_user_view.person,
|
&local_user_view.person,
|
||||||
|
|
|
@ -9,6 +9,7 @@ use lemmy_api_common::{
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
community::{Community, CommunityModerator, CommunityModeratorForm},
|
community::{Community, CommunityModerator, CommunityModeratorForm},
|
||||||
|
local_user::LocalUser,
|
||||||
moderator::{ModAddCommunity, ModAddCommunityForm},
|
moderator::{ModAddCommunity, ModAddCommunityForm},
|
||||||
},
|
},
|
||||||
traits::{Crud, Joinable},
|
traits::{Crud, Joinable},
|
||||||
|
@ -33,6 +34,18 @@ pub async fn add_mod_to_community(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
// If its a mod removal, also check that you're a higher mod.
|
||||||
|
if !data.added {
|
||||||
|
LocalUser::is_higher_mod_or_admin_check(
|
||||||
|
&mut context.pool(),
|
||||||
|
community_id,
|
||||||
|
local_user_view.person.id,
|
||||||
|
vec![data.person_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
let community = Community::read(&mut context.pool(), community_id)
|
let community = Community::read(&mut context.pool(), community_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||||
|
|
|
@ -14,6 +14,7 @@ use lemmy_db_schema::{
|
||||||
CommunityPersonBan,
|
CommunityPersonBan,
|
||||||
CommunityPersonBanForm,
|
CommunityPersonBanForm,
|
||||||
},
|
},
|
||||||
|
local_user::LocalUser,
|
||||||
moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
|
moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
|
||||||
},
|
},
|
||||||
traits::{Bannable, Crud, Followable},
|
traits::{Bannable, Crud, Followable},
|
||||||
|
@ -44,6 +45,14 @@ pub async fn ban_from_community(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
LocalUser::is_higher_mod_or_admin_check(
|
||||||
|
&mut context.pool(),
|
||||||
|
data.community_id,
|
||||||
|
local_user_view.person.id,
|
||||||
|
vec![data.person_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if let Some(reason) = &data.reason {
|
if let Some(reason) = &data.reason {
|
||||||
is_valid_body_field(reason, false)?;
|
is_valid_body_field(reason, false)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -50,10 +50,14 @@ pub async fn block_community(
|
||||||
.with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
|
.with_lemmy_type(LemmyErrorType::CommunityBlockAlreadyExists)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_view =
|
let community_view = CommunityView::read(
|
||||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
&mut context.pool(),
|
||||||
.await?
|
community_id,
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
Some(&local_user_view.local_user),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
ActivityChannel::submit_activity(
|
||||||
SendActivityData::FollowCommunity(
|
SendActivityData::FollowCommunity(
|
||||||
|
|
|
@ -62,11 +62,14 @@ pub async fn follow_community(
|
||||||
}
|
}
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let community_id = data.community_id;
|
||||||
let person_id = local_user_view.person.id;
|
let community_view = CommunityView::read(
|
||||||
let community_view =
|
&mut context.pool(),
|
||||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
community_id,
|
||||||
.await?
|
Some(&local_user_view.local_user),
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
false,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||||
|
|
||||||
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
|
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
|
||||||
|
|
||||||
|
|
|
@ -76,11 +76,14 @@ pub async fn transfer_community(
|
||||||
ModTransferCommunity::create(&mut context.pool(), &form).await?;
|
ModTransferCommunity::create(&mut context.pool(), &form).await?;
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let community_id = data.community_id;
|
||||||
let person_id = local_user_view.person.id;
|
let community_view = CommunityView::read(
|
||||||
let community_view =
|
&mut context.pool(),
|
||||||
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
|
community_id,
|
||||||
.await?
|
Some(&local_user_view.local_user),
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
false,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||||
|
|
||||||
let community_id = data.community_id;
|
let community_id = data.community_id;
|
||||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id)
|
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id)
|
||||||
|
|
|
@ -24,6 +24,16 @@ pub async fn add_admin(
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
is_admin(&local_user_view)?;
|
is_admin(&local_user_view)?;
|
||||||
|
|
||||||
|
// If its an admin removal, also check that you're a higher admin
|
||||||
|
if !data.added {
|
||||||
|
LocalUser::is_higher_admin_check(
|
||||||
|
&mut context.pool(),
|
||||||
|
local_user_view.person.id,
|
||||||
|
vec![data.person_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure that the person_id added is local
|
// Make sure that the person_id added is local
|
||||||
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
|
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
|
||||||
.await?
|
.await?
|
||||||
|
|
|
@ -9,6 +9,7 @@ use lemmy_api_common::{
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
|
local_user::LocalUser,
|
||||||
login_token::LoginToken,
|
login_token::LoginToken,
|
||||||
moderator::{ModBan, ModBanForm},
|
moderator::{ModBan, ModBanForm},
|
||||||
person::{Person, PersonUpdateForm},
|
person::{Person, PersonUpdateForm},
|
||||||
|
@ -31,6 +32,14 @@ pub async fn ban_from_site(
|
||||||
// Make sure user is an admin
|
// Make sure user is an admin
|
||||||
is_admin(&local_user_view)?;
|
is_admin(&local_user_view)?;
|
||||||
|
|
||||||
|
// Also make sure you're a higher admin than the target
|
||||||
|
LocalUser::is_higher_admin_check(
|
||||||
|
&mut context.pool(),
|
||||||
|
local_user_view.person.id,
|
||||||
|
vec![data.person_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if let Some(reason) = &data.reason {
|
if let Some(reason) = &data.reason {
|
||||||
is_valid_body_field(reason, false)?;
|
is_valid_body_field(reason, false)?;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,9 @@ use lemmy_api_common::{
|
||||||
utils::send_new_applicant_email_to_admins,
|
utils::send_new_applicant_email_to_admins,
|
||||||
SuccessResponse,
|
SuccessResponse,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::source::{
|
||||||
source::{
|
email_verification::EmailVerification,
|
||||||
email_verification::EmailVerification,
|
local_user::{LocalUser, LocalUserUpdateForm},
|
||||||
local_user::{LocalUser, LocalUserUpdateForm},
|
|
||||||
},
|
|
||||||
RegistrationMode,
|
|
||||||
};
|
};
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||||
|
@ -41,9 +38,7 @@ pub async fn verify_email(
|
||||||
EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?;
|
EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?;
|
||||||
|
|
||||||
// send out notification about registration application to admins if enabled
|
// send out notification about registration application to admins if enabled
|
||||||
if site_view.local_site.registration_mode == RegistrationMode::RequireApplication
|
if site_view.local_site.application_email_admins {
|
||||||
&& site_view.local_site.application_email_admins
|
|
||||||
{
|
|
||||||
let local_user = LocalUserView::read(&mut context.pool(), local_user_id)
|
let local_user = LocalUserView::read(&mut context.pool(), local_user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
||||||
|
|
|
@ -72,11 +72,5 @@ pub async fn feature_post(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
build_post_response(
|
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
|
||||||
&context,
|
|
||||||
orig_post.community_id,
|
|
||||||
&local_user_view.person,
|
|
||||||
post_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use lemmy_api_common::{
|
||||||
post::{GetSiteMetadata, GetSiteMetadataResponse},
|
post::{GetSiteMetadata, GetSiteMetadataResponse},
|
||||||
request::fetch_link_metadata,
|
request::fetch_link_metadata,
|
||||||
};
|
};
|
||||||
|
use lemmy_db_views::structs::LocalUserView;
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
error::{LemmyErrorExt, LemmyResult},
|
error::{LemmyErrorExt, LemmyResult},
|
||||||
LemmyErrorType,
|
LemmyErrorType,
|
||||||
|
@ -14,6 +15,8 @@ use url::Url;
|
||||||
pub async fn get_link_metadata(
|
pub async fn get_link_metadata(
|
||||||
data: Query<GetSiteMetadata>,
|
data: Query<GetSiteMetadata>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
|
// Require an account for this API
|
||||||
|
_local_user_view: LocalUserView,
|
||||||
) -> LemmyResult<Json<GetSiteMetadataResponse>> {
|
) -> LemmyResult<Json<GetSiteMetadataResponse>> {
|
||||||
let url = Url::parse(&data.url).with_lemmy_type(LemmyErrorType::InvalidUrl)?;
|
let url = Url::parse(&data.url).with_lemmy_type(LemmyErrorType::InvalidUrl)?;
|
||||||
let metadata = fetch_link_metadata(&url, &context).await?;
|
let metadata = fetch_link_metadata(&url, &context).await?;
|
||||||
|
|
|
@ -85,11 +85,5 @@ pub async fn like_post(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
build_post_response(
|
build_post_response(context.deref(), post.community_id, local_user_view, post_id).await
|
||||||
context.deref(),
|
|
||||||
post.community_id,
|
|
||||||
&local_user_view.person,
|
|
||||||
post_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -63,11 +63,5 @@ pub async fn lock_post(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
build_post_response(
|
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
|
||||||
&context,
|
|
||||||
orig_post.community_id,
|
|
||||||
&local_user_view.person,
|
|
||||||
post_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,9 +34,14 @@ pub async fn save_post(
|
||||||
|
|
||||||
let post_id = data.post_id;
|
let post_id = data.post_id;
|
||||||
let person_id = local_user_view.person.id;
|
let person_id = local_user_view.person.id;
|
||||||
let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false)
|
let post_view = PostView::read(
|
||||||
.await?
|
&mut context.pool(),
|
||||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
post_id,
|
||||||
|
Some(&local_user_view.local_user),
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||||
|
|
||||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ use lemmy_api_common::{
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
comment::Comment,
|
comment::Comment,
|
||||||
|
local_user::LocalUser,
|
||||||
moderator::{AdminPurgeComment, AdminPurgeCommentForm},
|
moderator::{AdminPurgeComment, AdminPurgeCommentForm},
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
|
@ -29,9 +30,21 @@ pub async fn purge_comment(
|
||||||
let comment_id = data.comment_id;
|
let comment_id = data.comment_id;
|
||||||
|
|
||||||
// Read the comment to get the post_id and community
|
// Read the comment to get the post_id and community
|
||||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
|
let comment_view = CommentView::read(
|
||||||
.await?
|
&mut context.pool(),
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
comment_id,
|
||||||
|
Some(&local_user_view.local_user),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
|
|
||||||
|
// Also check that you're a higher admin
|
||||||
|
LocalUser::is_higher_admin_check(
|
||||||
|
&mut context.pool(),
|
||||||
|
local_user_view.person.id,
|
||||||
|
vec![comment_view.creator.id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let post_id = comment_view.comment.post_id;
|
let post_id = comment_view.comment.post_id;
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,16 @@ use lemmy_api_common::{
|
||||||
SuccessResponse,
|
SuccessResponse,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
newtypes::PersonId,
|
||||||
source::{
|
source::{
|
||||||
community::Community,
|
community::Community,
|
||||||
|
local_user::LocalUser,
|
||||||
moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm},
|
moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm},
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
};
|
};
|
||||||
use lemmy_db_views::structs::LocalUserView;
|
use lemmy_db_views::structs::LocalUserView;
|
||||||
|
use lemmy_db_views_actor::structs::CommunityModeratorView;
|
||||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
#[tracing::instrument(skip(context))]
|
||||||
|
@ -32,6 +35,21 @@ pub async fn purge_community(
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||||
|
|
||||||
|
// Also check that you're a higher admin than all the mods
|
||||||
|
let community_mod_person_ids =
|
||||||
|
CommunityModeratorView::for_community(&mut context.pool(), community.id)
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|cmv| cmv.moderator.id)
|
||||||
|
.collect::<Vec<PersonId>>();
|
||||||
|
|
||||||
|
LocalUser::is_higher_admin_check(
|
||||||
|
&mut context.pool(),
|
||||||
|
local_user_view.person.id,
|
||||||
|
community_mod_person_ids,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if let Some(banner) = &community.banner {
|
if let Some(banner) = &community.banner {
|
||||||
purge_image_from_pictrs(banner, &context).await.ok();
|
purge_image_from_pictrs(banner, &context).await.ok();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ use lemmy_api_common::{
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
|
local_user::LocalUser,
|
||||||
moderator::{AdminPurgePerson, AdminPurgePersonForm},
|
moderator::{AdminPurgePerson, AdminPurgePersonForm},
|
||||||
person::{Person, PersonUpdateForm},
|
person::{Person, PersonUpdateForm},
|
||||||
},
|
},
|
||||||
|
@ -27,9 +28,18 @@ pub async fn purge_person(
|
||||||
// Only let admin purge an item
|
// Only let admin purge an item
|
||||||
is_admin(&local_user_view)?;
|
is_admin(&local_user_view)?;
|
||||||
|
|
||||||
|
// Also check that you're a higher admin
|
||||||
|
LocalUser::is_higher_admin_check(
|
||||||
|
&mut context.pool(),
|
||||||
|
local_user_view.person.id,
|
||||||
|
vec![data.person_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let person = Person::read(&mut context.pool(), data.person_id)
|
let person = Person::read(&mut context.pool(), data.person_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
||||||
|
|
||||||
ban_nonlocal_user_from_local_communities(
|
ban_nonlocal_user_from_local_communities(
|
||||||
&local_user_view,
|
&local_user_view,
|
||||||
&person,
|
&person,
|
||||||
|
|
|
@ -10,6 +10,7 @@ use lemmy_api_common::{
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
|
local_user::LocalUser,
|
||||||
moderator::{AdminPurgePost, AdminPurgePostForm},
|
moderator::{AdminPurgePost, AdminPurgePostForm},
|
||||||
post::Post,
|
post::Post,
|
||||||
},
|
},
|
||||||
|
@ -32,6 +33,14 @@ pub async fn purge_post(
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||||
|
|
||||||
|
// Also check that you're a higher admin
|
||||||
|
LocalUser::is_higher_admin_check(
|
||||||
|
&mut context.pool(),
|
||||||
|
local_user_view.person.id,
|
||||||
|
vec![post.creator_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Purge image
|
// Purge image
|
||||||
if let Some(url) = &post.url {
|
if let Some(url) = &post.url {
|
||||||
purge_image_from_pictrs(url, &context).await.ok();
|
purge_image_from_pictrs(url, &context).await.ok();
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use actix_web::web::{Data, Json};
|
use activitypub_federation::config::Data;
|
||||||
|
use actix_web::web::Json;
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{
|
||||||
context::LemmyContext,
|
context::LemmyContext,
|
||||||
site::{ApproveRegistrationApplication, RegistrationApplicationResponse},
|
site::{ApproveRegistrationApplication, RegistrationApplicationResponse},
|
||||||
|
@ -10,10 +11,13 @@ use lemmy_db_schema::{
|
||||||
registration_application::{RegistrationApplication, RegistrationApplicationUpdateForm},
|
registration_application::{RegistrationApplication, RegistrationApplicationUpdateForm},
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
utils::diesel_string_update,
|
utils::{diesel_string_update, get_conn},
|
||||||
};
|
};
|
||||||
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
|
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
|
||||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
use lemmy_utils::{
|
||||||
|
error::{LemmyError, LemmyResult},
|
||||||
|
LemmyErrorType,
|
||||||
|
};
|
||||||
|
|
||||||
pub async fn approve_registration_application(
|
pub async fn approve_registration_application(
|
||||||
data: Json<ApproveRegistrationApplication>,
|
data: Json<ApproveRegistrationApplication>,
|
||||||
|
@ -25,34 +29,46 @@ pub async fn approve_registration_application(
|
||||||
// Only let admins do this
|
// Only let admins do this
|
||||||
is_admin(&local_user_view)?;
|
is_admin(&local_user_view)?;
|
||||||
|
|
||||||
// Update the registration with reason, admin_id
|
let pool = &mut context.pool();
|
||||||
let deny_reason = diesel_string_update(data.deny_reason.as_deref());
|
let conn = &mut get_conn(pool).await?;
|
||||||
let app_form = RegistrationApplicationUpdateForm {
|
let tx_data = data.clone();
|
||||||
admin_id: Some(Some(local_user_view.person.id)),
|
let approved_user_id = conn
|
||||||
deny_reason,
|
.build_transaction()
|
||||||
};
|
.run(|conn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
// Update the registration with reason, admin_id
|
||||||
|
let deny_reason = diesel_string_update(tx_data.deny_reason.as_deref());
|
||||||
|
let app_form = RegistrationApplicationUpdateForm {
|
||||||
|
admin_id: Some(Some(local_user_view.person.id)),
|
||||||
|
deny_reason,
|
||||||
|
};
|
||||||
|
|
||||||
let registration_application =
|
let registration_application =
|
||||||
RegistrationApplication::update(&mut context.pool(), app_id, &app_form).await?;
|
RegistrationApplication::update(&mut conn.into(), app_id, &app_form).await?;
|
||||||
|
|
||||||
// Update the local_user row
|
// Update the local_user row
|
||||||
let local_user_form = LocalUserUpdateForm {
|
let local_user_form = LocalUserUpdateForm {
|
||||||
accepted_application: Some(data.approve),
|
accepted_application: Some(tx_data.approve),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
let approved_user_id = registration_application.local_user_id;
|
let approved_user_id = registration_application.local_user_id;
|
||||||
LocalUser::update(&mut context.pool(), approved_user_id, &local_user_form).await?;
|
LocalUser::update(&mut conn.into(), approved_user_id, &local_user_form).await?;
|
||||||
|
|
||||||
|
Ok::<_, LemmyError>(approved_user_id)
|
||||||
|
}) as _
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
if data.approve {
|
if data.approve {
|
||||||
let approved_local_user_view = LocalUserView::read(&mut context.pool(), approved_user_id)
|
let approved_local_user_view = LocalUserView::read(&mut context.pool(), approved_user_id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
|
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
|
||||||
|
|
||||||
if approved_local_user_view.local_user.email.is_some() {
|
if approved_local_user_view.local_user.email.is_some() {
|
||||||
|
// Email sending may fail, but this won't revert the application approval
|
||||||
send_application_approved_email(&approved_local_user_view, context.settings()).await?;
|
send_application_approved_email(&approved_local_user_view, context.settings()).await?;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Read the view
|
// Read the view
|
||||||
let registration_application = RegistrationApplicationView::read(&mut context.pool(), app_id)
|
let registration_application = RegistrationApplicationView::read(&mut context.pool(), app_id)
|
||||||
|
|
28
crates/api/src/site/registration_applications/get.rs
Normal file
28
crates/api/src/site/registration_applications/get.rs
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
use actix_web::web::{Data, Json, Query};
|
||||||
|
use lemmy_api_common::{
|
||||||
|
context::LemmyContext,
|
||||||
|
site::{GetRegistrationApplication, RegistrationApplicationResponse},
|
||||||
|
utils::is_admin,
|
||||||
|
};
|
||||||
|
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
|
||||||
|
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||||
|
|
||||||
|
/// Lists registration applications, filterable by undenied only.
|
||||||
|
pub async fn get_registration_application(
|
||||||
|
data: Query<GetRegistrationApplication>,
|
||||||
|
context: Data<LemmyContext>,
|
||||||
|
local_user_view: LocalUserView,
|
||||||
|
) -> LemmyResult<Json<RegistrationApplicationResponse>> {
|
||||||
|
// Make sure user is an admin
|
||||||
|
is_admin(&local_user_view)?;
|
||||||
|
|
||||||
|
// Read the view
|
||||||
|
let registration_application =
|
||||||
|
RegistrationApplicationView::read_by_person(&mut context.pool(), data.person_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
|
||||||
|
|
||||||
|
Ok(Json(RegistrationApplicationResponse {
|
||||||
|
registration_application,
|
||||||
|
}))
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
use actix_web::web::{Data, Json, Query};
|
use activitypub_federation::config::Data;
|
||||||
|
use actix_web::web::{Json, Query};
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{
|
||||||
context::LemmyContext,
|
context::LemmyContext,
|
||||||
site::{ListRegistrationApplications, ListRegistrationApplicationsResponse},
|
site::{ListRegistrationApplications, ListRegistrationApplicationsResponse},
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
pub mod approve;
|
pub mod approve;
|
||||||
|
pub mod get;
|
||||||
pub mod list;
|
pub mod list;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
pub mod unread_count;
|
pub mod unread_count;
|
||||||
|
|
428
crates/api/src/site/registration_applications/tests.rs
Normal file
428
crates/api/src/site/registration_applications/tests.rs
Normal file
|
@ -0,0 +1,428 @@
|
||||||
|
use crate::site::registration_applications::{
|
||||||
|
approve::approve_registration_application,
|
||||||
|
list::list_registration_applications,
|
||||||
|
unread_count::get_unread_registration_application_count,
|
||||||
|
};
|
||||||
|
use activitypub_federation::config::Data;
|
||||||
|
use actix_web::web::{Json, Query};
|
||||||
|
use lemmy_api_common::{
|
||||||
|
context::LemmyContext,
|
||||||
|
site::{
|
||||||
|
ApproveRegistrationApplication,
|
||||||
|
EditSite,
|
||||||
|
GetUnreadRegistrationApplicationCountResponse,
|
||||||
|
ListRegistrationApplicationsResponse,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use lemmy_api_crud::site::update::update_site;
|
||||||
|
use lemmy_db_schema::{
|
||||||
|
newtypes::InstanceId,
|
||||||
|
source::{
|
||||||
|
instance::Instance,
|
||||||
|
local_site::{LocalSite, LocalSiteInsertForm},
|
||||||
|
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitInsertForm},
|
||||||
|
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
|
||||||
|
person::{Person, PersonInsertForm},
|
||||||
|
registration_application::{RegistrationApplication, RegistrationApplicationInsertForm},
|
||||||
|
site::{Site, SiteInsertForm},
|
||||||
|
},
|
||||||
|
traits::Crud,
|
||||||
|
utils::DbPool,
|
||||||
|
RegistrationMode,
|
||||||
|
};
|
||||||
|
use lemmy_db_views::structs::LocalUserView;
|
||||||
|
use lemmy_utils::{error::LemmyResult, LemmyErrorType, CACHE_DURATION_API};
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
async fn create_test_site(context: &Data<LemmyContext>) -> LemmyResult<(Instance, LocalUserView)> {
|
||||||
|
let pool = &mut context.pool();
|
||||||
|
|
||||||
|
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
|
||||||
|
.await
|
||||||
|
.expect("Create test instance");
|
||||||
|
|
||||||
|
let admin_person = Person::create(
|
||||||
|
pool,
|
||||||
|
&PersonInsertForm::test_form(inserted_instance.id, "admin"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
LocalUser::create(
|
||||||
|
pool,
|
||||||
|
&LocalUserInsertForm::test_form_admin(admin_person.id),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let admin_local_user_view = LocalUserView::read_person(pool, admin_person.id)
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let site_form = SiteInsertForm::builder()
|
||||||
|
.name("test site".to_string())
|
||||||
|
.instance_id(inserted_instance.id)
|
||||||
|
.build();
|
||||||
|
let site = Site::create(pool, &site_form).await.unwrap();
|
||||||
|
|
||||||
|
// Create a local site, since this is necessary for determining if email verification is
|
||||||
|
// required
|
||||||
|
let local_site_form = LocalSiteInsertForm::builder()
|
||||||
|
.site_id(site.id)
|
||||||
|
.require_email_verification(Some(true))
|
||||||
|
.application_question(Some(".".to_string()))
|
||||||
|
.registration_mode(Some(RegistrationMode::RequireApplication))
|
||||||
|
.site_setup(Some(true))
|
||||||
|
.build();
|
||||||
|
let local_site = LocalSite::create(pool, &local_site_form).await.unwrap();
|
||||||
|
|
||||||
|
// Required to have a working local SiteView when updating the site to change email verification
|
||||||
|
// requirement or registration mode
|
||||||
|
let rate_limit_form = LocalSiteRateLimitInsertForm::builder()
|
||||||
|
.local_site_id(local_site.id)
|
||||||
|
.build();
|
||||||
|
LocalSiteRateLimit::create(pool, &rate_limit_form)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
Ok((inserted_instance, admin_local_user_view))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn signup(
|
||||||
|
pool: &mut DbPool<'_>,
|
||||||
|
instance_id: InstanceId,
|
||||||
|
name: &str,
|
||||||
|
email: Option<&str>,
|
||||||
|
) -> LemmyResult<(LocalUser, RegistrationApplication)> {
|
||||||
|
let person_insert_form = PersonInsertForm::test_form(instance_id, name);
|
||||||
|
let person = Person::create(pool, &person_insert_form).await?;
|
||||||
|
|
||||||
|
let local_user_insert_form = match email {
|
||||||
|
Some(email) => LocalUserInsertForm {
|
||||||
|
email: Some(email.to_string()),
|
||||||
|
email_verified: Some(false),
|
||||||
|
..LocalUserInsertForm::test_form(person.id)
|
||||||
|
},
|
||||||
|
None => LocalUserInsertForm::test_form(person.id),
|
||||||
|
};
|
||||||
|
|
||||||
|
let local_user = LocalUser::create(pool, &local_user_insert_form, vec![]).await?;
|
||||||
|
|
||||||
|
let application_insert_form = RegistrationApplicationInsertForm {
|
||||||
|
local_user_id: local_user.id,
|
||||||
|
answer: "x".to_string(),
|
||||||
|
};
|
||||||
|
let application = RegistrationApplication::create(pool, &application_insert_form).await?;
|
||||||
|
|
||||||
|
Ok((local_user, application))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
async fn get_application_statuses(
|
||||||
|
context: &Data<LemmyContext>,
|
||||||
|
admin: LocalUserView,
|
||||||
|
) -> LemmyResult<(
|
||||||
|
Json<GetUnreadRegistrationApplicationCountResponse>,
|
||||||
|
Json<ListRegistrationApplicationsResponse>,
|
||||||
|
Json<ListRegistrationApplicationsResponse>,
|
||||||
|
)> {
|
||||||
|
let application_count =
|
||||||
|
get_unread_registration_application_count(context.reset_request_count(), admin.clone()).await?;
|
||||||
|
|
||||||
|
let unread_applications = list_registration_applications(
|
||||||
|
Query::from_query("unread_only=true").unwrap(),
|
||||||
|
context.reset_request_count(),
|
||||||
|
admin.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let all_applications = list_registration_applications(
|
||||||
|
Query::from_query("unread_only=false").unwrap(),
|
||||||
|
context.reset_request_count(),
|
||||||
|
admin,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok((application_count, unread_applications, all_applications))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::indexing_slicing)]
|
||||||
|
#[allow(clippy::unwrap_used)]
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_application_approval() -> LemmyResult<()> {
|
||||||
|
let context = LemmyContext::init_test_context().await;
|
||||||
|
let pool = &mut context.pool();
|
||||||
|
|
||||||
|
let (instance, admin_local_user_view) = create_test_site(&context).await?;
|
||||||
|
|
||||||
|
// Non-unread counts unfortunately are duplicated due to different types (i64 vs usize)
|
||||||
|
let mut expected_total_applications = 0;
|
||||||
|
let mut expected_unread_applications = 0u8;
|
||||||
|
|
||||||
|
let (local_user_with_email, app_with_email) =
|
||||||
|
signup(pool, instance.id, "user_w_email", Some("lemmy@localhost")).await?;
|
||||||
|
|
||||||
|
let (application_count, unread_applications, all_applications) =
|
||||||
|
get_application_statuses(&context, admin_local_user_view.clone()).await?;
|
||||||
|
|
||||||
|
// When email verification is required and the email is not verified the application should not
|
||||||
|
// be visible to admins
|
||||||
|
assert_eq!(
|
||||||
|
application_count.registration_applications,
|
||||||
|
i64::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unread_applications.registration_applications.len(),
|
||||||
|
usize::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all_applications.registration_applications.len(),
|
||||||
|
expected_total_applications,
|
||||||
|
);
|
||||||
|
|
||||||
|
LocalUser::update(
|
||||||
|
pool,
|
||||||
|
local_user_with_email.id,
|
||||||
|
&LocalUserUpdateForm {
|
||||||
|
email_verified: Some(true),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
expected_total_applications += 1;
|
||||||
|
expected_unread_applications += 1;
|
||||||
|
|
||||||
|
let (application_count, unread_applications, all_applications) =
|
||||||
|
get_application_statuses(&context, admin_local_user_view.clone()).await?;
|
||||||
|
|
||||||
|
// When email verification is required and the email is verified the application should be
|
||||||
|
// visible to admins
|
||||||
|
assert_eq!(
|
||||||
|
application_count.registration_applications,
|
||||||
|
i64::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unread_applications.registration_applications.len(),
|
||||||
|
usize::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!unread_applications.registration_applications[0]
|
||||||
|
.creator_local_user
|
||||||
|
.accepted_application
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all_applications.registration_applications.len(),
|
||||||
|
expected_total_applications,
|
||||||
|
);
|
||||||
|
|
||||||
|
let approval = approve_registration_application(
|
||||||
|
Json(ApproveRegistrationApplication {
|
||||||
|
id: app_with_email.id,
|
||||||
|
approve: true,
|
||||||
|
deny_reason: None,
|
||||||
|
}),
|
||||||
|
context.reset_request_count(),
|
||||||
|
admin_local_user_view.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
// Approval should be processed up until email sending is attempted
|
||||||
|
assert!(approval.is_err_and(|e| e.error_type == LemmyErrorType::NoEmailSetup));
|
||||||
|
|
||||||
|
expected_unread_applications -= 1;
|
||||||
|
|
||||||
|
let (application_count, unread_applications, all_applications) =
|
||||||
|
get_application_statuses(&context, admin_local_user_view.clone()).await?;
|
||||||
|
|
||||||
|
// When the application is approved it should only be returned for unread queries
|
||||||
|
assert_eq!(
|
||||||
|
application_count.registration_applications,
|
||||||
|
i64::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unread_applications.registration_applications.len(),
|
||||||
|
usize::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all_applications.registration_applications.len(),
|
||||||
|
expected_total_applications,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
all_applications.registration_applications[0]
|
||||||
|
.creator_local_user
|
||||||
|
.accepted_application
|
||||||
|
);
|
||||||
|
|
||||||
|
let (_local_user, app_with_email_2) = signup(
|
||||||
|
pool,
|
||||||
|
instance.id,
|
||||||
|
"user_w_email_2",
|
||||||
|
Some("lemmy2@localhost"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let (application_count, unread_applications, all_applications) =
|
||||||
|
get_application_statuses(&context, admin_local_user_view.clone()).await?;
|
||||||
|
|
||||||
|
// Email not verified, so application still not visible
|
||||||
|
assert_eq!(
|
||||||
|
application_count.registration_applications,
|
||||||
|
i64::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unread_applications.registration_applications.len(),
|
||||||
|
usize::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all_applications.registration_applications.len(),
|
||||||
|
expected_total_applications,
|
||||||
|
);
|
||||||
|
|
||||||
|
update_site(
|
||||||
|
Json(EditSite {
|
||||||
|
require_email_verification: Some(false),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
context.reset_request_count(),
|
||||||
|
admin_local_user_view.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO: There is probably a better way to ensure cache invalidation
|
||||||
|
tokio::time::sleep(CACHE_DURATION_API).await;
|
||||||
|
|
||||||
|
expected_total_applications += 1;
|
||||||
|
expected_unread_applications += 1;
|
||||||
|
|
||||||
|
let (application_count, unread_applications, all_applications) =
|
||||||
|
get_application_statuses(&context, admin_local_user_view.clone()).await?;
|
||||||
|
|
||||||
|
// After disabling email verification the application should now be visible
|
||||||
|
assert_eq!(
|
||||||
|
application_count.registration_applications,
|
||||||
|
i64::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unread_applications.registration_applications.len(),
|
||||||
|
usize::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all_applications.registration_applications.len(),
|
||||||
|
expected_total_applications,
|
||||||
|
);
|
||||||
|
|
||||||
|
approve_registration_application(
|
||||||
|
Json(ApproveRegistrationApplication {
|
||||||
|
id: app_with_email_2.id,
|
||||||
|
approve: false,
|
||||||
|
deny_reason: None,
|
||||||
|
}),
|
||||||
|
context.reset_request_count(),
|
||||||
|
admin_local_user_view.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
expected_unread_applications -= 1;
|
||||||
|
|
||||||
|
let (application_count, unread_applications, all_applications) =
|
||||||
|
get_application_statuses(&context, admin_local_user_view.clone()).await?;
|
||||||
|
|
||||||
|
// Denied applications should not be marked as unread
|
||||||
|
assert_eq!(
|
||||||
|
application_count.registration_applications,
|
||||||
|
i64::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unread_applications.registration_applications.len(),
|
||||||
|
usize::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all_applications.registration_applications.len(),
|
||||||
|
expected_total_applications,
|
||||||
|
);
|
||||||
|
|
||||||
|
signup(pool, instance.id, "user_wo_email", None).await?;
|
||||||
|
|
||||||
|
expected_total_applications += 1;
|
||||||
|
expected_unread_applications += 1;
|
||||||
|
|
||||||
|
let (application_count, unread_applications, all_applications) =
|
||||||
|
get_application_statuses(&context, admin_local_user_view.clone()).await?;
|
||||||
|
|
||||||
|
// New user without email should immediately be visible
|
||||||
|
assert_eq!(
|
||||||
|
application_count.registration_applications,
|
||||||
|
i64::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unread_applications.registration_applications.len(),
|
||||||
|
usize::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all_applications.registration_applications.len(),
|
||||||
|
expected_total_applications,
|
||||||
|
);
|
||||||
|
|
||||||
|
signup(pool, instance.id, "user_w_email_3", None).await?;
|
||||||
|
|
||||||
|
expected_total_applications += 1;
|
||||||
|
expected_unread_applications += 1;
|
||||||
|
|
||||||
|
let (application_count, unread_applications, all_applications) =
|
||||||
|
get_application_statuses(&context, admin_local_user_view.clone()).await?;
|
||||||
|
|
||||||
|
// New user with email should immediately be visible
|
||||||
|
assert_eq!(
|
||||||
|
application_count.registration_applications,
|
||||||
|
i64::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unread_applications.registration_applications.len(),
|
||||||
|
usize::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all_applications.registration_applications.len(),
|
||||||
|
expected_total_applications,
|
||||||
|
);
|
||||||
|
|
||||||
|
update_site(
|
||||||
|
Json(EditSite {
|
||||||
|
registration_mode: Some(RegistrationMode::Open),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
context.reset_request_count(),
|
||||||
|
admin_local_user_view.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// TODO: There is probably a better way to ensure cache invalidation
|
||||||
|
tokio::time::sleep(CACHE_DURATION_API).await;
|
||||||
|
|
||||||
|
let (application_count, unread_applications, all_applications) =
|
||||||
|
get_application_statuses(&context, admin_local_user_view.clone()).await?;
|
||||||
|
|
||||||
|
// TODO: At this time applications do not get approved when switching to open registration, so the
|
||||||
|
// numbers will not change. See https://github.com/LemmyNet/lemmy/issues/4969
|
||||||
|
// expected_application_count = 0;
|
||||||
|
// expected_unread_applications_len = 0;
|
||||||
|
|
||||||
|
// When applications are not required all previous applications should become approved but still
|
||||||
|
// visible
|
||||||
|
assert_eq!(
|
||||||
|
application_count.registration_applications,
|
||||||
|
i64::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
unread_applications.registration_applications.len(),
|
||||||
|
usize::from(expected_unread_applications),
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
all_applications.registration_applications.len(),
|
||||||
|
expected_total_applications,
|
||||||
|
);
|
||||||
|
|
||||||
|
LocalSite::delete(pool).await?;
|
||||||
|
// Instance deletion cascades cleanup of all created persons
|
||||||
|
Instance::delete(pool, instance.id).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
use actix_web::web::{Data, Json};
|
use activitypub_federation::config::Data;
|
||||||
|
use actix_web::web::Json;
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{
|
||||||
context::LemmyContext,
|
context::LemmyContext,
|
||||||
site::GetUnreadRegistrationApplicationCountResponse,
|
site::GetUnreadRegistrationApplicationCountResponse,
|
||||||
|
|
|
@ -34,7 +34,6 @@ full = [
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"futures",
|
"futures",
|
||||||
"once_cell",
|
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"mime",
|
"mime",
|
||||||
]
|
]
|
||||||
|
@ -61,7 +60,6 @@ reqwest = { workspace = true, optional = true }
|
||||||
ts-rs = { workspace = true, optional = true }
|
ts-rs = { workspace = true, optional = true }
|
||||||
moka.workspace = true
|
moka.workspace = true
|
||||||
anyhow.workspace = true
|
anyhow.workspace = 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 }
|
||||||
urlencoding = { workspace = true }
|
urlencoding = { workspace = true }
|
||||||
|
|
|
@ -36,8 +36,8 @@ pub async fn build_comment_response(
|
||||||
local_user_view: Option<LocalUserView>,
|
local_user_view: Option<LocalUserView>,
|
||||||
recipient_ids: Vec<LocalUserId>,
|
recipient_ids: Vec<LocalUserId>,
|
||||||
) -> LemmyResult<CommentResponse> {
|
) -> LemmyResult<CommentResponse> {
|
||||||
let person_id = local_user_view.map(|l| l.person.id);
|
let local_user = local_user_view.map(|l| l.local_user);
|
||||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, person_id)
|
let comment_view = CommentView::read(&mut context.pool(), comment_id, local_user.as_ref())
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
Ok(CommentResponse {
|
Ok(CommentResponse {
|
||||||
|
@ -54,11 +54,11 @@ pub async fn build_community_response(
|
||||||
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view.person, community_id)
|
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view.person, community_id)
|
||||||
.await
|
.await
|
||||||
.is_ok();
|
.is_ok();
|
||||||
let person_id = local_user_view.person.id;
|
let local_user = local_user_view.local_user;
|
||||||
let community_view = CommunityView::read(
|
let community_view = CommunityView::read(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
community_id,
|
community_id,
|
||||||
Some(person_id),
|
Some(&local_user),
|
||||||
is_mod_or_admin,
|
is_mod_or_admin,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
@ -74,16 +74,17 @@ pub async fn build_community_response(
|
||||||
pub async fn build_post_response(
|
pub async fn build_post_response(
|
||||||
context: &LemmyContext,
|
context: &LemmyContext,
|
||||||
community_id: CommunityId,
|
community_id: CommunityId,
|
||||||
person: &Person,
|
local_user_view: LocalUserView,
|
||||||
post_id: PostId,
|
post_id: PostId,
|
||||||
) -> LemmyResult<Json<PostResponse>> {
|
) -> LemmyResult<Json<PostResponse>> {
|
||||||
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), person, community_id)
|
let local_user = local_user_view.local_user;
|
||||||
|
let is_mod_or_admin = is_mod_or_admin(&mut context.pool(), &local_user_view.person, community_id)
|
||||||
.await
|
.await
|
||||||
.is_ok();
|
.is_ok();
|
||||||
let post_view = PostView::read(
|
let post_view = PostView::read(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
post_id,
|
post_id,
|
||||||
Some(person.id),
|
Some(&local_user),
|
||||||
is_mod_or_admin,
|
is_mod_or_admin,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
@ -99,14 +100,20 @@ pub async fn send_local_notifs(
|
||||||
person: &Person,
|
person: &Person,
|
||||||
do_send_email: bool,
|
do_send_email: bool,
|
||||||
context: &LemmyContext,
|
context: &LemmyContext,
|
||||||
|
local_user_view: Option<&LocalUserView>,
|
||||||
) -> LemmyResult<Vec<LocalUserId>> {
|
) -> LemmyResult<Vec<LocalUserId>> {
|
||||||
let mut recipient_ids = Vec::new();
|
let mut recipient_ids = Vec::new();
|
||||||
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
|
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
|
||||||
|
|
||||||
|
// let person = my_local_user.person;
|
||||||
// Read the comment view to get extra info
|
// Read the comment view to get extra info
|
||||||
let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
|
let comment_view = CommentView::read(
|
||||||
.await?
|
&mut context.pool(),
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
comment_id,
|
||||||
|
local_user_view.map(|view| &view.local_user),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
let comment = comment_view.comment;
|
let comment = comment_view.comment;
|
||||||
let post = comment_view.post;
|
let post = comment_view.post;
|
||||||
let community = comment_view.community;
|
let community = comment_view.community;
|
||||||
|
|
|
@ -116,10 +116,7 @@ mod tests {
|
||||||
|
|
||||||
let inserted_person = Person::create(pool, &new_person).await.unwrap();
|
let inserted_person = Person::create(pool, &new_person).await.unwrap();
|
||||||
|
|
||||||
let local_user_form = LocalUserInsertForm::builder()
|
let local_user_form = LocalUserInsertForm::test_form(inserted_person.id);
|
||||||
.person_id(inserted_person.id)
|
|
||||||
.password_encrypted("123456".to_string())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![])
|
let inserted_local_user = LocalUser::create(pool, &local_user_form, vec![])
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -79,6 +79,10 @@ pub struct GetPosts {
|
||||||
pub liked_only: Option<bool>,
|
pub liked_only: Option<bool>,
|
||||||
pub disliked_only: Option<bool>,
|
pub disliked_only: Option<bool>,
|
||||||
pub show_hidden: Option<bool>,
|
pub show_hidden: Option<bool>,
|
||||||
|
/// If true, then show the read posts (even if your user setting is to hide them)
|
||||||
|
pub show_read: Option<bool>,
|
||||||
|
/// If true, then show the nsfw posts (even if your user setting is to hide them)
|
||||||
|
pub show_nsfw: Option<bool>,
|
||||||
pub page_cursor: Option<PaginationCursor>,
|
pub page_cursor: Option<PaginationCursor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,10 +8,11 @@ use crate::{
|
||||||
use activitypub_federation::config::Data;
|
use activitypub_federation::config::Data;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use encoding_rs::{Encoding, UTF_8};
|
use encoding_rs::{Encoding, UTF_8};
|
||||||
|
use futures::StreamExt;
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
newtypes::DbUrl,
|
newtypes::DbUrl,
|
||||||
source::{
|
source::{
|
||||||
images::{LocalImage, LocalImageForm},
|
images::{ImageDetailsForm, LocalImage, LocalImageForm},
|
||||||
local_site::LocalSite,
|
local_site::LocalSite,
|
||||||
post::{Post, PostUpdateForm},
|
post::{Post, PostUpdateForm},
|
||||||
},
|
},
|
||||||
|
@ -23,7 +24,12 @@ use lemmy_utils::{
|
||||||
VERSION,
|
VERSION,
|
||||||
};
|
};
|
||||||
use mime::Mime;
|
use mime::Mime;
|
||||||
use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder};
|
use reqwest::{
|
||||||
|
header::{CONTENT_TYPE, RANGE},
|
||||||
|
Client,
|
||||||
|
ClientBuilder,
|
||||||
|
Response,
|
||||||
|
};
|
||||||
use reqwest_middleware::ClientWithMiddleware;
|
use reqwest_middleware::ClientWithMiddleware;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
@ -44,7 +50,17 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder {
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResult<LinkMetadata> {
|
pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResult<LinkMetadata> {
|
||||||
info!("Fetching site metadata for url: {}", url);
|
info!("Fetching site metadata for url: {}", url);
|
||||||
let response = context.client().get(url.as_str()).send().await?;
|
// We only fetch the first 64kB of data in order to not waste bandwidth especially for large
|
||||||
|
// binary files
|
||||||
|
let bytes_to_fetch = 64 * 1024;
|
||||||
|
let response = context
|
||||||
|
.client()
|
||||||
|
.get(url.as_str())
|
||||||
|
// we only need the first chunk of data. Note that we do not check for Accept-Range so the
|
||||||
|
// server may ignore this and still respond with the full response
|
||||||
|
.header(RANGE, format!("bytes=0-{}", bytes_to_fetch - 1)) /* -1 because inclusive */
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
let content_type: Option<Mime> = response
|
let content_type: Option<Mime> = response
|
||||||
.headers()
|
.headers()
|
||||||
|
@ -52,19 +68,57 @@ pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResu
|
||||||
.and_then(|h| h.to_str().ok())
|
.and_then(|h| h.to_str().ok())
|
||||||
.and_then(|h| h.parse().ok());
|
.and_then(|h| h.parse().ok());
|
||||||
|
|
||||||
// Can't use .text() here, because it only checks the content header, not the actual bytes
|
let opengraph_data = {
|
||||||
// https://github.com/LemmyNet/lemmy/issues/1964
|
// if the content type is not text/html, we don't need to parse it
|
||||||
let html_bytes = response.bytes().await.map_err(LemmyError::from)?.to_vec();
|
let is_html = content_type
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| {
|
||||||
|
(c.type_() == mime::TEXT && c.subtype() == mime::HTML)
|
||||||
|
||
|
||||||
|
// application/xhtml+xml is a subset of HTML
|
||||||
|
(c.type_() == mime::APPLICATION && c.subtype() == "xhtml")
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !is_html {
|
||||||
|
Default::default()
|
||||||
|
} else {
|
||||||
|
// Can't use .text() here, because it only checks the content header, not the actual bytes
|
||||||
|
// https://github.com/LemmyNet/lemmy/issues/1964
|
||||||
|
// So we want to do deep inspection of the actually returned bytes but need to be careful not
|
||||||
|
// spend too much time parsing binary data as HTML
|
||||||
|
|
||||||
let opengraph_data = extract_opengraph_data(&html_bytes, url)
|
// only take first bytes regardless of how many bytes the server returns
|
||||||
.map_err(|e| info!("{e}"))
|
let html_bytes = collect_bytes_until_limit(response, bytes_to_fetch).await?;
|
||||||
.unwrap_or_default();
|
extract_opengraph_data(&html_bytes, url)
|
||||||
|
.map_err(|e| info!("{e}"))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
};
|
||||||
Ok(LinkMetadata {
|
Ok(LinkMetadata {
|
||||||
opengraph_data,
|
opengraph_data,
|
||||||
content_type: content_type.map(|c| c.to_string()),
|
content_type: content_type.map(|c| c.to_string()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn collect_bytes_until_limit(
|
||||||
|
response: Response,
|
||||||
|
requested_bytes: usize,
|
||||||
|
) -> Result<Vec<u8>, LemmyError> {
|
||||||
|
let mut stream = response.bytes_stream();
|
||||||
|
let mut bytes = Vec::with_capacity(requested_bytes);
|
||||||
|
while let Some(chunk) = stream.next().await {
|
||||||
|
let chunk = chunk.map_err(LemmyError::from)?;
|
||||||
|
// we may go over the requested size here but the important part is we don't keep aggregating
|
||||||
|
// more chunks than needed
|
||||||
|
bytes.extend_from_slice(&chunk);
|
||||||
|
if bytes.len() >= requested_bytes {
|
||||||
|
bytes.truncate(requested_bytes);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
/// Generates and saves a post thumbnail and metadata.
|
/// Generates and saves a post thumbnail and metadata.
|
||||||
///
|
///
|
||||||
/// Takes a callback to generate a send activity task, so that post can be federated with metadata.
|
/// Takes a callback to generate a send activity task, so that post can be federated with metadata.
|
||||||
|
@ -209,6 +263,19 @@ pub struct PictrsFileDetails {
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PictrsFileDetails {
|
||||||
|
/// Builds the image form. This should always use the thumbnail_url,
|
||||||
|
/// Because the post_view joins to it
|
||||||
|
pub fn build_image_details_form(&self, thumbnail_url: &Url) -> ImageDetailsForm {
|
||||||
|
ImageDetailsForm {
|
||||||
|
link: thumbnail_url.clone().into(),
|
||||||
|
width: self.width.into(),
|
||||||
|
height: self.height.into(),
|
||||||
|
content_type: self.content_type.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
struct PictrsPurgeResponse {
|
struct PictrsPurgeResponse {
|
||||||
msg: String,
|
msg: String,
|
||||||
|
@ -316,11 +383,52 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
|
||||||
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
||||||
let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?;
|
let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?;
|
||||||
|
|
||||||
LocalImage::create(&mut context.pool(), &form).await?;
|
// Also store the details for the image
|
||||||
|
let details_form = image.details.build_image_details_form(&thumbnail_url);
|
||||||
|
LocalImage::create(&mut context.pool(), &form, &details_form).await?;
|
||||||
|
|
||||||
Ok(thumbnail_url)
|
Ok(thumbnail_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fetches the image details for pictrs proxied images
|
||||||
|
///
|
||||||
|
/// We don't need to check for image mode, as that's already been done
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
pub async fn fetch_pictrs_proxied_image_details(
|
||||||
|
image_url: &Url,
|
||||||
|
context: &LemmyContext,
|
||||||
|
) -> LemmyResult<PictrsFileDetails> {
|
||||||
|
let pictrs_url = context.settings().pictrs_config()?.url;
|
||||||
|
let encoded_image_url = encode(image_url.as_str());
|
||||||
|
|
||||||
|
// Pictrs needs you to fetch the proxied image before you can fetch the details
|
||||||
|
let proxy_url = format!("{pictrs_url}image/original?proxy={encoded_image_url}");
|
||||||
|
|
||||||
|
let res = context
|
||||||
|
.client()
|
||||||
|
.get(&proxy_url)
|
||||||
|
.timeout(REQWEST_TIMEOUT)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.status();
|
||||||
|
if !res.is_success() {
|
||||||
|
Err(LemmyErrorType::NotAnImageType)?
|
||||||
|
}
|
||||||
|
|
||||||
|
let details_url = format!("{pictrs_url}image/details/original?proxy={encoded_image_url}");
|
||||||
|
|
||||||
|
let res = context
|
||||||
|
.client()
|
||||||
|
.get(&details_url)
|
||||||
|
.timeout(REQWEST_TIMEOUT)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.json()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: get rid of this by reading content type from db
|
// TODO: get rid of this by reading content type from db
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> LemmyResult<()> {
|
async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> LemmyResult<()> {
|
||||||
|
|
|
@ -13,7 +13,7 @@ use lemmy_db_schema::{
|
||||||
};
|
};
|
||||||
use lemmy_db_views::structs::PrivateMessageView;
|
use lemmy_db_views::structs::PrivateMessageView;
|
||||||
use lemmy_utils::error::LemmyResult;
|
use lemmy_utils::error::LemmyResult;
|
||||||
use once_cell::sync::{Lazy, OnceCell};
|
use std::sync::{LazyLock, OnceLock};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::{
|
sync::{
|
||||||
mpsc,
|
mpsc,
|
||||||
|
@ -28,7 +28,7 @@ type MatchOutgoingActivitiesBoxed =
|
||||||
Box<for<'a> fn(SendActivityData, &'a Data<LemmyContext>) -> BoxFuture<'a, LemmyResult<()>>>;
|
Box<for<'a> fn(SendActivityData, &'a Data<LemmyContext>) -> BoxFuture<'a, LemmyResult<()>>>;
|
||||||
|
|
||||||
/// This static is necessary so that the api_common crates don't need to depend on lemmy_apub
|
/// This static is necessary so that the api_common crates don't need to depend on lemmy_apub
|
||||||
pub static MATCH_OUTGOING_ACTIVITIES: OnceCell<MatchOutgoingActivitiesBoxed> = OnceCell::new();
|
pub static MATCH_OUTGOING_ACTIVITIES: OnceLock<MatchOutgoingActivitiesBoxed> = OnceLock::new();
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum SendActivityData {
|
pub enum SendActivityData {
|
||||||
|
@ -101,7 +101,7 @@ pub enum SendActivityData {
|
||||||
|
|
||||||
// TODO: instead of static, move this into LemmyContext. make sure that stopping the process with
|
// TODO: instead of static, move this into LemmyContext. make sure that stopping the process with
|
||||||
// ctrl+c still works.
|
// ctrl+c still works.
|
||||||
static ACTIVITY_CHANNEL: Lazy<ActivityChannel> = Lazy::new(|| {
|
static ACTIVITY_CHANNEL: LazyLock<ActivityChannel> = LazyLock::new(|| {
|
||||||
let (sender, receiver) = mpsc::unbounded_channel();
|
let (sender, receiver) = mpsc::unbounded_channel();
|
||||||
let weak_sender = sender.downgrade();
|
let weak_sender = sender.downgrade();
|
||||||
ActivityChannel {
|
ActivityChannel {
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
use crate::federate_retry_sleep_duration;
|
use crate::federate_retry_sleep_duration;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
newtypes::{CommentId, CommunityId, InstanceId, LanguageId, PersonId, PostId},
|
newtypes::{
|
||||||
|
CommentId,
|
||||||
|
CommunityId,
|
||||||
|
InstanceId,
|
||||||
|
LanguageId,
|
||||||
|
PersonId,
|
||||||
|
PostId,
|
||||||
|
RegistrationApplicationId,
|
||||||
|
},
|
||||||
source::{
|
source::{
|
||||||
federation_queue_state::FederationQueueState,
|
federation_queue_state::FederationQueueState,
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
|
@ -440,13 +448,22 @@ pub struct ListRegistrationApplicationsResponse {
|
||||||
pub registration_applications: Vec<RegistrationApplicationView>,
|
pub registration_applications: Vec<RegistrationApplicationView>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[skip_serializing_none]
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
||||||
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
|
/// Gets a registration application for a person
|
||||||
|
pub struct GetRegistrationApplication {
|
||||||
|
pub person_id: PersonId,
|
||||||
|
}
|
||||||
|
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
|
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
|
||||||
#[cfg_attr(feature = "full", derive(TS))]
|
#[cfg_attr(feature = "full", derive(TS))]
|
||||||
#[cfg_attr(feature = "full", ts(export))]
|
#[cfg_attr(feature = "full", ts(export))]
|
||||||
/// Approves a registration application.
|
/// Approves a registration application.
|
||||||
pub struct ApproveRegistrationApplication {
|
pub struct ApproveRegistrationApplication {
|
||||||
pub id: i32,
|
pub id: RegistrationApplicationId,
|
||||||
pub approve: bool,
|
pub approve: bool,
|
||||||
pub deny_reason: Option<String>,
|
pub deny_reason: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
context::LemmyContext,
|
context::LemmyContext,
|
||||||
request::{delete_image_from_pictrs, purge_image_from_pictrs},
|
request::{
|
||||||
|
delete_image_from_pictrs,
|
||||||
|
fetch_pictrs_proxied_image_details,
|
||||||
|
purge_image_from_pictrs,
|
||||||
|
},
|
||||||
site::{FederatedInstances, InstanceWithFederationState},
|
site::{FederatedInstances, InstanceWithFederationState},
|
||||||
};
|
};
|
||||||
use chrono::{DateTime, Days, Local, TimeZone, Utc};
|
use chrono::{DateTime, Days, Local, TimeZone, Utc};
|
||||||
|
@ -49,10 +53,9 @@ use lemmy_utils::{
|
||||||
CACHE_DURATION_FEDERATION,
|
CACHE_DURATION_FEDERATION,
|
||||||
};
|
};
|
||||||
use moka::future::Cache;
|
use moka::future::Cache;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use regex::{escape, Regex, RegexSet};
|
use regex::{escape, Regex, RegexSet};
|
||||||
use rosetta_i18n::{Language, LanguageId};
|
use rosetta_i18n::{Language, LanguageId};
|
||||||
use std::collections::HashSet;
|
use std::{collections::HashSet, sync::LazyLock};
|
||||||
use tracing::warn;
|
use tracing::warn;
|
||||||
use url::{ParseError, Url};
|
use url::{ParseError, Url};
|
||||||
use urlencoding::encode;
|
use urlencoding::encode;
|
||||||
|
@ -542,7 +545,7 @@ pub fn local_site_opt_to_sensitive(local_site: &Option<LocalSite>) -> bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet> {
|
pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet> {
|
||||||
static URL_BLOCKLIST: Lazy<Cache<(), RegexSet>> = Lazy::new(|| {
|
static URL_BLOCKLIST: LazyLock<Cache<(), RegexSet>> = LazyLock::new(|| {
|
||||||
Cache::builder()
|
Cache::builder()
|
||||||
.max_capacity(1)
|
.max_capacity(1)
|
||||||
.time_to_live(CACHE_DURATION_FEDERATION)
|
.time_to_live(CACHE_DURATION_FEDERATION)
|
||||||
|
@ -949,7 +952,18 @@ pub async fn process_markdown(
|
||||||
|
|
||||||
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?;
|
|
||||||
|
// Create images and image detail rows
|
||||||
|
for link in links {
|
||||||
|
// Insert image details for the remote image
|
||||||
|
let details_res = fetch_pictrs_proxied_image_details(&link, context).await;
|
||||||
|
if let Ok(details) = details_res {
|
||||||
|
let proxied =
|
||||||
|
build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
|
||||||
|
let details_form = details.build_image_details_form(&proxied);
|
||||||
|
RemoteImage::create(&mut context.pool(), &details_form).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
Ok(text)
|
Ok(text)
|
||||||
} else {
|
} else {
|
||||||
Ok(text)
|
Ok(text)
|
||||||
|
@ -984,8 +998,14 @@ async fn proxy_image_link_internal(
|
||||||
Ok(link.into())
|
Ok(link.into())
|
||||||
} else if image_mode == PictrsImageMode::ProxyAllImages {
|
} else if image_mode == PictrsImageMode::ProxyAllImages {
|
||||||
let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
|
let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
|
||||||
|
// This should fail softly, since pictrs might not even be running
|
||||||
|
let details_res = fetch_pictrs_proxied_image_details(&link, context).await;
|
||||||
|
|
||||||
|
if let Ok(details) = details_res {
|
||||||
|
let details_form = details.build_image_details_form(&proxied);
|
||||||
|
RemoteImage::create(&mut context.pool(), &details_form).await?;
|
||||||
|
};
|
||||||
|
|
||||||
RemoteImage::create(&mut context.pool(), vec![link]).await?;
|
|
||||||
Ok(proxied.into())
|
Ok(proxied.into())
|
||||||
} else {
|
} else {
|
||||||
Ok(link.into())
|
Ok(link.into())
|
||||||
|
@ -1123,10 +1143,13 @@ mod tests {
|
||||||
"https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png",
|
"https://lemmy-alpha/api/v3/image_proxy?url=http%3A%2F%2Flemmy-beta%2Fimage.png",
|
||||||
proxied.as_str()
|
proxied.as_str()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// This fails, because the details can't be fetched without pictrs running,
|
||||||
|
// And a remote image won't be inserted.
|
||||||
assert!(
|
assert!(
|
||||||
RemoteImage::validate(&mut context.pool(), remote_image.into())
|
RemoteImage::validate(&mut context.pool(), remote_image.into())
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_err()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,6 @@ url = { workspace = true }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
moka.workspace = true
|
moka.workspace = true
|
||||||
once_cell.workspace = true
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
webmention = "0.5.0"
|
webmention = "0.5.0"
|
||||||
accept-language = "3.1.0"
|
accept-language = "3.1.0"
|
||||||
|
|
|
@ -8,20 +8,18 @@ use lemmy_api_common::{
|
||||||
utils::{
|
utils::{
|
||||||
check_community_user_action,
|
check_community_user_action,
|
||||||
check_post_deleted_or_removed,
|
check_post_deleted_or_removed,
|
||||||
generate_local_apub_endpoint,
|
|
||||||
get_url_blocklist,
|
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,
|
||||||
update_read_comments,
|
update_read_comments,
|
||||||
EndpointType,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
impls::actor_language::default_post_language,
|
impls::actor_language::default_post_language,
|
||||||
source::{
|
source::{
|
||||||
actor_language::CommunityLanguage,
|
actor_language::CommunityLanguage,
|
||||||
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm, CommentUpdateForm},
|
comment::{Comment, CommentInsertForm, CommentLike, CommentLikeForm},
|
||||||
comment_reply::{CommentReply, CommentReplyUpdateForm},
|
comment_reply::{CommentReply, CommentReplyUpdateForm},
|
||||||
local_site::LocalSite,
|
local_site::LocalSite,
|
||||||
person_mention::{PersonMention, PersonMentionUpdateForm},
|
person_mention::{PersonMention, PersonMentionUpdateForm},
|
||||||
|
@ -56,7 +54,7 @@ pub async fn create_comment(
|
||||||
let post_view = PostView::read(
|
let post_view = PostView::read(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
post_id,
|
post_id,
|
||||||
Some(local_user_view.person.id),
|
Some(&local_user_view.local_user),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
@ -126,25 +124,7 @@ pub async fn create_comment(
|
||||||
.await
|
.await
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntCreateComment)?;
|
.with_lemmy_type(LemmyErrorType::CouldntCreateComment)?;
|
||||||
|
|
||||||
// Necessary to update the ap_id
|
|
||||||
let inserted_comment_id = inserted_comment.id;
|
let inserted_comment_id = inserted_comment.id;
|
||||||
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
|
||||||
|
|
||||||
let apub_id = generate_local_apub_endpoint(
|
|
||||||
EndpointType::Comment,
|
|
||||||
&inserted_comment_id.to_string(),
|
|
||||||
&protocol_and_hostname,
|
|
||||||
)?;
|
|
||||||
let updated_comment = Comment::update(
|
|
||||||
&mut context.pool(),
|
|
||||||
inserted_comment_id,
|
|
||||||
&CommentUpdateForm {
|
|
||||||
ap_id: Some(apub_id),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntCreateComment)?;
|
|
||||||
|
|
||||||
// Scan the comment for user mentions, add those rows
|
// Scan the comment for user mentions, add those rows
|
||||||
let mentions = scrape_text_for_mentions(&content);
|
let mentions = scrape_text_for_mentions(&content);
|
||||||
|
@ -154,6 +134,7 @@ pub async fn create_comment(
|
||||||
&local_user_view.person,
|
&local_user_view.person,
|
||||||
true,
|
true,
|
||||||
&context,
|
&context,
|
||||||
|
Some(&local_user_view),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -170,7 +151,7 @@ pub async fn create_comment(
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntLikeComment)?;
|
.with_lemmy_type(LemmyErrorType::CouldntLikeComment)?;
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
ActivityChannel::submit_activity(
|
||||||
SendActivityData::CreateComment(updated_comment.clone()),
|
SendActivityData::CreateComment(inserted_comment.clone()),
|
||||||
&context,
|
&context,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
|
@ -21,9 +21,13 @@ pub async fn delete_comment(
|
||||||
local_user_view: LocalUserView,
|
local_user_view: LocalUserView,
|
||||||
) -> LemmyResult<Json<CommentResponse>> {
|
) -> LemmyResult<Json<CommentResponse>> {
|
||||||
let comment_id = data.comment_id;
|
let comment_id = data.comment_id;
|
||||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
|
let orig_comment = CommentView::read(
|
||||||
.await?
|
&mut context.pool(),
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
comment_id,
|
||||||
|
Some(&local_user_view.local_user),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
|
|
||||||
// Dont delete it if its already been deleted.
|
// Dont delete it if its already been deleted.
|
||||||
if orig_comment.comment.deleted == data.deleted {
|
if orig_comment.comment.deleted == data.deleted {
|
||||||
|
@ -55,8 +59,15 @@ pub async fn delete_comment(
|
||||||
.await
|
.await
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
.with_lemmy_type(LemmyErrorType::CouldntUpdateComment)?;
|
||||||
|
|
||||||
let recipient_ids =
|
let recipient_ids = send_local_notifs(
|
||||||
send_local_notifs(vec![], comment_id, &local_user_view.person, false, &context).await?;
|
vec![],
|
||||||
|
comment_id,
|
||||||
|
&local_user_view.person,
|
||||||
|
false,
|
||||||
|
&context,
|
||||||
|
Some(&local_user_view),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
let updated_comment_id = updated_comment.id;
|
let updated_comment_id = updated_comment.id;
|
||||||
|
|
||||||
ActivityChannel::submit_activity(
|
ActivityChannel::submit_activity(
|
||||||
|
|
|
@ -11,6 +11,7 @@ use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
comment::{Comment, CommentUpdateForm},
|
comment::{Comment, CommentUpdateForm},
|
||||||
comment_report::CommentReport,
|
comment_report::CommentReport,
|
||||||
|
local_user::LocalUser,
|
||||||
moderator::{ModRemoveComment, ModRemoveCommentForm},
|
moderator::{ModRemoveComment, ModRemoveCommentForm},
|
||||||
},
|
},
|
||||||
traits::{Crud, Reportable},
|
traits::{Crud, Reportable},
|
||||||
|
@ -25,9 +26,13 @@ pub async fn remove_comment(
|
||||||
local_user_view: LocalUserView,
|
local_user_view: LocalUserView,
|
||||||
) -> LemmyResult<Json<CommentResponse>> {
|
) -> LemmyResult<Json<CommentResponse>> {
|
||||||
let comment_id = data.comment_id;
|
let comment_id = data.comment_id;
|
||||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
|
let orig_comment = CommentView::read(
|
||||||
.await?
|
&mut context.pool(),
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
comment_id,
|
||||||
|
Some(&local_user_view.local_user),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
|
|
||||||
check_community_mod_action(
|
check_community_mod_action(
|
||||||
&local_user_view.person,
|
&local_user_view.person,
|
||||||
|
@ -37,6 +42,14 @@ pub async fn remove_comment(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
LocalUser::is_higher_mod_or_admin_check(
|
||||||
|
&mut context.pool(),
|
||||||
|
orig_comment.community.id,
|
||||||
|
local_user_view.person.id,
|
||||||
|
vec![orig_comment.creator.id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Don't allow removing or restoring comment which was deleted by user, as it would reveal
|
// Don't allow removing or restoring comment which was deleted by user, as it would reveal
|
||||||
// the comment text in mod log.
|
// the comment text in mod log.
|
||||||
if orig_comment.comment.deleted {
|
if orig_comment.comment.deleted {
|
||||||
|
@ -71,9 +84,10 @@ pub async fn remove_comment(
|
||||||
let recipient_ids = send_local_notifs(
|
let recipient_ids = send_local_notifs(
|
||||||
vec![],
|
vec![],
|
||||||
comment_id,
|
comment_id,
|
||||||
&local_user_view.person.clone(),
|
&local_user_view.person,
|
||||||
false,
|
false,
|
||||||
&context,
|
&context,
|
||||||
|
Some(&local_user_view),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let updated_comment_id = updated_comment.id;
|
let updated_comment_id = updated_comment.id;
|
||||||
|
|
|
@ -36,9 +36,13 @@ pub async fn update_comment(
|
||||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||||
|
|
||||||
let comment_id = data.comment_id;
|
let comment_id = data.comment_id;
|
||||||
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
|
let orig_comment = CommentView::read(
|
||||||
.await?
|
&mut context.pool(),
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
comment_id,
|
||||||
|
Some(&local_user_view.local_user),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindComment)?;
|
||||||
|
|
||||||
check_community_user_action(
|
check_community_user_action(
|
||||||
&local_user_view.person,
|
&local_user_view.person,
|
||||||
|
@ -87,6 +91,7 @@ pub async fn update_comment(
|
||||||
&local_user_view.person,
|
&local_user_view.person,
|
||||||
false,
|
false,
|
||||||
&context,
|
&context,
|
||||||
|
Some(&local_user_view),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
@ -8,13 +8,11 @@ use lemmy_api_common::{
|
||||||
send_activity::SendActivityData,
|
send_activity::SendActivityData,
|
||||||
utils::{
|
utils::{
|
||||||
check_community_user_action,
|
check_community_user_action,
|
||||||
generate_local_apub_endpoint,
|
|
||||||
get_url_blocklist,
|
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,
|
||||||
process_markdown_opt,
|
process_markdown_opt,
|
||||||
EndpointType,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
@ -23,7 +21,7 @@ use lemmy_db_schema::{
|
||||||
actor_language::CommunityLanguage,
|
actor_language::CommunityLanguage,
|
||||||
community::Community,
|
community::Community,
|
||||||
local_site::LocalSite,
|
local_site::LocalSite,
|
||||||
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
|
post::{Post, PostInsertForm, PostLike, PostLikeForm},
|
||||||
},
|
},
|
||||||
traits::{Crud, Likeable},
|
traits::{Crud, Likeable},
|
||||||
utils::diesel_url_create,
|
utils::diesel_url_create,
|
||||||
|
@ -37,11 +35,11 @@ use lemmy_utils::{
|
||||||
utils::{
|
utils::{
|
||||||
slurs::check_slurs,
|
slurs::check_slurs,
|
||||||
validation::{
|
validation::{
|
||||||
check_url_scheme,
|
|
||||||
is_url_blocked,
|
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,
|
||||||
|
is_valid_url,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -71,11 +69,11 @@ pub async fn create_post(
|
||||||
|
|
||||||
if let Some(url) = &url {
|
if let Some(url) = &url {
|
||||||
is_url_blocked(url, &url_blocklist)?;
|
is_url_blocked(url, &url_blocklist)?;
|
||||||
check_url_scheme(url)?;
|
is_valid_url(url)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(custom_thumbnail) = &custom_thumbnail {
|
if let Some(custom_thumbnail) = &custom_thumbnail {
|
||||||
check_url_scheme(custom_thumbnail)?;
|
is_valid_url(custom_thumbnail)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(alt_text) = &data.alt_text {
|
if let Some(alt_text) = &data.alt_text {
|
||||||
|
@ -147,26 +145,8 @@ pub async fn create_post(
|
||||||
.await
|
.await
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
|
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
|
||||||
|
|
||||||
let inserted_post_id = inserted_post.id;
|
|
||||||
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
|
||||||
let apub_id = generate_local_apub_endpoint(
|
|
||||||
EndpointType::Post,
|
|
||||||
&inserted_post_id.to_string(),
|
|
||||||
&protocol_and_hostname,
|
|
||||||
)?;
|
|
||||||
let updated_post = Post::update(
|
|
||||||
&mut context.pool(),
|
|
||||||
inserted_post_id,
|
|
||||||
&PostUpdateForm {
|
|
||||||
ap_id: Some(apub_id),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntCreatePost)?;
|
|
||||||
|
|
||||||
generate_post_link_metadata(
|
generate_post_link_metadata(
|
||||||
updated_post.clone(),
|
inserted_post.clone(),
|
||||||
custom_thumbnail.map(Into::into),
|
custom_thumbnail.map(Into::into),
|
||||||
|post| Some(SendActivityData::CreatePost(post)),
|
|post| Some(SendActivityData::CreatePost(post)),
|
||||||
Some(local_site),
|
Some(local_site),
|
||||||
|
@ -189,11 +169,11 @@ pub async fn create_post(
|
||||||
|
|
||||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||||
|
|
||||||
if let Some(url) = updated_post.url.clone() {
|
if let Some(url) = inserted_post.url.clone() {
|
||||||
if community.visibility == CommunityVisibility::Public {
|
if community.visibility == CommunityVisibility::Public {
|
||||||
spawn_try_task(async move {
|
spawn_try_task(async move {
|
||||||
let mut webmention =
|
let mut webmention =
|
||||||
Webmention::new::<Url>(updated_post.ap_id.clone().into(), url.clone().into())?;
|
Webmention::new::<Url>(inserted_post.ap_id.clone().into(), url.clone().into())?;
|
||||||
webmention.set_checked(true);
|
webmention.set_checked(true);
|
||||||
match webmention
|
match webmention
|
||||||
.send()
|
.send()
|
||||||
|
@ -208,5 +188,5 @@ pub async fn create_post(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
build_post_response(&context, community_id, &local_user_view.person, post_id).await
|
build_post_response(&context, community_id, local_user_view, post_id).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -62,7 +62,7 @@ pub async fn delete_post(
|
||||||
build_post_response(
|
build_post_response(
|
||||||
&context,
|
&context,
|
||||||
orig_post.community_id,
|
orig_post.community_id,
|
||||||
&local_user_view.person,
|
local_user_view,
|
||||||
data.post_id,
|
data.post_id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -55,9 +55,15 @@ pub async fn get_post(
|
||||||
.await
|
.await
|
||||||
.is_ok();
|
.is_ok();
|
||||||
|
|
||||||
let post_view = PostView::read(&mut context.pool(), post_id, person_id, is_mod_or_admin)
|
let local_user = local_user_view.map(|l| l.local_user);
|
||||||
.await?
|
let post_view = PostView::read(
|
||||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
&mut context.pool(),
|
||||||
|
post_id,
|
||||||
|
local_user.as_ref(),
|
||||||
|
is_mod_or_admin,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||||
|
|
||||||
let post_id = post_view.post.id;
|
let post_id = post_view.post.id;
|
||||||
if let Some(person_id) = person_id {
|
if let Some(person_id) = person_id {
|
||||||
|
@ -76,20 +82,19 @@ pub async fn get_post(
|
||||||
let community_view = CommunityView::read(
|
let community_view = CommunityView::read(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
community_id,
|
community_id,
|
||||||
person_id,
|
local_user.as_ref(),
|
||||||
is_mod_or_admin,
|
is_mod_or_admin,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||||
|
|
||||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
||||||
let local_user = local_user_view.as_ref().map(|u| &u.local_user);
|
|
||||||
|
|
||||||
// Fetch the cross_posts
|
// Fetch the cross_posts
|
||||||
let cross_posts = if let Some(url) = &post_view.post.url {
|
let cross_posts = if let Some(url) = &post_view.post.url {
|
||||||
let mut x_posts = PostQuery {
|
let mut x_posts = PostQuery {
|
||||||
url_search: Some(url.inner().as_str().into()),
|
url_search: Some(url.inner().as_str().into()),
|
||||||
local_user,
|
local_user: local_user.as_ref(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.list(&local_site.site, &mut context.pool())
|
.list(&local_site.site, &mut context.pool())
|
||||||
|
|
|
@ -9,6 +9,7 @@ use lemmy_api_common::{
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
|
local_user::LocalUser,
|
||||||
moderator::{ModRemovePost, ModRemovePostForm},
|
moderator::{ModRemovePost, ModRemovePostForm},
|
||||||
post::{Post, PostUpdateForm},
|
post::{Post, PostUpdateForm},
|
||||||
post_report::PostReport,
|
post_report::PostReport,
|
||||||
|
@ -37,6 +38,14 @@ pub async fn remove_post(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
LocalUser::is_higher_mod_or_admin_check(
|
||||||
|
&mut context.pool(),
|
||||||
|
orig_post.community_id,
|
||||||
|
local_user_view.person.id,
|
||||||
|
vec![orig_post.creator_id],
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Update the post
|
// Update the post
|
||||||
let post_id = data.post_id;
|
let post_id = data.post_id;
|
||||||
let removed = data.removed;
|
let removed = data.removed;
|
||||||
|
@ -73,11 +82,5 @@ pub async fn remove_post(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
build_post_response(
|
build_post_response(&context, orig_post.community_id, local_user_view, post_id).await
|
||||||
&context,
|
|
||||||
orig_post.community_id,
|
|
||||||
&local_user_view.person,
|
|
||||||
post_id,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,11 +28,11 @@ use lemmy_utils::{
|
||||||
utils::{
|
utils::{
|
||||||
slurs::check_slurs,
|
slurs::check_slurs,
|
||||||
validation::{
|
validation::{
|
||||||
check_url_scheme,
|
|
||||||
is_url_blocked,
|
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,
|
||||||
|
is_valid_url,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -77,11 +77,11 @@ pub async fn update_post(
|
||||||
|
|
||||||
if let Some(Some(url)) = &url {
|
if let Some(Some(url)) = &url {
|
||||||
is_url_blocked(url, &url_blocklist)?;
|
is_url_blocked(url, &url_blocklist)?;
|
||||||
check_url_scheme(url)?;
|
is_valid_url(url)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Some(custom_thumbnail)) = &custom_thumbnail {
|
if let Some(Some(custom_thumbnail)) = &custom_thumbnail {
|
||||||
check_url_scheme(custom_thumbnail)?;
|
is_valid_url(custom_thumbnail)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let post_id = data.post_id;
|
let post_id = data.post_id;
|
||||||
|
@ -137,7 +137,7 @@ pub async fn update_post(
|
||||||
build_post_response(
|
build_post_response(
|
||||||
context.deref(),
|
context.deref(),
|
||||||
orig_post.community_id,
|
orig_post.community_id,
|
||||||
&local_user_view.person,
|
local_user_view,
|
||||||
post_id,
|
post_id,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
|
@ -6,19 +6,17 @@ use lemmy_api_common::{
|
||||||
send_activity::{ActivityChannel, SendActivityData},
|
send_activity::{ActivityChannel, SendActivityData},
|
||||||
utils::{
|
utils::{
|
||||||
check_person_block,
|
check_person_block,
|
||||||
generate_local_apub_endpoint,
|
|
||||||
get_interface_language,
|
get_interface_language,
|
||||||
get_url_blocklist,
|
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,
|
||||||
EndpointType,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
local_site::LocalSite,
|
local_site::LocalSite,
|
||||||
private_message::{PrivateMessage, PrivateMessageInsertForm, PrivateMessageUpdateForm},
|
private_message::{PrivateMessage, PrivateMessageInsertForm},
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
};
|
};
|
||||||
|
@ -58,24 +56,6 @@ pub async fn create_private_message(
|
||||||
.await
|
.await
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?;
|
.with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?;
|
||||||
|
|
||||||
let inserted_private_message_id = inserted_private_message.id;
|
|
||||||
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
|
||||||
let apub_id = generate_local_apub_endpoint(
|
|
||||||
EndpointType::PrivateMessage,
|
|
||||||
&inserted_private_message_id.to_string(),
|
|
||||||
&protocol_and_hostname,
|
|
||||||
)?;
|
|
||||||
PrivateMessage::update(
|
|
||||||
&mut context.pool(),
|
|
||||||
inserted_private_message.id,
|
|
||||||
&PrivateMessageUpdateForm {
|
|
||||||
ap_id: Some(apub_id),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?;
|
|
||||||
|
|
||||||
let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id)
|
let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
|
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
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 activitypub_federation::http_signatures::generate_actor_keypair;
|
use activitypub_federation::{config::Data, http_signatures::generate_actor_keypair};
|
||||||
use actix_web::web::{Data, Json};
|
use actix_web::web::Json;
|
||||||
use lemmy_api_common::{
|
use lemmy_api_common::{
|
||||||
context::LemmyContext,
|
context::LemmyContext,
|
||||||
site::{CreateSite, SiteResponse},
|
site::{CreateSite, SiteResponse},
|
||||||
|
|
|
@ -24,14 +24,14 @@ use lemmy_utils::{
|
||||||
VERSION,
|
VERSION,
|
||||||
};
|
};
|
||||||
use moka::future::Cache;
|
use moka::future::Cache;
|
||||||
use once_cell::sync::Lazy;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
#[tracing::instrument(skip(context))]
|
||||||
pub async fn get_site(
|
pub async fn get_site(
|
||||||
local_user_view: Option<LocalUserView>,
|
local_user_view: Option<LocalUserView>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
) -> LemmyResult<Json<GetSiteResponse>> {
|
) -> LemmyResult<Json<GetSiteResponse>> {
|
||||||
static CACHE: Lazy<Cache<(), GetSiteResponse>> = Lazy::new(|| {
|
static CACHE: LazyLock<Cache<(), GetSiteResponse>> = LazyLock::new(|| {
|
||||||
Cache::builder()
|
Cache::builder()
|
||||||
.max_capacity(1)
|
.max_capacity(1)
|
||||||
.time_to_live(CACHE_DURATION_API)
|
.time_to_live(CACHE_DURATION_API)
|
||||||
|
@ -84,7 +84,7 @@ pub async fn get_site(
|
||||||
|pool| CommunityBlockView::for_person(pool, person_id),
|
|pool| CommunityBlockView::for_person(pool, person_id),
|
||||||
|pool| InstanceBlockView::for_person(pool, person_id),
|
|pool| InstanceBlockView::for_person(pool, person_id),
|
||||||
|pool| PersonBlockView::for_person(pool, person_id),
|
|pool| PersonBlockView::for_person(pool, person_id),
|
||||||
|pool| CommunityModeratorView::for_person(pool, person_id, true),
|
|pool| CommunityModeratorView::for_person(pool, person_id, Some(&local_user_view.local_user)),
|
||||||
|pool| LocalUserLanguage::read(pool, local_user_id)
|
|pool| LocalUserLanguage::read(pool, local_user_id)
|
||||||
))
|
))
|
||||||
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
|
.with_lemmy_type(LemmyErrorType::SystemErrLogin)?;
|
||||||
|
|
|
@ -150,18 +150,18 @@ pub async fn register(
|
||||||
.unwrap_or(site_view.site.content_warning.is_some());
|
.unwrap_or(site_view.site.content_warning.is_some());
|
||||||
|
|
||||||
// Create the local user
|
// Create the local user
|
||||||
let local_user_form = LocalUserInsertForm::builder()
|
let local_user_form = LocalUserInsertForm {
|
||||||
.person_id(inserted_person.id)
|
email: data.email.as_deref().map(str::to_lowercase),
|
||||||
.email(data.email.as_deref().map(str::to_lowercase))
|
password_encrypted: data.password.to_string(),
|
||||||
.password_encrypted(data.password.to_string())
|
show_nsfw: Some(show_nsfw),
|
||||||
.show_nsfw(Some(show_nsfw))
|
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_tags.first().cloned(),
|
||||||
.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();
|
..LocalUserInsertForm::new(inserted_person.id, data.password.to_string())
|
||||||
|
};
|
||||||
|
|
||||||
let all_languages = Language::read_all(&mut context.pool()).await?;
|
let all_languages = Language::read_all(&mut context.pool()).await?;
|
||||||
// use hashset to avoid duplicates
|
// use hashset to avoid duplicates
|
||||||
|
|
|
@ -31,7 +31,7 @@ serde = { workspace = true }
|
||||||
actix-web = { workspace = true }
|
actix-web = { workspace = true }
|
||||||
tokio = { workspace = true }
|
tokio = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
strum_macros = { workspace = true }
|
strum = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
http = { workspace = true }
|
http = { workspace = true }
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
@ -40,7 +40,6 @@ uuid = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
|
||||||
moka.workspace = true
|
moka.workspace = true
|
||||||
serde_with.workspace = true
|
serde_with.workspace = true
|
||||||
html2md = "0.2.14"
|
html2md = "0.2.14"
|
||||||
|
|
|
@ -38,7 +38,6 @@ pub enum SiteOrCommunity {
|
||||||
Site(ApubSite),
|
Site(ApubSite),
|
||||||
Community(ApubCommunity),
|
Community(ApubCommunity),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum InstanceOrGroup {
|
pub enum InstanceOrGroup {
|
||||||
|
@ -74,12 +73,18 @@ impl Object for SiteOrCommunity {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(self, _data: &Data<Self::DataType>) -> LemmyResult<()> {
|
async fn delete(self, data: &Data<Self::DataType>) -> LemmyResult<()> {
|
||||||
unimplemented!()
|
match self {
|
||||||
|
SiteOrCommunity::Site(i) => i.delete(data).await,
|
||||||
|
SiteOrCommunity::Community(c) => c.delete(data).await,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn into_json(self, _data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
async fn into_json(self, data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
||||||
unimplemented!()
|
Ok(match self {
|
||||||
|
SiteOrCommunity::Site(i) => InstanceOrGroup::Instance(i.into_json(data).await?),
|
||||||
|
SiteOrCommunity::Community(c) => InstanceOrGroup::Group(c.into_json(data).await?),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
|
|
@ -179,7 +179,7 @@ impl ActivityHandler for CreateOrUpdateNote {
|
||||||
// TODO: for compatibility with other projects, it would be much better to read this from cc or
|
// TODO: for compatibility with other projects, it would be much better to read this from cc or
|
||||||
// tags
|
// tags
|
||||||
let mentions = scrape_text_for_mentions(&comment.content);
|
let mentions = scrape_text_for_mentions(&comment.content);
|
||||||
send_local_notifs(mentions, comment.id, &actor, do_send_email, context).await?;
|
send_local_notifs(mentions, comment.id, &actor, do_send_email, context, None).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -175,8 +175,9 @@ pub(in crate::activities) async fn receive_remove_action(
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
DeletableObjects::PrivateMessage(_) => unimplemented!(),
|
// TODO these need to be implemented yet, for now, return errors
|
||||||
DeletableObjects::Person { .. } => unimplemented!(),
|
DeletableObjects::PrivateMessage(_) => Err(LemmyErrorType::CouldntFindPrivateMessage)?,
|
||||||
|
DeletableObjects::Person(_) => Err(LemmyErrorType::CouldntFindPerson)?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -155,8 +155,9 @@ impl UndoDelete {
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
DeletableObjects::PrivateMessage(_) => unimplemented!(),
|
// TODO these need to be implemented yet, for now, return errors
|
||||||
DeletableObjects::Person { .. } => unimplemented!(),
|
DeletableObjects::PrivateMessage(_) => Err(LemmyErrorType::CouldntFindPrivateMessage)?,
|
||||||
|
DeletableObjects::Person(_) => Err(LemmyErrorType::CouldntFindPerson)?,
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use activitypub_federation::{config::Data, traits::ActivityHandler};
|
use activitypub_federation::{config::Data, traits::ActivityHandler};
|
||||||
use lemmy_api_common::context::LemmyContext;
|
use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_utils::error::LemmyResult;
|
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ impl InCommunity for AnnouncableActivities {
|
||||||
CollectionRemove(a) => a.community(context).await,
|
CollectionRemove(a) => a.community(context).await,
|
||||||
LockPost(a) => a.community(context).await,
|
LockPost(a) => a.community(context).await,
|
||||||
UndoLockPost(a) => a.community(context).await,
|
UndoLockPost(a) => a.community(context).await,
|
||||||
Page(_) => unimplemented!(),
|
Page(_) => Err(LemmyErrorType::CouldntFindPost.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,11 +37,11 @@ pub async fn list_comments(
|
||||||
};
|
};
|
||||||
let sort = data.sort;
|
let sort = data.sort;
|
||||||
let max_depth = data.max_depth;
|
let max_depth = data.max_depth;
|
||||||
let saved_only = data.saved_only.unwrap_or_default();
|
let saved_only = data.saved_only;
|
||||||
|
|
||||||
let liked_only = data.liked_only.unwrap_or_default();
|
let liked_only = data.liked_only;
|
||||||
let disliked_only = data.disliked_only.unwrap_or_default();
|
let disliked_only = data.disliked_only;
|
||||||
if liked_only && disliked_only {
|
if liked_only.unwrap_or_default() && disliked_only.unwrap_or_default() {
|
||||||
return Err(LemmyError::from(LemmyErrorType::ContradictingFilters));
|
return Err(LemmyError::from(LemmyErrorType::ContradictingFilters));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,12 +40,14 @@ pub async fn list_posts(
|
||||||
} else {
|
} else {
|
||||||
data.community_id
|
data.community_id
|
||||||
};
|
};
|
||||||
let saved_only = data.saved_only.unwrap_or_default();
|
let saved_only = data.saved_only;
|
||||||
let show_hidden = data.show_hidden.unwrap_or_default();
|
let show_hidden = data.show_hidden;
|
||||||
|
let show_read = data.show_read;
|
||||||
|
let show_nsfw = data.show_nsfw;
|
||||||
|
|
||||||
let liked_only = data.liked_only.unwrap_or_default();
|
let liked_only = data.liked_only;
|
||||||
let disliked_only = data.disliked_only.unwrap_or_default();
|
let disliked_only = data.disliked_only;
|
||||||
if liked_only && disliked_only {
|
if liked_only.unwrap_or_default() && disliked_only.unwrap_or_default() {
|
||||||
return Err(LemmyError::from(LemmyErrorType::ContradictingFilters));
|
return Err(LemmyError::from(LemmyErrorType::ContradictingFilters));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,6 +84,8 @@ pub async fn list_posts(
|
||||||
page_after,
|
page_after,
|
||||||
limit,
|
limit,
|
||||||
show_hidden,
|
show_hidden,
|
||||||
|
show_read,
|
||||||
|
show_nsfw,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
.list(&local_site.site, &mut context.pool())
|
.list(&local_site.site, &mut context.pool())
|
||||||
|
|
|
@ -29,7 +29,7 @@ pub async fn get_community(
|
||||||
|
|
||||||
check_private_instance(&local_user_view, &local_site)?;
|
check_private_instance(&local_user_view, &local_site)?;
|
||||||
|
|
||||||
let person_id = local_user_view.as_ref().map(|u| u.person.id);
|
let local_user = local_user_view.as_ref().map(|u| &u.local_user);
|
||||||
|
|
||||||
let community_id = match data.id {
|
let community_id = match data.id {
|
||||||
Some(id) => id,
|
Some(id) => id,
|
||||||
|
@ -53,7 +53,7 @@ pub async fn get_community(
|
||||||
let community_view = CommunityView::read(
|
let community_view = CommunityView::read(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
community_id,
|
community_id,
|
||||||
person_id,
|
local_user,
|
||||||
is_mod_or_admin,
|
is_mod_or_admin,
|
||||||
)
|
)
|
||||||
.await?
|
.await?
|
||||||
|
|
|
@ -55,11 +55,11 @@ pub async fn read_person(
|
||||||
let sort = data.sort;
|
let sort = data.sort;
|
||||||
let page = data.page;
|
let page = data.page;
|
||||||
let limit = data.limit;
|
let limit = data.limit;
|
||||||
let saved_only = data.saved_only.unwrap_or_default();
|
let saved_only = data.saved_only;
|
||||||
let community_id = data.community_id;
|
let community_id = data.community_id;
|
||||||
// If its saved only, you don't care what creator it was
|
// If its saved only, you don't care what creator it was
|
||||||
// Or, if its not saved, then you only want it for that specific creator
|
// Or, if its not saved, then you only want it for that specific creator
|
||||||
let creator_id = if !saved_only {
|
let creator_id = if !saved_only.unwrap_or_default() {
|
||||||
Some(person_details_id)
|
Some(person_details_id)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
@ -96,7 +96,7 @@ pub async fn read_person(
|
||||||
let moderates = CommunityModeratorView::for_person(
|
let moderates = CommunityModeratorView::for_person(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
person_details_id,
|
person_details_id,
|
||||||
local_user_view.is_some(),
|
local_user_view.map(|l| l.local_user).as_ref(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ use lemmy_api_common::{
|
||||||
site::{ResolveObject, ResolveObjectResponse},
|
site::{ResolveObject, ResolveObjectResponse},
|
||||||
utils::check_private_instance,
|
utils::check_private_instance,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{newtypes::PersonId, source::local_site::LocalSite, utils::DbPool};
|
use lemmy_db_schema::{source::local_site::LocalSite, utils::DbPool};
|
||||||
use lemmy_db_views::structs::{CommentView, LocalUserView, PostView};
|
use lemmy_db_views::structs::{CommentView, LocalUserView, PostView};
|
||||||
use lemmy_db_views_actor::structs::{CommunityView, PersonView};
|
use lemmy_db_views_actor::structs::{CommunityView, PersonView};
|
||||||
use lemmy_utils::error::{LemmyErrorExt2, LemmyErrorType, LemmyResult};
|
use lemmy_utils::error::{LemmyErrorExt2, LemmyErrorType, LemmyResult};
|
||||||
|
@ -23,10 +23,9 @@ pub async fn resolve_object(
|
||||||
) -> LemmyResult<Json<ResolveObjectResponse>> {
|
) -> LemmyResult<Json<ResolveObjectResponse>> {
|
||||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||||
check_private_instance(&local_user_view, &local_site)?;
|
check_private_instance(&local_user_view, &local_site)?;
|
||||||
let person_id = local_user_view.map(|v| v.person.id);
|
|
||||||
// If we get a valid personId back we can safely assume that the user is authenticated,
|
// If we get a valid personId back we can safely assume that the user is authenticated,
|
||||||
// if there's no personId then the JWT was missing or invalid.
|
// if there's no personId then the JWT was missing or invalid.
|
||||||
let is_authenticated = person_id.is_some();
|
let is_authenticated = local_user_view.is_some();
|
||||||
|
|
||||||
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.
|
||||||
|
@ -37,24 +36,26 @@ pub async fn resolve_object(
|
||||||
}
|
}
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntFindObject)?;
|
.with_lemmy_type(LemmyErrorType::CouldntFindObject)?;
|
||||||
|
|
||||||
convert_response(res, person_id, &mut context.pool())
|
convert_response(res, local_user_view, &mut context.pool())
|
||||||
.await
|
.await
|
||||||
.with_lemmy_type(LemmyErrorType::CouldntFindObject)
|
.with_lemmy_type(LemmyErrorType::CouldntFindObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn convert_response(
|
async fn convert_response(
|
||||||
object: SearchableObjects,
|
object: SearchableObjects,
|
||||||
user_id: Option<PersonId>,
|
local_user_view: Option<LocalUserView>,
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
) -> LemmyResult<Json<ResolveObjectResponse>> {
|
) -> LemmyResult<Json<ResolveObjectResponse>> {
|
||||||
use SearchableObjects::*;
|
use SearchableObjects::*;
|
||||||
let removed_or_deleted;
|
let removed_or_deleted;
|
||||||
let mut res = ResolveObjectResponse::default();
|
let mut res = ResolveObjectResponse::default();
|
||||||
|
let local_user = local_user_view.map(|l| l.local_user);
|
||||||
|
|
||||||
match object {
|
match object {
|
||||||
Post(p) => {
|
Post(p) => {
|
||||||
removed_or_deleted = p.deleted || p.removed;
|
removed_or_deleted = p.deleted || p.removed;
|
||||||
res.post = Some(
|
res.post = Some(
|
||||||
PostView::read(pool, p.id, user_id, false)
|
PostView::read(pool, p.id, local_user.as_ref(), false)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindPost)?,
|
.ok_or(LemmyErrorType::CouldntFindPost)?,
|
||||||
)
|
)
|
||||||
|
@ -62,7 +63,7 @@ async fn convert_response(
|
||||||
Comment(c) => {
|
Comment(c) => {
|
||||||
removed_or_deleted = c.deleted || c.removed;
|
removed_or_deleted = c.deleted || c.removed;
|
||||||
res.comment = Some(
|
res.comment = Some(
|
||||||
CommentView::read(pool, c.id, user_id)
|
CommentView::read(pool, c.id, local_user.as_ref())
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindComment)?,
|
.ok_or(LemmyErrorType::CouldntFindComment)?,
|
||||||
)
|
)
|
||||||
|
@ -79,7 +80,7 @@ async fn convert_response(
|
||||||
UserOrCommunity::Community(c) => {
|
UserOrCommunity::Community(c) => {
|
||||||
removed_or_deleted = c.deleted || c.removed;
|
removed_or_deleted = c.deleted || c.removed;
|
||||||
res.community = Some(
|
res.community = Some(
|
||||||
CommunityView::read(pool, c.id, user_id, false)
|
CommunityView::read(pool, c.id, local_user.as_ref(), false)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?,
|
.ok_or(LemmyErrorType::CouldntFindCommunity)?,
|
||||||
)
|
)
|
||||||
|
|
|
@ -345,10 +345,7 @@ mod tests {
|
||||||
};
|
};
|
||||||
let person = Person::create(&mut context.pool(), &person_form).await?;
|
let person = Person::create(&mut context.pool(), &person_form).await?;
|
||||||
|
|
||||||
let user_form = LocalUserInsertForm::builder()
|
let user_form = LocalUserInsertForm::test_form(person.id);
|
||||||
.person_id(person.id)
|
|
||||||
.password_encrypted("pass".to_string())
|
|
||||||
.build();
|
|
||||||
let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?;
|
let local_user = LocalUser::create(&mut context.pool(), &user_form, vec![]).await?;
|
||||||
|
|
||||||
Ok(
|
Ok(
|
||||||
|
|
|
@ -61,8 +61,11 @@ impl Object for PostOrComment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn into_json(self, _data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
async fn into_json(self, data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
||||||
unimplemented!()
|
Ok(match self {
|
||||||
|
PostOrComment::Post(p) => PageOrNote::Page(Box::new(p.into_json(data).await?)),
|
||||||
|
PostOrComment::Comment(c) => PageOrNote::Note(c.into_json(data).await?),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
|
|
@ -118,8 +118,17 @@ impl Object for SearchableObjects {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn into_json(self, _data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
async fn into_json(self, data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
||||||
unimplemented!()
|
Ok(match self {
|
||||||
|
SearchableObjects::Post(p) => SearchableKinds::Page(Box::new(p.into_json(data).await?)),
|
||||||
|
SearchableObjects::Comment(c) => SearchableKinds::Note(c.into_json(data).await?),
|
||||||
|
SearchableObjects::PersonOrCommunity(pc) => {
|
||||||
|
SearchableKinds::PersonOrGroup(Box::new(match *pc {
|
||||||
|
UserOrCommunity::User(p) => PersonOrGroup::Person(p.into_json(data).await?),
|
||||||
|
UserOrCommunity::Community(c) => PersonOrGroup::Group(c.into_json(data).await?),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
fetcher::user_or_community::{PersonOrGroup, UserOrCommunity},
|
fetcher::user_or_community::{PersonOrGroup, UserOrCommunity},
|
||||||
objects::instance::ApubSite,
|
objects::{community::ApubCommunity, instance::ApubSite, person::ApubPerson},
|
||||||
protocol::objects::instance::Instance,
|
protocol::objects::instance::Instance,
|
||||||
};
|
};
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
|
@ -41,11 +41,14 @@ impl Object for SiteOrCommunityOrUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn read_from_id(
|
async fn read_from_id(object_id: Url, data: &Data<Self::DataType>) -> LemmyResult<Option<Self>> {
|
||||||
_object_id: Url,
|
let site = ApubSite::read_from_id(object_id.clone(), data).await?;
|
||||||
_data: &Data<Self::DataType>,
|
Ok(match site {
|
||||||
) -> LemmyResult<Option<Self>> {
|
Some(o) => Some(SiteOrCommunityOrUser::Site(o)),
|
||||||
unimplemented!();
|
None => UserOrCommunity::read_from_id(object_id, data)
|
||||||
|
.await?
|
||||||
|
.map(SiteOrCommunityOrUser::UserOrCommunity),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
@ -56,8 +59,13 @@ impl Object for SiteOrCommunityOrUser {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn into_json(self, _data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
async fn into_json(self, data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
||||||
unimplemented!()
|
Ok(match self {
|
||||||
|
SiteOrCommunityOrUser::Site(p) => SiteOrPersonOrGroup::Instance(p.into_json(data).await?),
|
||||||
|
SiteOrCommunityOrUser::UserOrCommunity(p) => {
|
||||||
|
SiteOrPersonOrGroup::PersonOrGroup(p.into_json(data).await?)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
@ -75,8 +83,18 @@ impl Object for SiteOrCommunityOrUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn from_json(_apub: Self::Kind, _data: &Data<Self::DataType>) -> LemmyResult<Self> {
|
async fn from_json(apub: Self::Kind, data: &Data<Self::DataType>) -> LemmyResult<Self> {
|
||||||
unimplemented!();
|
Ok(match apub {
|
||||||
|
SiteOrPersonOrGroup::Instance(a) => {
|
||||||
|
SiteOrCommunityOrUser::Site(ApubSite::from_json(a, data).await?)
|
||||||
|
}
|
||||||
|
SiteOrPersonOrGroup::PersonOrGroup(a) => SiteOrCommunityOrUser::UserOrCommunity(match a {
|
||||||
|
PersonOrGroup::Person(p) => UserOrCommunity::User(ApubPerson::from_json(p, data).await?),
|
||||||
|
PersonOrGroup::Group(g) => {
|
||||||
|
UserOrCommunity::Community(ApubCommunity::from_json(g, data).await?)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,6 +121,9 @@ impl Actor for SiteOrCommunityOrUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inbox(&self) -> Url {
|
fn inbox(&self) -> Url {
|
||||||
unimplemented!()
|
match self {
|
||||||
|
SiteOrCommunityOrUser::Site(u) => u.inbox(),
|
||||||
|
SiteOrCommunityOrUser::UserOrCommunity(c) => c.inbox(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,8 +65,11 @@ impl Object for UserOrCommunity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn into_json(self, _data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
async fn into_json(self, data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
||||||
unimplemented!()
|
Ok(match self {
|
||||||
|
UserOrCommunity::User(p) => PersonOrGroup::Person(p.into_json(data).await?),
|
||||||
|
UserOrCommunity::Community(p) => PersonOrGroup::Group(p.into_json(data).await?),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
@ -115,7 +118,10 @@ impl Actor for UserOrCommunity {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inbox(&self) -> Url {
|
fn inbox(&self) -> Url {
|
||||||
unimplemented!()
|
match self {
|
||||||
|
UserOrCommunity::User(p) => p.inbox(),
|
||||||
|
UserOrCommunity::Community(p) => p.inbox(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,8 @@ use lemmy_utils::{
|
||||||
CACHE_DURATION_FEDERATION,
|
CACHE_DURATION_FEDERATION,
|
||||||
};
|
};
|
||||||
use moka::future::Cache;
|
use moka::future::Cache;
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, LazyLock};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub mod activities;
|
pub mod activities;
|
||||||
|
@ -36,7 +35,7 @@ pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 100;
|
||||||
/// Only include a basic context to save space and bandwidth. The main context is hosted statically
|
/// Only include a basic context to save space and bandwidth. The main context is hosted statically
|
||||||
/// on join-lemmy.org. Include activitystreams explicitly for better compat, but this could
|
/// on join-lemmy.org. Include activitystreams explicitly for better compat, but this could
|
||||||
/// theoretically also be moved.
|
/// theoretically also be moved.
|
||||||
pub static FEDERATION_CONTEXT: Lazy<Value> = Lazy::new(|| {
|
pub static FEDERATION_CONTEXT: LazyLock<Value> = LazyLock::new(|| {
|
||||||
Value::Array(vec![
|
Value::Array(vec![
|
||||||
Value::String("https://join-lemmy.org/context.json".to_string()),
|
Value::String("https://join-lemmy.org/context.json".to_string()),
|
||||||
Value::String("https://www.w3.org/ns/activitystreams".to_string()),
|
Value::String("https://www.w3.org/ns/activitystreams".to_string()),
|
||||||
|
@ -129,7 +128,7 @@ pub(crate) async fn local_site_data_cached(
|
||||||
// multiple times. This causes a huge number of database reads if we hit the db directly. So we
|
// multiple times. This causes a huge number of database reads if we hit the db directly. So we
|
||||||
// cache these values for a short time, which will already make a huge difference and ensures that
|
// cache these values for a short time, which will already make a huge difference and ensures that
|
||||||
// changes take effect quickly.
|
// changes take effect quickly.
|
||||||
static CACHE: Lazy<Cache<(), Arc<LocalSiteData>>> = Lazy::new(|| {
|
static CACHE: LazyLock<Cache<(), Arc<LocalSiteData>>> = LazyLock::new(|| {
|
||||||
Cache::builder()
|
Cache::builder()
|
||||||
.max_capacity(1)
|
.max_capacity(1)
|
||||||
.time_to_live(CACHE_DURATION_FEDERATION)
|
.time_to_live(CACHE_DURATION_FEDERATION)
|
||||||
|
|
|
@ -88,7 +88,7 @@ impl Object for ApubSite {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn delete(self, _data: &Data<Self::DataType>) -> LemmyResult<()> {
|
async fn delete(self, _data: &Data<Self::DataType>) -> LemmyResult<()> {
|
||||||
unimplemented!()
|
Err(LemmyErrorType::CantDeleteSite.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
@ -109,7 +109,7 @@ impl Object for ApubSite {
|
||||||
icon: self.icon.clone().map(ImageObject::new),
|
icon: self.icon.clone().map(ImageObject::new),
|
||||||
image: self.banner.clone().map(ImageObject::new),
|
image: self.banner.clone().map(ImageObject::new),
|
||||||
inbox: self.inbox_url.clone().into(),
|
inbox: self.inbox_url.clone().into(),
|
||||||
outbox: Url::parse(&format!("{}/site_outbox", self.actor_id))?,
|
outbox: Url::parse(&format!("{}site_outbox", self.actor_id))?,
|
||||||
public_key: self.public_key(),
|
public_key: self.public_key(),
|
||||||
language,
|
language,
|
||||||
content_warning: self.content_warning.clone(),
|
content_warning: self.content_warning.clone(),
|
||||||
|
|
|
@ -41,7 +41,11 @@ use lemmy_db_views_actor::structs::CommunityModeratorView;
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
error::{LemmyError, LemmyErrorType, LemmyResult},
|
error::{LemmyError, LemmyErrorType, LemmyResult},
|
||||||
spawn_try_task,
|
spawn_try_task,
|
||||||
utils::{markdown::markdown_to_html, slurs::check_slurs_opt, validation::check_url_scheme},
|
utils::{
|
||||||
|
markdown::markdown_to_html,
|
||||||
|
slurs::check_slurs_opt,
|
||||||
|
validation::{is_url_blocked, is_valid_url},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use stringreader::StringReader;
|
use stringreader::StringReader;
|
||||||
|
@ -180,8 +184,15 @@ impl Object for ApubPost {
|
||||||
let creator = page.creator()?.dereference(context).await?;
|
let creator = page.creator()?.dereference(context).await?;
|
||||||
let community = page.community(context).await?;
|
let community = page.community(context).await?;
|
||||||
if community.posting_restricted_to_mods {
|
if community.posting_restricted_to_mods {
|
||||||
CommunityModeratorView::is_community_moderator(&mut context.pool(), community.id, creator.id)
|
let is_mod = CommunityModeratorView::is_community_moderator(
|
||||||
.await?;
|
&mut context.pool(),
|
||||||
|
community.id,
|
||||||
|
creator.id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if !is_mod {
|
||||||
|
Err(LemmyErrorType::OnlyModsCanPostInCommunity)?
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let mut name = page
|
let mut name = page
|
||||||
.name
|
.name
|
||||||
|
@ -220,14 +231,16 @@ impl Object for ApubPost {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let url_blocklist = get_url_blocklist(context).await?;
|
||||||
|
|
||||||
if let Some(url) = &url {
|
if let Some(url) = &url {
|
||||||
check_url_scheme(url)?;
|
is_url_blocked(url, &url_blocklist)?;
|
||||||
|
is_valid_url(url)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let alt_text = first_attachment.cloned().and_then(Attachment::alt_text);
|
let alt_text = first_attachment.cloned().and_then(Attachment::alt_text);
|
||||||
|
|
||||||
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, &url_blocklist, context).await?;
|
let body = process_markdown_opt(&body, slur_regex, &url_blocklist, context).await?;
|
||||||
|
|
|
@ -73,7 +73,7 @@ impl Object for ApubPrivateMessage {
|
||||||
|
|
||||||
async fn delete(self, _context: &Data<Self::DataType>) -> LemmyResult<()> {
|
async fn delete(self, _context: &Data<Self::DataType>) -> LemmyResult<()> {
|
||||||
// do nothing, because pm can't be fetched over http
|
// do nothing, because pm can't be fetched over http
|
||||||
unimplemented!()
|
Err(LemmyErrorType::CouldntFindPrivateMessage.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
|
|
|
@ -13,10 +13,10 @@ use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_db_schema::{source::community::Community, traits::Crud};
|
use lemmy_db_schema::{source::community::Community, traits::Crud};
|
||||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum_macros::Display;
|
use strum::Display;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, Display)]
|
#[derive(Clone, Debug, Display, Deserialize, Serialize)]
|
||||||
pub enum LockType {
|
pub enum LockType {
|
||||||
Lock,
|
Lock,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum_macros::Display;
|
use strum::Display;
|
||||||
|
|
||||||
pub mod block;
|
pub mod block;
|
||||||
pub mod community;
|
pub mod community;
|
||||||
|
|
|
@ -8,7 +8,7 @@ use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
|
||||||
use lemmy_api_common::context::LemmyContext;
|
use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
|
use lemmy_utils::error::{LemmyError, LemmyErrorType, LemmyResult};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use strum_macros::Display;
|
use strum::Display;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
|
|
@ -7,7 +7,7 @@ use crate::{
|
||||||
community_outbox::ApubCommunityOutbox,
|
community_outbox::ApubCommunityOutbox,
|
||||||
},
|
},
|
||||||
local_site_data_cached,
|
local_site_data_cached,
|
||||||
objects::{community::ApubCommunity, read_from_string_or_source_opt, verify_is_remote_object},
|
objects::{community::ApubCommunity, read_from_string_or_source_opt},
|
||||||
protocol::{
|
protocol::{
|
||||||
objects::{Endpoints, LanguageTag},
|
objects::{Endpoints, LanguageTag},
|
||||||
ImageObject,
|
ImageObject,
|
||||||
|
@ -80,7 +80,6 @@ impl Group {
|
||||||
) -> LemmyResult<()> {
|
) -> LemmyResult<()> {
|
||||||
check_apub_id_valid_with_strictness(self.id.inner(), true, context).await?;
|
check_apub_id_valid_with_strictness(self.id.inner(), true, context).await?;
|
||||||
verify_domains_match(expected_domain, self.id.inner())?;
|
verify_domains_match(expected_domain, self.id.inner())?;
|
||||||
verify_is_remote_object(&self.id, context)?;
|
|
||||||
|
|
||||||
let local_site_data = local_site_data_cached(&mut context.pool()).await?;
|
let local_site_data = local_site_data_cached(&mut context.pool()).await?;
|
||||||
let slur_regex = &local_site_opt_to_slur_regex(&local_site_data.local_site);
|
let slur_regex = &local_site_opt_to_slur_regex(&local_site_data.local_site);
|
||||||
|
|
|
@ -193,10 +193,12 @@ impl ActivityHandler for Page {
|
||||||
type DataType = LemmyContext;
|
type DataType = LemmyContext;
|
||||||
type Error = LemmyError;
|
type Error = LemmyError;
|
||||||
fn id(&self) -> &Url {
|
fn id(&self) -> &Url {
|
||||||
unimplemented!()
|
self.id.inner()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn actor(&self) -> &Url {
|
fn actor(&self) -> &Url {
|
||||||
unimplemented!()
|
debug_assert!(false);
|
||||||
|
self.id.inner()
|
||||||
}
|
}
|
||||||
async fn verify(&self, data: &Data<Self::DataType>) -> LemmyResult<()> {
|
async fn verify(&self, data: &Data<Self::DataType>) -> LemmyResult<()> {
|
||||||
ApubPost::verify(self, self.id.inner(), data).await
|
ApubPost::verify(self, self.id.inner(), data).await
|
||||||
|
|
|
@ -27,7 +27,6 @@ full = [
|
||||||
"lemmy_utils",
|
"lemmy_utils",
|
||||||
"activitypub_federation",
|
"activitypub_federation",
|
||||||
"regex",
|
"regex",
|
||||||
"once_cell",
|
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"diesel_ltree",
|
"diesel_ltree",
|
||||||
"diesel-async",
|
"diesel-async",
|
||||||
|
@ -46,7 +45,6 @@ serde = { workspace = true }
|
||||||
serde_with = { workspace = true }
|
serde_with = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
strum_macros = { workspace = true }
|
|
||||||
serde_json = { workspace = true, optional = true }
|
serde_json = { workspace = true, optional = true }
|
||||||
activitypub_federation = { workspace = true, optional = true }
|
activitypub_federation = { workspace = true, optional = true }
|
||||||
lemmy_utils = { workspace = true, optional = true }
|
lemmy_utils = { workspace = true, optional = true }
|
||||||
|
@ -65,7 +63,6 @@ diesel-async = { workspace = true, features = [
|
||||||
"deadpool",
|
"deadpool",
|
||||||
], optional = true }
|
], optional = true }
|
||||||
regex = { workspace = true, optional = true }
|
regex = { workspace = true, optional = true }
|
||||||
once_cell = { workspace = true, optional = true }
|
|
||||||
diesel_ltree = { workspace = true, optional = true }
|
diesel_ltree = { workspace = true, optional = true }
|
||||||
typed-builder = { workspace = true }
|
typed-builder = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
|
|
|
@ -564,6 +564,10 @@ BEGIN
|
||||||
IF NOT (NEW.path ~ ('*.' || id)::lquery) THEN
|
IF NOT (NEW.path ~ ('*.' || id)::lquery) THEN
|
||||||
NEW.path = NEW.path || id;
|
NEW.path = NEW.path || id;
|
||||||
END IF;
|
END IF;
|
||||||
|
-- Set local ap_id
|
||||||
|
IF NEW.local THEN
|
||||||
|
NEW.ap_id = coalesce(NEW.ap_id, r.local_url ('/comment/' || id));
|
||||||
|
END IF;
|
||||||
RETURN NEW;
|
RETURN NEW;
|
||||||
END
|
END
|
||||||
$$;
|
$$;
|
||||||
|
@ -573,3 +577,39 @@ CREATE TRIGGER change_values
|
||||||
FOR EACH ROW
|
FOR EACH ROW
|
||||||
EXECUTE FUNCTION r.comment_change_values ();
|
EXECUTE FUNCTION r.comment_change_values ();
|
||||||
|
|
||||||
|
CREATE FUNCTION r.post_change_values ()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Set local ap_id
|
||||||
|
IF NEW.local THEN
|
||||||
|
NEW.ap_id = coalesce(NEW.ap_id, r.local_url ('/post/' || NEW.id::text));
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER change_values
|
||||||
|
BEFORE INSERT ON post
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION r.post_change_values ();
|
||||||
|
|
||||||
|
CREATE FUNCTION r.private_message_change_values ()
|
||||||
|
RETURNS TRIGGER
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
-- Set local ap_id
|
||||||
|
IF NEW.local THEN
|
||||||
|
NEW.ap_id = coalesce(NEW.ap_id, r.local_url ('/private_message/' || NEW.id::text));
|
||||||
|
END IF;
|
||||||
|
RETURN NEW;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TRIGGER change_values
|
||||||
|
BEFORE INSERT ON private_message
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION r.private_message_change_values ();
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,7 @@ CREATE FUNCTION r.controversy_rank (upvotes numeric, downvotes numeric)
|
||||||
0
|
0
|
||||||
ELSE
|
ELSE
|
||||||
(
|
(
|
||||||
upvotes + downvotes) * CASE WHEN upvotes > downvotes THEN
|
upvotes + downvotes) ^ CASE WHEN upvotes > downvotes THEN
|
||||||
downvotes::float / upvotes::float
|
downvotes::float / upvotes::float
|
||||||
ELSE
|
ELSE
|
||||||
upvotes::float / downvotes::float
|
upvotes::float / downvotes::float
|
||||||
|
@ -57,6 +57,13 @@ BEGIN
|
||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
CREATE FUNCTION r.local_url (url_path text)
|
||||||
|
RETURNS text
|
||||||
|
LANGUAGE sql
|
||||||
|
STABLE PARALLEL SAFE RETURN (
|
||||||
|
current_setting('lemmy.protocol_and_hostname') || url_path
|
||||||
|
);
|
||||||
|
|
||||||
-- This function creates statement-level triggers for all operation types. It's designed this way
|
-- This function creates statement-level triggers for all operation types. It's designed this way
|
||||||
-- because of these limitations:
|
-- because of these limitations:
|
||||||
-- * A trigger that uses transition tables can only handle 1 operation type.
|
-- * A trigger that uses transition tables can only handle 1 operation type.
|
||||||
|
|
|
@ -533,10 +533,7 @@ mod tests {
|
||||||
|
|
||||||
let person_form = PersonInsertForm::test_form(instance.id, "my test person");
|
let person_form = PersonInsertForm::test_form(instance.id, "my test person");
|
||||||
let person = Person::create(pool, &person_form).await.unwrap();
|
let person = Person::create(pool, &person_form).await.unwrap();
|
||||||
let local_user_form = LocalUserInsertForm::builder()
|
let local_user_form = LocalUserInsertForm::test_form(person.id);
|
||||||
.person_id(person.id)
|
|
||||||
.password_encrypted("my_pw".to_string())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let local_user = LocalUser::create(pool, &local_user_form, vec![])
|
let local_user = LocalUser::create(pool, &local_user_form, vec![])
|
||||||
.await
|
.await
|
||||||
|
@ -645,10 +642,7 @@ mod tests {
|
||||||
|
|
||||||
let person_form = PersonInsertForm::test_form(instance.id, "my test person");
|
let person_form = PersonInsertForm::test_form(instance.id, "my test person");
|
||||||
let person = Person::create(pool, &person_form).await.unwrap();
|
let person = Person::create(pool, &person_form).await.unwrap();
|
||||||
let local_user_form = LocalUserInsertForm::builder()
|
let local_user_form = LocalUserInsertForm::test_form(person.id);
|
||||||
.person_id(person.id)
|
|
||||||
.password_encrypted("my_pw".to_string())
|
|
||||||
.build();
|
|
||||||
let local_user = LocalUser::create(pool, &local_user_form, vec![])
|
let local_user = LocalUser::create(pool, &local_user_form, vec![])
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
|
@ -117,7 +117,7 @@ impl Crud for Comment {
|
||||||
type UpdateForm = CommentUpdateForm;
|
type UpdateForm = CommentUpdateForm;
|
||||||
type IdType = CommentId;
|
type IdType = CommentId;
|
||||||
|
|
||||||
/// This is unimplemented, use [[Comment::create]]
|
/// Use [[Comment::create]]
|
||||||
async fn create(pool: &mut DbPool<'_>, comment_form: &Self::InsertForm) -> Result<Self, Error> {
|
async fn create(pool: &mut DbPool<'_>, comment_form: &Self::InsertForm) -> Result<Self, Error> {
|
||||||
debug_assert!(false);
|
debug_assert!(false);
|
||||||
Comment::create(pool, comment_form, None).await
|
Comment::create(pool, comment_form, None).await
|
||||||
|
@ -223,6 +223,7 @@ mod tests {
|
||||||
use diesel_ltree::Ltree;
|
use diesel_ltree::Ltree;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial]
|
#[serial]
|
||||||
|
@ -273,7 +274,12 @@ mod tests {
|
||||||
path: Ltree(format!("0.{}", inserted_comment.id)),
|
path: Ltree(format!("0.{}", inserted_comment.id)),
|
||||||
published: inserted_comment.published,
|
published: inserted_comment.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
ap_id: inserted_comment.ap_id.clone(),
|
ap_id: Url::parse(&format!(
|
||||||
|
"https://lemmy-alpha/comment/{}",
|
||||||
|
inserted_comment.id
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
.into(),
|
||||||
distinguished: false,
|
distinguished: false,
|
||||||
local: true,
|
local: true,
|
||||||
language_id: LanguageId::default(),
|
language_id: LanguageId::default(),
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
diesel::{DecoratableTarget, OptionalExtension},
|
diesel::{DecoratableTarget, OptionalExtension},
|
||||||
newtypes::{CommunityId, DbUrl, PersonId},
|
newtypes::{CommunityId, DbUrl, PersonId},
|
||||||
schema::{community, community_follower, instance},
|
schema::{
|
||||||
|
community,
|
||||||
|
community_follower,
|
||||||
|
community_moderator,
|
||||||
|
community_person_ban,
|
||||||
|
instance,
|
||||||
|
post,
|
||||||
|
},
|
||||||
source::{
|
source::{
|
||||||
actor_language::CommunityLanguage,
|
actor_language::CommunityLanguage,
|
||||||
community::{
|
community::{
|
||||||
|
@ -42,6 +49,7 @@ use diesel::{
|
||||||
Queryable,
|
Queryable,
|
||||||
};
|
};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
|
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl Crud for Community {
|
impl Crud for Community {
|
||||||
|
@ -83,9 +91,8 @@ impl Joinable for CommunityModerator {
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
community_moderator_form: &CommunityModeratorForm,
|
community_moderator_form: &CommunityModeratorForm,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
use crate::schema::community_moderator::dsl::community_moderator;
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
insert_into(community_moderator)
|
insert_into(community_moderator::table)
|
||||||
.values(community_moderator_form)
|
.values(community_moderator_form)
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
.await
|
.await
|
||||||
|
@ -95,9 +102,8 @@ impl Joinable for CommunityModerator {
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
community_moderator_form: &CommunityModeratorForm,
|
community_moderator_form: &CommunityModeratorForm,
|
||||||
) -> Result<usize, Error> {
|
) -> Result<usize, Error> {
|
||||||
use crate::schema::community_moderator::dsl::community_moderator;
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
diesel::delete(community_moderator.find((
|
diesel::delete(community_moderator::table.find((
|
||||||
community_moderator_form.person_id,
|
community_moderator_form.person_id,
|
||||||
community_moderator_form.community_id,
|
community_moderator_form.community_id,
|
||||||
)))
|
)))
|
||||||
|
@ -147,25 +153,23 @@ impl Community {
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
url: &DbUrl,
|
url: &DbUrl,
|
||||||
) -> Result<Option<(Community, CollectionType)>, Error> {
|
) -> Result<Option<(Community, CollectionType)>, Error> {
|
||||||
use crate::schema::community::dsl::{featured_url, moderators_url};
|
|
||||||
use CollectionType::*;
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
let res = community::table
|
let res = community::table
|
||||||
.filter(moderators_url.eq(url))
|
.filter(community::moderators_url.eq(url))
|
||||||
.first(conn)
|
.first(conn)
|
||||||
.await
|
.await
|
||||||
.optional()?;
|
.optional()?;
|
||||||
|
|
||||||
if let Some(c) = res {
|
if let Some(c) = res {
|
||||||
Ok(Some((c, Moderators)))
|
Ok(Some((c, CollectionType::Moderators)))
|
||||||
} else {
|
} else {
|
||||||
let res = community::table
|
let res = community::table
|
||||||
.filter(featured_url.eq(url))
|
.filter(community::featured_url.eq(url))
|
||||||
.first(conn)
|
.first(conn)
|
||||||
.await
|
.await
|
||||||
.optional()?;
|
.optional()?;
|
||||||
if let Some(c) = res {
|
if let Some(c) = res {
|
||||||
Ok(Some((c, Featured)))
|
Ok(Some((c, CollectionType::Featured)))
|
||||||
} else {
|
} else {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
@ -177,7 +181,6 @@ impl Community {
|
||||||
posts: Vec<Post>,
|
posts: Vec<Post>,
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
) -> Result<(), Error> {
|
) -> Result<(), Error> {
|
||||||
use crate::schema::post;
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
for p in &posts {
|
for p in &posts {
|
||||||
debug_assert!(p.community_id == community_id);
|
debug_assert!(p.community_id == community_id);
|
||||||
|
@ -185,10 +188,10 @@ impl Community {
|
||||||
// Mark the given posts as featured and all other posts as not featured.
|
// Mark the given posts as featured and all other posts as not featured.
|
||||||
let post_ids = posts.iter().map(|p| p.id);
|
let post_ids = posts.iter().map(|p| p.id);
|
||||||
update(post::table)
|
update(post::table)
|
||||||
.filter(post::dsl::community_id.eq(community_id))
|
.filter(post::community_id.eq(community_id))
|
||||||
// This filter is just for performance
|
// This filter is just for performance
|
||||||
.filter(post::dsl::featured_community.or(post::dsl::id.eq_any(post_ids.clone())))
|
.filter(post::featured_community.or(post::id.eq_any(post_ids.clone())))
|
||||||
.set(post::dsl::featured_community.eq(post::dsl::id.eq_any(post_ids)))
|
.set(post::featured_community.eq(post::id.eq_any(post_ids)))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -200,37 +203,68 @@ impl CommunityModerator {
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
for_community_id: CommunityId,
|
for_community_id: CommunityId,
|
||||||
) -> Result<usize, Error> {
|
) -> Result<usize, Error> {
|
||||||
use crate::schema::community_moderator::dsl::{community_id, community_moderator};
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
|
||||||
diesel::delete(community_moderator.filter(community_id.eq(for_community_id)))
|
diesel::delete(
|
||||||
.execute(conn)
|
community_moderator::table.filter(community_moderator::community_id.eq(for_community_id)),
|
||||||
.await
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn leave_all_communities(
|
pub async fn leave_all_communities(
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
for_person_id: PersonId,
|
for_person_id: PersonId,
|
||||||
) -> Result<usize, Error> {
|
) -> Result<usize, Error> {
|
||||||
use crate::schema::community_moderator::dsl::{community_moderator, person_id};
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
diesel::delete(community_moderator.filter(person_id.eq(for_person_id)))
|
diesel::delete(
|
||||||
.execute(conn)
|
community_moderator::table.filter(community_moderator::person_id.eq(for_person_id)),
|
||||||
.await
|
)
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_person_moderated_communities(
|
pub async fn get_person_moderated_communities(
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
for_person_id: PersonId,
|
for_person_id: PersonId,
|
||||||
) -> Result<Vec<CommunityId>, Error> {
|
) -> Result<Vec<CommunityId>, Error> {
|
||||||
use crate::schema::community_moderator::dsl::{community_id, community_moderator, person_id};
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
community_moderator
|
community_moderator::table
|
||||||
.filter(person_id.eq(for_person_id))
|
.filter(community_moderator::person_id.eq(for_person_id))
|
||||||
.select(community_id)
|
.select(community_moderator::community_id)
|
||||||
.load::<CommunityId>(conn)
|
.load::<CommunityId>(conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks to make sure the acting moderator was added earlier than the target moderator
|
||||||
|
pub async fn is_higher_mod_check(
|
||||||
|
pool: &mut DbPool<'_>,
|
||||||
|
for_community_id: CommunityId,
|
||||||
|
mod_person_id: PersonId,
|
||||||
|
target_person_ids: Vec<PersonId>,
|
||||||
|
) -> LemmyResult<()> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
|
||||||
|
// Build the list of persons
|
||||||
|
let mut persons = target_person_ids;
|
||||||
|
persons.push(mod_person_id);
|
||||||
|
persons.dedup();
|
||||||
|
|
||||||
|
let res = community_moderator::table
|
||||||
|
.filter(community_moderator::community_id.eq(for_community_id))
|
||||||
|
.filter(community_moderator::person_id.eq_any(persons))
|
||||||
|
.order_by(community_moderator::published)
|
||||||
|
// This does a limit 1 select first
|
||||||
|
.first::<CommunityModerator>(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// If the first result sorted by published is the acting mod
|
||||||
|
if res.person_id == mod_person_id {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(LemmyErrorType::NotHigherMod)?
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -240,11 +274,13 @@ impl Bannable for CommunityPersonBan {
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
community_person_ban_form: &CommunityPersonBanForm,
|
community_person_ban_form: &CommunityPersonBanForm,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
use crate::schema::community_person_ban::dsl::{community_id, community_person_ban, person_id};
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
insert_into(community_person_ban)
|
insert_into(community_person_ban::table)
|
||||||
.values(community_person_ban_form)
|
.values(community_person_ban_form)
|
||||||
.on_conflict((community_id, person_id))
|
.on_conflict((
|
||||||
|
community_person_ban::community_id,
|
||||||
|
community_person_ban::person_id,
|
||||||
|
))
|
||||||
.do_update()
|
.do_update()
|
||||||
.set(community_person_ban_form)
|
.set(community_person_ban_form)
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
|
@ -255,9 +291,8 @@ impl Bannable for CommunityPersonBan {
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
community_person_ban_form: &CommunityPersonBanForm,
|
community_person_ban_form: &CommunityPersonBanForm,
|
||||||
) -> Result<usize, Error> {
|
) -> Result<usize, Error> {
|
||||||
use crate::schema::community_person_ban::dsl::community_person_ban;
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
diesel::delete(community_person_ban.find((
|
diesel::delete(community_person_ban::table.find((
|
||||||
community_person_ban_form.person_id,
|
community_person_ban_form.person_id,
|
||||||
community_person_ban_form.community_id,
|
community_person_ban_form.community_id,
|
||||||
)))
|
)))
|
||||||
|
@ -291,11 +326,10 @@ impl CommunityFollower {
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
remote_community_id: CommunityId,
|
remote_community_id: CommunityId,
|
||||||
) -> Result<bool, Error> {
|
) -> Result<bool, Error> {
|
||||||
use crate::schema::community_follower::dsl::{community_follower, community_id};
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
select(exists(
|
select(exists(community_follower::table.filter(
|
||||||
community_follower.filter(community_id.eq(remote_community_id)),
|
community_follower::community_id.eq(remote_community_id),
|
||||||
))
|
)))
|
||||||
.get_result(conn)
|
.get_result(conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -316,11 +350,13 @@ impl Queryable<sql_types::Nullable<sql_types::Bool>, Pg> for SubscribedType {
|
||||||
impl Followable for CommunityFollower {
|
impl Followable for CommunityFollower {
|
||||||
type Form = CommunityFollowerForm;
|
type Form = CommunityFollowerForm;
|
||||||
async fn follow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result<Self, Error> {
|
async fn follow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result<Self, Error> {
|
||||||
use crate::schema::community_follower::dsl::{community_follower, community_id, person_id};
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
insert_into(community_follower)
|
insert_into(community_follower::table)
|
||||||
.values(form)
|
.values(form)
|
||||||
.on_conflict((community_id, person_id))
|
.on_conflict((
|
||||||
|
community_follower::community_id,
|
||||||
|
community_follower::person_id,
|
||||||
|
))
|
||||||
.do_update()
|
.do_update()
|
||||||
.set(form)
|
.set(form)
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
|
@ -331,17 +367,16 @@ impl Followable for CommunityFollower {
|
||||||
community_id: CommunityId,
|
community_id: CommunityId,
|
||||||
person_id: PersonId,
|
person_id: PersonId,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
use crate::schema::community_follower::dsl::{community_follower, pending};
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
diesel::update(community_follower.find((person_id, community_id)))
|
diesel::update(community_follower::table.find((person_id, community_id)))
|
||||||
.set(pending.eq(false))
|
.set(community_follower::pending.eq(false))
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unfollow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result<usize, Error> {
|
async fn unfollow(pool: &mut DbPool<'_>, form: &CommunityFollowerForm) -> Result<usize, Error> {
|
||||||
use crate::schema::community_follower::dsl::community_follower;
|
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
diesel::delete(community_follower.find((form.person_id, form.community_id)))
|
diesel::delete(community_follower::table.find((form.person_id, form.community_id)))
|
||||||
.execute(conn)
|
.execute(conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -397,10 +432,8 @@ impl ApubActor for Community {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
#[allow(clippy::indexing_slicing)]
|
#[allow(clippy::indexing_slicing)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
source::{
|
source::{
|
||||||
community::{
|
community::{
|
||||||
|
@ -415,28 +448,30 @@ mod tests {
|
||||||
CommunityUpdateForm,
|
CommunityUpdateForm,
|
||||||
},
|
},
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
|
local_user::LocalUser,
|
||||||
person::{Person, PersonInsertForm},
|
person::{Person, PersonInsertForm},
|
||||||
},
|
},
|
||||||
traits::{Bannable, Crud, Followable, Joinable},
|
traits::{Bannable, Crud, Followable, Joinable},
|
||||||
utils::build_db_pool_for_tests,
|
utils::build_db_pool_for_tests,
|
||||||
CommunityVisibility,
|
CommunityVisibility,
|
||||||
};
|
};
|
||||||
|
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
|
|
||||||
#[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 inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string())
|
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let new_person = PersonInsertForm::test_form(inserted_instance.id, "bobbee");
|
let bobby_person = PersonInsertForm::test_form(inserted_instance.id, "bobby");
|
||||||
|
let inserted_bobby = Person::create(pool, &bobby_person).await?;
|
||||||
|
|
||||||
let inserted_person = Person::create(pool, &new_person).await.unwrap();
|
let artemis_person = PersonInsertForm::test_form(inserted_instance.id, "artemis");
|
||||||
|
let inserted_artemis = Person::create(pool, &artemis_person).await?;
|
||||||
|
|
||||||
let new_community = CommunityInsertForm::builder()
|
let new_community = CommunityInsertForm::builder()
|
||||||
.name("TIL".into())
|
.name("TIL".into())
|
||||||
|
@ -445,7 +480,7 @@ mod tests {
|
||||||
.instance_id(inserted_instance.id)
|
.instance_id(inserted_instance.id)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let inserted_community = Community::create(pool, &new_community).await.unwrap();
|
let inserted_community = Community::create(pool, &new_community).await?;
|
||||||
|
|
||||||
let expected_community = Community {
|
let expected_community = Community {
|
||||||
id: inserted_community.id,
|
id: inserted_community.id,
|
||||||
|
@ -477,91 +512,120 @@ mod tests {
|
||||||
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
let community_follower_form = CommunityFollowerForm {
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
person_id: inserted_person.id,
|
person_id: inserted_bobby.id,
|
||||||
pending: false,
|
pending: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community_follower = CommunityFollower::follow(pool, &community_follower_form)
|
let inserted_community_follower =
|
||||||
.await
|
CommunityFollower::follow(pool, &community_follower_form).await?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let expected_community_follower = CommunityFollower {
|
let expected_community_follower = CommunityFollower {
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
person_id: inserted_person.id,
|
person_id: inserted_bobby.id,
|
||||||
pending: false,
|
pending: false,
|
||||||
published: inserted_community_follower.published,
|
published: inserted_community_follower.published,
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_moderator_form = CommunityModeratorForm {
|
let bobby_moderator_form = CommunityModeratorForm {
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
person_id: inserted_person.id,
|
person_id: inserted_bobby.id,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community_moderator = CommunityModerator::join(pool, &community_moderator_form)
|
let inserted_bobby_moderator = CommunityModerator::join(pool, &bobby_moderator_form).await?;
|
||||||
.await
|
|
||||||
.unwrap();
|
let artemis_moderator_form = CommunityModeratorForm {
|
||||||
|
community_id: inserted_community.id,
|
||||||
|
person_id: inserted_artemis.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _inserted_artemis_moderator =
|
||||||
|
CommunityModerator::join(pool, &artemis_moderator_form).await?;
|
||||||
|
|
||||||
let expected_community_moderator = CommunityModerator {
|
let expected_community_moderator = CommunityModerator {
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
person_id: inserted_person.id,
|
person_id: inserted_bobby.id,
|
||||||
published: inserted_community_moderator.published,
|
published: inserted_bobby_moderator.published,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let moderator_person_ids = vec![inserted_bobby.id, inserted_artemis.id];
|
||||||
|
|
||||||
|
// Make sure bobby is marked as a higher mod than artemis, and vice versa
|
||||||
|
let bobby_higher_check = CommunityModerator::is_higher_mod_check(
|
||||||
|
pool,
|
||||||
|
inserted_community.id,
|
||||||
|
inserted_bobby.id,
|
||||||
|
moderator_person_ids.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(bobby_higher_check.is_ok());
|
||||||
|
|
||||||
|
// Also check the other is_higher_mod_or_admin function just in case
|
||||||
|
let bobby_higher_check_2 = LocalUser::is_higher_mod_or_admin_check(
|
||||||
|
pool,
|
||||||
|
inserted_community.id,
|
||||||
|
inserted_bobby.id,
|
||||||
|
moderator_person_ids.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(bobby_higher_check_2.is_ok());
|
||||||
|
|
||||||
|
// This should throw an error, since artemis was added later
|
||||||
|
let artemis_higher_check = CommunityModerator::is_higher_mod_check(
|
||||||
|
pool,
|
||||||
|
inserted_community.id,
|
||||||
|
inserted_artemis.id,
|
||||||
|
moderator_person_ids,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(artemis_higher_check.is_err());
|
||||||
|
|
||||||
let community_person_ban_form = CommunityPersonBanForm {
|
let community_person_ban_form = CommunityPersonBanForm {
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
person_id: inserted_person.id,
|
person_id: inserted_bobby.id,
|
||||||
expires: None,
|
expires: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community_person_ban = CommunityPersonBan::ban(pool, &community_person_ban_form)
|
let inserted_community_person_ban =
|
||||||
.await
|
CommunityPersonBan::ban(pool, &community_person_ban_form).await?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let expected_community_person_ban = CommunityPersonBan {
|
let expected_community_person_ban = CommunityPersonBan {
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
person_id: inserted_person.id,
|
person_id: inserted_bobby.id,
|
||||||
published: inserted_community_person_ban.published,
|
published: inserted_community_person_ban.published,
|
||||||
expires: None,
|
expires: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let read_community = Community::read(pool, inserted_community.id)
|
let read_community = Community::read(pool, inserted_community.id)
|
||||||
.await
|
.await?
|
||||||
.unwrap()
|
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let update_community_form = CommunityUpdateForm {
|
let update_community_form = CommunityUpdateForm {
|
||||||
title: Some("nada".to_owned()),
|
title: Some("nada".to_owned()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
let updated_community = Community::update(pool, inserted_community.id, &update_community_form)
|
let updated_community =
|
||||||
.await
|
Community::update(pool, inserted_community.id, &update_community_form).await?;
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form)
|
let ignored_community = CommunityFollower::unfollow(pool, &community_follower_form).await?;
|
||||||
.await
|
let left_community = CommunityModerator::leave(pool, &bobby_moderator_form).await?;
|
||||||
.unwrap();
|
let unban = CommunityPersonBan::unban(pool, &community_person_ban_form).await?;
|
||||||
let left_community = CommunityModerator::leave(pool, &community_moderator_form)
|
let num_deleted = Community::delete(pool, inserted_community.id).await?;
|
||||||
.await
|
Person::delete(pool, inserted_bobby.id).await?;
|
||||||
.unwrap();
|
Person::delete(pool, inserted_artemis.id).await?;
|
||||||
let unban = CommunityPersonBan::unban(pool, &community_person_ban_form)
|
Instance::delete(pool, inserted_instance.id).await?;
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let num_deleted = Community::delete(pool, inserted_community.id)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
Person::delete(pool, inserted_person.id).await.unwrap();
|
|
||||||
Instance::delete(pool, inserted_instance.id).await.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(expected_community, read_community);
|
assert_eq!(expected_community, read_community);
|
||||||
assert_eq!(expected_community, inserted_community);
|
assert_eq!(expected_community, inserted_community);
|
||||||
assert_eq!(expected_community, updated_community);
|
assert_eq!(expected_community, updated_community);
|
||||||
assert_eq!(expected_community_follower, inserted_community_follower);
|
assert_eq!(expected_community_follower, inserted_community_follower);
|
||||||
assert_eq!(expected_community_moderator, inserted_community_moderator);
|
assert_eq!(expected_community_moderator, inserted_bobby_moderator);
|
||||||
assert_eq!(expected_community_person_ban, inserted_community_person_ban);
|
assert_eq!(expected_community_person_ban, inserted_community_person_ban);
|
||||||
assert_eq!(1, ignored_community);
|
assert_eq!(1, ignored_community);
|
||||||
assert_eq!(1, left_community);
|
assert_eq!(1, left_community);
|
||||||
assert_eq!(1, unban);
|
assert_eq!(1, unban);
|
||||||
// assert_eq!(2, loaded_count);
|
// assert_eq!(2, loaded_count);
|
||||||
assert_eq!(1, num_deleted);
|
assert_eq!(1, num_deleted);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
newtypes::DbUrl,
|
newtypes::DbUrl,
|
||||||
schema::{local_image, remote_image},
|
schema::{image_details, local_image, remote_image},
|
||||||
source::images::{LocalImage, LocalImageForm, RemoteImage, RemoteImageForm},
|
source::images::{
|
||||||
|
ImageDetails,
|
||||||
|
ImageDetailsForm,
|
||||||
|
LocalImage,
|
||||||
|
LocalImageForm,
|
||||||
|
RemoteImage,
|
||||||
|
RemoteImageForm,
|
||||||
|
},
|
||||||
utils::{get_conn, DbPool},
|
utils::{get_conn, DbPool},
|
||||||
};
|
};
|
||||||
use diesel::{
|
use diesel::{
|
||||||
|
@ -13,15 +20,29 @@ use diesel::{
|
||||||
NotFound,
|
NotFound,
|
||||||
QueryDsl,
|
QueryDsl,
|
||||||
};
|
};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::{AsyncPgConnection, RunQueryDsl};
|
||||||
use url::Url;
|
|
||||||
|
|
||||||
impl LocalImage {
|
impl LocalImage {
|
||||||
pub async fn create(pool: &mut DbPool<'_>, form: &LocalImageForm) -> Result<Self, Error> {
|
pub async fn create(
|
||||||
|
pool: &mut DbPool<'_>,
|
||||||
|
form: &LocalImageForm,
|
||||||
|
image_details_form: &ImageDetailsForm,
|
||||||
|
) -> Result<Self, Error> {
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
insert_into(local_image::table)
|
conn
|
||||||
.values(form)
|
.build_transaction()
|
||||||
.get_result::<Self>(conn)
|
.run(|conn| {
|
||||||
|
Box::pin(async move {
|
||||||
|
let local_insert = insert_into(local_image::table)
|
||||||
|
.values(form)
|
||||||
|
.get_result::<Self>(conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
ImageDetails::create(conn, image_details_form).await?;
|
||||||
|
|
||||||
|
local_insert
|
||||||
|
}) as _
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,16 +60,26 @@ impl LocalImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RemoteImage {
|
impl RemoteImage {
|
||||||
pub async fn create(pool: &mut DbPool<'_>, links: Vec<Url>) -> Result<usize, Error> {
|
pub async fn create(pool: &mut DbPool<'_>, form: &ImageDetailsForm) -> Result<usize, Error> {
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
let forms = links
|
conn
|
||||||
.into_iter()
|
.build_transaction()
|
||||||
.map(|url| RemoteImageForm { link: url.into() })
|
.run(|conn| {
|
||||||
.collect::<Vec<_>>();
|
Box::pin(async move {
|
||||||
insert_into(remote_image::table)
|
let remote_image_form = RemoteImageForm {
|
||||||
.values(forms)
|
link: form.link.clone(),
|
||||||
.on_conflict_do_nothing()
|
};
|
||||||
.execute(conn)
|
let remote_insert = insert_into(remote_image::table)
|
||||||
|
.values(remote_image_form)
|
||||||
|
.on_conflict_do_nothing()
|
||||||
|
.execute(conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
ImageDetails::create(conn, form).await?;
|
||||||
|
|
||||||
|
remote_insert
|
||||||
|
}) as _
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,3 +98,16 @@ impl RemoteImage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ImageDetails {
|
||||||
|
pub(crate) async fn create(
|
||||||
|
conn: &mut AsyncPgConnection,
|
||||||
|
form: &ImageDetailsForm,
|
||||||
|
) -> Result<usize, Error> {
|
||||||
|
insert_into(image_details::table)
|
||||||
|
.values(form)
|
||||||
|
.on_conflict_do_nothing()
|
||||||
|
.execute(conn)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,7 +7,7 @@ use diesel::{dsl::insert_into, result::Error};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use lemmy_utils::{error::LemmyResult, CACHE_DURATION_API};
|
use lemmy_utils::{error::LemmyResult, CACHE_DURATION_API};
|
||||||
use moka::future::Cache;
|
use moka::future::Cache;
|
||||||
use once_cell::sync::Lazy;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
impl LocalSite {
|
impl LocalSite {
|
||||||
pub async fn create(pool: &mut DbPool<'_>, form: &LocalSiteInsertForm) -> Result<Self, Error> {
|
pub async fn create(pool: &mut DbPool<'_>, form: &LocalSiteInsertForm) -> Result<Self, Error> {
|
||||||
|
@ -18,7 +18,7 @@ impl LocalSite {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
pub async fn read(pool: &mut DbPool<'_>) -> LemmyResult<Self> {
|
pub async fn read(pool: &mut DbPool<'_>) -> LemmyResult<Self> {
|
||||||
static CACHE: Lazy<Cache<(), LocalSite>> = Lazy::new(|| {
|
static CACHE: LazyLock<Cache<(), LocalSite>> = LazyLock::new(|| {
|
||||||
Cache::builder()
|
Cache::builder()
|
||||||
.max_capacity(1)
|
.max_capacity(1)
|
||||||
.time_to_live(CACHE_DURATION_API)
|
.time_to_live(CACHE_DURATION_API)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
newtypes::{DbUrl, LanguageId, LocalUserId, PersonId},
|
newtypes::{CommunityId, DbUrl, LanguageId, LocalUserId, PersonId},
|
||||||
schema::{local_user, person, registration_application},
|
schema::{community, community_moderator, local_user, person, registration_application},
|
||||||
source::{
|
source::{
|
||||||
actor_language::LocalUserLanguage,
|
actor_language::LocalUserLanguage,
|
||||||
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
|
local_user::{LocalUser, LocalUserInsertForm, LocalUserUpdateForm},
|
||||||
|
@ -13,16 +13,19 @@ use crate::{
|
||||||
now,
|
now,
|
||||||
DbPool,
|
DbPool,
|
||||||
},
|
},
|
||||||
|
CommunityVisibility,
|
||||||
};
|
};
|
||||||
use bcrypt::{hash, DEFAULT_COST};
|
use bcrypt::{hash, DEFAULT_COST};
|
||||||
use diesel::{
|
use diesel::{
|
||||||
dsl::{insert_into, not, IntervalDsl},
|
dsl::{insert_into, not, IntervalDsl},
|
||||||
result::Error,
|
result::Error,
|
||||||
|
CombineDsl,
|
||||||
ExpressionMethods,
|
ExpressionMethods,
|
||||||
JoinOnDsl,
|
JoinOnDsl,
|
||||||
QueryDsl,
|
QueryDsl,
|
||||||
};
|
};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
|
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||||
|
|
||||||
impl LocalUser {
|
impl LocalUser {
|
||||||
pub async fn create(
|
pub async fn create(
|
||||||
|
@ -112,11 +115,11 @@ impl LocalUser {
|
||||||
let conn = &mut get_conn(pool).await?;
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
|
||||||
// Make sure:
|
// Make sure:
|
||||||
// - The deny reason exists
|
// - An admin has interacted with the application
|
||||||
// - The app is older than a week
|
// - The app is older than a week
|
||||||
// - The accepted_application is false
|
// - The accepted_application is false
|
||||||
let old_denied_registrations = registration_application::table
|
let old_denied_registrations = registration_application::table
|
||||||
.filter(registration_application::deny_reason.is_not_null())
|
.filter(registration_application::admin_id.is_not_null())
|
||||||
.filter(registration_application::published.lt(now() - 1.week()))
|
.filter(registration_application::published.lt(now() - 1.week()))
|
||||||
.select(registration_application::local_user_id);
|
.select(registration_application::local_user_id);
|
||||||
|
|
||||||
|
@ -215,6 +218,72 @@ impl LocalUser {
|
||||||
blocked_instances,
|
blocked_instances,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks to make sure the acting admin is higher than the target admin
|
||||||
|
pub async fn is_higher_admin_check(
|
||||||
|
pool: &mut DbPool<'_>,
|
||||||
|
admin_person_id: PersonId,
|
||||||
|
target_person_ids: Vec<PersonId>,
|
||||||
|
) -> LemmyResult<()> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
|
||||||
|
// Build the list of persons
|
||||||
|
let mut persons = target_person_ids;
|
||||||
|
persons.push(admin_person_id);
|
||||||
|
persons.dedup();
|
||||||
|
|
||||||
|
let res = local_user::table
|
||||||
|
.filter(local_user::admin.eq(true))
|
||||||
|
.filter(local_user::person_id.eq_any(persons))
|
||||||
|
.order_by(local_user::id)
|
||||||
|
// This does a limit 1 select first
|
||||||
|
.first::<LocalUser>(conn)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// If the first result sorted by published is the acting admin
|
||||||
|
if res.person_id == admin_person_id {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(LemmyErrorType::NotHigherAdmin)?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks to make sure the acting moderator is higher than the target moderator
|
||||||
|
pub async fn is_higher_mod_or_admin_check(
|
||||||
|
pool: &mut DbPool<'_>,
|
||||||
|
for_community_id: CommunityId,
|
||||||
|
admin_person_id: PersonId,
|
||||||
|
target_person_ids: Vec<PersonId>,
|
||||||
|
) -> LemmyResult<()> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
|
||||||
|
// Build the list of persons
|
||||||
|
let mut persons = target_person_ids;
|
||||||
|
persons.push(admin_person_id);
|
||||||
|
persons.dedup();
|
||||||
|
|
||||||
|
let admins = local_user::table
|
||||||
|
.filter(local_user::admin.eq(true))
|
||||||
|
.filter(local_user::person_id.eq_any(&persons))
|
||||||
|
.order_by(local_user::id)
|
||||||
|
.select(local_user::person_id);
|
||||||
|
|
||||||
|
let mods = community_moderator::table
|
||||||
|
.filter(community_moderator::community_id.eq(for_community_id))
|
||||||
|
.filter(community_moderator::person_id.eq_any(&persons))
|
||||||
|
.order_by(community_moderator::published)
|
||||||
|
.select(community_moderator::person_id);
|
||||||
|
|
||||||
|
let res = admins.union_all(mods).get_results::<PersonId>(conn).await?;
|
||||||
|
let first_person = res.as_slice().first().ok_or(LemmyErrorType::NotHigherMod)?;
|
||||||
|
|
||||||
|
// If the first result sorted by published is the acting mod
|
||||||
|
if *first_person == admin_person_id {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(LemmyErrorType::NotHigherMod)?
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds some helper functions for an optional LocalUser
|
/// Adds some helper functions for an optional LocalUser
|
||||||
|
@ -225,6 +294,12 @@ pub trait LocalUserOptionHelper {
|
||||||
fn show_read_posts(&self) -> bool;
|
fn show_read_posts(&self) -> bool;
|
||||||
fn is_admin(&self) -> bool;
|
fn is_admin(&self) -> bool;
|
||||||
fn show_nsfw(&self, site: &Site) -> bool;
|
fn show_nsfw(&self, site: &Site) -> bool;
|
||||||
|
fn visible_communities_only<Q>(&self, query: Q) -> Q
|
||||||
|
where
|
||||||
|
Q: diesel::query_dsl::methods::FilterDsl<
|
||||||
|
diesel::dsl::Eq<community::visibility, CommunityVisibility>,
|
||||||
|
Output = Q,
|
||||||
|
>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalUserOptionHelper for Option<&LocalUser> {
|
impl LocalUserOptionHelper for Option<&LocalUser> {
|
||||||
|
@ -253,14 +328,32 @@ impl LocalUserOptionHelper for Option<&LocalUser> {
|
||||||
.map(|l| l.show_nsfw)
|
.map(|l| l.show_nsfw)
|
||||||
.unwrap_or(site.content_warning.is_some())
|
.unwrap_or(site.content_warning.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn visible_communities_only<Q>(&self, query: Q) -> Q
|
||||||
|
where
|
||||||
|
Q: diesel::query_dsl::methods::FilterDsl<
|
||||||
|
diesel::dsl::Eq<community::visibility, CommunityVisibility>,
|
||||||
|
Output = Q,
|
||||||
|
>,
|
||||||
|
{
|
||||||
|
if self.is_none() {
|
||||||
|
query.filter(community::visibility.eq(CommunityVisibility::Public))
|
||||||
|
} else {
|
||||||
|
query
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalUserInsertForm {
|
impl LocalUserInsertForm {
|
||||||
pub fn test_form(person_id: PersonId) -> Self {
|
pub fn test_form(person_id: PersonId) -> Self {
|
||||||
Self::builder()
|
Self::new(person_id, String::new())
|
||||||
.person_id(person_id)
|
}
|
||||||
.password_encrypted(String::new())
|
|
||||||
.build()
|
pub fn test_form_admin(person_id: PersonId) -> Self {
|
||||||
|
LocalUserInsertForm {
|
||||||
|
admin: Some(true),
|
||||||
|
..Self::test_form(person_id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,3 +365,58 @@ pub struct UserBackupLists {
|
||||||
pub blocked_users: Vec<DbUrl>,
|
pub blocked_users: Vec<DbUrl>,
|
||||||
pub blocked_instances: Vec<String>,
|
pub blocked_instances: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[allow(clippy::indexing_slicing)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{
|
||||||
|
source::{
|
||||||
|
instance::Instance,
|
||||||
|
local_user::{LocalUser, LocalUserInsertForm},
|
||||||
|
person::{Person, PersonInsertForm},
|
||||||
|
},
|
||||||
|
traits::Crud,
|
||||||
|
utils::build_db_pool_for_tests,
|
||||||
|
};
|
||||||
|
use lemmy_utils::error::LemmyResult;
|
||||||
|
use serial_test::serial;
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[serial]
|
||||||
|
async fn test_admin_higher_check() -> LemmyResult<()> {
|
||||||
|
let pool = &build_db_pool_for_tests().await;
|
||||||
|
let pool = &mut pool.into();
|
||||||
|
|
||||||
|
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
|
||||||
|
|
||||||
|
let fiona_person = PersonInsertForm::test_form(inserted_instance.id, "fiona");
|
||||||
|
let inserted_fiona_person = Person::create(pool, &fiona_person).await?;
|
||||||
|
|
||||||
|
let fiona_local_user_form = LocalUserInsertForm::test_form_admin(inserted_fiona_person.id);
|
||||||
|
let _inserted_fiona_local_user =
|
||||||
|
LocalUser::create(pool, &fiona_local_user_form, vec![]).await?;
|
||||||
|
|
||||||
|
let delores_person = PersonInsertForm::test_form(inserted_instance.id, "delores");
|
||||||
|
let inserted_delores_person = Person::create(pool, &delores_person).await?;
|
||||||
|
let delores_local_user_form = LocalUserInsertForm::test_form_admin(inserted_delores_person.id);
|
||||||
|
let _inserted_delores_local_user =
|
||||||
|
LocalUser::create(pool, &delores_local_user_form, vec![]).await?;
|
||||||
|
|
||||||
|
let admin_person_ids = vec![inserted_fiona_person.id, inserted_delores_person.id];
|
||||||
|
|
||||||
|
// Make sure fiona is marked as a higher admin than delores, and vice versa
|
||||||
|
let fiona_higher_check =
|
||||||
|
LocalUser::is_higher_admin_check(pool, inserted_fiona_person.id, admin_person_ids.clone())
|
||||||
|
.await;
|
||||||
|
assert!(fiona_higher_check.is_ok());
|
||||||
|
|
||||||
|
// This should throw an error, since delores was added later
|
||||||
|
let delores_higher_check =
|
||||||
|
LocalUser::is_higher_admin_check(pool, inserted_delores_person.id, admin_person_ids).await;
|
||||||
|
assert!(delores_higher_check.is_err());
|
||||||
|
|
||||||
|
Instance::delete(pool, inserted_instance.id).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -31,6 +31,7 @@ impl LocalUserVoteDisplayMode {
|
||||||
.get_result::<Self>(conn)
|
.get_result::<Self>(conn)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn update(
|
pub async fn update(
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
local_user_id: LocalUserId,
|
local_user_id: LocalUserId,
|
||||||
|
|
|
@ -72,10 +72,7 @@ mod tests {
|
||||||
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
|
let inserted_instance = Instance::read_or_create(pool, "my_domain.tld".to_string()).await?;
|
||||||
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy prw");
|
let new_person = PersonInsertForm::test_form(inserted_instance.id, "thommy prw");
|
||||||
let inserted_person = Person::create(pool, &new_person).await?;
|
let inserted_person = Person::create(pool, &new_person).await?;
|
||||||
let new_local_user = LocalUserInsertForm::builder()
|
let new_local_user = LocalUserInsertForm::test_form(inserted_person.id);
|
||||||
.person_id(inserted_person.id)
|
|
||||||
.password_encrypted("pass".to_string())
|
|
||||||
.build();
|
|
||||||
let inserted_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?;
|
let inserted_local_user = LocalUser::create(pool, &new_local_user, vec![]).await?;
|
||||||
|
|
||||||
// Create password reset token
|
// Create password reset token
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue