mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-06-10 09:29:22 +00:00
Compare commits
103 commits
0.19.4-bet
...
main
Author | SHA1 | Date | |
---|---|---|---|
b2a480f55c | |||
9236cf7d21 | |||
b559e0206b | |||
f5f2b5ffc6 | |||
1e11faf741 | |||
5d31f0d516 | |||
844b84a01a | |||
b0447ad94d | |||
3d25322089 | |||
16a82862b8 | |||
79e6dbf0de | |||
fda5ce4482 | |||
e8cfb5665f | |||
bb94fb1c79 | |||
92214a9364 | |||
78ae874b89 | |||
a947474c64 | |||
8bf17946bd | |||
9ceb5b6386 | |||
aefb41b551 | |||
4195a9b5a1 | |||
69b4c6647b | |||
f7fe0d46fc | |||
609a6411a7 | |||
44666a34a2 | |||
6db878f761 | |||
6031709fcf | |||
4d9e38d875 | |||
6a6c915014 | |||
96b7afc0b1 | |||
d2083f79d9 | |||
e8a7bb07a3 | |||
91e57ff954 | |||
7d80a3c7d6 | |||
abcfa266af | |||
51970ffc81 | |||
fd6a1283a5 | |||
af034f3b5e | |||
0d5db29bc9 | |||
ec77c00ef8 | |||
69bdcb3069 | |||
6a6108ac55 | |||
b2c1a14234 | |||
d8dc38eb06 | |||
c96017c009 | |||
9aa565b559 | |||
7d7cd8ded4 | |||
943c31cc72 | |||
973f39601c | |||
a39c19c9db | |||
55f84dd38a | |||
6b46a70535 | |||
4ffaa93431 | |||
a0ad7806cb | |||
99aac07714 | |||
1a4aa3eaba | |||
93c9a5f2b1 | |||
9a9d518153 | |||
7fb03c502e | |||
49bb17b583 | |||
723cb549d4 | |||
8b6a4c060e | |||
cb80980027 | |||
c4fc3a8ede | |||
b4f9ef24a5 | |||
866d752a3c | |||
e0b1d0553d | |||
7c146272c3 | |||
cfdc732d3a | |||
522f974e30 | |||
b152be7951 | |||
485b0f1a54 | |||
7540b02723 | |||
7746db4169 | |||
db2ce81fc4 | |||
4175a1af80 | |||
563280456e | |||
2fecb7ecdf | |||
2c6f9c7fd5 | |||
e338e59868 | |||
b0caa85ed4 | |||
ad60d91f5c | |||
6423d2dde5 | |||
12163701e7 | |||
5c35e97a75 | |||
b05f221565 | |||
beec080274 | |||
492d8f1b01 | |||
d3737d4453 | |||
b459949f57 | |||
93f5df2d2a | |||
cf426493e1 | |||
8e3ff0408e | |||
66e06b3952 | |||
6b9d9dfaa5 | |||
0eaf8d33e7 | |||
c31a29ec7f | |||
80635c9e24 | |||
95d75e07b2 | |||
efbfdc9340 | |||
1ae3aab764 | |||
f68881c552 | |||
2ba1ba88b8 |
|
@ -3,3 +3,5 @@ edition = "2021"
|
|||
imports_layout = "HorizontalVertical"
|
||||
imports_granularity = "Crate"
|
||||
group_imports = "One"
|
||||
wrap_comments = true
|
||||
comment_width = 100
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
# See https://github.com/woodpecker-ci/woodpecker/issues/1677
|
||||
|
||||
variables:
|
||||
- &rust_image "rust:1.77"
|
||||
- &rust_image "rust:1.78"
|
||||
- &rust_nightly_image "rustlang/rust:nightly"
|
||||
- &install_pnpm "corepack enable pnpm"
|
||||
- &slow_check_paths
|
||||
- event: pull_request
|
||||
|
@ -24,15 +25,17 @@ variables:
|
|||
"diesel.toml",
|
||||
".gitmodules",
|
||||
]
|
||||
|
||||
# Broken for cron jobs currently, see
|
||||
# https://github.com/woodpecker-ci/woodpecker/issues/1716
|
||||
# clone:
|
||||
# git:
|
||||
# image: woodpeckerci/plugin-git
|
||||
# settings:
|
||||
# recursive: true
|
||||
# submodule_update_remote: true
|
||||
- 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:
|
||||
prepare_repo:
|
||||
|
@ -66,7 +69,7 @@ steps:
|
|||
- event: pull_request
|
||||
|
||||
cargo_fmt:
|
||||
image: rustlang/rust:nightly
|
||||
image: *rust_nightly_image
|
||||
environment:
|
||||
# store cargo data in repo folder so that it gets cached between steps
|
||||
CARGO_HOME: .cargo_home
|
||||
|
@ -77,11 +80,9 @@ steps:
|
|||
- event: pull_request
|
||||
|
||||
cargo_machete:
|
||||
image: rustlang/rust:nightly
|
||||
image: *rust_nightly_image
|
||||
commands:
|
||||
- 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_binstall
|
||||
- cargo binstall -y cargo-machete
|
||||
- cargo machete
|
||||
when:
|
||||
|
@ -133,11 +134,12 @@ steps:
|
|||
when: *slow_check_paths
|
||||
|
||||
check_diesel_schema:
|
||||
image: willsquire/diesel-cli
|
||||
image: *rust_image
|
||||
environment:
|
||||
CARGO_HOME: .cargo_home
|
||||
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||
commands:
|
||||
- <<: *install_diesel_cli
|
||||
- diesel migration run
|
||||
- diesel print-schema --config-file=diesel.toml > tmp.schema
|
||||
- diff tmp.schema crates/db_schema/src/schema.rs
|
||||
|
@ -197,8 +199,8 @@ steps:
|
|||
PGHOST: database
|
||||
PGDATABASE: lemmy
|
||||
commands:
|
||||
- cargo install diesel_cli
|
||||
- export PATH="$CARGO_HOME/bin:$PATH"
|
||||
# Install diesel_cli
|
||||
- <<: *install_diesel_cli
|
||||
# Run all migrations
|
||||
- diesel migration run
|
||||
# Dump schema to before.sqldump (PostgreSQL apt repo is used to prevent pg_dump version mismatch error)
|
||||
|
@ -276,10 +278,11 @@ steps:
|
|||
publish_to_crates_io:
|
||||
image: *rust_image
|
||||
commands:
|
||||
- cargo install cargo-workspaces
|
||||
- <<: *install_binstall
|
||||
# Install cargo-workspaces
|
||||
- cargo binstall -y cargo-workspaces
|
||||
- cp -r migrations crates/db_schema/
|
||||
- cargo login "$CARGO_API_TOKEN"
|
||||
- cargo workspaces publish --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
|
||||
- cargo workspaces publish --token "$CARGO_API_TOKEN" --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
|
||||
secrets: [cargo_api_token]
|
||||
when:
|
||||
- event: tag
|
||||
|
|
2373
Cargo.lock
generated
2373
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
103
Cargo.toml
103
Cargo.toml
|
@ -1,5 +1,5 @@
|
|||
[workspace.package]
|
||||
version = "0.19.4-beta.4"
|
||||
version = "0.19.4"
|
||||
edition = "2021"
|
||||
description = "A link aggregator for the fediverse"
|
||||
license = "AGPL-3.0"
|
||||
|
@ -67,8 +67,8 @@ members = [
|
|||
|
||||
[workspace.lints.clippy]
|
||||
cast_lossless = "deny"
|
||||
complexity = "deny"
|
||||
correctness = "deny"
|
||||
complexity = { level = "deny", priority = -1 }
|
||||
correctness = { level = "deny", priority = -1 }
|
||||
dbg_macro = "deny"
|
||||
explicit_into_iter_loop = "deny"
|
||||
explicit_iter_loop = "deny"
|
||||
|
@ -79,74 +79,74 @@ inefficient_to_string = "deny"
|
|||
items-after-statements = "deny"
|
||||
manual_string_new = "deny"
|
||||
needless_collect = "deny"
|
||||
perf = "deny"
|
||||
perf = { level = "deny", priority = -1 }
|
||||
redundant_closure_for_method_calls = "deny"
|
||||
style = "deny"
|
||||
suspicious = "deny"
|
||||
style = { level = "deny", priority = -1 }
|
||||
suspicious = { level = "deny", priority = -1 }
|
||||
uninlined_format_args = "allow"
|
||||
unused_self = "deny"
|
||||
unwrap_used = "deny"
|
||||
|
||||
[workspace.dependencies]
|
||||
lemmy_api = { version = "=0.19.4-beta.4", path = "./crates/api" }
|
||||
lemmy_api_crud = { version = "=0.19.4-beta.4", path = "./crates/api_crud" }
|
||||
lemmy_apub = { version = "=0.19.4-beta.4", path = "./crates/apub" }
|
||||
lemmy_utils = { version = "=0.19.4-beta.4", path = "./crates/utils", default-features = false }
|
||||
lemmy_db_schema = { version = "=0.19.4-beta.4", path = "./crates/db_schema" }
|
||||
lemmy_api_common = { version = "=0.19.4-beta.4", path = "./crates/api_common" }
|
||||
lemmy_routes = { version = "=0.19.4-beta.4", path = "./crates/routes" }
|
||||
lemmy_db_views = { version = "=0.19.4-beta.4", path = "./crates/db_views" }
|
||||
lemmy_db_views_actor = { version = "=0.19.4-beta.4", path = "./crates/db_views_actor" }
|
||||
lemmy_db_views_moderator = { version = "=0.19.4-beta.4", path = "./crates/db_views_moderator" }
|
||||
lemmy_federate = { version = "=0.19.4-beta.4", path = "./crates/federate" }
|
||||
activitypub_federation = { version = "0.5.4", default-features = false, features = [
|
||||
lemmy_api = { version = "=0.19.4", path = "./crates/api" }
|
||||
lemmy_api_crud = { version = "=0.19.4", path = "./crates/api_crud" }
|
||||
lemmy_apub = { version = "=0.19.4", path = "./crates/apub" }
|
||||
lemmy_utils = { version = "=0.19.4", path = "./crates/utils", default-features = false }
|
||||
lemmy_db_schema = { version = "=0.19.4", path = "./crates/db_schema" }
|
||||
lemmy_api_common = { version = "=0.19.4", path = "./crates/api_common" }
|
||||
lemmy_routes = { version = "=0.19.4", path = "./crates/routes" }
|
||||
lemmy_db_views = { version = "=0.19.4", path = "./crates/db_views" }
|
||||
lemmy_db_views_actor = { version = "=0.19.4", path = "./crates/db_views_actor" }
|
||||
lemmy_db_views_moderator = { version = "=0.19.4", path = "./crates/db_views_moderator" }
|
||||
lemmy_federate = { version = "=0.19.4", path = "./crates/federate" }
|
||||
activitypub_federation = { version = "0.5.6", default-features = false, features = [
|
||||
"actix-web",
|
||||
] }
|
||||
diesel = "2.1.4"
|
||||
diesel = "2.1.6"
|
||||
diesel_migrations = "2.1.0"
|
||||
diesel-async = "0.4.1"
|
||||
serde = { version = "1.0.197", features = ["derive"] }
|
||||
serde_with = "3.7.0"
|
||||
actix-web = { version = "4.5.1", default-features = false, features = [
|
||||
serde = { version = "1.0.203", features = ["derive"] }
|
||||
serde_with = "3.8.1"
|
||||
actix-web = { version = "4.6.0", default-features = false, features = [
|
||||
"macros",
|
||||
"rustls",
|
||||
"rustls-0_23",
|
||||
"compress-brotli",
|
||||
"compress-gzip",
|
||||
"compress-zstd",
|
||||
"cookies",
|
||||
] }
|
||||
tracing = "0.1.40"
|
||||
tracing-actix-web = { version = "0.7.10", default-features = false }
|
||||
tracing-actix-web = { version = "0.7.11", default-features = false }
|
||||
tracing-error = "0.2.0"
|
||||
tracing-log = "0.2.0"
|
||||
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
reqwest = { version = "0.11.26", features = ["json", "blocking", "gzip"] }
|
||||
reqwest-middleware = "0.2.4"
|
||||
reqwest-tracing = "0.4.7"
|
||||
reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] }
|
||||
reqwest-middleware = "0.2.5"
|
||||
reqwest-tracing = "0.4.8"
|
||||
clokwerk = "0.4.0"
|
||||
doku = { version = "0.21.1", features = ["url-2"] }
|
||||
bcrypt = "0.15.0"
|
||||
chrono = { version = "0.4.35", features = ["serde"], default-features = false }
|
||||
serde_json = { version = "1.0.114", features = ["preserve_order"] }
|
||||
base64 = "0.21.7"
|
||||
uuid = { version = "1.7.0", features = ["serde", "v4"] }
|
||||
async-trait = "0.1.77"
|
||||
bcrypt = "0.15.1"
|
||||
chrono = { version = "0.4.38", features = ["serde"], default-features = false }
|
||||
serde_json = { version = "1.0.117", features = ["preserve_order"] }
|
||||
base64 = "0.22.1"
|
||||
uuid = { version = "1.8.0", features = ["serde", "v4"] }
|
||||
async-trait = "0.1.80"
|
||||
captcha = "0.0.9"
|
||||
anyhow = { version = "1.0.81", features = [
|
||||
anyhow = { version = "1.0.86", features = [
|
||||
"backtrace",
|
||||
] } # backtrace is on by default on nightly, but not stable rust
|
||||
diesel_ltree = "0.3.1"
|
||||
typed-builder = "0.18.1"
|
||||
serial_test = "2.0.0"
|
||||
tokio = { version = "1.36.0", features = ["full"] }
|
||||
regex = "1.10.3"
|
||||
typed-builder = "0.18.2"
|
||||
serial_test = "3.1.1"
|
||||
tokio = { version = "1.38.0", features = ["full"] }
|
||||
regex = "1.10.4"
|
||||
once_cell = "1.19.0"
|
||||
diesel-derive-newtype = "2.1.0"
|
||||
diesel-derive-newtype = "2.1.2"
|
||||
diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
|
||||
strum = "0.25.0"
|
||||
strum_macros = "0.25.3"
|
||||
itertools = "0.12.1"
|
||||
strum = "0.26.2"
|
||||
strum_macros = "0.26.4"
|
||||
itertools = "0.13.0"
|
||||
futures = "0.3.30"
|
||||
http = "0.2.12"
|
||||
rosetta-i18n = "0.1.3"
|
||||
|
@ -157,16 +157,17 @@ ts-rs = { version = "7.1.1", features = [
|
|||
"chrono-impl",
|
||||
"no-serde-warnings",
|
||||
] }
|
||||
rustls = { version = "0.21.10", features = ["dangerous_configuration"] }
|
||||
rustls = { version = "0.23.9", features = ["ring"] }
|
||||
futures-util = "0.3.30"
|
||||
tokio-postgres = "0.7.10"
|
||||
tokio-postgres-rustls = "0.10.0"
|
||||
tokio-postgres-rustls = "0.12.0"
|
||||
urlencoding = "2.1.3"
|
||||
enum-map = "2.7"
|
||||
moka = { version = "0.12.5", features = ["future"] }
|
||||
moka = { version = "0.12.7", features = ["future"] }
|
||||
i-love-jesus = { version = "0.1.0" }
|
||||
clap = { version = "4.5.2", features = ["derive"] }
|
||||
clap = { version = "4.5.6", features = ["derive", "env"] }
|
||||
pretty_assertions = "1.4.0"
|
||||
derive-new = "0.6.0"
|
||||
|
||||
[dependencies]
|
||||
lemmy_api = { workspace = true }
|
||||
|
@ -194,17 +195,17 @@ clokwerk = { workspace = true }
|
|||
serde_json = { workspace = true }
|
||||
tracing-opentelemetry = { workspace = true, optional = true }
|
||||
opentelemetry = { workspace = true, optional = true }
|
||||
console-subscriber = { version = "0.1.10", optional = true }
|
||||
console-subscriber = { version = "0.2.0", optional = true }
|
||||
opentelemetry-otlp = { version = "0.12.0", optional = true }
|
||||
pict-rs = { version = "0.5.9", optional = true }
|
||||
pict-rs = { version = "0.5.15", optional = true }
|
||||
tokio.workspace = true
|
||||
actix-cors = "0.6.5"
|
||||
actix-cors = "0.7.0"
|
||||
futures-util = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
prometheus = { version = "0.13.3", features = ["process"] }
|
||||
prometheus = { version = "0.13.4", features = ["process"] }
|
||||
serial_test = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
actix-web-prom = "0.7.0"
|
||||
actix-web-prom = "0.8.0"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = { workspace = true }
|
||||
|
|
1
api_tests/.npmrc
Normal file
1
api_tests/.npmrc
Normal file
|
@ -0,0 +1 @@
|
|||
package-manager-strict=false
|
|
@ -6,11 +6,11 @@
|
|||
"repository": "https://github.com/LemmyNet/lemmy",
|
||||
"author": "Dessalines",
|
||||
"license": "AGPL-3.0",
|
||||
"packageManager": "pnpm@9.0.1+sha256.46d50ee2afecb42b185ebbd662dc7bdd52ef5be56bf035bb615cab81a75345df",
|
||||
"packageManager": "pnpm@9.1.4",
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'",
|
||||
"fix": "prettier --write src && eslint --fix src",
|
||||
"api-test": "jest -i follow.spec.ts && jest -i post.spec.ts && jest -i comment.spec.ts && jest -i private_message.spec.ts && jest -i user.spec.ts && jest -i community.spec.ts && jest -i image.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-comment": "jest -i comment.spec.ts",
|
||||
"api-test-post": "jest -i post.spec.ts",
|
||||
|
@ -28,7 +28,7 @@
|
|||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"jest": "^29.5.0",
|
||||
"lemmy-js-client": "0.19.4-alpha.18",
|
||||
"lemmy-js-client": "0.19.4",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.1.0",
|
||||
"typescript": "^5.4.4"
|
||||
|
|
|
@ -16,10 +16,10 @@ importers:
|
|||
version: 20.12.4
|
||||
'@typescript-eslint/eslint-plugin':
|
||||
specifier: ^7.5.0
|
||||
version: 7.5.0(@typescript-eslint/parser@7.5.0(eslint@8.57.0)(typescript@5.4.4))(eslint@8.57.0)(typescript@5.4.4)
|
||||
version: 7.5.0(@typescript-eslint/parser@7.5.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^7.5.0
|
||||
version: 7.5.0(eslint@8.57.0)(typescript@5.4.4)
|
||||
version: 7.5.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
download-file-sync:
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
|
@ -33,17 +33,17 @@ importers:
|
|||
specifier: ^29.5.0
|
||||
version: 29.7.0(@types/node@20.12.4)
|
||||
lemmy-js-client:
|
||||
specifier: 0.19.4-alpha.18
|
||||
version: 0.19.4-alpha.18
|
||||
specifier: 0.19.4
|
||||
version: 0.19.4
|
||||
prettier:
|
||||
specifier: ^3.2.5
|
||||
version: 3.2.5
|
||||
ts-jest:
|
||||
specifier: ^29.1.0
|
||||
version: 29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@20.12.4))(typescript@5.4.4)
|
||||
version: 29.1.4(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@20.12.4))(typescript@5.4.5)
|
||||
typescript:
|
||||
specifier: ^5.4.4
|
||||
version: 5.4.4
|
||||
version: 5.4.5
|
||||
|
||||
packages:
|
||||
|
||||
|
@ -865,6 +865,7 @@ packages:
|
|||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
|
||||
globals@11.12.0:
|
||||
resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==}
|
||||
|
@ -922,6 +923,7 @@ packages:
|
|||
|
||||
inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
@ -1156,8 +1158,8 @@ packages:
|
|||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
lemmy-js-client@0.19.4-alpha.18:
|
||||
resolution: {integrity: sha512-CUKRIiINZF2zOfK5WzBDF071LjMmRBFHwiSYBMGJyQP1zu8sPKCb/ptg25WWrf79Y4uOaVLctgHg3oEUXmSUmQ==}
|
||||
lemmy-js-client@0.19.4:
|
||||
resolution: {integrity: sha512-k3d+YRDj3+JuuEP+nuEg27efR/e4m8oMk2BoC8jq9AnMrwSAKfsN2F2vG70Zke0amXtOclDZrCSHkIpNw99ikg==}
|
||||
|
||||
leven@3.1.0:
|
||||
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
||||
|
@ -1380,6 +1382,7 @@ packages:
|
|||
|
||||
rimraf@3.0.2:
|
||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
run-parallel@1.2.0:
|
||||
|
@ -1389,13 +1392,13 @@ packages:
|
|||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||
hasBin: true
|
||||
|
||||
semver@7.5.4:
|
||||
resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==}
|
||||
semver@7.6.0:
|
||||
resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.6.0:
|
||||
resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
|
||||
semver@7.6.2:
|
||||
resolution: {integrity: sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
|
@ -1499,12 +1502,13 @@ packages:
|
|||
peerDependencies:
|
||||
typescript: '>=4.2.0'
|
||||
|
||||
ts-jest@29.1.2:
|
||||
resolution: {integrity: sha512-br6GJoH/WUX4pu7FbZXuWGKGNDuU7b8Uj77g/Sp7puZV6EXzuByl6JrECvm0MzVzSTkSHWTihsXt+5XYER5b+g==}
|
||||
engines: {node: ^16.10.0 || ^18.0.0 || >=20.0.0}
|
||||
ts-jest@29.1.4:
|
||||
resolution: {integrity: sha512-YiHwDhSvCiItoAgsKtoLFCuakDzDsJ1DLDnSouTaTmdOcOwIkSzbLXduaQ6M5DRVhuZC/NYaaZ/mtHbWMv/S6Q==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@babel/core': '>=7.0.0-beta.0 <8'
|
||||
'@jest/transform': ^29.0.0
|
||||
'@jest/types': ^29.0.0
|
||||
babel-jest: ^29.0.0
|
||||
esbuild: '*'
|
||||
|
@ -1513,6 +1517,8 @@ packages:
|
|||
peerDependenciesMeta:
|
||||
'@babel/core':
|
||||
optional: true
|
||||
'@jest/transform':
|
||||
optional: true
|
||||
'@jest/types':
|
||||
optional: true
|
||||
babel-jest:
|
||||
|
@ -1539,8 +1545,8 @@ packages:
|
|||
resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
typescript@5.4.4:
|
||||
resolution: {integrity: sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==}
|
||||
typescript@5.4.5:
|
||||
resolution: {integrity: sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
|
@ -2113,13 +2119,13 @@ snapshots:
|
|||
dependencies:
|
||||
'@types/yargs-parser': 21.0.3
|
||||
|
||||
'@typescript-eslint/eslint-plugin@7.5.0(@typescript-eslint/parser@7.5.0(eslint@8.57.0)(typescript@5.4.4))(eslint@8.57.0)(typescript@5.4.4)':
|
||||
'@typescript-eslint/eslint-plugin@7.5.0(@typescript-eslint/parser@7.5.0(eslint@8.57.0)(typescript@5.4.5))(eslint@8.57.0)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.10.0
|
||||
'@typescript-eslint/parser': 7.5.0(eslint@8.57.0)(typescript@5.4.4)
|
||||
'@typescript-eslint/parser': 7.5.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
'@typescript-eslint/scope-manager': 7.5.0
|
||||
'@typescript-eslint/type-utils': 7.5.0(eslint@8.57.0)(typescript@5.4.4)
|
||||
'@typescript-eslint/utils': 7.5.0(eslint@8.57.0)(typescript@5.4.4)
|
||||
'@typescript-eslint/type-utils': 7.5.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
'@typescript-eslint/utils': 7.5.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
'@typescript-eslint/visitor-keys': 7.5.0
|
||||
debug: 4.3.4
|
||||
eslint: 8.57.0
|
||||
|
@ -2127,22 +2133,22 @@ snapshots:
|
|||
ignore: 5.3.1
|
||||
natural-compare: 1.4.0
|
||||
semver: 7.6.0
|
||||
ts-api-utils: 1.3.0(typescript@5.4.4)
|
||||
ts-api-utils: 1.3.0(typescript@5.4.5)
|
||||
optionalDependencies:
|
||||
typescript: 5.4.4
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/parser@7.5.0(eslint@8.57.0)(typescript@5.4.4)':
|
||||
'@typescript-eslint/parser@7.5.0(eslint@8.57.0)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@typescript-eslint/scope-manager': 7.5.0
|
||||
'@typescript-eslint/types': 7.5.0
|
||||
'@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.4)
|
||||
'@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.5)
|
||||
'@typescript-eslint/visitor-keys': 7.5.0
|
||||
debug: 4.3.4
|
||||
eslint: 8.57.0
|
||||
optionalDependencies:
|
||||
typescript: 5.4.4
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
@ -2151,21 +2157,21 @@ snapshots:
|
|||
'@typescript-eslint/types': 7.5.0
|
||||
'@typescript-eslint/visitor-keys': 7.5.0
|
||||
|
||||
'@typescript-eslint/type-utils@7.5.0(eslint@8.57.0)(typescript@5.4.4)':
|
||||
'@typescript-eslint/type-utils@7.5.0(eslint@8.57.0)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.4)
|
||||
'@typescript-eslint/utils': 7.5.0(eslint@8.57.0)(typescript@5.4.4)
|
||||
'@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.5)
|
||||
'@typescript-eslint/utils': 7.5.0(eslint@8.57.0)(typescript@5.4.5)
|
||||
debug: 4.3.4
|
||||
eslint: 8.57.0
|
||||
ts-api-utils: 1.3.0(typescript@5.4.4)
|
||||
ts-api-utils: 1.3.0(typescript@5.4.5)
|
||||
optionalDependencies:
|
||||
typescript: 5.4.4
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/types@7.5.0': {}
|
||||
|
||||
'@typescript-eslint/typescript-estree@7.5.0(typescript@5.4.4)':
|
||||
'@typescript-eslint/typescript-estree@7.5.0(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 7.5.0
|
||||
'@typescript-eslint/visitor-keys': 7.5.0
|
||||
|
@ -2173,21 +2179,21 @@ snapshots:
|
|||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.3
|
||||
semver: 7.6.0
|
||||
ts-api-utils: 1.3.0(typescript@5.4.4)
|
||||
semver: 7.6.2
|
||||
ts-api-utils: 1.3.0(typescript@5.4.5)
|
||||
optionalDependencies:
|
||||
typescript: 5.4.4
|
||||
typescript: 5.4.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@typescript-eslint/utils@7.5.0(eslint@8.57.0)(typescript@5.4.4)':
|
||||
'@typescript-eslint/utils@7.5.0(eslint@8.57.0)(typescript@5.4.5)':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.4.0(eslint@8.57.0)
|
||||
'@types/json-schema': 7.0.15
|
||||
'@types/semver': 7.5.8
|
||||
'@typescript-eslint/scope-manager': 7.5.0
|
||||
'@typescript-eslint/types': 7.5.0
|
||||
'@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.4)
|
||||
'@typescript-eslint/typescript-estree': 7.5.0(typescript@5.4.5)
|
||||
eslint: 8.57.0
|
||||
semver: 7.6.0
|
||||
transitivePeerDependencies:
|
||||
|
@ -2716,7 +2722,7 @@ snapshots:
|
|||
'@babel/parser': 7.23.9
|
||||
'@istanbuljs/schema': 0.1.3
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
@ -2995,7 +3001,7 @@ snapshots:
|
|||
jest-util: 29.7.0
|
||||
natural-compare: 1.4.0
|
||||
pretty-format: 29.7.0
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
@ -3076,7 +3082,7 @@ snapshots:
|
|||
|
||||
kleur@3.0.3: {}
|
||||
|
||||
lemmy-js-client@0.19.4-alpha.18: {}
|
||||
lemmy-js-client@0.19.4: {}
|
||||
|
||||
leven@3.1.0: {}
|
||||
|
||||
|
@ -3109,7 +3115,7 @@ snapshots:
|
|||
|
||||
make-dir@4.0.0:
|
||||
dependencies:
|
||||
semver: 7.5.4
|
||||
semver: 7.6.2
|
||||
|
||||
make-error@1.3.6: {}
|
||||
|
||||
|
@ -3273,14 +3279,12 @@ snapshots:
|
|||
|
||||
semver@6.3.1: {}
|
||||
|
||||
semver@7.5.4:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
semver@7.6.0:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
semver@7.6.2: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
|
@ -3362,11 +3366,11 @@ snapshots:
|
|||
dependencies:
|
||||
is-number: 7.0.0
|
||||
|
||||
ts-api-utils@1.3.0(typescript@5.4.4):
|
||||
ts-api-utils@1.3.0(typescript@5.4.5):
|
||||
dependencies:
|
||||
typescript: 5.4.4
|
||||
typescript: 5.4.5
|
||||
|
||||
ts-jest@29.1.2(@babel/core@7.23.9)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@20.12.4))(typescript@5.4.4):
|
||||
ts-jest@29.1.4(@babel/core@7.23.9)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.23.9))(jest@29.7.0(@types/node@20.12.4))(typescript@5.4.5):
|
||||
dependencies:
|
||||
bs-logger: 0.2.6
|
||||
fast-json-stable-stringify: 2.1.0
|
||||
|
@ -3375,11 +3379,12 @@ snapshots:
|
|||
json5: 2.2.3
|
||||
lodash.memoize: 4.1.2
|
||||
make-error: 1.3.6
|
||||
semver: 7.5.4
|
||||
typescript: 5.4.4
|
||||
semver: 7.6.2
|
||||
typescript: 5.4.5
|
||||
yargs-parser: 21.1.1
|
||||
optionalDependencies:
|
||||
'@babel/core': 7.23.9
|
||||
'@jest/transform': 29.7.0
|
||||
'@jest/types': 29.6.3
|
||||
babel-jest: 29.7.0(@babel/core@7.23.9)
|
||||
|
||||
|
@ -3395,7 +3400,7 @@ snapshots:
|
|||
|
||||
type-fest@0.21.3: {}
|
||||
|
||||
typescript@5.4.4: {}
|
||||
typescript@5.4.5: {}
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
|
|
|
@ -3,19 +3,19 @@
|
|||
# it is expected that this script is called by run-federation-test.sh script.
|
||||
set -e
|
||||
|
||||
if [ -n "$LEMMY_LOG_LEVEL" ];
|
||||
if [ -z "$LEMMY_LOG_LEVEL" ];
|
||||
then
|
||||
LEMMY_LOG_LEVEL=warn
|
||||
LEMMY_LOG_LEVEL=info
|
||||
fi
|
||||
|
||||
export RUST_BACKTRACE=1
|
||||
#export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL"
|
||||
export RUST_LOG="warn,lemmy_server=$LEMMY_LOG_LEVEL,lemmy_federate=$LEMMY_LOG_LEVEL,lemmy_api=$LEMMY_LOG_LEVEL,lemmy_api_common=$LEMMY_LOG_LEVEL,lemmy_api_crud=$LEMMY_LOG_LEVEL,lemmy_apub=$LEMMY_LOG_LEVEL,lemmy_db_schema=$LEMMY_LOG_LEVEL,lemmy_db_views=$LEMMY_LOG_LEVEL,lemmy_db_views_actor=$LEMMY_LOG_LEVEL,lemmy_db_views_moderator=$LEMMY_LOG_LEVEL,lemmy_routes=$LEMMY_LOG_LEVEL,lemmy_utils=$LEMMY_LOG_LEVEL,lemmy_websocket=$LEMMY_LOG_LEVEL"
|
||||
|
||||
export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
|
||||
|
||||
# pictrs setup
|
||||
if [ ! -f "api_tests/pict-rs" ]; then
|
||||
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.0-beta.2/pict-rs-linux-amd64" -o api_tests/pict-rs
|
||||
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.13/pict-rs-linux-amd64" -o api_tests/pict-rs
|
||||
chmod +x api_tests/pict-rs
|
||||
fi
|
||||
./api_tests/pict-rs \
|
||||
|
|
|
@ -37,15 +37,15 @@ import {
|
|||
followCommunity,
|
||||
blockCommunity,
|
||||
delay,
|
||||
saveUserSettings,
|
||||
} from "./shared";
|
||||
import { CommentView, CommunityView } from "lemmy-js-client";
|
||||
import { CommentView, CommunityView, SaveUserSettings } from "lemmy-js-client";
|
||||
|
||||
let betaCommunity: CommunityView | undefined;
|
||||
let postOnAlphaRes: PostResponse;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupLogins();
|
||||
await unfollows();
|
||||
await Promise.all([followBeta(alpha), followBeta(gamma)]);
|
||||
betaCommunity = (await resolveBetaCommunity(alpha)).community;
|
||||
if (betaCommunity) {
|
||||
|
@ -444,6 +444,59 @@ test("Reply to a comment from another instance, get notification", async () => {
|
|||
assertCommentFederation(alphaReply, replyRes.comment_view);
|
||||
});
|
||||
|
||||
test("Bot reply notifications are filtered when bots are hidden", async () => {
|
||||
const newAlphaBot = await registerUser(alpha, alphaUrl);
|
||||
let form: SaveUserSettings = {
|
||||
bot_account: true,
|
||||
};
|
||||
await saveUserSettings(newAlphaBot, form);
|
||||
|
||||
const alphaCommunity = (
|
||||
await resolveCommunity(alpha, "!main@lemmy-alpha:8541")
|
||||
).community;
|
||||
|
||||
if (!alphaCommunity) {
|
||||
throw "Missing alpha community";
|
||||
}
|
||||
|
||||
await alpha.markAllAsRead();
|
||||
form = {
|
||||
show_bot_accounts: false,
|
||||
};
|
||||
await saveUserSettings(alpha, form);
|
||||
const postOnAlphaRes = await createPost(alpha, alphaCommunity.community.id);
|
||||
|
||||
// Bot reply to alpha's post
|
||||
let commentRes = await createComment(
|
||||
newAlphaBot,
|
||||
postOnAlphaRes.post_view.post.id,
|
||||
);
|
||||
expect(commentRes).toBeDefined();
|
||||
|
||||
let alphaUnreadCountRes = await getUnreadCount(alpha);
|
||||
expect(alphaUnreadCountRes.replies).toBe(0);
|
||||
|
||||
let alphaUnreadRepliesRes = await getReplies(alpha, true);
|
||||
expect(alphaUnreadRepliesRes.replies.length).toBe(0);
|
||||
|
||||
// This both restores the original state that may be expected by other tests
|
||||
// implicitly and is used by the next steps to ensure replies are still
|
||||
// returned when a user later decides to show bot accounts again.
|
||||
form = {
|
||||
show_bot_accounts: true,
|
||||
};
|
||||
await saveUserSettings(alpha, form);
|
||||
|
||||
alphaUnreadCountRes = await getUnreadCount(alpha);
|
||||
expect(alphaUnreadCountRes.replies).toBe(1);
|
||||
|
||||
alphaUnreadRepliesRes = await getReplies(alpha, true);
|
||||
expect(alphaUnreadRepliesRes.replies.length).toBe(1);
|
||||
expect(alphaUnreadRepliesRes.replies[0].comment.id).toBe(
|
||||
commentRes.comment_view.comment.id,
|
||||
);
|
||||
});
|
||||
|
||||
test("Mention beta from alpha", async () => {
|
||||
if (!betaCommunity) throw Error("no community");
|
||||
const postOnAlphaRes = await createPost(alpha, betaCommunity.community.id);
|
||||
|
|
|
@ -380,8 +380,8 @@ test("User blocks instance, communities are hidden", async () => {
|
|||
test("Community follower count is federated", async () => {
|
||||
// Follow the beta community from alpha
|
||||
let community = await createCommunity(beta);
|
||||
let community_id = community.community_view.community.actor_id;
|
||||
let resolved = await resolveCommunity(alpha, community_id);
|
||||
let communityActorId = community.community_view.community.actor_id;
|
||||
let resolved = await resolveCommunity(alpha, communityActorId);
|
||||
if (!resolved.community) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
@ -389,7 +389,7 @@ test("Community follower count is federated", async () => {
|
|||
await followCommunity(alpha, true, resolved.community.community.id);
|
||||
let followed = (
|
||||
await waitUntil(
|
||||
() => resolveCommunity(alpha, community_id),
|
||||
() => resolveCommunity(alpha, communityActorId),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
|
@ -398,7 +398,7 @@ test("Community follower count is federated", async () => {
|
|||
expect(followed?.counts.subscribers).toBe(1);
|
||||
|
||||
// Follow the community from gamma
|
||||
resolved = await resolveCommunity(gamma, community_id);
|
||||
resolved = await resolveCommunity(gamma, communityActorId);
|
||||
if (!resolved.community) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
@ -406,7 +406,7 @@ test("Community follower count is federated", async () => {
|
|||
await followCommunity(gamma, true, resolved.community.community.id);
|
||||
followed = (
|
||||
await waitUntil(
|
||||
() => resolveCommunity(gamma, community_id),
|
||||
() => resolveCommunity(gamma, communityActorId),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
|
@ -415,7 +415,7 @@ test("Community follower count is federated", async () => {
|
|||
expect(followed?.counts?.subscribers).toBe(2);
|
||||
|
||||
// Follow the community from delta
|
||||
resolved = await resolveCommunity(delta, community_id);
|
||||
resolved = await resolveCommunity(delta, communityActorId);
|
||||
if (!resolved.community) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
|
@ -423,7 +423,7 @@ test("Community follower count is federated", async () => {
|
|||
await followCommunity(delta, true, resolved.community.community.id);
|
||||
followed = (
|
||||
await waitUntil(
|
||||
() => resolveCommunity(delta, community_id),
|
||||
() => resolveCommunity(delta, communityActorId),
|
||||
c => c.community?.subscribed === "Subscribed",
|
||||
)
|
||||
).community;
|
||||
|
|
|
@ -29,14 +29,17 @@ import {
|
|||
unfollows,
|
||||
getPost,
|
||||
waitUntil,
|
||||
randomString,
|
||||
createPostWithThumbnail,
|
||||
sampleImage,
|
||||
sampleSite,
|
||||
} from "./shared";
|
||||
const downloadFileSync = require("download-file-sync");
|
||||
|
||||
beforeAll(setupLogins);
|
||||
|
||||
afterAll(unfollows);
|
||||
afterAll(async () => {
|
||||
await Promise.all([unfollows(), deleteAllImages(alpha)]);
|
||||
});
|
||||
|
||||
test("Upload image and delete it", async () => {
|
||||
// Before running this test, you need to delete all previous images in the DB
|
||||
|
@ -159,7 +162,6 @@ test("Purge post, linked image removed", async () => {
|
|||
expect(post.post_view.post.url).toBe(upload.url);
|
||||
|
||||
// purge post
|
||||
|
||||
const purgeForm: PurgePost = {
|
||||
post_id: post.post_view.post.id,
|
||||
};
|
||||
|
@ -171,48 +173,94 @@ test("Purge post, linked image removed", async () => {
|
|||
expect(content2).toBe("");
|
||||
});
|
||||
|
||||
test("Images in remote post are proxied if setting enabled", async () => {
|
||||
let user = await registerUser(beta, betaUrl);
|
||||
test("Images in remote image post are proxied if setting enabled", async () => {
|
||||
let community = await createCommunity(gamma);
|
||||
|
||||
const upload_form: UploadImage = {
|
||||
image: Buffer.from("test"),
|
||||
};
|
||||
const upload = await user.uploadImage(upload_form);
|
||||
let post = await createPost(
|
||||
let postRes = await createPost(
|
||||
gamma,
|
||||
community.community_view.community.id,
|
||||
upload.url,
|
||||
"![](http://example.com/image2.png)",
|
||||
sampleImage,
|
||||
`![](${sampleImage})`,
|
||||
);
|
||||
expect(post.post_view.post).toBeDefined();
|
||||
const post = postRes.post_view.post;
|
||||
expect(post).toBeDefined();
|
||||
|
||||
// remote image gets proxied after upload
|
||||
expect(
|
||||
post.post_view.post.url?.startsWith(
|
||||
post.thumbnail_url?.startsWith(
|
||||
"http://lemmy-gamma:8561/api/v3/image_proxy?url",
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
post.post_view.post.body?.startsWith(
|
||||
"![](http://lemmy-gamma:8561/api/v3/image_proxy?url",
|
||||
),
|
||||
post.body?.startsWith("![](http://lemmy-gamma:8561/api/v3/image_proxy?url"),
|
||||
).toBeTruthy();
|
||||
|
||||
let epsilonPost = await resolvePost(epsilon, post.post_view.post);
|
||||
expect(epsilonPost.post).toBeDefined();
|
||||
// Make sure that it ends with jpg, to be sure its an image
|
||||
expect(post.thumbnail_url?.endsWith(".jpg")).toBeTruthy();
|
||||
|
||||
let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post);
|
||||
expect(epsilonPostRes.post).toBeDefined();
|
||||
|
||||
// Fetch the post again, the metadata should be backgrounded now
|
||||
// Wait for the metadata to get fetched, since this is backgrounded now
|
||||
let epsilonPostRes2 = await waitUntil(
|
||||
() => getPost(epsilon, epsilonPostRes.post!.post.id),
|
||||
p => p.post_view.post.thumbnail_url != undefined,
|
||||
);
|
||||
const epsilonPost = epsilonPostRes2.post_view.post;
|
||||
|
||||
// remote image gets proxied after federation
|
||||
expect(
|
||||
epsilonPost.post!.post.url?.startsWith(
|
||||
epsilonPost.thumbnail_url?.startsWith(
|
||||
"http://lemmy-epsilon:8581/api/v3/image_proxy?url",
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
epsilonPost.post!.post.body?.startsWith(
|
||||
epsilonPost.body?.startsWith(
|
||||
"![](http://lemmy-epsilon:8581/api/v3/image_proxy?url",
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// Make sure that it ends with jpg, to be sure its an image
|
||||
expect(epsilonPost.thumbnail_url?.endsWith(".jpg")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("Thumbnail of remote image link is proxied if setting enabled", async () => {
|
||||
let community = await createCommunity(gamma);
|
||||
let postRes = await createPost(
|
||||
gamma,
|
||||
community.community_view.community.id,
|
||||
// The sample site metadata thumbnail ends in png
|
||||
sampleSite,
|
||||
);
|
||||
const post = postRes.post_view.post;
|
||||
expect(post).toBeDefined();
|
||||
|
||||
// remote image gets proxied after upload
|
||||
expect(
|
||||
post.thumbnail_url?.startsWith(
|
||||
"http://lemmy-gamma:8561/api/v3/image_proxy?url",
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// Make sure that it ends with png, to be sure its an image
|
||||
expect(post.thumbnail_url?.endsWith(".png")).toBeTruthy();
|
||||
|
||||
let epsilonPostRes = await resolvePost(epsilon, postRes.post_view.post);
|
||||
expect(epsilonPostRes.post).toBeDefined();
|
||||
|
||||
let epsilonPostRes2 = await waitUntil(
|
||||
() => getPost(epsilon, epsilonPostRes.post!.post.id),
|
||||
p => p.post_view.post.thumbnail_url != undefined,
|
||||
);
|
||||
const epsilonPost = epsilonPostRes2.post_view.post;
|
||||
|
||||
expect(
|
||||
epsilonPost.thumbnail_url?.startsWith(
|
||||
"http://lemmy-epsilon:8581/api/v3/image_proxy?url",
|
||||
),
|
||||
).toBeTruthy();
|
||||
|
||||
// Make sure that it ends with png, to be sure its an image
|
||||
expect(epsilonPost.thumbnail_url?.endsWith(".png")).toBeTruthy();
|
||||
});
|
||||
|
||||
test("No image proxying if setting is disabled", async () => {
|
||||
|
@ -232,7 +280,7 @@ test("No image proxying if setting is disabled", async () => {
|
|||
alpha,
|
||||
community.community_view.community.id,
|
||||
upload.url,
|
||||
"![](http://example.com/image2.png)",
|
||||
`![](${sampleImage})`,
|
||||
);
|
||||
expect(post.post_view.post).toBeDefined();
|
||||
|
||||
|
@ -240,7 +288,7 @@ test("No image proxying if setting is disabled", async () => {
|
|||
expect(
|
||||
post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
|
||||
).toBeTruthy();
|
||||
expect(post.post_view.post.body).toBe("![](http://example.com/image2.png)");
|
||||
expect(post.post_view.post.body).toBe(`![](${sampleImage})`);
|
||||
|
||||
let betaPost = await waitForPost(
|
||||
beta,
|
||||
|
@ -253,8 +301,7 @@ test("No image proxying if setting is disabled", async () => {
|
|||
expect(
|
||||
betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
|
||||
).toBeTruthy();
|
||||
expect(betaPost.post.body).toBe("![](http://example.com/image2.png)");
|
||||
|
||||
expect(betaPost.post.body).toBe(`![](${sampleImage})`);
|
||||
// Make sure the alt text got federated
|
||||
expect(post.post_view.post.alt_text).toBe(betaPost.post.alt_text);
|
||||
});
|
||||
|
|
|
@ -48,7 +48,6 @@ beforeAll(async () => {
|
|||
await setupLogins();
|
||||
betaCommunity = (await resolveBetaCommunity(alpha)).community;
|
||||
expect(betaCommunity).toBeDefined();
|
||||
await unfollows();
|
||||
});
|
||||
|
||||
afterAll(unfollows);
|
||||
|
@ -83,10 +82,7 @@ async function assertPostFederation(postOne: PostView, postTwo: PostView) {
|
|||
|
||||
test("Create a post", async () => {
|
||||
// Setup some allowlists and blocklists
|
||||
let editSiteForm: EditSite = {
|
||||
allowed_instances: ["lemmy-beta"],
|
||||
};
|
||||
await delta.editSite(editSiteForm);
|
||||
const editSiteForm: EditSite = {};
|
||||
|
||||
editSiteForm.allowed_instances = [];
|
||||
editSiteForm.blocked_instances = ["lemmy-alpha"];
|
||||
|
@ -661,40 +657,60 @@ test("A and G subscribe to B (center) A posts, it gets announced to G", async ()
|
|||
});
|
||||
|
||||
test("Report a post", async () => {
|
||||
// Note, this is a different one from the setup
|
||||
let betaCommunity = (await resolveBetaCommunity(beta)).community;
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
}
|
||||
// Create post from alpha
|
||||
let alphaCommunity = (await resolveBetaCommunity(alpha)).community!;
|
||||
await followBeta(alpha);
|
||||
let postRes = await createPost(beta, betaCommunity.community.id);
|
||||
let postRes = await createPost(alpha, alphaCommunity.community.id);
|
||||
expect(postRes.post_view.post).toBeDefined();
|
||||
|
||||
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post;
|
||||
if (!alphaPost) {
|
||||
throw "Missing alpha post";
|
||||
}
|
||||
let alphaReport = (
|
||||
await reportPost(alpha, alphaPost.post.id, randomString(10))
|
||||
).post_report_view.post_report;
|
||||
|
||||
// Send report from gamma
|
||||
let gammaPost = (await resolvePost(gamma, alphaPost.post)).post!;
|
||||
let gammaReport = (
|
||||
await reportPost(gamma, gammaPost.post.id, randomString(10))
|
||||
).post_report_view.post_report;
|
||||
expect(gammaReport).toBeDefined();
|
||||
|
||||
// Report was federated to community instance
|
||||
let betaReport = (await waitUntil(
|
||||
() =>
|
||||
listPostReports(beta).then(p =>
|
||||
p.post_reports.find(
|
||||
r =>
|
||||
r.post_report.original_post_name === alphaReport.original_post_name,
|
||||
r.post_report.original_post_name === gammaReport.original_post_name,
|
||||
),
|
||||
),
|
||||
res => !!res,
|
||||
))!.post_report;
|
||||
expect(betaReport).toBeDefined();
|
||||
expect(betaReport.resolved).toBe(false);
|
||||
expect(betaReport.original_post_name).toBe(alphaReport.original_post_name);
|
||||
expect(betaReport.original_post_url).toBe(alphaReport.original_post_url);
|
||||
expect(betaReport.original_post_body).toBe(alphaReport.original_post_body);
|
||||
expect(betaReport.reason).toBe(alphaReport.reason);
|
||||
expect(betaReport.original_post_name).toBe(gammaReport.original_post_name);
|
||||
//expect(betaReport.original_post_url).toBe(gammaReport.original_post_url);
|
||||
expect(betaReport.original_post_body).toBe(gammaReport.original_post_body);
|
||||
expect(betaReport.reason).toBe(gammaReport.reason);
|
||||
await unfollowRemotes(alpha);
|
||||
|
||||
// Report was federated to poster's instance
|
||||
let alphaReport = (await waitUntil(
|
||||
() =>
|
||||
listPostReports(alpha).then(p =>
|
||||
p.post_reports.find(
|
||||
r =>
|
||||
r.post_report.original_post_name === gammaReport.original_post_name,
|
||||
),
|
||||
),
|
||||
res => !!res,
|
||||
))!.post_report;
|
||||
expect(alphaReport).toBeDefined();
|
||||
expect(alphaReport.resolved).toBe(false);
|
||||
expect(alphaReport.original_post_name).toBe(gammaReport.original_post_name);
|
||||
//expect(alphaReport.original_post_url).toBe(gammaReport.original_post_url);
|
||||
expect(alphaReport.original_post_body).toBe(gammaReport.original_post_body);
|
||||
expect(alphaReport.reason).toBe(gammaReport.reason);
|
||||
});
|
||||
|
||||
test("Fetch post via redirect", async () => {
|
||||
|
@ -729,7 +745,7 @@ test("Block post that contains banned URL", async () => {
|
|||
|
||||
await epsilon.editSite(editSiteForm);
|
||||
|
||||
await delay(500);
|
||||
await delay();
|
||||
|
||||
if (!betaCommunity) {
|
||||
throw "Missing beta community";
|
||||
|
|
|
@ -81,21 +81,24 @@ import { ListingType } from "lemmy-js-client/dist/types/ListingType";
|
|||
|
||||
export const fetchFunction = fetch;
|
||||
export const imageFetchLimit = 50;
|
||||
export const sampleImage =
|
||||
"https://i.pinimg.com/originals/df/5f/5b/df5f5b1b174a2b4b6026cc6c8f9395c1.jpg";
|
||||
export const sampleSite = "https://yahoo.com";
|
||||
|
||||
export let alphaUrl = "http://127.0.0.1:8541";
|
||||
export let betaUrl = "http://127.0.0.1:8551";
|
||||
export let gammaUrl = "http://127.0.0.1:8561";
|
||||
export let deltaUrl = "http://127.0.0.1:8571";
|
||||
export let epsilonUrl = "http://127.0.0.1:8581";
|
||||
export const alphaUrl = "http://127.0.0.1:8541";
|
||||
export const betaUrl = "http://127.0.0.1:8551";
|
||||
export const gammaUrl = "http://127.0.0.1:8561";
|
||||
export const deltaUrl = "http://127.0.0.1:8571";
|
||||
export const epsilonUrl = "http://127.0.0.1:8581";
|
||||
|
||||
export let alpha = new LemmyHttp(alphaUrl, { fetchFunction });
|
||||
export let alphaImage = new LemmyHttp(alphaUrl);
|
||||
export let beta = new LemmyHttp(betaUrl, { fetchFunction });
|
||||
export let gamma = new LemmyHttp(gammaUrl, { fetchFunction });
|
||||
export let delta = new LemmyHttp(deltaUrl, { fetchFunction });
|
||||
export let epsilon = new LemmyHttp(epsilonUrl, { fetchFunction });
|
||||
export const alpha = new LemmyHttp(alphaUrl, { fetchFunction });
|
||||
export const alphaImage = new LemmyHttp(alphaUrl);
|
||||
export const beta = new LemmyHttp(betaUrl, { fetchFunction });
|
||||
export const gamma = new LemmyHttp(gammaUrl, { fetchFunction });
|
||||
export const delta = new LemmyHttp(deltaUrl, { fetchFunction });
|
||||
export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction });
|
||||
|
||||
export let betaAllowedInstances = [
|
||||
export const betaAllowedInstances = [
|
||||
"lemmy-alpha",
|
||||
"lemmy-gamma",
|
||||
"lemmy-delta",
|
||||
|
@ -180,6 +183,10 @@ export async function setupLogins() {
|
|||
];
|
||||
await gamma.editSite(editSiteForm);
|
||||
|
||||
// Setup delta allowed instance
|
||||
editSiteForm.allowed_instances = ["lemmy-beta"];
|
||||
await delta.editSite(editSiteForm);
|
||||
|
||||
// Create the main alpha/beta communities
|
||||
// Ignore thrown errors of duplicates
|
||||
try {
|
||||
|
@ -357,10 +364,13 @@ export async function getUnreadCount(
|
|||
return api.getUnreadCount();
|
||||
}
|
||||
|
||||
export async function getReplies(api: LemmyHttp): Promise<GetRepliesResponse> {
|
||||
export async function getReplies(
|
||||
api: LemmyHttp,
|
||||
unread_only: boolean = false,
|
||||
): Promise<GetRepliesResponse> {
|
||||
let form: GetReplies = {
|
||||
sort: "New",
|
||||
unread_only: false,
|
||||
unread_only,
|
||||
};
|
||||
return api.getReplies(form);
|
||||
}
|
||||
|
@ -693,8 +703,8 @@ export async function saveUserSettingsBio(
|
|||
export async function saveUserSettingsFederated(
|
||||
api: LemmyHttp,
|
||||
): Promise<SuccessResponse> {
|
||||
let avatar = "https://image.flaticon.com/icons/png/512/35/35896.png";
|
||||
let banner = "https://image.flaticon.com/icons/png/512/36/35896.png";
|
||||
let avatar = sampleImage;
|
||||
let banner = sampleImage;
|
||||
let bio = "a changed bio";
|
||||
let form: SaveUserSettings = {
|
||||
show_nsfw: false,
|
||||
|
@ -760,6 +770,7 @@ export async function unfollowRemotes(
|
|||
await Promise.all(
|
||||
remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)),
|
||||
);
|
||||
|
||||
let siteRes = await getSite(api);
|
||||
return siteRes;
|
||||
}
|
||||
|
@ -889,14 +900,17 @@ export async function deleteAllImages(api: LemmyHttp) {
|
|||
limit: imageFetchLimit,
|
||||
});
|
||||
imagesRes.images;
|
||||
|
||||
for (const image of imagesRes.images) {
|
||||
Promise.all(
|
||||
imagesRes.images
|
||||
.map(image => {
|
||||
const form: DeleteImage = {
|
||||
token: image.local_image.pictrs_delete_token,
|
||||
filename: image.local_image.pictrs_alias,
|
||||
};
|
||||
await api.deleteImage(form);
|
||||
}
|
||||
return form;
|
||||
})
|
||||
.map(form => api.deleteImage(form)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function unfollows() {
|
||||
|
@ -907,6 +921,24 @@ export async function unfollows() {
|
|||
unfollowRemotes(delta),
|
||||
unfollowRemotes(epsilon),
|
||||
]);
|
||||
await Promise.all([
|
||||
purgeAllPosts(alpha),
|
||||
purgeAllPosts(beta),
|
||||
purgeAllPosts(gamma),
|
||||
purgeAllPosts(delta),
|
||||
purgeAllPosts(epsilon),
|
||||
]);
|
||||
}
|
||||
|
||||
export async function purgeAllPosts(api: LemmyHttp) {
|
||||
// The best way to get all federated items, is to find the posts
|
||||
let res = await api.getPosts({ type_: "All", limit: 50 });
|
||||
await Promise.all(
|
||||
Array.from(new Set(res.posts.map(p => p.post.id)))
|
||||
.map(post_id => api.purgePost({ post_id }))
|
||||
// Ignore errors
|
||||
.map(p => p.catch(e => e)),
|
||||
);
|
||||
}
|
||||
|
||||
export function getCommentParentId(comment: Comment): number | undefined {
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
fetchFunction,
|
||||
alphaImage,
|
||||
unfollows,
|
||||
saveUserSettingsBio,
|
||||
} from "./shared";
|
||||
import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client";
|
||||
import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
|
||||
|
@ -186,10 +187,26 @@ test("Set a new avatar, old avatar is deleted", async () => {
|
|||
expect(upload2.url).toBeDefined();
|
||||
|
||||
let form2 = {
|
||||
avatar: upload1.url,
|
||||
avatar: upload2.url,
|
||||
};
|
||||
await saveUserSettings(alpha, form2);
|
||||
// make sure only the new avatar is kept
|
||||
const listMediaRes2 = await alphaImage.listMedia();
|
||||
expect(listMediaRes2.images.length).toBe(1);
|
||||
|
||||
// Upload that same form2 avatar, make sure it isn't replaced / deleted
|
||||
await saveUserSettings(alpha, form2);
|
||||
// make sure only the new avatar is kept
|
||||
const listMediaRes3 = await alphaImage.listMedia();
|
||||
expect(listMediaRes3.images.length).toBe(1);
|
||||
|
||||
// Now try to save a user settings, with the icon missing,
|
||||
// and make sure it doesn't clear the data, or delete the image
|
||||
await saveUserSettingsBio(alpha);
|
||||
let site = await getSite(alpha);
|
||||
expect(site.my_user?.local_user_view.person.avatar).toBe(upload2.url);
|
||||
|
||||
// make sure only the new avatar is kept
|
||||
const listMediaRes4 = await alphaImage.listMedia();
|
||||
expect(listMediaRes4.images.length).toBe(1);
|
||||
});
|
||||
|
|
|
@ -47,7 +47,8 @@
|
|||
#
|
||||
# To be removed in 0.20
|
||||
cache_external_link_previews: true
|
||||
# Specifies how to handle remote images, so that users don't have to connect directly to remote servers.
|
||||
# Specifies how to handle remote images, so that users don't have to connect directly to remote
|
||||
# servers.
|
||||
image_mode:
|
||||
# Leave images unchanged, don't generate any local thumbnails for post urls. Instead the
|
||||
# Opengraph image is directly returned as thumbnail
|
||||
|
@ -64,10 +65,11 @@
|
|||
|
||||
# or
|
||||
|
||||
# If enabled, all images from remote domains are rewritten to pass through `/api/v3/image_proxy`,
|
||||
# including embedded images in markdown. Images are stored temporarily in pict-rs for caching.
|
||||
# This improves privacy as users don't expose their IP to untrusted servers, and decreases load
|
||||
# on other servers. However it increases bandwidth use for the local server.
|
||||
# If enabled, all images from remote domains are rewritten to pass through
|
||||
# `/api/v3/image_proxy`, including embedded images in markdown. Images are stored temporarily
|
||||
# in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted
|
||||
# servers, and decreases load on other servers. However it increases bandwidth use for the
|
||||
# local server.
|
||||
#
|
||||
# Requires pict-rs 0.5
|
||||
"ProxyAllImages"
|
||||
|
|
|
@ -33,7 +33,7 @@ anyhow = { workspace = true }
|
|||
tracing = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
url = { workspace = true }
|
||||
wav = "1.0.0"
|
||||
hound = "3.5.1"
|
||||
sitemap-rs = "0.2.1"
|
||||
totp-rs = { version = "5.5.1", features = ["gen_secret", "otpauth"] }
|
||||
actix-web-httpauth = "0.8.1"
|
||||
|
|
|
@ -36,9 +36,21 @@ pub async fn add_mod_to_community(
|
|||
let community = Community::read(&mut context.pool(), community_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
// If user is admin and community is remote, explicitly check that he is a
|
||||
// moderator. This is necessary because otherwise the action would be rejected
|
||||
// by the community's home instance.
|
||||
if local_user_view.local_user.admin && !community.local {
|
||||
let is_mod = CommunityModeratorView::is_community_moderator(
|
||||
&mut context.pool(),
|
||||
community.id,
|
||||
local_user_view.person.id,
|
||||
)
|
||||
.await?;
|
||||
if !is_mod {
|
||||
Err(LemmyErrorType::NotAModerator)?
|
||||
}
|
||||
}
|
||||
|
||||
// Update in local database
|
||||
let community_moderator_form = CommunityModeratorForm {
|
||||
|
|
|
@ -43,7 +43,10 @@ pub async fn ban_from_community(
|
|||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
is_valid_body_field(&data.reason, false)?;
|
||||
|
||||
if let Some(reason) = &data.reason {
|
||||
is_valid_body_field(reason, false)?;
|
||||
}
|
||||
|
||||
let community_user_ban_form = CommunityPersonBanForm {
|
||||
community_id: data.community_id,
|
||||
|
|
|
@ -49,26 +49,32 @@ pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult<String> {
|
|||
|
||||
// Decode each wav file, concatenate the samples
|
||||
let mut concat_samples: Vec<i16> = Vec::new();
|
||||
let mut any_header: Option<wav::Header> = None;
|
||||
let mut any_header: Option<hound::WavSpec> = None;
|
||||
for letter in letters {
|
||||
let mut cursor = Cursor::new(letter.unwrap_or_default());
|
||||
let (header, samples) = wav::read(&mut cursor)?;
|
||||
any_header = Some(header);
|
||||
if let Some(samples16) = samples.as_sixteen() {
|
||||
let reader = hound::WavReader::new(&mut cursor)?;
|
||||
any_header = Some(reader.spec());
|
||||
let samples16 = reader
|
||||
.into_samples::<i16>()
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
|
||||
concat_samples.extend(samples16);
|
||||
} else {
|
||||
Err(LemmyErrorType::CouldntCreateAudioCaptcha)?
|
||||
}
|
||||
}
|
||||
|
||||
// Encode the concatenated result as a wav file
|
||||
let mut output_buffer = Cursor::new(vec![]);
|
||||
if let Some(header) = any_header {
|
||||
wav::write(
|
||||
header,
|
||||
&wav::BitDepth::Sixteen(concat_samples),
|
||||
&mut output_buffer,
|
||||
)
|
||||
let mut writer = hound::WavWriter::new(&mut output_buffer, header)
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
|
||||
let mut writer16 = writer.get_i16_writer(concat_samples.len() as u32);
|
||||
for sample in concat_samples {
|
||||
writer16.write_sample(sample);
|
||||
}
|
||||
writer16
|
||||
.flush()
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
|
||||
writer
|
||||
.finalize()
|
||||
.with_lemmy_type(LemmyErrorType::CouldntCreateAudioCaptcha)?;
|
||||
|
||||
Ok(base64.encode(output_buffer.into_inner()))
|
||||
|
|
|
@ -29,7 +29,7 @@ pub async fn add_admin(
|
|||
.await?
|
||||
.ok_or(LemmyErrorType::ObjectNotLocal)?;
|
||||
|
||||
let added_admin = LocalUser::update(
|
||||
LocalUser::update(
|
||||
&mut context.pool(),
|
||||
added_local_user.local_user.id,
|
||||
&LocalUserUpdateForm {
|
||||
|
@ -43,7 +43,7 @@ pub async fn add_admin(
|
|||
// Mod tables
|
||||
let form = ModAddForm {
|
||||
mod_person_id: local_user_view.person.id,
|
||||
other_person_id: added_admin.person_id,
|
||||
other_person_id: added_local_user.person.id,
|
||||
removed: Some(!data.added),
|
||||
};
|
||||
|
||||
|
|
|
@ -31,7 +31,9 @@ pub async fn ban_from_site(
|
|||
// Make sure user is an admin
|
||||
is_admin(&local_user_view)?;
|
||||
|
||||
is_valid_body_field(&data.reason, false)?;
|
||||
if let Some(reason) = &data.reason {
|
||||
is_valid_body_field(reason, false)?;
|
||||
}
|
||||
|
||||
let expires = check_expire_time(data.expires)?;
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ pub async fn change_password_after_reset(
|
|||
) -> LemmyResult<Json<SuccessResponse>> {
|
||||
// Fetch the user_id from the token
|
||||
let token = data.token.clone();
|
||||
let local_user_id = PasswordResetRequest::read_from_token(&mut context.pool(), &token)
|
||||
let local_user_id = PasswordResetRequest::read_and_delete(&mut context.pool(), &token)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::TokenNotFound)?
|
||||
.local_user_id;
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
use crate::{build_totp_2fa, generate_totp_2fa_secret};
|
||||
use activitypub_federation::config::Data;
|
||||
use actix_web::web::Json;
|
||||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
person::GenerateTotpSecretResponse,
|
||||
sensitive::Sensitive,
|
||||
};
|
||||
use lemmy_api_common::{context::LemmyContext, person::GenerateTotpSecretResponse};
|
||||
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
@ -41,6 +37,6 @@ pub async fn generate_totp_secret(
|
|||
.await?;
|
||||
|
||||
Ok(Json(GenerateTotpSecretResponse {
|
||||
totp_secret_url: Sensitive::new(secret_url),
|
||||
totp_secret_url: secret_url.into(),
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -11,9 +11,12 @@ pub async fn unread_count(
|
|||
) -> LemmyResult<Json<GetUnreadCountResponse>> {
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
let replies = CommentReplyView::get_unread_replies(&mut context.pool(), person_id).await?;
|
||||
let replies =
|
||||
CommentReplyView::get_unread_replies(&mut context.pool(), &local_user_view.local_user).await?;
|
||||
|
||||
let mentions = PersonMentionView::get_unread_mentions(&mut context.pool(), person_id).await?;
|
||||
let mentions =
|
||||
PersonMentionView::get_unread_mentions(&mut context.pool(), &local_user_view.local_user)
|
||||
.await?;
|
||||
|
||||
let private_messages =
|
||||
PrivateMessageView::get_unread_messages(&mut context.pool(), person_id).await?;
|
||||
|
|
|
@ -6,7 +6,6 @@ use lemmy_api_common::{
|
|||
utils::send_password_reset_email,
|
||||
SuccessResponse,
|
||||
};
|
||||
use lemmy_db_schema::source::password_reset_request::PasswordResetRequest;
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
|
@ -21,15 +20,6 @@ pub async fn reset_password(
|
|||
.await?
|
||||
.ok_or(LemmyErrorType::IncorrectLogin)?;
|
||||
|
||||
// Check for too many attempts (to limit potential abuse)
|
||||
let recent_resets_count = PasswordResetRequest::get_recent_password_resets_count(
|
||||
&mut context.pool(),
|
||||
local_user_view.local_user.id,
|
||||
)
|
||||
.await?;
|
||||
if recent_resets_count >= 3 {
|
||||
Err(LemmyErrorType::PasswordResetLimitReached)?
|
||||
}
|
||||
let site_view = SiteView::read_local(&mut context.pool())
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
|
||||
|
|
|
@ -21,13 +21,14 @@ use lemmy_db_schema::{
|
|||
person::{Person, PersonUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
utils::diesel_option_overwrite,
|
||||
utils::{diesel_string_update, diesel_url_update},
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorType, LemmyResult},
|
||||
utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id},
|
||||
};
|
||||
use std::ops::Deref;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn save_user_settings(
|
||||
|
@ -41,23 +42,29 @@ pub async fn save_user_settings(
|
|||
|
||||
let slur_regex = local_site_to_slur_regex(&site_view.local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
let bio = diesel_option_overwrite(
|
||||
process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context).await?,
|
||||
let bio = diesel_string_update(
|
||||
process_markdown_opt(&data.bio, &slur_regex, &url_blocklist, &context)
|
||||
.await?
|
||||
.as_deref(),
|
||||
);
|
||||
replace_image(&data.avatar, &local_user_view.person.avatar, &context).await?;
|
||||
replace_image(&data.banner, &local_user_view.person.banner, &context).await?;
|
||||
|
||||
let avatar = proxy_image_link_opt_api(&data.avatar, &context).await?;
|
||||
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;
|
||||
let display_name = diesel_option_overwrite(data.display_name.clone());
|
||||
let matrix_user_id = diesel_option_overwrite(data.matrix_user_id.clone());
|
||||
let avatar = diesel_url_update(data.avatar.as_deref())?;
|
||||
replace_image(&avatar, &local_user_view.person.avatar, &context).await?;
|
||||
let avatar = proxy_image_link_opt_api(avatar, &context).await?;
|
||||
|
||||
let banner = diesel_url_update(data.banner.as_deref())?;
|
||||
replace_image(&banner, &local_user_view.person.banner, &context).await?;
|
||||
let banner = proxy_image_link_opt_api(banner, &context).await?;
|
||||
|
||||
let display_name = diesel_string_update(data.display_name.as_deref());
|
||||
let matrix_user_id = diesel_string_update(data.matrix_user_id.as_deref());
|
||||
let email_deref = data.email.as_deref().map(str::to_lowercase);
|
||||
let email = diesel_option_overwrite(email_deref.clone());
|
||||
let email = diesel_string_update(email_deref.as_deref());
|
||||
|
||||
if let Some(Some(email)) = &email {
|
||||
let previous_email = local_user_view.local_user.email.clone().unwrap_or_default();
|
||||
// if email was changed, check that it is not taken and send verification mail
|
||||
if &previous_email != email {
|
||||
if previous_email.deref() != email {
|
||||
if LocalUser::is_email_taken(&mut context.pool(), email).await? {
|
||||
return Err(LemmyErrorType::EmailAlreadyExists)?;
|
||||
}
|
||||
|
@ -71,7 +78,8 @@ pub async fn save_user_settings(
|
|||
}
|
||||
}
|
||||
|
||||
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None value
|
||||
// When the site requires email, make sure email is not Some(None). IE, an overwrite to a None
|
||||
// value
|
||||
if let Some(email) = &email {
|
||||
if email.is_none() && site_view.local_site.require_email_verification {
|
||||
Err(LemmyErrorType::EmailRequired)?
|
||||
|
@ -141,11 +149,7 @@ pub async fn save_user_settings(
|
|||
..Default::default()
|
||||
};
|
||||
|
||||
// Ignore errors, because 'no fields updated' will return an error.
|
||||
// https://github.com/LemmyNet/lemmy/issues/4076
|
||||
LocalUser::update(&mut context.pool(), local_user_id, &local_user_form)
|
||||
.await
|
||||
.ok();
|
||||
LocalUser::update(&mut context.pool(), local_user_id, &local_user_form).await?;
|
||||
|
||||
// Update the vote display modes
|
||||
let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm {
|
||||
|
|
|
@ -9,12 +9,10 @@ use lemmy_db_schema::{
|
|||
source::{
|
||||
email_verification::EmailVerification,
|
||||
local_user::{LocalUser, LocalUserUpdateForm},
|
||||
person::Person,
|
||||
},
|
||||
traits::Crud,
|
||||
RegistrationMode,
|
||||
};
|
||||
use lemmy_db_views::structs::SiteView;
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
pub async fn verify_email(
|
||||
|
@ -38,7 +36,7 @@ pub async fn verify_email(
|
|||
};
|
||||
let local_user_id = verification.local_user_id;
|
||||
|
||||
let local_user = LocalUser::update(&mut context.pool(), local_user_id, &form).await?;
|
||||
LocalUser::update(&mut context.pool(), local_user_id, &form).await?;
|
||||
|
||||
EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?;
|
||||
|
||||
|
@ -46,11 +44,15 @@ pub async fn verify_email(
|
|||
if site_view.local_site.registration_mode == RegistrationMode::RequireApplication
|
||||
&& site_view.local_site.application_email_admins
|
||||
{
|
||||
let person = Person::read(&mut context.pool(), local_user.person_id)
|
||||
let local_user = LocalUserView::read(&mut context.pool(), local_user_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPerson)?;
|
||||
|
||||
send_new_applicant_email_to_admins(&person.name, &mut context.pool(), context.settings())
|
||||
send_new_applicant_email_to_admins(
|
||||
&local_user.person.name,
|
||||
&mut context.pool(),
|
||||
context.settings(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,14 +4,19 @@ use lemmy_api_common::{
|
|||
post::{GetSiteMetadata, GetSiteMetadataResponse},
|
||||
request::fetch_link_metadata,
|
||||
};
|
||||
use lemmy_utils::error::LemmyResult;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyResult},
|
||||
LemmyErrorType,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn get_link_metadata(
|
||||
data: Query<GetSiteMetadata>,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<Json<GetSiteMetadataResponse>> {
|
||||
let metadata = fetch_link_metadata(&data.url, &context).await?;
|
||||
let url = Url::parse(&data.url).with_lemmy_type(LemmyErrorType::InvalidUrl)?;
|
||||
let metadata = fetch_link_metadata(&url, &context).await?;
|
||||
|
||||
Ok(Json(GetSiteMetadataResponse { metadata }))
|
||||
}
|
||||
|
|
|
@ -68,7 +68,6 @@ pub async fn like_post(
|
|||
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
|
||||
}
|
||||
|
||||
// Mark the post as read
|
||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||
|
||||
let community = Community::read(&mut context.pool(), post.community_id)
|
||||
|
|
|
@ -38,7 +38,6 @@ pub async fn save_post(
|
|||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
// Mark the post as read
|
||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||
|
||||
Ok(Json(PostResponse { post_view }))
|
||||
|
|
|
@ -10,7 +10,7 @@ use lemmy_db_schema::{
|
|||
registration_application::{RegistrationApplication, RegistrationApplicationUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
utils::diesel_option_overwrite,
|
||||
utils::diesel_string_update,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
|
||||
use lemmy_utils::{error::LemmyResult, LemmyErrorType};
|
||||
|
@ -26,7 +26,7 @@ pub async fn approve_registration_application(
|
|||
is_admin(&local_user_view)?;
|
||||
|
||||
// Update the registration with reason, admin_id
|
||||
let deny_reason = diesel_option_overwrite(data.deny_reason.clone());
|
||||
let deny_reason = diesel_string_update(data.deny_reason.as_deref());
|
||||
let app_form = RegistrationApplicationUpdateForm {
|
||||
admin_id: Some(Some(local_user_view.person.id)),
|
||||
deny_reason,
|
||||
|
|
|
@ -25,7 +25,7 @@ full = [
|
|||
"lemmy_db_views_moderator/full",
|
||||
"lemmy_utils/full",
|
||||
"activitypub_federation",
|
||||
"encoding",
|
||||
"encoding_rs",
|
||||
"reqwest-middleware",
|
||||
"webpage",
|
||||
"ts-rs",
|
||||
|
@ -66,13 +66,13 @@ actix-web = { workspace = true, optional = true }
|
|||
enum-map = { workspace = true }
|
||||
urlencoding = { workspace = true }
|
||||
mime = { version = "0.3.17", optional = true }
|
||||
webpage = { version = "1.6", default-features = false, features = [
|
||||
webpage = { version = "2.0", default-features = false, features = [
|
||||
"serde",
|
||||
], optional = true }
|
||||
encoding = { version = "0.2.33", optional = true }
|
||||
jsonwebtoken = { version = "8.3.0", optional = true }
|
||||
encoding_rs = { version = "0.8.34", optional = true }
|
||||
jsonwebtoken = { version = "9.3.0", optional = true }
|
||||
# necessary for wasmt compilation
|
||||
getrandom = { version = "0.2.12", features = ["js"] }
|
||||
getrandom = { version = "0.2.15", features = ["js"] }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["getrandom"]
|
||||
|
|
|
@ -121,7 +121,8 @@ pub async fn send_local_notifs(
|
|||
if let Ok(Some(mention_user_view)) = user_view {
|
||||
// TODO
|
||||
// At some point, make it so you can't tag the parent creator either
|
||||
// Potential duplication of notifications, one for reply and the other for mention, is handled below by checking recipient ids
|
||||
// Potential duplication of notifications, one for reply and the other for mention, is handled
|
||||
// below by checking recipient ids
|
||||
recipient_ids.push(mention_user_view.local_user.id);
|
||||
|
||||
let user_mention_form = PersonMentionInsertForm {
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
use crate::{context::LemmyContext, sensitive::Sensitive};
|
||||
use crate::context::LemmyContext;
|
||||
use actix_web::{http::header::USER_AGENT, HttpRequest};
|
||||
use chrono::Utc;
|
||||
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::LocalUserId,
|
||||
sensitive::SensitiveString,
|
||||
source::login_token::{LoginToken, LoginTokenCreateForm},
|
||||
};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
|
@ -40,7 +41,7 @@ impl Claims {
|
|||
user_id: LocalUserId,
|
||||
req: HttpRequest,
|
||||
context: &LemmyContext,
|
||||
) -> LemmyResult<Sensitive<String>> {
|
||||
) -> LemmyResult<SensitiveString> {
|
||||
let hostname = context.settings().hostname.clone();
|
||||
let my_claims = Claims {
|
||||
sub: user_id.0.to_string(),
|
||||
|
@ -50,7 +51,7 @@ impl Claims {
|
|||
|
||||
let secret = &context.secret().jwt_secret;
|
||||
let key = EncodingKey::from_secret(secret.as_ref());
|
||||
let token = encode(&Header::default(), &my_claims, &key)?;
|
||||
let token: SensitiveString = encode(&Header::default(), &my_claims, &key)?.into();
|
||||
let ip = req
|
||||
.connection_info()
|
||||
.realip_remote_addr()
|
||||
|
@ -67,7 +68,7 @@ impl Claims {
|
|||
user_agent,
|
||||
};
|
||||
LoginToken::create(&mut context.pool(), form).await?;
|
||||
Ok(Sensitive::new(token))
|
||||
Ok(token)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -111,11 +112,7 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
let new_person = PersonInsertForm::builder()
|
||||
.name("Gerry9812".into())
|
||||
.public_key("pubkey".to_string())
|
||||
.instance_id(inserted_instance.id)
|
||||
.build();
|
||||
let new_person = PersonInsertForm::test_form(inserted_instance.id, "Gerry9812");
|
||||
|
||||
let inserted_person = Person::create(pool, &new_person).await.unwrap();
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ impl LemmyContext {
|
|||
let client = ClientBuilder::new(client).build();
|
||||
let secret = Secret {
|
||||
id: 0,
|
||||
jwt_secret: String::new(),
|
||||
jwt_secret: String::new().into(),
|
||||
};
|
||||
|
||||
let rate_limit_cell = RateLimitCell::with_test_config();
|
||||
|
|
|
@ -14,7 +14,6 @@ pub mod private_message;
|
|||
pub mod request;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod send_activity;
|
||||
pub mod sensitive;
|
||||
pub mod site;
|
||||
#[cfg(feature = "full")]
|
||||
pub mod utils;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::sensitive::Sensitive;
|
||||
use lemmy_db_schema::{
|
||||
newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId},
|
||||
sensitive::SensitiveString,
|
||||
source::site::Site,
|
||||
CommentSortType,
|
||||
ListingType,
|
||||
|
@ -25,8 +25,8 @@ use ts_rs::TS;
|
|||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Logging into lemmy.
|
||||
pub struct Login {
|
||||
pub username_or_email: Sensitive<String>,
|
||||
pub password: Sensitive<String>,
|
||||
pub username_or_email: SensitiveString,
|
||||
pub password: SensitiveString,
|
||||
/// May be required, if totp is enabled for their account.
|
||||
pub totp_2fa_token: Option<String>,
|
||||
}
|
||||
|
@ -38,11 +38,11 @@ pub struct Login {
|
|||
/// Register / Sign up to lemmy.
|
||||
pub struct Register {
|
||||
pub username: String,
|
||||
pub password: Sensitive<String>,
|
||||
pub password_verify: Sensitive<String>,
|
||||
pub show_nsfw: bool,
|
||||
pub password: SensitiveString,
|
||||
pub password_verify: SensitiveString,
|
||||
pub show_nsfw: Option<bool>,
|
||||
/// email is mandatory if email verification is enabled on the server
|
||||
pub email: Option<Sensitive<String>>,
|
||||
pub email: Option<SensitiveString>,
|
||||
/// The UUID of the captcha item.
|
||||
pub captcha_uuid: Option<String>,
|
||||
/// Your captcha answer.
|
||||
|
@ -99,7 +99,7 @@ pub struct SaveUserSettings {
|
|||
/// Your display name, which can contain strange characters, and does not need to be unique.
|
||||
pub display_name: Option<String>,
|
||||
/// Your email.
|
||||
pub email: Option<Sensitive<String>>,
|
||||
pub email: Option<SensitiveString>,
|
||||
/// Your bio / info, in markdown.
|
||||
pub bio: Option<String>,
|
||||
/// Your matrix user id. Ex: @my_user:matrix.org
|
||||
|
@ -124,7 +124,8 @@ pub struct SaveUserSettings {
|
|||
pub post_listing_mode: Option<PostListingMode>,
|
||||
/// Whether to allow keyboard navigation (for browsing and interacting with posts and comments).
|
||||
pub enable_keyboard_navigation: Option<bool>,
|
||||
/// Whether user avatars or inline images in the UI that are gifs should be allowed to play or should be paused
|
||||
/// Whether user avatars or inline images in the UI that are gifs should be allowed to play or
|
||||
/// should be paused
|
||||
pub enable_animated_images: Option<bool>,
|
||||
/// Whether to auto-collapse bot comments.
|
||||
pub collapse_bot_comments: Option<bool>,
|
||||
|
@ -140,9 +141,9 @@ pub struct SaveUserSettings {
|
|||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Changes your account password.
|
||||
pub struct ChangePassword {
|
||||
pub new_password: Sensitive<String>,
|
||||
pub new_password_verify: Sensitive<String>,
|
||||
pub old_password: Sensitive<String>,
|
||||
pub new_password: SensitiveString,
|
||||
pub new_password_verify: SensitiveString,
|
||||
pub old_password: SensitiveString,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
|
@ -151,8 +152,9 @@ pub struct ChangePassword {
|
|||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// A response for your login.
|
||||
pub struct LoginResponse {
|
||||
/// This is None in response to `Register` if email verification is enabled, or the server requires registration applications.
|
||||
pub jwt: Option<Sensitive<String>>,
|
||||
/// This is None in response to `Register` if email verification is enabled, or the server
|
||||
/// requires registration applications.
|
||||
pub jwt: Option<SensitiveString>,
|
||||
/// If registration applications are required, this will return true for a signup response.
|
||||
pub registration_created: bool,
|
||||
/// If email verifications are required, this will return true for a signup response.
|
||||
|
@ -340,7 +342,7 @@ pub struct CommentReplyResponse {
|
|||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Delete your account.
|
||||
pub struct DeleteAccount {
|
||||
pub password: Sensitive<String>,
|
||||
pub password: SensitiveString,
|
||||
pub delete_content: bool,
|
||||
}
|
||||
|
||||
|
@ -349,7 +351,7 @@ pub struct DeleteAccount {
|
|||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Reset your password via email.
|
||||
pub struct PasswordReset {
|
||||
pub email: Sensitive<String>,
|
||||
pub email: SensitiveString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
|
||||
|
@ -357,9 +359,9 @@ pub struct PasswordReset {
|
|||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Change your password after receiving a reset request.
|
||||
pub struct PasswordChangeAfterReset {
|
||||
pub token: Sensitive<String>,
|
||||
pub password: Sensitive<String>,
|
||||
pub password_verify: Sensitive<String>,
|
||||
pub token: SensitiveString,
|
||||
pub password: SensitiveString,
|
||||
pub password_verify: SensitiveString,
|
||||
}
|
||||
|
||||
#[skip_serializing_none]
|
||||
|
@ -405,7 +407,7 @@ pub struct VerifyEmail {
|
|||
#[cfg_attr(feature = "full", derive(TS))]
|
||||
#[cfg_attr(feature = "full", ts(export))]
|
||||
pub struct GenerateTotpSecretResponse {
|
||||
pub totp_secret_url: Sensitive<String>,
|
||||
pub totp_secret_url: SensitiveString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
|
||||
|
|
|
@ -10,7 +10,6 @@ use serde::{Deserialize, Serialize};
|
|||
use serde_with::skip_serializing_none;
|
||||
#[cfg(feature = "full")]
|
||||
use ts_rs::TS;
|
||||
use url::Url;
|
||||
|
||||
#[skip_serializing_none]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
|
||||
|
@ -20,8 +19,7 @@ use url::Url;
|
|||
pub struct CreatePost {
|
||||
pub name: String,
|
||||
pub community_id: CommunityId,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub url: Option<Url>,
|
||||
pub url: Option<String>,
|
||||
/// An optional body for the post in markdown.
|
||||
pub body: Option<String>,
|
||||
/// An optional alt_text, usable for image posts.
|
||||
|
@ -30,9 +28,8 @@ pub struct CreatePost {
|
|||
pub honeypot: Option<String>,
|
||||
pub nsfw: Option<bool>,
|
||||
pub language_id: Option<LanguageId>,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
/// Instead of fetching a thumbnail, use a custom one.
|
||||
pub custom_thumbnail: Option<Url>,
|
||||
pub custom_thumbnail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
@ -114,17 +111,15 @@ pub struct CreatePostLike {
|
|||
pub struct EditPost {
|
||||
pub post_id: PostId,
|
||||
pub name: Option<String>,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub url: Option<Url>,
|
||||
pub url: Option<String>,
|
||||
/// An optional body for the post in markdown.
|
||||
pub body: Option<String>,
|
||||
/// An optional alt_text, usable for image posts.
|
||||
pub alt_text: Option<String>,
|
||||
pub nsfw: Option<bool>,
|
||||
pub language_id: Option<LanguageId>,
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
/// Instead of fetching a thumbnail, use a custom one.
|
||||
pub custom_thumbnail: Option<Url>,
|
||||
pub custom_thumbnail: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Copy, Default, PartialEq, Eq, Hash)]
|
||||
|
@ -249,8 +244,7 @@ pub struct ListPostReportsResponse {
|
|||
#[cfg_attr(feature = "full", ts(export))]
|
||||
/// Get metadata for a given site.
|
||||
pub struct GetSiteMetadata {
|
||||
#[cfg_attr(feature = "full", ts(type = "string"))]
|
||||
pub url: Url,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
|
|
@ -3,10 +3,11 @@ use crate::{
|
|||
lemmy_db_schema::traits::Crud,
|
||||
post::{LinkMetadata, OpenGraphData},
|
||||
send_activity::{ActivityChannel, SendActivityData},
|
||||
utils::{local_site_opt_to_sensitive, proxy_image_link, proxy_image_link_opt_apub},
|
||||
utils::{local_site_opt_to_sensitive, proxy_image_link},
|
||||
};
|
||||
use activitypub_federation::config::Data;
|
||||
use encoding::{all::encodings, DecoderTrap};
|
||||
use chrono::{DateTime, Utc};
|
||||
use encoding_rs::{Encoding, UTF_8};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::DbUrl,
|
||||
source::{
|
||||
|
@ -18,14 +19,13 @@ use lemmy_db_schema::{
|
|||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyErrorType, LemmyResult},
|
||||
settings::structs::{PictrsImageMode, Settings},
|
||||
spawn_try_task,
|
||||
REQWEST_TIMEOUT,
|
||||
VERSION,
|
||||
};
|
||||
use mime::Mime;
|
||||
use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder};
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::info;
|
||||
use url::Url;
|
||||
use urlencoding::encode;
|
||||
|
@ -65,22 +65,20 @@ pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResu
|
|||
})
|
||||
}
|
||||
|
||||
/// Generate post thumbnail in background task, because some sites can be very slow to respond.
|
||||
/// 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.
|
||||
///
|
||||
/// TODO: `federated_thumbnail` param can be removed once we federate full metadata and can
|
||||
/// write it to db directly, without calling this function.
|
||||
/// https://github.com/LemmyNet/lemmy/issues/4598
|
||||
pub fn generate_post_link_metadata(
|
||||
pub async fn generate_post_link_metadata(
|
||||
post: Post,
|
||||
custom_thumbnail: Option<Url>,
|
||||
federated_thumbnail: Option<Url>,
|
||||
send_activity: impl FnOnce(Post) -> Option<SendActivityData> + Send + 'static,
|
||||
local_site: Option<LocalSite>,
|
||||
context: Data<LemmyContext>,
|
||||
) {
|
||||
spawn_try_task(async move {
|
||||
) -> LemmyResult<()> {
|
||||
let metadata = match &post.url {
|
||||
Some(url) => fetch_link_metadata(url, &context).await.unwrap_or_default(),
|
||||
_ => Default::default(),
|
||||
|
@ -95,34 +93,28 @@ pub fn generate_post_link_metadata(
|
|||
let allow_sensitive = local_site_opt_to_sensitive(&local_site);
|
||||
let allow_generate_thumbnail = allow_sensitive || !post.nsfw;
|
||||
|
||||
// Use custom thumbnail if available and its not an image post
|
||||
let thumbnail_url = if !is_image_post && custom_thumbnail.is_some() {
|
||||
custom_thumbnail
|
||||
}
|
||||
// Use federated thumbnail if available
|
||||
else if federated_thumbnail.is_some() {
|
||||
federated_thumbnail
|
||||
}
|
||||
// Generate local thumbnail if allowed
|
||||
else if allow_generate_thumbnail {
|
||||
match post.url.or(metadata.opengraph_data.image) {
|
||||
Some(url) => generate_pictrs_thumbnail(&url, &context).await.ok(),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
// Otherwise use opengraph preview image directly
|
||||
else {
|
||||
metadata.opengraph_data.image.map(Into::into)
|
||||
let image_url = if is_image_post {
|
||||
post.url
|
||||
} else {
|
||||
metadata.opengraph_data.image.clone()
|
||||
};
|
||||
|
||||
// Proxy the image fetch if necessary
|
||||
let proxied_thumbnail_url = proxy_image_link_opt_apub(thumbnail_url, &context).await?;
|
||||
let thumbnail_url = if let (false, Some(url)) = (is_image_post, custom_thumbnail) {
|
||||
proxy_image_link(url, &context).await.ok()
|
||||
} else if let (true, Some(url)) = (allow_generate_thumbnail, image_url) {
|
||||
generate_pictrs_thumbnail(&url, &context)
|
||||
.await
|
||||
.ok()
|
||||
.map(Into::into)
|
||||
} else {
|
||||
metadata.opengraph_data.image.clone()
|
||||
};
|
||||
|
||||
let form = PostUpdateForm {
|
||||
embed_title: Some(metadata.opengraph_data.title),
|
||||
embed_description: Some(metadata.opengraph_data.description),
|
||||
embed_video_url: Some(metadata.opengraph_data.embed_video_url),
|
||||
thumbnail_url: Some(proxied_thumbnail_url),
|
||||
thumbnail_url: Some(thumbnail_url),
|
||||
url_content_type: Some(metadata.content_type),
|
||||
..Default::default()
|
||||
};
|
||||
|
@ -131,36 +123,21 @@ pub fn generate_post_link_metadata(
|
|||
ActivityChannel::submit_activity(send_activity, &context).await?;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
|
||||
/// Extract site metadata from HTML Opengraph attributes.
|
||||
fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraphData> {
|
||||
let html = String::from_utf8_lossy(html_bytes);
|
||||
|
||||
// Make sure the first line is doctype html
|
||||
let first_line = html
|
||||
.trim_start()
|
||||
.lines()
|
||||
.next()
|
||||
.ok_or(LemmyErrorType::NoLinesInHtml)?
|
||||
.to_lowercase();
|
||||
|
||||
if !first_line.starts_with("<!doctype html") {
|
||||
Err(LemmyErrorType::SiteMetadataPageIsNotDoctypeHtml)?
|
||||
}
|
||||
|
||||
let mut page = HTML::from_string(html.to_string(), None)?;
|
||||
|
||||
// If the web page specifies that it isn't actually UTF-8, re-decode the received bytes with the
|
||||
// proper encoding. If the specified encoding cannot be found, fall back to the original UTF-8
|
||||
// version.
|
||||
if let Some(charset) = page.meta.get("charset") {
|
||||
if charset.to_lowercase() != "utf-8" {
|
||||
if let Some(encoding_ref) = encodings().iter().find(|e| e.name() == charset) {
|
||||
if let Ok(html_with_encoding) = encoding_ref.decode(html_bytes, DecoderTrap::Replace) {
|
||||
page = HTML::from_string(html_with_encoding, None)?;
|
||||
}
|
||||
if charset != UTF_8.name() {
|
||||
if let Some(encoding) = Encoding::for_label(charset.as_bytes()) {
|
||||
page = HTML::from_string(encoding.decode(html_bytes).0.into(), None)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -199,19 +176,40 @@ fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraph
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct PictrsResponse {
|
||||
files: Vec<PictrsFile>,
|
||||
msg: String,
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct PictrsResponse {
|
||||
pub files: Option<Vec<PictrsFile>>,
|
||||
pub msg: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct PictrsFile {
|
||||
file: String,
|
||||
delete_token: String,
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct PictrsFile {
|
||||
pub file: String,
|
||||
pub delete_token: String,
|
||||
pub details: PictrsFileDetails,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
impl PictrsFile {
|
||||
pub fn thumbnail_url(&self, protocol_and_hostname: &str) -> Result<Url, url::ParseError> {
|
||||
Url::parse(&format!(
|
||||
"{protocol_and_hostname}/pictrs/image/{}",
|
||||
self.file
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// Stores extra details about a Pictrs image.
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
pub struct PictrsFileDetails {
|
||||
/// In pixels
|
||||
pub width: u16,
|
||||
/// In pixels
|
||||
pub height: u16,
|
||||
pub content_type: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Debug)]
|
||||
struct PictrsPurgeResponse {
|
||||
msg: String,
|
||||
}
|
||||
|
@ -293,33 +291,34 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
|
|||
encode(image_url.as_str())
|
||||
);
|
||||
|
||||
let response = context
|
||||
let res = context
|
||||
.client()
|
||||
.get(&fetch_url)
|
||||
.timeout(REQWEST_TIMEOUT)
|
||||
.send()
|
||||
.await?
|
||||
.json::<PictrsResponse>()
|
||||
.await?;
|
||||
|
||||
let response: PictrsResponse = response.json().await?;
|
||||
let files = res.files.unwrap_or_default();
|
||||
|
||||
let image = files
|
||||
.first()
|
||||
.ok_or(LemmyErrorType::PictrsResponseError(res.msg))?;
|
||||
|
||||
if response.msg == "ok" {
|
||||
let thumbnail_url = Url::parse(&format!(
|
||||
"{}/pictrs/image/{}",
|
||||
context.settings().get_protocol_and_hostname(),
|
||||
response.files.first().expect("missing pictrs file").file
|
||||
))?;
|
||||
for uploaded_image in response.files {
|
||||
let form = LocalImageForm {
|
||||
// This is none because its an internal request.
|
||||
// IE, a local user shouldn't get to delete the thumbnails for their link posts
|
||||
local_user_id: None,
|
||||
pictrs_alias: uploaded_image.file.to_string(),
|
||||
pictrs_delete_token: uploaded_image.delete_token.to_string(),
|
||||
pictrs_alias: image.file.clone(),
|
||||
pictrs_delete_token: image.delete_token.clone(),
|
||||
};
|
||||
let protocol_and_hostname = context.settings().get_protocol_and_hostname();
|
||||
let thumbnail_url = image.thumbnail_url(&protocol_and_hostname)?;
|
||||
|
||||
LocalImage::create(&mut context.pool(), &form).await?;
|
||||
}
|
||||
|
||||
Ok(thumbnail_url)
|
||||
} else {
|
||||
Err(LemmyErrorType::PictrsResponseError(response.msg))?
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: get rid of this by reading content type from db
|
||||
|
@ -339,16 +338,19 @@ async fn is_image_content_type(client: &ClientWithMiddleware, url: &Url) -> Lemm
|
|||
}
|
||||
}
|
||||
|
||||
/// When adding a new avatar or similar image, delete the old one.
|
||||
/// When adding a new avatar, banner or similar image, delete the old one.
|
||||
pub async fn replace_image(
|
||||
new_image: &Option<String>,
|
||||
new_image: &Option<Option<DbUrl>>,
|
||||
old_image: &Option<DbUrl>,
|
||||
context: &Data<LemmyContext>,
|
||||
) -> LemmyResult<()> {
|
||||
if new_image.is_some() {
|
||||
if let (Some(Some(new_image)), Some(old_image)) = (new_image, old_image) {
|
||||
// Note: Oftentimes front ends will include the current image in the form.
|
||||
// In this case, deleting `old_image` would also be deletion of `new_image`,
|
||||
// so the deletion must be skipped for the image to be kept.
|
||||
if new_image != old_image {
|
||||
// Ignore errors because image may be stored externally.
|
||||
if let Some(avatar) = &old_image {
|
||||
let image = LocalImage::delete_by_url(&mut context.pool(), avatar)
|
||||
let image = LocalImage::delete_by_url(&mut context.pool(), old_image)
|
||||
.await
|
||||
.ok();
|
||||
if let Some(image) = image {
|
||||
|
|
|
@ -1,116 +0,0 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
#[cfg(feature = "full")]
|
||||
use ts_rs::TS;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, Default)]
|
||||
#[serde(transparent)]
|
||||
pub struct Sensitive<T>(T);
|
||||
|
||||
impl<T> Sensitive<T> {
|
||||
pub fn new(item: T) -> Self {
|
||||
Sensitive(item)
|
||||
}
|
||||
pub fn into_inner(self) -> T {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> std::fmt::Debug for Sensitive<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Sensitive").finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsRef<T> for Sensitive<T> {
|
||||
fn as_ref(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<str> for Sensitive<String> {
|
||||
fn as_ref(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Sensitive<String> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Sensitive<Vec<u8>> {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
self.0.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> AsMut<T> for Sensitive<T> {
|
||||
fn as_mut(&mut self) -> &mut T {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl AsMut<str> for Sensitive<String> {
|
||||
fn as_mut(&mut self) -> &mut str {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for Sensitive<String> {
|
||||
type Target = str;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for Sensitive<String> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<T> for Sensitive<T> {
|
||||
fn from(t: T) -> Self {
|
||||
Sensitive(t)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for Sensitive<String> {
|
||||
fn from(s: &str) -> Self {
|
||||
Sensitive(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Borrow<T> for Sensitive<T> {
|
||||
fn borrow(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Borrow<str> for Sensitive<String> {
|
||||
fn borrow(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "full")]
|
||||
impl TS for Sensitive<String> {
|
||||
fn name() -> String {
|
||||
"string".to_string()
|
||||
}
|
||||
fn name_with_type_args(_args: Vec<String>) -> String {
|
||||
"string".to_string()
|
||||
}
|
||||
fn dependencies() -> Vec<ts_rs::Dependency> {
|
||||
Vec::new()
|
||||
}
|
||||
fn transparent() -> bool {
|
||||
true
|
||||
}
|
||||
}
|
|
@ -375,7 +375,8 @@ impl From<FederationQueueState> for ReadableFederationState {
|
|||
pub struct InstanceWithFederationState {
|
||||
#[serde(flatten)]
|
||||
pub instance: Instance,
|
||||
/// if federation to this instance is or was active, show state of outgoing federation to this instance
|
||||
/// if federation to this instance is or was active, show state of outgoing federation to this
|
||||
/// instance
|
||||
pub federation_state: Option<ReadableFederationState>,
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use crate::{
|
|||
use chrono::{DateTime, Days, Local, TimeZone, Utc};
|
||||
use enum_map::{enum_map, EnumMap};
|
||||
use lemmy_db_schema::{
|
||||
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
|
||||
newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId},
|
||||
source::{
|
||||
comment::{Comment, CommentUpdateForm},
|
||||
|
@ -139,13 +140,7 @@ pub fn is_top_mod(
|
|||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn get_post(post_id: PostId, pool: &mut DbPool<'_>) -> LemmyResult<Post> {
|
||||
Post::read(pool, post_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost.into())
|
||||
}
|
||||
|
||||
/// Marks a post as read for a given person.
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn mark_post_as_read(
|
||||
person_id: PersonId,
|
||||
|
@ -158,6 +153,28 @@ pub async fn mark_post_as_read(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Updates the read comment count for a post. Usually done when reading or creating a new comment.
|
||||
#[tracing::instrument(skip_all)]
|
||||
pub async fn update_read_comments(
|
||||
person_id: PersonId,
|
||||
post_id: PostId,
|
||||
read_comments: i64,
|
||||
pool: &mut DbPool<'_>,
|
||||
) -> LemmyResult<()> {
|
||||
let person_post_agg_form = PersonPostAggregatesForm {
|
||||
person_id,
|
||||
post_id,
|
||||
read_comments,
|
||||
..PersonPostAggregatesForm::default()
|
||||
};
|
||||
|
||||
PersonPostAggregates::upsert(pool, &person_post_agg_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn check_user_valid(person: &Person) -> LemmyResult<()> {
|
||||
// Check for a site ban
|
||||
if person.banned {
|
||||
|
@ -356,7 +373,8 @@ pub async fn build_federated_instances(
|
|||
federation_state: federation_state.map(std::convert::Into::into),
|
||||
};
|
||||
if is_blocked {
|
||||
// blocked instances will only have an entry here if they had been federated with in the past.
|
||||
// blocked instances will only have an entry here if they had been federated with in the
|
||||
// past.
|
||||
blocked.push(i);
|
||||
} else if is_allowed {
|
||||
allowed.push(i.clone());
|
||||
|
@ -440,7 +458,7 @@ pub async fn send_password_reset_email(
|
|||
// Insert the row after successful send, to avoid using daily reset limit while
|
||||
// email sending is broken.
|
||||
let local_user_id = user.local_user.id;
|
||||
PasswordResetRequest::create_token(pool, local_user_id, token.clone()).await?;
|
||||
PasswordResetRequest::create(pool, local_user_id, token.clone()).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -536,25 +554,8 @@ pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet>
|
|||
.try_get_with::<_, LemmyError>((), async {
|
||||
let urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
|
||||
|
||||
let regexes = urls.iter().map(|url| {
|
||||
let url = &url.url;
|
||||
let parsed = Url::parse(url).expect("Coundln't parse URL.");
|
||||
if url.ends_with('/') {
|
||||
format!(
|
||||
"({}://)?{}{}?",
|
||||
parsed.scheme(),
|
||||
escape(parsed.domain().expect("No domain.")),
|
||||
escape(parsed.path())
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"({}://)?{}{}",
|
||||
parsed.scheme(),
|
||||
escape(parsed.domain().expect("No domain.")),
|
||||
escape(parsed.path())
|
||||
)
|
||||
}
|
||||
});
|
||||
// The urls are already validated on saving, so just escape them.
|
||||
let regexes = urls.iter().map(|url| escape(&url.url));
|
||||
|
||||
let set = RegexSet::new(regexes)?;
|
||||
Ok(set)
|
||||
|
@ -971,8 +972,8 @@ pub async fn process_markdown_opt(
|
|||
|
||||
/// A wrapper for `proxy_image_link` for use in tests.
|
||||
///
|
||||
/// The parameter `force_image_proxy` is the config value of `pictrs.image_proxy`. Its necessary to pass
|
||||
/// as separate parameter so it can be changed in tests.
|
||||
/// The parameter `force_image_proxy` is the config value of `pictrs.image_proxy`. Its necessary to
|
||||
/// pass as separate parameter so it can be changed in tests.
|
||||
async fn proxy_image_link_internal(
|
||||
link: Url,
|
||||
image_mode: PictrsImageMode,
|
||||
|
@ -982,13 +983,10 @@ async fn proxy_image_link_internal(
|
|||
if link.domain() == Some(&context.settings().hostname) {
|
||||
Ok(link.into())
|
||||
} else if image_mode == PictrsImageMode::ProxyAllImages {
|
||||
let proxied = format!(
|
||||
"{}/api/v3/image_proxy?url={}",
|
||||
context.settings().get_protocol_and_hostname(),
|
||||
encode(link.as_str())
|
||||
);
|
||||
let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
|
||||
|
||||
RemoteImage::create(&mut context.pool(), vec![link]).await?;
|
||||
Ok(Url::parse(&proxied)?.into())
|
||||
Ok(proxied.into())
|
||||
} else {
|
||||
Ok(link.into())
|
||||
}
|
||||
|
@ -1006,26 +1004,25 @@ pub(crate) async fn proxy_image_link(link: Url, context: &LemmyContext) -> Lemmy
|
|||
}
|
||||
|
||||
pub async fn proxy_image_link_opt_api(
|
||||
link: &Option<String>,
|
||||
link: Option<Option<DbUrl>>,
|
||||
context: &LemmyContext,
|
||||
) -> LemmyResult<Option<Option<DbUrl>>> {
|
||||
proxy_image_link_api(link, context).await.map(Some)
|
||||
if let Some(Some(link)) = link {
|
||||
proxy_image_link(link.into(), context)
|
||||
.await
|
||||
.map(Some)
|
||||
.map(Some)
|
||||
} else {
|
||||
Ok(link)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn proxy_image_link_api(
|
||||
link: &Option<String>,
|
||||
link: Option<DbUrl>,
|
||||
context: &LemmyContext,
|
||||
) -> LemmyResult<Option<DbUrl>> {
|
||||
let link: Option<DbUrl> = match link.as_ref().map(String::as_str) {
|
||||
// An empty string is an erase
|
||||
Some("") => None,
|
||||
Some(str_url) => Url::parse(str_url)
|
||||
.map(|u| Some(u.into()))
|
||||
.with_lemmy_type(LemmyErrorType::InvalidUrl)?,
|
||||
None => None,
|
||||
};
|
||||
if let Some(l) = link {
|
||||
proxy_image_link(l.into(), context).await.map(Some)
|
||||
if let Some(link) = link {
|
||||
proxy_image_link(link.into(), context).await.map(Some)
|
||||
} else {
|
||||
Ok(link)
|
||||
}
|
||||
|
@ -1042,6 +1039,17 @@ pub async fn proxy_image_link_opt_apub(
|
|||
}
|
||||
}
|
||||
|
||||
fn build_proxied_image_url(
|
||||
link: &Url,
|
||||
protocol_and_hostname: &str,
|
||||
) -> Result<Url, url::ParseError> {
|
||||
Url::parse(&format!(
|
||||
"{}/api/v3/image_proxy?url={}",
|
||||
protocol_and_hostname,
|
||||
encode(link.as_str())
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::unwrap_used)]
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
|
@ -1121,29 +1129,4 @@ mod tests {
|
|||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_diesel_option_overwrite_to_url() {
|
||||
let context = LemmyContext::init_test_context().await;
|
||||
|
||||
assert!(matches!(
|
||||
proxy_image_link_api(&None, &context).await,
|
||||
Ok(None)
|
||||
));
|
||||
assert!(matches!(
|
||||
proxy_image_link_opt_api(&Some(String::new()), &context).await,
|
||||
Ok(Some(None))
|
||||
));
|
||||
assert!(
|
||||
proxy_image_link_opt_api(&Some("invalid_url".to_string()), &context)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
let example_url = "https://lemmy-alpha/image.png";
|
||||
assert!(matches!(
|
||||
proxy_image_link_opt_api(&Some(example_url.to_string()), &context).await,
|
||||
Ok(Some(Some(url))) if url == Url::parse(example_url).unwrap().into()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,11 +9,11 @@ use lemmy_api_common::{
|
|||
check_community_user_action,
|
||||
check_post_deleted_or_removed,
|
||||
generate_local_apub_endpoint,
|
||||
get_post,
|
||||
get_url_blocklist,
|
||||
is_mod_or_admin,
|
||||
local_site_to_slur_regex,
|
||||
process_markdown,
|
||||
update_read_comments,
|
||||
EndpointType,
|
||||
},
|
||||
};
|
||||
|
@ -28,7 +28,7 @@ use lemmy_db_schema::{
|
|||
},
|
||||
traits::{Crud, Likeable},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_db_views::structs::{LocalUserView, PostView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field},
|
||||
|
@ -47,12 +47,23 @@ pub async fn create_comment(
|
|||
let slur_regex = local_site_to_slur_regex(&local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?;
|
||||
is_valid_body_field(&Some(content.clone()), false)?;
|
||||
is_valid_body_field(&content, false)?;
|
||||
|
||||
// Check for a community ban
|
||||
let post_id = data.post_id;
|
||||
let post = get_post(post_id, &mut context.pool()).await?;
|
||||
let community_id = post.community_id;
|
||||
|
||||
// Read the full post view in order to get the comments count.
|
||||
let post_view = PostView::read(
|
||||
&mut context.pool(),
|
||||
post_id,
|
||||
Some(local_user_view.person.id),
|
||||
true,
|
||||
)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
let post = post_view.post;
|
||||
let community_id = post_view.community.id;
|
||||
|
||||
check_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?;
|
||||
check_post_deleted_or_removed(&post)?;
|
||||
|
@ -164,6 +175,15 @@ pub async fn create_comment(
|
|||
)
|
||||
.await?;
|
||||
|
||||
// Update the read comments, so your own new comment doesn't appear as a +1 unread
|
||||
update_read_comments(
|
||||
local_user_view.person.id,
|
||||
post_id,
|
||||
post_view.counts.comments + 1,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// If we're responding to a comment where we're the recipient,
|
||||
// (ie we're the grandparent, or the recipient of the parent comment_reply),
|
||||
// then mark the parent as read.
|
||||
|
|
|
@ -37,6 +37,12 @@ pub async fn remove_comment(
|
|||
)
|
||||
.await?;
|
||||
|
||||
// Don't allow removing or restoring comment which was deleted by user, as it would reveal
|
||||
// the comment text in mod log.
|
||||
if orig_comment.comment.deleted {
|
||||
return Err(LemmyErrorType::CouldntUpdateComment.into());
|
||||
}
|
||||
|
||||
// Do the remove
|
||||
let removed = data.removed;
|
||||
let updated_comment = Comment::update(
|
||||
|
|
|
@ -63,7 +63,9 @@ pub async fn update_comment(
|
|||
let slur_regex = local_site_to_slur_regex(&local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
let content = process_markdown_opt(&data.content, &slur_regex, &url_blocklist, &context).await?;
|
||||
is_valid_body_field(&content, false)?;
|
||||
if let Some(content) = &content {
|
||||
is_valid_body_field(content, false)?;
|
||||
}
|
||||
|
||||
let comment_id = data.comment_id;
|
||||
let form = CommentUpdateForm {
|
||||
|
|
|
@ -30,6 +30,7 @@ use lemmy_db_schema::{
|
|||
},
|
||||
},
|
||||
traits::{ApubActor, Crud, Followable, Joinable},
|
||||
utils::diesel_url_create,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::{
|
||||
|
@ -61,11 +62,18 @@ pub async fn create_community(
|
|||
check_slurs(&data.title, &slur_regex)?;
|
||||
let description =
|
||||
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?;
|
||||
let icon = proxy_image_link_api(&data.icon, &context).await?;
|
||||
let banner = proxy_image_link_api(&data.banner, &context).await?;
|
||||
|
||||
let icon = diesel_url_create(data.icon.as_deref())?;
|
||||
let icon = proxy_image_link_api(icon, &context).await?;
|
||||
|
||||
let banner = diesel_url_create(data.banner.as_deref())?;
|
||||
let banner = proxy_image_link_api(banner, &context).await?;
|
||||
|
||||
is_valid_actor_name(&data.name, local_site.actor_name_max_length as usize)?;
|
||||
is_valid_body_field(&data.description, false)?;
|
||||
|
||||
if let Some(desc) = &data.description {
|
||||
is_valid_body_field(desc, false)?;
|
||||
}
|
||||
|
||||
// Double check for duplicate community actor_ids
|
||||
let community_actor_id = generate_local_apub_endpoint(
|
||||
|
|
|
@ -21,7 +21,7 @@ use lemmy_db_schema::{
|
|||
local_site::LocalSite,
|
||||
},
|
||||
traits::Crud,
|
||||
utils::{diesel_option_overwrite, naive_now},
|
||||
utils::{diesel_string_update, diesel_url_update, naive_now},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::{
|
||||
|
@ -40,18 +40,28 @@ pub async fn update_community(
|
|||
let slur_regex = local_site_to_slur_regex(&local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
check_slurs_opt(&data.title, &slur_regex)?;
|
||||
let description =
|
||||
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?;
|
||||
is_valid_body_field(&data.description, false)?;
|
||||
|
||||
let description = diesel_string_update(
|
||||
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context)
|
||||
.await?
|
||||
.as_deref(),
|
||||
);
|
||||
|
||||
if let Some(Some(desc)) = &description {
|
||||
is_valid_body_field(desc, false)?;
|
||||
}
|
||||
|
||||
let old_community = Community::read(&mut context.pool(), data.community_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
replace_image(&data.icon, &old_community.icon, &context).await?;
|
||||
replace_image(&data.banner, &old_community.banner, &context).await?;
|
||||
|
||||
let description = diesel_option_overwrite(description);
|
||||
let icon = proxy_image_link_opt_api(&data.icon, &context).await?;
|
||||
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;
|
||||
let icon = diesel_url_update(data.icon.as_deref())?;
|
||||
replace_image(&icon, &old_community.icon, &context).await?;
|
||||
let icon = proxy_image_link_opt_api(icon, &context).await?;
|
||||
|
||||
let banner = diesel_url_update(data.banner.as_deref())?;
|
||||
replace_image(&banner, &old_community.banner, &context).await?;
|
||||
let banner = proxy_image_link_opt_api(banner, &context).await?;
|
||||
|
||||
// Verify its a mod (only mods can edit it)
|
||||
check_community_mod_action(
|
||||
|
|
|
@ -14,7 +14,6 @@ use lemmy_api_common::{
|
|||
local_site_to_slur_regex,
|
||||
mark_post_as_read,
|
||||
process_markdown_opt,
|
||||
proxy_image_link_opt_apub,
|
||||
EndpointType,
|
||||
},
|
||||
};
|
||||
|
@ -27,6 +26,7 @@ use lemmy_db_schema::{
|
|||
post::{Post, PostInsertForm, PostLike, PostLikeForm, PostUpdateForm},
|
||||
},
|
||||
traits::{Crud, Likeable},
|
||||
utils::diesel_url_create,
|
||||
CommunityVisibility,
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
|
@ -38,7 +38,6 @@ use lemmy_utils::{
|
|||
slurs::check_slurs,
|
||||
validation::{
|
||||
check_url_scheme,
|
||||
clean_url_params,
|
||||
is_url_blocked,
|
||||
is_valid_alt_text_field,
|
||||
is_valid_body_field,
|
||||
|
@ -65,17 +64,27 @@ pub async fn create_post(
|
|||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
|
||||
let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?;
|
||||
let data_url = data.url.as_ref();
|
||||
let url = data_url.map(clean_url_params); // TODO no good way to handle a "clear"
|
||||
let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params);
|
||||
let url = diesel_url_create(data.url.as_deref())?;
|
||||
let custom_thumbnail = diesel_url_create(data.custom_thumbnail.as_deref())?;
|
||||
|
||||
is_valid_post_title(&data.name)?;
|
||||
is_valid_body_field(&body, true)?;
|
||||
is_valid_alt_text_field(&data.alt_text)?;
|
||||
is_url_blocked(&url, &url_blocklist)?;
|
||||
check_url_scheme(&url)?;
|
||||
check_url_scheme(&custom_thumbnail)?;
|
||||
let url = proxy_image_link_opt_apub(url, &context).await?;
|
||||
|
||||
if let Some(url) = &url {
|
||||
is_url_blocked(url, &url_blocklist)?;
|
||||
check_url_scheme(url)?;
|
||||
}
|
||||
|
||||
if let Some(custom_thumbnail) = &custom_thumbnail {
|
||||
check_url_scheme(custom_thumbnail)?;
|
||||
}
|
||||
|
||||
if let Some(alt_text) = &data.alt_text {
|
||||
is_valid_alt_text_field(alt_text)?;
|
||||
}
|
||||
|
||||
if let Some(body) = &body {
|
||||
is_valid_body_field(body, true)?;
|
||||
}
|
||||
|
||||
check_community_user_action(
|
||||
&local_user_view.person,
|
||||
|
@ -125,7 +134,7 @@ pub async fn create_post(
|
|||
|
||||
let post_form = PostInsertForm::builder()
|
||||
.name(data.name.trim().to_string())
|
||||
.url(url)
|
||||
.url(url.map(Into::into))
|
||||
.body(body)
|
||||
.alt_text(data.alt_text.clone())
|
||||
.community_id(data.community_id)
|
||||
|
@ -158,12 +167,12 @@ pub async fn create_post(
|
|||
|
||||
generate_post_link_metadata(
|
||||
updated_post.clone(),
|
||||
custom_thumbnail,
|
||||
None,
|
||||
custom_thumbnail.map(Into::into),
|
||||
|post| Some(SendActivityData::CreatePost(post)),
|
||||
Some(local_site),
|
||||
context.reset_request_count(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
|
||||
// They like their own post by default
|
||||
let person_id = local_user_view.person.id;
|
||||
|
@ -178,7 +187,6 @@ pub async fn create_post(
|
|||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
|
||||
|
||||
// Mark the post as read
|
||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||
|
||||
if let Some(url) = updated_post.url.clone() {
|
||||
|
|
|
@ -2,10 +2,9 @@ use actix_web::web::{Data, Json, Query};
|
|||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
post::{GetPost, GetPostResponse},
|
||||
utils::{check_private_instance, is_mod_or_admin_opt, mark_post_as_read},
|
||||
utils::{check_private_instance, is_mod_or_admin_opt, mark_post_as_read, update_read_comments},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
|
||||
source::{comment::Comment, post::Post},
|
||||
traits::Crud,
|
||||
};
|
||||
|
@ -14,7 +13,7 @@ use lemmy_db_views::{
|
|||
structs::{LocalUserView, PostView, SiteView},
|
||||
};
|
||||
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView};
|
||||
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
|
||||
#[tracing::instrument(skip(context))]
|
||||
pub async fn get_post(
|
||||
|
@ -60,10 +59,17 @@ pub async fn get_post(
|
|||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPost)?;
|
||||
|
||||
// Mark the post as read
|
||||
let post_id = post_view.post.id;
|
||||
if let Some(person_id) = person_id {
|
||||
mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
|
||||
|
||||
update_read_comments(
|
||||
person_id,
|
||||
post_id,
|
||||
post_view.counts.comments,
|
||||
&mut context.pool(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
// Necessary for the sidebar subscribed
|
||||
|
@ -76,21 +82,6 @@ pub async fn get_post(
|
|||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
|
||||
|
||||
// Insert into PersonPostAggregates
|
||||
// to update the read_comments count
|
||||
if let Some(person_id) = person_id {
|
||||
let read_comments = post_view.counts.comments;
|
||||
let person_post_agg_form = PersonPostAggregatesForm {
|
||||
person_id,
|
||||
post_id,
|
||||
read_comments,
|
||||
..PersonPostAggregatesForm::default()
|
||||
};
|
||||
PersonPostAggregates::upsert(&mut context.pool(), &person_post_agg_form)
|
||||
.await
|
||||
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?;
|
||||
}
|
||||
|
||||
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;
|
||||
|
||||
// Fetch the cross_posts
|
||||
|
|
|
@ -11,7 +11,6 @@ use lemmy_api_common::{
|
|||
get_url_blocklist,
|
||||
local_site_to_slur_regex,
|
||||
process_markdown_opt,
|
||||
proxy_image_link_opt_apub,
|
||||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
|
@ -21,16 +20,15 @@ use lemmy_db_schema::{
|
|||
post::{Post, PostUpdateForm},
|
||||
},
|
||||
traits::Crud,
|
||||
utils::{diesel_option_overwrite, naive_now},
|
||||
utils::{diesel_string_update, diesel_url_update, naive_now},
|
||||
};
|
||||
use lemmy_db_views::structs::LocalUserView;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||
utils::{
|
||||
slurs::check_slurs_opt,
|
||||
slurs::check_slurs,
|
||||
validation::{
|
||||
check_url_scheme,
|
||||
clean_url_params,
|
||||
is_url_blocked,
|
||||
is_valid_alt_text_field,
|
||||
is_valid_body_field,
|
||||
|
@ -48,26 +46,43 @@ pub async fn update_post(
|
|||
) -> LemmyResult<Json<PostResponse>> {
|
||||
let local_site = LocalSite::read(&mut context.pool()).await?;
|
||||
|
||||
// TODO No good way to handle a clear.
|
||||
// Issue link: https://github.com/LemmyNet/lemmy/issues/2287
|
||||
let url = data.url.as_ref().map(clean_url_params);
|
||||
let custom_thumbnail = data.custom_thumbnail.as_ref().map(clean_url_params);
|
||||
let url = diesel_url_update(data.url.as_deref())?;
|
||||
|
||||
let custom_thumbnail = diesel_url_update(data.custom_thumbnail.as_deref())?;
|
||||
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
|
||||
let slur_regex = local_site_to_slur_regex(&local_site);
|
||||
check_slurs_opt(&data.name, &slur_regex)?;
|
||||
let body = process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context).await?;
|
||||
|
||||
let body = diesel_string_update(
|
||||
process_markdown_opt(&data.body, &slur_regex, &url_blocklist, &context)
|
||||
.await?
|
||||
.as_deref(),
|
||||
);
|
||||
|
||||
let alt_text = diesel_string_update(data.alt_text.as_deref());
|
||||
|
||||
if let Some(name) = &data.name {
|
||||
is_valid_post_title(name)?;
|
||||
check_slurs(name, &slur_regex)?;
|
||||
}
|
||||
|
||||
is_valid_body_field(&body, true)?;
|
||||
is_valid_alt_text_field(&data.alt_text)?;
|
||||
is_url_blocked(&url, &url_blocklist)?;
|
||||
check_url_scheme(&url)?;
|
||||
check_url_scheme(&custom_thumbnail)?;
|
||||
if let Some(Some(body)) = &body {
|
||||
is_valid_body_field(body, true)?;
|
||||
}
|
||||
|
||||
if let Some(Some(alt_text)) = &alt_text {
|
||||
is_valid_alt_text_field(alt_text)?;
|
||||
}
|
||||
|
||||
if let Some(Some(url)) = &url {
|
||||
is_url_blocked(url, &url_blocklist)?;
|
||||
check_url_scheme(url)?;
|
||||
}
|
||||
|
||||
if let Some(Some(custom_thumbnail)) = &custom_thumbnail {
|
||||
check_url_scheme(custom_thumbnail)?;
|
||||
}
|
||||
|
||||
let post_id = data.post_id;
|
||||
let orig_post = Post::read(&mut context.pool(), post_id)
|
||||
|
@ -86,11 +101,6 @@ pub async fn update_post(
|
|||
Err(LemmyErrorType::NoPostEditAllowed)?
|
||||
}
|
||||
|
||||
let url = match url {
|
||||
Some(url) => Some(proxy_image_link_opt_apub(Some(url), &context).await?),
|
||||
_ => Default::default(),
|
||||
};
|
||||
|
||||
let language_id = data.language_id;
|
||||
CommunityLanguage::is_allowed_community_language(
|
||||
&mut context.pool(),
|
||||
|
@ -102,8 +112,8 @@ pub async fn update_post(
|
|||
let post_form = PostUpdateForm {
|
||||
name: data.name.clone(),
|
||||
url,
|
||||
body: diesel_option_overwrite(body),
|
||||
alt_text: diesel_option_overwrite(data.alt_text.clone()),
|
||||
body,
|
||||
alt_text,
|
||||
nsfw: data.nsfw,
|
||||
language_id: data.language_id,
|
||||
updated: Some(Some(naive_now())),
|
||||
|
@ -117,12 +127,12 @@ pub async fn update_post(
|
|||
|
||||
generate_post_link_metadata(
|
||||
updated_post.clone(),
|
||||
custom_thumbnail,
|
||||
None,
|
||||
custom_thumbnail.flatten().map(Into::into),
|
||||
|post| Some(SendActivityData::UpdatePost(post)),
|
||||
Some(local_site),
|
||||
context.reset_request_count(),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
|
||||
build_post_response(
|
||||
context.deref(),
|
||||
|
|
|
@ -39,7 +39,7 @@ pub async fn create_private_message(
|
|||
let slur_regex = local_site_to_slur_regex(&local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?;
|
||||
is_valid_body_field(&Some(content.clone()), false)?;
|
||||
is_valid_body_field(&content, false)?;
|
||||
|
||||
check_person_block(
|
||||
local_user_view.person.id,
|
||||
|
|
|
@ -41,7 +41,7 @@ pub async fn update_private_message(
|
|||
let slur_regex = local_site_to_slur_regex(&local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
let content = process_markdown(&data.content, &slur_regex, &url_blocklist, &context).await?;
|
||||
is_valid_body_field(&Some(content.clone()), false)?;
|
||||
is_valid_body_field(&content, false)?;
|
||||
|
||||
let private_message_id = data.private_message_id;
|
||||
PrivateMessage::update(
|
||||
|
|
|
@ -11,7 +11,7 @@ use lemmy_api_common::{
|
|||
local_site_rate_limit_to_rate_limit_config,
|
||||
local_site_to_slur_regex,
|
||||
process_markdown_opt,
|
||||
proxy_image_link_opt_api,
|
||||
proxy_image_link_api,
|
||||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
|
@ -23,7 +23,7 @@ use lemmy_db_schema::{
|
|||
tagline::Tagline,
|
||||
},
|
||||
traits::Crud,
|
||||
utils::{diesel_option_overwrite, naive_now},
|
||||
utils::{diesel_string_update, diesel_url_create, naive_now},
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
use lemmy_utils::{
|
||||
|
@ -61,21 +61,25 @@ pub async fn create_site(
|
|||
let slur_regex = local_site_to_slur_regex(&local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?;
|
||||
let icon = proxy_image_link_opt_api(&data.icon, &context).await?;
|
||||
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;
|
||||
|
||||
let icon = diesel_url_create(data.icon.as_deref())?;
|
||||
let icon = proxy_image_link_api(icon, &context).await?;
|
||||
|
||||
let banner = diesel_url_create(data.banner.as_deref())?;
|
||||
let banner = proxy_image_link_api(banner, &context).await?;
|
||||
|
||||
let site_form = SiteUpdateForm {
|
||||
name: Some(data.name.clone()),
|
||||
sidebar: diesel_option_overwrite(sidebar),
|
||||
description: diesel_option_overwrite(data.description.clone()),
|
||||
icon,
|
||||
banner,
|
||||
sidebar: diesel_string_update(sidebar.as_deref()),
|
||||
description: diesel_string_update(data.description.as_deref()),
|
||||
icon: Some(icon),
|
||||
banner: Some(banner),
|
||||
actor_id: Some(actor_id),
|
||||
last_refreshed_at: Some(naive_now()),
|
||||
inbox_url,
|
||||
private_key: Some(Some(keypair.private_key)),
|
||||
public_key: Some(keypair.public_key),
|
||||
content_warning: diesel_option_overwrite(data.content_warning.clone()),
|
||||
content_warning: diesel_string_update(data.content_warning.as_deref()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
|
@ -91,16 +95,16 @@ pub async fn create_site(
|
|||
enable_nsfw: data.enable_nsfw,
|
||||
community_creation_admin_only: data.community_creation_admin_only,
|
||||
require_email_verification: data.require_email_verification,
|
||||
application_question: diesel_option_overwrite(data.application_question.clone()),
|
||||
application_question: diesel_string_update(data.application_question.as_deref()),
|
||||
private_instance: data.private_instance,
|
||||
default_theme: data.default_theme.clone(),
|
||||
default_post_listing_type: data.default_post_listing_type,
|
||||
default_sort_type: data.default_sort_type,
|
||||
legal_information: diesel_option_overwrite(data.legal_information.clone()),
|
||||
legal_information: diesel_string_update(data.legal_information.as_deref()),
|
||||
application_email_admins: data.application_email_admins,
|
||||
hide_modlog_mod_names: data.hide_modlog_mod_names,
|
||||
updated: Some(Some(naive_now())),
|
||||
slur_filter_regex: diesel_option_overwrite(data.slur_filter_regex.clone()),
|
||||
slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()),
|
||||
actor_name_max_length: data.actor_name_max_length,
|
||||
federation_enabled: data.federation_enabled,
|
||||
captcha_enabled: data.captcha_enabled,
|
||||
|
@ -179,7 +183,9 @@ fn validate_create_payload(local_site: &LocalSite, create_site: &CreateSite) ->
|
|||
)?;
|
||||
|
||||
// Ensure that the sidebar has fewer than the max num characters...
|
||||
is_valid_body_field(&create_site.sidebar, false)?;
|
||||
if let Some(body) = &create_site.sidebar {
|
||||
is_valid_body_field(body, false)?;
|
||||
}
|
||||
|
||||
application_question_check(
|
||||
&local_site.application_question,
|
||||
|
|
|
@ -27,7 +27,7 @@ use lemmy_db_schema::{
|
|||
tagline::Tagline,
|
||||
},
|
||||
traits::Crud,
|
||||
utils::{diesel_option_overwrite, naive_now},
|
||||
utils::{diesel_string_update, diesel_url_update, naive_now},
|
||||
RegistrationMode,
|
||||
};
|
||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||
|
@ -67,22 +67,29 @@ pub async fn update_site(
|
|||
SiteLanguage::update(&mut context.pool(), discussion_languages.clone(), &site).await?;
|
||||
}
|
||||
|
||||
replace_image(&data.icon, &site.icon, &context).await?;
|
||||
replace_image(&data.banner, &site.banner, &context).await?;
|
||||
|
||||
let slur_regex = local_site_to_slur_regex(&local_site);
|
||||
let url_blocklist = get_url_blocklist(&context).await?;
|
||||
let sidebar = process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context).await?;
|
||||
let icon = proxy_image_link_opt_api(&data.icon, &context).await?;
|
||||
let banner = proxy_image_link_opt_api(&data.banner, &context).await?;
|
||||
let sidebar = diesel_string_update(
|
||||
process_markdown_opt(&data.sidebar, &slur_regex, &url_blocklist, &context)
|
||||
.await?
|
||||
.as_deref(),
|
||||
);
|
||||
|
||||
let icon = diesel_url_update(data.icon.as_deref())?;
|
||||
replace_image(&icon, &site.icon, &context).await?;
|
||||
let icon = proxy_image_link_opt_api(icon, &context).await?;
|
||||
|
||||
let banner = diesel_url_update(data.banner.as_deref())?;
|
||||
replace_image(&banner, &site.banner, &context).await?;
|
||||
let banner = proxy_image_link_opt_api(banner, &context).await?;
|
||||
|
||||
let site_form = SiteUpdateForm {
|
||||
name: data.name.clone(),
|
||||
sidebar: diesel_option_overwrite(sidebar),
|
||||
description: diesel_option_overwrite(data.description.clone()),
|
||||
sidebar,
|
||||
description: diesel_string_update(data.description.as_deref()),
|
||||
icon,
|
||||
banner,
|
||||
content_warning: diesel_option_overwrite(data.content_warning.clone()),
|
||||
content_warning: diesel_string_update(data.content_warning.as_deref()),
|
||||
updated: Some(Some(naive_now())),
|
||||
..Default::default()
|
||||
};
|
||||
|
@ -99,16 +106,16 @@ pub async fn update_site(
|
|||
enable_nsfw: data.enable_nsfw,
|
||||
community_creation_admin_only: data.community_creation_admin_only,
|
||||
require_email_verification: data.require_email_verification,
|
||||
application_question: diesel_option_overwrite(data.application_question.clone()),
|
||||
application_question: diesel_string_update(data.application_question.as_deref()),
|
||||
private_instance: data.private_instance,
|
||||
default_theme: data.default_theme.clone(),
|
||||
default_post_listing_type: data.default_post_listing_type,
|
||||
default_sort_type: data.default_sort_type,
|
||||
legal_information: diesel_option_overwrite(data.legal_information.clone()),
|
||||
legal_information: diesel_string_update(data.legal_information.as_deref()),
|
||||
application_email_admins: data.application_email_admins,
|
||||
hide_modlog_mod_names: data.hide_modlog_mod_names,
|
||||
updated: Some(Some(naive_now())),
|
||||
slur_filter_regex: diesel_option_overwrite(data.slur_filter_regex.clone()),
|
||||
slur_filter_regex: diesel_string_update(data.slur_filter_regex.as_deref()),
|
||||
actor_name_max_length: data.actor_name_max_length,
|
||||
federation_enabled: data.federation_enabled,
|
||||
captcha_enabled: data.captcha_enabled,
|
||||
|
@ -156,7 +163,8 @@ pub async fn update_site(
|
|||
// TODO can't think of a better way to do this.
|
||||
// If the server suddenly requires email verification, or required applications, no old users
|
||||
// will be able to log in. It really only wants this to be a requirement for NEW signups.
|
||||
// So if it was set from false, to true, you need to update all current users columns to be verified.
|
||||
// So if it was set from false, to true, you need to update all current users columns to be
|
||||
// verified.
|
||||
|
||||
let old_require_application =
|
||||
local_site.registration_mode == RegistrationMode::RequireApplication;
|
||||
|
@ -228,7 +236,9 @@ fn validate_update_payload(local_site: &LocalSite, edit_site: &EditSite) -> Lemm
|
|||
)?;
|
||||
|
||||
// Ensure that the sidebar has fewer than the max num characters...
|
||||
is_valid_body_field(&edit_site.sidebar, false)?;
|
||||
if let Some(body) = &edit_site.sidebar {
|
||||
is_valid_body_field(body, false)?;
|
||||
}
|
||||
|
||||
application_question_check(
|
||||
&local_site.application_question,
|
||||
|
|
|
@ -112,15 +112,17 @@ pub async fn register(
|
|||
// We have to create both a person, and local_user
|
||||
|
||||
// Register the new person
|
||||
let person_form = PersonInsertForm::builder()
|
||||
.name(data.username.clone())
|
||||
.actor_id(Some(actor_id.clone()))
|
||||
.private_key(Some(actor_keypair.private_key))
|
||||
.public_key(actor_keypair.public_key)
|
||||
.inbox_url(Some(generate_inbox_url(&actor_id)?))
|
||||
.shared_inbox_url(Some(generate_shared_inbox_url(context.settings())?))
|
||||
.instance_id(site_view.site.instance_id)
|
||||
.build();
|
||||
let person_form = PersonInsertForm {
|
||||
actor_id: Some(actor_id.clone()),
|
||||
inbox_url: Some(generate_inbox_url(&actor_id)?),
|
||||
shared_inbox_url: Some(generate_shared_inbox_url(context.settings())?),
|
||||
private_key: Some(actor_keypair.private_key),
|
||||
..PersonInsertForm::new(
|
||||
data.username.clone(),
|
||||
actor_keypair.public_key,
|
||||
site_view.site.instance_id,
|
||||
)
|
||||
};
|
||||
|
||||
// insert the person
|
||||
let inserted_person = Person::create(&mut context.pool(), &person_form)
|
||||
|
@ -142,12 +144,17 @@ pub async fn register(
|
|||
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string())
|
||||
.collect();
|
||||
|
||||
// Show nsfw content if param is true, or if content_warning exists
|
||||
let show_nsfw = data
|
||||
.show_nsfw
|
||||
.unwrap_or(site_view.site.content_warning.is_some());
|
||||
|
||||
// Create the local user
|
||||
let local_user_form = LocalUserInsertForm::builder()
|
||||
.person_id(inserted_person.id)
|
||||
.email(data.email.as_deref().map(str::to_lowercase))
|
||||
.password_encrypted(data.password.to_string())
|
||||
.show_nsfw(Some(data.show_nsfw))
|
||||
.show_nsfw(Some(show_nsfw))
|
||||
.accepted_application(accepted_application)
|
||||
.default_listing_type(Some(local_site.default_post_listing_type))
|
||||
.post_listing_mode(Some(local_site.default_post_listing_mode))
|
||||
|
@ -192,7 +199,8 @@ pub async fn register(
|
|||
verify_email_sent: false,
|
||||
};
|
||||
|
||||
// Log the user in directly if the site is not setup, or email verification and application aren't required
|
||||
// Log the user in directly if the site is not setup, or email verification and application aren't
|
||||
// required
|
||||
if !local_site.site_setup
|
||||
|| (!require_registration_application && !local_site.require_email_verification)
|
||||
{
|
||||
|
|
|
@ -44,7 +44,7 @@ once_cell = { workspace = true }
|
|||
moka.workspace = true
|
||||
serde_with.workspace = true
|
||||
html2md = "0.2.14"
|
||||
html2text = "0.6.0"
|
||||
html2text = "0.12.5"
|
||||
stringreader = "0.1.1"
|
||||
enum_delegate = "0.2.0"
|
||||
|
||||
|
|
22
crates/apub/assets/discourse/objects/group.json
Normal file
22
crates/apub/assets/discourse/objects/group.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"id": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146",
|
||||
"type": "Group",
|
||||
"updated": "2024-04-05T12:49:51Z",
|
||||
"url": "https://socialhub.activitypub.rocks/c/meeting/threadiverse-wg/88",
|
||||
"name": "Threadiverse Working Group (SocialHub)",
|
||||
"inbox": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146/inbox",
|
||||
"outbox": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146/outbox",
|
||||
"followers": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146/followers",
|
||||
"preferredUsername": "threadiverse-wg",
|
||||
"publicKey": {
|
||||
"id": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146#main-key",
|
||||
"owner": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApJi4iAcW6bPiHVCxT9p0\n8DVnrDDO4QtLNy7bpRFdMFifmmmXprsuAi9D2MSwbhH49V54HtIkxBpKd2IR/UD8\nmhMDY4CNI9FHpjqLw0wtkzxcqF9urSqhn0/vWX+9oxyhIgQS5KMiIkYDMJiAc691\niEcZ8LCran23xIGl6Dk54Nr3TqTMLcjDhzQYUJbxMrLq5/knWqOKG3IF5OxK+9ZZ\n1wxDF872eJTxJLkmpag+WYNtHzvB2SGTp8j5IF1/pZ9J1c3cpYfaeolTch/B/GQn\najCB4l27U52rIIObxJqFXSY8wHyd0aAmNmxzPZ7cduRlBDhmI40cAmnCV1YQPvpk\nDwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/png",
|
||||
"url": "https://socialhub.activitypub.rocks/uploads/default/original/1X/8faac84234dc73d074dadaa2bcf24dc746b8647f.png"
|
||||
},
|
||||
"@context": "https://www.w3.org/ns/activitystreams"
|
||||
}
|
13
crates/apub/assets/discourse/objects/page.json
Normal file
13
crates/apub/assets/discourse/objects/page.json
Normal file
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"id": "https://socialhub.activitypub.rocks/ap/object/1899f65c062200daec50a4c89ed76dc9",
|
||||
"type": "Note",
|
||||
"audience": "https://socialhub.activitypub.rocks/ap/actor/797217cf18c0e819dfafc52425590146",
|
||||
"published": "2024-04-13T14:36:19Z",
|
||||
"updated": "2024-04-13T14:36:19Z",
|
||||
"url": "https://socialhub.activitypub.rocks/t/our-next-meeting/4079/1",
|
||||
"attributedTo": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1",
|
||||
"name": "Our next meeting",
|
||||
"context": "https://socialhub.activitypub.rocks/ap/collection/8850f6e85b57c490da915a5dfbbd5045",
|
||||
"content": "<h3>Last Meeting</h3>\n<h4>Recording</h4>\n<a href=\"https://us06web.zoom.us/rec/share/4hGBTvgXJPlu8UkjkkxVARypNg5DH0eeaKlIBv71D4G3lokYyrCrg7cqBCJmL109.FsHYTZDlVvZXrgcn?startTime=1712254114000\">https://us06web.zoom.us/rec/share/4hGBTvgXJPlu8UkjkkxVARypNg5DH0eeaKlIBv71D4G3lokYyrCrg7cqBCJmL109.FsHYTZDlVvZXrgcn?startTime=1712254114000</a>\nPasscode: z+1*4pUB\n<h4>Minutes</h4>\nTo refresh your memory, you can read the minutes of last week's meeting <a href=\"https://community.nodebb.org/topic/17949/minutes…",
|
||||
"@context": "https://www.w3.org/ns/activitystreams"
|
||||
}
|
23
crates/apub/assets/discourse/objects/person.json
Normal file
23
crates/apub/assets/discourse/objects/person.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"id": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1",
|
||||
"type": "Person",
|
||||
"updated": "2024-01-15T12:27:03Z",
|
||||
"url": "https://socialhub.activitypub.rocks/u/angus",
|
||||
"name": "Angus McLeod",
|
||||
"inbox": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1/inbox",
|
||||
"outbox": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1/outbox",
|
||||
"sharedInbox": "https://socialhub.activitypub.rocks/ap/users/inbox",
|
||||
"followers": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1/followers",
|
||||
"preferredUsername": "angus",
|
||||
"publicKey": {
|
||||
"id": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1#main-key",
|
||||
"owner": "https://socialhub.activitypub.rocks/ap/actor/495843076e9e469fbd35ccf467ae9fb1",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3RpuFDuwXZzOeHO5fO2O\nHmP7Flc5JDXJ8OOEJYq5T/dzUKqREOF1ZT0WMww8/E3P6w+gfFsjzThraJb8nHuW\nP6798SUD35CWBclfhw9DapjVn99JyFcAWcH3b9fr6LYshc4y1BoeJagk1kcro2Dc\n+pX0vVXgNjwWnGfyucAgGIUWrNUjcvIvXmyVCBSQfXG3nCALV1JbI4KSgf/5KyBn\nza/QefaetxYiFV8wAisPKLsz3XQAaITsQmbSi+8gmwXt/9U808PK1KphCiClDOWg\noi0HPzJn0rn+mwFCfgNWenvribfeG40AHLG33OkWKvslufjifdWDCOcBYYzyCEV6\n+wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/png",
|
||||
"url": "https://socialhub.activitypub.rocks/user_avatar/socialhub.activitypub.rocks/angus/96/2295_2.png"
|
||||
},
|
||||
"@context": "https://www.w3.org/ns/activitystreams"
|
||||
}
|
|
@ -23,7 +23,6 @@
|
|||
"href": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg"
|
||||
}
|
||||
],
|
||||
"commentsEnabled": true,
|
||||
"sensitive": false,
|
||||
"language": {
|
||||
"identifier": "ko",
|
||||
|
|
|
@ -23,7 +23,6 @@
|
|||
"href": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg"
|
||||
}
|
||||
],
|
||||
"commentsEnabled": true,
|
||||
"sensitive": false,
|
||||
"published": "2021-10-29T15:10:51.557399Z",
|
||||
"updated": "2021-10-29T15:11:35.976374Z"
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
"cc": [],
|
||||
"mediaType": "text/html",
|
||||
"attachment": [],
|
||||
"commentsEnabled": true,
|
||||
"sensitive": false,
|
||||
"published": "2023-02-06T06:42:41.939437Z",
|
||||
"language": {
|
||||
|
@ -36,7 +35,6 @@
|
|||
"cc": [],
|
||||
"mediaType": "text/html",
|
||||
"attachment": [],
|
||||
"commentsEnabled": true,
|
||||
"sensitive": false,
|
||||
"published": "2023-02-06T06:42:37.119567Z",
|
||||
"language": {
|
||||
|
|
|
@ -22,7 +22,6 @@
|
|||
],
|
||||
"name": "another outbox test",
|
||||
"mediaType": "text/html",
|
||||
"commentsEnabled": true,
|
||||
"sensitive": false,
|
||||
"stickied": false,
|
||||
"published": "2021-11-18T17:19:45.895163Z"
|
||||
|
@ -51,7 +50,6 @@
|
|||
],
|
||||
"name": "outbox test",
|
||||
"mediaType": "text/html",
|
||||
"commentsEnabled": true,
|
||||
"sensitive": false,
|
||||
"stickied": false,
|
||||
"published": "2021-11-18T17:19:05.763109Z"
|
||||
|
|
|
@ -25,7 +25,6 @@
|
|||
"url": "https://enterprise.lemmy.ml/pictrs/image/eOtYb9iEiB.png"
|
||||
},
|
||||
"sensitive": false,
|
||||
"commentsEnabled": true,
|
||||
"language": {
|
||||
"identifier": "fr",
|
||||
"name": "Français"
|
||||
|
|
22
crates/apub/assets/nodebb/objects/group.json
Normal file
22
crates/apub/assets/nodebb/objects/group.json
Normal file
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://community.nodebb.org/category/31",
|
||||
"url": "https://community.nodebb.org/category/31/threadiverse-working-group",
|
||||
"inbox": "https://community.nodebb.org/category/31/inbox",
|
||||
"outbox": "https://community.nodebb.org/category/31/outbox",
|
||||
"sharedInbox": "https://community.nodebb.org/inbox",
|
||||
"type": "Group",
|
||||
"name": "Threadiverse Working Group",
|
||||
"preferredUsername": "swicg-threadiverse-wg",
|
||||
"summary": "Discussion and announcements related to the SWICG Threadiverse task force",
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/png",
|
||||
"url": "https://community.nodebb.org/assets/uploads/system/site-logo.png"
|
||||
},
|
||||
"publicKey": {
|
||||
"id": "https://community.nodebb.org/category/31#key",
|
||||
"owner": "https://community.nodebb.org/category/31",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0/Or3Ox2/jbhBZzF8W0Y\nWuS/4lgm5O5rxQk2nDRBXU/qNaZnMPkW2FxFPuPetndUVKSD2+vWF3SUlFyZ/vhT\nITzLkbRSILMiZCUg+0mvqi6va1WMBglMe5jLkc7wdfgNsosqBzKMdyMxqDZr++mJ\n8DjuqzWHENcjWcbMfSfAa9nkZHBIQUsHGGIwxEbKNlPqF0JIB66py7xmXbboDxpD\nPVF3EMkgZNnbmDGtlkZCKbztradyNRVl/u6KJpV3fbi+m/8CZ+POc4I5sKCQY1Hr\ndslHlm6tCkJQxIIKQtz0ZJ5yCUYmk48C2gFCndfJtYoEy9iR62xSemky6y04gWVc\naQIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
}
|
||||
}
|
38
crates/apub/assets/nodebb/objects/page.json
Normal file
38
crates/apub/assets/nodebb/objects/page.json
Normal file
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://community.nodebb.org/topic/17908",
|
||||
"type": "Page",
|
||||
"to": ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"cc": ["https://community.nodebb.org/uid/2/followers"],
|
||||
"inReplyTo": null,
|
||||
"published": "2024-03-19T20:25:39.462Z",
|
||||
"url": "https://community.nodebb.org/topic/17908/threadiverse-working-group",
|
||||
"attributedTo": "https://community.nodebb.org/uid/2",
|
||||
"audience": "https://community.nodebb.org/category/31/threadiverse-working-group",
|
||||
"sensitive": false,
|
||||
"summary": null,
|
||||
"name": "Threadiverse Working Group",
|
||||
"content": "<p dir=\"auto\">NodeBB is at this year's FediForum, and one of the breakout sessions centred around <strong>the Theadiverse</strong>, the subset of ActivityPub-enabled applications built around a topic-centric model of content representation.</p>\n<p dir=\"auto\">Some of the topic touched upon included:</p>\n<ul>\n<li>Aligning on a standard representation for collections of Notes</li>\n<li>FEP-1b12 — Group federation and implementation thereof by Lemmy, et al.</li>\n<li>Offering a comparatively more feature-rich experience vis-a-vis restrictions re: microblogging</li>\n<li>Going forward: collaborating on building compatible threadiverse implementations</li>\n</ul>\n<p dir=\"auto\">The main action item involved <strong>the genesis of an informal working group for the threadiverse</strong>, in order to align our disparate implementations toward a common path.</p>\n<p dir=\"auto\">We intend to meet monthly at first, with the first meeting likely sometime early-to-mid April.</p>\n<p dir=\"auto\">The topic of the first WG call is: <strong>Representation of the higherlevel collection of Notes (posts, etc.) — Article vs. Page, etc?</strong></p>\n<p dir=\"auto\">Interested?</p>\n<ul>\n<li>Publicly reply to this post (NodeBB does not support non-public posts at this time) if you'd like to join the list</li>\n<li>If you prefer to remain private, please email <a href=\"mailto:julian@nodebb.org\" rel=\"nofollow ugc\">julian@nodebb.org</a></li>\n</ul>\n<hr />\n<p dir=\"auto\">As an aside, I'd love to try something new and attempt tokeep as much of this as I can on the social web. Can you do me a favour and boost this to your followers?</p>\n",
|
||||
"source": {
|
||||
"content": "NodeBB is at this year's FediForum, and one of the breakout sessions centred around **the Theadiverse**, the subset of ActivityPub-enabled applications built around a topic-centric model of content representation.\n\nSome of the topic touched upon included:\n\n* Aligning on a standard representation for collections of Notes\n* FEP-1b12 — Group federation and implementation thereof by Lemmy, et al.\n* Offering a comparatively more feature-rich experience vis-a-vis restrictions re: microblogging\n* Going forward: collaborating on building compatible threadiverse implementations\n\nThe main action item involved **the genesis of an informal working group for the threadiverse**, in order to align our disparate implementations toward a common path.\n\nWe intend to meet monthly at first, with the first meeting likely sometime early-to-mid April.\n\nThe topic of the first WG call is: **Representation of the higher level collection of Notes (posts, etc.) — Article vs. Page, etc?**\n\nInterested?\n\n* Publicly reply to this post (NodeBB does not support non-public postsat this time) if you'd like to join the list\n* If you prefer to remain private, please email julian@nodebb.org\n\n----\n\nAs an aside, I'd love to try something new and attempt to keep as much of this as I can on the social web. Can you do me a favour and boost this to your followers?",
|
||||
"mediaType": "text/markdown"
|
||||
},
|
||||
"tag": [
|
||||
{
|
||||
"type": "Hashtag",
|
||||
"href": "https://community.nodebb.org/tags/fediforum",
|
||||
"name": "#fediforum"
|
||||
},
|
||||
{
|
||||
"type": "Hashtag",
|
||||
"href": "https://community.nodebb.org/tags/activitypub",
|
||||
"name": "#activitypub"
|
||||
},
|
||||
{
|
||||
"type": "Hashtag",
|
||||
"href": "https://community.nodebb.org/tags/threadiverse",
|
||||
"name": "#threadiverse"
|
||||
}
|
||||
],
|
||||
"attachment": []
|
||||
}
|
29
crates/apub/assets/nodebb/objects/person.json
Normal file
29
crates/apub/assets/nodebb/objects/person.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"@context": "https://www.w3.org/ns/activitystreams",
|
||||
"id": "https://community.nodebb.org/uid/2",
|
||||
"url": "https://community.nodebb.org/user/julian",
|
||||
"followers": "https://community.nodebb.org/uid/2/followers",
|
||||
"following": "https://community.nodebb.org/uid/2/following",
|
||||
"inbox": "https://community.nodebb.org/uid/2/inbox",
|
||||
"outbox": "https://community.nodebb.org/uid/2/outbox",
|
||||
"sharedInbox": "https://community.nodebb.org/inbox",
|
||||
"type": "Person",
|
||||
"name": "julian",
|
||||
"preferredUsername": "julian",
|
||||
"summary": "Hi! I'm Julian, one of the co-founders of NodeBB, the forum software you are using right now.\r\n\r\nI started this company with two colleagues, Baris and Andrew, in 2013, and have been doing the startup thing since (although I think at some point along the way we stopped being a startup and just became a boring ol' small business).\r\n\r\nIn my free time I rock climb, cycle, and lift weights. I live just outside Toronto, Canada, with my wife and three children.",
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/jpeg",
|
||||
"url": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profileavatar-1701457270279.jpeg"
|
||||
},
|
||||
"image": {
|
||||
"type": "Image",
|
||||
"mediaType": "image/jpeg",
|
||||
"url": "https://community.nodebb.org/assets/uploads/profile/uid-2/2-profilecover-1649468285913.jpeg"
|
||||
},
|
||||
"publicKey": {
|
||||
"id": "https://community.nodebb.org/uid/2#key",
|
||||
"owner": "https://community.nodebb.org/uid/2",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzEr0sFdATahQzprS4EOT\nZq+KMc6UTbt2GDP20OrQi/P5AXAbMaQiRCRdGWhYGjnH0jicn5NnozNxRo+HchJT\nV6NOHxpsxqPCoaLeoBkhfhbSCLr2Gzil6mmfqf9TjnI7A7ZTtCc0G+n0ztyL9HwL\nkEAI178l2gckk4XKKYnEd+dyiIevExrq/ROLgwW1o428FZvlF5amKxhpVUEygRU8\nCd1hqWYs+xYDOJURCP5qEx/MmRPpV/yGMTMyF+/gcQc0TUZnhWAM2E4M+aq3aKh6\nJP/vsry+5YZPUaPWfopbT5Ijyt6ZSElp6Avkg56eTz0a5SRcjCVS6IFVPwiLlzOe\nYwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
}
|
||||
}
|
49
crates/apub/assets/wordpress/activities/announce.json
Normal file
49
crates/apub/assets/wordpress/activities/announce.json
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"@context": ["https://www.w3.org/ns/activitystreams"],
|
||||
"id": "https://pfefferle.org/lemmy-part-4/#activity#activity",
|
||||
"type": "Announce",
|
||||
"audience": "https://pfefferle.org/@pfefferle.org",
|
||||
"published": "2024-05-03T12:32:29Z",
|
||||
"updated": "2024-05-06T08:20:33Z",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://pfefferle.org/wp-json/activitypub/1.0/actors/1/followers"
|
||||
],
|
||||
"cc": [],
|
||||
"object": {
|
||||
"id": "https://pfefferle.org/lemmy-part-4/#activity",
|
||||
"type": "Update",
|
||||
"audience": "https://pfefferle.org/@pfefferle.org",
|
||||
"published": "2024-05-03T12:32:29Z",
|
||||
"updated": "2024-05-06T08:20:33Z",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://pfefferle.org/wp-json/activitypub/1.0/actors/1/followers"
|
||||
],
|
||||
"cc": [],
|
||||
"object": {
|
||||
"id": "https://pfefferle.org/lemmy-part-4/",
|
||||
"type": "Article",
|
||||
"attachment": [],
|
||||
"attributedTo": "https://pfefferle.org/author/pfefferle/",
|
||||
"audience": "https://pfefferle.org/@pfefferle.org",
|
||||
"content": "\u003Cp\u003EIdentifies one or more entities that represent the total population of entities for which the object can considered to be relevant. Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant. \u003C/p\u003E",
|
||||
"contentMap": {
|
||||
"en": "\u003Cp\u003EIdentifies one or more entities that represent the total population of entities for which the object can considered to be relevant. Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant. \u003C/p\u003E"
|
||||
},
|
||||
"name": "Lemmy (Part 4)",
|
||||
"published": "2024-05-03T12:32:29Z",
|
||||
"summary": "Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant. Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object can considered to be relevant.Identifies one or more entities that represent the total population of entities for which the object [...]",
|
||||
"tag": [],
|
||||
"updated": "2024-05-06T08:20:33Z",
|
||||
"url": "https://pfefferle.org/lemmy-part-4/",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://pfefferle.org/wp-json/activitypub/1.0/actors/1/followers"
|
||||
],
|
||||
"cc": []
|
||||
},
|
||||
"actor": "https://pfefferle.org/author/pfefferle/"
|
||||
},
|
||||
"actor": "https://pfefferle.org/@pfefferle.org"
|
||||
}
|
66
crates/apub/assets/wordpress/objects/group.json
Normal file
66
crates/apub/assets/wordpress/objects/group.json
Normal file
|
@ -0,0 +1,66 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
"https://purl.archive.org/socialweb/webfinger",
|
||||
{
|
||||
"schema": "http://schema.org#",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"webfinger": "https://webfinger.net/#",
|
||||
"lemmy": "https://join-lemmy.org/ns#",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"value": "schema:value",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"featured": {
|
||||
"@id": "toot:featured",
|
||||
"@type": "@id"
|
||||
},
|
||||
"featuredTags": {
|
||||
"@id": "toot:featuredTags",
|
||||
"@type": "@id"
|
||||
},
|
||||
"moderators": {
|
||||
"@id": "lemmy:moderators",
|
||||
"@type": "@id"
|
||||
},
|
||||
"postingRestrictedToMods": "lemmy:postingRestrictedToMods",
|
||||
"discoverable": "toot:discoverable",
|
||||
"indexable": "toot:indexable",
|
||||
"resource": "webfinger:resource"
|
||||
}
|
||||
],
|
||||
"id": "https://pfefferle.org/@pfefferle.org",
|
||||
"type": "Group",
|
||||
"attachment": [],
|
||||
"attributedTo": "https://pfefferle.org/wp-json/activitypub/1.0/collections/moderators",
|
||||
"name": "Matthias Pfefferle",
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"url": "https://pfefferle.org/wp-content/uploads/2023/06/cropped-BeLItBV-_400x400.jpg"
|
||||
},
|
||||
"published": "2024-04-03T16:58:22Z",
|
||||
"summary": "<p>Webworker, blogger und podcaster</p>\n",
|
||||
"tag": [],
|
||||
"url": "https://pfefferle.org/@pfefferle.org",
|
||||
"inbox": "https://pfefferle.org/wp-json/activitypub/1.0/users/0/inbox",
|
||||
"outbox": "https://pfefferle.org/wp-json/activitypub/1.0/users/0/outbox",
|
||||
"following": "https://pfefferle.org/wp-json/activitypub/1.0/users/0/following",
|
||||
"followers": "https://pfefferle.org/wp-json/activitypub/1.0/users/0/followers",
|
||||
"preferredUsername": "pfefferle.org",
|
||||
"endpoints": {
|
||||
"sharedInbox": "https://pfefferle.org/wp-json/activitypub/1.0/inbox"
|
||||
},
|
||||
"publicKey": {
|
||||
"id": "https://pfefferle.org/@pfefferle.org#main-key",
|
||||
"owner": "https://pfefferle.org/@pfefferle.org",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuq8xeLMFcaCwPFBhgMRE\n/dDh2XKoNXFXnixctmK8BXSuuLMxucm3I/8NyhIvb3LqU+uP1fO8F0ecUbk2sN+x\nKag5vIV6yKXzJ8ILMWQ9AaELpXDmMZqL0zal0LUJRAOkDgPDovDAoq6tx++yDoV0\njdVbf9CoZKit1cz2ZrEuE5dswq3J/z9+c6POkhCkWEX5TPJzkOrmnjkvrXxGHUJ2\nA3+P+VaZhd5cmvqYosSpYNJshxCdev12pIF78OnYLiYiyXlgGHU+7uQR0M4tTcij\n6cUdLkms9m+b6H3ctXntPn410e5YLFPldjAYzQB5wHVdFZsWtyrbqfYdCa+KkKpA\nvwIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"manuallyApprovesFollowers": false,
|
||||
"featured": "https://pfefferle.org/wp-json/activitypub/1.0/users/0/collections/featured",
|
||||
"moderators": "https://pfefferle.org/wp-json/activitypub/1.0/collections/moderators",
|
||||
"discoverable": true,
|
||||
"indexable": true,
|
||||
"webfinger": "pfefferle.org@pfefferle.org",
|
||||
"postingRestrictedToMods": true
|
||||
}
|
24
crates/apub/assets/wordpress/objects/note.json
Normal file
24
crates/apub/assets/wordpress/objects/note.json
Normal file
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
{
|
||||
"Hashtag": "as:Hashtag"
|
||||
}
|
||||
],
|
||||
"id": "https://pfefferle.org?c=148",
|
||||
"type": "Note",
|
||||
"attributedTo": "https://pfefferle.org/author/pfefferle/",
|
||||
"content": "<p>Nice! Hello from WordPress!</p>",
|
||||
"contentMap": {
|
||||
"en": "<p>Nice! Hello from WordPress!</p>"
|
||||
},
|
||||
"inReplyTo": "https://socialhub.activitypub.rocks/ap/object/ce040f1ead95964f6dbbf1084b81432d",
|
||||
"published": "2024-04-30T15:21:13Z",
|
||||
"tag": [],
|
||||
"url": "https://pfefferle.org?c=148",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://pfefferle.org/wp-json/activitypub/1.0/users/0/followers"
|
||||
],
|
||||
"cc": []
|
||||
}
|
26
crates/apub/assets/wordpress/objects/page.json
Normal file
26
crates/apub/assets/wordpress/objects/page.json
Normal file
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
{
|
||||
"Hashtag": "as:Hashtag"
|
||||
}
|
||||
],
|
||||
"id": "https://pfefferle.org/this-is-a-test-federation/",
|
||||
"type": "Article",
|
||||
"attachment": [],
|
||||
"attributedTo": "https://pfefferle.org/author/pfefferle/",
|
||||
"content": "<p>with Discource!</p>",
|
||||
"contentMap": {
|
||||
"en": "<p>with Discource!</p>"
|
||||
},
|
||||
"name": "This is a test-federation",
|
||||
"published": "2024-04-30T15:16:41Z",
|
||||
"summary": "with Discource! [...]",
|
||||
"tag": [],
|
||||
"url": "https://pfefferle.org/this-is-a-test-federation/",
|
||||
"to": [
|
||||
"https://www.w3.org/ns/activitystreams#Public",
|
||||
"https://pfefferle.org/wp-json/activitypub/1.0/users/1/followers"
|
||||
],
|
||||
"cc": []
|
||||
}
|
74
crates/apub/assets/wordpress/objects/person.json
Normal file
74
crates/apub/assets/wordpress/objects/person.json
Normal file
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://www.w3.org/ns/activitystreams",
|
||||
"https://w3id.org/security/v1",
|
||||
"https://purl.archive.org/socialweb/webfinger",
|
||||
{
|
||||
"schema": "http://schema.org#",
|
||||
"toot": "http://joinmastodon.org/ns#",
|
||||
"webfinger": "https://webfinger.net/#",
|
||||
"lemmy": "https://join-lemmy.org/ns#",
|
||||
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
|
||||
"PropertyValue": "schema:PropertyValue",
|
||||
"value": "schema:value",
|
||||
"Hashtag": "as:Hashtag",
|
||||
"featured": {
|
||||
"@id": "toot:featured",
|
||||
"@type": "@id"
|
||||
},
|
||||
"featuredTags": {
|
||||
"@id": "toot:featuredTags",
|
||||
"@type": "@id"
|
||||
},
|
||||
"moderators": {
|
||||
"@id": "lemmy:moderators",
|
||||
"@type": "@id"
|
||||
},
|
||||
"postingRestrictedToMods": "lemmy:postingRestrictedToMods",
|
||||
"discoverable": "toot:discoverable",
|
||||
"indexable": "toot:indexable",
|
||||
"resource": "webfinger:resource"
|
||||
}
|
||||
],
|
||||
"id": "https://pfefferle.org/author/pfefferle/",
|
||||
"type": "Person",
|
||||
"attachment": [
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "Blog",
|
||||
"value": "<a rel=\"me\" title=\"https://pfefferle.org/\" target=\"_blank\" href=\"https://pfefferle.org/\">pfefferle.org</a>"
|
||||
},
|
||||
{
|
||||
"type": "PropertyValue",
|
||||
"name": "Profile",
|
||||
"value": "<a rel=\"me\" title=\"https://pfefferle.org/author/pfefferle/\" target=\"_blank\" href=\"https://pfefferle.org/author/pfefferle/\">pfefferle.org</a>"
|
||||
}
|
||||
],
|
||||
"name": "Matthias Pfefferle",
|
||||
"icon": {
|
||||
"type": "Image",
|
||||
"url": "https://secure.gravatar.com/avatar/a2bdca7870e859658cece96c044b3be5?s=120&d=mm&r=g"
|
||||
},
|
||||
"published": "2014-02-10T15:23:08Z",
|
||||
"summary": "<p>Ich arbeite als Open Web Lead für Automattic.</p>\n",
|
||||
"tag": [],
|
||||
"url": "https://pfefferle.org/author/pfefferle/",
|
||||
"inbox": "https://pfefferle.org/wp-json/activitypub/1.0/users/1/inbox",
|
||||
"outbox": "https://pfefferle.org/wp-json/activitypub/1.0/users/1/outbox",
|
||||
"following": "https://pfefferle.org/wp-json/activitypub/1.0/users/1/following",
|
||||
"followers": "https://pfefferle.org/wp-json/activitypub/1.0/users/1/followers",
|
||||
"preferredUsername": "matthias",
|
||||
"endpoints": {
|
||||
"sharedInbox": "https://pfefferle.org/wp-json/activitypub/1.0/inbox"
|
||||
},
|
||||
"publicKey": {
|
||||
"id": "https://pfefferle.org/author/pfefferle/#main-key",
|
||||
"owner": "https://pfefferle.org/author/pfefferle/",
|
||||
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTA5RA40nOsso04RSwyX\nHXTojRPUMlIlArDcSy3M5GUJp9/xbxSUOdBjqd31KKB1GIi3vrLmD1Qi/ZqS95Qy\nw2Zd3xOsCg+o9bsyOG+O6Y8Lu+HEB5JKLUbNHdiSviakJ8wGadH9Wm4WIiN20y+q\n/u6lgxgiWfZ2CFCN6SOc28fUKi9NmKvXK+M12BhFfy1tC5KWXKDm0UbfI1+dmqhR\n3Ffe6vEsCI/YIVVdWxQ9kouOd0XSHOGdslktkepRO7IP9i9TdwyeCa0WWRoeO5Wa\ntVpc1Y0WuNbTM2ksIXTg0G+rO1/6KO/hrHnGu3RCfb/ZIHK5L/aWYb9B3PG3LyKV\n+wIDAQAB\n-----END PUBLIC KEY-----\n"
|
||||
},
|
||||
"manuallyApprovesFollowers": false,
|
||||
"featured": "https://pfefferle.org/wp-json/activitypub/1.0/users/1/collections/featured",
|
||||
"discoverable": true,
|
||||
"indexable": true,
|
||||
"webfinger": "matthias@pfefferle.org"
|
||||
}
|
|
@ -39,7 +39,10 @@ use lemmy_db_schema::{
|
|||
},
|
||||
traits::{Bannable, Crud, Followable},
|
||||
};
|
||||
use lemmy_utils::error::{LemmyError, LemmyResult};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyResult},
|
||||
LemmyErrorType,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
impl BlockUser {
|
||||
|
@ -129,7 +132,11 @@ impl ActivityHandler for BlockUser {
|
|||
verify_is_public(&self.to, &self.cc)?;
|
||||
match self.target.dereference(context).await? {
|
||||
SiteOrCommunity::Site(site) => {
|
||||
let domain = self.object.inner().domain().expect("url needs domain");
|
||||
let domain = self
|
||||
.object
|
||||
.inner()
|
||||
.domain()
|
||||
.ok_or(LemmyErrorType::UrlWithoutDomain)?;
|
||||
if context.settings().hostname == domain {
|
||||
return Err(
|
||||
anyhow!("Site bans from remote instance can't affect user's home instance").into(),
|
||||
|
|
|
@ -94,7 +94,12 @@ impl AnnounceActivity {
|
|||
actor: community.id().into(),
|
||||
to: vec![public()],
|
||||
object: IdOrNestedObject::NestedObject(object),
|
||||
cc: vec![community.followers_url.clone().into()],
|
||||
cc: community
|
||||
.followers_url
|
||||
.clone()
|
||||
.map(Into::into)
|
||||
.into_iter()
|
||||
.collect(),
|
||||
kind: AnnounceType::Announce,
|
||||
id,
|
||||
})
|
||||
|
|
|
@ -138,8 +138,8 @@ impl ActivityHandler for CollectionAdd {
|
|||
.dereference(context)
|
||||
.await?;
|
||||
|
||||
// If we had to refetch the community while parsing the activity, then the new mod has already
|
||||
// been added. Skip it here as it would result in a duplicate key error.
|
||||
// If we had to refetch the community while parsing the activity, then the new mod has
|
||||
// already been added. Skip it here as it would result in a duplicate key error.
|
||||
let new_mod_id = new_mod.id;
|
||||
let moderated_communities =
|
||||
CommunityModerator::get_person_moderated_communities(&mut context.pool(), new_mod_id)
|
||||
|
|
|
@ -26,6 +26,7 @@ use lemmy_db_schema::{
|
|||
source::{
|
||||
activity::ActivitySendTargets,
|
||||
community::Community,
|
||||
moderator::{ModLockPost, ModLockPostForm},
|
||||
person::Person,
|
||||
post::{Post, PostUpdateForm},
|
||||
},
|
||||
|
@ -60,12 +61,22 @@ impl ActivityHandler for LockPage {
|
|||
}
|
||||
|
||||
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
insert_received_activity(&self.id, context).await?;
|
||||
let locked = Some(true);
|
||||
let form = PostUpdateForm {
|
||||
locked: Some(true),
|
||||
locked,
|
||||
..Default::default()
|
||||
};
|
||||
let post = self.object.dereference(context).await?;
|
||||
Post::update(&mut context.pool(), post.id, &form).await?;
|
||||
|
||||
let form = ModLockPostForm {
|
||||
mod_person_id: self.actor.dereference(context).await?.id,
|
||||
post_id: post.id,
|
||||
locked,
|
||||
};
|
||||
ModLockPost::create(&mut context.pool(), &form).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -94,12 +105,21 @@ impl ActivityHandler for UndoLockPage {
|
|||
|
||||
async fn receive(self, context: &Data<Self::DataType>) -> Result<(), Self::Error> {
|
||||
insert_received_activity(&self.id, context).await?;
|
||||
let locked = Some(false);
|
||||
let form = PostUpdateForm {
|
||||
locked: Some(false),
|
||||
locked,
|
||||
..Default::default()
|
||||
};
|
||||
let post = self.object.object.dereference(context).await?;
|
||||
Post::update(&mut context.pool(), post.id, &form).await?;
|
||||
|
||||
let form = ModLockPostForm {
|
||||
mod_person_id: self.actor.dereference(context).await?.id,
|
||||
post_id: post.id,
|
||||
locked,
|
||||
};
|
||||
ModLockPost::create(&mut context.pool(), &form).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,13 +24,14 @@ pub mod update;
|
|||
///
|
||||
/// Activities are sent to the community itself if it lives on another instance. If the community
|
||||
/// is local, the activity is directly wrapped into Announce and sent to community followers.
|
||||
/// Activities are also sent to those who follow the actor (with exception of moderation activities).
|
||||
/// Activities are also sent to those who follow the actor (with exception of moderation
|
||||
/// activities).
|
||||
///
|
||||
/// * `activity` - The activity which is being sent
|
||||
/// * `actor` - The user who is sending the activity
|
||||
/// * `community` - Community inside which the activity is sent
|
||||
/// * `inboxes` - Any additional inboxes the activity should be sent to (for example,
|
||||
/// to the user who is being promoted to moderator)
|
||||
/// * `inboxes` - Any additional inboxes the activity should be sent to (for example, to the user
|
||||
/// who is being promoted to moderator)
|
||||
/// * `is_mod_activity` - True for things like Add/Mod, these are not sent to user followers
|
||||
pub(crate) async fn send_activity_in_community(
|
||||
activity: AnnouncableActivities,
|
||||
|
|
|
@ -105,7 +105,7 @@ impl ActivityHandler for UpdateCommunity {
|
|||
last_refreshed_at: Some(naive_now()),
|
||||
icon: Some(self.object.icon.map(|i| i.url.into())),
|
||||
banner: Some(self.object.image.map(|i| i.url.into())),
|
||||
followers_url: Some(self.object.followers.into()),
|
||||
followers_url: self.object.followers.map(Into::into),
|
||||
inbox_url: Some(self.object.inbox.into()),
|
||||
shared_inbox_url: Some(self.object.endpoints.map(|e| e.shared_inbox.into())),
|
||||
moderators_url: self.object.attributed_to.map(Into::into),
|
||||
|
|
|
@ -19,7 +19,7 @@ use activitypub_federation::{
|
|||
config::Data,
|
||||
fetch::object_id::ObjectId,
|
||||
kinds::public,
|
||||
protocol::verification::verify_domains_match,
|
||||
protocol::verification::{verify_domains_match, verify_urls_match},
|
||||
traits::{ActivityHandler, Actor, Object},
|
||||
};
|
||||
use lemmy_api_common::{
|
||||
|
@ -133,6 +133,7 @@ impl ActivityHandler for CreateOrUpdateNote {
|
|||
verify_domains_match(self.actor.inner(), self.object.id.inner())?;
|
||||
check_community_deleted_or_removed(&community)?;
|
||||
check_post_deleted_or_removed(&post)?;
|
||||
verify_urls_match(self.actor.inner(), self.object.attributed_to.inner())?;
|
||||
|
||||
ApubComment::verify(&self.object, self.actor.inner(), context).await?;
|
||||
Ok(())
|
||||
|
@ -175,7 +176,8 @@ impl ActivityHandler for CreateOrUpdateNote {
|
|||
// Although mentions could be gotten from the post tags (they are included there), or the ccs,
|
||||
// Its much easier to scrape them from the comment body, since the API has to do that
|
||||
// anyway.
|
||||
// TODO: for compatibility with other projects, it would be much better to read this from cc or tags
|
||||
// TODO: for compatibility with other projects, it would be much better to read this from cc or
|
||||
// tags
|
||||
let mentions = scrape_text_for_mentions(&comment.content);
|
||||
send_local_notifs(mentions, comment.id, &actor, do_send_email, context).await?;
|
||||
Ok(())
|
||||
|
|
|
@ -4,7 +4,6 @@ use crate::{
|
|||
community::send_activity_in_community,
|
||||
generate_activity_id,
|
||||
verify_is_public,
|
||||
verify_mod_action,
|
||||
verify_person_in_community,
|
||||
},
|
||||
activity_lists::AnnouncableActivities,
|
||||
|
@ -66,7 +65,6 @@ impl CreateOrUpdatePage {
|
|||
kind: CreateOrUpdateType,
|
||||
context: Data<LemmyContext>,
|
||||
) -> LemmyResult<()> {
|
||||
let post = ApubPost(post);
|
||||
let community_id = post.community_id;
|
||||
let person: ApubPerson = Person::read(&mut context.pool(), person_id)
|
||||
.await?
|
||||
|
@ -78,15 +76,14 @@ impl CreateOrUpdatePage {
|
|||
.into();
|
||||
|
||||
let create_or_update =
|
||||
CreateOrUpdatePage::new(post, &person, &community, kind, &context).await?;
|
||||
let is_mod_action = create_or_update.object.is_mod_action(&context).await?;
|
||||
CreateOrUpdatePage::new(post.into(), &person, &community, kind, &context).await?;
|
||||
let activity = AnnouncableActivities::CreateOrUpdatePost(create_or_update);
|
||||
send_activity_in_community(
|
||||
activity,
|
||||
&person,
|
||||
&community,
|
||||
ActivitySendTargets::empty(),
|
||||
is_mod_action,
|
||||
false,
|
||||
&context,
|
||||
)
|
||||
.await?;
|
||||
|
@ -113,30 +110,8 @@ impl ActivityHandler for CreateOrUpdatePage {
|
|||
let community = self.community(context).await?;
|
||||
verify_person_in_community(&self.actor, &community, context).await?;
|
||||
check_community_deleted_or_removed(&community)?;
|
||||
|
||||
match self.kind {
|
||||
CreateOrUpdateType::Create => {
|
||||
verify_domains_match(self.actor.inner(), self.object.id.inner())?;
|
||||
verify_urls_match(self.actor.inner(), self.object.creator()?.inner())?;
|
||||
// Check that the post isnt locked, as that isnt possible for newly created posts.
|
||||
// However, when fetching a remote post we generate a new create activity with the current
|
||||
// locked value, so this check may fail. So only check if its a local community,
|
||||
// because then we will definitely receive all create and update activities separately.
|
||||
let is_locked = self.object.comments_enabled == Some(false);
|
||||
if community.local && is_locked {
|
||||
Err(LemmyErrorType::NewPostCannotBeLocked)?
|
||||
}
|
||||
}
|
||||
CreateOrUpdateType::Update => {
|
||||
let is_mod_action = self.object.is_mod_action(context).await?;
|
||||
if is_mod_action {
|
||||
verify_mod_action(&self.actor, &community, context).await?;
|
||||
} else {
|
||||
verify_domains_match(self.actor.inner(), self.object.id.inner())?;
|
||||
verify_urls_match(self.actor.inner(), self.object.creator()?.inner())?;
|
||||
}
|
||||
}
|
||||
}
|
||||
ApubPost::verify(&self.object, self.actor.inner(), context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
};
|
||||
use activitypub_federation::{
|
||||
config::Data,
|
||||
protocol::verification::verify_domains_match,
|
||||
protocol::verification::{verify_domains_match, verify_urls_match},
|
||||
traits::{ActivityHandler, Actor, Object},
|
||||
};
|
||||
use lemmy_api_common::context::LemmyContext;
|
||||
|
@ -61,6 +61,7 @@ impl ActivityHandler for CreateOrUpdateChatMessage {
|
|||
verify_person(&self.actor, context).await?;
|
||||
verify_domains_match(self.actor.inner(), self.object.id.inner())?;
|
||||
verify_domains_match(self.to[0].inner(), self.object.to[0].inner())?;
|
||||
verify_urls_match(self.actor.inner(), self.object.attributed_to.inner())?;
|
||||
ApubPrivateMessage::verify(&self.object, self.actor.inner(), context).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,9 +4,10 @@ use crate::objects::{
|
|||
person::ApubPerson,
|
||||
post::ApubPost,
|
||||
};
|
||||
use activitypub_federation::{config::Data, fetch::object_id::ObjectId};
|
||||
use activitypub_federation::{config::Data, fetch::object_id::ObjectId, traits::Object};
|
||||
use actix_web::web::Json;
|
||||
use futures::{future::try_join_all, StreamExt};
|
||||
use itertools::Itertools;
|
||||
use lemmy_api_common::{context::LemmyContext, SuccessResponse};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::DbUrl,
|
||||
|
@ -30,8 +31,11 @@ use lemmy_utils::{
|
|||
spawn_try_task,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::future::Future;
|
||||
use tracing::info;
|
||||
|
||||
const PARALLELISM: usize = 10;
|
||||
|
||||
/// Backup of user data. This struct should never be changed so that the data can be used as a
|
||||
/// long-term backup in case the instance goes down unexpectedly. All fields are optional to allow
|
||||
/// importing partial backups.
|
||||
|
@ -40,7 +44,7 @@ use tracing::info;
|
|||
///
|
||||
/// Be careful with any changes to this struct, to avoid breaking changes which could prevent
|
||||
/// importing older backups.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
pub struct UserSettingsBackup {
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
|
@ -167,28 +171,17 @@ pub async fn import_settings(
|
|||
}
|
||||
|
||||
spawn_try_task(async move {
|
||||
const PARALLELISM: usize = 10;
|
||||
let person_id = local_user_view.person.id;
|
||||
|
||||
// These tasks fetch objects from remote instances which might be down.
|
||||
// TODO: Would be nice if we could send a list of failed items with api response, but then
|
||||
// the request would likely timeout.
|
||||
let mut failed_items = vec![];
|
||||
|
||||
info!(
|
||||
"Starting settings backup for {}",
|
||||
"Starting settings import for {}",
|
||||
local_user_view.person.name
|
||||
);
|
||||
|
||||
futures::stream::iter(
|
||||
data
|
||||
.followed_communities
|
||||
.clone()
|
||||
.into_iter()
|
||||
// reset_request_count works like clone, and is necessary to avoid running into request limit
|
||||
.map(|f| (f, context.reset_request_count()))
|
||||
.map(|(followed, context)| async move {
|
||||
// need to reset outgoing request count to avoid running into limit
|
||||
let failed_followed_communities = fetch_and_import(
|
||||
data.followed_communities.clone(),
|
||||
&context,
|
||||
|(followed, context)| async move {
|
||||
let community = followed.dereference(&context).await?;
|
||||
let form = CommunityFollowerForm {
|
||||
person_id,
|
||||
|
@ -197,27 +190,14 @@ pub async fn import_settings(
|
|||
};
|
||||
CommunityFollower::follow(&mut context.pool(), &form).await?;
|
||||
LemmyResult::Ok(())
|
||||
}),
|
||||
},
|
||||
)
|
||||
.buffer_unordered(PARALLELISM)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.for_each(|(i, r)| {
|
||||
if let Err(e) = r {
|
||||
failed_items.push(data.followed_communities.get(i).map(|u| u.inner().clone()));
|
||||
info!("Failed to import followed community: {e}");
|
||||
}
|
||||
});
|
||||
.await?;
|
||||
|
||||
futures::stream::iter(
|
||||
data
|
||||
.saved_posts
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|s| (s, context.reset_request_count()))
|
||||
.map(|(saved, context)| async move {
|
||||
let failed_saved_posts = fetch_and_import(
|
||||
data.saved_posts.clone(),
|
||||
&context,
|
||||
|(saved, context)| async move {
|
||||
let post = saved.dereference(&context).await?;
|
||||
let form = PostSavedForm {
|
||||
person_id,
|
||||
|
@ -225,27 +205,14 @@ pub async fn import_settings(
|
|||
};
|
||||
PostSaved::save(&mut context.pool(), &form).await?;
|
||||
LemmyResult::Ok(())
|
||||
}),
|
||||
},
|
||||
)
|
||||
.buffer_unordered(PARALLELISM)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.for_each(|(i, r)| {
|
||||
if let Err(e) = r {
|
||||
failed_items.push(data.followed_communities.get(i).map(|u| u.inner().clone()));
|
||||
info!("Failed to import saved post community: {e}");
|
||||
}
|
||||
});
|
||||
.await?;
|
||||
|
||||
futures::stream::iter(
|
||||
data
|
||||
.saved_comments
|
||||
.clone()
|
||||
.into_iter()
|
||||
.map(|s| (s, context.reset_request_count()))
|
||||
.map(|(saved, context)| async move {
|
||||
let failed_saved_comments = fetch_and_import(
|
||||
data.saved_comments.clone(),
|
||||
&context,
|
||||
|(saved, context)| async move {
|
||||
let comment = saved.dereference(&context).await?;
|
||||
let form = CommentSavedForm {
|
||||
person_id,
|
||||
|
@ -253,55 +220,42 @@ pub async fn import_settings(
|
|||
};
|
||||
CommentSaved::save(&mut context.pool(), &form).await?;
|
||||
LemmyResult::Ok(())
|
||||
}),
|
||||
},
|
||||
)
|
||||
.buffer_unordered(PARALLELISM)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.for_each(|(i, r)| {
|
||||
if let Err(e) = r {
|
||||
failed_items.push(data.followed_communities.get(i).map(|u| u.inner().clone()));
|
||||
info!("Failed to import saved comment community: {e}");
|
||||
}
|
||||
});
|
||||
.await?;
|
||||
|
||||
let failed_items: Vec<_> = failed_items.into_iter().flatten().collect();
|
||||
info!(
|
||||
"Finished settings backup for {}, failed items: {:#?}",
|
||||
local_user_view.person.name, failed_items
|
||||
);
|
||||
|
||||
// These tasks don't connect to any remote instances but only insert directly in the database.
|
||||
// That means the only error condition are db connection failures, so no extra error handling is
|
||||
// needed.
|
||||
try_join_all(data.blocked_communities.iter().map(|blocked| async {
|
||||
// dont fetch unknown blocked objects from home server
|
||||
let community = blocked.dereference_local(&context).await?;
|
||||
let failed_community_blocks = fetch_and_import(
|
||||
data.blocked_communities.clone(),
|
||||
&context,
|
||||
|(blocked, context)| async move {
|
||||
let community = blocked.dereference(&context).await?;
|
||||
let form = CommunityBlockForm {
|
||||
person_id,
|
||||
community_id: community.id,
|
||||
};
|
||||
CommunityBlock::block(&mut context.pool(), &form).await?;
|
||||
LemmyResult::Ok(())
|
||||
}))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
try_join_all(data.blocked_users.iter().map(|blocked| async {
|
||||
// dont fetch unknown blocked objects from home server
|
||||
let target = blocked.dereference_local(&context).await?;
|
||||
let failed_user_blocks = fetch_and_import(
|
||||
data.blocked_users.clone(),
|
||||
&context,
|
||||
|(blocked, context)| async move {
|
||||
let context = context.reset_request_count();
|
||||
let target = blocked.dereference(&context).await?;
|
||||
let form = PersonBlockForm {
|
||||
person_id,
|
||||
target_id: target.id,
|
||||
};
|
||||
PersonBlock::block(&mut context.pool(), &form).await?;
|
||||
LemmyResult::Ok(())
|
||||
}))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
try_join_all(data.blocked_instances.iter().map(|domain| async {
|
||||
// dont fetch unknown blocked objects from home server
|
||||
let instance = Instance::read_or_create(&mut context.pool(), domain.clone()).await?;
|
||||
let form = InstanceBlockForm {
|
||||
person_id,
|
||||
|
@ -312,17 +266,53 @@ pub async fn import_settings(
|
|||
}))
|
||||
.await?;
|
||||
|
||||
info!("Settings import completed for {}, the following items failed: {failed_followed_communities}, {failed_saved_posts}, {failed_saved_comments}, {failed_community_blocks}, {failed_user_blocks}",
|
||||
local_user_view.person.name);
|
||||
|
||||
Ok(())
|
||||
});
|
||||
|
||||
Ok(Json(Default::default()))
|
||||
}
|
||||
|
||||
async fn fetch_and_import<Kind, Fut>(
|
||||
objects: Vec<ObjectId<Kind>>,
|
||||
context: &Data<LemmyContext>,
|
||||
import_fn: impl FnMut((ObjectId<Kind>, Data<LemmyContext>)) -> Fut,
|
||||
) -> LemmyResult<String>
|
||||
where
|
||||
Kind: Object + Send + 'static,
|
||||
for<'de2> <Kind as Object>::Kind: Deserialize<'de2>,
|
||||
Fut: Future<Output = LemmyResult<()>>,
|
||||
{
|
||||
let mut failed_items = vec![];
|
||||
futures::stream::iter(
|
||||
objects
|
||||
.clone()
|
||||
.into_iter()
|
||||
// need to reset outgoing request count to avoid running into limit
|
||||
.map(|s| (s, context.reset_request_count()))
|
||||
.map(import_fn),
|
||||
)
|
||||
.buffer_unordered(PARALLELISM)
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.for_each(|(i, r): (usize, LemmyResult<()>)| {
|
||||
if r.is_err() {
|
||||
if let Some(object) = objects.get(i) {
|
||||
failed_items.push(object.inner().clone());
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(failed_items.into_iter().join(","))
|
||||
}
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
mod tests {
|
||||
|
||||
use crate::api::user_settings_backup::{export_settings, import_settings};
|
||||
use crate::api::user_settings_backup::{export_settings, import_settings, UserSettingsBackup};
|
||||
use activitypub_federation::config::Data;
|
||||
use lemmy_api_common::context::LemmyContext;
|
||||
use lemmy_db_schema::{
|
||||
|
@ -348,13 +338,11 @@ mod tests {
|
|||
context: &Data<LemmyContext>,
|
||||
) -> LemmyResult<LocalUserView> {
|
||||
let instance = Instance::read_or_create(&mut context.pool(), "example.com".to_string()).await?;
|
||||
let person_form = PersonInsertForm::builder()
|
||||
.name(name.clone())
|
||||
.display_name(Some(name.clone()))
|
||||
.bio(bio)
|
||||
.public_key("asd".to_string())
|
||||
.instance_id(instance.id)
|
||||
.build();
|
||||
let person_form = PersonInsertForm {
|
||||
display_name: Some(name.clone()),
|
||||
bio,
|
||||
..PersonInsertForm::test_form(instance.id, &name)
|
||||
};
|
||||
let person = Person::create(&mut context.pool(), &person_form).await?;
|
||||
|
||||
let user_form = LocalUserInsertForm::builder()
|
||||
|
@ -420,6 +408,44 @@ mod tests {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn test_settings_partial_import() -> LemmyResult<()> {
|
||||
let context = LemmyContext::init_test_context().await;
|
||||
|
||||
let export_user =
|
||||
create_user("hanna".to_string(), Some("my bio".to_string()), &context).await?;
|
||||
|
||||
let community_form = CommunityInsertForm::builder()
|
||||
.name("testcom".to_string())
|
||||
.title("testcom".to_string())
|
||||
.instance_id(export_user.person.instance_id)
|
||||
.build();
|
||||
let community = Community::create(&mut context.pool(), &community_form).await?;
|
||||
let follower_form = CommunityFollowerForm {
|
||||
community_id: community.id,
|
||||
person_id: export_user.person.id,
|
||||
pending: false,
|
||||
};
|
||||
CommunityFollower::follow(&mut context.pool(), &follower_form).await?;
|
||||
|
||||
let backup = export_settings(export_user.clone(), context.reset_request_count()).await?;
|
||||
|
||||
let import_user = create_user("charles".to_string(), None, &context).await?;
|
||||
|
||||
let backup2 = UserSettingsBackup {
|
||||
followed_communities: backup.followed_communities.clone(),
|
||||
..Default::default()
|
||||
};
|
||||
import_settings(
|
||||
actix_web::web::Json(backup2),
|
||||
import_user.clone(),
|
||||
context.reset_request_count(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[serial]
|
||||
async fn disallow_large_backup() -> LemmyResult<()> {
|
||||
|
|
|
@ -72,7 +72,8 @@ impl Collection for ApubCommunityFeatured {
|
|||
.to_vec();
|
||||
}
|
||||
|
||||
// process items in parallel, to avoid long delay from fetch_site_metadata() and other processing
|
||||
// process items in parallel, to avoid long delay from fetch_site_metadata() and other
|
||||
// processing
|
||||
let stickied_posts: Vec<Post> = join_all(pages.into_iter().map(|page| {
|
||||
async {
|
||||
// use separate request counter for each item, otherwise there will be problems with
|
||||
|
|
|
@ -129,11 +129,7 @@ mod tests {
|
|||
let inserted_instance =
|
||||
Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?;
|
||||
|
||||
let old_mod = PersonInsertForm::builder()
|
||||
.name("holly".into())
|
||||
.public_key("pubkey".to_string())
|
||||
.instance_id(inserted_instance.id)
|
||||
.build();
|
||||
let old_mod = PersonInsertForm::test_form(inserted_instance.id, "holly");
|
||||
|
||||
let old_mod = Person::create(&mut context.pool(), &old_mod).await?;
|
||||
let community_moderator_form = CommunityModeratorForm {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use crate::{
|
||||
activity_lists::AnnouncableActivities,
|
||||
objects::{community::ApubCommunity, post::ApubPost},
|
||||
objects::community::ApubCommunity,
|
||||
protocol::{
|
||||
activities::{
|
||||
community::announce::AnnounceActivity,
|
||||
|
@ -18,11 +18,8 @@ use activitypub_federation::{
|
|||
};
|
||||
use futures::future::join_all;
|
||||
use lemmy_api_common::{context::LemmyContext, utils::generate_outbox_url};
|
||||
use lemmy_db_schema::{
|
||||
source::{person::Person, post::Post},
|
||||
traits::Crud,
|
||||
utils::FETCH_LIMIT_MAX,
|
||||
};
|
||||
use lemmy_db_schema::{utils::FETCH_LIMIT_MAX, SortType};
|
||||
use lemmy_db_views::{post_view::PostQuery, structs::SiteView};
|
||||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyResult},
|
||||
LemmyErrorType,
|
||||
|
@ -41,19 +38,30 @@ impl Collection for ApubCommunityOutbox {
|
|||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn read_local(owner: &Self::Owner, data: &Data<Self::DataType>) -> LemmyResult<Self::Kind> {
|
||||
let post_list: Vec<ApubPost> = Post::list_for_community(&mut data.pool(), owner.id)
|
||||
let site = SiteView::read_local(&mut data.pool())
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
.ok_or(LemmyErrorType::LocalSiteNotSetup)?
|
||||
.site;
|
||||
|
||||
let post_views = PostQuery {
|
||||
community_id: Some(owner.id),
|
||||
sort: Some(SortType::New),
|
||||
limit: Some(FETCH_LIMIT_MAX),
|
||||
..Default::default()
|
||||
}
|
||||
.list(&site, &mut data.pool())
|
||||
.await?;
|
||||
|
||||
let mut ordered_items = vec![];
|
||||
for post in post_list {
|
||||
let person = Person::read(&mut data.pool(), post.creator_id)
|
||||
.await?
|
||||
.ok_or(LemmyErrorType::CouldntFindPerson)?
|
||||
.into();
|
||||
let create =
|
||||
CreateOrUpdatePage::new(post, &person, owner, CreateOrUpdateType::Create, data).await?;
|
||||
for post_view in post_views {
|
||||
let create = CreateOrUpdatePage::new(
|
||||
post_view.post.into(),
|
||||
&post_view.creator.into(),
|
||||
owner,
|
||||
CreateOrUpdateType::Create,
|
||||
data,
|
||||
)
|
||||
.await?;
|
||||
let announcable = AnnouncableActivities::CreateOrUpdatePost(create);
|
||||
let announce = AnnounceActivity::new(announcable.try_into()?, owner, data)?;
|
||||
ordered_items.push(announce);
|
||||
|
@ -94,7 +102,8 @@ impl Collection for ApubCommunityOutbox {
|
|||
// We intentionally ignore errors here. This is because the outbox might contain posts from old
|
||||
// Lemmy versions, or from other software which we cant parse. In that case, we simply skip the
|
||||
// item and only parse the ones that work.
|
||||
// process items in parallel, to avoid long delay from fetch_site_metadata() and other processing
|
||||
// process items in parallel, to avoid long delay from fetch_site_metadata() and other
|
||||
// processing
|
||||
join_all(outbox_activities.into_iter().map(|activity| {
|
||||
async {
|
||||
// Receiving announce requires at least one local community follower for anti spam purposes.
|
||||
|
|
|
@ -128,7 +128,14 @@ pub(crate) mod tests {
|
|||
use crate::protocol::objects::{group::Group, tombstone::Tombstone};
|
||||
use actix_web::body::to_bytes;
|
||||
use lemmy_db_schema::{
|
||||
source::{community::CommunityInsertForm, instance::Instance},
|
||||
newtypes::InstanceId,
|
||||
source::{
|
||||
community::CommunityInsertForm,
|
||||
instance::Instance,
|
||||
local_site::{LocalSite, LocalSiteInsertForm},
|
||||
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitInsertForm},
|
||||
site::{Site, SiteInsertForm},
|
||||
},
|
||||
traits::Crud,
|
||||
CommunityVisibility,
|
||||
};
|
||||
|
@ -142,6 +149,8 @@ pub(crate) mod tests {
|
|||
) -> LemmyResult<(Instance, Community)> {
|
||||
let instance =
|
||||
Instance::read_or_create(&mut context.pool(), "my_domain.tld".to_string()).await?;
|
||||
create_local_site(context, instance.id).await?;
|
||||
|
||||
let community_form = CommunityInsertForm::builder()
|
||||
.name("testcom6".to_string())
|
||||
.title("nada".to_owned())
|
||||
|
@ -154,6 +163,28 @@ pub(crate) mod tests {
|
|||
Ok((instance, community))
|
||||
}
|
||||
|
||||
/// Necessary for the community outbox fetching
|
||||
async fn create_local_site(
|
||||
context: &Data<LemmyContext>,
|
||||
instance_id: InstanceId,
|
||||
) -> LemmyResult<()> {
|
||||
// Create a local site, since this is necessary for community fetching.
|
||||
let site_form = SiteInsertForm::builder()
|
||||
.name("test site".to_string())
|
||||
.instance_id(instance_id)
|
||||
.build();
|
||||
let site = Site::create(&mut context.pool(), &site_form).await?;
|
||||
|
||||
let local_site_form = LocalSiteInsertForm::builder().site_id(site.id).build();
|
||||
let local_site = LocalSite::create(&mut context.pool(), &local_site_form).await?;
|
||||
let local_site_rate_limit_form = LocalSiteRateLimitInsertForm::builder()
|
||||
.local_site_id(local_site.id)
|
||||
.build();
|
||||
|
||||
LocalSiteRateLimit::create(&mut context.pool(), &local_site_rate_limit_form).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn decode_response<T: DeserializeOwned>(res: HttpResponse) -> LemmyResult<T> {
|
||||
let body = to_bytes(res.into_body()).await.unwrap();
|
||||
let body = std::str::from_utf8(&body)?;
|
||||
|
@ -164,6 +195,7 @@ pub(crate) mod tests {
|
|||
#[serial]
|
||||
async fn test_get_community() -> LemmyResult<()> {
|
||||
let context = LemmyContext::init_test_context().await;
|
||||
let (instance, community) = init(false, CommunityVisibility::Public, &context).await?;
|
||||
|
||||
// fetch invalid community
|
||||
let query = CommunityQuery {
|
||||
|
@ -172,8 +204,6 @@ pub(crate) mod tests {
|
|||
let res = get_apub_community_http(query.into(), context.reset_request_count()).await;
|
||||
assert!(res.is_err());
|
||||
|
||||
let (instance, community) = init(false, CommunityVisibility::Public, &context).await?;
|
||||
|
||||
// fetch valid community
|
||||
let query = CommunityQuery {
|
||||
community_name: community.name.clone(),
|
||||
|
|
|
@ -20,7 +20,8 @@ use lemmy_db_schema::{
|
|||
};
|
||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use std::{ops::Deref, time::Duration};
|
||||
use tokio::time::timeout;
|
||||
use url::Url;
|
||||
|
||||
mod comment;
|
||||
|
@ -30,13 +31,22 @@ mod post;
|
|||
pub mod routes;
|
||||
pub mod site;
|
||||
|
||||
const INCOMING_ACTIVITY_TIMEOUT: Duration = Duration::from_secs(9);
|
||||
|
||||
pub async fn shared_inbox(
|
||||
request: HttpRequest,
|
||||
body: Bytes,
|
||||
data: Data<LemmyContext>,
|
||||
) -> LemmyResult<HttpResponse> {
|
||||
receive_activity::<SharedInboxActivities, UserOrCommunity, LemmyContext>(request, body, &data)
|
||||
let receive_fut =
|
||||
receive_activity::<SharedInboxActivities, UserOrCommunity, LemmyContext>(request, body, &data);
|
||||
// Set a timeout shorter than `REQWEST_TIMEOUT` for processing incoming activities. This is to
|
||||
// avoid taking a long time to process an incoming activity when a required data fetch times out.
|
||||
// In this case our own instance would timeout and be marked as dead by the sender. Better to
|
||||
// consider the activity broken and move on.
|
||||
timeout(INCOMING_ACTIVITY_TIMEOUT, receive_fut)
|
||||
.await
|
||||
.map_err(|_| LemmyErrorType::InboxTimeout)?
|
||||
}
|
||||
|
||||
/// Convert the data to json and turn it into an HTTP Response with the correct ActivityPub
|
||||
|
|
|
@ -29,7 +29,9 @@ pub(crate) mod mentions;
|
|||
pub mod objects;
|
||||
pub mod protocol;
|
||||
|
||||
pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 50;
|
||||
/// Maximum number of outgoing HTTP requests to fetch a single object. Needs to be high enough
|
||||
/// to fetch a new community with posts, moderators and featured posts.
|
||||
pub const FEDERATION_HTTP_FETCH_LIMIT: u32 = 100;
|
||||
|
||||
/// 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
|
||||
|
@ -78,7 +80,10 @@ impl UrlVerifier for VerifyUrlData {
|
|||
/// - URL not being in the blocklist (if it is active)
|
||||
#[tracing::instrument(skip(local_site_data))]
|
||||
fn check_apub_id_valid(apub_id: &Url, local_site_data: &LocalSiteData) -> LemmyResult<()> {
|
||||
let domain = apub_id.domain().expect("apud id has domain").to_string();
|
||||
let domain = apub_id
|
||||
.domain()
|
||||
.ok_or(LemmyErrorType::UrlWithoutDomain)?
|
||||
.to_string();
|
||||
|
||||
if !local_site_data
|
||||
.local_site
|
||||
|
@ -158,7 +163,10 @@ pub(crate) async fn check_apub_id_valid_with_strictness(
|
|||
is_strict: bool,
|
||||
context: &LemmyContext,
|
||||
) -> LemmyResult<()> {
|
||||
let domain = apub_id.domain().expect("apud id has domain").to_string();
|
||||
let domain = apub_id
|
||||
.domain()
|
||||
.ok_or(LemmyErrorType::UrlWithoutDomain)?
|
||||
.to_string();
|
||||
let local_instance = context
|
||||
.settings()
|
||||
.get_hostname_without_port()
|
||||
|
@ -185,7 +193,10 @@ pub(crate) async fn check_apub_id_valid_with_strictness(
|
|||
.expect("local hostname is valid");
|
||||
allowed_and_local.push(local_instance);
|
||||
|
||||
let domain = apub_id.domain().expect("apud id has domain").to_string();
|
||||
let domain = apub_id
|
||||
.domain()
|
||||
.ok_or(LemmyErrorType::UrlWithoutDomain)?
|
||||
.to_string();
|
||||
if !allowed_and_local.contains(&domain) {
|
||||
Err(LemmyErrorType::FederationDisabledByStrictAllowList)?
|
||||
}
|
||||
|
|
|
@ -54,7 +54,10 @@ pub async fn collect_non_local_mentions(
|
|||
name: Some(format!(
|
||||
"@{}@{}",
|
||||
&parent_creator.name,
|
||||
&parent_creator.id().domain().expect("has domain")
|
||||
&parent_creator
|
||||
.id()
|
||||
.domain()
|
||||
.ok_or(LemmyErrorType::UrlWithoutDomain)?
|
||||
)),
|
||||
kind: MentionType::Mention,
|
||||
};
|
||||
|
|
|
@ -28,6 +28,7 @@ use lemmy_api_common::{
|
|||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
sensitive::SensitiveString,
|
||||
source::{
|
||||
activity::ActorType,
|
||||
actor_language::CommunityLanguage,
|
||||
|
@ -113,7 +114,7 @@ impl Object for ApubCommunity {
|
|||
featured: Some(generate_featured_url(&self.actor_id)?.into()),
|
||||
inbox: self.inbox_url.clone().into(),
|
||||
outbox: generate_outbox_url(&self.actor_id)?.into(),
|
||||
followers: self.followers_url.clone().into(),
|
||||
followers: self.followers_url.clone().map(Into::into),
|
||||
endpoints: self.shared_inbox_url.clone().map(|s| Endpoints {
|
||||
shared_inbox: s.into(),
|
||||
}),
|
||||
|
@ -164,7 +165,7 @@ impl Object for ApubCommunity {
|
|||
last_refreshed_at: Some(naive_now()),
|
||||
icon,
|
||||
banner,
|
||||
followers_url: Some(group.followers.clone().into()),
|
||||
followers_url: group.followers.clone().map(Into::into),
|
||||
inbox_url: Some(group.inbox.into()),
|
||||
shared_inbox_url: group.endpoints.map(|e| e.shared_inbox.into()),
|
||||
moderators_url: group.attributed_to.clone().map(Into::into),
|
||||
|
@ -187,11 +188,9 @@ impl Object for ApubCommunity {
|
|||
let context_ = context.reset_request_count();
|
||||
spawn_try_task(async move {
|
||||
group.outbox.dereference(&community_, &context_).await.ok();
|
||||
group
|
||||
.followers
|
||||
.dereference(&community_, &context_)
|
||||
.await
|
||||
.ok();
|
||||
if let Some(followers) = group.followers {
|
||||
followers.dereference(&community_, &context_).await.ok();
|
||||
}
|
||||
if let Some(featured) = group.featured {
|
||||
featured.dereference(&community_, &context_).await.ok();
|
||||
}
|
||||
|
@ -215,7 +214,7 @@ impl Actor for ApubCommunity {
|
|||
}
|
||||
|
||||
fn private_key_pem(&self) -> Option<String> {
|
||||
self.private_key.clone()
|
||||
self.private_key.clone().map(SensitiveString::into_inner)
|
||||
}
|
||||
|
||||
fn inbox(&self) -> Url {
|
||||
|
@ -275,7 +274,9 @@ pub(crate) mod tests {
|
|||
// change these links so they dont fetch over the network
|
||||
json.attributed_to = None;
|
||||
json.outbox = CollectionId::parse("https://enterprise.lemmy.ml/c/tenforward/not_outbox")?;
|
||||
json.followers = CollectionId::parse("https://enterprise.lemmy.ml/c/tenforward/not_followers")?;
|
||||
json.followers = Some(CollectionId::parse(
|
||||
"https://enterprise.lemmy.ml/c/tenforward/not_followers",
|
||||
)?);
|
||||
|
||||
let url = Url::parse("https://enterprise.lemmy.ml/c/tenforward")?;
|
||||
ApubCommunity::verify(&json, &url, &context2).await?;
|
||||
|
|
|
@ -29,6 +29,7 @@ use lemmy_api_common::{
|
|||
};
|
||||
use lemmy_db_schema::{
|
||||
newtypes::InstanceId,
|
||||
sensitive::SensitiveString,
|
||||
source::{
|
||||
activity::ActorType,
|
||||
actor_language::SiteLanguage,
|
||||
|
@ -45,6 +46,7 @@ use lemmy_utils::{
|
|||
markdown::markdown_to_html,
|
||||
slurs::{check_slurs, check_slurs_opt},
|
||||
},
|
||||
LemmyErrorType,
|
||||
};
|
||||
use std::ops::Deref;
|
||||
use tracing::debug;
|
||||
|
@ -99,7 +101,7 @@ impl Object for ApubSite {
|
|||
kind: ApplicationType::Application,
|
||||
id: self.id().into(),
|
||||
name: self.name.clone(),
|
||||
preferred_username: data.domain().to_string(),
|
||||
preferred_username: Some(data.domain().to_string()),
|
||||
content: self.sidebar.as_ref().map(|d| markdown_to_html(d)),
|
||||
source: self.sidebar.clone().map(Source::new),
|
||||
summary: self.description.clone(),
|
||||
|
@ -137,7 +139,11 @@ impl Object for ApubSite {
|
|||
|
||||
#[tracing::instrument(skip_all)]
|
||||
async fn from_json(apub: Self::Kind, context: &Data<Self::DataType>) -> LemmyResult<Self> {
|
||||
let domain = apub.id.inner().domain().expect("group id has domain");
|
||||
let domain = apub
|
||||
.id
|
||||
.inner()
|
||||
.domain()
|
||||
.ok_or(LemmyErrorType::UrlWithoutDomain)?;
|
||||
let instance = DbInstance::read_or_create(&mut context.pool(), domain.to_string()).await?;
|
||||
|
||||
let local_site = LocalSite::read(&mut context.pool()).await.ok();
|
||||
|
@ -182,7 +188,7 @@ impl Actor for ApubSite {
|
|||
}
|
||||
|
||||
fn private_key_pem(&self) -> Option<String> {
|
||||
self.private_key.clone()
|
||||
self.private_key.clone().map(SensitiveString::into_inner)
|
||||
}
|
||||
|
||||
fn inbox(&self) -> Url {
|
||||
|
@ -210,7 +216,9 @@ pub(in crate::objects) async fn fetch_instance_actor_for_object<T: Into<Url> + C
|
|||
Err(e) => {
|
||||
// Failed to fetch instance actor, its probably not a lemmy instance
|
||||
debug!("Failed to dereference site for {}: {}", &instance_id, e);
|
||||
let domain = instance_id.domain().expect("has domain");
|
||||
let domain = instance_id
|
||||
.domain()
|
||||
.ok_or(LemmyErrorType::UrlWithoutDomain)?;
|
||||
Ok(
|
||||
DbInstance::read_or_create(&mut context.pool(), domain.to_string())
|
||||
.await?
|
||||
|
|
|
@ -30,6 +30,7 @@ use lemmy_api_common::{
|
|||
},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
sensitive::SensitiveString,
|
||||
source::{
|
||||
activity::ActorType,
|
||||
local_site::LocalSite,
|
||||
|
@ -200,7 +201,7 @@ impl Actor for ApubPerson {
|
|||
}
|
||||
|
||||
fn private_key_pem(&self) -> Option<String> {
|
||||
self.private_key.clone()
|
||||
self.private_key.clone().map(SensitiveString::into_inner)
|
||||
}
|
||||
|
||||
fn inbox(&self) -> Url {
|
||||
|
|
|
@ -25,18 +25,12 @@ use html2text::{from_read_with_decorator, render::text_renderer::TrivialDecorato
|
|||
use lemmy_api_common::{
|
||||
context::LemmyContext,
|
||||
request::generate_post_link_metadata,
|
||||
utils::{
|
||||
get_url_blocklist,
|
||||
local_site_opt_to_slur_regex,
|
||||
process_markdown_opt,
|
||||
proxy_image_link_opt_apub,
|
||||
},
|
||||
utils::{get_url_blocklist, local_site_opt_to_slur_regex, process_markdown_opt},
|
||||
};
|
||||
use lemmy_db_schema::{
|
||||
source::{
|
||||
community::Community,
|
||||
local_site::LocalSite,
|
||||
moderator::{ModLockPost, ModLockPostForm},
|
||||
person::Person,
|
||||
post::{Post, PostInsertForm, PostUpdateForm},
|
||||
},
|
||||
|
@ -46,6 +40,7 @@ use lemmy_db_schema::{
|
|||
use lemmy_db_views_actor::structs::CommunityModeratorView;
|
||||
use lemmy_utils::{
|
||||
error::{LemmyError, LemmyErrorType, LemmyResult},
|
||||
spawn_try_task,
|
||||
utils::{markdown::markdown_to_html, slurs::check_slurs_opt, validation::check_url_scheme},
|
||||
};
|
||||
use std::ops::Deref;
|
||||
|
@ -147,7 +142,6 @@ impl Object for ApubPost {
|
|||
source: self.body.clone().map(Source::new),
|
||||
attachment,
|
||||
image: self.thumbnail_url.clone().map(ImageObject::new),
|
||||
comments_enabled: Some(!self.locked),
|
||||
sensitive: Some(self.nsfw),
|
||||
language,
|
||||
published: Some(self.published),
|
||||
|
@ -165,12 +159,8 @@ impl Object for ApubPost {
|
|||
expected_domain: &Url,
|
||||
context: &Data<Self::DataType>,
|
||||
) -> LemmyResult<()> {
|
||||
// We can't verify the domain in case of mod action, because the mod may be on a different
|
||||
// instance from the post author.
|
||||
if !page.is_mod_action(context).await? {
|
||||
verify_domains_match(page.id.inner(), expected_domain)?;
|
||||
verify_is_remote_object(&page.id, context)?;
|
||||
};
|
||||
|
||||
let community = page.community(context).await?;
|
||||
check_apub_id_valid_with_strictness(page.id.inner(), community.local, context).await?;
|
||||
|
@ -218,13 +208,9 @@ impl Object for ApubPost {
|
|||
name = name.chars().take(MAX_TITLE_LENGTH).collect();
|
||||
}
|
||||
|
||||
// read existing, local post if any (for generating mod log)
|
||||
let old_post = page.id.dereference_local(context).await;
|
||||
|
||||
let first_attachment = page.attachment.first();
|
||||
let local_site = LocalSite::read(&mut context.pool()).await.ok();
|
||||
|
||||
let form = if !page.is_mod_action(context).await? {
|
||||
let url = if let Some(attachment) = first_attachment.cloned() {
|
||||
Some(attachment.url())
|
||||
} else if page.kind == PageType::Video {
|
||||
|
@ -233,12 +219,13 @@ impl Object for ApubPost {
|
|||
} else {
|
||||
None
|
||||
};
|
||||
check_url_scheme(&url)?;
|
||||
|
||||
if let Some(url) = &url {
|
||||
check_url_scheme(url)?;
|
||||
}
|
||||
|
||||
let alt_text = first_attachment.cloned().and_then(Attachment::alt_text);
|
||||
|
||||
let url = proxy_image_link_opt_apub(url, context).await?;
|
||||
|
||||
let slur_regex = &local_site_opt_to_slur_regex(&local_site);
|
||||
let url_blocklist = get_url_blocklist(context).await?;
|
||||
|
||||
|
@ -247,14 +234,13 @@ impl Object for ApubPost {
|
|||
let language_id =
|
||||
LanguageTag::to_language_id_single(page.language, &mut context.pool()).await?;
|
||||
|
||||
PostInsertForm::builder()
|
||||
let form = PostInsertForm::builder()
|
||||
.name(name)
|
||||
.url(url.map(Into::into))
|
||||
.body(body)
|
||||
.alt_text(alt_text)
|
||||
.creator_id(creator.id)
|
||||
.community_id(community.id)
|
||||
.locked(page.comments_enabled.map(|e| !e))
|
||||
.published(page.published.map(Into::into))
|
||||
.updated(page.updated.map(Into::into))
|
||||
.deleted(Some(false))
|
||||
|
@ -262,40 +248,18 @@ impl Object for ApubPost {
|
|||
.ap_id(Some(page.id.clone().into()))
|
||||
.local(Some(false))
|
||||
.language_id(language_id)
|
||||
.build()
|
||||
} else {
|
||||
// if is mod action, only update locked/stickied fields, nothing else
|
||||
PostInsertForm::builder()
|
||||
.name(name)
|
||||
.creator_id(creator.id)
|
||||
.community_id(community.id)
|
||||
.ap_id(Some(page.id.clone().into()))
|
||||
.locked(page.comments_enabled.map(|e| !e))
|
||||
.updated(page.updated.map(Into::into))
|
||||
.build()
|
||||
};
|
||||
.build();
|
||||
|
||||
let timestamp = page.updated.or(page.published).unwrap_or_else(naive_now);
|
||||
let post = Post::insert_apub(&mut context.pool(), timestamp, &form).await?;
|
||||
let post_ = post.clone();
|
||||
let context_ = context.reset_request_count();
|
||||
|
||||
generate_post_link_metadata(
|
||||
post.clone(),
|
||||
None,
|
||||
page.image.map(|i| i.url),
|
||||
|_| None,
|
||||
local_site,
|
||||
context.reset_request_count(),
|
||||
);
|
||||
|
||||
// write mod log entry for lock
|
||||
if Page::is_locked_changed(&old_post, &page.comments_enabled) {
|
||||
let form = ModLockPostForm {
|
||||
mod_person_id: creator.id,
|
||||
post_id: post.id,
|
||||
locked: Some(post.locked),
|
||||
};
|
||||
ModLockPost::create(&mut context.pool(), &form).await?;
|
||||
}
|
||||
// Generates a post thumbnail in background task, because some sites can be very slow to
|
||||
// respond.
|
||||
spawn_try_task(async move {
|
||||
generate_post_link_metadata(post_, None, |_| None, local_site, context_).await
|
||||
});
|
||||
|
||||
Ok(post.into())
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@ pub struct DeleteUser {
|
|||
#[serde(deserialize_with = "deserialize_one_or_many", default)]
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub(crate) cc: Vec<Url>,
|
||||
/// Nonstandard field. If present, all content from the user should be deleted along with the account
|
||||
/// Nonstandard field. If present, all content from the user should be deleted along with the
|
||||
/// account
|
||||
pub(crate) remove_data: Option<bool>,
|
||||
}
|
||||
|
|
|
@ -96,4 +96,10 @@ mod tests {
|
|||
test_json::<Report>("assets/mbin/activities/flag.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_wordpress_activities() -> LemmyResult<()> {
|
||||
test_json::<AnnounceActivity>("assets/wordpress/activities/announce.json")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ pub struct Group {
|
|||
/// username, set at account creation and usually fixed after that
|
||||
pub(crate) preferred_username: String,
|
||||
pub(crate) inbox: Url,
|
||||
pub(crate) followers: CollectionId<ApubCommunityFollower>,
|
||||
pub(crate) followers: Option<CollectionId<ApubCommunityFollower>>,
|
||||
pub(crate) public_key: PublicKey,
|
||||
|
||||
/// title
|
||||
|
|
|
@ -22,7 +22,7 @@ pub struct Instance {
|
|||
/// site name
|
||||
pub(crate) name: String,
|
||||
/// instance domain, necessary for mastodon authorized fetch
|
||||
pub(crate) preferred_username: String,
|
||||
pub(crate) preferred_username: Option<String>,
|
||||
pub(crate) inbox: Url,
|
||||
/// mandatory field in activitypub, lemmy currently serves an empty outbox
|
||||
pub(crate) outbox: Url,
|
||||
|
|
|
@ -190,4 +190,29 @@ mod tests {
|
|||
test_json::<Person>("assets/mobilizon/objects/person.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_object_discourse() -> LemmyResult<()> {
|
||||
test_json::<Group>("assets/discourse/objects/group.json")?;
|
||||
test_json::<Page>("assets/discourse/objects/page.json")?;
|
||||
test_json::<Person>("assets/discourse/objects/person.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_object_nodebb() -> LemmyResult<()> {
|
||||
test_json::<Group>("assets/nodebb/objects/group.json")?;
|
||||
test_json::<Page>("assets/nodebb/objects/page.json")?;
|
||||
test_json::<Person>("assets/nodebb/objects/person.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_object_wordpress() -> LemmyResult<()> {
|
||||
test_json::<Group>("assets/wordpress/objects/group.json")?;
|
||||
test_json::<Page>("assets/wordpress/objects/page.json")?;
|
||||
test_json::<Person>("assets/wordpress/objects/person.json")?;
|
||||
test_json::<Note>("assets/wordpress/objects/note.json")?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ pub struct Page {
|
|||
pub(crate) kind: PageType,
|
||||
pub(crate) id: ObjectId<ApubPost>,
|
||||
pub(crate) attributed_to: AttributedTo,
|
||||
#[serde(deserialize_with = "deserialize_one_or_many")]
|
||||
#[serde(deserialize_with = "deserialize_one_or_many", default)]
|
||||
pub(crate) to: Vec<Url>,
|
||||
// If there is inReplyTo field this is actually a comment and must not be parsed
|
||||
#[serde(deserialize_with = "deserialize_not_present", default)]
|
||||
|
@ -60,7 +60,6 @@ pub struct Page {
|
|||
#[serde(default)]
|
||||
pub(crate) attachment: Vec<Attachment>,
|
||||
pub(crate) image: Option<ImageObject>,
|
||||
pub(crate) comments_enabled: Option<bool>,
|
||||
pub(crate) sensitive: Option<bool>,
|
||||
pub(crate) published: Option<DateTime<Utc>>,
|
||||
pub(crate) updated: Option<DateTime<Utc>>,
|
||||
|
@ -156,28 +155,6 @@ pub enum HashtagType {
|
|||
}
|
||||
|
||||
impl Page {
|
||||
/// Only mods can change the post's locked status. So if it is changed from the default value,
|
||||
/// it is a mod action and needs to be verified as such.
|
||||
///
|
||||
/// Locked needs to be false on a newly created post (verified in [[CreatePost]].
|
||||
pub(crate) async fn is_mod_action(&self, context: &Data<LemmyContext>) -> LemmyResult<bool> {
|
||||
let old_post = self.id.clone().dereference_local(context).await;
|
||||
Ok(Page::is_locked_changed(&old_post, &self.comments_enabled))
|
||||
}
|
||||
|
||||
pub(crate) fn is_locked_changed<E>(
|
||||
old_post: &Result<ApubPost, E>,
|
||||
new_comments_enabled: &Option<bool>,
|
||||
) -> bool {
|
||||
if let Some(new_comments_enabled) = new_comments_enabled {
|
||||
if let Ok(old_post) = old_post {
|
||||
return new_comments_enabled != &!old_post.locked;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
pub(crate) fn creator(&self) -> LemmyResult<ObjectId<ApubPerson>> {
|
||||
match &self.attributed_to {
|
||||
AttributedTo::Lemmy(l) => Ok(l.clone()),
|
||||
|
@ -233,6 +210,10 @@ impl ActivityHandler for Page {
|
|||
#[async_trait::async_trait]
|
||||
impl InCommunity for Page {
|
||||
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
|
||||
if let Some(audience) = &self.audience {
|
||||
return audience.dereference(context).await;
|
||||
}
|
||||
|
||||
let community = match &self.attributed_to {
|
||||
AttributedTo::Lemmy(_) => {
|
||||
let mut iter = self.to.iter().merge(self.cc.iter());
|
||||
|
@ -243,7 +224,7 @@ impl InCommunity for Page {
|
|||
break c;
|
||||
}
|
||||
} else {
|
||||
Err(LemmyErrorType::NoCommunityFoundInCc)?
|
||||
Err(LemmyErrorType::CouldntFindCommunity)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -251,11 +232,12 @@ impl InCommunity for Page {
|
|||
p.iter()
|
||||
.find(|a| a.kind == PersonOrGroupType::Group)
|
||||
.map(|a| ObjectId::<ApubCommunity>::from(a.id.clone().into_inner()))
|
||||
.ok_or(LemmyErrorType::PageDoesNotSpecifyGroup)?
|
||||
.ok_or(LemmyErrorType::CouldntFindCommunity)?
|
||||
.dereference(context)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(audience) = &self.audience {
|
||||
verify_community_matches(audience, community.actor_id.clone())?;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
[package]
|
||||
name = "lemmy_db_perf"
|
||||
publish = false
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
description.workspace = true
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue