mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-12-23 19:31:33 +00:00
Merge upstream/main into migration-runner (using imerge)
This commit is contained in:
commit
a066c16537
57 changed files with 1022 additions and 360 deletions
108
.woodpecker.yml
108
.woodpecker.yml
|
@ -91,6 +91,66 @@ steps:
|
||||||
when:
|
when:
|
||||||
- event: pull_request
|
- event: pull_request
|
||||||
|
|
||||||
|
cargo_clippy:
|
||||||
|
image: *rust_image
|
||||||
|
environment:
|
||||||
|
CARGO_HOME: .cargo_home
|
||||||
|
commands:
|
||||||
|
- rustup component add clippy
|
||||||
|
- cargo clippy --workspace --tests --all-targets -- -D warnings
|
||||||
|
when: *slow_check_paths
|
||||||
|
|
||||||
|
cargo_test:
|
||||||
|
image: *rust_image
|
||||||
|
environment:
|
||||||
|
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||||
|
RUST_BACKTRACE: "1"
|
||||||
|
CARGO_HOME: .cargo_home
|
||||||
|
LEMMY_TEST_FAST_FEDERATION: "1"
|
||||||
|
LEMMY_CONFIG_LOCATION: ../../config/config.hjson
|
||||||
|
commands:
|
||||||
|
- cargo test --workspace --no-fail-fast
|
||||||
|
when: *slow_check_paths
|
||||||
|
|
||||||
|
check_ts_bindings:
|
||||||
|
image: *rust_image
|
||||||
|
environment:
|
||||||
|
CARGO_HOME: .cargo_home
|
||||||
|
commands:
|
||||||
|
- ./scripts/ts_bindings_check.sh
|
||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
|
||||||
|
# `DROP OWNED` doesn't work for default user
|
||||||
|
create_database_user:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
environment:
|
||||||
|
PGUSER: postgres
|
||||||
|
PGPASSWORD: password
|
||||||
|
PGHOST: database
|
||||||
|
PGDATABASE: lemmy
|
||||||
|
commands:
|
||||||
|
- psql -c "CREATE USER lemmy WITH PASSWORD 'password' SUPERUSER;"
|
||||||
|
when: *slow_check_paths
|
||||||
|
|
||||||
|
cargo_test:
|
||||||
|
image: *rust_image
|
||||||
|
environment:
|
||||||
|
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
||||||
|
RUST_BACKTRACE: "1"
|
||||||
|
CARGO_HOME: .cargo_home
|
||||||
|
LEMMY_TEST_FAST_FEDERATION: "1"
|
||||||
|
LEMMY_CONFIG_LOCATION: ../../config/config.hjson
|
||||||
|
commands:
|
||||||
|
# Install pg_dump for the schema setup test (must match server version)
|
||||||
|
- 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
|
||||||
|
# Run tests
|
||||||
|
- cargo test --workspace --no-fail-fast
|
||||||
|
when: *slow_check_paths
|
||||||
|
|
||||||
# make sure api builds with default features (used by other crates relying on lemmy api)
|
# make sure api builds with default features (used by other crates relying on lemmy api)
|
||||||
check_api_common_default_features:
|
check_api_common_default_features:
|
||||||
image: *rust_image
|
image: *rust_image
|
||||||
|
@ -126,15 +186,6 @@ steps:
|
||||||
- diff config/defaults.hjson config/defaults_current.hjson
|
- diff config/defaults.hjson config/defaults_current.hjson
|
||||||
when: *slow_check_paths
|
when: *slow_check_paths
|
||||||
|
|
||||||
cargo_clippy:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- rustup component add clippy
|
|
||||||
- cargo clippy --workspace --tests --all-targets -- -D warnings
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
cargo_build:
|
cargo_build:
|
||||||
image: *rust_image
|
image: *rust_image
|
||||||
environment:
|
environment:
|
||||||
|
@ -144,18 +195,6 @@ steps:
|
||||||
- mv target/debug/lemmy_server target/lemmy_server
|
- mv target/debug/lemmy_server target/lemmy_server
|
||||||
when: *slow_check_paths
|
when: *slow_check_paths
|
||||||
|
|
||||||
# `DROP OWNED` doesn't work for default user
|
|
||||||
create_database_user:
|
|
||||||
image: postgres:16-alpine
|
|
||||||
environment:
|
|
||||||
PGUSER: postgres
|
|
||||||
PGPASSWORD: password
|
|
||||||
PGHOST: database
|
|
||||||
PGDATABASE: lemmy
|
|
||||||
commands:
|
|
||||||
- psql -c "CREATE USER lemmy WITH PASSWORD 'password' SUPERUSER;"
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
check_diesel_schema:
|
check_diesel_schema:
|
||||||
image: *rust_image
|
image: *rust_image
|
||||||
environment:
|
environment:
|
||||||
|
@ -171,33 +210,6 @@ steps:
|
||||||
- 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
|
||||||
|
|
||||||
cargo_test:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
|
|
||||||
RUST_BACKTRACE: "1"
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
LEMMY_TEST_FAST_FEDERATION: "1"
|
|
||||||
LEMMY_CONFIG_LOCATION: ../../config/config.hjson
|
|
||||||
commands:
|
|
||||||
# Install pg_dump for the schema setup test (must match server version)
|
|
||||||
- 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
|
|
||||||
# Run tests
|
|
||||||
- cargo test --workspace --no-fail-fast
|
|
||||||
when: *slow_check_paths
|
|
||||||
|
|
||||||
check_ts_bindings:
|
|
||||||
image: *rust_image
|
|
||||||
environment:
|
|
||||||
CARGO_HOME: .cargo_home
|
|
||||||
commands:
|
|
||||||
- ./scripts/ts_bindings_check.sh
|
|
||||||
when:
|
|
||||||
- event: pull_request
|
|
||||||
|
|
||||||
check_db_perf_tool:
|
check_db_perf_tool:
|
||||||
image: *rust_image
|
image: *rust_image
|
||||||
environment:
|
environment:
|
||||||
|
|
9
Cargo.lock
generated
9
Cargo.lock
generated
|
@ -10,9 +10,9 @@ checksum = "8f27d075294830fcab6f66e320dab524bc6d048f4a151698e153205559113772"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "activitypub_federation"
|
name = "activitypub_federation"
|
||||||
version = "0.6.0-alpha2"
|
version = "0.6.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4877d467ddf2fac85e9ee33aba6f2560df14125b8bfa864f85ab40e9b87753a9"
|
checksum = "ee819cada736b6e26c59706f9e6ff89a48060e635c0546ff984d84baefc8c13a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activitystreams-kinds",
|
"activitystreams-kinds",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
@ -2520,7 +2520,6 @@ dependencies = [
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"enum-map",
|
"enum-map",
|
||||||
"futures",
|
"futures",
|
||||||
"getrandom",
|
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"lemmy_db_schema",
|
"lemmy_db_schema",
|
||||||
"lemmy_db_views",
|
"lemmy_db_views",
|
||||||
|
@ -2563,7 +2562,6 @@ dependencies = [
|
||||||
"lemmy_db_views",
|
"lemmy_db_views",
|
||||||
"lemmy_db_views_actor",
|
"lemmy_db_views_actor",
|
||||||
"lemmy_utils",
|
"lemmy_utils",
|
||||||
"moka",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
@ -2597,6 +2595,7 @@ dependencies = [
|
||||||
"moka",
|
"moka",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"reqwest 0.12.8",
|
"reqwest 0.12.8",
|
||||||
|
"semver",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_with",
|
"serde_with",
|
||||||
|
@ -2646,7 +2645,6 @@ dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"i-love-jesus",
|
"i-love-jesus",
|
||||||
"lemmy_utils",
|
"lemmy_utils",
|
||||||
"moka",
|
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"regex",
|
"regex",
|
||||||
"rustls 0.23.16",
|
"rustls 0.23.16",
|
||||||
|
@ -2833,6 +2831,7 @@ dependencies = [
|
||||||
"markdown-it-ruby",
|
"markdown-it-ruby",
|
||||||
"markdown-it-sub",
|
"markdown-it-sub",
|
||||||
"markdown-it-sup",
|
"markdown-it-sup",
|
||||||
|
"moka",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest-middleware",
|
"reqwest-middleware",
|
||||||
|
|
|
@ -94,7 +94,7 @@ lemmy_db_views = { version = "=0.19.6-beta.7", path = "./crates/db_views" }
|
||||||
lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" }
|
lemmy_db_views_actor = { version = "=0.19.6-beta.7", path = "./crates/db_views_actor" }
|
||||||
lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" }
|
lemmy_db_views_moderator = { version = "=0.19.6-beta.7", path = "./crates/db_views_moderator" }
|
||||||
lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" }
|
lemmy_federate = { version = "=0.19.6-beta.7", path = "./crates/federate" }
|
||||||
activitypub_federation = { version = "0.6.0-alpha2", default-features = false, features = [
|
activitypub_federation = { version = "0.6.1", default-features = false, features = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
] }
|
] }
|
||||||
diesel = "2.2.4"
|
diesel = "2.2.4"
|
||||||
|
@ -131,7 +131,7 @@ chrono = { version = "0.4.38", features = [
|
||||||
], default-features = false }
|
], default-features = false }
|
||||||
serde_json = { version = "1.0.132", features = ["preserve_order"] }
|
serde_json = { version = "1.0.132", features = ["preserve_order"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
uuid = { version = "1.11.0", features = ["serde", "v4"] }
|
uuid = { version = "1.11.0", features = ["serde"] }
|
||||||
async-trait = "0.1.83"
|
async-trait = "0.1.83"
|
||||||
captcha = "0.0.9"
|
captcha = "0.0.9"
|
||||||
anyhow = { version = "1.0.93", features = [
|
anyhow = { version = "1.0.93", features = [
|
||||||
|
|
|
@ -41,6 +41,9 @@ afterAll(async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Upload image and delete it", async () => {
|
test("Upload image and delete it", async () => {
|
||||||
|
const healthz = await fetch(alphaUrl + "/pictrs/healthz");
|
||||||
|
expect(healthz.status).toBe(200);
|
||||||
|
|
||||||
// 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);
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
jest.setTimeout(120000);
|
jest.setTimeout(120000);
|
||||||
|
|
||||||
import { FollowCommunity } from "lemmy-js-client";
|
import { FollowCommunity, LemmyHttp } from "lemmy-js-client";
|
||||||
import {
|
import {
|
||||||
alpha,
|
alpha,
|
||||||
setupLogins,
|
setupLogins,
|
||||||
|
@ -21,6 +21,9 @@ import {
|
||||||
resolveComment,
|
resolveComment,
|
||||||
likeComment,
|
likeComment,
|
||||||
waitUntil,
|
waitUntil,
|
||||||
|
gamma,
|
||||||
|
getPosts,
|
||||||
|
getComments,
|
||||||
} from "./shared";
|
} from "./shared";
|
||||||
|
|
||||||
beforeAll(setupLogins);
|
beforeAll(setupLogins);
|
||||||
|
@ -47,6 +50,7 @@ test("Follow a private community", async () => {
|
||||||
await resolveCommunity(user, community.community_view.community.actor_id)
|
await resolveCommunity(user, community.community_view.community.actor_id)
|
||||||
).community;
|
).community;
|
||||||
expect(betaCommunity).toBeDefined();
|
expect(betaCommunity).toBeDefined();
|
||||||
|
expect(betaCommunity?.community.visibility).toBe("Private");
|
||||||
const betaCommunityId = betaCommunity!.community.id;
|
const betaCommunityId = betaCommunity!.community.id;
|
||||||
const follow_form: FollowCommunity = {
|
const follow_form: FollowCommunity = {
|
||||||
community_id: betaCommunityId,
|
community_id: betaCommunityId,
|
||||||
|
@ -148,16 +152,7 @@ test("Only followers can view and interact with private community content", asyn
|
||||||
follow: true,
|
follow: true,
|
||||||
};
|
};
|
||||||
await user.followCommunity(follow_form);
|
await user.followCommunity(follow_form);
|
||||||
const pendingFollows1 = await waitUntil(
|
approveFollower(alpha, alphaCommunityId);
|
||||||
() => listCommunityPendingFollows(alpha),
|
|
||||||
f => f.items.length == 1,
|
|
||||||
);
|
|
||||||
const approve = await approveCommunityPendingFollow(
|
|
||||||
alpha,
|
|
||||||
alphaCommunityId,
|
|
||||||
pendingFollows1.items[0].person.id,
|
|
||||||
);
|
|
||||||
expect(approve.success).toBe(true);
|
|
||||||
|
|
||||||
// now user can fetch posts and comments in community (using signed fetch), and create posts
|
// now user can fetch posts and comments in community (using signed fetch), and create posts
|
||||||
await waitUntil(
|
await waitUntil(
|
||||||
|
@ -212,3 +207,151 @@ test("Reject follower", async () => {
|
||||||
c => c.community_view.subscribed == "NotSubscribed",
|
c => c.community_view.subscribed == "NotSubscribed",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Follow a private community and receive activities", async () => {
|
||||||
|
// create private community
|
||||||
|
const community = await createCommunity(alpha, randomString(10), "Private");
|
||||||
|
expect(community.community_view.community.visibility).toBe("Private");
|
||||||
|
const alphaCommunityId = community.community_view.community.id;
|
||||||
|
|
||||||
|
// follow with users from beta and gamma
|
||||||
|
const betaCommunity = (
|
||||||
|
await resolveCommunity(beta, community.community_view.community.actor_id)
|
||||||
|
).community;
|
||||||
|
expect(betaCommunity).toBeDefined();
|
||||||
|
const betaCommunityId = betaCommunity!.community.id;
|
||||||
|
const follow_form_beta: FollowCommunity = {
|
||||||
|
community_id: betaCommunityId,
|
||||||
|
follow: true,
|
||||||
|
};
|
||||||
|
await beta.followCommunity(follow_form_beta);
|
||||||
|
await approveFollower(alpha, alphaCommunityId);
|
||||||
|
|
||||||
|
const gammaCommunityId = (
|
||||||
|
await resolveCommunity(gamma, community.community_view.community.actor_id)
|
||||||
|
).community!.community.id;
|
||||||
|
const follow_form_gamma: FollowCommunity = {
|
||||||
|
community_id: gammaCommunityId,
|
||||||
|
follow: true,
|
||||||
|
};
|
||||||
|
await gamma.followCommunity(follow_form_gamma);
|
||||||
|
await approveFollower(alpha, alphaCommunityId);
|
||||||
|
|
||||||
|
// Follow is confirmed
|
||||||
|
await waitUntil(
|
||||||
|
() => getCommunity(beta, betaCommunityId),
|
||||||
|
c => c.community_view.subscribed == "Subscribed",
|
||||||
|
);
|
||||||
|
await waitUntil(
|
||||||
|
() => getCommunity(gamma, gammaCommunityId),
|
||||||
|
c => c.community_view.subscribed == "Subscribed",
|
||||||
|
);
|
||||||
|
|
||||||
|
// create a post and comment from gamma
|
||||||
|
const post = await createPost(gamma, gammaCommunityId);
|
||||||
|
const post_id = post.post_view.post.id;
|
||||||
|
expect(post_id).toBeDefined();
|
||||||
|
const comment = await createComment(gamma, post_id);
|
||||||
|
const comment_id = comment.comment_view.comment.id;
|
||||||
|
expect(comment_id).toBeDefined();
|
||||||
|
|
||||||
|
// post and comment were federated to beta
|
||||||
|
let posts = await waitUntil(
|
||||||
|
() => getPosts(beta, "All", betaCommunityId),
|
||||||
|
c => c.posts.length == 1,
|
||||||
|
);
|
||||||
|
expect(posts.posts[0].post.ap_id).toBe(post.post_view.post.ap_id);
|
||||||
|
expect(posts.posts[0].post.name).toBe(post.post_view.post.name);
|
||||||
|
let comments = await waitUntil(
|
||||||
|
() => getComments(beta, posts.posts[0].post.id),
|
||||||
|
c => c.comments.length == 1,
|
||||||
|
);
|
||||||
|
expect(comments.comments[0].comment.ap_id).toBe(
|
||||||
|
comment.comment_view.comment.ap_id,
|
||||||
|
);
|
||||||
|
expect(comments.comments[0].comment.content).toBe(
|
||||||
|
comment.comment_view.comment.content,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Fetch remote content in private community", async () => {
|
||||||
|
// create private community
|
||||||
|
const community = await createCommunity(alpha, randomString(10), "Private");
|
||||||
|
expect(community.community_view.community.visibility).toBe("Private");
|
||||||
|
const alphaCommunityId = community.community_view.community.id;
|
||||||
|
|
||||||
|
const betaCommunityId = (
|
||||||
|
await resolveCommunity(beta, community.community_view.community.actor_id)
|
||||||
|
).community!.community.id;
|
||||||
|
const follow_form_beta: FollowCommunity = {
|
||||||
|
community_id: betaCommunityId,
|
||||||
|
follow: true,
|
||||||
|
};
|
||||||
|
await beta.followCommunity(follow_form_beta);
|
||||||
|
await approveFollower(alpha, alphaCommunityId);
|
||||||
|
|
||||||
|
// Follow is confirmed
|
||||||
|
await waitUntil(
|
||||||
|
() => getCommunity(beta, betaCommunityId),
|
||||||
|
c => c.community_view.subscribed == "Subscribed",
|
||||||
|
);
|
||||||
|
|
||||||
|
// beta creates post and comment
|
||||||
|
const post = await createPost(beta, betaCommunityId);
|
||||||
|
const post_id = post.post_view.post.id;
|
||||||
|
expect(post_id).toBeDefined();
|
||||||
|
const comment = await createComment(beta, post_id);
|
||||||
|
const comment_id = comment.comment_view.comment.id;
|
||||||
|
expect(comment_id).toBeDefined();
|
||||||
|
|
||||||
|
// Wait for it to federate
|
||||||
|
await waitUntil(
|
||||||
|
() => resolveComment(alpha, comment.comment_view.comment),
|
||||||
|
p => p?.comment?.comment.id != undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
// create gamma user
|
||||||
|
const gammaCommunityId = (
|
||||||
|
await resolveCommunity(gamma, community.community_view.community.actor_id)
|
||||||
|
).community!.community.id;
|
||||||
|
const follow_form: FollowCommunity = {
|
||||||
|
community_id: gammaCommunityId,
|
||||||
|
follow: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
// cannot fetch post yet
|
||||||
|
await expect(resolvePost(gamma, post.post_view.post)).rejects.toStrictEqual(
|
||||||
|
Error("not_found"),
|
||||||
|
);
|
||||||
|
// follow community and approve
|
||||||
|
await gamma.followCommunity(follow_form);
|
||||||
|
await approveFollower(alpha, alphaCommunityId);
|
||||||
|
|
||||||
|
// now user can fetch posts and comments in community (using signed fetch), and create posts.
|
||||||
|
// for this to work, beta checks with alpha if gamma is really an approved follower.
|
||||||
|
let resolvedPost = await waitUntil(
|
||||||
|
() => resolvePost(gamma, post.post_view.post),
|
||||||
|
p => p?.post?.post.id != undefined,
|
||||||
|
);
|
||||||
|
expect(resolvedPost.post?.post.ap_id).toBe(post.post_view.post.ap_id);
|
||||||
|
const resolvedComment = await waitUntil(
|
||||||
|
() => resolveComment(gamma, comment.comment_view.comment),
|
||||||
|
p => p?.comment?.comment.id != undefined,
|
||||||
|
);
|
||||||
|
expect(resolvedComment?.comment?.comment.ap_id).toBe(
|
||||||
|
comment.comment_view.comment.ap_id,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
async function approveFollower(user: LemmyHttp, community_id: number) {
|
||||||
|
let pendingFollows1 = await waitUntil(
|
||||||
|
() => listCommunityPendingFollows(user),
|
||||||
|
f => f.items.length == 1,
|
||||||
|
);
|
||||||
|
const approve = await approveCommunityPendingFollow(
|
||||||
|
alpha,
|
||||||
|
community_id,
|
||||||
|
pendingFollows1.items[0].person.id,
|
||||||
|
);
|
||||||
|
expect(approve.success).toBe(true);
|
||||||
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ full = [
|
||||||
"futures",
|
"futures",
|
||||||
"jsonwebtoken",
|
"jsonwebtoken",
|
||||||
"mime",
|
"mime",
|
||||||
|
"moka",
|
||||||
]
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
@ -58,7 +59,7 @@ uuid = { workspace = true, optional = true }
|
||||||
tokio = { workspace = true, optional = true }
|
tokio = { workspace = true, optional = true }
|
||||||
reqwest = { workspace = true, optional = true }
|
reqwest = { workspace = true, optional = true }
|
||||||
ts-rs = { workspace = true, optional = true }
|
ts-rs = { workspace = true, optional = true }
|
||||||
moka.workspace = true
|
moka = { workspace = true, optional = true }
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
actix-web = { workspace = true, optional = true }
|
actix-web = { workspace = true, optional = true }
|
||||||
enum-map = { workspace = true }
|
enum-map = { workspace = true }
|
||||||
|
@ -70,11 +71,6 @@ webpage = { version = "2.0", default-features = false, features = [
|
||||||
], optional = true }
|
], optional = true }
|
||||||
encoding_rs = { version = "0.8.35", optional = true }
|
encoding_rs = { version = "0.8.35", optional = true }
|
||||||
jsonwebtoken = { version = "9.3.0", optional = true }
|
jsonwebtoken = { version = "9.3.0", optional = true }
|
||||||
# necessary for wasmt compilation
|
|
||||||
getrandom = { version = "0.2.15", features = ["js"] }
|
|
||||||
|
|
||||||
[package.metadata.cargo-shear]
|
|
||||||
ignored = ["getrandom"]
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = { workspace = true }
|
serial_test = { workspace = true }
|
||||||
|
|
|
@ -17,8 +17,10 @@ use lemmy_db_schema::{
|
||||||
actor_language::CommunityLanguage,
|
actor_language::CommunityLanguage,
|
||||||
comment::Comment,
|
comment::Comment,
|
||||||
comment_reply::{CommentReply, CommentReplyInsertForm},
|
comment_reply::{CommentReply, CommentReplyInsertForm},
|
||||||
|
community::Community,
|
||||||
person::Person,
|
person::Person,
|
||||||
person_mention::{PersonMention, PersonMentionInsertForm},
|
person_mention::{PersonMention, PersonMentionInsertForm},
|
||||||
|
post::Post,
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
};
|
};
|
||||||
|
@ -101,17 +103,28 @@ pub async fn send_local_notifs(
|
||||||
let mut recipient_ids = Vec::new();
|
let mut recipient_ids = Vec::new();
|
||||||
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
|
let inbox_link = format!("{}/inbox", context.settings().get_protocol_and_hostname());
|
||||||
|
|
||||||
// let person = my_local_user.person;
|
// When called from api code, we have local user view and can read with CommentView
|
||||||
// Read the comment view to get extra info
|
// to reduce db queries. But when receiving a federated comment the user view is None,
|
||||||
|
// which means that comments inside private communities cant be read. As a workaround
|
||||||
|
// we need to read the items manually to bypass this check.
|
||||||
|
let (comment, post, community) = if let Some(local_user_view) = local_user_view {
|
||||||
let comment_view = CommentView::read(
|
let comment_view = CommentView::read(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
comment_id,
|
comment_id,
|
||||||
local_user_view.map(|view| &view.local_user),
|
Some(&local_user_view.local_user),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let comment = comment_view.comment;
|
(
|
||||||
let post = comment_view.post;
|
comment_view.comment,
|
||||||
let community = comment_view.community;
|
comment_view.post,
|
||||||
|
comment_view.community,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let comment = Comment::read(&mut context.pool(), comment_id).await?;
|
||||||
|
let post = Post::read(&mut context.pool(), comment.post_id).await?;
|
||||||
|
let community = Community::read(&mut context.pool(), post.community_id).await?;
|
||||||
|
(comment, post, community)
|
||||||
|
};
|
||||||
|
|
||||||
// Send the local mentions
|
// Send the local mentions
|
||||||
for mention in mentions
|
for mention in mentions
|
||||||
|
|
|
@ -60,6 +60,7 @@ use lemmy_utils::{
|
||||||
slurs::{build_slur_regex, remove_slurs},
|
slurs::{build_slur_regex, remove_slurs},
|
||||||
validation::clean_urls_in_text,
|
validation::clean_urls_in_text,
|
||||||
},
|
},
|
||||||
|
CacheLock,
|
||||||
CACHE_DURATION_FEDERATION,
|
CACHE_DURATION_FEDERATION,
|
||||||
};
|
};
|
||||||
use moka::future::Cache;
|
use moka::future::Cache;
|
||||||
|
@ -535,7 +536,7 @@ pub fn local_site_opt_to_slur_regex(local_site: &Option<LocalSite>) -> Option<Le
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet> {
|
pub async fn get_url_blocklist(context: &LemmyContext) -> LemmyResult<RegexSet> {
|
||||||
static URL_BLOCKLIST: LazyLock<Cache<(), RegexSet>> = LazyLock::new(|| {
|
static URL_BLOCKLIST: CacheLock<RegexSet> = LazyLock::new(|| {
|
||||||
Cache::builder()
|
Cache::builder()
|
||||||
.max_capacity(1)
|
.max_capacity(1)
|
||||||
.time_to_live(CACHE_DURATION_FEDERATION)
|
.time_to_live(CACHE_DURATION_FEDERATION)
|
||||||
|
|
|
@ -25,7 +25,6 @@ tracing = { workspace = true }
|
||||||
url = { workspace = true }
|
url = { workspace = true }
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
uuid = { workspace = true }
|
uuid = { workspace = true }
|
||||||
moka.workspace = true
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
chrono.workspace = true
|
chrono.workspace = true
|
||||||
webmention = "0.6.0"
|
webmention = "0.6.0"
|
||||||
|
|
|
@ -16,11 +16,11 @@ use lemmy_db_schema::source::{
|
||||||
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
use lemmy_db_views::structs::{LocalUserView, SiteView};
|
||||||
use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView, PersonView};
|
use lemmy_db_views_actor::structs::{CommunityFollowerView, CommunityModeratorView, PersonView};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
error::{LemmyError, LemmyErrorExt, LemmyErrorType, LemmyResult},
|
build_cache,
|
||||||
CACHE_DURATION_API,
|
error::{LemmyErrorExt, LemmyErrorType, LemmyResult},
|
||||||
|
CacheLock,
|
||||||
VERSION,
|
VERSION,
|
||||||
};
|
};
|
||||||
use moka::future::Cache;
|
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
#[tracing::instrument(skip(context))]
|
#[tracing::instrument(skip(context))]
|
||||||
|
@ -28,41 +28,10 @@ pub async fn get_site(
|
||||||
local_user_view: Option<LocalUserView>,
|
local_user_view: Option<LocalUserView>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
) -> LemmyResult<Json<GetSiteResponse>> {
|
) -> LemmyResult<Json<GetSiteResponse>> {
|
||||||
static CACHE: LazyLock<Cache<(), GetSiteResponse>> = LazyLock::new(|| {
|
|
||||||
Cache::builder()
|
|
||||||
.max_capacity(1)
|
|
||||||
.time_to_live(CACHE_DURATION_API)
|
|
||||||
.build()
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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
|
||||||
|
static CACHE: CacheLock<GetSiteResponse> = LazyLock::new(build_cache);
|
||||||
let mut site_response = CACHE
|
let mut site_response = CACHE
|
||||||
.try_get_with::<_, LemmyError>((), async {
|
.try_get_with((), read_site(&context))
|
||||||
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
|
||||||
let admins = PersonView::admins(&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 blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
|
|
||||||
let tagline = Tagline::get_random(&mut context.pool()).await.ok();
|
|
||||||
let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?;
|
|
||||||
let oauth_providers =
|
|
||||||
OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone());
|
|
||||||
|
|
||||||
Ok(GetSiteResponse {
|
|
||||||
site_view,
|
|
||||||
admins,
|
|
||||||
version: VERSION.to_string(),
|
|
||||||
my_user: None,
|
|
||||||
all_languages,
|
|
||||||
discussion_languages,
|
|
||||||
blocked_urls,
|
|
||||||
tagline,
|
|
||||||
oauth_providers: Some(oauth_providers),
|
|
||||||
admin_oauth_providers: Some(admin_oauth_providers),
|
|
||||||
taglines: vec![],
|
|
||||||
custom_emojis: vec![],
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("Failed to construct site response: {e}"))?;
|
||||||
|
|
||||||
|
@ -112,3 +81,29 @@ pub async fn get_site(
|
||||||
|
|
||||||
Ok(Json(site_response))
|
Ok(Json(site_response))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_site(context: &LemmyContext) -> LemmyResult<GetSiteResponse> {
|
||||||
|
let site_view = SiteView::read_local(&mut context.pool()).await?;
|
||||||
|
let admins = PersonView::admins(&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 blocked_urls = LocalSiteUrlBlocklist::get_all(&mut context.pool()).await?;
|
||||||
|
let tagline = Tagline::get_random(&mut context.pool()).await.ok();
|
||||||
|
let admin_oauth_providers = OAuthProvider::get_all(&mut context.pool()).await?;
|
||||||
|
let oauth_providers = OAuthProvider::convert_providers_to_public(admin_oauth_providers.clone());
|
||||||
|
|
||||||
|
Ok(GetSiteResponse {
|
||||||
|
site_view,
|
||||||
|
admins,
|
||||||
|
version: VERSION.to_string(),
|
||||||
|
my_user: None,
|
||||||
|
all_languages,
|
||||||
|
discussion_languages,
|
||||||
|
blocked_urls,
|
||||||
|
tagline,
|
||||||
|
oauth_providers: Some(oauth_providers),
|
||||||
|
admin_oauth_providers: Some(admin_oauth_providers),
|
||||||
|
taglines: vec![],
|
||||||
|
custom_emojis: vec![],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -45,6 +45,7 @@ html2md = "0.2.14"
|
||||||
html2text = "0.12.6"
|
html2text = "0.12.6"
|
||||||
stringreader = "0.1.1"
|
stringreader = "0.1.1"
|
||||||
enum_delegate = "0.2.0"
|
enum_delegate = "0.2.0"
|
||||||
|
semver = "1.0.23"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
serial_test = { workspace = true }
|
serial_test = { workspace = true }
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"actor": "http://enterprise.lemmy.ml/u/lemmy_beta",
|
"actor": "http://enterprise.lemmy.ml/u/lemmy_beta",
|
||||||
"to": ["http://ds9.lemmy.ml/u/lemmy_alpha"],
|
"to": ["http://ds9.lemmy.ml/u/lemmy_alpha"],
|
||||||
"object": {
|
"object": {
|
||||||
"type": "ChatMessage",
|
"type": "Note",
|
||||||
"id": "http://enterprise.lemmy.ml/private_message/1",
|
"id": "http://enterprise.lemmy.ml/private_message/1",
|
||||||
"attributedTo": "http://enterprise.lemmy.ml/u/lemmy_beta",
|
"attributedTo": "http://enterprise.lemmy.ml/u/lemmy_beta",
|
||||||
"to": ["http://ds9.lemmy.ml/u/lemmy_alpha"],
|
"to": ["http://ds9.lemmy.ml/u/lemmy_alpha"],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"id": "https://enterprise.lemmy.ml/private_message/1621",
|
"id": "https://enterprise.lemmy.ml/private_message/1621",
|
||||||
"type": "ChatMessage",
|
"type": "Note",
|
||||||
"attributedTo": "https://enterprise.lemmy.ml/u/picard",
|
"attributedTo": "https://enterprise.lemmy.ml/u/picard",
|
||||||
"to": ["https://queer.hacktivis.me/users/lanodan"],
|
"to": ["https://queer.hacktivis.me/users/lanodan"],
|
||||||
"content": "<p>Hello hello, testing</p>\n",
|
"content": "<p>Hello hello, testing</p>\n",
|
49
crates/apub/assets/mastodon/activities/private_message.json
Normal file
49
crates/apub/assets/mastodon/activities/private_message.json
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"@context": [
|
||||||
|
"https://www.w3.org/ns/activitystreams",
|
||||||
|
{
|
||||||
|
"ostatus": "http://ostatus.org#",
|
||||||
|
"atomUri": "ostatus:atomUri",
|
||||||
|
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
|
||||||
|
"conversation": "ostatus:conversation",
|
||||||
|
"sensitive": "as:sensitive",
|
||||||
|
"toot": "http://joinmastodon.org/ns#",
|
||||||
|
"votersCount": "toot:votersCount"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "https://mastodon.world/users/nutomic/statuses/110854468010322301",
|
||||||
|
"type": "Note",
|
||||||
|
"summary": null,
|
||||||
|
"inReplyTo": "https://mastodon.world/users/nutomic/statuses/110854464248188528",
|
||||||
|
"published": "2023-08-08T14:29:04Z",
|
||||||
|
"url": "https://mastodon.world/@nutomic/110854468010322301",
|
||||||
|
"attributedTo": "https://mastodon.world/users/nutomic",
|
||||||
|
"to": ["https://ds9.lemmy.ml/u/nutomic"],
|
||||||
|
"cc": [],
|
||||||
|
"sensitive": false,
|
||||||
|
"atomUri": "https://mastodon.world/users/nutomic/statuses/110854468010322301",
|
||||||
|
"inReplyToAtomUri": "https://mastodon.world/users/nutomic/statuses/110854464248188528",
|
||||||
|
"conversation": "tag:mastodon.world,2023-08-08:objectId=121377096:objectType=Conversation",
|
||||||
|
"content": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://ds9.lemmy.ml/u/nutomic\" class=\"u-url mention\">@<span>nutomic@ds9.lemmy.ml</span></a></span> 444</p>",
|
||||||
|
"contentMap": {
|
||||||
|
"es": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://ds9.lemmy.ml/u/nutomic\" class=\"u-url mention\">@<span>nutomic@ds9.lemmy.ml</span></a></span> 444</p>"
|
||||||
|
},
|
||||||
|
"attachment": [],
|
||||||
|
"tag": [
|
||||||
|
{
|
||||||
|
"type": "Mention",
|
||||||
|
"href": "https://ds9.lemmy.ml/u/nutomic",
|
||||||
|
"name": "@nutomic@ds9.lemmy.ml"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"replies": {
|
||||||
|
"id": "https://mastodon.world/users/nutomic/statuses/110854468010322301/replies",
|
||||||
|
"type": "Collection",
|
||||||
|
"first": {
|
||||||
|
"type": "CollectionPage",
|
||||||
|
"next": "https://mastodon.world/users/nutomic/statuses/110854468010322301/replies?only_other_accounts=true&page=true",
|
||||||
|
"partOf": "https://mastodon.world/users/nutomic/statuses/110854468010322301/replies",
|
||||||
|
"items": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"@context": [
|
|
||||||
"https://www.w3.org/ns/activitystreams",
|
|
||||||
"https://queer.hacktivis.me/schemas/litepub-0.1.jsonld",
|
|
||||||
{
|
|
||||||
"@language": "und"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"attributedTo": "https://queer.hacktivis.me/users/lanodan",
|
|
||||||
"content": "Hi!",
|
|
||||||
"id": "https://queer.hacktivis.me/objects/2",
|
|
||||||
"published": "2020-02-12T14:08:20Z",
|
|
||||||
"to": ["https://enterprise.lemmy.ml/u/picard"],
|
|
||||||
"type": "ChatMessage"
|
|
||||||
}
|
|
|
@ -64,16 +64,17 @@ impl ActivityHandler for RawAnnouncableActivities {
|
||||||
|
|
||||||
// verify and receive activity
|
// verify and receive activity
|
||||||
activity.verify(context).await?;
|
activity.verify(context).await?;
|
||||||
activity.clone().receive(context).await?;
|
let actor_id = activity.actor().clone().into();
|
||||||
|
activity.receive(context).await?;
|
||||||
|
|
||||||
// if community is local, send activity to followers
|
// if community is local, send activity to followers
|
||||||
if let Some(community) = community {
|
if let Some(community) = community {
|
||||||
if community.local {
|
if community.local {
|
||||||
let actor_id = activity.actor().clone().into();
|
|
||||||
verify_person_in_community(&actor_id, &community, context).await?;
|
verify_person_in_community(&actor_id, &community, context).await?;
|
||||||
AnnounceActivity::send(self, &community, context).await?;
|
AnnounceActivity::send(self, &community, context).await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -215,7 +216,7 @@ async fn can_accept_activity_in_community(
|
||||||
) -> LemmyResult<()> {
|
) -> LemmyResult<()> {
|
||||||
if let Some(community) = community {
|
if let Some(community) = community {
|
||||||
// Local only community can't federate
|
// Local only community can't federate
|
||||||
if community.visibility != CommunityVisibility::Public {
|
if community.visibility == CommunityVisibility::LocalOnly {
|
||||||
return Err(LemmyErrorType::NotFound.into());
|
return Err(LemmyErrorType::NotFound.into());
|
||||||
}
|
}
|
||||||
if !community.local {
|
if !community.local {
|
||||||
|
|
|
@ -42,7 +42,7 @@ pub(crate) async fn send_activity_in_community(
|
||||||
context: &Data<LemmyContext>,
|
context: &Data<LemmyContext>,
|
||||||
) -> LemmyResult<()> {
|
) -> LemmyResult<()> {
|
||||||
// If community is local only, don't send anything out
|
// If community is local only, don't send anything out
|
||||||
if community.visibility != CommunityVisibility::Public {
|
if community.visibility == CommunityVisibility::LocalOnly {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,7 @@ use lemmy_utils::{
|
||||||
error::{LemmyError, LemmyResult},
|
error::{LemmyError, LemmyResult},
|
||||||
utils::mention::scrape_text_for_mentions,
|
utils::mention::scrape_text_for_mentions,
|
||||||
};
|
};
|
||||||
|
use serde_json::{from_value, to_value};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
impl CreateOrUpdateNote {
|
impl CreateOrUpdateNote {
|
||||||
|
@ -98,7 +99,8 @@ impl CreateOrUpdateNote {
|
||||||
inboxes.add_inbox(person.shared_inbox_or_inbox());
|
inboxes.add_inbox(person.shared_inbox_or_inbox());
|
||||||
}
|
}
|
||||||
|
|
||||||
let activity = AnnouncableActivities::CreateOrUpdateComment(create_or_update);
|
let activity =
|
||||||
|
AnnouncableActivities::CreateOrUpdateNoteWrapper(from_value(to_value(create_or_update)?)?);
|
||||||
send_activity_in_community(activity, &person, &community, inboxes, false, &context).await
|
send_activity_in_community(activity, &person, &community, inboxes, false, &context).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -171,6 +173,9 @@ impl ActivityHandler for CreateOrUpdateNote {
|
||||||
// TODO: for compatibility with other projects, it would be much better to read this from cc or
|
// TODO: for compatibility with other projects, it would be much better to read this from cc or
|
||||||
// tags
|
// tags
|
||||||
let mentions = scrape_text_for_mentions(&comment.content);
|
let mentions = scrape_text_for_mentions(&comment.content);
|
||||||
|
|
||||||
|
// TODO: this fails in local community comment as CommentView::read() returns nothing
|
||||||
|
// without passing LocalUser
|
||||||
send_local_notifs(mentions, comment.id, &actor, do_send_email, context, None).await?;
|
send_local_notifs(mentions, comment.id, &actor, do_send_email, context, None).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
|
pub(crate) mod note_wrapper;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod private_message;
|
pub mod private_message;
|
||||||
|
|
66
crates/apub/src/activities/create_or_update/note_wrapper.rs
Normal file
66
crates/apub/src/activities/create_or_update/note_wrapper.rs
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
use crate::{
|
||||||
|
objects::{community::ApubCommunity, note_wrapper::is_public},
|
||||||
|
protocol::{
|
||||||
|
activities::create_or_update::{
|
||||||
|
note::CreateOrUpdateNote,
|
||||||
|
note_wrapper::CreateOrUpdateNoteWrapper,
|
||||||
|
private_message::CreateOrUpdatePrivateMessage,
|
||||||
|
},
|
||||||
|
InCommunity,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use activitypub_federation::{config::Data, traits::ActivityHandler};
|
||||||
|
use lemmy_api_common::context::LemmyContext;
|
||||||
|
use lemmy_utils::error::{FederationError, LemmyError, LemmyResult};
|
||||||
|
use serde_json::{from_value, to_value};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl ActivityHandler for CreateOrUpdateNoteWrapper {
|
||||||
|
type DataType = LemmyContext;
|
||||||
|
type Error = LemmyError;
|
||||||
|
|
||||||
|
fn id(&self) -> &Url {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn actor(&self) -> &Url {
|
||||||
|
&self.actor
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn verify(&self, context: &Data<Self::DataType>) -> LemmyResult<()> {
|
||||||
|
let val = to_value(self)?;
|
||||||
|
if is_public(&self.to, &self.cc) {
|
||||||
|
CreateOrUpdateNote::verify(&from_value(val)?, context).await?;
|
||||||
|
} else {
|
||||||
|
CreateOrUpdatePrivateMessage::verify(&from_value(val)?, context).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn receive(self, context: &Data<Self::DataType>) -> LemmyResult<()> {
|
||||||
|
let is_public = is_public(&self.to, &self.cc);
|
||||||
|
let val = to_value(self)?;
|
||||||
|
if is_public {
|
||||||
|
CreateOrUpdateNote::receive(from_value(val)?, context).await?;
|
||||||
|
} else {
|
||||||
|
CreateOrUpdatePrivateMessage::receive(from_value(val)?, context).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl InCommunity for CreateOrUpdateNoteWrapper {
|
||||||
|
#[tracing::instrument(skip(self, context))]
|
||||||
|
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
|
||||||
|
if is_public(&self.to, &self.cc) {
|
||||||
|
let comment: CreateOrUpdateNote = from_value(to_value(self)?)?;
|
||||||
|
comment.community(context).await
|
||||||
|
} else {
|
||||||
|
Err(FederationError::ObjectIsNotPublic.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
insert_received_activity,
|
insert_received_activity,
|
||||||
objects::{person::ApubPerson, private_message::ApubPrivateMessage},
|
objects::{person::ApubPerson, private_message::ApubPrivateMessage},
|
||||||
protocol::activities::{
|
protocol::activities::{
|
||||||
create_or_update::chat_message::CreateOrUpdateChatMessage,
|
create_or_update::private_message::CreateOrUpdatePrivateMessage,
|
||||||
CreateOrUpdateType,
|
CreateOrUpdateType,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -30,7 +30,7 @@ pub(crate) async fn send_create_or_update_pm(
|
||||||
kind.clone(),
|
kind.clone(),
|
||||||
&context.settings().get_protocol_and_hostname(),
|
&context.settings().get_protocol_and_hostname(),
|
||||||
)?;
|
)?;
|
||||||
let create_or_update = CreateOrUpdateChatMessage {
|
let create_or_update = CreateOrUpdatePrivateMessage {
|
||||||
id: id.clone(),
|
id: id.clone(),
|
||||||
actor: actor.id().into(),
|
actor: actor.id().into(),
|
||||||
to: [recipient.id().into()],
|
to: [recipient.id().into()],
|
||||||
|
@ -44,7 +44,7 @@ pub(crate) async fn send_create_or_update_pm(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl ActivityHandler for CreateOrUpdateChatMessage {
|
impl ActivityHandler for CreateOrUpdatePrivateMessage {
|
||||||
type DataType = LemmyContext;
|
type DataType = LemmyContext;
|
||||||
type Error = LemmyError;
|
type Error = LemmyError;
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ use lemmy_db_schema::{
|
||||||
community::{Community, CommunityUpdateForm},
|
community::{Community, CommunityUpdateForm},
|
||||||
person::Person,
|
person::Person,
|
||||||
post::{Post, PostUpdateForm},
|
post::{Post, PostUpdateForm},
|
||||||
private_message::{PrivateMessage, PrivateMessageUpdateForm},
|
private_message::{PrivateMessage as DbPrivateMessage, PrivateMessageUpdateForm},
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
};
|
};
|
||||||
|
@ -82,7 +82,7 @@ pub(crate) async fn send_apub_delete_in_community(
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub(crate) async fn send_apub_delete_private_message(
|
pub(crate) async fn send_apub_delete_private_message(
|
||||||
actor: &ApubPerson,
|
actor: &ApubPerson,
|
||||||
pm: PrivateMessage,
|
pm: DbPrivateMessage,
|
||||||
deleted: bool,
|
deleted: bool,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
) -> LemmyResult<()> {
|
) -> LemmyResult<()> {
|
||||||
|
@ -298,7 +298,7 @@ async fn receive_delete_action(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DeletableObjects::PrivateMessage(pm) => {
|
DeletableObjects::PrivateMessage(pm) => {
|
||||||
PrivateMessage::update(
|
DbPrivateMessage::update(
|
||||||
&mut context.pool(),
|
&mut context.pool(),
|
||||||
pm.id,
|
pm.id,
|
||||||
&PrivateMessageUpdateForm {
|
&PrivateMessageUpdateForm {
|
||||||
|
|
|
@ -11,11 +11,7 @@ use crate::{
|
||||||
report::Report,
|
report::Report,
|
||||||
update::UpdateCommunity,
|
update::UpdateCommunity,
|
||||||
},
|
},
|
||||||
create_or_update::{
|
create_or_update::{note_wrapper::CreateOrUpdateNoteWrapper, page::CreateOrUpdatePage},
|
||||||
chat_message::CreateOrUpdateChatMessage,
|
|
||||||
note::CreateOrUpdateNote,
|
|
||||||
page::CreateOrUpdatePage,
|
|
||||||
},
|
|
||||||
deletion::{delete::Delete, undo_delete::UndoDelete},
|
deletion::{delete::Delete, undo_delete::UndoDelete},
|
||||||
following::{
|
following::{
|
||||||
accept::AcceptFollow,
|
accept::AcceptFollow,
|
||||||
|
@ -48,47 +44,17 @@ pub enum SharedInboxActivities {
|
||||||
AcceptFollow(AcceptFollow),
|
AcceptFollow(AcceptFollow),
|
||||||
RejectFollow(RejectFollow),
|
RejectFollow(RejectFollow),
|
||||||
UndoFollow(UndoFollow),
|
UndoFollow(UndoFollow),
|
||||||
CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage),
|
|
||||||
Report(Report),
|
Report(Report),
|
||||||
AnnounceActivity(AnnounceActivity),
|
AnnounceActivity(AnnounceActivity),
|
||||||
/// This is a catch-all and needs to be last
|
/// This is a catch-all and needs to be last
|
||||||
RawAnnouncableActivities(RawAnnouncableActivities),
|
RawAnnouncableActivities(RawAnnouncableActivities),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// List of activities which the group inbox can handle.
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
#[enum_delegate::implement(ActivityHandler)]
|
|
||||||
pub enum GroupInboxActivities {
|
|
||||||
Follow(Follow),
|
|
||||||
UndoFollow(UndoFollow),
|
|
||||||
Report(Report),
|
|
||||||
/// This is a catch-all and needs to be last
|
|
||||||
AnnouncableActivities(RawAnnouncableActivities),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// List of activities which the person inbox can handle.
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
#[enum_delegate::implement(ActivityHandler)]
|
|
||||||
pub enum PersonInboxActivities {
|
|
||||||
Follow(Follow),
|
|
||||||
AcceptFollow(AcceptFollow),
|
|
||||||
RejectFollow(RejectFollow),
|
|
||||||
UndoFollow(UndoFollow),
|
|
||||||
CreateOrUpdatePrivateMessage(CreateOrUpdateChatMessage),
|
|
||||||
Delete(Delete),
|
|
||||||
UndoDelete(UndoDelete),
|
|
||||||
AnnounceActivity(AnnounceActivity),
|
|
||||||
/// User can also receive some "announcable" activities, eg a comment mention.
|
|
||||||
AnnouncableActivities(AnnouncableActivities),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
#[enum_delegate::implement(ActivityHandler)]
|
#[enum_delegate::implement(ActivityHandler)]
|
||||||
pub enum AnnouncableActivities {
|
pub enum AnnouncableActivities {
|
||||||
CreateOrUpdateComment(CreateOrUpdateNote),
|
CreateOrUpdateNoteWrapper(CreateOrUpdateNoteWrapper),
|
||||||
CreateOrUpdatePost(CreateOrUpdatePage),
|
CreateOrUpdatePost(CreateOrUpdatePage),
|
||||||
Vote(Vote),
|
Vote(Vote),
|
||||||
UndoVote(UndoVote),
|
UndoVote(UndoVote),
|
||||||
|
@ -111,7 +77,7 @@ impl InCommunity for AnnouncableActivities {
|
||||||
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
|
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
|
||||||
use AnnouncableActivities::*;
|
use AnnouncableActivities::*;
|
||||||
match self {
|
match self {
|
||||||
CreateOrUpdateComment(a) => a.community(context).await,
|
CreateOrUpdateNoteWrapper(a) => a.community(context).await,
|
||||||
CreateOrUpdatePost(a) => a.community(context).await,
|
CreateOrUpdatePost(a) => a.community(context).await,
|
||||||
Vote(a) => a.community(context).await,
|
Vote(a) => a.community(context).await,
|
||||||
UndoVote(a) => a.community(context).await,
|
UndoVote(a) => a.community(context).await,
|
||||||
|
@ -133,40 +99,32 @@ impl InCommunity for AnnouncableActivities {
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
activity_lists::{GroupInboxActivities, PersonInboxActivities, SharedInboxActivities},
|
activity_lists::SharedInboxActivities,
|
||||||
protocol::tests::{test_json, test_parse_lemmy_item},
|
protocol::tests::{test_json, test_parse_lemmy_item},
|
||||||
};
|
};
|
||||||
use lemmy_utils::error::LemmyResult;
|
use lemmy_utils::error::LemmyResult;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_group_inbox() -> LemmyResult<()> {
|
|
||||||
test_parse_lemmy_item::<GroupInboxActivities>("assets/lemmy/activities/following/follow.json")?;
|
|
||||||
test_parse_lemmy_item::<GroupInboxActivities>(
|
|
||||||
"assets/lemmy/activities/create_or_update/create_note.json",
|
|
||||||
)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_person_inbox() -> LemmyResult<()> {
|
|
||||||
test_parse_lemmy_item::<PersonInboxActivities>(
|
|
||||||
"assets/lemmy/activities/following/accept.json",
|
|
||||||
)?;
|
|
||||||
test_parse_lemmy_item::<PersonInboxActivities>(
|
|
||||||
"assets/lemmy/activities/create_or_update/create_note.json",
|
|
||||||
)?;
|
|
||||||
test_parse_lemmy_item::<PersonInboxActivities>(
|
|
||||||
"assets/lemmy/activities/create_or_update/create_private_message.json",
|
|
||||||
)?;
|
|
||||||
test_json::<PersonInboxActivities>("assets/mastodon/activities/follow.json")?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_shared_inbox() -> LemmyResult<()> {
|
fn test_shared_inbox() -> LemmyResult<()> {
|
||||||
test_parse_lemmy_item::<SharedInboxActivities>(
|
test_parse_lemmy_item::<SharedInboxActivities>(
|
||||||
"assets/lemmy/activities/deletion/delete_user.json",
|
"assets/lemmy/activities/deletion/delete_user.json",
|
||||||
)?;
|
)?;
|
||||||
|
test_parse_lemmy_item::<SharedInboxActivities>(
|
||||||
|
"assets/lemmy/activities/following/accept.json",
|
||||||
|
)?;
|
||||||
|
test_parse_lemmy_item::<SharedInboxActivities>(
|
||||||
|
"assets/lemmy/activities/create_or_update/create_comment.json",
|
||||||
|
)?;
|
||||||
|
test_parse_lemmy_item::<SharedInboxActivities>(
|
||||||
|
"assets/lemmy/activities/create_or_update/create_private_message.json",
|
||||||
|
)?;
|
||||||
|
test_parse_lemmy_item::<SharedInboxActivities>(
|
||||||
|
"assets/lemmy/activities/following/follow.json",
|
||||||
|
)?;
|
||||||
|
test_parse_lemmy_item::<SharedInboxActivities>(
|
||||||
|
"assets/lemmy/activities/create_or_update/create_comment.json",
|
||||||
|
)?;
|
||||||
|
test_json::<SharedInboxActivities>("assets/mastodon/activities/follow.json")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,28 +6,41 @@ use crate::{
|
||||||
community_moderators::ApubCommunityModerators,
|
community_moderators::ApubCommunityModerators,
|
||||||
community_outbox::ApubCommunityOutbox,
|
community_outbox::ApubCommunityOutbox,
|
||||||
},
|
},
|
||||||
|
fetcher::site_or_community_or_user::SiteOrCommunityOrUser,
|
||||||
http::{check_community_fetchable, create_apub_response, create_apub_tombstone_response},
|
http::{check_community_fetchable, create_apub_response, create_apub_tombstone_response},
|
||||||
objects::community::ApubCommunity,
|
objects::community::ApubCommunity,
|
||||||
};
|
};
|
||||||
use activitypub_federation::{
|
use activitypub_federation::{
|
||||||
|
actix_web::signing_actor,
|
||||||
config::Data,
|
config::Data,
|
||||||
|
fetch::object_id::ObjectId,
|
||||||
traits::{Collection, Object},
|
traits::{Collection, Object},
|
||||||
};
|
};
|
||||||
use actix_web::{web, HttpRequest, HttpResponse};
|
use actix_web::{
|
||||||
|
web::{Path, Query},
|
||||||
|
HttpRequest,
|
||||||
|
HttpResponse,
|
||||||
|
};
|
||||||
use lemmy_api_common::context::LemmyContext;
|
use lemmy_api_common::context::LemmyContext;
|
||||||
use lemmy_db_schema::{source::community::Community, traits::ApubActor};
|
use lemmy_db_schema::{source::community::Community, traits::ApubActor, CommunityVisibility};
|
||||||
|
use lemmy_db_views_actor::structs::CommunityFollowerView;
|
||||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
#[derive(Deserialize, Clone)]
|
||||||
pub(crate) struct CommunityQuery {
|
pub(crate) struct CommunityPath {
|
||||||
community_name: String,
|
community_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Clone)]
|
||||||
|
pub struct CommunityIsFollowerQuery {
|
||||||
|
is_follower: Option<ObjectId<SiteOrCommunityOrUser>>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the ActivityPub json representation of a local community over HTTP.
|
/// Return the ActivityPub json representation of a local community over HTTP.
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub(crate) async fn get_apub_community_http(
|
pub(crate) async fn get_apub_community_http(
|
||||||
info: web::Path<CommunityQuery>,
|
info: Path<CommunityPath>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
) -> LemmyResult<HttpResponse> {
|
) -> LemmyResult<HttpResponse> {
|
||||||
let community: ApubCommunity =
|
let community: ApubCommunity =
|
||||||
|
@ -47,21 +60,59 @@ pub(crate) async fn get_apub_community_http(
|
||||||
|
|
||||||
/// Returns an empty followers collection, only populating the size (for privacy).
|
/// Returns an empty followers collection, only populating the size (for privacy).
|
||||||
pub(crate) async fn get_apub_community_followers(
|
pub(crate) async fn get_apub_community_followers(
|
||||||
info: web::Path<CommunityQuery>,
|
info: Path<CommunityPath>,
|
||||||
|
query: Query<CommunityIsFollowerQuery>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
|
request: HttpRequest,
|
||||||
) -> LemmyResult<HttpResponse> {
|
) -> LemmyResult<HttpResponse> {
|
||||||
let community = Community::read_from_name(&mut context.pool(), &info.community_name, false)
|
let community = Community::read_from_name(&mut context.pool(), &info.community_name, false)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(LemmyErrorType::NotFound)?;
|
.ok_or(LemmyErrorType::NotFound)?;
|
||||||
|
if let Some(is_follower) = &query.is_follower {
|
||||||
|
return check_is_follower(community, is_follower, context, request).await;
|
||||||
|
}
|
||||||
check_community_fetchable(&community)?;
|
check_community_fetchable(&community)?;
|
||||||
let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?;
|
let followers = ApubCommunityFollower::read_local(&community.into(), &context).await?;
|
||||||
create_apub_response(&followers)
|
create_apub_response(&followers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Checks if a given actor follows the private community. Returns status 200 if true.
|
||||||
|
async fn check_is_follower(
|
||||||
|
community: Community,
|
||||||
|
is_follower: &ObjectId<SiteOrCommunityOrUser>,
|
||||||
|
context: Data<LemmyContext>,
|
||||||
|
request: HttpRequest,
|
||||||
|
) -> LemmyResult<HttpResponse> {
|
||||||
|
if community.visibility != CommunityVisibility::Private {
|
||||||
|
return Ok(HttpResponse::BadRequest().body("must be a private community"));
|
||||||
|
}
|
||||||
|
// also check for http sig so that followers are not exposed publicly
|
||||||
|
let signing_actor = signing_actor::<SiteOrCommunityOrUser>(&request, None, &context).await?;
|
||||||
|
CommunityFollowerView::check_has_followers_from_instance(
|
||||||
|
community.id,
|
||||||
|
signing_actor.instance_id(),
|
||||||
|
&mut context.pool(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let instance_id = is_follower.dereference(&context).await?.instance_id();
|
||||||
|
let has_followers = CommunityFollowerView::check_has_followers_from_instance(
|
||||||
|
community.id,
|
||||||
|
instance_id,
|
||||||
|
&mut context.pool(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if has_followers.is_ok() {
|
||||||
|
Ok(HttpResponse::Ok().finish())
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::NotFound().finish())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the community outbox, which is populated by a maximum of 20 posts (but no other
|
/// Returns the community outbox, which is populated by a maximum of 20 posts (but no other
|
||||||
/// activities like votes or comments).
|
/// activities like votes or comments).
|
||||||
pub(crate) async fn get_apub_community_outbox(
|
pub(crate) async fn get_apub_community_outbox(
|
||||||
info: web::Path<CommunityQuery>,
|
info: Path<CommunityPath>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
) -> LemmyResult<HttpResponse> {
|
) -> LemmyResult<HttpResponse> {
|
||||||
|
@ -77,7 +128,7 @@ pub(crate) async fn get_apub_community_outbox(
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
pub(crate) async fn get_apub_community_moderators(
|
pub(crate) async fn get_apub_community_moderators(
|
||||||
info: web::Path<CommunityQuery>,
|
info: Path<CommunityPath>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
) -> LemmyResult<HttpResponse> {
|
) -> LemmyResult<HttpResponse> {
|
||||||
let community: ApubCommunity =
|
let community: ApubCommunity =
|
||||||
|
@ -92,7 +143,7 @@ pub(crate) async fn get_apub_community_moderators(
|
||||||
|
|
||||||
/// Returns collection of featured (stickied) posts.
|
/// Returns collection of featured (stickied) posts.
|
||||||
pub(crate) async fn get_apub_community_featured(
|
pub(crate) async fn get_apub_community_featured(
|
||||||
info: web::Path<CommunityQuery>,
|
info: Path<CommunityPath>,
|
||||||
context: Data<LemmyContext>,
|
context: Data<LemmyContext>,
|
||||||
request: HttpRequest,
|
request: HttpRequest,
|
||||||
) -> LemmyResult<HttpResponse> {
|
) -> LemmyResult<HttpResponse> {
|
||||||
|
@ -181,17 +232,17 @@ pub(crate) mod tests {
|
||||||
let request = TestRequest::default().to_http_request();
|
let request = TestRequest::default().to_http_request();
|
||||||
|
|
||||||
// fetch invalid community
|
// fetch invalid community
|
||||||
let query = CommunityQuery {
|
let query = CommunityPath {
|
||||||
community_name: "asd".to_string(),
|
community_name: "asd".to_string(),
|
||||||
};
|
};
|
||||||
let res = get_apub_community_http(query.into(), context.reset_request_count()).await;
|
let res = get_apub_community_http(query.into(), context.reset_request_count()).await;
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
|
|
||||||
// fetch valid community
|
// fetch valid community
|
||||||
let query = CommunityQuery {
|
let path = CommunityPath {
|
||||||
community_name: community.name.clone(),
|
community_name: community.name.clone(),
|
||||||
};
|
};
|
||||||
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await?;
|
let res = get_apub_community_http(path.clone().into(), context.reset_request_count()).await?;
|
||||||
assert_eq!(200, res.status());
|
assert_eq!(200, res.status());
|
||||||
let res_group: Group = decode_response(res).await?;
|
let res_group: Group = decode_response(res).await?;
|
||||||
let community: ApubCommunity = community.into();
|
let community: ApubCommunity = community.into();
|
||||||
|
@ -199,20 +250,26 @@ pub(crate) mod tests {
|
||||||
assert_eq!(group, res_group);
|
assert_eq!(group, res_group);
|
||||||
|
|
||||||
let res = get_apub_community_featured(
|
let res = get_apub_community_featured(
|
||||||
query.clone().into(),
|
path.clone().into(),
|
||||||
|
context.reset_request_count(),
|
||||||
|
request.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
assert_eq!(200, res.status());
|
||||||
|
let query = Query(CommunityIsFollowerQuery { is_follower: None });
|
||||||
|
let res = get_apub_community_followers(
|
||||||
|
path.clone().into(),
|
||||||
|
query,
|
||||||
context.reset_request_count(),
|
context.reset_request_count(),
|
||||||
request.clone(),
|
request.clone(),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(200, res.status());
|
assert_eq!(200, res.status());
|
||||||
let res =
|
let res =
|
||||||
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await?;
|
get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await?;
|
||||||
assert_eq!(200, res.status());
|
assert_eq!(200, res.status());
|
||||||
let res =
|
let res =
|
||||||
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await?;
|
get_apub_community_outbox(path.into(), context.reset_request_count(), request).await?;
|
||||||
assert_eq!(200, res.status());
|
|
||||||
let res =
|
|
||||||
get_apub_community_outbox(query.into(), context.reset_request_count(), request).await?;
|
|
||||||
assert_eq!(200, res.status());
|
assert_eq!(200, res.status());
|
||||||
|
|
||||||
Instance::delete(&mut context.pool(), instance.id).await?;
|
Instance::delete(&mut context.pool(), instance.id).await?;
|
||||||
|
@ -227,28 +284,35 @@ pub(crate) mod tests {
|
||||||
let request = TestRequest::default().to_http_request();
|
let request = TestRequest::default().to_http_request();
|
||||||
|
|
||||||
// should return tombstone
|
// should return tombstone
|
||||||
let query = CommunityQuery {
|
let path: Path<CommunityPath> = CommunityPath {
|
||||||
community_name: community.name.clone(),
|
community_name: community.name.clone(),
|
||||||
};
|
}
|
||||||
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await?;
|
.into();
|
||||||
|
let res = get_apub_community_http(path.clone().into(), context.reset_request_count()).await?;
|
||||||
assert_eq!(410, res.status());
|
assert_eq!(410, res.status());
|
||||||
let res_tombstone = decode_response::<Tombstone>(res).await;
|
let res_tombstone = decode_response::<Tombstone>(res).await;
|
||||||
assert!(res_tombstone.is_ok());
|
assert!(res_tombstone.is_ok());
|
||||||
|
|
||||||
let res = get_apub_community_featured(
|
let res = get_apub_community_featured(
|
||||||
query.clone().into(),
|
path.clone().into(),
|
||||||
|
context.reset_request_count(),
|
||||||
|
request.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let query = Query(CommunityIsFollowerQuery { is_follower: None });
|
||||||
|
let res = get_apub_community_followers(
|
||||||
|
path.clone().into(),
|
||||||
|
query,
|
||||||
context.reset_request_count(),
|
context.reset_request_count(),
|
||||||
request.clone(),
|
request.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
let res =
|
let res =
|
||||||
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await;
|
get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await;
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
let res =
|
let res = get_apub_community_outbox(path, context.reset_request_count(), request).await;
|
||||||
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await;
|
|
||||||
assert!(res.is_err());
|
|
||||||
let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await;
|
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
|
|
||||||
//Community::delete(&mut context.pool(), community.id).await?;
|
//Community::delete(&mut context.pool(), community.id).await?;
|
||||||
|
@ -263,25 +327,32 @@ pub(crate) mod tests {
|
||||||
let (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?;
|
let (instance, community) = init(false, CommunityVisibility::LocalOnly, &context).await?;
|
||||||
let request = TestRequest::default().to_http_request();
|
let request = TestRequest::default().to_http_request();
|
||||||
|
|
||||||
let query = CommunityQuery {
|
let path: Path<CommunityPath> = CommunityPath {
|
||||||
community_name: community.name.clone(),
|
community_name: community.name.clone(),
|
||||||
};
|
}
|
||||||
let res = get_apub_community_http(query.clone().into(), context.reset_request_count()).await;
|
.into();
|
||||||
|
let res = get_apub_community_http(path.clone().into(), context.reset_request_count()).await;
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
let res = get_apub_community_featured(
|
let res = get_apub_community_featured(
|
||||||
query.clone().into(),
|
path.clone().into(),
|
||||||
|
context.reset_request_count(),
|
||||||
|
request.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert!(res.is_err());
|
||||||
|
let query = Query(CommunityIsFollowerQuery { is_follower: None });
|
||||||
|
let res = get_apub_community_followers(
|
||||||
|
path.clone().into(),
|
||||||
|
query,
|
||||||
context.reset_request_count(),
|
context.reset_request_count(),
|
||||||
request.clone(),
|
request.clone(),
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
let res =
|
let res =
|
||||||
get_apub_community_followers(query.clone().into(), context.reset_request_count()).await;
|
get_apub_community_moderators(path.clone().into(), context.reset_request_count()).await;
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
let res =
|
let res = get_apub_community_outbox(path, context.reset_request_count(), request).await;
|
||||||
get_apub_community_moderators(query.clone().into(), context.reset_request_count()).await;
|
|
||||||
assert!(res.is_err());
|
|
||||||
let res = get_apub_community_outbox(query.into(), context.reset_request_count(), request).await;
|
|
||||||
assert!(res.is_err());
|
assert!(res.is_err());
|
||||||
|
|
||||||
Instance::delete(&mut context.pool(), instance.id).await?;
|
Instance::delete(&mut context.pool(), instance.id).await?;
|
||||||
|
|
|
@ -8,6 +8,7 @@ use activitypub_federation::{
|
||||||
actix_web::{inbox::receive_activity, signing_actor},
|
actix_web::{inbox::receive_activity, signing_actor},
|
||||||
config::Data,
|
config::Data,
|
||||||
protocol::context::WithContext,
|
protocol::context::WithContext,
|
||||||
|
traits::Actor,
|
||||||
FEDERATION_CONTENT_TYPE,
|
FEDERATION_CONTENT_TYPE,
|
||||||
};
|
};
|
||||||
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
|
use actix_web::{web, web::Bytes, HttpRequest, HttpResponse};
|
||||||
|
@ -145,6 +146,7 @@ async fn check_community_content_fetchable(
|
||||||
// from the fetching instance then fetching is allowed
|
// from the fetching instance then fetching is allowed
|
||||||
Private => {
|
Private => {
|
||||||
let signing_actor = signing_actor::<SiteOrCommunityOrUser>(request, None, context).await?;
|
let signing_actor = signing_actor::<SiteOrCommunityOrUser>(request, None, context).await?;
|
||||||
|
if community.local {
|
||||||
Ok(
|
Ok(
|
||||||
CommunityFollowerView::check_has_followers_from_instance(
|
CommunityFollowerView::check_has_followers_from_instance(
|
||||||
community.id,
|
community.id,
|
||||||
|
@ -153,6 +155,18 @@ async fn check_community_content_fetchable(
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
)
|
)
|
||||||
|
} else if let Some(followers_url) = community.followers_url.clone() {
|
||||||
|
let mut followers_url = followers_url.inner().clone();
|
||||||
|
followers_url
|
||||||
|
.query_pairs_mut()
|
||||||
|
.append_pair("is_follower", signing_actor.id().as_str());
|
||||||
|
let req = context.client().get(followers_url.as_str());
|
||||||
|
let req = context.sign_request(req, Bytes::new()).await?;
|
||||||
|
context.client().execute(req).await?.error_for_status()?;
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(LemmyErrorType::NotFound.into())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,7 @@ use lemmy_db_schema::{
|
||||||
};
|
};
|
||||||
use lemmy_utils::{
|
use lemmy_utils::{
|
||||||
error::{FederationError, LemmyError, LemmyErrorType, LemmyResult},
|
error::{FederationError, LemmyError, LemmyErrorType, LemmyResult},
|
||||||
|
CacheLock,
|
||||||
CACHE_DURATION_FEDERATION,
|
CACHE_DURATION_FEDERATION,
|
||||||
};
|
};
|
||||||
use moka::future::Cache;
|
use moka::future::Cache;
|
||||||
|
@ -139,7 +140,7 @@ pub(crate) async fn local_site_data_cached(
|
||||||
// multiple times. This causes a huge number of database reads if we hit the db directly. So we
|
// multiple times. This causes a huge number of database reads if we hit the db directly. So we
|
||||||
// cache these values for a short time, which will already make a huge difference and ensures that
|
// cache these values for a short time, which will already make a huge difference and ensures that
|
||||||
// changes take effect quickly.
|
// changes take effect quickly.
|
||||||
static CACHE: LazyLock<Cache<(), Arc<LocalSiteData>>> = LazyLock::new(|| {
|
static CACHE: CacheLock<Arc<LocalSiteData>> = LazyLock::new(|| {
|
||||||
Cache::builder()
|
Cache::builder()
|
||||||
.max_capacity(1)
|
.max_capacity(1)
|
||||||
.time_to_live(CACHE_DURATION_FEDERATION)
|
.time_to_live(CACHE_DURATION_FEDERATION)
|
||||||
|
|
|
@ -266,7 +266,7 @@ pub(crate) mod tests {
|
||||||
let url = Url::parse("https://enterprise.lemmy.ml/comment/38741")?;
|
let url = Url::parse("https://enterprise.lemmy.ml/comment/38741")?;
|
||||||
let data = prepare_comment_test(&url, &context).await?;
|
let data = prepare_comment_test(&url, &context).await?;
|
||||||
|
|
||||||
let json: Note = file_to_json_object("assets/lemmy/objects/note.json")?;
|
let json: Note = file_to_json_object("assets/lemmy/objects/comment.json")?;
|
||||||
ApubComment::verify(&json, &url, &context).await?;
|
ApubComment::verify(&json, &url, &context).await?;
|
||||||
let comment = ApubComment::from_json(json.clone(), &context).await?;
|
let comment = ApubComment::from_json(json.clone(), &context).await?;
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ use std::fmt::Debug;
|
||||||
pub mod comment;
|
pub mod comment;
|
||||||
pub mod community;
|
pub mod community;
|
||||||
pub mod instance;
|
pub mod instance;
|
||||||
|
pub mod note_wrapper;
|
||||||
pub mod person;
|
pub mod person;
|
||||||
pub mod post;
|
pub mod post;
|
||||||
pub mod private_message;
|
pub mod private_message;
|
||||||
|
|
85
crates/apub/src/objects/note_wrapper.rs
Normal file
85
crates/apub/src/objects/note_wrapper.rs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
use super::comment::ApubComment;
|
||||||
|
use crate::{
|
||||||
|
objects::private_message::ApubPrivateMessage,
|
||||||
|
protocol::objects::note_wrapper::NoteWrapper,
|
||||||
|
};
|
||||||
|
use activitypub_federation::{config::Data, kinds::public, traits::Object};
|
||||||
|
use chrono::{DateTime, Utc};
|
||||||
|
use lemmy_api_common::{context::LemmyContext, LemmyErrorType};
|
||||||
|
use lemmy_utils::error::{LemmyError, LemmyResult};
|
||||||
|
use serde_json::{from_value, to_value};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
/// Private messages and public comments are quite awkward in Activitypub, because the json
|
||||||
|
/// format looks identical. They only way to differentiate them is to check for the presence
|
||||||
|
/// or absence of `https://www.w3.org/ns/activitystreams#Public` in `to` or `cc` which this
|
||||||
|
/// wrapper does.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct ApubNote {}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl Object for ApubNote {
|
||||||
|
type DataType = LemmyContext;
|
||||||
|
type Kind = NoteWrapper;
|
||||||
|
type Error = LemmyError;
|
||||||
|
|
||||||
|
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn read_from_id(
|
||||||
|
_object_id: Url,
|
||||||
|
_context: &Data<Self::DataType>,
|
||||||
|
) -> LemmyResult<Option<Self>> {
|
||||||
|
Err(LemmyErrorType::Unknown("not implemented".to_string()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument(skip_all)]
|
||||||
|
async fn delete(self, _context: &Data<Self::DataType>) -> LemmyResult<()> {
|
||||||
|
Err(LemmyErrorType::Unknown("not implemented".to_string()).into())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn verify(
|
||||||
|
note: &NoteWrapper,
|
||||||
|
expected_domain: &Url,
|
||||||
|
context: &Data<LemmyContext>,
|
||||||
|
) -> LemmyResult<()> {
|
||||||
|
let val = to_value(note)?;
|
||||||
|
if is_public(¬e.to, ¬e.cc) {
|
||||||
|
ApubComment::verify(&from_value(val)?, expected_domain, context).await?;
|
||||||
|
} else {
|
||||||
|
ApubPrivateMessage::verify(&from_value(val)?, expected_domain, context).await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn from_json(note: NoteWrapper, context: &Data<LemmyContext>) -> LemmyResult<ApubNote> {
|
||||||
|
let is_public = is_public(¬e.to, ¬e.cc);
|
||||||
|
let val = to_value(note)?;
|
||||||
|
if is_public {
|
||||||
|
ApubComment::from_json(from_value(val)?, context).await?;
|
||||||
|
} else {
|
||||||
|
ApubPrivateMessage::from_json(from_value(val)?, context).await?;
|
||||||
|
}
|
||||||
|
Ok(ApubNote {})
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn into_json(self, _context: &Data<Self::DataType>) -> LemmyResult<NoteWrapper> {
|
||||||
|
Err(LemmyErrorType::Unknown("not implemented".to_string()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_public(to: &Option<Vec<Url>>, cc: &Option<Vec<Url>>) -> bool {
|
||||||
|
if let Some(to) = to {
|
||||||
|
if to.contains(&public()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(cc) = cc {
|
||||||
|
if cc.contains(&public()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
fetcher::markdown_links::markdown_rewrite_remote_links,
|
fetcher::markdown_links::markdown_rewrite_remote_links,
|
||||||
objects::read_from_string_or_source,
|
objects::read_from_string_or_source,
|
||||||
protocol::{
|
protocol::{
|
||||||
objects::chat_message::{ChatMessage, ChatMessageType},
|
objects::private_message::{PrivateMessage, PrivateMessageType},
|
||||||
Source,
|
Source,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -25,10 +25,11 @@ use lemmy_api_common::{
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
source::{
|
source::{
|
||||||
|
instance::Instance,
|
||||||
local_site::LocalSite,
|
local_site::LocalSite,
|
||||||
person::Person,
|
person::Person,
|
||||||
person_block::PersonBlock,
|
person_block::PersonBlock,
|
||||||
private_message::{PrivateMessage, PrivateMessageInsertForm},
|
private_message::{PrivateMessage as DbPrivateMessage, PrivateMessageInsertForm},
|
||||||
},
|
},
|
||||||
traits::Crud,
|
traits::Crud,
|
||||||
};
|
};
|
||||||
|
@ -37,21 +38,22 @@ use lemmy_utils::{
|
||||||
error::{FederationError, LemmyError, LemmyErrorType, LemmyResult},
|
error::{FederationError, LemmyError, LemmyErrorType, LemmyResult},
|
||||||
utils::markdown::markdown_to_html,
|
utils::markdown::markdown_to_html,
|
||||||
};
|
};
|
||||||
|
use semver::{Version, VersionReq};
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ApubPrivateMessage(pub(crate) PrivateMessage);
|
pub struct ApubPrivateMessage(pub(crate) DbPrivateMessage);
|
||||||
|
|
||||||
impl Deref for ApubPrivateMessage {
|
impl Deref for ApubPrivateMessage {
|
||||||
type Target = PrivateMessage;
|
type Target = DbPrivateMessage;
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PrivateMessage> for ApubPrivateMessage {
|
impl From<DbPrivateMessage> for ApubPrivateMessage {
|
||||||
fn from(pm: PrivateMessage) -> Self {
|
fn from(pm: DbPrivateMessage) -> Self {
|
||||||
ApubPrivateMessage(pm)
|
ApubPrivateMessage(pm)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +61,7 @@ impl From<PrivateMessage> for ApubPrivateMessage {
|
||||||
#[async_trait::async_trait]
|
#[async_trait::async_trait]
|
||||||
impl Object for ApubPrivateMessage {
|
impl Object for ApubPrivateMessage {
|
||||||
type DataType = LemmyContext;
|
type DataType = LemmyContext;
|
||||||
type Kind = ChatMessage;
|
type Kind = PrivateMessage;
|
||||||
type Error = LemmyError;
|
type Error = LemmyError;
|
||||||
|
|
||||||
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
fn last_refreshed_at(&self) -> Option<DateTime<Utc>> {
|
||||||
|
@ -72,7 +74,7 @@ impl Object for ApubPrivateMessage {
|
||||||
context: &Data<Self::DataType>,
|
context: &Data<Self::DataType>,
|
||||||
) -> LemmyResult<Option<Self>> {
|
) -> LemmyResult<Option<Self>> {
|
||||||
Ok(
|
Ok(
|
||||||
PrivateMessage::read_from_apub_id(&mut context.pool(), object_id)
|
DbPrivateMessage::read_from_apub_id(&mut context.pool(), object_id)
|
||||||
.await?
|
.await?
|
||||||
.map(Into::into),
|
.map(Into::into),
|
||||||
)
|
)
|
||||||
|
@ -84,15 +86,26 @@ impl Object for ApubPrivateMessage {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn into_json(self, context: &Data<Self::DataType>) -> LemmyResult<ChatMessage> {
|
async fn into_json(self, context: &Data<Self::DataType>) -> LemmyResult<PrivateMessage> {
|
||||||
let creator_id = self.creator_id;
|
let creator_id = self.creator_id;
|
||||||
let creator = Person::read(&mut context.pool(), creator_id).await?;
|
let creator = Person::read(&mut context.pool(), creator_id).await?;
|
||||||
|
|
||||||
let recipient_id = self.recipient_id;
|
let recipient_id = self.recipient_id;
|
||||||
let recipient = Person::read(&mut context.pool(), recipient_id).await?;
|
let recipient = Person::read(&mut context.pool(), recipient_id).await?;
|
||||||
|
|
||||||
let note = ChatMessage {
|
let instance = Instance::read(&mut context.pool(), recipient.instance_id).await?;
|
||||||
r#type: ChatMessageType::ChatMessage,
|
let mut kind = PrivateMessageType::Note;
|
||||||
|
|
||||||
|
// Deprecated: For Lemmy versions before 0.20, send private messages with old type
|
||||||
|
if let (Some(software), Some(version)) = (instance.software, &instance.version) {
|
||||||
|
let req = VersionReq::parse("<0.20")?;
|
||||||
|
if software == "lemmy" && req.matches(&Version::parse(version)?) {
|
||||||
|
kind = PrivateMessageType::ChatMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let note = PrivateMessage {
|
||||||
|
kind,
|
||||||
id: self.ap_id.clone().into(),
|
id: self.ap_id.clone().into(),
|
||||||
attributed_to: creator.actor_id.into(),
|
attributed_to: creator.actor_id.into(),
|
||||||
to: [recipient.actor_id.into()],
|
to: [recipient.actor_id.into()],
|
||||||
|
@ -107,7 +120,7 @@ impl Object for ApubPrivateMessage {
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn verify(
|
async fn verify(
|
||||||
note: &ChatMessage,
|
note: &PrivateMessage,
|
||||||
expected_domain: &Url,
|
expected_domain: &Url,
|
||||||
context: &Data<Self::DataType>,
|
context: &Data<Self::DataType>,
|
||||||
) -> LemmyResult<()> {
|
) -> LemmyResult<()> {
|
||||||
|
@ -128,7 +141,7 @@ impl Object for ApubPrivateMessage {
|
||||||
|
|
||||||
#[tracing::instrument(skip_all)]
|
#[tracing::instrument(skip_all)]
|
||||||
async fn from_json(
|
async fn from_json(
|
||||||
note: ChatMessage,
|
note: PrivateMessage,
|
||||||
context: &Data<Self::DataType>,
|
context: &Data<Self::DataType>,
|
||||||
) -> LemmyResult<ApubPrivateMessage> {
|
) -> LemmyResult<ApubPrivateMessage> {
|
||||||
let creator = note.attributed_to.dereference(context).await?;
|
let creator = note.attributed_to.dereference(context).await?;
|
||||||
|
@ -161,7 +174,7 @@ impl Object for ApubPrivateMessage {
|
||||||
local: Some(false),
|
local: Some(false),
|
||||||
};
|
};
|
||||||
let timestamp = note.updated.or(note.published).unwrap_or_else(Utc::now);
|
let timestamp = note.updated.or(note.published).unwrap_or_else(Utc::now);
|
||||||
let pm = PrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?;
|
let pm = DbPrivateMessage::insert_apub(&mut context.pool(), timestamp, &form).await?;
|
||||||
Ok(pm.into())
|
Ok(pm.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -213,7 +226,7 @@ mod tests {
|
||||||
let context = LemmyContext::init_test_context().await;
|
let context = LemmyContext::init_test_context().await;
|
||||||
let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621")?;
|
let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621")?;
|
||||||
let data = prepare_comment_test(&url, &context).await?;
|
let data = prepare_comment_test(&url, &context).await?;
|
||||||
let json: ChatMessage = file_to_json_object("assets/lemmy/objects/chat_message.json")?;
|
let json: PrivateMessage = file_to_json_object("assets/lemmy/objects/private_message.json")?;
|
||||||
ApubPrivateMessage::verify(&json, &url, &context).await?;
|
ApubPrivateMessage::verify(&json, &url, &context).await?;
|
||||||
let pm = ApubPrivateMessage::from_json(json.clone(), &context).await?;
|
let pm = ApubPrivateMessage::from_json(json.clone(), &context).await?;
|
||||||
|
|
||||||
|
@ -225,27 +238,7 @@ mod tests {
|
||||||
let to_apub = pm.into_json(&context).await?;
|
let to_apub = pm.into_json(&context).await?;
|
||||||
assert_json_include!(actual: json, expected: to_apub);
|
assert_json_include!(actual: json, expected: to_apub);
|
||||||
|
|
||||||
PrivateMessage::delete(&mut context.pool(), pm_id).await?;
|
DbPrivateMessage::delete(&mut context.pool(), pm_id).await?;
|
||||||
cleanup(data, &context).await?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[serial]
|
|
||||||
async fn test_parse_pleroma_pm() -> LemmyResult<()> {
|
|
||||||
let context = LemmyContext::init_test_context().await;
|
|
||||||
let url = Url::parse("https://enterprise.lemmy.ml/private_message/1621")?;
|
|
||||||
let data = prepare_comment_test(&url, &context).await?;
|
|
||||||
let pleroma_url = Url::parse("https://queer.hacktivis.me/objects/2")?;
|
|
||||||
let json = file_to_json_object("assets/pleroma/objects/chat_message.json")?;
|
|
||||||
ApubPrivateMessage::verify(&json, &pleroma_url, &context).await?;
|
|
||||||
let pm = ApubPrivateMessage::from_json(json, &context).await?;
|
|
||||||
|
|
||||||
assert_eq!(pm.ap_id, pleroma_url.into());
|
|
||||||
assert_eq!(pm.content.len(), 3);
|
|
||||||
assert_eq!(context.request_count(), 0);
|
|
||||||
|
|
||||||
PrivateMessage::delete(&mut context.pool(), pm.id).await?;
|
|
||||||
cleanup(data, &context).await?;
|
cleanup(data, &context).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,16 @@
|
||||||
pub mod chat_message;
|
|
||||||
pub mod note;
|
pub mod note;
|
||||||
|
pub(crate) mod note_wrapper;
|
||||||
pub mod page;
|
pub mod page;
|
||||||
|
pub mod private_message;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::note_wrapper::CreateOrUpdateNoteWrapper;
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
activities::create_or_update::{
|
activities::create_or_update::{
|
||||||
chat_message::CreateOrUpdateChatMessage,
|
|
||||||
note::CreateOrUpdateNote,
|
note::CreateOrUpdateNote,
|
||||||
page::CreateOrUpdatePage,
|
page::CreateOrUpdatePage,
|
||||||
|
private_message::CreateOrUpdatePrivateMessage,
|
||||||
},
|
},
|
||||||
tests::test_parse_lemmy_item,
|
tests::test_parse_lemmy_item,
|
||||||
};
|
};
|
||||||
|
@ -23,9 +25,15 @@ mod tests {
|
||||||
"assets/lemmy/activities/create_or_update/update_page.json",
|
"assets/lemmy/activities/create_or_update/update_page.json",
|
||||||
)?;
|
)?;
|
||||||
test_parse_lemmy_item::<CreateOrUpdateNote>(
|
test_parse_lemmy_item::<CreateOrUpdateNote>(
|
||||||
"assets/lemmy/activities/create_or_update/create_note.json",
|
"assets/lemmy/activities/create_or_update/create_comment.json",
|
||||||
)?;
|
)?;
|
||||||
test_parse_lemmy_item::<CreateOrUpdateChatMessage>(
|
test_parse_lemmy_item::<CreateOrUpdatePrivateMessage>(
|
||||||
|
"assets/lemmy/activities/create_or_update/create_private_message.json",
|
||||||
|
)?;
|
||||||
|
test_parse_lemmy_item::<CreateOrUpdateNoteWrapper>(
|
||||||
|
"assets/lemmy/activities/create_or_update/create_comment.json",
|
||||||
|
)?;
|
||||||
|
test_parse_lemmy_item::<CreateOrUpdateNoteWrapper>(
|
||||||
"assets/lemmy/activities/create_or_update/create_private_message.json",
|
"assets/lemmy/activities/create_or_update/create_private_message.json",
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
use crate::protocol::objects::note_wrapper::NoteWrapper;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct CreateOrUpdateNoteWrapper {
|
||||||
|
object: NoteWrapper,
|
||||||
|
pub(crate) id: Url,
|
||||||
|
pub(crate) actor: Url,
|
||||||
|
pub(crate) to: Option<Vec<Url>>,
|
||||||
|
pub(crate) cc: Option<Vec<Url>>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
other: Map<String, Value>,
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
objects::person::ApubPerson,
|
objects::person::ApubPerson,
|
||||||
protocol::{activities::CreateOrUpdateType, objects::chat_message::ChatMessage},
|
protocol::{activities::CreateOrUpdateType, objects::private_message::PrivateMessage},
|
||||||
};
|
};
|
||||||
use activitypub_federation::{fetch::object_id::ObjectId, protocol::helpers::deserialize_one};
|
use activitypub_federation::{fetch::object_id::ObjectId, protocol::helpers::deserialize_one};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -8,12 +8,12 @@ use url::Url;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct CreateOrUpdateChatMessage {
|
pub struct CreateOrUpdatePrivateMessage {
|
||||||
pub(crate) id: Url,
|
pub(crate) id: Url,
|
||||||
pub(crate) actor: ObjectId<ApubPerson>,
|
pub(crate) actor: ObjectId<ApubPerson>,
|
||||||
#[serde(deserialize_with = "deserialize_one")]
|
#[serde(deserialize_with = "deserialize_one")]
|
||||||
pub(crate) to: [ObjectId<ApubPerson>; 1],
|
pub(crate) to: [ObjectId<ApubPerson>; 1],
|
||||||
pub(crate) object: ChatMessage,
|
pub(crate) object: PrivateMessage,
|
||||||
#[serde(rename = "type")]
|
#[serde(rename = "type")]
|
||||||
pub(crate) kind: CreateOrUpdateType,
|
pub(crate) kind: CreateOrUpdateType,
|
||||||
}
|
}
|
|
@ -116,6 +116,7 @@ pub(crate) mod tests {
|
||||||
// parse file into hashmap, which ensures that every field is included
|
// parse file into hashmap, which ensures that every field is included
|
||||||
let raw = file_to_json_object::<HashMap<String, serde_json::Value>>(path)?;
|
let raw = file_to_json_object::<HashMap<String, serde_json::Value>>(path)?;
|
||||||
// assert that all fields are identical, otherwise print diff
|
// assert that all fields are identical, otherwise print diff
|
||||||
|
//dbg!(&parsed, &raw);
|
||||||
assert_json_include!(actual: &parsed, expected: raw);
|
assert_json_include!(actual: &parsed, expected: raw);
|
||||||
Ok(parsed)
|
Ok(parsed)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,12 +8,13 @@ use lemmy_utils::error::LemmyResult;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
pub(crate) mod chat_message;
|
|
||||||
pub(crate) mod group;
|
pub(crate) mod group;
|
||||||
pub(crate) mod instance;
|
pub(crate) mod instance;
|
||||||
pub(crate) mod note;
|
pub(crate) mod note;
|
||||||
|
pub(crate) mod note_wrapper;
|
||||||
pub(crate) mod page;
|
pub(crate) mod page;
|
||||||
pub(crate) mod person;
|
pub(crate) mod person;
|
||||||
|
pub(crate) mod private_message;
|
||||||
pub(crate) mod tombstone;
|
pub(crate) mod tombstone;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
|
||||||
|
@ -101,14 +102,15 @@ impl LanguageTag {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::note_wrapper::NoteWrapper;
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
objects::{
|
objects::{
|
||||||
chat_message::ChatMessage,
|
|
||||||
group::Group,
|
group::Group,
|
||||||
instance::Instance,
|
instance::Instance,
|
||||||
note::Note,
|
note::Note,
|
||||||
page::Page,
|
page::Page,
|
||||||
person::Person,
|
person::Person,
|
||||||
|
private_message::PrivateMessage,
|
||||||
tombstone::Tombstone,
|
tombstone::Tombstone,
|
||||||
},
|
},
|
||||||
tests::{test_json, test_parse_lemmy_item},
|
tests::{test_json, test_parse_lemmy_item},
|
||||||
|
@ -121,8 +123,10 @@ mod tests {
|
||||||
test_parse_lemmy_item::<Group>("assets/lemmy/objects/group.json")?;
|
test_parse_lemmy_item::<Group>("assets/lemmy/objects/group.json")?;
|
||||||
test_parse_lemmy_item::<Person>("assets/lemmy/objects/person.json")?;
|
test_parse_lemmy_item::<Person>("assets/lemmy/objects/person.json")?;
|
||||||
test_parse_lemmy_item::<Page>("assets/lemmy/objects/page.json")?;
|
test_parse_lemmy_item::<Page>("assets/lemmy/objects/page.json")?;
|
||||||
test_parse_lemmy_item::<Note>("assets/lemmy/objects/note.json")?;
|
test_parse_lemmy_item::<Note>("assets/lemmy/objects/comment.json")?;
|
||||||
test_parse_lemmy_item::<ChatMessage>("assets/lemmy/objects/chat_message.json")?;
|
test_parse_lemmy_item::<PrivateMessage>("assets/lemmy/objects/private_message.json")?;
|
||||||
|
test_parse_lemmy_item::<NoteWrapper>("assets/lemmy/objects/comment.json")?;
|
||||||
|
test_parse_lemmy_item::<NoteWrapper>("assets/lemmy/objects/private_message.json")?;
|
||||||
test_parse_lemmy_item::<Tombstone>("assets/lemmy/objects/tombstone.json")?;
|
test_parse_lemmy_item::<Tombstone>("assets/lemmy/objects/tombstone.json")?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -131,7 +135,6 @@ mod tests {
|
||||||
fn test_parse_objects_pleroma() -> LemmyResult<()> {
|
fn test_parse_objects_pleroma() -> LemmyResult<()> {
|
||||||
test_json::<Person>("assets/pleroma/objects/person.json")?;
|
test_json::<Person>("assets/pleroma/objects/person.json")?;
|
||||||
test_json::<Note>("assets/pleroma/objects/note.json")?;
|
test_json::<Note>("assets/pleroma/objects/note.json")?;
|
||||||
test_json::<ChatMessage>("assets/pleroma/objects/chat_message.json")?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
14
crates/apub/src/protocol/objects/note_wrapper.rs
Normal file
14
crates/apub/src/protocol/objects/note_wrapper.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
use activitypub_federation::kinds::object::NoteType;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub(crate) struct NoteWrapper {
|
||||||
|
pub(crate) r#type: NoteType,
|
||||||
|
pub(crate) to: Option<Vec<Url>>,
|
||||||
|
pub(crate) cc: Option<Vec<Url>>,
|
||||||
|
#[serde(flatten)]
|
||||||
|
other: Map<String, Value>,
|
||||||
|
}
|
|
@ -16,8 +16,9 @@ use serde_with::skip_serializing_none;
|
||||||
#[skip_serializing_none]
|
#[skip_serializing_none]
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ChatMessage {
|
pub struct PrivateMessage {
|
||||||
pub(crate) r#type: ChatMessageType,
|
#[serde(rename = "type")]
|
||||||
|
pub(crate) kind: PrivateMessageType,
|
||||||
pub(crate) id: ObjectId<ApubPrivateMessage>,
|
pub(crate) id: ObjectId<ApubPrivateMessage>,
|
||||||
pub(crate) attributed_to: ObjectId<ApubPerson>,
|
pub(crate) attributed_to: ObjectId<ApubPerson>,
|
||||||
#[serde(deserialize_with = "deserialize_one")]
|
#[serde(deserialize_with = "deserialize_one")]
|
||||||
|
@ -31,8 +32,10 @@ pub struct ChatMessage {
|
||||||
pub(crate) updated: Option<DateTime<Utc>>,
|
pub(crate) updated: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages
|
|
||||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
pub enum ChatMessageType {
|
pub enum PrivateMessageType {
|
||||||
|
/// For compatibility with Lemmy 0.19 and earlier
|
||||||
|
/// https://docs.pleroma.social/backend/development/ap_extensions/#chatmessages
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
|
Note,
|
||||||
}
|
}
|
|
@ -75,11 +75,10 @@ tokio = { workspace = true, optional = true }
|
||||||
tokio-postgres = { workspace = true, optional = true }
|
tokio-postgres = { workspace = true, optional = true }
|
||||||
tokio-postgres-rustls = { workspace = true, optional = true }
|
tokio-postgres-rustls = { workspace = true, optional = true }
|
||||||
rustls = { workspace = true, optional = true }
|
rustls = { workspace = true, optional = true }
|
||||||
uuid = { workspace = true, features = ["v4"] }
|
uuid.workspace = true
|
||||||
i-love-jesus = { workspace = true, optional = true }
|
i-love-jesus = { workspace = true, optional = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
diesel-bind-if-some = { workspace = true, optional = true }
|
diesel-bind-if-some = { workspace = true, optional = true }
|
||||||
moka.workspace = true
|
|
||||||
derive-new.workspace = true
|
derive-new.workspace = true
|
||||||
tuplex = { workspace = true, optional = true }
|
tuplex = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
|
|
@ -384,6 +384,44 @@ END;
|
||||||
|
|
||||||
$$);
|
$$);
|
||||||
|
|
||||||
|
CALL r.create_triggers ('post_report', $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE
|
||||||
|
post_aggregates AS a
|
||||||
|
SET
|
||||||
|
report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
(post_report).post_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (post_report).resolved), 0) AS unresolved_report_count
|
||||||
|
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (post_report).post_id) AS diff
|
||||||
|
WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0)
|
||||||
|
AND a.post_id = diff.post_id;
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
|
||||||
|
END;
|
||||||
|
|
||||||
|
$$);
|
||||||
|
|
||||||
|
CALL r.create_triggers ('comment_report', $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE
|
||||||
|
comment_aggregates AS a
|
||||||
|
SET
|
||||||
|
report_count = a.report_count + diff.report_count, unresolved_report_count = a.unresolved_report_count + diff.unresolved_report_count
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
(comment_report).comment_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (comment_report).resolved), 0) AS unresolved_report_count
|
||||||
|
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (comment_report).comment_id) AS diff
|
||||||
|
WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0)
|
||||||
|
AND a.comment_id = diff.comment_id;
|
||||||
|
|
||||||
|
RETURN NULL;
|
||||||
|
|
||||||
|
END;
|
||||||
|
|
||||||
|
$$);
|
||||||
|
|
||||||
-- These triggers create and update rows in each aggregates table to match its associated table's rows.
|
-- These triggers create and update rows in each aggregates table to match its associated table's rows.
|
||||||
-- Deleting rows and updating IDs are already handled by `CASCADE` in foreign key constraints.
|
-- Deleting rows and updating IDs are already handled by `CASCADE` in foreign key constraints.
|
||||||
CREATE FUNCTION r.comment_aggregates_from_comment ()
|
CREATE FUNCTION r.comment_aggregates_from_comment ()
|
||||||
|
|
|
@ -39,6 +39,8 @@ pub struct CommentAggregates {
|
||||||
pub hot_rank: f64,
|
pub hot_rank: f64,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub controversy_rank: f64,
|
pub controversy_rank: f64,
|
||||||
|
pub report_count: i16,
|
||||||
|
pub unresolved_report_count: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
|
#[derive(PartialEq, Debug, Serialize, Deserialize, Clone)]
|
||||||
|
@ -146,6 +148,8 @@ pub struct PostAggregates {
|
||||||
/// A rank that amplifies smaller communities
|
/// A rank that amplifies smaller communities
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub scaled_rank: f64,
|
pub scaled_rank: f64,
|
||||||
|
pub report_count: i16,
|
||||||
|
pub unresolved_report_count: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone)]
|
||||||
|
|
|
@ -5,8 +5,7 @@ use crate::{
|
||||||
};
|
};
|
||||||
use diesel::{dsl::insert_into, result::Error};
|
use diesel::{dsl::insert_into, result::Error};
|
||||||
use diesel_async::RunQueryDsl;
|
use diesel_async::RunQueryDsl;
|
||||||
use lemmy_utils::{error::LemmyResult, CACHE_DURATION_API};
|
use lemmy_utils::{build_cache, error::LemmyResult, CacheLock};
|
||||||
use moka::future::Cache;
|
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
impl LocalSite {
|
impl LocalSite {
|
||||||
|
@ -18,12 +17,7 @@ impl LocalSite {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
pub async fn read(pool: &mut DbPool<'_>) -> LemmyResult<Self> {
|
pub async fn read(pool: &mut DbPool<'_>) -> LemmyResult<Self> {
|
||||||
static CACHE: LazyLock<Cache<(), LocalSite>> = LazyLock::new(|| {
|
static CACHE: CacheLock<LocalSite> = LazyLock::new(build_cache);
|
||||||
Cache::builder()
|
|
||||||
.max_capacity(1)
|
|
||||||
.time_to_live(CACHE_DURATION_API)
|
|
||||||
.build()
|
|
||||||
});
|
|
||||||
Ok(
|
Ok(
|
||||||
CACHE
|
CACHE
|
||||||
.try_get_with((), async {
|
.try_get_with((), async {
|
||||||
|
|
|
@ -130,6 +130,8 @@ diesel::table! {
|
||||||
child_count -> Int4,
|
child_count -> Int4,
|
||||||
hot_rank -> Float8,
|
hot_rank -> Float8,
|
||||||
controversy_rank -> Float8,
|
controversy_rank -> Float8,
|
||||||
|
report_count -> Int2,
|
||||||
|
unresolved_report_count -> Int2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -777,6 +779,8 @@ diesel::table! {
|
||||||
controversy_rank -> Float8,
|
controversy_rank -> Float8,
|
||||||
instance_id -> Int4,
|
instance_id -> Int4,
|
||||||
scaled_rank -> Float8,
|
scaled_rank -> Float8,
|
||||||
|
report_count -> Int2,
|
||||||
|
unresolved_report_count -> Int2,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -96,7 +96,7 @@ pub async fn get_conn<'a, 'b: 'a>(pool: &'a mut DbPool<'b>) -> Result<DbConn<'a>
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Deref for DbConn<'a> {
|
impl Deref for DbConn<'_> {
|
||||||
type Target = AsyncPgConnection;
|
type Target = AsyncPgConnection;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
|
@ -107,7 +107,7 @@ impl<'a> Deref for DbConn<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> DerefMut for DbConn<'a> {
|
impl DerefMut for DbConn<'_> {
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
match self {
|
match self {
|
||||||
DbConn::Pool(conn) => conn.deref_mut(),
|
DbConn::Pool(conn) => conn.deref_mut(),
|
||||||
|
|
|
@ -444,6 +444,8 @@ mod tests {
|
||||||
child_count: 0,
|
child_count: 0,
|
||||||
hot_rank: RANK_DEFAULT,
|
hot_rank: RANK_DEFAULT,
|
||||||
controversy_rank: 0.0,
|
controversy_rank: 0.0,
|
||||||
|
report_count: 2,
|
||||||
|
unresolved_report_count: 2,
|
||||||
},
|
},
|
||||||
my_vote: None,
|
my_vote: None,
|
||||||
resolver: None,
|
resolver: None,
|
||||||
|
@ -511,6 +513,10 @@ mod tests {
|
||||||
.updated = read_jessica_report_view_after_resolve
|
.updated = read_jessica_report_view_after_resolve
|
||||||
.comment_report
|
.comment_report
|
||||||
.updated;
|
.updated;
|
||||||
|
expected_jessica_report_view_after_resolve
|
||||||
|
.counts
|
||||||
|
.unresolved_report_count = 1;
|
||||||
|
expected_sara_report_view.counts.unresolved_report_count = 1;
|
||||||
expected_jessica_report_view_after_resolve.resolver = Some(Person {
|
expected_jessica_report_view_after_resolve.resolver = Some(Person {
|
||||||
id: inserted_timmy.id,
|
id: inserted_timmy.id,
|
||||||
name: inserted_timmy.name.clone(),
|
name: inserted_timmy.name.clone(),
|
||||||
|
|
|
@ -50,9 +50,12 @@ use lemmy_db_schema::{
|
||||||
ListingType,
|
ListingType,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type QueriesReadTypes<'a> = (CommentId, Option<&'a LocalUser>);
|
||||||
|
type QueriesListTypes<'a> = (CommentQuery<'a>, &'a Site);
|
||||||
|
|
||||||
fn queries<'a>() -> Queries<
|
fn queries<'a>() -> Queries<
|
||||||
impl ReadFn<'a, CommentView, (CommentId, Option<&'a LocalUser>)>,
|
impl ReadFn<'a, CommentView, QueriesReadTypes<'a>>,
|
||||||
impl ListFn<'a, CommentView, (CommentQuery<'a>, &'a Site)>,
|
impl ListFn<'a, CommentView, QueriesListTypes<'a>>,
|
||||||
> {
|
> {
|
||||||
let creator_is_admin = exists(
|
let creator_is_admin = exists(
|
||||||
local_user::table.filter(
|
local_user::table.filter(
|
||||||
|
@ -308,10 +311,10 @@ fn queries<'a>() -> Queries<
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommentView {
|
impl CommentView {
|
||||||
pub async fn read<'a>(
|
pub async fn read(
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
comment_id: CommentId,
|
comment_id: CommentId,
|
||||||
my_local_user: Option<&'a LocalUser>,
|
my_local_user: Option<&'_ LocalUser>,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
// If a person is given, then my_vote (res.9), if None, should be 0, not null
|
// If a person is given, then my_vote (res.9), if None, should be 0, not null
|
||||||
// Necessary to differentiate between other person's votes
|
// Necessary to differentiate between other person's votes
|
||||||
|
@ -345,7 +348,7 @@ pub struct CommentQuery<'a> {
|
||||||
pub max_depth: Option<i32>,
|
pub max_depth: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CommentQuery<'a> {
|
impl CommentQuery<'_> {
|
||||||
pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result<Vec<CommentView>, Error> {
|
pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result<Vec<CommentView>, Error> {
|
||||||
Ok(
|
Ok(
|
||||||
queries()
|
queries()
|
||||||
|
@ -1065,6 +1068,8 @@ mod tests {
|
||||||
child_count: 5,
|
child_count: 5,
|
||||||
hot_rank: RANK_DEFAULT,
|
hot_rank: RANK_DEFAULT,
|
||||||
controversy_rank: 0.0,
|
controversy_rank: 0.0,
|
||||||
|
report_count: 0,
|
||||||
|
unresolved_report_count: 0,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,6 +232,7 @@ mod tests {
|
||||||
structs::LocalUserView,
|
structs::LocalUserView,
|
||||||
};
|
};
|
||||||
use lemmy_db_schema::{
|
use lemmy_db_schema::{
|
||||||
|
aggregates::structs::PostAggregates,
|
||||||
assert_length,
|
assert_length,
|
||||||
source::{
|
source::{
|
||||||
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
|
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
|
||||||
|
@ -336,6 +337,10 @@ mod tests {
|
||||||
let read_jessica_report_view =
|
let read_jessica_report_view =
|
||||||
PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?;
|
PostReportView::read(pool, inserted_jessica_report.id, inserted_timmy.id).await?;
|
||||||
|
|
||||||
|
// Make sure the triggers are reading the aggregates correctly.
|
||||||
|
let agg_1 = PostAggregates::read(pool, inserted_post.id).await?;
|
||||||
|
let agg_2 = PostAggregates::read(pool, inserted_post_2.id).await?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
read_jessica_report_view.post_report,
|
read_jessica_report_view.post_report,
|
||||||
inserted_jessica_report
|
inserted_jessica_report
|
||||||
|
@ -346,6 +351,10 @@ mod tests {
|
||||||
assert_eq!(read_jessica_report_view.post_creator.id, inserted_timmy.id);
|
assert_eq!(read_jessica_report_view.post_creator.id, inserted_timmy.id);
|
||||||
assert_eq!(read_jessica_report_view.my_vote, None);
|
assert_eq!(read_jessica_report_view.my_vote, None);
|
||||||
assert_eq!(read_jessica_report_view.resolver, None);
|
assert_eq!(read_jessica_report_view.resolver, None);
|
||||||
|
assert_eq!(agg_1.report_count, 1);
|
||||||
|
assert_eq!(agg_1.unresolved_report_count, 1);
|
||||||
|
assert_eq!(agg_2.report_count, 1);
|
||||||
|
assert_eq!(agg_2.unresolved_report_count, 1);
|
||||||
|
|
||||||
// Do a batch read of timmys reports
|
// Do a batch read of timmys reports
|
||||||
let reports = PostReportQuery::default().list(pool, &timmy_view).await?;
|
let reports = PostReportQuery::default().list(pool, &timmy_view).await?;
|
||||||
|
@ -379,6 +388,16 @@ mod tests {
|
||||||
Some(inserted_timmy.id)
|
Some(inserted_timmy.id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Make sure the unresolved_post report got decremented in the trigger
|
||||||
|
let agg_2 = PostAggregates::read(pool, inserted_post_2.id).await?;
|
||||||
|
assert_eq!(agg_2.report_count, 1);
|
||||||
|
assert_eq!(agg_2.unresolved_report_count, 0);
|
||||||
|
|
||||||
|
// Make sure the other unresolved report isn't changed
|
||||||
|
let agg_1 = PostAggregates::read(pool, inserted_post.id).await?;
|
||||||
|
assert_eq!(agg_1.report_count, 1);
|
||||||
|
assert_eq!(agg_1.unresolved_report_count, 1);
|
||||||
|
|
||||||
// Do a batch read of timmys reports
|
// Do a batch read of timmys reports
|
||||||
// It should only show saras, which is unresolved
|
// It should only show saras, which is unresolved
|
||||||
let reports_after_resolve = PostReportQuery {
|
let reports_after_resolve = PostReportQuery {
|
||||||
|
|
|
@ -62,9 +62,12 @@ use lemmy_db_schema::{
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use PostSortType::*;
|
use PostSortType::*;
|
||||||
|
|
||||||
|
type QueriesReadTypes<'a> = (PostId, Option<&'a LocalUser>, bool);
|
||||||
|
type QueriesListTypes<'a> = (PostQuery<'a>, &'a Site);
|
||||||
|
|
||||||
fn queries<'a>() -> Queries<
|
fn queries<'a>() -> Queries<
|
||||||
impl ReadFn<'a, PostView, (PostId, Option<&'a LocalUser>, bool)>,
|
impl ReadFn<'a, PostView, QueriesReadTypes<'a>>,
|
||||||
impl ListFn<'a, PostView, (PostQuery<'a>, &'a Site)>,
|
impl ListFn<'a, PostView, QueriesListTypes<'a>>,
|
||||||
> {
|
> {
|
||||||
let creator_is_admin = exists(
|
let creator_is_admin = exists(
|
||||||
local_user::table.filter(
|
local_user::table.filter(
|
||||||
|
@ -431,10 +434,10 @@ fn queries<'a>() -> Queries<
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PostView {
|
impl PostView {
|
||||||
pub async fn read<'a>(
|
pub async fn read(
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
post_id: PostId,
|
post_id: PostId,
|
||||||
my_local_user: Option<&'a LocalUser>,
|
my_local_user: Option<&'_ LocalUser>,
|
||||||
is_mod_or_admin: bool,
|
is_mod_or_admin: bool,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
queries()
|
queries()
|
||||||
|
@ -1735,6 +1738,8 @@ mod tests {
|
||||||
community_id: inserted_post.community_id,
|
community_id: inserted_post.community_id,
|
||||||
creator_id: inserted_post.creator_id,
|
creator_id: inserted_post.creator_id,
|
||||||
instance_id: data.inserted_instance.id,
|
instance_id: data.inserted_instance.id,
|
||||||
|
report_count: 0,
|
||||||
|
unresolved_report_count: 0,
|
||||||
},
|
},
|
||||||
subscribed: SubscribedType::NotSubscribed,
|
subscribed: SubscribedType::NotSubscribed,
|
||||||
read: false,
|
read: false,
|
||||||
|
|
|
@ -232,6 +232,25 @@ impl CommunityFollowerView {
|
||||||
.then_some(())
|
.then_some(())
|
||||||
.ok_or(diesel::NotFound)
|
.ok_or(diesel::NotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn is_follower(
|
||||||
|
community_id: CommunityId,
|
||||||
|
instance_id: InstanceId,
|
||||||
|
pool: &mut DbPool<'_>,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let conn = &mut get_conn(pool).await?;
|
||||||
|
select(exists(
|
||||||
|
action_query(community_actions::followed)
|
||||||
|
.inner_join(person::table.on(community_actions::person_id.eq(person::id)))
|
||||||
|
.filter(community_actions::community_id.eq(community_id))
|
||||||
|
.filter(person::instance_id.eq(instance_id))
|
||||||
|
.filter(community_actions::follow_state.eq(CommunityFollowerState::Accepted)),
|
||||||
|
))
|
||||||
|
.get_result::<bool>(conn)
|
||||||
|
.await?
|
||||||
|
.then_some(())
|
||||||
|
.ok_or(diesel::NotFound)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -34,9 +34,12 @@ use lemmy_db_schema::{
|
||||||
};
|
};
|
||||||
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
|
||||||
|
|
||||||
|
type QueriesReadTypes<'a> = (CommunityId, Option<&'a LocalUser>, bool);
|
||||||
|
type QueriesListTypes<'a> = (CommunityQuery<'a>, &'a Site);
|
||||||
|
|
||||||
fn queries<'a>() -> Queries<
|
fn queries<'a>() -> Queries<
|
||||||
impl ReadFn<'a, CommunityView, (CommunityId, Option<&'a LocalUser>, bool)>,
|
impl ReadFn<'a, CommunityView, QueriesReadTypes<'a>>,
|
||||||
impl ListFn<'a, CommunityView, (CommunityQuery<'a>, &'a Site)>,
|
impl ListFn<'a, CommunityView, QueriesListTypes<'a>>,
|
||||||
> {
|
> {
|
||||||
let all_joins = |query: community::BoxedQuery<'a, Pg>, my_local_user: Option<&'a LocalUser>| {
|
let all_joins = |query: community::BoxedQuery<'a, Pg>, my_local_user: Option<&'a LocalUser>| {
|
||||||
query
|
query
|
||||||
|
@ -166,10 +169,10 @@ fn queries<'a>() -> Queries<
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommunityView {
|
impl CommunityView {
|
||||||
pub async fn read<'a>(
|
pub async fn read(
|
||||||
pool: &mut DbPool<'_>,
|
pool: &mut DbPool<'_>,
|
||||||
community_id: CommunityId,
|
community_id: CommunityId,
|
||||||
my_local_user: Option<&'a LocalUser>,
|
my_local_user: Option<&'_ LocalUser>,
|
||||||
is_mod_or_admin: bool,
|
is_mod_or_admin: bool,
|
||||||
) -> Result<Self, Error> {
|
) -> Result<Self, Error> {
|
||||||
queries()
|
queries()
|
||||||
|
@ -253,7 +256,7 @@ pub struct CommunityQuery<'a> {
|
||||||
pub limit: Option<i64>,
|
pub limit: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> CommunityQuery<'a> {
|
impl CommunityQuery<'_> {
|
||||||
pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result<Vec<CommunityView>, Error> {
|
pub async fn list(self, site: &Site, pool: &mut DbPool<'_>) -> Result<Vec<CommunityView>, Error> {
|
||||||
queries().list(pool, (self, site)).await
|
queries().list(pool, (self, site)).await
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ pub(crate) struct SendRetryTask<'a> {
|
||||||
pub stop: CancellationToken,
|
pub stop: CancellationToken,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> SendRetryTask<'a> {
|
impl SendRetryTask<'_> {
|
||||||
// this function will return successfully when (a) send succeeded or (b) worker cancelled
|
// this function will return successfully when (a) send succeeded or (b) worker cancelled
|
||||||
// and will return an error if an internal error occurred (send errors cause an infinite loop)
|
// and will return an error if an internal error occurred (send errors cause an infinite loop)
|
||||||
pub async fn send_retry_loop(self) -> Result<()> {
|
pub async fn send_retry_loop(self) -> Result<()> {
|
||||||
|
|
|
@ -38,7 +38,8 @@ pub fn config(
|
||||||
)
|
)
|
||||||
// This has optional query params: /image/{filename}?format=jpg&thumbnail=256
|
// This has optional query params: /image/{filename}?format=jpg&thumbnail=256
|
||||||
.service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res)))
|
.service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res)))
|
||||||
.service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)));
|
.service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)))
|
||||||
|
.service(web::resource("/pictrs/healthz").route(web::get().to(healthz)));
|
||||||
}
|
}
|
||||||
|
|
||||||
trait ProcessUrl {
|
trait ProcessUrl {
|
||||||
|
@ -250,6 +251,25 @@ async fn delete(
|
||||||
Ok(HttpResponse::build(convert_status(res.status())).body(BodyStream::new(res.bytes_stream())))
|
Ok(HttpResponse::build(convert_status(res.status())).body(BodyStream::new(res.bytes_stream())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn healthz(
|
||||||
|
req: HttpRequest,
|
||||||
|
client: web::Data<ClientWithMiddleware>,
|
||||||
|
context: web::Data<LemmyContext>,
|
||||||
|
) -> LemmyResult<HttpResponse> {
|
||||||
|
let pictrs_config = context.settings().pictrs_config()?;
|
||||||
|
let url = format!("{}healthz", pictrs_config.url);
|
||||||
|
|
||||||
|
let mut client_req = adapt_request(&req, &client, url);
|
||||||
|
|
||||||
|
if let Some(addr) = req.head().peer_addr {
|
||||||
|
client_req = client_req.header("X-Forwarded-For", addr.to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let res = client_req.send().await?;
|
||||||
|
|
||||||
|
Ok(HttpResponse::build(convert_status(res.status())).body(BodyStream::new(res.bytes_stream())))
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn image_proxy(
|
pub async fn image_proxy(
|
||||||
Query(params): Query<ImageProxyParams>,
|
Query(params): Query<ImageProxyParams>,
|
||||||
req: HttpRequest,
|
req: HttpRequest,
|
||||||
|
|
|
@ -23,30 +23,31 @@ workspace = true
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
full = [
|
full = [
|
||||||
"dep:ts-rs",
|
"ts-rs",
|
||||||
"dep:diesel",
|
"diesel",
|
||||||
"dep:rosetta-i18n",
|
"rosetta-i18n",
|
||||||
"dep:actix-web",
|
"actix-web",
|
||||||
"dep:reqwest-middleware",
|
"reqwest-middleware",
|
||||||
"dep:tracing",
|
"tracing",
|
||||||
"dep:actix-web",
|
"actix-web",
|
||||||
"dep:serde_json",
|
"serde_json",
|
||||||
"dep:anyhow",
|
"anyhow",
|
||||||
"dep:http",
|
"http",
|
||||||
"dep:deser-hjson",
|
"deser-hjson",
|
||||||
"dep:regex",
|
"regex",
|
||||||
"dep:urlencoding",
|
"urlencoding",
|
||||||
"dep:doku",
|
"doku",
|
||||||
"dep:url",
|
"url",
|
||||||
"dep:smart-default",
|
"smart-default",
|
||||||
"dep:enum-map",
|
"enum-map",
|
||||||
"dep:futures",
|
"futures",
|
||||||
"dep:tokio",
|
"tokio",
|
||||||
"dep:html2text",
|
"html2text",
|
||||||
"dep:lettre",
|
"lettre",
|
||||||
"dep:uuid",
|
"uuid",
|
||||||
"dep:itertools",
|
"itertools",
|
||||||
"dep:markdown-it",
|
"markdown-it",
|
||||||
|
"moka",
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata.cargo-shear]
|
[package.metadata.cargo-shear]
|
||||||
|
@ -89,6 +90,7 @@ markdown-it-block-spoiler = "1.0.0"
|
||||||
markdown-it-sub = "1.0.0"
|
markdown-it-sub = "1.0.0"
|
||||||
markdown-it-sup = "1.0.0"
|
markdown-it-sup = "1.0.0"
|
||||||
markdown-it-ruby = "1.0.0"
|
markdown-it-ruby = "1.0.0"
|
||||||
|
moka = { workspace = true, optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
|
|
|
@ -42,7 +42,10 @@ macro_rules! location_info {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "full")]
|
cfg_if! {
|
||||||
|
if #[cfg(feature = "full")] {
|
||||||
|
use moka::future::Cache;use std::fmt::Debug;use std::hash::Hash;
|
||||||
|
|
||||||
/// tokio::spawn, but accepts a future that may fail and also
|
/// tokio::spawn, but accepts a future that may fail and also
|
||||||
/// * logs errors
|
/// * logs errors
|
||||||
/// * attaches the spawned task to the tracing span of the caller for better logging
|
/// * attaches the spawned task to the tracing span of the caller for better logging
|
||||||
|
@ -60,3 +63,20 @@ pub fn spawn_try_task(
|
||||||
* spawn was called */
|
* spawn was called */
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn build_cache<K, V>() -> Cache<K, V>
|
||||||
|
where
|
||||||
|
K: Debug + Eq + Hash + Send + Sync + 'static,
|
||||||
|
V: Debug + Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
Cache::<K, V>::builder()
|
||||||
|
.max_capacity(1)
|
||||||
|
.time_to_live(CACHE_DURATION_API)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "full")]
|
||||||
|
pub type CacheLock<T> = std::sync::LazyLock<Cache<(), T>>;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
8
migrations/2024-11-21-195004_add_report_count/down.sql
Normal file
8
migrations/2024-11-21-195004_add_report_count/down.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
ALTER TABLE post_aggregates
|
||||||
|
DROP COLUMN report_count,
|
||||||
|
DROP COLUMN unresolved_report_count;
|
||||||
|
|
||||||
|
ALTER TABLE comment_aggregates
|
||||||
|
DROP COLUMN report_count,
|
||||||
|
DROP COLUMN unresolved_report_count;
|
||||||
|
|
79
migrations/2024-11-21-195004_add_report_count/up.sql
Normal file
79
migrations/2024-11-21-195004_add_report_count/up.sql
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
-- Adding report_count and unresolved_report_count
|
||||||
|
-- to the post and comment aggregate tables
|
||||||
|
ALTER TABLE post_aggregates
|
||||||
|
ADD COLUMN report_count smallint NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN unresolved_report_count smallint NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE comment_aggregates
|
||||||
|
ADD COLUMN report_count smallint NOT NULL DEFAULT 0,
|
||||||
|
ADD COLUMN unresolved_report_count smallint NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
-- Update the historical counts
|
||||||
|
-- Posts
|
||||||
|
UPDATE
|
||||||
|
post_aggregates AS a
|
||||||
|
SET
|
||||||
|
report_count = cnt.count
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
post_id,
|
||||||
|
count(*) AS count
|
||||||
|
FROM
|
||||||
|
post_report
|
||||||
|
GROUP BY
|
||||||
|
post_id) cnt
|
||||||
|
WHERE
|
||||||
|
a.post_id = cnt.post_id;
|
||||||
|
|
||||||
|
-- The unresolved
|
||||||
|
UPDATE
|
||||||
|
post_aggregates AS a
|
||||||
|
SET
|
||||||
|
unresolved_report_count = cnt.count
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
post_id,
|
||||||
|
count(*) AS count
|
||||||
|
FROM
|
||||||
|
post_report
|
||||||
|
WHERE
|
||||||
|
resolved = 'f'
|
||||||
|
GROUP BY
|
||||||
|
post_id) cnt
|
||||||
|
WHERE
|
||||||
|
a.post_id = cnt.post_id;
|
||||||
|
|
||||||
|
-- Comments
|
||||||
|
UPDATE
|
||||||
|
comment_aggregates AS a
|
||||||
|
SET
|
||||||
|
report_count = cnt.count
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
comment_id,
|
||||||
|
count(*) AS count
|
||||||
|
FROM
|
||||||
|
comment_report
|
||||||
|
GROUP BY
|
||||||
|
comment_id) cnt
|
||||||
|
WHERE
|
||||||
|
a.comment_id = cnt.comment_id;
|
||||||
|
|
||||||
|
-- The unresolved
|
||||||
|
UPDATE
|
||||||
|
comment_aggregates AS a
|
||||||
|
SET
|
||||||
|
unresolved_report_count = cnt.count
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
comment_id,
|
||||||
|
count(*) AS count
|
||||||
|
FROM
|
||||||
|
comment_report
|
||||||
|
WHERE
|
||||||
|
resolved = 'f'
|
||||||
|
GROUP BY
|
||||||
|
comment_id) cnt
|
||||||
|
WHERE
|
||||||
|
a.comment_id = cnt.comment_id;
|
||||||
|
|
Loading…
Reference in a new issue