Merge branch 'fix-dupe-activity-sending' into federation-send-parallel

federation-send-parallel
phiresky 2024-05-29 15:38:53 +02:00
commit 7cb4e8222c
284 changed files with 9452 additions and 5109 deletions

View File

@ -3,3 +3,5 @@ edition = "2021"
imports_layout = "HorizontalVertical" imports_layout = "HorizontalVertical"
imports_granularity = "Crate" imports_granularity = "Crate"
group_imports = "One" group_imports = "One"
wrap_comments = true
comment_width = 100

View File

@ -3,6 +3,7 @@
variables: variables:
- &rust_image "rust:1.77" - &rust_image "rust:1.77"
- &rust_nightly_image "rustlang/rust:nightly"
- &install_pnpm "corepack enable pnpm" - &install_pnpm "corepack enable pnpm"
- &slow_check_paths - &slow_check_paths
- event: pull_request - event: pull_request
@ -24,15 +25,17 @@ variables:
"diesel.toml", "diesel.toml",
".gitmodules", ".gitmodules",
] ]
- install_binstall: &install_binstall
# Broken for cron jobs currently, see - wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
# https://github.com/woodpecker-ci/woodpecker/issues/1716 - tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
# clone: - cp cargo-binstall /usr/local/cargo/bin
# git: - install_diesel_cli: &install_diesel_cli
# image: woodpeckerci/plugin-git - apt update && apt install -y lsb-release build-essential
# settings: - sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
# recursive: true - wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
# submodule_update_remote: true - apt update && apt install -y postgresql-client-16
- cargo install diesel_cli --no-default-features --features postgres
- export PATH="$CARGO_HOME/bin:$PATH"
steps: steps:
prepare_repo: prepare_repo:
@ -66,7 +69,7 @@ steps:
- event: pull_request - event: pull_request
cargo_fmt: cargo_fmt:
image: rustlang/rust:nightly image: *rust_nightly_image
environment: environment:
# store cargo data in repo folder so that it gets cached between steps # store cargo data in repo folder so that it gets cached between steps
CARGO_HOME: .cargo_home CARGO_HOME: .cargo_home
@ -77,11 +80,9 @@ steps:
- event: pull_request - event: pull_request
cargo_machete: cargo_machete:
image: rustlang/rust:nightly image: *rust_nightly_image
commands: commands:
- wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz - <<: *install_binstall
- tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
- cp cargo-binstall /usr/local/cargo/bin
- cargo binstall -y cargo-machete - cargo binstall -y cargo-machete
- cargo machete - cargo machete
when: when:
@ -133,26 +134,17 @@ steps:
when: *slow_check_paths when: *slow_check_paths
check_diesel_schema: check_diesel_schema:
image: willsquire/diesel-cli image: *rust_image
environment: environment:
CARGO_HOME: .cargo_home CARGO_HOME: .cargo_home
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
commands: commands:
- <<: *install_diesel_cli
- diesel migration run - diesel migration run
- diesel print-schema --config-file=diesel.toml > tmp.schema - diesel print-schema --config-file=diesel.toml > tmp.schema
- diff tmp.schema crates/db_schema/src/schema.rs - diff tmp.schema crates/db_schema/src/schema.rs
when: *slow_check_paths when: *slow_check_paths
check_diesel_migration_revertable:
image: willsquire/diesel-cli
environment:
CARGO_HOME: .cargo_home
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
commands:
- diesel migration run
- diesel migration redo
when: *slow_check_paths
check_db_perf_tool: check_db_perf_tool:
image: *rust_image image: *rust_image
environment: environment:
@ -194,6 +186,44 @@ steps:
- cargo test --workspace --no-fail-fast - cargo test --workspace --no-fail-fast
when: *slow_check_paths when: *slow_check_paths
check_diesel_migration:
# TODO: use willsquire/diesel-cli image when shared libraries become optional in lemmy_server
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
PGUSER: lemmy
PGPASSWORD: password
PGHOST: database
PGDATABASE: lemmy
commands:
# 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)
- apt update && apt install -y lsb-release
- sh -c 'echo "deb https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
- wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
- apt update && apt install -y postgresql-client-16
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f before.sqldump
# Make sure that the newest migration is revertable without the `r` schema
- diesel migration redo
# Run schema setup twice, which fails on the 2nd time if `DROP SCHEMA IF EXISTS r CASCADE` drops the wrong things
- alias lemmy_schema_setup="target/lemmy_server --disable-scheduled-tasks --disable-http-server --disable-activity-sending"
- lemmy_schema_setup
- lemmy_schema_setup
# Make sure that the newest migration is revertable with the `r` schema
- diesel migration redo
# Check for changes in the schema, which would be caused by an incorrect migration
- psql -c "DROP SCHEMA IF EXISTS r CASCADE;"
- pg_dump --no-owner --no-privileges --no-table-access-method --schema-only --no-sync -f after.sqldump
- diff before.sqldump after.sqldump
when: *slow_check_paths
run_federation_tests: run_federation_tests:
image: node:20-bookworm-slim image: node:20-bookworm-slim
environment: environment:
@ -248,10 +278,11 @@ steps:
publish_to_crates_io: publish_to_crates_io:
image: *rust_image image: *rust_image
commands: commands:
- cargo install cargo-workspaces - <<: *install_binstall
# Install cargo-workspaces
- cargo binstall -y cargo-workspaces
- cp -r migrations crates/db_schema/ - cp -r migrations crates/db_schema/
- cargo login "$CARGO_API_TOKEN" - cargo workspaces publish --token "$CARGO_API_TOKEN" --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
- cargo workspaces publish --from-git --allow-dirty --no-verify --allow-branch "${CI_COMMIT_TAG}" --yes custom "${CI_COMMIT_TAG}"
secrets: [cargo_api_token] secrets: [cargo_api_token]
when: when:
- event: tag - event: tag

2232
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
[workspace.package] [workspace.package]
version = "0.19.4-beta.3" version = "0.19.4-rc.3"
edition = "2021" edition = "2021"
description = "A link aggregator for the fediverse" description = "A link aggregator for the fediverse"
license = "AGPL-3.0" license = "AGPL-3.0"
@ -67,8 +67,8 @@ members = [
[workspace.lints.clippy] [workspace.lints.clippy]
cast_lossless = "deny" cast_lossless = "deny"
complexity = "deny" complexity = { level = "deny", priority = -1 }
correctness = "deny" correctness = { level = "deny", priority = -1 }
dbg_macro = "deny" dbg_macro = "deny"
explicit_into_iter_loop = "deny" explicit_into_iter_loop = "deny"
explicit_iter_loop = "deny" explicit_iter_loop = "deny"
@ -79,37 +79,37 @@ inefficient_to_string = "deny"
items-after-statements = "deny" items-after-statements = "deny"
manual_string_new = "deny" manual_string_new = "deny"
needless_collect = "deny" needless_collect = "deny"
perf = "deny" perf = { level = "deny", priority = -1 }
redundant_closure_for_method_calls = "deny" redundant_closure_for_method_calls = "deny"
style = "deny" style = { level = "deny", priority = -1 }
suspicious = "deny" suspicious = { level = "deny", priority = -1 }
uninlined_format_args = "allow" uninlined_format_args = "allow"
unused_self = "deny" unused_self = "deny"
unwrap_used = "deny" unwrap_used = "deny"
[workspace.dependencies] [workspace.dependencies]
lemmy_api = { version = "=0.19.4-beta.3", path = "./crates/api" } lemmy_api = { version = "=0.19.4-rc.3", path = "./crates/api" }
lemmy_api_crud = { version = "=0.19.4-beta.3", path = "./crates/api_crud" } lemmy_api_crud = { version = "=0.19.4-rc.3", path = "./crates/api_crud" }
lemmy_apub = { version = "=0.19.4-beta.3", path = "./crates/apub" } lemmy_apub = { version = "=0.19.4-rc.3", path = "./crates/apub" }
lemmy_utils = { version = "=0.19.4-beta.3", path = "./crates/utils", default-features = false } lemmy_utils = { version = "=0.19.4-rc.3", path = "./crates/utils", default-features = false }
lemmy_db_schema = { version = "=0.19.4-beta.3", path = "./crates/db_schema" } lemmy_db_schema = { version = "=0.19.4-rc.3", path = "./crates/db_schema" }
lemmy_api_common = { version = "=0.19.4-beta.3", path = "./crates/api_common" } lemmy_api_common = { version = "=0.19.4-rc.3", path = "./crates/api_common" }
lemmy_routes = { version = "=0.19.4-beta.3", path = "./crates/routes" } lemmy_routes = { version = "=0.19.4-rc.3", path = "./crates/routes" }
lemmy_db_views = { version = "=0.19.4-beta.3", path = "./crates/db_views" } lemmy_db_views = { version = "=0.19.4-rc.3", path = "./crates/db_views" }
lemmy_db_views_actor = { version = "=0.19.4-beta.3", path = "./crates/db_views_actor" } lemmy_db_views_actor = { version = "=0.19.4-rc.3", path = "./crates/db_views_actor" }
lemmy_db_views_moderator = { version = "=0.19.4-beta.3", path = "./crates/db_views_moderator" } lemmy_db_views_moderator = { version = "=0.19.4-rc.3", path = "./crates/db_views_moderator" }
lemmy_federate = { version = "=0.19.4-beta.3", path = "./crates/federate" } lemmy_federate = { version = "=0.19.4-rc.3", path = "./crates/federate" }
activitypub_federation = { version = "0.5.4", default-features = false, features = [ activitypub_federation = { version = "0.5.6", default-features = false, features = [
"actix-web", "actix-web",
] } ] }
diesel = "2.1.4" diesel = "2.1.6"
diesel_migrations = "2.1.0" diesel_migrations = "2.1.0"
diesel-async = "0.4.1" diesel-async = "0.4.1"
serde = { version = "1.0.197", features = ["derive"] } serde = { version = "1.0.202", features = ["derive"] }
serde_with = "3.7.0" serde_with = "3.8.1"
actix-web = { version = "4.5.1", default-features = false, features = [ actix-web = { version = "4.6.0", default-features = false, features = [
"macros", "macros",
"rustls", "rustls-0_23",
"compress-brotli", "compress-brotli",
"compress-gzip", "compress-gzip",
"compress-zstd", "compress-zstd",
@ -121,32 +121,32 @@ tracing-error = "0.2.0"
tracing-log = "0.2.0" tracing-log = "0.2.0"
tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } tracing-subscriber = { version = "0.3.18", features = ["env-filter"] }
url = { version = "2.5.0", features = ["serde"] } url = { version = "2.5.0", features = ["serde"] }
reqwest = { version = "0.11.26", features = ["json", "blocking", "gzip"] } reqwest = { version = "0.11.27", features = ["json", "blocking", "gzip"] }
reqwest-middleware = "0.2.4" reqwest-middleware = "0.2.5"
reqwest-tracing = "0.4.7" reqwest-tracing = "0.4.8"
clokwerk = "0.4.0" clokwerk = "0.4.0"
doku = { version = "0.21.1", features = ["url-2"] } doku = { version = "0.21.1", features = ["url-2"] }
bcrypt = "0.15.0" bcrypt = "0.15.1"
chrono = { version = "0.4.35", features = ["serde"], default-features = false } chrono = { version = "0.4.38", features = ["serde"], default-features = false }
serde_json = { version = "1.0.114", features = ["preserve_order"] } serde_json = { version = "1.0.117", features = ["preserve_order"] }
base64 = "0.21.7" base64 = "0.22.1"
uuid = { version = "1.7.0", features = ["serde", "v4"] } uuid = { version = "1.8.0", features = ["serde", "v4"] }
async-trait = "0.1.77" async-trait = "0.1.80"
captcha = "0.0.9" captcha = "0.0.9"
anyhow = { version = "1.0.81", features = [ anyhow = { version = "1.0.86", features = [
"backtrace", "backtrace",
] } # backtrace is on by default on nightly, but not stable rust ] } # backtrace is on by default on nightly, but not stable rust
diesel_ltree = "0.3.1" diesel_ltree = "0.3.1"
typed-builder = "0.18.1" typed-builder = "0.18.2"
serial_test = "2.0.0" serial_test = "3.1.1"
tokio = { version = "1.37.0", features = ["full"] } tokio = { version = "1.37.0", features = ["full"] }
regex = "1.10.3" regex = "1.10.4"
once_cell = "1.19.0" 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"] } diesel-derive-enum = { version = "2.1.0", features = ["postgres"] }
strum = "0.25.0" strum = "0.26.2"
strum_macros = "0.25.3" strum_macros = "0.26.2"
itertools = "0.12.1" itertools = "0.13.0"
futures = "0.3.30" futures = "0.3.30"
http = "0.2.12" http = "0.2.12"
rosetta-i18n = "0.1.3" rosetta-i18n = "0.1.3"
@ -157,15 +157,15 @@ ts-rs = { version = "7.1.1", features = [
"chrono-impl", "chrono-impl",
"no-serde-warnings", "no-serde-warnings",
] } ] }
rustls = { version = "0.21.10", features = ["dangerous_configuration"] } rustls = { version = "0.23.8", features = ["ring"] }
futures-util = "0.3.30" futures-util = "0.3.30"
tokio-postgres = "0.7.10" tokio-postgres = "0.7.10"
tokio-postgres-rustls = "0.10.0" tokio-postgres-rustls = "0.12.0"
urlencoding = "2.1.3" urlencoding = "2.1.3"
enum-map = "2.7" enum-map = "2.7"
moka = { version = "0.12.5", features = ["future"] } moka = { version = "0.12.7", features = ["future"] }
i-love-jesus = { version = "0.1.0" } i-love-jesus = { version = "0.1.0" }
clap = { version = "4.5.2", features = ["derive"] } clap = { version = "4.5.4", features = ["derive", "env"] }
pretty_assertions = "1.4.0" pretty_assertions = "1.4.0"
[dependencies] [dependencies]
@ -196,15 +196,15 @@ tracing-opentelemetry = { workspace = true, optional = true }
opentelemetry = { workspace = true, optional = true } opentelemetry = { workspace = true, optional = true }
console-subscriber = { version = "0.1.10", optional = true } console-subscriber = { version = "0.1.10", optional = true }
opentelemetry-otlp = { version = "0.12.0", optional = true } opentelemetry-otlp = { version = "0.12.0", optional = true }
pict-rs = { version = "0.5.9", optional = true } pict-rs = { version = "0.5.14", optional = true }
tokio.workspace = true tokio.workspace = true
actix-cors = "0.6.5" actix-cors = "0.7.0"
futures-util = { workspace = true } futures-util = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
prometheus = { version = "0.13.3", features = ["process"] } prometheus = { version = "0.13.4", features = ["process"] }
serial_test = { workspace = true } serial_test = { workspace = true }
clap = { workspace = true } clap = { workspace = true }
actix-web-prom = "0.7.0" actix-web-prom = "0.8.0"
[dev-dependencies] [dev-dependencies]
pretty_assertions = { workspace = true } pretty_assertions = { workspace = true }

1
api_tests/.npmrc 100644
View File

@ -0,0 +1 @@
package-manager-strict=false

View File

@ -6,10 +6,11 @@
"repository": "https://github.com/LemmyNet/lemmy", "repository": "https://github.com/LemmyNet/lemmy",
"author": "Dessalines", "author": "Dessalines",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"packageManager": "pnpm@9.1.1+sha256.9551e803dcb7a1839fdf5416153a844060c7bce013218ce823410532504ac10b",
"scripts": { "scripts": {
"lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'", "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src && prettier --check 'src/**/*.ts'",
"fix": "prettier --write src && eslint --fix src", "fix": "prettier --write src && eslint --fix src",
"api-test": "jest -i follow.spec.ts && jest -i 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-follow": "jest -i follow.spec.ts",
"api-test-comment": "jest -i comment.spec.ts", "api-test-comment": "jest -i comment.spec.ts",
"api-test-post": "jest -i post.spec.ts", "api-test-post": "jest -i post.spec.ts",
@ -27,7 +28,7 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0", "jest": "^29.5.0",
"lemmy-js-client": "0.19.4-alpha.16", "lemmy-js-client": "0.19.4-alpha.18",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "^5.4.4" "typescript": "^5.4.4"

File diff suppressed because it is too large Load Diff

View File

@ -3,19 +3,19 @@
# it is expected that this script is called by run-federation-test.sh script. # it is expected that this script is called by run-federation-test.sh script.
set -e set -e
if [ -n "$LEMMY_LOG_LEVEL" ]; if [ -z "$LEMMY_LOG_LEVEL" ];
then then
LEMMY_LOG_LEVEL=warn LEMMY_LOG_LEVEL=info
fi fi
export RUST_BACKTRACE=1 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 export LEMMY_TEST_FAST_FEDERATION=1 # by default, the persistent federation queue has delays in the scale of 30s-5min
# pictrs setup # pictrs setup
if [ ! -f "api_tests/pict-rs" ]; then if [ ! -f "api_tests/pict-rs" ]; then
curl "https://git.asonix.dog/asonix/pict-rs/releases/download/v0.5.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 chmod +x api_tests/pict-rs
fi fi
./api_tests/pict-rs \ ./api_tests/pict-rs \

View File

@ -45,7 +45,6 @@ let postOnAlphaRes: PostResponse;
beforeAll(async () => { beforeAll(async () => {
await setupLogins(); await setupLogins();
await unfollows();
await Promise.all([followBeta(alpha), followBeta(gamma)]); await Promise.all([followBeta(alpha), followBeta(gamma)]);
betaCommunity = (await resolveBetaCommunity(alpha)).community; betaCommunity = (await resolveBetaCommunity(alpha)).community;
if (betaCommunity) { if (betaCommunity) {

View File

@ -242,7 +242,7 @@ test("Admin actions in remote community are not federated to origin", async () =
); );
expect(banRes.banned).toBe(true); expect(banRes.banned).toBe(true);
// ban doesnt federate to community's origin instance alpha // ban doesn't federate to community's origin instance alpha
let alphaPost = (await resolvePost(alpha, gammaPost.post)).post; let alphaPost = (await resolvePost(alpha, gammaPost.post)).post;
expect(alphaPost?.creator_banned_from_community).toBe(false); expect(alphaPost?.creator_banned_from_community).toBe(false);
@ -380,8 +380,8 @@ test("User blocks instance, communities are hidden", async () => {
test("Community follower count is federated", async () => { test("Community follower count is federated", async () => {
// Follow the beta community from alpha // Follow the beta community from alpha
let community = await createCommunity(beta); let community = await createCommunity(beta);
let community_id = community.community_view.community.actor_id; let communityActorId = community.community_view.community.actor_id;
let resolved = await resolveCommunity(alpha, community_id); let resolved = await resolveCommunity(alpha, communityActorId);
if (!resolved.community) { if (!resolved.community) {
throw "Missing beta community"; throw "Missing beta community";
} }
@ -389,7 +389,7 @@ test("Community follower count is federated", async () => {
await followCommunity(alpha, true, resolved.community.community.id); await followCommunity(alpha, true, resolved.community.community.id);
let followed = ( let followed = (
await waitUntil( await waitUntil(
() => resolveCommunity(alpha, community_id), () => resolveCommunity(alpha, communityActorId),
c => c.community?.subscribed === "Subscribed", c => c.community?.subscribed === "Subscribed",
) )
).community; ).community;
@ -398,7 +398,7 @@ test("Community follower count is federated", async () => {
expect(followed?.counts.subscribers).toBe(1); expect(followed?.counts.subscribers).toBe(1);
// Follow the community from gamma // Follow the community from gamma
resolved = await resolveCommunity(gamma, community_id); resolved = await resolveCommunity(gamma, communityActorId);
if (!resolved.community) { if (!resolved.community) {
throw "Missing beta community"; throw "Missing beta community";
} }
@ -406,7 +406,7 @@ test("Community follower count is federated", async () => {
await followCommunity(gamma, true, resolved.community.community.id); await followCommunity(gamma, true, resolved.community.community.id);
followed = ( followed = (
await waitUntil( await waitUntil(
() => resolveCommunity(gamma, community_id), () => resolveCommunity(gamma, communityActorId),
c => c.community?.subscribed === "Subscribed", c => c.community?.subscribed === "Subscribed",
) )
).community; ).community;
@ -415,7 +415,7 @@ test("Community follower count is federated", async () => {
expect(followed?.counts?.subscribers).toBe(2); expect(followed?.counts?.subscribers).toBe(2);
// Follow the community from delta // Follow the community from delta
resolved = await resolveCommunity(delta, community_id); resolved = await resolveCommunity(delta, communityActorId);
if (!resolved.community) { if (!resolved.community) {
throw "Missing beta community"; throw "Missing beta community";
} }
@ -423,7 +423,7 @@ test("Community follower count is federated", async () => {
await followCommunity(delta, true, resolved.community.community.id); await followCommunity(delta, true, resolved.community.community.id);
followed = ( followed = (
await waitUntil( await waitUntil(
() => resolveCommunity(delta, community_id), () => resolveCommunity(delta, communityActorId),
c => c.community?.subscribed === "Subscribed", c => c.community?.subscribed === "Subscribed",
) )
).community; ).community;
@ -452,7 +452,7 @@ test("Dont receive community activities after unsubscribe", async () => {
); );
expect(communityRes1.community_view.counts.subscribers).toBe(2); expect(communityRes1.community_view.counts.subscribers).toBe(2);
// temporarily block alpha, so that it doesnt know about unfollow // temporarily block alpha, so that it doesn't know about unfollow
let editSiteForm: EditSite = {}; let editSiteForm: EditSite = {};
editSiteForm.allowed_instances = ["lemmy-epsilon"]; editSiteForm.allowed_instances = ["lemmy-epsilon"];
await beta.editSite(editSiteForm); await beta.editSite(editSiteForm);
@ -513,7 +513,7 @@ test("Fetch community, includes posts", async () => {
expect(post_listing.posts[0].post.ap_id).toBe(postRes.post_view.post.ap_id); expect(post_listing.posts[0].post.ap_id).toBe(postRes.post_view.post.ap_id);
}); });
test("Content in local-only community doesnt federate", async () => { test("Content in local-only community doesn't federate", async () => {
// create a community and set it local-only // create a community and set it local-only
let communityRes = (await createCommunity(alpha)).community_view.community; let communityRes = (await createCommunity(alpha)).community_view.community;
let form: EditCommunity = { let form: EditCommunity = {

View File

@ -27,21 +27,25 @@ import {
setupLogins, setupLogins,
waitForPost, waitForPost,
unfollows, unfollows,
editPostThumbnail,
getPost, getPost,
waitUntil, waitUntil,
createPostWithThumbnail,
sampleImage,
sampleSite,
} from "./shared"; } from "./shared";
const downloadFileSync = require("download-file-sync"); const downloadFileSync = require("download-file-sync");
beforeAll(setupLogins); beforeAll(setupLogins);
afterAll(unfollows); afterAll(async () => {
await Promise.all([unfollows(), deleteAllImages(alpha)]);
});
test("Upload image and delete it", async () => { test("Upload image and delete it", async () => {
// Before running this test, you need to delete all previous images in the DB // Before running this test, you need to delete all previous images in the DB
await deleteAllImages(alpha); await deleteAllImages(alpha);
// Upload test image. We use a simple string buffer as pictrs doesnt require an actual image // Upload test image. We use a simple string buffer as pictrs doesn't require an actual image
// in testing mode. // in testing mode.
const upload_form: UploadImage = { const upload_form: UploadImage = {
image: Buffer.from("test"), image: Buffer.from("test"),
@ -71,9 +75,14 @@ test("Upload image and delete it", async () => {
// The deleteUrl is a combination of the endpoint, delete token, and alias // The deleteUrl is a combination of the endpoint, delete token, and alias
let firstImage = listMediaRes.images[0]; let firstImage = listMediaRes.images[0];
let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.pictrs_delete_token}/${firstImage.pictrs_alias}`; let deleteUrl = `${alphaUrl}/pictrs/image/delete/${firstImage.local_image.pictrs_delete_token}/${firstImage.local_image.pictrs_alias}`;
expect(deleteUrl).toBe(upload.delete_url); expect(deleteUrl).toBe(upload.delete_url);
// Make sure the uploader is correct
expect(firstImage.person.actor_id).toBe(
`http://lemmy-alpha:8541/u/lemmy_alpha`,
);
// delete image // delete image
const delete_form: DeleteImage = { const delete_form: DeleteImage = {
token: upload.files![0].delete_token, token: upload.files![0].delete_token,
@ -153,7 +162,6 @@ test("Purge post, linked image removed", async () => {
expect(post.post_view.post.url).toBe(upload.url); expect(post.post_view.post.url).toBe(upload.url);
// purge post // purge post
const purgeForm: PurgePost = { const purgeForm: PurgePost = {
post_id: post.post_view.post.id, post_id: post.post_view.post.id,
}; };
@ -165,48 +173,94 @@ test("Purge post, linked image removed", async () => {
expect(content2).toBe(""); expect(content2).toBe("");
}); });
test("Images in remote post are proxied if setting enabled", async () => { test("Images in remote image post are proxied if setting enabled", async () => {
let user = await registerUser(beta, betaUrl);
let community = await createCommunity(gamma); let community = await createCommunity(gamma);
let postRes = await createPost(
const upload_form: UploadImage = {
image: Buffer.from("test"),
};
const upload = await user.uploadImage(upload_form);
let post = await createPost(
gamma, gamma,
community.community_view.community.id, community.community_view.community.id,
upload.url, sampleImage,
"![](http://example.com/image2.png)", `![](${sampleImage})`,
); );
expect(post.post_view.post).toBeDefined(); const post = postRes.post_view.post;
expect(post).toBeDefined();
// remote image gets proxied after upload // remote image gets proxied after upload
expect( expect(
post.post_view.post.url?.startsWith( post.thumbnail_url?.startsWith(
"http://lemmy-gamma:8561/api/v3/image_proxy?url", "http://lemmy-gamma:8561/api/v3/image_proxy?url",
), ),
).toBeTruthy(); ).toBeTruthy();
expect( expect(
post.post_view.post.body?.startsWith( post.body?.startsWith("![](http://lemmy-gamma:8561/api/v3/image_proxy?url"),
"![](http://lemmy-gamma:8561/api/v3/image_proxy?url",
),
).toBeTruthy(); ).toBeTruthy();
let epsilonPost = await resolvePost(epsilon, post.post_view.post); // Make sure that it ends with jpg, to be sure its an image
expect(epsilonPost.post).toBeDefined(); 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( expect(
epsilonPost.post!.post.url?.startsWith( epsilonPost.thumbnail_url?.startsWith(
"http://lemmy-epsilon:8581/api/v3/image_proxy?url", "http://lemmy-epsilon:8581/api/v3/image_proxy?url",
), ),
).toBeTruthy(); ).toBeTruthy();
expect( expect(
epsilonPost.post!.post.body?.startsWith( epsilonPost.body?.startsWith(
"![](http://lemmy-epsilon:8581/api/v3/image_proxy?url", "![](http://lemmy-epsilon:8581/api/v3/image_proxy?url",
), ),
).toBeTruthy(); ).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 () => { test("No image proxying if setting is disabled", async () => {
@ -226,15 +280,15 @@ test("No image proxying if setting is disabled", async () => {
alpha, alpha,
community.community_view.community.id, community.community_view.community.id,
upload.url, upload.url,
"![](http://example.com/image2.png)", `![](${sampleImage})`,
); );
expect(post.post_view.post).toBeDefined(); expect(post.post_view.post).toBeDefined();
// remote image doesnt get proxied after upload // remote image doesn't get proxied after upload
expect( expect(
post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"), post.post_view.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
).toBeTruthy(); ).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( let betaPost = await waitForPost(
beta, beta,
@ -243,12 +297,11 @@ test("No image proxying if setting is disabled", async () => {
); );
expect(betaPost.post).toBeDefined(); expect(betaPost.post).toBeDefined();
// remote image doesnt get proxied after federation // remote image doesn't get proxied after federation
expect( expect(
betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"), betaPost.post.url?.startsWith("http://127.0.0.1:8551/pictrs/image/"),
).toBeTruthy(); ).toBeTruthy();
expect(betaPost.post.body).toBe("![](http://example.com/image2.png)"); expect(betaPost.post.body).toBe(`![](${sampleImage})`);
// Make sure the alt text got federated // Make sure the alt text got federated
expect(post.post_view.post.alt_text).toBe(betaPost.post.alt_text); expect(post.post_view.post.alt_text).toBe(betaPost.post.alt_text);
}); });
@ -264,10 +317,11 @@ test("Make regular post, and give it a custom thumbnail", async () => {
// Use wikipedia since it has an opengraph image // Use wikipedia since it has an opengraph image
const wikipediaUrl = "https://wikipedia.org/"; const wikipediaUrl = "https://wikipedia.org/";
let post = await createPost( let post = await createPostWithThumbnail(
alphaImage, alphaImage,
community.community_view.community.id, community.community_view.community.id,
wikipediaUrl, wikipediaUrl,
upload1.url!,
); );
// Wait for the metadata to get fetched, since this is backgrounded now // Wait for the metadata to get fetched, since this is backgrounded now
@ -276,21 +330,11 @@ test("Make regular post, and give it a custom thumbnail", async () => {
p => p.post_view.post.thumbnail_url != undefined, p => p.post_view.post.thumbnail_url != undefined,
); );
expect(post.post_view.post.url).toBe(wikipediaUrl); expect(post.post_view.post.url).toBe(wikipediaUrl);
expect(post.post_view.post.thumbnail_url).toBeDefined(); // Make sure it uses custom thumbnail
// Edit the thumbnail
await editPostThumbnail(alphaImage, post.post_view.post, upload1.url!);
post = await waitUntil(
() => getPost(alphaImage, post.post_view.post.id),
p => p.post_view.post.thumbnail_url == upload1.url,
);
// Make sure the thumbnail got edited.
expect(post.post_view.post.thumbnail_url).toBe(upload1.url); expect(post.post_view.post.thumbnail_url).toBe(upload1.url);
}); });
test("Create an image post, and make sure a custom thumbnail doesnt overwrite it", async () => { test("Create an image post, and make sure a custom thumbnail doesn't overwrite it", async () => {
const uploadForm1: UploadImage = { const uploadForm1: UploadImage = {
image: Buffer.from("test1"), image: Buffer.from("test1"),
}; };
@ -303,23 +347,17 @@ test("Create an image post, and make sure a custom thumbnail doesnt overwrite it
const community = await createCommunity(alphaImage); const community = await createCommunity(alphaImage);
let post = await createPost( let post = await createPostWithThumbnail(
alphaImage, alphaImage,
community.community_view.community.id, community.community_view.community.id,
upload1.url, upload1.url!,
upload2.url!,
); );
expect(post.post_view.post.url).toBe(upload1.url);
// Edit the post
await editPostThumbnail(alphaImage, post.post_view.post, upload2.url!);
// Wait for the metadata to get fetched
post = await waitUntil( post = await waitUntil(
() => getPost(alphaImage, post.post_view.post.id), () => getPost(alphaImage, post.post_view.post.id),
p => p.post_view.post.thumbnail_url == upload1.url, p => p.post_view.post.thumbnail_url != undefined,
); );
// Make sure the new custom thumbnail is ignored, and doesn't overwrite the image post
expect(post.post_view.post.url).toBe(upload1.url); expect(post.post_view.post.url).toBe(upload1.url);
expect(post.post_view.post.thumbnail_url).toBe(upload1.url); // Make sure the custom thumbnail is ignored
expect(post.post_view.post.thumbnail_url == upload2.url).toBe(false);
}); });

View File

@ -48,7 +48,6 @@ beforeAll(async () => {
await setupLogins(); await setupLogins();
betaCommunity = (await resolveBetaCommunity(alpha)).community; betaCommunity = (await resolveBetaCommunity(alpha)).community;
expect(betaCommunity).toBeDefined(); expect(betaCommunity).toBeDefined();
await unfollows();
}); });
afterAll(unfollows); afterAll(unfollows);
@ -83,10 +82,7 @@ async function assertPostFederation(postOne: PostView, postTwo: PostView) {
test("Create a post", async () => { test("Create a post", async () => {
// Setup some allowlists and blocklists // Setup some allowlists and blocklists
let editSiteForm: EditSite = { const editSiteForm: EditSite = {};
allowed_instances: ["lemmy-beta"],
};
await delta.editSite(editSiteForm);
editSiteForm.allowed_instances = []; editSiteForm.allowed_instances = [];
editSiteForm.blocked_instances = ["lemmy-alpha"]; 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 () => { test("Report a post", async () => {
// Note, this is a different one from the setup // Create post from alpha
let betaCommunity = (await resolveBetaCommunity(beta)).community; let alphaCommunity = (await resolveBetaCommunity(alpha)).community!;
if (!betaCommunity) {
throw "Missing beta community";
}
await followBeta(alpha); 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(); expect(postRes.post_view.post).toBeDefined();
let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post; let alphaPost = (await resolvePost(alpha, postRes.post_view.post)).post;
if (!alphaPost) { if (!alphaPost) {
throw "Missing alpha post"; 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( let betaReport = (await waitUntil(
() => () =>
listPostReports(beta).then(p => listPostReports(beta).then(p =>
p.post_reports.find( p.post_reports.find(
r => r =>
r.post_report.original_post_name === alphaReport.original_post_name, r.post_report.original_post_name === gammaReport.original_post_name,
), ),
), ),
res => !!res, res => !!res,
))!.post_report; ))!.post_report;
expect(betaReport).toBeDefined(); expect(betaReport).toBeDefined();
expect(betaReport.resolved).toBe(false); expect(betaReport.resolved).toBe(false);
expect(betaReport.original_post_name).toBe(alphaReport.original_post_name); expect(betaReport.original_post_name).toBe(gammaReport.original_post_name);
expect(betaReport.original_post_url).toBe(alphaReport.original_post_url); //expect(betaReport.original_post_url).toBe(gammaReport.original_post_url);
expect(betaReport.original_post_body).toBe(alphaReport.original_post_body); expect(betaReport.original_post_body).toBe(gammaReport.original_post_body);
expect(betaReport.reason).toBe(alphaReport.reason); expect(betaReport.reason).toBe(gammaReport.reason);
await unfollowRemotes(alpha); 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 () => { test("Fetch post via redirect", async () => {
@ -729,7 +745,7 @@ test("Block post that contains banned URL", async () => {
await epsilon.editSite(editSiteForm); await epsilon.editSite(editSiteForm);
await delay(500); await delay();
if (!betaCommunity) { if (!betaCommunity) {
throw "Missing beta community"; throw "Missing beta community";

View File

@ -81,21 +81,24 @@ import { ListingType } from "lemmy-js-client/dist/types/ListingType";
export const fetchFunction = fetch; export const fetchFunction = fetch;
export const imageFetchLimit = 50; 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 const alphaUrl = "http://127.0.0.1:8541";
export let betaUrl = "http://127.0.0.1:8551"; export const betaUrl = "http://127.0.0.1:8551";
export let gammaUrl = "http://127.0.0.1:8561"; export const gammaUrl = "http://127.0.0.1:8561";
export let deltaUrl = "http://127.0.0.1:8571"; export const deltaUrl = "http://127.0.0.1:8571";
export let epsilonUrl = "http://127.0.0.1:8581"; export const epsilonUrl = "http://127.0.0.1:8581";
export let alpha = new LemmyHttp(alphaUrl, { fetchFunction }); export const alpha = new LemmyHttp(alphaUrl, { fetchFunction });
export let alphaImage = new LemmyHttp(alphaUrl); export const alphaImage = new LemmyHttp(alphaUrl);
export let beta = new LemmyHttp(betaUrl, { fetchFunction }); export const beta = new LemmyHttp(betaUrl, { fetchFunction });
export let gamma = new LemmyHttp(gammaUrl, { fetchFunction }); export const gamma = new LemmyHttp(gammaUrl, { fetchFunction });
export let delta = new LemmyHttp(deltaUrl, { fetchFunction }); export const delta = new LemmyHttp(deltaUrl, { fetchFunction });
export let epsilon = new LemmyHttp(epsilonUrl, { fetchFunction }); export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction });
export let betaAllowedInstances = [ export const betaAllowedInstances = [
"lemmy-alpha", "lemmy-alpha",
"lemmy-gamma", "lemmy-gamma",
"lemmy-delta", "lemmy-delta",
@ -180,6 +183,10 @@ export async function setupLogins() {
]; ];
await gamma.editSite(editSiteForm); await gamma.editSite(editSiteForm);
// Setup delta allowed instance
editSiteForm.allowed_instances = ["lemmy-beta"];
await delta.editSite(editSiteForm);
// Create the main alpha/beta communities // Create the main alpha/beta communities
// Ignore thrown errors of duplicates // Ignore thrown errors of duplicates
try { try {
@ -203,6 +210,7 @@ export async function createPost(
// use example.com for consistent title and embed description // use example.com for consistent title and embed description
name: string = randomString(5), name: string = randomString(5),
alt_text = randomString(10), alt_text = randomString(10),
custom_thumbnail: string | undefined = undefined,
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: CreatePost = { let form: CreatePost = {
name, name,
@ -210,6 +218,7 @@ export async function createPost(
body, body,
alt_text, alt_text,
community_id, community_id,
custom_thumbnail,
}; };
return api.createPost(form); return api.createPost(form);
} }
@ -226,16 +235,19 @@ export async function editPost(
return api.editPost(form); return api.editPost(form);
} }
export async function editPostThumbnail( export async function createPostWithThumbnail(
api: LemmyHttp, api: LemmyHttp,
post: Post, community_id: number,
customThumbnail: string, url: string,
custom_thumbnail: string,
): Promise<PostResponse> { ): Promise<PostResponse> {
let form: EditPost = { let form: CreatePost = {
post_id: post.id, name: randomString(10),
custom_thumbnail: customThumbnail, url,
community_id,
custom_thumbnail,
}; };
return api.editPost(form); return api.createPost(form);
} }
export async function deletePost( export async function deletePost(
@ -688,8 +700,8 @@ export async function saveUserSettingsBio(
export async function saveUserSettingsFederated( export async function saveUserSettingsFederated(
api: LemmyHttp, api: LemmyHttp,
): Promise<SuccessResponse> { ): Promise<SuccessResponse> {
let avatar = "https://image.flaticon.com/icons/png/512/35/35896.png"; let avatar = sampleImage;
let banner = "https://image.flaticon.com/icons/png/512/36/35896.png"; let banner = sampleImage;
let bio = "a changed bio"; let bio = "a changed bio";
let form: SaveUserSettings = { let form: SaveUserSettings = {
show_nsfw: false, show_nsfw: false,
@ -755,6 +767,7 @@ export async function unfollowRemotes(
await Promise.all( await Promise.all(
remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)), remoteFollowed.map(cu => followCommunity(api, false, cu.community.id)),
); );
let siteRes = await getSite(api); let siteRes = await getSite(api);
return siteRes; return siteRes;
} }
@ -884,14 +897,17 @@ export async function deleteAllImages(api: LemmyHttp) {
limit: imageFetchLimit, limit: imageFetchLimit,
}); });
imagesRes.images; imagesRes.images;
Promise.all(
for (const image of imagesRes.images) { imagesRes.images
.map(image => {
const form: DeleteImage = { const form: DeleteImage = {
token: image.pictrs_delete_token, token: image.local_image.pictrs_delete_token,
filename: image.pictrs_alias, filename: image.local_image.pictrs_alias,
}; };
await api.deleteImage(form); return form;
} })
.map(form => api.deleteImage(form)),
);
} }
export async function unfollows() { export async function unfollows() {
@ -902,6 +918,24 @@ export async function unfollows() {
unfollowRemotes(delta), unfollowRemotes(delta),
unfollowRemotes(epsilon), 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 { export function getCommentParentId(comment: Comment): number | undefined {

View File

@ -47,9 +47,10 @@
# #
# To be removed in 0.20 # To be removed in 0.20
cache_external_link_previews: true 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: image_mode:
# Leave images unchanged, don't generate any local thumbnails for post urls. Instead the the # Leave images unchanged, don't generate any local thumbnails for post urls. Instead the
# Opengraph image is directly returned as thumbnail # Opengraph image is directly returned as thumbnail
"None" "None"
@ -64,10 +65,11 @@
# or # or
# If enabled, all images from remote domains are rewritten to pass through `/api/v3/image_proxy`, # If enabled, all images from remote domains are rewritten to pass through
# including embedded images in markdown. Images are stored temporarily in pict-rs for caching. # `/api/v3/image_proxy`, including embedded images in markdown. Images are stored temporarily
# This improves privacy as users don't expose their IP to untrusted servers, and decreases load # in pict-rs for caching. This improves privacy as users don't expose their IP to untrusted
# on other servers. However it increases bandwidth use for the local server. # servers, and decreases load on other servers. However it increases bandwidth use for the
# local server.
# #
# Requires pict-rs 0.5 # Requires pict-rs 0.5
"ProxyAllImages" "ProxyAllImages"

View File

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

View File

@ -17,7 +17,9 @@ pub async fn distinguish_comment(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> { ) -> LemmyResult<Json<CommentResponse>> {
let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None).await?; let orig_comment = CommentView::read(&mut context.pool(), data.comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -54,7 +56,8 @@ pub async fn distinguish_comment(
data.comment_id, data.comment_id,
Some(local_user_view.person.id), Some(local_user_view.person.id),
) )
.await?; .await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(Json(CommentResponse { Ok(Json(CommentResponse {
comment_view, comment_view,

View File

@ -35,7 +35,9 @@ pub async fn like_comment(
check_bot_account(&local_user_view.person)?; check_bot_account(&local_user_view.person)?;
let comment_id = data.comment_id; let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?; let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -46,9 +48,10 @@ pub async fn like_comment(
// Add parent poster or commenter to recipients // Add parent poster or commenter to recipients
let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await; let comment_reply = CommentReply::read_by_comment(&mut context.pool(), comment_id).await;
if let Ok(reply) = comment_reply { if let Ok(Some(reply)) = comment_reply {
let recipient_id = reply.recipient_id; let recipient_id = reply.recipient_id;
if let Ok(local_recipient) = LocalUserView::read_person(&mut context.pool(), recipient_id).await if let Ok(Some(local_recipient)) =
LocalUserView::read_person(&mut context.pool(), recipient_id).await
{ {
recipient_ids.push(local_recipient.local_user.id); recipient_ids.push(local_recipient.local_user.id);
} }

View File

@ -5,7 +5,7 @@ use lemmy_api_common::{
utils::is_mod_or_admin, utils::is_mod_or_admin,
}; };
use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView}; use lemmy_db_views::structs::{CommentView, LocalUserView, VoteView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
/// Lists likes for a comment /// Lists likes for a comment
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -19,7 +19,9 @@ pub async fn list_comment_likes(
data.comment_id, data.comment_id,
Some(local_user_view.person.id), Some(local_user_view.person.id),
) )
.await?; .await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
is_mod_or_admin( is_mod_or_admin(
&mut context.pool(), &mut context.pool(),
&local_user_view.person, &local_user_view.person,

View File

@ -33,7 +33,9 @@ pub async fn save_comment(
let comment_id = data.comment_id; let comment_id = data.comment_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id)).await?; let comment_view = CommentView::read(&mut context.pool(), comment_id, Some(person_id))
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(Json(CommentResponse { Ok(Json(CommentResponse {
comment_view, comment_view,

View File

@ -35,7 +35,9 @@ pub async fn create_comment_report(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_id = data.comment_id; let comment_id = data.comment_id;
let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?; let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -58,8 +60,9 @@ pub async fn create_comment_report(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let comment_report_view = let comment_report_view = CommentReportView::read(&mut context.pool(), report.id, person_id)
CommentReportView::read(&mut context.pool(), report.id, person_id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
// Email the admins // Email the admins
if local_site.reports_email_admins { if local_site.reports_email_admins {

View File

@ -17,7 +17,9 @@ pub async fn resolve_comment_report(
) -> LemmyResult<Json<CommentReportResponse>> { ) -> LemmyResult<Json<CommentReportResponse>> {
let report_id = data.report_id; let report_id = data.report_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let report = CommentReportView::read(&mut context.pool(), report_id, person_id).await?; let report = CommentReportView::read(&mut context.pool(), report_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
check_community_mod_action( check_community_mod_action(
@ -39,8 +41,9 @@ pub async fn resolve_comment_report(
} }
let report_id = data.report_id; let report_id = data.report_id;
let comment_report_view = let comment_report_view = CommentReportView::read(&mut context.pool(), report_id, person_id)
CommentReportView::read(&mut context.pool(), report_id, person_id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindCommentReport)?;
Ok(Json(CommentReportResponse { Ok(Json(CommentReportResponse {
comment_report_view, comment_report_view,

View File

@ -33,10 +33,24 @@ pub async fn add_mod_to_community(
&mut context.pool(), &mut context.pool(),
) )
.await?; .await?;
let community = Community::read(&mut context.pool(), community_id).await?; 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 { 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)? Err(LemmyErrorType::NotAModerator)?
} }
}
// Update in local database // Update in local database
let community_moderator_form = CommunityModeratorForm { let community_moderator_form = CommunityModeratorForm {

View File

@ -89,7 +89,9 @@ pub async fn ban_from_community(
ModBanFromCommunity::create(&mut context.pool(), &form).await?; ModBanFromCommunity::create(&mut context.pool(), &form).await?;
let person_view = PersonView::read(&mut context.pool(), data.person_id).await?; let person_view = PersonView::read(&mut context.pool(), data.person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::BanFromCommunity { SendActivityData::BanFromCommunity {

View File

@ -51,7 +51,9 @@ pub async fn block_community(
} }
let community_view = let community_view =
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false).await?; CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::FollowCommunity( SendActivityData::FollowCommunity(

View File

@ -23,7 +23,9 @@ pub async fn follow_community(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> { ) -> LemmyResult<Json<CommunityResponse>> {
let community = Community::read(&mut context.pool(), data.community_id).await?; let community = Community::read(&mut context.pool(), data.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let mut community_follower_form = CommunityFollowerForm { let mut community_follower_form = CommunityFollowerForm {
community_id: community.id, community_id: community.id,
person_id: local_user_view.person.id, person_id: local_user_view.person.id,
@ -62,7 +64,10 @@ pub async fn follow_community(
let community_id = data.community_id; let community_id = data.community_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let community_view = let community_view =
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false).await?; CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
Ok(Json(CommunityResponse { Ok(Json(CommunityResponse {

View File

@ -79,8 +79,8 @@ pub async fn transfer_community(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let community_view = let community_view =
CommunityView::read(&mut context.pool(), community_id, Some(person_id), false) CommunityView::read(&mut context.pool(), community_id, Some(person_id), false)
.await .await?
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; .ok_or(LemmyErrorType::CouldntFindCommunity)?;
let community_id = data.community_id; let community_id = data.community_id;
let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id) let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id)

View File

@ -44,6 +44,7 @@ pub mod site;
pub mod sitemap; pub mod sitemap;
/// Converts the captcha to a base64 encoded wav audio file /// Converts the captcha to a base64 encoded wav audio file
#[allow(deprecated)]
pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult<String> { pub(crate) fn captcha_as_wav_base64(captcha: &Captcha) -> LemmyResult<String> {
let letters = captcha.as_wav(); let letters = captcha.as_wav();
@ -252,7 +253,9 @@ pub async fn local_user_view_from_jwt(
let local_user_id = Claims::validate(jwt, context) let local_user_id = Claims::validate(jwt, context)
.await .await
.with_lemmy_type(LemmyErrorType::NotLoggedIn)?; .with_lemmy_type(LemmyErrorType::NotLoggedIn)?;
let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id).await?; let local_user_view = LocalUserView::read(&mut context.pool(), local_user_id)
.await?
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
check_user_valid(&local_user_view.person)?; check_user_valid(&local_user_view.person)?;
Ok(local_user_view) Ok(local_user_view)

View File

@ -26,10 +26,10 @@ pub async fn add_admin(
// Make sure that the person_id added is local // Make sure that the person_id added is local
let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id) let added_local_user = LocalUserView::read_person(&mut context.pool(), data.person_id)
.await .await?
.with_lemmy_type(LemmyErrorType::ObjectNotLocal)?; .ok_or(LemmyErrorType::ObjectNotLocal)?;
let added_admin = LocalUser::update( LocalUser::update(
&mut context.pool(), &mut context.pool(),
added_local_user.local_user.id, added_local_user.local_user.id,
&LocalUserUpdateForm { &LocalUserUpdateForm {
@ -43,7 +43,7 @@ pub async fn add_admin(
// Mod tables // Mod tables
let form = ModAddForm { let form = ModAddForm {
mod_person_id: local_user_view.person.id, 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), removed: Some(!data.added),
}; };

View File

@ -49,7 +49,7 @@ pub async fn ban_from_site(
// if its a local user, invalidate logins // if its a local user, invalidate logins
let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await; let local_user = LocalUserView::read_person(&mut context.pool(), person.id).await;
if let Ok(local_user) = local_user { if let Ok(Some(local_user)) = local_user {
LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?; LoginToken::invalidate_all(&mut context.pool(), local_user.local_user.id).await?;
} }
@ -70,7 +70,9 @@ pub async fn ban_from_site(
ModBan::create(&mut context.pool(), &form).await?; ModBan::create(&mut context.pool(), &form).await?;
let person_view = PersonView::read(&mut context.pool(), person.id).await?; let person_view = PersonView::read(&mut context.pool(), person.id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ban_nonlocal_user_from_local_communities( ban_nonlocal_user_from_local_communities(
&local_user_view, &local_user_view,

View File

@ -30,8 +30,12 @@ pub async fn block_person(
target_id, target_id,
}; };
let target_user = LocalUserView::read_person(&mut context.pool(), target_id).await; let target_user = LocalUserView::read_person(&mut context.pool(), target_id)
if target_user.map(|t| t.local_user.admin) == Ok(true) { .await
.ok()
.flatten();
if target_user.is_some_and(|t| t.local_user.admin) {
Err(LemmyErrorType::CantBlockAdmin)? Err(LemmyErrorType::CantBlockAdmin)?
} }
@ -45,7 +49,9 @@ pub async fn block_person(
.with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?; .with_lemmy_type(LemmyErrorType::PersonBlockAlreadyExists)?;
} }
let person_view = PersonView::read(&mut context.pool(), target_id).await?; let person_view = PersonView::read(&mut context.pool(), target_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
Ok(Json(BlockPersonResponse { Ok(Json(BlockPersonResponse {
person_view, person_view,
blocked: data.block, blocked: data.block,

View File

@ -19,9 +19,10 @@ pub async fn change_password_after_reset(
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
// Fetch the user_id from the token // Fetch the user_id from the token
let token = data.token.clone(); 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 .await?
.map(|p| p.local_user_id)?; .ok_or(LemmyErrorType::TokenNotFound)?
.local_user_id;
password_length_check(&data.password)?; password_length_check(&data.password)?;

View File

@ -1,11 +1,7 @@
use crate::{build_totp_2fa, generate_totp_2fa_secret}; use crate::{build_totp_2fa, generate_totp_2fa_secret};
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{context::LemmyContext, person::GenerateTotpSecretResponse};
context::LemmyContext,
person::GenerateTotpSecretResponse,
sensitive::Sensitive,
};
use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm}; use lemmy_db_schema::source::local_user::{LocalUser, LocalUserUpdateForm};
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
@ -17,7 +13,9 @@ pub async fn generate_totp_secret(
local_user_view: LocalUserView, local_user_view: LocalUserView,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GenerateTotpSecretResponse>> { ) -> LemmyResult<Json<GenerateTotpSecretResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
if local_user_view.local_user.totp_2fa_enabled { if local_user_view.local_user.totp_2fa_enabled {
return Err(LemmyErrorType::TotpAlreadyEnabled)?; return Err(LemmyErrorType::TotpAlreadyEnabled)?;
@ -39,6 +37,6 @@ pub async fn generate_totp_secret(
.await?; .await?;
Ok(Json(GenerateTotpSecretResponse { Ok(Json(GenerateTotpSecretResponse {
totp_secret_url: Sensitive::new(secret_url), totp_secret_url: secret_url.into(),
})) }))
} }

View File

@ -3,8 +3,7 @@ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
person::{ListMedia, ListMediaResponse}, person::{ListMedia, ListMediaResponse},
}; };
use lemmy_db_schema::source::images::LocalImage; use lemmy_db_views::structs::{LocalImageView, LocalUserView};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -15,7 +14,7 @@ pub async fn list_media(
) -> LemmyResult<Json<ListMediaResponse>> { ) -> LemmyResult<Json<ListMediaResponse>> {
let page = data.page; let page = data.page;
let limit = data.limit; let limit = data.limit;
let images = LocalImage::get_all_paged_by_local_user_id( let images = LocalImageView::get_all_paged_by_local_user_id(
&mut context.pool(), &mut context.pool(),
local_user_view.local_user.id, local_user_view.local_user.id,
page, page,

View File

@ -16,7 +16,7 @@ use lemmy_db_schema::{
RegistrationMode, RegistrationMode,
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn login( pub async fn login(
@ -24,14 +24,16 @@ pub async fn login(
req: HttpRequest, req: HttpRequest,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<LoginResponse>> { ) -> LemmyResult<Json<LoginResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
// Fetch that username / email // Fetch that username / email
let username_or_email = data.username_or_email.clone(); let username_or_email = data.username_or_email.clone();
let local_user_view = let local_user_view =
LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email) LocalUserView::find_by_email_or_name(&mut context.pool(), &username_or_email)
.await .await?
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?; .ok_or(LemmyErrorType::IncorrectLogin)?;
// Verify the password // Verify the password
let valid: bool = verify( let valid: bool = verify(
@ -79,7 +81,9 @@ async fn check_registration_application(
// Fetch the registration application. If no admin id is present its still pending. Otherwise it // Fetch the registration application. If no admin id is present its still pending. Otherwise it
// was processed (either accepted or denied). // was processed (either accepted or denied).
let local_user_id = local_user_view.local_user.id; let local_user_id = local_user_view.local_user.id;
let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id).await?; let registration = RegistrationApplication::find_by_local_user_id(pool, local_user_id)
.await?
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
if registration.admin_id.is_some() { if registration.admin_id.is_some() {
Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))? Err(LemmyErrorType::RegistrationDenied(registration.deny_reason))?
} else { } else {

View File

@ -18,7 +18,9 @@ pub async fn mark_person_mention_as_read(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PersonMentionResponse>> { ) -> LemmyResult<Json<PersonMentionResponse>> {
let person_mention_id = data.person_mention_id; let person_mention_id = data.person_mention_id;
let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id).await?; let read_person_mention = PersonMention::read(&mut context.pool(), person_mention_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
if local_user_view.person.id != read_person_mention.recipient_id { if local_user_view.person.id != read_person_mention.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)? Err(LemmyErrorType::CouldntUpdateComment)?
@ -37,7 +39,9 @@ pub async fn mark_person_mention_as_read(
let person_mention_id = read_person_mention.id; let person_mention_id = read_person_mention.id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let person_mention_view = let person_mention_view =
PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id)).await?; PersonMentionView::read(&mut context.pool(), person_mention_id, Some(person_id))
.await?
.ok_or(LemmyErrorType::CouldntFindPersonMention)?;
Ok(Json(PersonMentionResponse { Ok(Json(PersonMentionResponse {
person_mention_view, person_mention_view,

View File

@ -18,7 +18,9 @@ pub async fn mark_reply_as_read(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentReplyResponse>> { ) -> LemmyResult<Json<CommentReplyResponse>> {
let comment_reply_id = data.comment_reply_id; let comment_reply_id = data.comment_reply_id;
let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id).await?; let read_comment_reply = CommentReply::read(&mut context.pool(), comment_reply_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
if local_user_view.person.id != read_comment_reply.recipient_id { if local_user_view.person.id != read_comment_reply.recipient_id {
Err(LemmyErrorType::CouldntUpdateComment)? Err(LemmyErrorType::CouldntUpdateComment)?
@ -38,7 +40,9 @@ pub async fn mark_reply_as_read(
let comment_reply_id = read_comment_reply.id; let comment_reply_id = read_comment_reply.id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let comment_reply_view = let comment_reply_view =
CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id)).await?; CommentReplyView::read(&mut context.pool(), comment_reply_id, Some(person_id))
.await?
.ok_or(LemmyErrorType::CouldntFindCommentReply)?;
Ok(Json(CommentReplyResponse { comment_reply_view })) Ok(Json(CommentReplyResponse { comment_reply_view }))
} }

View File

@ -6,9 +6,8 @@ use lemmy_api_common::{
utils::send_password_reset_email, utils::send_password_reset_email,
SuccessResponse, SuccessResponse,
}; };
use lemmy_db_schema::source::password_reset_request::PasswordResetRequest;
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn reset_password( pub async fn reset_password(
@ -18,19 +17,12 @@ pub async fn reset_password(
// Fetch that email // Fetch that email
let email = data.email.to_lowercase(); let email = data.email.to_lowercase();
let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email) let local_user_view = LocalUserView::find_by_email(&mut context.pool(), &email)
.await .await?
.with_lemmy_type(LemmyErrorType::IncorrectLogin)?; .ok_or(LemmyErrorType::IncorrectLogin)?;
// Check for too many attempts (to limit potential abuse) let site_view = SiteView::read_local(&mut context.pool())
let recent_resets_count = PasswordResetRequest::get_recent_password_resets_count( .await?
&mut context.pool(), .ok_or(LemmyErrorType::LocalSiteNotSetup)?;
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?;
check_email_verified(&local_user_view, &site_view)?; check_email_verified(&local_user_view, &site_view)?;
// Email the pure token to the user. // Email the pure token to the user.

View File

@ -28,6 +28,7 @@ use lemmy_utils::{
error::{LemmyErrorType, LemmyResult}, error::{LemmyErrorType, LemmyResult},
utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id}, utils::validation::{is_valid_bio_field, is_valid_display_name, is_valid_matrix_id},
}; };
use std::ops::Deref;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn save_user_settings( pub async fn save_user_settings(
@ -35,7 +36,9 @@ pub async fn save_user_settings(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let slur_regex = local_site_to_slur_regex(&site_view.local_site); let slur_regex = local_site_to_slur_regex(&site_view.local_site);
let url_blocklist = get_url_blocklist(&context).await?; let url_blocklist = get_url_blocklist(&context).await?;
@ -55,7 +58,7 @@ pub async fn save_user_settings(
if let Some(Some(email)) = &email { if let Some(Some(email)) = &email {
let previous_email = local_user_view.local_user.email.clone().unwrap_or_default(); 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 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? { if LocalUser::is_email_taken(&mut context.pool(), email).await? {
return Err(LemmyErrorType::EmailAlreadyExists)?; return Err(LemmyErrorType::EmailAlreadyExists)?;
} }
@ -69,7 +72,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 let Some(email) = &email {
if email.is_none() && site_view.local_site.require_email_verification { if email.is_none() && site_view.local_site.require_email_verification {
Err(LemmyErrorType::EmailRequired)? Err(LemmyErrorType::EmailRequired)?
@ -139,11 +143,7 @@ pub async fn save_user_settings(
..Default::default() ..Default::default()
}; };
// Ignore errors, because 'no fields updated' will return an error. LocalUser::update(&mut context.pool(), local_user_id, &local_user_form).await?;
// https://github.com/LemmyNet/lemmy/issues/4076
LocalUser::update(&mut context.pool(), local_user_id, &local_user_form)
.await
.ok();
// Update the vote display modes // Update the vote display modes
let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm { let vote_display_modes_form = LocalUserVoteDisplayModeUpdateForm {

View File

@ -9,23 +9,23 @@ use lemmy_db_schema::{
source::{ source::{
email_verification::EmailVerification, email_verification::EmailVerification,
local_user::{LocalUser, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserUpdateForm},
person::Person,
}, },
traits::Crud,
RegistrationMode, RegistrationMode,
}; };
use lemmy_db_views::structs::SiteView; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorType, LemmyResult};
pub async fn verify_email( pub async fn verify_email(
data: Json<VerifyEmail>, data: Json<VerifyEmail>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let token = data.token.clone(); let token = data.token.clone();
let verification = EmailVerification::read_for_token(&mut context.pool(), &token) let verification = EmailVerification::read_for_token(&mut context.pool(), &token)
.await .await?
.with_lemmy_type(LemmyErrorType::TokenNotFound)?; .ok_or(LemmyErrorType::TokenNotFound)?;
let form = LocalUserUpdateForm { let form = LocalUserUpdateForm {
// necessary in case this is a new signup // necessary in case this is a new signup
@ -36,7 +36,7 @@ pub async fn verify_email(
}; };
let local_user_id = verification.local_user_id; 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?; EmailVerification::delete_old_tokens_for_local_user(&mut context.pool(), local_user_id).await?;
@ -44,8 +44,15 @@ pub async fn verify_email(
if site_view.local_site.registration_mode == RegistrationMode::RequireApplication if site_view.local_site.registration_mode == RegistrationMode::RequireApplication
&& site_view.local_site.application_email_admins && site_view.local_site.application_email_admins
{ {
let person = Person::read(&mut context.pool(), local_user.person_id).await?; let local_user = LocalUserView::read(&mut context.pool(), local_user_id)
send_new_applicant_email_to_admins(&person.name, &mut context.pool(), context.settings()) .await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
send_new_applicant_email_to_admins(
&local_user.person.name,
&mut context.pool(),
context.settings(),
)
.await?; .await?;
} }

View File

@ -16,7 +16,7 @@ use lemmy_db_schema::{
PostFeatureType, PostFeatureType,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn feature_post( pub async fn feature_post(
@ -25,7 +25,9 @@ pub async fn feature_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?; let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,

View File

@ -11,7 +11,7 @@ pub async fn get_link_metadata(
data: Query<GetSiteMetadata>, data: Query<GetSiteMetadata>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GetSiteMetadataResponse>> { ) -> LemmyResult<Json<GetSiteMetadataResponse>> {
let metadata = fetch_link_metadata(&data.url, false, &context).await?; let metadata = fetch_link_metadata(&data.url, &context).await?;
Ok(Json(GetSiteMetadataResponse { metadata })) Ok(Json(GetSiteMetadataResponse { metadata }))
} }

View File

@ -38,7 +38,9 @@ pub async fn like_post(
// Check for a community ban // Check for a community ban
let post_id = data.post_id; let post_id = data.post_id;
let post = Post::read(&mut context.pool(), post_id).await?; let post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -66,14 +68,17 @@ pub async fn like_post(
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?; .with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
} }
// Mark the post as read
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
let community = Community::read(&mut context.pool(), post.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::LikePostOrComment { SendActivityData::LikePostOrComment {
object_id: post.ap_id, object_id: post.ap_id,
actor: local_user_view.person.clone(), actor: local_user_view.person.clone(),
community: Community::read(&mut context.pool(), post.community_id).await?, community,
score: data.score, score: data.score,
}, },
&context, &context,

View File

@ -6,7 +6,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{source::post::Post, traits::Crud}; use lemmy_db_schema::{source::post::Post, traits::Crud};
use lemmy_db_views::structs::{LocalUserView, VoteView}; use lemmy_db_views::structs::{LocalUserView, VoteView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
/// Lists likes for a post /// Lists likes for a post
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -15,7 +15,9 @@ pub async fn list_post_likes(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<ListPostLikesResponse>> { ) -> LemmyResult<Json<ListPostLikesResponse>> {
let post = Post::read(&mut context.pool(), data.post_id).await?; let post = Post::read(&mut context.pool(), data.post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
is_mod_or_admin( is_mod_or_admin(
&mut context.pool(), &mut context.pool(),
&local_user_view.person, &local_user_view.person,

View File

@ -15,7 +15,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn lock_post( pub async fn lock_post(
@ -24,7 +24,9 @@ pub async fn lock_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?; let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,

View File

@ -34,9 +34,10 @@ pub async fn save_post(
let post_id = data.post_id; let post_id = data.post_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false).await?; let post_view = PostView::read(&mut context.pool(), post_id, Some(person_id), false)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
// Mark the post as read
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
Ok(Json(PostResponse { post_view })) Ok(Json(PostResponse { post_view }))

View File

@ -35,7 +35,9 @@ pub async fn create_post_report(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let post_id = data.post_id; let post_id = data.post_id;
let post_view = PostView::read(&mut context.pool(), post_id, None, false).await?; let post_view = PostView::read(&mut context.pool(), post_id, None, false)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -59,7 +61,9 @@ pub async fn create_post_report(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id).await?; let post_report_view = PostReportView::read(&mut context.pool(), report.id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
// Email the admins // Email the admins
if local_site.reports_email_admins { if local_site.reports_email_admins {

View File

@ -17,7 +17,9 @@ pub async fn resolve_post_report(
) -> LemmyResult<Json<PostReportResponse>> { ) -> LemmyResult<Json<PostReportResponse>> {
let report_id = data.report_id; let report_id = data.report_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let report = PostReportView::read(&mut context.pool(), report_id, person_id).await?; let report = PostReportView::read(&mut context.pool(), report_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
check_community_mod_action( check_community_mod_action(
@ -38,7 +40,9 @@ pub async fn resolve_post_report(
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
} }
let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id).await?; let post_report_view = PostReportView::read(&mut context.pool(), report_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPostReport)?;
Ok(Json(PostReportResponse { post_report_view })) Ok(Json(PostReportResponse { post_report_view }))
} }

View File

@ -18,7 +18,9 @@ pub async fn mark_pm_as_read(
) -> LemmyResult<Json<PrivateMessageResponse>> { ) -> LemmyResult<Json<PrivateMessageResponse>> {
// Checking permissions // Checking permissions
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
if local_user_view.person.id != orig_private_message.recipient_id { if local_user_view.person.id != orig_private_message.recipient_id {
Err(LemmyErrorType::CouldntUpdatePrivateMessage)? Err(LemmyErrorType::CouldntUpdatePrivateMessage)?
} }
@ -37,7 +39,9 @@ pub async fn mark_pm_as_read(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; let view = PrivateMessageView::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
Ok(Json(PrivateMessageResponse { Ok(Json(PrivateMessageResponse {
private_message_view: view, private_message_view: view,
})) }))

View File

@ -29,7 +29,9 @@ pub async fn create_pm_report(
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; let private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
// Make sure that only the recipient of the private message can create a report // Make sure that only the recipient of the private message can create a report
if person_id != private_message.recipient_id { if person_id != private_message.recipient_id {
@ -47,8 +49,9 @@ pub async fn create_pm_report(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreateReport)?; .with_lemmy_type(LemmyErrorType::CouldntCreateReport)?;
let private_message_report_view = let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report.id)
PrivateMessageReportView::read(&mut context.pool(), report.id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?;
// Email the admins // Email the admins
if local_site.reports_email_admins { if local_site.reports_email_admins {

View File

@ -28,8 +28,9 @@ pub async fn resolve_pm_report(
.with_lemmy_type(LemmyErrorType::CouldntResolveReport)?; .with_lemmy_type(LemmyErrorType::CouldntResolveReport)?;
} }
let private_message_report_view = let private_message_report_view = PrivateMessageReportView::read(&mut context.pool(), report_id)
PrivateMessageReportView::read(&mut context.pool(), report_id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessageReport)?;
Ok(Json(PrivateMessageReportResponse { Ok(Json(PrivateMessageReportResponse {
private_message_report_view, private_message_report_view,

View File

@ -5,13 +5,15 @@ use lemmy_api_common::{
utils::build_federated_instances, utils::build_federated_instances,
}; };
use lemmy_db_views::structs::SiteView; use lemmy_db_views::structs::SiteView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn get_federated_instances( pub async fn get_federated_instances(
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<GetFederatedInstancesResponse>> { ) -> LemmyResult<Json<GetFederatedInstancesResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let federated_instances = let federated_instances =
build_federated_instances(&site_view.local_site, &mut context.pool()).await?; build_federated_instances(&site_view.local_site, &mut context.pool()).await?;

View File

@ -55,7 +55,9 @@ pub async fn leave_admin(
ModAdd::create(&mut context.pool(), &form).await?; ModAdd::create(&mut context.pool(), &form).await?;
// Reread site and admins // Reread site and admins
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let admins = PersonView::admins(&mut context.pool()).await?; let admins = PersonView::admins(&mut context.pool()).await?;
let all_languages = Language::read_all(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?;

View File

@ -4,8 +4,7 @@ use lemmy_api_common::{
person::{ListMedia, ListMediaResponse}, person::{ListMedia, ListMediaResponse},
utils::is_admin, utils::is_admin,
}; };
use lemmy_db_schema::source::images::LocalImage; use lemmy_db_views::structs::{LocalImageView, LocalUserView};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
@ -19,6 +18,6 @@ pub async fn list_all_media(
let page = data.page; let page = data.page;
let limit = data.limit; let limit = data.limit;
let images = LocalImage::get_all(&mut context.pool(), page, limit).await?; let images = LocalImageView::get_all(&mut context.pool(), page, limit).await?;
Ok(Json(ListMediaResponse { images })) Ok(Json(ListMediaResponse { images }))
} }

View File

@ -15,7 +15,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::{CommentView, LocalUserView}; use lemmy_db_views::structs::{CommentView, LocalUserView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_comment( pub async fn purge_comment(
@ -29,7 +29,9 @@ pub async fn purge_comment(
let comment_id = data.comment_id; let comment_id = data.comment_id;
// Read the comment to get the post_id and community // Read the comment to get the post_id and community
let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?; let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
let post_id = comment_view.comment.post_id; let post_id = comment_view.comment.post_id;

View File

@ -16,7 +16,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_community( pub async fn purge_community(
@ -28,7 +28,9 @@ pub async fn purge_community(
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// Read the community to get its images // Read the community to get its images
let community = Community::read(&mut context.pool(), data.community_id).await?; let community = Community::read(&mut context.pool(), data.community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
if let Some(banner) = &community.banner { if let Some(banner) = &community.banner {
purge_image_from_pictrs(banner, &context).await.ok(); purge_image_from_pictrs(banner, &context).await.ok();

View File

@ -16,7 +16,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_person( pub async fn purge_person(
@ -27,7 +27,9 @@ pub async fn purge_person(
// Only let admin purge an item // Only let admin purge an item
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
let person = Person::read(&mut context.pool(), data.person_id).await?; let person = Person::read(&mut context.pool(), data.person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
ban_nonlocal_user_from_local_communities( ban_nonlocal_user_from_local_communities(
&local_user_view, &local_user_view,
&person, &person,

View File

@ -16,7 +16,7 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn purge_post( pub async fn purge_post(
@ -28,7 +28,9 @@ pub async fn purge_post(
is_admin(&local_user_view)?; is_admin(&local_user_view)?;
// Read the post to get the community_id // Read the post to get the community_id
let post = Post::read(&mut context.pool(), data.post_id).await?; let post = Post::read(&mut context.pool(), data.post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
// Purge image // Purge image
if let Some(url) = &post.url { if let Some(url) = &post.url {

View File

@ -13,7 +13,7 @@ use lemmy_db_schema::{
utils::diesel_option_overwrite, utils::diesel_option_overwrite,
}; };
use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView}; use lemmy_db_views::structs::{LocalUserView, RegistrationApplicationView};
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
pub async fn approve_registration_application( pub async fn approve_registration_application(
data: Json<ApproveRegistrationApplication>, data: Json<ApproveRegistrationApplication>,
@ -45,8 +45,9 @@ pub async fn approve_registration_application(
LocalUser::update(&mut context.pool(), approved_user_id, &local_user_form).await?; LocalUser::update(&mut context.pool(), approved_user_id, &local_user_form).await?;
if data.approve { if data.approve {
let approved_local_user_view = let approved_local_user_view = LocalUserView::read(&mut context.pool(), approved_user_id)
LocalUserView::read(&mut context.pool(), approved_user_id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindLocalUser)?;
if approved_local_user_view.local_user.email.is_some() { if approved_local_user_view.local_user.email.is_some() {
send_application_approved_email(&approved_local_user_view, context.settings()).await?; send_application_approved_email(&approved_local_user_view, context.settings()).await?;
@ -54,8 +55,9 @@ pub async fn approve_registration_application(
} }
// Read the view // Read the view
let registration_application = let registration_application = RegistrationApplicationView::read(&mut context.pool(), app_id)
RegistrationApplicationView::read(&mut context.pool(), app_id).await?; .await?
.ok_or(LemmyErrorType::CouldntFindRegistrationApplication)?;
Ok(Json(RegistrationApplicationResponse { Ok(Json(RegistrationApplicationResponse {
registration_application, registration_application,

View File

@ -25,7 +25,7 @@ full = [
"lemmy_db_views_moderator/full", "lemmy_db_views_moderator/full",
"lemmy_utils/full", "lemmy_utils/full",
"activitypub_federation", "activitypub_federation",
"encoding", "encoding_rs",
"reqwest-middleware", "reqwest-middleware",
"webpage", "webpage",
"ts-rs", "ts-rs",
@ -66,13 +66,13 @@ actix-web = { workspace = true, optional = true }
enum-map = { workspace = true } enum-map = { workspace = true }
urlencoding = { workspace = true } urlencoding = { workspace = true }
mime = { version = "0.3.17", optional = 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", "serde",
], optional = true } ], optional = true }
encoding = { version = "0.2.33", optional = true } encoding_rs = { version = "0.8.34", optional = true }
jsonwebtoken = { version = "8.3.0", optional = true } jsonwebtoken = { version = "9.3.0", optional = true }
# necessary for wasmt compilation # necessary for wasmt compilation
getrandom = { version = "0.2.12", features = ["js"] } getrandom = { version = "0.2.15", features = ["js"] }
[package.metadata.cargo-machete] [package.metadata.cargo-machete]
ignored = ["getrandom"] ignored = ["getrandom"]

View File

@ -27,6 +27,7 @@ use lemmy_db_views_actor::structs::CommunityView;
use lemmy_utils::{ use lemmy_utils::{
error::LemmyResult, error::LemmyResult,
utils::{markdown::markdown_to_html, mention::MentionData}, utils::{markdown::markdown_to_html, mention::MentionData},
LemmyErrorType,
}; };
pub async fn build_comment_response( pub async fn build_comment_response(
@ -36,7 +37,9 @@ pub async fn build_comment_response(
recipient_ids: Vec<LocalUserId>, recipient_ids: Vec<LocalUserId>,
) -> LemmyResult<CommentResponse> { ) -> LemmyResult<CommentResponse> {
let person_id = local_user_view.map(|l| l.person.id); let person_id = local_user_view.map(|l| l.person.id);
let comment_view = CommentView::read(&mut context.pool(), comment_id, person_id).await?; let comment_view = CommentView::read(&mut context.pool(), comment_id, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
Ok(CommentResponse { Ok(CommentResponse {
comment_view, comment_view,
recipient_ids, recipient_ids,
@ -58,7 +61,8 @@ pub async fn build_community_response(
Some(person_id), Some(person_id),
is_mod_or_admin, is_mod_or_admin,
) )
.await?; .await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?; let discussion_languages = CommunityLanguage::read(&mut context.pool(), community_id).await?;
Ok(Json(CommunityResponse { Ok(Json(CommunityResponse {
@ -82,7 +86,8 @@ pub async fn build_post_response(
Some(person.id), Some(person.id),
is_mod_or_admin, is_mod_or_admin,
) )
.await?; .await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
Ok(Json(PostResponse { post_view })) Ok(Json(PostResponse { post_view }))
} }
@ -99,7 +104,9 @@ pub async fn send_local_notifs(
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
// Read the comment view to get extra info // Read the comment view to get extra info
let comment_view = CommentView::read(&mut context.pool(), comment_id, None).await?; let comment_view = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
let comment = comment_view.comment; let comment = comment_view.comment;
let post = comment_view.post; let post = comment_view.post;
let community = comment_view.community; let community = comment_view.community;
@ -111,10 +118,11 @@ pub async fn send_local_notifs(
{ {
let mention_name = mention.name.clone(); let mention_name = mention.name.clone();
let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await; let user_view = LocalUserView::read_from_name(&mut context.pool(), &mention_name).await;
if let Ok(mention_user_view) = user_view { if let Ok(Some(mention_user_view)) = user_view {
// TODO // TODO
// At some point, make it so you can't tag the parent creator either // 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); recipient_ids.push(mention_user_view.local_user.id);
let user_mention_form = PersonMentionInsertForm { let user_mention_form = PersonMentionInsertForm {
@ -146,7 +154,9 @@ pub async fn send_local_notifs(
// Send comment_reply to the parent commenter / poster // Send comment_reply to the parent commenter / poster
if let Some(parent_comment_id) = comment.parent_comment_id() { if let Some(parent_comment_id) = comment.parent_comment_id() {
let parent_comment = Comment::read(&mut context.pool(), parent_comment_id).await?; let parent_comment = Comment::read(&mut context.pool(), parent_comment_id)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
// Get the parent commenter local_user // Get the parent commenter local_user
let parent_creator_id = parent_comment.creator_id; let parent_creator_id = parent_comment.creator_id;
@ -165,7 +175,7 @@ pub async fn send_local_notifs(
// Don't send a notif to yourself // Don't send a notif to yourself
if parent_comment.creator_id != person.id && !check_blocks { if parent_comment.creator_id != person.id && !check_blocks {
let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await; let user_view = LocalUserView::read_person(&mut context.pool(), parent_creator_id).await;
if let Ok(parent_user_view) = user_view { if let Ok(Some(parent_user_view)) = user_view {
// Don't duplicate notif if already mentioned by checking recipient ids // Don't duplicate notif if already mentioned by checking recipient ids
if !recipient_ids.contains(&parent_user_view.local_user.id) { if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id); recipient_ids.push(parent_user_view.local_user.id);
@ -212,7 +222,7 @@ pub async fn send_local_notifs(
if post.creator_id != person.id && !check_blocks { if post.creator_id != person.id && !check_blocks {
let creator_id = post.creator_id; let creator_id = post.creator_id;
let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await; let parent_user = LocalUserView::read_person(&mut context.pool(), creator_id).await;
if let Ok(parent_user_view) = parent_user { if let Ok(Some(parent_user_view)) = parent_user {
if !recipient_ids.contains(&parent_user_view.local_user.id) { if !recipient_ids.contains(&parent_user_view.local_user.id) {
recipient_ids.push(parent_user_view.local_user.id); recipient_ids.push(parent_user_view.local_user.id);

View File

@ -1,9 +1,10 @@
use crate::{context::LemmyContext, sensitive::Sensitive}; use crate::context::LemmyContext;
use actix_web::{http::header::USER_AGENT, HttpRequest}; use actix_web::{http::header::USER_AGENT, HttpRequest};
use chrono::Utc; use chrono::Utc;
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::LocalUserId, newtypes::LocalUserId,
sensitive::SensitiveString,
source::login_token::{LoginToken, LoginTokenCreateForm}, source::login_token::{LoginToken, LoginTokenCreateForm},
}; };
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
@ -40,7 +41,7 @@ impl Claims {
user_id: LocalUserId, user_id: LocalUserId,
req: HttpRequest, req: HttpRequest,
context: &LemmyContext, context: &LemmyContext,
) -> LemmyResult<Sensitive<String>> { ) -> LemmyResult<SensitiveString> {
let hostname = context.settings().hostname.clone(); let hostname = context.settings().hostname.clone();
let my_claims = Claims { let my_claims = Claims {
sub: user_id.0.to_string(), sub: user_id.0.to_string(),
@ -50,7 +51,7 @@ impl Claims {
let secret = &context.secret().jwt_secret; let secret = &context.secret().jwt_secret;
let key = EncodingKey::from_secret(secret.as_ref()); 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 let ip = req
.connection_info() .connection_info()
.realip_remote_addr() .realip_remote_addr()
@ -67,7 +68,7 @@ impl Claims {
user_agent, user_agent,
}; };
LoginToken::create(&mut context.pool(), form).await?; LoginToken::create(&mut context.pool(), form).await?;
Ok(Sensitive::new(token)) Ok(token)
} }
} }
@ -99,7 +100,7 @@ mod tests {
async fn test_should_not_validate_user_token_after_password_change() { async fn test_should_not_validate_user_token_after_password_change() {
let pool_ = build_db_pool_for_tests().await; let pool_ = build_db_pool_for_tests().await;
let pool = &mut (&pool_).into(); let pool = &mut (&pool_).into();
let secret = Secret::init(pool).await.unwrap(); let secret = Secret::init(pool).await.unwrap().unwrap();
let context = LemmyContext::create( let context = LemmyContext::create(
pool_.clone(), pool_.clone(),
ClientBuilder::new(Client::default()).build(), ClientBuilder::new(Client::default()).build(),

View File

@ -64,7 +64,7 @@ impl LemmyContext {
let client = ClientBuilder::new(client).build(); let client = ClientBuilder::new(client).build();
let secret = Secret { let secret = Secret {
id: 0, id: 0,
jwt_secret: String::new(), jwt_secret: String::new().into(),
}; };
let rate_limit_cell = RateLimitCell::with_test_config(); let rate_limit_cell = RateLimitCell::with_test_config();

View File

@ -14,7 +14,6 @@ pub mod private_message;
pub mod request; pub mod request;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod send_activity; pub mod send_activity;
pub mod sensitive;
pub mod site; pub mod site;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod utils; pub mod utils;

View File

@ -1,13 +1,13 @@
use crate::sensitive::Sensitive;
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId}, newtypes::{CommentReplyId, CommunityId, LanguageId, PersonId, PersonMentionId},
source::{images::LocalImage, site::Site}, sensitive::SensitiveString,
source::site::Site,
CommentSortType, CommentSortType,
ListingType, ListingType,
PostListingMode, PostListingMode,
SortType, SortType,
}; };
use lemmy_db_views::structs::{CommentView, PostView}; use lemmy_db_views::structs::{CommentView, LocalImageView, PostView};
use lemmy_db_views_actor::structs::{ use lemmy_db_views_actor::structs::{
CommentReplyView, CommentReplyView,
CommunityModeratorView, CommunityModeratorView,
@ -25,8 +25,8 @@ use ts_rs::TS;
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Logging into lemmy. /// Logging into lemmy.
pub struct Login { pub struct Login {
pub username_or_email: Sensitive<String>, pub username_or_email: SensitiveString,
pub password: Sensitive<String>, pub password: SensitiveString,
/// May be required, if totp is enabled for their account. /// May be required, if totp is enabled for their account.
pub totp_2fa_token: Option<String>, pub totp_2fa_token: Option<String>,
} }
@ -38,11 +38,11 @@ pub struct Login {
/// Register / Sign up to lemmy. /// Register / Sign up to lemmy.
pub struct Register { pub struct Register {
pub username: String, pub username: String,
pub password: Sensitive<String>, pub password: SensitiveString,
pub password_verify: Sensitive<String>, pub password_verify: SensitiveString,
pub show_nsfw: bool, pub show_nsfw: Option<bool>,
/// email is mandatory if email verification is enabled on the server /// 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. /// The UUID of the captcha item.
pub captcha_uuid: Option<String>, pub captcha_uuid: Option<String>,
/// Your captcha answer. /// 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. /// Your display name, which can contain strange characters, and does not need to be unique.
pub display_name: Option<String>, pub display_name: Option<String>,
/// Your email. /// Your email.
pub email: Option<Sensitive<String>>, pub email: Option<SensitiveString>,
/// Your bio / info, in markdown. /// Your bio / info, in markdown.
pub bio: Option<String>, pub bio: Option<String>,
/// Your matrix user id. Ex: @my_user:matrix.org /// Your matrix user id. Ex: @my_user:matrix.org
@ -124,7 +124,8 @@ pub struct SaveUserSettings {
pub post_listing_mode: Option<PostListingMode>, pub post_listing_mode: Option<PostListingMode>,
/// Whether to allow keyboard navigation (for browsing and interacting with posts and comments). /// Whether to allow keyboard navigation (for browsing and interacting with posts and comments).
pub enable_keyboard_navigation: Option<bool>, 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>, pub enable_animated_images: Option<bool>,
/// Whether to auto-collapse bot comments. /// Whether to auto-collapse bot comments.
pub collapse_bot_comments: Option<bool>, pub collapse_bot_comments: Option<bool>,
@ -140,9 +141,9 @@ pub struct SaveUserSettings {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Changes your account password. /// Changes your account password.
pub struct ChangePassword { pub struct ChangePassword {
pub new_password: Sensitive<String>, pub new_password: SensitiveString,
pub new_password_verify: Sensitive<String>, pub new_password_verify: SensitiveString,
pub old_password: Sensitive<String>, pub old_password: SensitiveString,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -151,8 +152,9 @@ pub struct ChangePassword {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// A response for your login. /// A response for your login.
pub struct LoginResponse { pub struct LoginResponse {
/// This is None in response to `Register` if email verification is enabled, or the server requires registration applications. /// This is None in response to `Register` if email verification is enabled, or the server
pub jwt: Option<Sensitive<String>>, /// requires registration applications.
pub jwt: Option<SensitiveString>,
/// If registration applications are required, this will return true for a signup response. /// If registration applications are required, this will return true for a signup response.
pub registration_created: bool, pub registration_created: bool,
/// If email verifications are required, this will return true for a signup response. /// 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))] #[cfg_attr(feature = "full", ts(export))]
/// Delete your account. /// Delete your account.
pub struct DeleteAccount { pub struct DeleteAccount {
pub password: Sensitive<String>, pub password: SensitiveString,
pub delete_content: bool, pub delete_content: bool,
} }
@ -349,7 +351,7 @@ pub struct DeleteAccount {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Reset your password via email. /// Reset your password via email.
pub struct PasswordReset { pub struct PasswordReset {
pub email: Sensitive<String>, pub email: SensitiveString,
} }
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
@ -357,9 +359,9 @@ pub struct PasswordReset {
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Change your password after receiving a reset request. /// Change your password after receiving a reset request.
pub struct PasswordChangeAfterReset { pub struct PasswordChangeAfterReset {
pub token: Sensitive<String>, pub token: SensitiveString,
pub password: Sensitive<String>, pub password: SensitiveString,
pub password_verify: Sensitive<String>, pub password_verify: SensitiveString,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -405,7 +407,7 @@ pub struct VerifyEmail {
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
pub struct GenerateTotpSecretResponse { pub struct GenerateTotpSecretResponse {
pub totp_secret_url: Sensitive<String>, pub totp_secret_url: SensitiveString,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
@ -437,5 +439,5 @@ pub struct ListMedia {
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
pub struct ListMediaResponse { pub struct ListMediaResponse {
pub images: Vec<LocalImage>, pub images: Vec<LocalImageView>,
} }

View File

@ -270,8 +270,6 @@ pub struct LinkMetadata {
#[serde(flatten)] #[serde(flatten)]
pub opengraph_data: OpenGraphData, pub opengraph_data: OpenGraphData,
pub content_type: Option<String>, pub content_type: Option<String>,
#[serde(skip)]
pub thumbnail: Option<DbUrl>,
} }
#[skip_serializing_none] #[skip_serializing_none]

View File

@ -3,10 +3,11 @@ use crate::{
lemmy_db_schema::traits::Crud, lemmy_db_schema::traits::Crud,
post::{LinkMetadata, OpenGraphData}, post::{LinkMetadata, OpenGraphData},
send_activity::{ActivityChannel, SendActivityData}, 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 activitypub_federation::config::Data;
use encoding::{all::encodings, DecoderTrap}; use chrono::{DateTime, Utc};
use encoding_rs::{Encoding, UTF_8};
use lemmy_db_schema::{ use lemmy_db_schema::{
newtypes::DbUrl, newtypes::DbUrl,
source::{ source::{
@ -18,14 +19,13 @@ use lemmy_db_schema::{
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyError, LemmyErrorType, LemmyResult}, error::{LemmyError, LemmyErrorType, LemmyResult},
settings::structs::{PictrsImageMode, Settings}, settings::structs::{PictrsImageMode, Settings},
spawn_try_task,
REQWEST_TIMEOUT, REQWEST_TIMEOUT,
VERSION, VERSION,
}; };
use mime::Mime; use mime::Mime;
use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder}; use reqwest::{header::CONTENT_TYPE, Client, ClientBuilder};
use reqwest_middleware::ClientWithMiddleware; use reqwest_middleware::ClientWithMiddleware;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use tracing::info; use tracing::info;
use url::Url; use url::Url;
use urlencoding::encode; use urlencoding::encode;
@ -42,11 +42,7 @@ pub fn client_builder(settings: &Settings) -> ClientBuilder {
/// Fetches metadata for the given link and optionally generates thumbnail. /// Fetches metadata for the given link and optionally generates thumbnail.
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn fetch_link_metadata( pub async fn fetch_link_metadata(url: &Url, context: &LemmyContext) -> LemmyResult<LinkMetadata> {
url: &Url,
generate_thumbnail: bool,
context: &LemmyContext,
) -> LemmyResult<LinkMetadata> {
info!("Fetching site metadata for url: {}", url); info!("Fetching site metadata for url: {}", url);
let response = context.client().get(url.as_str()).send().await?; let response = context.client().get(url.as_str()).send().await?;
@ -63,80 +59,62 @@ pub async fn fetch_link_metadata(
let opengraph_data = extract_opengraph_data(&html_bytes, url) let opengraph_data = extract_opengraph_data(&html_bytes, url)
.map_err(|e| info!("{e}")) .map_err(|e| info!("{e}"))
.unwrap_or_default(); .unwrap_or_default();
let thumbnail =
extract_thumbnail_from_opengraph_data(url, &opengraph_data, generate_thumbnail, context).await;
Ok(LinkMetadata { Ok(LinkMetadata {
opengraph_data, opengraph_data,
content_type: content_type.map(|c| c.to_string()), content_type: content_type.map(|c| c.to_string()),
thumbnail,
}) })
} }
#[tracing::instrument(skip_all)] /// Generates and saves a post thumbnail and metadata.
pub async fn fetch_link_metadata_opt(
url: Option<&Url>,
generate_thumbnail: bool,
context: &LemmyContext,
) -> LinkMetadata {
match &url {
Some(url) => fetch_link_metadata(url, generate_thumbnail, context)
.await
.unwrap_or_default(),
_ => Default::default(),
}
}
/// Generate post thumbnail in background task, because some sites can be very slow to respond.
/// ///
/// Takes a callback to generate a send activity task, so that post can be federated with metadata. /// Takes a callback to generate a send activity task, so that post can be federated with metadata.
pub fn generate_post_link_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 async fn generate_post_link_metadata(
post: Post, post: Post,
custom_thumbnail: Option<Url>, custom_thumbnail: Option<Url>,
send_activity: impl FnOnce(Post) -> Option<SendActivityData> + Send + 'static, send_activity: impl FnOnce(Post) -> Option<SendActivityData> + Send + 'static,
local_site: Option<LocalSite>, local_site: Option<LocalSite>,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) { ) -> LemmyResult<()> {
spawn_try_task(async move { let metadata = match &post.url {
// Decide if the thumbnail should be generated Some(url) => fetch_link_metadata(url, &context).await.unwrap_or_default(),
let allow_sensitive = local_site_opt_to_sensitive(&local_site); _ => Default::default(),
let page_is_sensitive = post.nsfw;
let allow_generate_thumbnail = allow_sensitive || !page_is_sensitive;
let do_generate_thumbnail =
allow_generate_thumbnail && custom_thumbnail.is_none() && post.thumbnail_url.is_none();
// Generate local thumbnail only if no thumbnail was federated and 'sensitive' attributes allow it.
let metadata = fetch_link_metadata_opt(
post.url.as_ref().map(DbUrl::inner),
do_generate_thumbnail,
&context,
)
.await;
// If its an image post, it needs to overwrite the thumbnail, and take precedence
let image_url = if metadata
.content_type
.as_ref()
.is_some_and(|content_type| content_type.starts_with("image"))
{
post.url.map(Into::into)
} else {
None
}; };
// Build the thumbnail url based on either the post image url, custom thumbnail, metadata fetch, or existing thumbnail. let is_image_post = metadata
let thumbnail_url = image_url .content_type
.or(custom_thumbnail) .as_ref()
.or(metadata.thumbnail.map(Into::into)) .is_some_and(|content_type| content_type.starts_with("image"));
.or(post.thumbnail_url.map(Into::into));
// Proxy the image fetch if necessary // Decide if we are allowed to generate local thumbnail
let proxied_thumbnail_url = proxy_image_link_opt_apub(thumbnail_url, &context).await?; let allow_sensitive = local_site_opt_to_sensitive(&local_site);
let allow_generate_thumbnail = allow_sensitive || !post.nsfw;
let image_url = if is_image_post {
post.url
} else {
metadata.opengraph_data.image.clone()
};
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 { let form = PostUpdateForm {
embed_title: Some(metadata.opengraph_data.title), embed_title: Some(metadata.opengraph_data.title),
embed_description: Some(metadata.opengraph_data.description), embed_description: Some(metadata.opengraph_data.description),
embed_video_url: Some(metadata.opengraph_data.embed_video_url), 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), url_content_type: Some(metadata.content_type),
..Default::default() ..Default::default()
}; };
@ -145,36 +123,21 @@ pub fn generate_post_link_metadata(
ActivityChannel::submit_activity(send_activity, &context).await?; ActivityChannel::submit_activity(send_activity, &context).await?;
} }
Ok(()) Ok(())
});
} }
/// Extract site metadata from HTML Opengraph attributes. /// Extract site metadata from HTML Opengraph attributes.
fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraphData> { fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraphData> {
let html = String::from_utf8_lossy(html_bytes); 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)?; 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 // 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 // proper encoding. If the specified encoding cannot be found, fall back to the original UTF-8
// version. // version.
if let Some(charset) = page.meta.get("charset") { if let Some(charset) = page.meta.get("charset") {
if charset.to_lowercase() != "utf-8" { if charset != UTF_8.name() {
if let Some(encoding_ref) = encodings().iter().find(|e| e.name() == charset) { if let Some(encoding) = Encoding::for_label(charset.as_bytes()) {
if let Ok(html_with_encoding) = encoding_ref.decode(html_bytes, DecoderTrap::Replace) { page = HTML::from_string(encoding.decode(html_bytes).0.into(), None)?;
page = HTML::from_string(html_with_encoding, None)?;
}
} }
} }
} }
@ -213,41 +176,40 @@ fn extract_opengraph_data(html_bytes: &[u8], url: &Url) -> LemmyResult<OpenGraph
}) })
} }
#[tracing::instrument(skip_all)] #[derive(Deserialize, Serialize, Debug)]
pub async fn extract_thumbnail_from_opengraph_data( pub struct PictrsResponse {
url: &Url, pub files: Option<Vec<PictrsFile>>,
opengraph_data: &OpenGraphData, pub msg: String,
generate_thumbnail: bool, }
context: &LemmyContext,
) -> Option<DbUrl> { #[derive(Deserialize, Serialize, Debug)]
if generate_thumbnail { pub struct PictrsFile {
let image_url = opengraph_data pub file: String,
.image pub delete_token: String,
.as_ref() pub details: PictrsFileDetails,
.map(DbUrl::inner) }
.unwrap_or(url);
generate_pictrs_thumbnail(image_url, context) impl PictrsFile {
.await pub fn thumbnail_url(&self, protocol_and_hostname: &str) -> Result<Url, url::ParseError> {
.ok() Url::parse(&format!(
.map(Into::into) "{protocol_and_hostname}/pictrs/image/{}",
} else { self.file
opengraph_data.image.clone() ))
} }
} }
#[derive(Deserialize, Debug)] /// Stores extra details about a Pictrs image.
struct PictrsResponse { #[derive(Deserialize, Serialize, Debug)]
files: Vec<PictrsFile>, pub struct PictrsFileDetails {
msg: String, /// In pixels
pub width: u16,
/// In pixels
pub height: u16,
pub content_type: String,
pub created_at: DateTime<Utc>,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Serialize, Debug)]
struct PictrsFile {
file: String,
delete_token: String,
}
#[derive(Deserialize, Debug)]
struct PictrsPurgeResponse { struct PictrsPurgeResponse {
msg: String, msg: String,
} }
@ -329,33 +291,34 @@ async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> L
encode(image_url.as_str()) encode(image_url.as_str())
); );
let response = context let res = context
.client() .client()
.get(&fetch_url) .get(&fetch_url)
.timeout(REQWEST_TIMEOUT) .timeout(REQWEST_TIMEOUT)
.send() .send()
.await?
.json::<PictrsResponse>()
.await?; .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 { 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, local_user_id: None,
pictrs_alias: uploaded_image.file.to_string(), pictrs_alias: image.file.clone(),
pictrs_delete_token: uploaded_image.delete_token.to_string(), 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?; LocalImage::create(&mut context.pool(), &form).await?;
}
Ok(thumbnail_url) Ok(thumbnail_url)
} else {
Err(LemmyErrorType::PictrsResponseError(response.msg))?
}
} }
// TODO: get rid of this by reading content type from db // TODO: get rid of this by reading content type from db
@ -414,9 +377,7 @@ mod tests {
async fn test_link_metadata() { async fn test_link_metadata() {
let context = LemmyContext::init_test_context().await; let context = LemmyContext::init_test_context().await;
let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap(); let sample_url = Url::parse("https://gitlab.com/IzzyOnDroid/repo/-/wikis/FAQ").unwrap();
let sample_res = fetch_link_metadata(&sample_url, false, &context) let sample_res = fetch_link_metadata(&sample_url, &context).await.unwrap();
.await
.unwrap();
assert_eq!( assert_eq!(
Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()), Some("FAQ · Wiki · IzzyOnDroid / repo · GitLab".to_string()),
sample_res.opengraph_data.title sample_res.opengraph_data.title
@ -438,17 +399,8 @@ mod tests {
Some(mime::TEXT_HTML_UTF_8.to_string()), Some(mime::TEXT_HTML_UTF_8.to_string()),
sample_res.content_type sample_res.content_type
); );
assert!(sample_res.thumbnail.is_some());
} }
// #[test]
// fn test_pictshare() {
// let res = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpg");
// assert!(res.is_ok());
// let res_other = fetch_pictshare("https://upload.wikimedia.org/wikipedia/en/2/27/The_Mandalorian_logo.jpgaoeu");
// assert!(res_other.is_err());
// }
#[test] #[test]
fn test_resolve_image_url() { fn test_resolve_image_url() {
// url that lists the opengraph fields // url that lists the opengraph fields

View File

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

View File

@ -375,7 +375,8 @@ impl From<FederationQueueState> for ReadableFederationState {
pub struct InstanceWithFederationState { pub struct InstanceWithFederationState {
#[serde(flatten)] #[serde(flatten)]
pub instance: Instance, 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>, pub federation_state: Option<ReadableFederationState>,
} }

View File

@ -6,13 +6,14 @@ use crate::{
use chrono::{DateTime, Days, Local, TimeZone, Utc}; use chrono::{DateTime, Days, Local, TimeZone, Utc};
use enum_map::{enum_map, EnumMap}; use enum_map::{enum_map, EnumMap};
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId}, newtypes::{CommunityId, DbUrl, InstanceId, PersonId, PostId},
source::{ source::{
comment::{Comment, CommentUpdateForm}, comment::{Comment, CommentUpdateForm},
community::{Community, CommunityModerator, CommunityUpdateForm}, community::{Community, CommunityModerator, CommunityUpdateForm},
community_block::CommunityBlock, community_block::CommunityBlock,
email_verification::{EmailVerification, EmailVerificationForm}, email_verification::{EmailVerification, EmailVerificationForm},
images::{LocalImage, RemoteImage}, images::RemoteImage,
instance::Instance, instance::Instance,
instance_block::InstanceBlock, instance_block::InstanceBlock,
local_site::LocalSite, local_site::LocalSite,
@ -27,7 +28,10 @@ use lemmy_db_schema::{
traits::Crud, traits::Crud,
utils::DbPool, utils::DbPool,
}; };
use lemmy_db_views::{comment_view::CommentQuery, structs::LocalUserView}; use lemmy_db_views::{
comment_view::CommentQuery,
structs::{LocalImageView, LocalUserView},
};
use lemmy_db_views_actor::structs::{ use lemmy_db_views_actor::structs::{
CommunityModeratorView, CommunityModeratorView,
CommunityPersonBanView, CommunityPersonBanView,
@ -136,13 +140,7 @@ pub fn is_top_mod(
} }
} }
#[tracing::instrument(skip_all)] /// Marks a post as read for a given person.
pub async fn get_post(post_id: PostId, pool: &mut DbPool<'_>) -> LemmyResult<Post> {
Post::read(pool, post_id)
.await
.with_lemmy_type(LemmyErrorType::CouldntFindPost)
}
#[tracing::instrument(skip_all)] #[tracing::instrument(skip_all)]
pub async fn mark_post_as_read( pub async fn mark_post_as_read(
person_id: PersonId, person_id: PersonId,
@ -155,6 +153,28 @@ pub async fn mark_post_as_read(
Ok(()) 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<()> { pub fn check_user_valid(person: &Person) -> LemmyResult<()> {
// Check for a site ban // Check for a site ban
if person.banned { if person.banned {
@ -188,8 +208,8 @@ async fn check_community_deleted_removed(
pool: &mut DbPool<'_>, pool: &mut DbPool<'_>,
) -> LemmyResult<()> { ) -> LemmyResult<()> {
let community = Community::read(pool, community_id) let community = Community::read(pool, community_id)
.await .await?
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; .ok_or(LemmyErrorType::CouldntFindCommunity)?;
if community.deleted || community.removed { if community.deleted || community.removed {
Err(LemmyErrorType::Deleted)? Err(LemmyErrorType::Deleted)?
} }
@ -353,7 +373,8 @@ pub async fn build_federated_instances(
federation_state: federation_state.map(std::convert::Into::into), federation_state: federation_state.map(std::convert::Into::into),
}; };
if is_blocked { 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); blocked.push(i);
} else if is_allowed { } else if is_allowed {
allowed.push(i.clone()); allowed.push(i.clone());
@ -437,7 +458,7 @@ pub async fn send_password_reset_email(
// Insert the row after successful send, to avoid using daily reset limit while // Insert the row after successful send, to avoid using daily reset limit while
// email sending is broken. // email sending is broken.
let local_user_id = user.local_user.id; 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(()) Ok(())
} }
@ -533,25 +554,8 @@ pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet>
.try_get_with::<_, LemmyError>((), async { .try_get_with::<_, LemmyError>((), async {
let urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?; let urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
let regexes = urls.iter().map(|url| { // The urls are already validated on saving, so just escape them.
let url = &url.url; let regexes = urls.iter().map(|url| escape(&url.url));
let parsed = Url::parse(url).expect("Coundln't parse URL.");
if url.ends_with('/') {
format!(
"({}://)?{}{}?",
parsed.scheme(),
escape(parsed.domain().expect("No domain.")),
escape(parsed.path())
)
} else {
format!(
"({}://)?{}{}",
parsed.scheme(),
escape(parsed.domain().expect("No domain.")),
escape(parsed.path())
)
}
});
let set = RegexSet::new(regexes)?; let set = RegexSet::new(regexes)?;
Ok(set) Ok(set)
@ -660,13 +664,18 @@ pub async fn purge_image_posts_for_person(
/// Delete a local_user's images /// Delete a local_user's images
async fn delete_local_user_images(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> { async fn delete_local_user_images(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> {
if let Ok(local_user) = LocalUserView::read_person(&mut context.pool(), person_id).await { if let Ok(Some(local_user)) = LocalUserView::read_person(&mut context.pool(), person_id).await {
let pictrs_uploads = let pictrs_uploads =
LocalImage::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id).await?; LocalImageView::get_all_by_local_user_id(&mut context.pool(), local_user.local_user.id)
.await?;
// Delete their images // Delete their images
for upload in pictrs_uploads { for upload in pictrs_uploads {
delete_image_from_pictrs(&upload.pictrs_alias, &upload.pictrs_delete_token, context) delete_image_from_pictrs(
&upload.local_image.pictrs_alias,
&upload.local_image.pictrs_delete_token,
context,
)
.await .await
.ok(); .ok();
} }
@ -700,7 +709,9 @@ pub async fn remove_user_data(
) -> LemmyResult<()> { ) -> LemmyResult<()> {
let pool = &mut context.pool(); let pool = &mut context.pool();
// Purge user images // Purge user images
let person = Person::read(pool, banned_person_id).await?; let person = Person::read(pool, banned_person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
if let Some(avatar) = person.avatar { if let Some(avatar) = person.avatar {
purge_image_from_pictrs(&avatar, context).await.ok(); purge_image_from_pictrs(&avatar, context).await.ok();
} }
@ -813,7 +824,9 @@ pub async fn remove_user_data_in_community(
pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> { pub async fn purge_user_account(person_id: PersonId, context: &LemmyContext) -> LemmyResult<()> {
let pool = &mut context.pool(); let pool = &mut context.pool();
let person = Person::read(pool, person_id).await?; let person = Person::read(pool, person_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
// Delete their local images, if they're a local user // Delete their local images, if they're a local user
delete_local_user_images(person_id, context).await.ok(); delete_local_user_images(person_id, context).await.ok();
@ -959,8 +972,8 @@ pub async fn process_markdown_opt(
/// A wrapper for `proxy_image_link` for use in tests. /// 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 /// The parameter `force_image_proxy` is the config value of `pictrs.image_proxy`. Its necessary to
/// as separate parameter so it can be changed in tests. /// pass as separate parameter so it can be changed in tests.
async fn proxy_image_link_internal( async fn proxy_image_link_internal(
link: Url, link: Url,
image_mode: PictrsImageMode, image_mode: PictrsImageMode,
@ -970,13 +983,10 @@ async fn proxy_image_link_internal(
if link.domain() == Some(&context.settings().hostname) { if link.domain() == Some(&context.settings().hostname) {
Ok(link.into()) Ok(link.into())
} else if image_mode == PictrsImageMode::ProxyAllImages { } else if image_mode == PictrsImageMode::ProxyAllImages {
let proxied = format!( let proxied = build_proxied_image_url(&link, &context.settings().get_protocol_and_hostname())?;
"{}/api/v3/image_proxy?url={}",
context.settings().get_protocol_and_hostname(),
encode(link.as_str())
);
RemoteImage::create(&mut context.pool(), vec![link]).await?; RemoteImage::create(&mut context.pool(), vec![link]).await?;
Ok(Url::parse(&proxied)?.into()) Ok(proxied.into())
} else { } else {
Ok(link.into()) Ok(link.into())
} }
@ -1030,6 +1040,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)] #[cfg(test)]
#[allow(clippy::unwrap_used)] #[allow(clippy::unwrap_used)]
#[allow(clippy::indexing_slicing)] #[allow(clippy::indexing_slicing)]

View File

@ -9,11 +9,11 @@ use lemmy_api_common::{
check_community_user_action, check_community_user_action,
check_post_deleted_or_removed, check_post_deleted_or_removed,
generate_local_apub_endpoint, generate_local_apub_endpoint,
get_post,
get_url_blocklist, get_url_blocklist,
is_mod_or_admin, is_mod_or_admin,
local_site_to_slur_regex, local_site_to_slur_regex,
process_markdown, process_markdown,
update_read_comments,
EndpointType, EndpointType,
}, },
}; };
@ -28,7 +28,7 @@ use lemmy_db_schema::{
}, },
traits::{Crud, Likeable}, traits::{Crud, Likeable},
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::{LocalUserView, PostView};
use lemmy_utils::{ use lemmy_utils::{
error::{LemmyErrorExt, LemmyErrorType, LemmyResult}, error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field}, utils::{mention::scrape_text_for_mentions, validation::is_valid_body_field},
@ -51,8 +51,19 @@ pub async fn create_comment(
// Check for a community ban // Check for a community ban
let post_id = data.post_id; 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_community_user_action(&local_user_view.person, community_id, &mut context.pool()).await?;
check_post_deleted_or_removed(&post)?; check_post_deleted_or_removed(&post)?;
@ -70,7 +81,8 @@ pub async fn create_comment(
Comment::read(&mut context.pool(), parent_id).await.ok() Comment::read(&mut context.pool(), parent_id).await.ok()
} else { } else {
None None
}; }
.flatten();
// If there's a parent_id, check to make sure that comment is in that post // If there's a parent_id, check to make sure that comment is in that post
// Strange issue where sometimes the post ID of the parent comment is incorrect // Strange issue where sometimes the post ID of the parent comment is incorrect
@ -163,6 +175,15 @@ pub async fn create_comment(
) )
.await?; .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, // 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), // (ie we're the grandparent, or the recipient of the parent comment_reply),
// then mark the parent as read. // then mark the parent as read.
@ -172,7 +193,7 @@ pub async fn create_comment(
let parent_id = parent.id; let parent_id = parent.id;
let comment_reply = let comment_reply =
CommentReply::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await; CommentReply::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
if let Ok(reply) = comment_reply { if let Ok(Some(reply)) = comment_reply {
CommentReply::update( CommentReply::update(
&mut context.pool(), &mut context.pool(),
reply.id, reply.id,
@ -185,7 +206,7 @@ pub async fn create_comment(
// If the parent has PersonMentions mark them as read too // If the parent has PersonMentions mark them as read too
let person_mention = let person_mention =
PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await; PersonMention::read_by_comment_and_person(&mut context.pool(), parent_id, person_id).await;
if let Ok(mention) = person_mention { if let Ok(Some(mention)) = person_mention {
PersonMention::update( PersonMention::update(
&mut context.pool(), &mut context.pool(),
mention.id, mention.id,

View File

@ -21,7 +21,9 @@ pub async fn delete_comment(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> { ) -> LemmyResult<Json<CommentResponse>> {
let comment_id = data.comment_id; let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?; let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
// Dont delete it if its already been deleted. // Dont delete it if its already been deleted.
if orig_comment.comment.deleted == data.deleted { if orig_comment.comment.deleted == data.deleted {

View File

@ -25,7 +25,9 @@ pub async fn remove_comment(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommentResponse>> { ) -> LemmyResult<Json<CommentResponse>> {
let comment_id = data.comment_id; let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?; let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,
@ -35,6 +37,12 @@ pub async fn remove_comment(
) )
.await?; .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 // Do the remove
let removed = data.removed; let removed = data.removed;
let updated_comment = Comment::update( let updated_comment = Comment::update(

View File

@ -36,7 +36,9 @@ pub async fn update_comment(
let local_site = LocalSite::read(&mut context.pool()).await?; let local_site = LocalSite::read(&mut context.pool()).await?;
let comment_id = data.comment_id; let comment_id = data.comment_id;
let orig_comment = CommentView::read(&mut context.pool(), comment_id, None).await?; let orig_comment = CommentView::read(&mut context.pool(), comment_id, None)
.await?
.ok_or(LemmyErrorType::CouldntFindComment)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,

View File

@ -46,7 +46,9 @@ pub async fn create_community(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<CommunityResponse>> { ) -> LemmyResult<Json<CommunityResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let local_site = site_view.local_site; let local_site = site_view.local_site;
if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() { if local_site.community_creation_admin_only && is_admin(&local_user_view).is_err() {

View File

@ -6,7 +6,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_views::structs::{LocalUserView, SiteView}; use lemmy_db_views::structs::{LocalUserView, SiteView};
use lemmy_db_views_actor::community_view::CommunityQuery; use lemmy_db_views_actor::community_view::CommunityQuery;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn list_communities( pub async fn list_communities(
@ -14,7 +14,9 @@ pub async fn list_communities(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<ListCommunitiesResponse>> { ) -> LemmyResult<Json<ListCommunitiesResponse>> {
let local_site = SiteView::read_local(&mut context.pool()).await?; let local_site = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let is_admin = local_user_view let is_admin = local_user_view
.as_ref() .as_ref()
.map(|luv| is_admin(luv).is_ok()) .map(|luv| is_admin(luv).is_ok())

View File

@ -43,7 +43,9 @@ pub async fn update_community(
let description = let description =
process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?; process_markdown_opt(&data.description, &slur_regex, &url_blocklist, &context).await?;
is_valid_body_field(&data.description, false)?; is_valid_body_field(&data.description, false)?;
let old_community = Community::read(&mut context.pool(), data.community_id).await?; 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.icon, &old_community.icon, &context).await?;
replace_image(&data.banner, &old_community.banner, &context).await?; replace_image(&data.banner, &old_community.banner, &context).await?;

View File

@ -14,7 +14,6 @@ use lemmy_api_common::{
local_site_to_slur_regex, local_site_to_slur_regex,
mark_post_as_read, mark_post_as_read,
process_markdown_opt, process_markdown_opt,
proxy_image_link_opt_apub,
EndpointType, EndpointType,
}, },
}; };
@ -75,7 +74,6 @@ pub async fn create_post(
is_url_blocked(&url, &url_blocklist)?; is_url_blocked(&url, &url_blocklist)?;
check_url_scheme(&url)?; check_url_scheme(&url)?;
check_url_scheme(&custom_thumbnail)?; check_url_scheme(&custom_thumbnail)?;
let url = proxy_image_link_opt_apub(url, &context).await?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -85,7 +83,9 @@ pub async fn create_post(
.await?; .await?;
let community_id = data.community_id; let community_id = data.community_id;
let community = Community::read(&mut context.pool(), community_id).await?; let community = Community::read(&mut context.pool(), community_id)
.await?
.ok_or(LemmyErrorType::CouldntFindCommunity)?;
if community.posting_restricted_to_mods { if community.posting_restricted_to_mods {
let community_id = data.community_id; let community_id = data.community_id;
let is_mod = CommunityModeratorView::is_community_moderator( let is_mod = CommunityModeratorView::is_community_moderator(
@ -123,7 +123,7 @@ pub async fn create_post(
let post_form = PostInsertForm::builder() let post_form = PostInsertForm::builder()
.name(data.name.trim().to_string()) .name(data.name.trim().to_string())
.url(url) .url(url.map(Into::into))
.body(body) .body(body)
.alt_text(data.alt_text.clone()) .alt_text(data.alt_text.clone())
.community_id(data.community_id) .community_id(data.community_id)
@ -160,7 +160,8 @@ pub async fn create_post(
|post| Some(SendActivityData::CreatePost(post)), |post| Some(SendActivityData::CreatePost(post)),
Some(local_site), Some(local_site),
context.reset_request_count(), context.reset_request_count(),
); )
.await?;
// They like their own post by default // They like their own post by default
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
@ -175,7 +176,6 @@ pub async fn create_post(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntLikePost)?; .with_lemmy_type(LemmyErrorType::CouldntLikePost)?;
// Mark the post as read
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; mark_post_as_read(person_id, post_id, &mut context.pool()).await?;
if let Some(url) = updated_post.url.clone() { if let Some(url) = updated_post.url.clone() {

View File

@ -21,7 +21,9 @@ pub async fn delete_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?; let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
// Dont delete it if its already been deleted. // Dont delete it if its already been deleted.
if orig_post.deleted == data.deleted { if orig_post.deleted == data.deleted {

View File

@ -2,10 +2,9 @@ use actix_web::web::{Data, Json, Query};
use lemmy_api_common::{ use lemmy_api_common::{
context::LemmyContext, context::LemmyContext,
post::{GetPost, GetPostResponse}, 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::{ use lemmy_db_schema::{
aggregates::structs::{PersonPostAggregates, PersonPostAggregatesForm},
source::{comment::Comment, post::Post}, source::{comment::Comment, post::Post},
traits::Crud, traits::Crud,
}; };
@ -14,7 +13,7 @@ use lemmy_db_views::{
structs::{LocalUserView, PostView, SiteView}, structs::{LocalUserView, PostView, SiteView},
}; };
use lemmy_db_views_actor::structs::{CommunityModeratorView, CommunityView}; 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))] #[tracing::instrument(skip(context))]
pub async fn get_post( pub async fn get_post(
@ -22,7 +21,9 @@ pub async fn get_post(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
) -> LemmyResult<Json<GetPostResponse>> { ) -> LemmyResult<Json<GetPostResponse>> {
let local_site = SiteView::read_local(&mut context.pool()).await?; let local_site = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
check_private_instance(&local_user_view, &local_site.local_site)?; check_private_instance(&local_user_view, &local_site.local_site)?;
@ -33,15 +34,19 @@ pub async fn get_post(
id id
} else if let Some(comment_id) = data.comment_id { } else if let Some(comment_id) = data.comment_id {
Comment::read(&mut context.pool(), comment_id) Comment::read(&mut context.pool(), comment_id)
.await .await?
.with_lemmy_type(LemmyErrorType::CouldntFindPost)? .ok_or(LemmyErrorType::CouldntFindComment)?
.post_id .post_id
} else { } else {
Err(LemmyErrorType::CouldntFindPost)? Err(LemmyErrorType::CouldntFindPost)?
}; };
// Check to see if the person is a mod or admin, to show deleted / removed // Check to see if the person is a mod or admin, to show deleted / removed
let community_id = Post::read(&mut context.pool(), post_id).await?.community_id; let community_id = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?
.community_id;
let is_mod_or_admin = is_mod_or_admin_opt( let is_mod_or_admin = is_mod_or_admin_opt(
&mut context.pool(), &mut context.pool(),
local_user_view.as_ref(), local_user_view.as_ref(),
@ -51,13 +56,20 @@ pub async fn get_post(
.is_ok(); .is_ok();
let post_view = PostView::read(&mut context.pool(), post_id, person_id, is_mod_or_admin) let post_view = PostView::read(&mut context.pool(), post_id, person_id, is_mod_or_admin)
.await .await?
.with_lemmy_type(LemmyErrorType::CouldntFindPost)?; .ok_or(LemmyErrorType::CouldntFindPost)?;
// Mark the post as read
let post_id = post_view.post.id; let post_id = post_view.post.id;
if let Some(person_id) = person_id { if let Some(person_id) = person_id {
mark_post_as_read(person_id, post_id, &mut context.pool()).await?; 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 // Necessary for the sidebar subscribed
@ -67,23 +79,8 @@ pub async fn get_post(
person_id, person_id,
is_mod_or_admin, is_mod_or_admin,
) )
.await .await?
.with_lemmy_type(LemmyErrorType::CouldntFindCommunity)?; .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?; let moderators = CommunityModeratorView::for_community(&mut context.pool(), community_id).await?;

View File

@ -16,7 +16,7 @@ use lemmy_db_schema::{
traits::{Crud, Reportable}, traits::{Crud, Reportable},
}; };
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult; use lemmy_utils::{error::LemmyResult, LemmyErrorType};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn remove_post( pub async fn remove_post(
@ -25,7 +25,9 @@ pub async fn remove_post(
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<PostResponse>> { ) -> LemmyResult<Json<PostResponse>> {
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?; let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_mod_action( check_community_mod_action(
&local_user_view.person, &local_user_view.person,

View File

@ -11,7 +11,6 @@ use lemmy_api_common::{
get_url_blocklist, get_url_blocklist,
local_site_to_slur_regex, local_site_to_slur_regex,
process_markdown_opt, process_markdown_opt,
proxy_image_link_opt_apub,
}, },
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
@ -70,7 +69,9 @@ pub async fn update_post(
check_url_scheme(&custom_thumbnail)?; check_url_scheme(&custom_thumbnail)?;
let post_id = data.post_id; let post_id = data.post_id;
let orig_post = Post::read(&mut context.pool(), post_id).await?; let orig_post = Post::read(&mut context.pool(), post_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPost)?;
check_community_user_action( check_community_user_action(
&local_user_view.person, &local_user_view.person,
@ -84,11 +85,6 @@ pub async fn update_post(
Err(LemmyErrorType::NoPostEditAllowed)? 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; let language_id = data.language_id;
CommunityLanguage::is_allowed_community_language( CommunityLanguage::is_allowed_community_language(
&mut context.pool(), &mut context.pool(),
@ -99,7 +95,7 @@ pub async fn update_post(
let post_form = PostUpdateForm { let post_form = PostUpdateForm {
name: data.name.clone(), name: data.name.clone(),
url, url: Some(url.map(Into::into)),
body: diesel_option_overwrite(body), body: diesel_option_overwrite(body),
alt_text: diesel_option_overwrite(data.alt_text.clone()), alt_text: diesel_option_overwrite(data.alt_text.clone()),
nsfw: data.nsfw, nsfw: data.nsfw,
@ -119,7 +115,8 @@ pub async fn update_post(
|post| Some(SendActivityData::UpdatePost(post)), |post| Some(SendActivityData::UpdatePost(post)),
Some(local_site), Some(local_site),
context.reset_request_count(), context.reset_request_count(),
); )
.await?;
build_post_response( build_post_response(
context.deref(), context.deref(),

View File

@ -76,12 +76,16 @@ pub async fn create_private_message(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?; .with_lemmy_type(LemmyErrorType::CouldntCreatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id).await?; let view = PrivateMessageView::read(&mut context.pool(), inserted_private_message.id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
// Send email to the local recipient, if one exists // Send email to the local recipient, if one exists
if view.recipient.local { if view.recipient.local {
let recipient_id = data.recipient_id; let recipient_id = data.recipient_id;
let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id).await?; let local_recipient = LocalUserView::read_person(&mut context.pool(), recipient_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPerson)?;
let lang = get_interface_language(&local_recipient); let lang = get_interface_language(&local_recipient);
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname()); let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
let sender_name = &local_user_view.person.name; let sender_name = &local_user_view.person.name;

View File

@ -20,7 +20,9 @@ pub async fn delete_private_message(
) -> LemmyResult<Json<PrivateMessageResponse>> { ) -> LemmyResult<Json<PrivateMessageResponse>> {
// Checking permissions // Checking permissions
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
if local_user_view.person.id != orig_private_message.creator_id { if local_user_view.person.id != orig_private_message.creator_id {
Err(LemmyErrorType::EditPrivateMessageNotAllowed)? Err(LemmyErrorType::EditPrivateMessageNotAllowed)?
} }
@ -45,7 +47,9 @@ pub async fn delete_private_message(
) )
.await?; .await?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; let view = PrivateMessageView::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
Ok(Json(PrivateMessageResponse { Ok(Json(PrivateMessageResponse {
private_message_view: view, private_message_view: view,
})) }))

View File

@ -30,7 +30,9 @@ pub async fn update_private_message(
// Checking permissions // Checking permissions
let private_message_id = data.private_message_id; let private_message_id = data.private_message_id;
let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id).await?; let orig_private_message = PrivateMessage::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
if local_user_view.person.id != orig_private_message.creator_id { if local_user_view.person.id != orig_private_message.creator_id {
Err(LemmyErrorType::EditPrivateMessageNotAllowed)? Err(LemmyErrorType::EditPrivateMessageNotAllowed)?
} }
@ -54,7 +56,9 @@ pub async fn update_private_message(
.await .await
.with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?; .with_lemmy_type(LemmyErrorType::CouldntUpdatePrivateMessage)?;
let view = PrivateMessageView::read(&mut context.pool(), private_message_id).await?; let view = PrivateMessageView::read(&mut context.pool(), private_message_id)
.await?
.ok_or(LemmyErrorType::CouldntFindPrivateMessage)?;
ActivityChannel::submit_activity( ActivityChannel::submit_activity(
SendActivityData::UpdatePrivateMessage(view.clone()), SendActivityData::UpdatePrivateMessage(view.clone()),

View File

@ -129,7 +129,9 @@ pub async fn create_site(
LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?; LocalSiteRateLimit::update(&mut context.pool(), &local_site_rate_limit_form).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let new_taglines = data.taglines.clone(); let new_taglines = data.taglines.clone();
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;

View File

@ -41,7 +41,9 @@ pub async fn get_site(
// This data is independent from the user account so we can cache it across requests // This data is independent from the user account so we can cache it across requests
let mut site_response = CACHE let mut site_response = CACHE
.try_get_with::<_, LemmyError>((), async { .try_get_with::<_, LemmyError>((), async {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let admins = PersonView::admins(&mut context.pool()).await?; let admins = PersonView::admins(&mut context.pool()).await?;
let all_languages = Language::read_all(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?;
let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?; let discussion_languages = SiteLanguage::read_local_raw(&mut context.pool()).await?;

View File

@ -52,7 +52,9 @@ pub async fn update_site(
context: Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
) -> LemmyResult<Json<SiteResponse>> { ) -> LemmyResult<Json<SiteResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let local_site = site_view.local_site; let local_site = site_view.local_site;
let site = site_view.site; let site = site_view.site;
@ -154,7 +156,8 @@ pub async fn update_site(
// TODO can't think of a better way to do this. // TODO can't think of a better way to do this.
// If the server suddenly requires email verification, or required applications, no old users // If the server suddenly requires email verification, or required applications, no old users
// will be able to log in. It really only wants this to be a requirement for NEW signups. // will be able to log in. It really only wants this to be a requirement for NEW signups.
// 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 = let old_require_application =
local_site.registration_mode == RegistrationMode::RequireApplication; local_site.registration_mode == RegistrationMode::RequireApplication;
@ -181,7 +184,9 @@ pub async fn update_site(
let new_taglines = data.taglines.clone(); let new_taglines = data.taglines.clone();
let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?; let taglines = Tagline::replace(&mut context.pool(), local_site.id, new_taglines).await?;
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let rate_limit_config = let rate_limit_config =
local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit); local_site_rate_limit_to_rate_limit_config(&site_view.local_site_rate_limit);

View File

@ -45,7 +45,9 @@ pub async fn register(
req: HttpRequest, req: HttpRequest,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<LoginResponse>> { ) -> LemmyResult<Json<LoginResponse>> {
let site_view = SiteView::read_local(&mut context.pool()).await?; let site_view = SiteView::read_local(&mut context.pool())
.await?
.ok_or(LemmyErrorType::LocalSiteNotSetup)?;
let local_site = site_view.local_site; let local_site = site_view.local_site;
let require_registration_application = let require_registration_application =
local_site.registration_mode == RegistrationMode::RequireApplication; local_site.registration_mode == RegistrationMode::RequireApplication;
@ -140,12 +142,17 @@ pub async fn register(
.map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string()) .map(|lang_str| lang_str.split('-').next().unwrap_or_default().to_string())
.collect(); .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 // Create the local user
let local_user_form = LocalUserInsertForm::builder() let local_user_form = LocalUserInsertForm::builder()
.person_id(inserted_person.id) .person_id(inserted_person.id)
.email(data.email.as_deref().map(str::to_lowercase)) .email(data.email.as_deref().map(str::to_lowercase))
.password_encrypted(data.password.to_string()) .password_encrypted(data.password.to_string())
.show_nsfw(Some(data.show_nsfw)) .show_nsfw(Some(show_nsfw))
.accepted_application(accepted_application) .accepted_application(accepted_application)
.default_listing_type(Some(local_site.default_post_listing_type)) .default_listing_type(Some(local_site.default_post_listing_type))
.post_listing_mode(Some(local_site.default_post_listing_mode)) .post_listing_mode(Some(local_site.default_post_listing_mode))
@ -190,7 +197,8 @@ pub async fn register(
verify_email_sent: false, 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 if !local_site.site_setup
|| (!require_registration_application && !local_site.require_email_verification) || (!require_registration_application && !local_site.require_email_verification)
{ {

View File

@ -44,7 +44,7 @@ once_cell = { workspace = true }
moka.workspace = true moka.workspace = true
serde_with.workspace = true serde_with.workspace = true
html2md = "0.2.14" html2md = "0.2.14"
html2text = "0.6.0" html2text = "0.12.5"
stringreader = "0.1.1" stringreader = "0.1.1"
enum_delegate = "0.2.0" enum_delegate = "0.2.0"

View 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"
}

View 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&hellip;",
"@context": "https://www.w3.org/ns/activitystreams"
}

View 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"
}

View File

@ -23,7 +23,6 @@
"href": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg" "href": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg"
} }
], ],
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"language": { "language": {
"identifier": "ko", "identifier": "ko",

View File

@ -23,7 +23,6 @@
"href": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg" "href": "https://lemmy.ml/pictrs/image/xl8W7FZfk9.jpg"
} }
], ],
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"published": "2021-10-29T15:10:51.557399Z", "published": "2021-10-29T15:10:51.557399Z",
"updated": "2021-10-29T15:11:35.976374Z" "updated": "2021-10-29T15:11:35.976374Z"

View File

@ -15,7 +15,6 @@
"cc": [], "cc": [],
"mediaType": "text/html", "mediaType": "text/html",
"attachment": [], "attachment": [],
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"published": "2023-02-06T06:42:41.939437Z", "published": "2023-02-06T06:42:41.939437Z",
"language": { "language": {
@ -36,7 +35,6 @@
"cc": [], "cc": [],
"mediaType": "text/html", "mediaType": "text/html",
"attachment": [], "attachment": [],
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"published": "2023-02-06T06:42:37.119567Z", "published": "2023-02-06T06:42:37.119567Z",
"language": { "language": {

View File

@ -22,7 +22,6 @@
], ],
"name": "another outbox test", "name": "another outbox test",
"mediaType": "text/html", "mediaType": "text/html",
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"stickied": false, "stickied": false,
"published": "2021-11-18T17:19:45.895163Z" "published": "2021-11-18T17:19:45.895163Z"
@ -51,7 +50,6 @@
], ],
"name": "outbox test", "name": "outbox test",
"mediaType": "text/html", "mediaType": "text/html",
"commentsEnabled": true,
"sensitive": false, "sensitive": false,
"stickied": false, "stickied": false,
"published": "2021-11-18T17:19:05.763109Z" "published": "2021-11-18T17:19:05.763109Z"

View File

@ -25,7 +25,6 @@
"url": "https://enterprise.lemmy.ml/pictrs/image/eOtYb9iEiB.png" "url": "https://enterprise.lemmy.ml/pictrs/image/eOtYb9iEiB.png"
}, },
"sensitive": false, "sensitive": false,
"commentsEnabled": true,
"language": { "language": {
"identifier": "fr", "identifier": "fr",
"name": "Français" "name": "Français"

View 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"
}
}

View 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": []
}

View 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"
}
}

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