Merge branch 'main' into image-api-rework

This commit is contained in:
Felix Ableitner 2025-01-13 12:32:14 +01:00
commit 4b940431b2
88 changed files with 640 additions and 281 deletions

View file

@ -95,6 +95,15 @@ steps:
when:
- 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
# `DROP OWNED` doesn't work for default user
create_database_user:
image: postgres:16-alpine
@ -107,6 +116,68 @@ steps:
- 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
check_ts_bindings:
image: *rust_image
environment:
CARGO_HOME: .cargo_home
commands:
- ./scripts/ts_bindings_check.sh
when:
- event: pull_request
# make sure api builds with default features (used by other crates relying on lemmy api)
check_api_common_default_features:
image: *rust_image
environment:
CARGO_HOME: .cargo_home
commands:
- cargo check --package lemmy_api_common
when: *slow_check_paths
lemmy_api_common_doesnt_depend_on_diesel:
image: *rust_image
environment:
CARGO_HOME: .cargo_home
commands:
- "! cargo tree -p lemmy_api_common --no-default-features -i diesel"
when: *slow_check_paths
lemmy_api_common_works_with_wasm:
image: *rust_image
environment:
CARGO_HOME: .cargo_home
commands:
- "rustup target add wasm32-unknown-unknown"
- "cargo check --target wasm32-unknown-unknown -p lemmy_api_common"
when: *slow_check_paths
check_defaults_hjson_updated:
image: *rust_image
environment:
CARGO_HOME: .cargo_home
commands:
- ./scripts/update_config_defaults.sh config/defaults_current.hjson
- diff config/defaults.hjson config/defaults_current.hjson
when: *slow_check_paths
cargo_build:
image: *rust_image
environment:
@ -116,6 +187,32 @@ steps:
- mv target/debug/lemmy_server target/lemmy_server
when: *slow_check_paths
check_diesel_schema:
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
commands:
- cp crates/db_schema/src/schema.rs tmp.schema
- target/lemmy_server migration --all run
- <<: *install_diesel_cli
- diesel print-schema
- diff tmp.schema crates/db_schema/src/schema.rs
when: *slow_check_paths
check_db_perf_tool:
image: *rust_image
environment:
LEMMY_DATABASE_URL: postgres://lemmy:password@database:5432/lemmy
RUST_BACKTRACE: "1"
CARGO_HOME: .cargo_home
commands:
# same as scripts/db_perf.sh but without creating a new database server
- cargo run --package lemmy_db_perf -- --posts 10 --read-post-pages 1
when: *slow_check_paths
run_federation_tests:
image: node:22-bookworm-slim
environment:
@ -127,7 +224,7 @@ steps:
- bash api_tests/prepare-drone-federation-test.sh
- cd api_tests/
- pnpm i
- pnpm api-test-image
- pnpm api-test
when: *slow_check_paths
federation_tests_server_output:

View file

@ -11,7 +11,7 @@ killall -s1 lemmy_server || true
popd
pnpm i
pnpm api-test-image || true
pnpm api-test || true
killall -s1 lemmy_server || true
killall -s1 pict-rs || true

View file

@ -104,7 +104,7 @@ test("Upload image and delete it", async () => {
expect(deletedListAllMediaRes.images.length).toBe(previousThumbnails - 1);
});
test.only("Purge user, uploaded image removed", async () => {
test("Purge user, uploaded image removed", async () => {
let user = await registerUser(alphaImage, alphaUrl);
// upload test image
@ -128,7 +128,6 @@ test.only("Purge user, uploaded image removed", async () => {
};
const delete_ = await alphaImage.purgePerson(purgeForm);
expect(delete_.success).toBe(true);
console.log(upload.image_url + " should be purged");
// ensure that image is deleted
const response2 = await fetch(upload.image_url ?? "");

View file

@ -5,7 +5,7 @@ import {
CommunityId,
CommunityVisibility,
CreatePrivateMessageReport,
DeleteImageParams,
DeleteImage,
EditCommunity,
GetCommunityPendingFollowsCountResponse,
GetReplies,
@ -714,6 +714,8 @@ export async function saveUserSettingsBio(
export async function saveUserSettingsFederated(
api: LemmyHttp,
): Promise<SuccessResponse> {
let avatar = sampleImage;
let banner = sampleImage;
let bio = "a changed bio";
let form: SaveUserSettings = {
show_nsfw: false,
@ -721,6 +723,8 @@ export async function saveUserSettingsFederated(
default_post_sort_type: "Hot",
default_listing_type: "All",
interface_language: "",
avatar,
banner,
display_name: "user321",
show_avatars: false,
send_notifications_to_email: false,
@ -932,19 +936,17 @@ export async function deleteAllImages(api: LemmyHttp) {
const imagesRes = await api.listAllMedia({
limit: imageFetchLimit,
});
const forms = imagesRes.images.map(image => {
const form: DeleteImageParams = {
token: image.local_image.pictrs_delete_token,
filename: image.local_image.pictrs_alias,
};
return form;
});
for (const form of forms) {
console.log(
"delete image: token=" + form.token + ", name=" + form.filename,
);
await api.deleteImage(form);
}
Promise.all(
imagesRes.images
.map(image => {
const form: DeleteImage = {
token: image.local_image.pictrs_delete_token,
filename: image.local_image.pictrs_alias,
};
return form;
})
.map(form => api.deleteImage(form)),
);
}
export async function unfollows() {

View file

@ -20,11 +20,6 @@
url: "http://localhost:8080/"
# Set a custom pictrs API key. ( Required for deleting images )
api_key: "string"
# Backwards compatibility with 0.18.1. False is equivalent to `image_mode: None`, true is
# equivalent to `image_mode: StoreLinkPreviews`.
#
# To be removed in 0.20
cache_external_link_previews: true
# Specifies how to handle remote images, so that users don't have to connect directly to remote
# servers.
image_mode:
@ -38,7 +33,7 @@
# ensures that they can be reliably retrieved and can be resized using pict-rs APIs. However
# it also increases storage usage.
#
# This is the default behaviour, and also matches Lemmy 0.18.
# This behaviour matches Lemmy 0.18.
"StoreLinkPreviews"
# or

View file

@ -385,7 +385,7 @@ pub async fn delete_image_from_pictrs(
async fn generate_pictrs_thumbnail(image_url: &Url, context: &LemmyContext) -> LemmyResult<Url> {
let pictrs_config = context.settings().pictrs()?;
match pictrs_config.image_mode() {
match pictrs_config.image_mode {
PictrsImageMode::None => return Ok(image_url.clone()),
PictrsImageMode::ProxyAllImages => {
return Ok(proxy_image_link(image_url.clone(), context).await?.into())

View file

@ -1060,7 +1060,7 @@ pub async fn process_markdown(
markdown_check_for_blocked_urls(&text, url_blocklist)?;
if context.settings().pictrs()?.image_mode() == PictrsImageMode::ProxyAllImages {
if context.settings().pictrs()?.image_mode == PictrsImageMode::ProxyAllImages {
let (text, links) = markdown_rewrite_image_links(text);
RemoteImage::create(&mut context.pool(), links.clone()).await?;
@ -1128,7 +1128,7 @@ async fn proxy_image_link_internal(
/// Rewrite a link to go through `/api/v4/image_proxy` endpoint. This is only for remote urls and
/// if image_proxy setting is enabled.
pub async fn proxy_image_link(link: Url, context: &LemmyContext) -> LemmyResult<DbUrl> {
proxy_image_link_internal(link, context.settings().pictrs()?.image_mode(), context).await
proxy_image_link_internal(link, context.settings().pictrs()?.image_mode, context).await
}
pub async fn proxy_image_link_opt_apub(

View file

@ -3,7 +3,6 @@
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "http://ds9.lemmy.ml/u/lemmy_alpha",
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"target": "http://enterprise.lemmy.ml/c/main",
"type": "Block",
"removeData": true,

View file

@ -6,7 +6,6 @@
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "http://ds9.lemmy.ml/u/lemmy_alpha",
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"target": "http://enterprise.lemmy.ml/c/main",
"type": "Block",
"removeData": true,
@ -15,7 +14,6 @@
"id": "http://enterprise.lemmy.ml/activities/block/726f43ab-bd0e-4ab3-89c8-627e976f553c"
},
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"type": "Undo",
"id": "http://enterprise.lemmy.ml/activities/undo/06a20ffb-3e32-42fb-8f4c-674b36d7c557"
}

View file

@ -5,6 +5,5 @@
"type": "Add",
"actor": "https://ds9.lemmy.ml/u/lemmy_alpha",
"object": "https://ds9.lemmy.ml/post/2",
"target": "https://ds9.lemmy.ml/c/main/featured",
"audience": "https://ds9.lemmy.ml/c/main"
"target": "https://ds9.lemmy.ml/c/main/featured"
}

View file

@ -4,7 +4,6 @@
"object": "http://ds9.lemmy.ml/u/lemmy_alpha",
"target": "http://enterprise.lemmy.ml/c/main/moderators",
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"type": "Add",
"id": "http://enterprise.lemmy.ml/activities/add/ec069147-77c3-447f-88c8-0ef1df10403f"
}

View file

@ -4,6 +4,5 @@
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "http://lemmy-alpha:8541/post/2",
"cc": ["http://lemmy-alpha:8541/c/main"],
"type": "Lock",
"audience": "http://lemmy-alpha:8541/c/main"
"type": "Lock"
}

View file

@ -5,6 +5,5 @@
"type": "Remove",
"actor": "https://ds9.lemmy.ml/u/lemmy_alpha",
"object": "https://ds9.lemmy.ml/post/2",
"target": "https://ds9.lemmy.ml/c/main/featured",
"audience": "https://ds9.lemmy.ml/c/main"
"target": "https://ds9.lemmy.ml/c/main/featured"
}

View file

@ -5,6 +5,5 @@
"cc": ["http://enterprise.lemmy.ml/c/main"],
"type": "Remove",
"target": "http://enterprise.lemmy.ml/c/main/moderators",
"audience": "http://enterprise.lemmy.ml/u/main",
"id": "http://enterprise.lemmy.ml/activities/remove/aab114f8-cfbd-4935-a5b7-e1a64603650d"
}

View file

@ -1,7 +1,6 @@
{
"actor": "http://ds9.lemmy.ml/u/lemmy_alpha",
"to": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"object": "http://enterprise.lemmy.ml/post/7",
"summary": "report this post",
"type": "Flag",

View file

@ -8,10 +8,8 @@
"object": "http://lemmy-alpha:8541/post/2",
"cc": ["http://lemmy-alpha:8541/c/main"],
"type": "Lock",
"id": "http://lemmy-alpha:8541/activities/lock/08b6fd3e-9ef3-4358-a987-8bb641f3e2c3",
"audience": "http://lemmy-alpha:8541/c/main"
"id": "http://lemmy-alpha:8541/activities/lock/08b6fd3e-9ef3-4358-a987-8bb641f3e2c3"
},
"cc": ["http://lemmy-alpha:8541/c/main"],
"type": "Undo",
"audience": "http://lemmy-alpha:8541/c/main"
"type": "Undo"
}

View file

@ -39,7 +39,6 @@
"updated": "2021-11-01T12:23:50.151874Z"
},
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"type": "Update",
"id": "http://ds9.lemmy.ml/activities/update/d3717cf5-096d-473f-9530-5d52f9d51f5f"
}

View file

@ -10,7 +10,6 @@
"http://enterprise.lemmy.ml/c/main",
"http://ds9.lemmy.ml/u/lemmy_alpha"
],
"audience": "http://ds9.lemmy.ml/u/lemmy_alpha",
"content": "hello",
"mediaType": "text/html",
"source": {
@ -24,7 +23,6 @@
"http://enterprise.lemmy.ml/c/main",
"http://ds9.lemmy.ml/u/lemmy_alpha"
],
"audience": "http://ds9.lemmy.ml/u/lemmy_alpha",
"tag": [
{
"href": "http://ds9.lemmy.ml/u/lemmy_alpha",

View file

@ -9,7 +9,6 @@
"http://enterprise.lemmy.ml/c/main",
"https://www.w3.org/ns/activitystreams#Public"
],
"audience": "https://enterprise.lemmy.ml/c/main",
"name": "test post",
"content": "<p>test body</p>\n",
"mediaType": "text/html",
@ -31,7 +30,6 @@
"published": "2021-10-29T15:10:51.557399Z"
},
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "https://enterprise.lemmy.ml/c/main",
"type": "Create",
"id": "http://ds9.lemmy.ml/activities/create/eee6a57a-622f-464d-b560-73ae1fcd3ddf"
}

View file

@ -9,7 +9,6 @@
"http://enterprise.lemmy.ml/c/main",
"https://www.w3.org/ns/activitystreams#Public"
],
"audience": "https://enterprise.lemmy.ml/c/main",
"name": "test post 1",
"content": "<p>test body</p>\n",
"mediaType": "text/html",
@ -28,7 +27,6 @@
"updated": "2021-10-29T15:11:35.976374Z"
},
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "https://enterprise.lemmy.ml/c/main",
"type": "Update",
"id": "http://ds9.lemmy.ml/activities/update/ab360117-e165-4de4-b7fc-906b62c98631"
}

View file

@ -3,7 +3,6 @@
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "http://ds9.lemmy.ml/post/1",
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"type": "Delete",
"id": "http://ds9.lemmy.ml/activities/delete/f2abee48-c7bb-41d5-9e27-8775ff32db12"
}

View file

@ -3,7 +3,6 @@
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "http://ds9.lemmy.ml/comment/1",
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"type": "Delete",
"summary": "bad comment",
"id": "http://enterprise.lemmy.ml/activities/delete/42ca1a79-f99e-4518-a2ca-ba2df221eb5e"

View file

@ -6,12 +6,10 @@
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "http://ds9.lemmy.ml/post/1",
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"type": "Delete",
"id": "http://ds9.lemmy.ml/activities/delete/b13cca96-7737-41e1-9769-8fbf972b3509"
},
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"type": "Undo",
"id": "http://ds9.lemmy.ml/activities/undo/5e939cfb-b8a1-4de8-950f-9d684e9162b9"
}

View file

@ -6,13 +6,11 @@
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"object": "http://ds9.lemmy.ml/comment/1",
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"type": "Delete",
"summary": "bad comment",
"id": "http://enterprise.lemmy.ml/activities/delete/2598435c-87a3-49cd-81f3-a44b03b7af9d"
},
"cc": ["http://enterprise.lemmy.ml/c/main"],
"audience": "http://enterprise.lemmy.ml/u/main",
"type": "Undo",
"id": "http://enterprise.lemmy.ml/activities/undo/a850cf21-3866-4b3a-b80b-56aa00997fee"
}

View file

@ -1,7 +1,6 @@
{
"actor": "http://enterprise.lemmy.ml/u/lemmy_beta",
"object": "http://ds9.lemmy.ml/post/1",
"audience": "https://enterprise.lemmy.ml/c/tenforward",
"type": "Dislike",
"id": "http://enterprise.lemmy.ml/activities/dislike/64d40d40-a829-43a5-8247-1fb595b3ca1c"
}

View file

@ -1,7 +1,6 @@
{
"actor": "http://ds9.lemmy.ml/u/lemmy_alpha",
"object": "http://ds9.lemmy.ml/comment/1",
"audience": "https://enterprise.lemmy.ml/c/tenforward",
"type": "Like",
"id": "http://ds9.lemmy.ml/activities/like/fd61d070-7382-46a9-b2b7-6bb253732877"
}

View file

@ -3,11 +3,9 @@
"object": {
"actor": "http://enterprise.lemmy.ml/u/lemmy_beta",
"object": "http://ds9.lemmy.ml/post/1",
"audience": "https://enterprise.lemmy.ml/c/tenforward",
"type": "Like",
"id": "http://enterprise.lemmy.ml/activities/like/2227ab2c-79e2-4fca-a1d2-1d67dacf2457"
},
"audience": "https://enterprise.lemmy.ml/c/tenforward",
"type": "Undo",
"id": "http://enterprise.lemmy.ml/activities/undo/6cc6fb71-39fe-49ea-9506-f0423b101e98"
}

View file

@ -3,11 +3,9 @@
"object": {
"actor": "http://ds9.lemmy.ml/u/lemmy_alpha",
"object": "http://ds9.lemmy.ml/comment/1",
"audience": "https://enterprise.lemmy.ml/c/tenforward",
"type": "Like",
"id": "http://ds9.lemmy.ml/activities/like/efcf7ae2-dfcc-4ff4-9ce4-6adf251ff004"
},
"audience": "https://enterprise.lemmy.ml/c/tenforward",
"type": "Undo",
"id": "http://ds9.lemmy.ml/activities/undo/3518565c-24a7-4d9e-8e0a-f7a2f45ac618"
}

View file

@ -20,8 +20,7 @@
"language": {
"identifier": "de",
"name": "Deutsch"
},
"audience": "https://ds9.lemmy.ml/c/main"
}
},
{
"type": "Page",
@ -40,8 +39,7 @@
"language": {
"identifier": "de",
"name": "Deutsch"
},
"audience": "https://ds9.lemmy.ml/c/main"
}
}
]
}

View file

@ -2,12 +2,11 @@
"id": "https://enterprise.lemmy.ml/comment/38741",
"type": "Note",
"attributedTo": "https://enterprise.lemmy.ml/u/picard",
"to": ["https://www.w3.org/ns/activitystreams#Public"],
"cc": [
"to": [
"https://enterprise.lemmy.ml/c/tenforward",
"https://enterprise.lemmy.ml/u/picard"
"https://www.w3.org/ns/activitystreams#Public"
],
"audience": "https://enterprise.lemmy.ml/c/tenforward",
"cc": ["https://enterprise.lemmy.ml/u/picard"],
"inReplyTo": "https://enterprise.lemmy.ml/post/55143",
"content": "<p>first comment!</p>\n",
"mediaType": "text/html",

View file

@ -6,7 +6,6 @@
"https://enterprise.lemmy.ml/c/tenforward",
"https://www.w3.org/ns/activitystreams#Public"
],
"audience": "https://enterprise.lemmy.ml/c/tenforward",
"name": "Post title",
"content": "<p>This is a post in the /c/tenforward community</p>\n",
"mediaType": "text/html",

View file

@ -1,4 +1,4 @@
use super::to_and_audience;
use super::to;
use crate::{
activities::{
block::{generate_cc, SiteOrCommunity},
@ -54,7 +54,7 @@ impl BlockUser {
expires: Option<DateTime<Utc>>,
context: &Data<LemmyContext>,
) -> LemmyResult<BlockUser> {
let (to, audience) = to_and_audience(target)?;
let to = to(target)?;
Ok(BlockUser {
actor: mod_.id().into(),
to,
@ -68,7 +68,6 @@ impl BlockUser {
BlockType::Block,
&context.settings().get_protocol_and_hostname(),
)?,
audience,
end_time: expires,
})
}

View file

@ -208,12 +208,10 @@ pub(crate) async fn send_ban_from_community(
}
}
fn to_and_audience(
target: &SiteOrCommunity,
) -> LemmyResult<(Vec<Url>, Option<ObjectId<ApubCommunity>>)> {
fn to(target: &SiteOrCommunity) -> LemmyResult<Vec<Url>> {
Ok(if let SiteOrCommunity::Community(c) = target {
(vec![generate_to(c)?], Some(c.id().into()))
generate_to(c)?
} else {
(vec![public()], None)
vec![public()]
})
}

View file

@ -1,4 +1,4 @@
use super::to_and_audience;
use super::to;
use crate::{
activities::{
block::{generate_cc, SiteOrCommunity},
@ -46,7 +46,7 @@ impl UndoBlockUser {
context: &Data<LemmyContext>,
) -> LemmyResult<()> {
let block = BlockUser::new(target, user, mod_, None, reason, None, context).await?;
let (to, audience) = to_and_audience(target)?;
let to = to(target)?;
let id = generate_activity_id(
UndoType::Undo,
@ -59,7 +59,6 @@ impl UndoBlockUser {
cc: generate_cc(target, &mut context.pool()).await?,
kind: UndoType::Undo,
id: id.clone(),
audience,
restore_data: Some(restore_data),
};

View file

@ -94,7 +94,7 @@ impl AnnounceActivity {
generate_announce_activity_id(inner_kind, &context.settings().get_protocol_and_hostname())?;
Ok(AnnounceActivity {
actor: community.id().into(),
to: vec![generate_to(community)?],
to: generate_to(community)?,
object: IdOrNestedObject::NestedObject(object),
cc: community
.followers_url

View file

@ -54,13 +54,12 @@ impl CollectionAdd {
)?;
let add = CollectionAdd {
actor: actor.id().into(),
to: vec![generate_to(community)?],
to: generate_to(community)?,
object: added_mod.id(),
target: generate_moderators_url(&community.actor_id)?.into(),
cc: vec![community.id()],
kind: AddType::Add,
id: id.clone(),
audience: Some(community.id().into()),
};
let activity = AnnouncableActivities::CollectionAdd(add);
@ -80,13 +79,12 @@ impl CollectionAdd {
)?;
let add = CollectionAdd {
actor: actor.id().into(),
to: vec![generate_to(community)?],
to: generate_to(community)?,
object: featured_post.ap_id.clone().into(),
target: generate_featured_url(&community.actor_id)?.into(),
cc: vec![community.id()],
kind: AddType::Add,
id: id.clone(),
audience: Some(community.id().into()),
};
let activity = AnnouncableActivities::CollectionAdd(add);
send_activity_in_community(

View file

@ -49,13 +49,12 @@ impl CollectionRemove {
)?;
let remove = CollectionRemove {
actor: actor.id().into(),
to: vec![generate_to(community)?],
to: generate_to(community)?,
object: removed_mod.id(),
target: generate_moderators_url(&community.actor_id)?.into(),
id: id.clone(),
cc: vec![community.id()],
kind: RemoveType::Remove,
audience: Some(community.id().into()),
};
let activity = AnnouncableActivities::CollectionRemove(remove);
@ -75,13 +74,12 @@ impl CollectionRemove {
)?;
let remove = CollectionRemove {
actor: actor.id().into(),
to: vec![generate_to(community)?],
to: generate_to(community)?,
object: featured_post.ap_id.clone().into(),
target: generate_featured_url(&community.actor_id)?.into(),
cc: vec![community.id()],
kind: RemoveType::Remove,
id: id.clone(),
audience: Some(community.id().into()),
};
let activity = AnnouncableActivities::CollectionRemove(remove);
send_activity_in_community(

View file

@ -138,12 +138,11 @@ pub(crate) async fn send_lock_post(
let community_id = community.actor_id.inner().clone();
let lock = LockPage {
actor: actor.actor_id.clone().into(),
to: vec![generate_to(&community)?],
to: generate_to(&community)?,
object: ObjectId::from(post.ap_id),
cc: vec![community_id.clone()],
kind: LockType::Lock,
id,
audience: Some(community_id.into()),
};
let activity = if locked {
AnnouncableActivities::LockPost(lock)
@ -154,11 +153,10 @@ pub(crate) async fn send_lock_post(
)?;
let undo = UndoLockPage {
actor: lock.actor.clone(),
to: vec![generate_to(&community)?],
to: generate_to(&community)?,
cc: lock.cc.clone(),
kind: UndoType::Undo,
id,
audience: lock.audience.clone(),
object: lock,
};
AnnouncableActivities::UndoLockPost(undo)

View file

@ -56,7 +56,6 @@ impl Report {
content: None,
kind,
id: id.clone(),
audience: Some(community.id().into()),
};
// send report to the community where object was posted

View file

@ -43,12 +43,11 @@ pub(crate) async fn send_update_community(
)?;
let update = UpdateCommunity {
actor: actor.id().into(),
to: vec![generate_to(&community)?],
to: generate_to(&community)?,
object: Box::new(community.clone().into_json(&context).await?),
cc: vec![community.id()],
kind: UpdateType::Update,
id: id.clone(),
audience: Some(community.id().into()),
};
let activity = AnnouncableActivities::UpdateCommunity(update);

View file

@ -71,13 +71,12 @@ impl CreateOrUpdateNote {
let create_or_update = CreateOrUpdateNote {
actor: person.id().into(),
to: vec![generate_to(&community)?],
to: generate_to(&community)?,
cc: note.cc.clone(),
tag: note.tag.clone(),
object: note,
kind,
id: id.clone(),
audience: Some(community.id().into()),
};
let tagged_users: Vec<ObjectId<ApubPerson>> = create_or_update

View file

@ -49,12 +49,11 @@ impl CreateOrUpdatePage {
)?;
Ok(CreateOrUpdatePage {
actor: actor.id().into(),
to: vec![generate_to(community)?],
to: generate_to(community)?,
object: post.into_json(context).await?,
cc: vec![community.id()],
kind,
id: id.clone(),
audience: Some(community.id().into()),
})
}

View file

@ -102,7 +102,6 @@ impl Delete {
kind: DeleteType::Delete,
summary,
id,
audience: community.map(|c| c.actor_id.clone().into()),
remove_data: None,
})
}

View file

@ -60,7 +60,7 @@ pub(crate) async fn send_apub_delete_in_community(
) -> LemmyResult<()> {
let actor = ApubPerson::from(actor);
let is_mod_action = reason.is_some();
let to = vec![generate_to(&community)?];
let to = generate_to(&community)?;
let activity = if deleted {
let delete = Delete::new(&actor, object, to, Some(&community), reason, context)?;
AnnouncableActivities::Delete(delete)

View file

@ -87,7 +87,6 @@ impl UndoDelete {
cc: cc.into_iter().collect(),
kind: UndoType::Undo,
id,
audience: community.map(|c| c.actor_id.clone().into()),
})
}

View file

@ -136,23 +136,15 @@ pub(crate) fn verify_visibility(to: &[Url], cc: &[Url], community: &Community) -
}
/// Marks object as public only if the community is public
pub(crate) fn generate_to(community: &Community) -> LemmyResult<Url> {
pub(crate) fn generate_to(community: &Community) -> LemmyResult<Vec<Url>> {
let actor_id = community.actor_id.clone().into();
if community.visibility == CommunityVisibility::Public {
Ok(public())
Ok(vec![actor_id, public()])
} else {
Ok(Url::parse(&format!("{}/followers", community.actor_id))?)
}
}
pub(crate) fn verify_community_matches<T>(a: &ObjectId<ApubCommunity>, b: T) -> LemmyResult<()>
where
T: Into<ObjectId<ApubCommunity>>,
{
let b: ObjectId<ApubCommunity> = b.into();
if a != &b {
Err(FederationError::InvalidCommunity)?
} else {
Ok(())
Ok(vec![
actor_id.clone(),
Url::parse(&format!("{}/followers", actor_id))?,
])
}
}

View file

@ -40,13 +40,13 @@ pub(crate) async fn send_like_activity(
let empty = ActivitySendTargets::empty();
// score of 1 means upvote, -1 downvote, 0 undo a previous vote
if score != 0 {
let vote = Vote::new(object_id, &actor, &community, score.try_into()?, &context)?;
let vote = Vote::new(object_id, &actor, score.try_into()?, &context)?;
let activity = AnnouncableActivities::Vote(vote);
send_activity_in_community(activity, &actor, &community, empty, false, &context).await
} else {
// Lemmy API doesn't distinguish between Undo/Like and Undo/Dislike, so we hardcode it here.
let vote = Vote::new(object_id, &actor, &community, VoteType::Like, &context)?;
let undo_vote = UndoVote::new(vote, &actor, &community, &context)?;
let vote = Vote::new(object_id, &actor, VoteType::Like, &context)?;
let undo_vote = UndoVote::new(vote, &actor, &context)?;
let activity = AnnouncableActivities::UndoVote(undo_vote);
send_activity_in_community(activity, &actor, &community, empty, false, &context).await
}

View file

@ -5,7 +5,7 @@ use crate::{
voting::{undo_vote_comment, undo_vote_post},
},
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson},
objects::person::ApubPerson,
protocol::{
activities::voting::{undo_vote::UndoVote, vote::Vote},
InCommunity,
@ -26,7 +26,6 @@ impl UndoVote {
pub(in crate::activities::voting) fn new(
vote: Vote,
actor: &ApubPerson,
community: &ApubCommunity,
context: &Data<LemmyContext>,
) -> LemmyResult<Self> {
Ok(UndoVote {
@ -37,7 +36,6 @@ impl UndoVote {
UndoType::Undo,
&context.settings().get_protocol_and_hostname(),
)?,
audience: Some(community.id().into()),
})
}
}

View file

@ -5,7 +5,7 @@ use crate::{
voting::{undo_vote_comment, undo_vote_post, vote_comment, vote_post},
},
insert_received_activity,
objects::{community::ApubCommunity, person::ApubPerson},
objects::person::ApubPerson,
protocol::{
activities::voting::vote::{Vote, VoteType},
InCommunity,
@ -26,7 +26,6 @@ impl Vote {
pub(in crate::activities::voting) fn new(
object_id: ObjectId<PostOrComment>,
actor: &ApubPerson,
community: &ApubCommunity,
kind: VoteType,
context: &Data<LemmyContext>,
) -> LemmyResult<Vote> {
@ -35,7 +34,6 @@ impl Vote {
object: object_id,
kind: kind.clone(),
id: generate_activity_id(kind, &context.settings().get_protocol_and_hostname())?,
audience: Some(community.id().into()),
})
}
}

View file

@ -110,7 +110,7 @@ pub(crate) async fn get_activity(
.into();
let activity = SentActivity::read_from_apub_id(&mut context.pool(), &activity_id)
.await
.with_lemmy_type(FederationError::CouldntFindActivity.into())?;
.with_lemmy_type(LemmyErrorType::NotFound)?;
let sensitive = activity.sensitive;
if sensitive {

View file

@ -1,7 +1,7 @@
use crate::objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson};
use crate::objects::{comment::ApubComment, person::ApubPerson};
use activitypub_federation::{
config::Data,
fetch::{object_id::ObjectId, webfinger::webfinger_resolve_actor},
fetch::webfinger::webfinger_resolve_actor,
kinds::link::MentionType,
traits::Actor,
};
@ -42,14 +42,13 @@ pub struct MentionsAndAddresses {
/// This takes a comment, and builds a list of to_addresses, inboxes,
/// and mention tags, so they know where to be sent to.
/// Addresses are the persons / addresses that go in the cc field.
#[tracing::instrument(skip(comment, community_id, context))]
#[tracing::instrument(skip(comment, context))]
pub async fn collect_non_local_mentions(
comment: &ApubComment,
community_id: ObjectId<ApubCommunity>,
context: &Data<LemmyContext>,
) -> LemmyResult<MentionsAndAddresses> {
let parent_creator = get_comment_parent_creator(&mut context.pool(), comment).await?;
let mut addressed_ccs: Vec<Url> = vec![community_id.into(), parent_creator.id()];
let mut addressed_ccs: Vec<Url> = vec![parent_creator.id()];
// Add the mention tag
let parent_creator_tag = Mention {

View file

@ -105,13 +105,13 @@ impl Object for ApubComment {
post.ap_id.into()
};
let language = Some(LanguageTag::new_single(self.language_id, &mut context.pool()).await?);
let maa = collect_non_local_mentions(&self, community.actor_id.clone().into(), context).await?;
let maa = collect_non_local_mentions(&self, context).await?;
let note = Note {
r#type: NoteType::Note,
id: self.ap_id.clone().into(),
attributed_to: creator.actor_id.into(),
to: vec![generate_to(&community)?],
to: generate_to(&community)?,
cc: maa.ccs,
content: markdown_to_html(&self.content),
media_type: Some(MediaTypeMarkdownOrHtml::Html),
@ -122,7 +122,6 @@ impl Object for ApubComment {
tag: maa.tags,
distinguished: Some(self.distinguished),
language,
audience: Some(community.actor_id.into()),
attachment: vec![],
};

View file

@ -133,7 +133,7 @@ impl Object for ApubPost {
kind: PageType::Page,
id: self.ap_id.clone().into(),
attributed_to: AttributedTo::Lemmy(creator.actor_id.into()),
to: vec![generate_to(&community)?],
to: generate_to(&community)?,
cc: vec![],
name: Some(self.name.clone()),
content: self.body.as_ref().map(|b| markdown_to_html(b)),
@ -145,7 +145,6 @@ impl Object for ApubPost {
language,
published: Some(self.published),
updated: self.updated,
audience: Some(community.actor_id.into()),
in_reply_to: None,
tag: vec![hashtag],
};

View file

@ -1,5 +1,5 @@
use crate::{
activities::{block::SiteOrCommunity, verify_community_matches},
activities::block::SiteOrCommunity,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
};
@ -31,7 +31,6 @@ pub struct BlockUser {
#[serde(rename = "type")]
pub(crate) kind: BlockType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
/// Quick and dirty solution.
/// TODO: send a separate Delete activity instead
@ -49,9 +48,6 @@ impl InCommunity for BlockUser {
SiteOrCommunity::Community(c) => c,
SiteOrCommunity::Site(_) => return Err(anyhow!("activity is not in community").into()),
};
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::block::block_user::BlockUser, InCommunity},
};
@ -28,7 +27,6 @@ pub struct UndoBlockUser {
#[serde(rename = "type")]
pub(crate) kind: UndoType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
/// Quick and dirty solution.
/// TODO: send a separate Delete activity instead
@ -39,9 +37,6 @@ pub struct UndoBlockUser {
impl InCommunity for UndoBlockUser {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let community = self.object.community(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
};
@ -28,7 +27,6 @@ pub struct CollectionAdd {
#[serde(rename = "type")]
pub(crate) kind: AddType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait]
@ -36,9 +34,6 @@ impl InCommunity for CollectionAdd {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let (community, _) =
Community::get_by_collection_url(&mut context.pool(), &self.clone().target.into()).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
};
@ -28,7 +27,6 @@ pub struct CollectionRemove {
pub(crate) kind: RemoveType,
pub(crate) target: Url,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait]
@ -36,9 +34,6 @@ impl InCommunity for CollectionRemove {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let (community, _) =
Community::get_by_collection_url(&mut context.pool(), &self.clone().target.into()).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::InCommunity,
};
@ -33,7 +32,6 @@ pub struct LockPage {
#[serde(rename = "type")]
pub(crate) kind: LockType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
@ -48,7 +46,6 @@ pub struct UndoLockPage {
#[serde(rename = "type")]
pub(crate) kind: UndoType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait]
@ -56,9 +53,6 @@ impl InCommunity for LockPage {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let post = self.object.dereference(context).await?;
let community = Community::read(&mut context.pool(), post.community_id).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}
@ -67,9 +61,6 @@ impl InCommunity for LockPage {
impl InCommunity for UndoLockPage {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let community = self.object.community(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
fetcher::post_or_comment::PostOrComment,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
@ -29,7 +28,6 @@ pub struct Report {
#[serde(rename = "type")]
pub(crate) kind: FlagType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
impl Report {
@ -73,9 +71,6 @@ impl ReportObject {
impl InCommunity for Report {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let community = self.to[0].dereference(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{objects::group::Group, InCommunity},
};
@ -29,16 +28,12 @@ pub struct UpdateCommunity {
#[serde(rename = "type")]
pub(crate) kind: UpdateType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait]
impl InCommunity for UpdateCommunity {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let community: ApubCommunity = self.object.id.clone().dereference(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
mentions::MentionOrValue,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::CreateOrUpdateType, objects::note::Note, InCommunity},
@ -29,7 +28,6 @@ pub struct CreateOrUpdateNote {
#[serde(rename = "type")]
pub(crate) kind: CreateOrUpdateType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait]
@ -37,9 +35,6 @@ impl InCommunity for CreateOrUpdateNote {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let post = self.object.get_parents(context).await?.0;
let community = Community::read(&mut context.pool(), post.community_id).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::CreateOrUpdateType, objects::page::Page, InCommunity},
};
@ -25,16 +24,12 @@ pub struct CreateOrUpdatePage {
#[serde(rename = "type")]
pub(crate) kind: CreateOrUpdateType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait]
impl InCommunity for CreateOrUpdatePage {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let community = self.object.community(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,5 +1,5 @@
use crate::{
activities::{deletion::DeletableObjects, verify_community_matches},
activities::deletion::DeletableObjects,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{objects::tombstone::Tombstone, IdOrNestedObject, InCommunity},
};
@ -31,7 +31,6 @@ pub struct Delete {
#[serde(rename = "type")]
pub(crate) kind: DeleteType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
#[serde(deserialize_with = "deserialize_one_or_many")]
#[serde(default)]
@ -61,9 +60,6 @@ impl InCommunity for Delete {
}
};
let community = Community::read(&mut context.pool(), community_id).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::deletion::delete::Delete, InCommunity},
};
@ -26,7 +25,6 @@ pub struct UndoDelete {
#[serde(rename = "type")]
pub(crate) kind: UndoType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
#[serde(deserialize_with = "deserialize_one_or_many", default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
@ -37,9 +35,6 @@ pub struct UndoDelete {
impl InCommunity for UndoDelete {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let community = self.object.community(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::{activities::voting::vote::Vote, InCommunity},
};
@ -17,16 +16,12 @@ pub struct UndoVote {
#[serde(rename = "type")]
pub(crate) kind: UndoType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[async_trait::async_trait]
impl InCommunity for UndoVote {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let community = self.object.community(context).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
fetcher::post_or_comment::PostOrComment,
objects::{community::ApubCommunity, person::ApubPerson},
protocol::InCommunity,
@ -19,7 +18,6 @@ pub struct Vote {
#[serde(rename = "type")]
pub(crate) kind: VoteType,
pub(crate) id: Url,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
}
#[derive(Clone, Debug, Display, Deserialize, Serialize, PartialEq, Eq)]
@ -58,9 +56,6 @@ impl InCommunity for Vote {
.await?
.community(context)
.await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -82,8 +82,6 @@ impl<Kind: Id + DeserializeOwned + Send> IdOrNestedObject<Kind> {
#[async_trait::async_trait]
pub trait InCommunity {
// TODO: after we use audience field and remove backwards compat, it should be possible to change
// this to simply `fn community(&self) -> LemmyResult<ObjectId<ApubCommunity>>`
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity>;
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
fetcher::post_or_comment::PostOrComment,
mentions::MentionOrValue,
objects::{comment::ApubComment, community::ApubCommunity, person::ApubPerson, post::ApubPost},
@ -56,7 +55,6 @@ pub struct Note {
// lemmy extension
pub(crate) distinguished: Option<bool>,
pub(crate) language: Option<LanguageTag>,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
#[serde(default)]
pub(crate) attachment: Vec<Attachment>,
}
@ -94,9 +92,6 @@ impl InCommunity for Note {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
let (post, _) = self.get_parents(context).await?;
let community = Community::read(&mut context.pool(), post.community_id).await?;
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community.into())
}
}

View file

@ -1,5 +1,4 @@
use crate::{
activities::verify_community_matches,
fetcher::user_or_community::{PersonOrGroupType, UserOrCommunity},
objects::{community::ApubCommunity, person::ApubPerson, post::ApubPost},
protocol::{objects::LanguageTag, ImageObject, InCommunity, Source},
@ -64,7 +63,6 @@ pub struct Page {
pub(crate) published: Option<DateTime<Utc>>,
pub(crate) updated: Option<DateTime<Utc>>,
pub(crate) language: Option<LanguageTag>,
pub(crate) audience: Option<ObjectId<ApubCommunity>>,
#[serde(deserialize_with = "deserialize_skip_error", default)]
pub(crate) tag: Vec<Hashtag>,
}
@ -231,10 +229,6 @@ impl ActivityHandler for Page {
#[async_trait::async_trait]
impl InCommunity for Page {
async fn community(&self, context: &Data<LemmyContext>) -> LemmyResult<ApubCommunity> {
if let Some(audience) = &self.audience {
return audience.dereference(context).await;
}
let community = match &self.attributed_to {
AttributedTo::Lemmy(_) => {
let mut iter = self.to.iter().merge(self.cc.iter());
@ -259,9 +253,6 @@ impl InCommunity for Page {
}
};
if let Some(audience) = &self.audience {
verify_community_matches(audience, community.actor_id.clone())?;
}
Ok(community)
}
}

View file

@ -422,6 +422,25 @@ END;
$$);
CALL r.create_triggers ('community_report', $$
BEGIN
UPDATE
community_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
(community_report).community_id, coalesce(sum(count_diff), 0) AS report_count, coalesce(sum(count_diff) FILTER (WHERE NOT (community_report).resolved), 0) AS unresolved_report_count
FROM select_old_and_new_rows AS old_and_new_rows GROUP BY (community_report).community_id) AS diff
WHERE (diff.report_count, diff.unresolved_report_count) != (0, 0)
AND a.community_id = diff.community_id;
RETURN NULL;
END;
$$);
-- 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.
CREATE FUNCTION r.comment_aggregates_from_comment ()
@ -685,6 +704,8 @@ CALL r.create_report_combined_trigger ('comment_report');
CALL r.create_report_combined_trigger ('private_message_report');
CALL r.create_report_combined_trigger ('community_report');
-- person_content (comment, post)
CREATE PROCEDURE r.create_person_content_combined_trigger (table_name text)
LANGUAGE plpgsql

View file

@ -73,6 +73,8 @@ pub struct CommunityAggregates {
#[serde(skip)]
pub hot_rank: f64,
pub subscribers_local: i64,
pub report_count: i16,
pub unresolved_report_count: i16,
}
#[derive(PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Default)]

View file

@ -0,0 +1,97 @@
use crate::{
newtypes::{CommunityId, CommunityReportId, PersonId},
schema::community_report::{
community_id,
dsl::{community_report, resolved, resolver_id, updated},
},
source::community_report::{CommunityReport, CommunityReportForm},
traits::Reportable,
utils::{get_conn, DbPool},
};
use chrono::Utc;
use diesel::{
dsl::{insert_into, update},
result::Error,
ExpressionMethods,
QueryDsl,
};
use diesel_async::RunQueryDsl;
#[async_trait]
impl Reportable for CommunityReport {
type Form = CommunityReportForm;
type IdType = CommunityReportId;
type ObjectIdType = CommunityId;
/// creates a community report and returns it
///
/// * `conn` - the postgres connection
/// * `community_report_form` - the filled CommunityReportForm to insert
async fn report(
pool: &mut DbPool<'_>,
community_report_form: &CommunityReportForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
insert_into(community_report)
.values(community_report_form)
.get_result::<Self>(conn)
.await
}
/// resolve a community report
///
/// * `conn` - the postgres connection
/// * `report_id` - the id of the report to resolve
/// * `by_resolver_id` - the id of the user resolving the report
async fn resolve(
pool: &mut DbPool<'_>,
report_id_: Self::IdType,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
update(community_report.find(report_id_))
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(Utc::now()),
))
.execute(conn)
.await
}
async fn resolve_all_for_object(
pool: &mut DbPool<'_>,
community_id_: CommunityId,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
update(community_report.filter(community_id.eq(community_id_)))
.set((
resolved.eq(true),
resolver_id.eq(by_resolver_id),
updated.eq(Utc::now()),
))
.execute(conn)
.await
}
/// unresolve a community report
///
/// * `conn` - the postgres connection
/// * `report_id` - the id of the report to unresolve
/// * `by_resolver_id` - the id of the user unresolving the report
async fn unresolve(
pool: &mut DbPool<'_>,
report_id_: Self::IdType,
by_resolver_id: PersonId,
) -> Result<usize, Error> {
let conn = &mut get_conn(pool).await?;
update(community_report.find(report_id_))
.set((
resolved.eq(false),
resolver_id.eq(by_resolver_id),
updated.eq(Utc::now()),
))
.execute(conn)
.await
}
}

View file

@ -6,6 +6,7 @@ pub mod comment_reply;
pub mod comment_report;
pub mod community;
pub mod community_block;
pub mod community_report;
pub mod custom_emoji;
pub mod email_verification;
pub mod federation_allowlist;

View file

@ -91,6 +91,12 @@ pub struct PersonMentionId(i32);
/// The comment report id.
pub struct CommentReportId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
#[cfg_attr(feature = "full", ts(export))]
/// The community report id.
pub struct CommunityReportId(pub i32);
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "full", derive(DieselNewType, TS))]
#[cfg_attr(feature = "full", ts(export))]

View file

@ -253,6 +253,8 @@ diesel::table! {
users_active_half_year -> Int8,
hot_rank -> Float8,
subscribers_local -> Int8,
report_count -> Int2,
unresolved_report_count -> Int2,
}
}
@ -263,6 +265,25 @@ diesel::table! {
}
}
diesel::table! {
community_report (id) {
id -> Int4,
creator_id -> Int4,
community_id -> Int4,
original_community_name -> Text,
original_community_title -> Text,
original_community_description -> Nullable<Text>,
original_community_sidebar -> Nullable<Text>,
original_community_icon -> Nullable<Text>,
original_community_banner -> Nullable<Text>,
reason -> Text,
resolved -> Bool,
resolver_id -> Nullable<Int4>,
published -> Timestamptz,
updated -> Nullable<Timestamptz>,
}
}
diesel::table! {
custom_emoji (id) {
id -> Int4,
@ -922,6 +943,7 @@ diesel::table! {
post_report_id -> Nullable<Int4>,
comment_report_id -> Nullable<Int4>,
private_message_report_id -> Nullable<Int4>,
community_report_id -> Nullable<Int4>,
}
}
@ -1040,6 +1062,7 @@ diesel::joinable!(community_actions -> community (community_id));
diesel::joinable!(community_aggregates -> community (community_id));
diesel::joinable!(community_language -> community (community_id));
diesel::joinable!(community_language -> language (language_id));
diesel::joinable!(community_report -> community (community_id));
diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id));
diesel::joinable!(email_verification -> local_user (local_user_id));
diesel::joinable!(federation_allowlist -> instance (instance_id));
@ -1099,6 +1122,7 @@ diesel::joinable!(private_message_report -> private_message (private_message_id)
diesel::joinable!(registration_application -> local_user (local_user_id));
diesel::joinable!(registration_application -> person (admin_id));
diesel::joinable!(report_combined -> comment_report (comment_report_id));
diesel::joinable!(report_combined -> community_report (community_report_id));
diesel::joinable!(report_combined -> post_report (post_report_id));
diesel::joinable!(report_combined -> private_message_report (private_message_report_id));
diesel::joinable!(site -> instance (instance_id));
@ -1124,6 +1148,7 @@ diesel::allow_tables_to_appear_in_same_query!(
community_actions,
community_aggregates,
community_language,
community_report,
custom_emoji,
custom_emoji_keyword,
email_verification,

View file

@ -0,0 +1,60 @@
use crate::newtypes::{CommunityId, CommunityReportId, DbUrl, PersonId};
#[cfg(feature = "full")]
use crate::schema::community_report;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
#[cfg(feature = "full")]
use ts_rs::TS;
#[skip_serializing_none]
#[derive(PartialEq, Eq, Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(
feature = "full",
derive(Queryable, Selectable, Associations, Identifiable, TS)
)]
#[cfg_attr(
feature = "full",
diesel(belongs_to(crate::source::community::Community))
)]
#[cfg_attr(feature = "full", diesel(table_name = community_report))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
/// A comment report.
pub struct CommunityReport {
pub id: CommunityReportId,
pub creator_id: PersonId,
pub community_id: CommunityId,
pub original_community_name: String,
pub original_community_title: String,
#[cfg_attr(feature = "full", ts(optional))]
pub original_community_description: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub original_community_sidebar: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub original_community_icon: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub original_community_banner: Option<String>,
pub reason: String,
pub resolved: bool,
#[cfg_attr(feature = "full", ts(optional))]
pub resolver_id: Option<PersonId>,
pub published: DateTime<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub updated: Option<DateTime<Utc>>,
}
#[derive(Clone)]
#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))]
#[cfg_attr(feature = "full", diesel(table_name = community_report))]
pub struct CommunityReportForm {
pub creator_id: PersonId,
pub community_id: CommunityId,
pub original_community_name: String,
pub original_community_title: String,
pub original_community_description: Option<String>,
pub original_community_sidebar: Option<String>,
pub original_community_icon: Option<DbUrl>,
pub original_community_banner: Option<DbUrl>,
pub reason: String,
}

View file

@ -11,6 +11,7 @@ pub mod comment_reply;
pub mod comment_report;
pub mod community;
pub mod community_block;
pub mod community_report;
pub mod custom_emoji;
pub mod custom_emoji_keyword;
pub mod email_verification;

View file

@ -1,6 +1,7 @@
use crate::{
structs::{
CommentReportView,
CommunityReportView,
LocalUserView,
PostReportView,
PrivateMessageReportView,
@ -32,6 +33,8 @@ use lemmy_db_schema::{
comment_report,
community,
community_actions,
community_aggregates,
community_report,
local_user,
person,
person_actions,
@ -67,6 +70,7 @@ impl ReportCombinedViewInternal {
.left_join(post_report::table)
.left_join(comment_report::table)
.left_join(private_message_report::table)
.left_join(community_report::table)
// Need to join to comment and post to get the community
.left_join(comment::table.on(comment_report::comment_id.eq(comment::id)))
// The post
@ -87,6 +91,7 @@ impl ReportCombinedViewInternal {
post_report::resolved
.or(comment_report::resolved)
.or(private_message_report::resolved)
.or(community_report::resolved)
.is_distinct_from(true),
)
.into_boxed();
@ -114,6 +119,7 @@ impl ReportCombinedPaginationCursor {
ReportCombinedView::Comment(v) => ('C', v.comment_report.id.0),
ReportCombinedView::Post(v) => ('P', v.post_report.id.0),
ReportCombinedView::PrivateMessage(v) => ('M', v.private_message_report.id.0),
ReportCombinedView::Community(v) => ('Y', v.community_report.id.0),
};
// hex encoding to prevent ossification
ReportCombinedPaginationCursor(format!("{prefix}{id:x}"))
@ -130,6 +136,7 @@ impl ReportCombinedPaginationCursor {
"C" => query.filter(report_combined::comment_report_id.eq(id)),
"P" => query.filter(report_combined::post_report_id.eq(id)),
"M" => query.filter(report_combined::private_message_report_id.eq(id)),
"Y" => query.filter(report_combined::community_report_id.eq(id)),
_ => return Err(err_msg()),
};
let token = query.first(&mut get_conn(pool).await?).await?;
@ -171,13 +178,15 @@ impl ReportCombinedQuery {
.left_join(post_report::table)
.left_join(comment_report::table)
.left_join(private_message_report::table)
.left_join(community_report::table)
// The report creator
.inner_join(
person::table.on(
post_report::creator_id
.eq(report_creator)
.or(comment_report::creator_id.eq(report_creator))
.or(private_message_report::creator_id.eq(report_creator)),
.or(private_message_report::creator_id.eq(report_creator))
.or(community_report::creator_id.eq(report_creator)),
),
)
// The comment
@ -196,7 +205,7 @@ impl ReportCombinedQuery {
),
)
// The item creator (`item_creator` is the id of this person)
.inner_join(
.left_join(
aliases::person1.on(
post::creator_id
.eq(item_creator)
@ -205,7 +214,13 @@ impl ReportCombinedQuery {
),
)
// The community
.left_join(community::table.on(post::community_id.eq(community::id)))
.left_join(
community::table.on(
post::community_id
.eq(community::id)
.or(community_report::community_id.eq(community::id)),
),
)
.left_join(actions_alias(
creator_community_actions,
item_creator,
@ -221,7 +236,7 @@ impl ReportCombinedQuery {
.left_join(actions(
community_actions::table,
Some(my_person_id),
post::community_id,
community::id,
))
.left_join(actions(post_actions::table, Some(my_person_id), post::id))
.left_join(actions(
@ -233,13 +248,18 @@ impl ReportCombinedQuery {
.left_join(
comment_aggregates::table.on(comment_report::comment_id.eq(comment_aggregates::comment_id)),
)
.left_join(
community_aggregates::table
.on(community_report::community_id.eq(community_aggregates::community_id)),
)
// The resolver
.left_join(
aliases::person2.on(
private_message_report::resolver_id
.eq(resolver)
.or(post_report::resolver_id.eq(resolver))
.or(comment_report::resolver_id.eq(resolver)),
.or(comment_report::resolver_id.eq(resolver))
.or(community_report::resolver_id.eq(resolver)),
),
)
.left_join(actions(
@ -270,9 +290,12 @@ impl ReportCombinedQuery {
// Private-message-specific
private_message_report::all_columns.nullable(),
private_message::all_columns.nullable(),
// Community-specific
community_report::all_columns.nullable(),
community_aggregates::all_columns.nullable(),
// Shared
person::all_columns,
aliases::person1.fields(person::all_columns),
aliases::person1.fields(person::all_columns.nullable()),
community::all_columns.nullable(),
CommunityFollower::select_subscribed_type(),
aliases::person2.fields(person::all_columns.nullable()),
@ -290,12 +313,20 @@ impl ReportCombinedQuery {
.into_boxed();
if let Some(community_id) = self.community_id {
query = query.filter(community::id.eq(community_id));
query = query.filter(
community::id
.eq(community_id)
.and(report_combined::community_report_id.is_null()),
);
}
// If its not an admin, get only the ones you mod
if !user.local_user.admin {
query = query.filter(community_actions::became_moderator.is_not_null());
query = query.filter(
community_actions::became_moderator
.is_not_null()
.and(report_combined::community_report_id.is_null()),
);
}
let mut query = PaginatedQueryBuilder::new(query);
@ -316,6 +347,7 @@ impl ReportCombinedQuery {
post_report::resolved
.or(comment_report::resolved)
.or(private_message_report::resolved)
.or(community_report::resolved)
.is_distinct_from(true),
)
// TODO: when a `then_asc` method is added, use it here, make the id sort direction match,
@ -344,12 +376,20 @@ impl InternalToCombinedView for ReportCombinedViewInternal {
// Use for a short alias
let v = self.clone();
if let (Some(post_report), Some(post), Some(community), Some(unread_comments), Some(counts)) = (
if let (
Some(post_report),
Some(post),
Some(community),
Some(unread_comments),
Some(counts),
Some(post_creator),
) = (
v.post_report,
v.post.clone(),
v.community.clone(),
v.post_unread_comments,
v.post_counts,
v.item_creator.clone(),
) {
Some(ReportCombinedView::Post(PostReportView {
post_report,
@ -358,7 +398,7 @@ impl InternalToCombinedView for ReportCombinedViewInternal {
unread_comments,
counts,
creator: v.report_creator,
post_creator: v.item_creator,
post_creator,
creator_banned_from_community: v.item_creator_banned_from_community,
creator_is_moderator: v.item_creator_is_moderator,
creator_is_admin: v.item_creator_is_admin,
@ -370,12 +410,20 @@ impl InternalToCombinedView for ReportCombinedViewInternal {
my_vote: v.my_post_vote,
resolver: v.resolver,
}))
} else if let (Some(comment_report), Some(comment), Some(counts), Some(post), Some(community)) = (
} else if let (
Some(comment_report),
Some(comment),
Some(counts),
Some(post),
Some(community),
Some(comment_creator),
) = (
v.comment_report,
v.comment,
v.comment_counts,
v.post,
v.community,
v.community.clone(),
v.item_creator.clone(),
) {
Some(ReportCombinedView::Comment(CommentReportView {
comment_report,
@ -384,7 +432,7 @@ impl InternalToCombinedView for ReportCombinedViewInternal {
post,
community,
creator: v.report_creator,
comment_creator: v.item_creator,
comment_creator,
creator_banned_from_community: v.item_creator_banned_from_community,
creator_is_moderator: v.item_creator_is_moderator,
creator_is_admin: v.item_creator_is_admin,
@ -394,18 +442,32 @@ impl InternalToCombinedView for ReportCombinedViewInternal {
my_vote: v.my_comment_vote,
resolver: v.resolver,
}))
} else if let (Some(private_message_report), Some(private_message)) =
(v.private_message_report, v.private_message)
} else if let (
Some(private_message_report),
Some(private_message),
Some(private_message_creator),
) = (v.private_message_report, v.private_message, v.item_creator)
{
Some(ReportCombinedView::PrivateMessage(
PrivateMessageReportView {
private_message_report,
private_message,
creator: v.report_creator,
private_message_creator: v.item_creator,
private_message_creator,
resolver: v.resolver,
},
))
} else if let (Some(community), Some(community_report), Some(counts)) =
(v.community, v.community_report, v.community_counts)
{
Some(ReportCombinedView::Community(CommunityReportView {
community_report,
community,
creator: v.report_creator,
counts,
subscribed: v.subscribed,
resolver: v.resolver,
}))
} else {
None
}
@ -433,6 +495,7 @@ mod tests {
comment::{Comment, CommentInsertForm},
comment_report::{CommentReport, CommentReportForm},
community::{Community, CommunityInsertForm, CommunityModerator, CommunityModeratorForm},
community_report::{CommunityReport, CommunityReportForm},
instance::Instance,
local_user::{LocalUser, LocalUserInsertForm},
local_user_vote_display_mode::LocalUserVoteDisplayMode,
@ -558,6 +621,20 @@ mod tests {
let pool = &mut pool.into();
let data = init_data(pool).await?;
// Sara reports the community
let sara_report_community_form = CommunityReportForm {
creator_id: data.sara.id,
community_id: data.community.id,
original_community_name: data.community.name.clone(),
original_community_title: data.community.title.clone(),
original_community_banner: None,
original_community_description: None,
original_community_sidebar: None,
original_community_icon: None,
reason: "from sara".into(),
};
CommunityReport::report(pool, &sara_report_community_form).await?;
// sara reports the post
let sara_report_post_form = PostReportForm {
creator_id: data.sara.id,
@ -599,9 +676,14 @@ mod tests {
let reports = ReportCombinedQuery::default()
.list(pool, &data.admin_view)
.await?;
assert_eq!(3, reports.len());
assert_eq!(4, reports.len());
// Make sure the report types are correct
if let ReportCombinedView::Community(v) = &reports[3] {
assert_eq!(data.community.id, v.community.id);
} else {
panic!("wrong type");
}
if let ReportCombinedView::Post(v) = &reports[2] {
assert_eq!(data.post.id, v.post.id);
assert_eq!(data.sara.id, v.creator.id);
@ -624,7 +706,7 @@ mod tests {
let report_count_admin =
ReportCombinedViewInternal::get_report_count(pool, &data.admin_view, None).await?;
assert_eq!(3, report_count_admin);
assert_eq!(4, report_count_admin);
// Timmy should only see 2 reports, since they're not an admin,
// but they do mod the community
@ -971,4 +1053,62 @@ mod tests {
Ok(())
}
#[tokio::test]
#[serial]
async fn test_community_reports() -> LemmyResult<()> {
let pool = &build_db_pool_for_tests();
let pool = &mut pool.into();
let data = init_data(pool).await?;
// jessica reports community
let community_report_form = CommunityReportForm {
creator_id: data.jessica.id,
community_id: data.community.id,
original_community_name: data.community.name.clone(),
original_community_title: data.community.title.clone(),
original_community_banner: None,
original_community_description: None,
original_community_sidebar: None,
original_community_icon: None,
reason: "the ice cream incident".into(),
};
let community_report = CommunityReport::report(pool, &community_report_form).await?;
let reports = ReportCombinedQuery::default()
.list(pool, &data.admin_view)
.await?;
assert_length!(1, reports);
if let ReportCombinedView::Community(v) = &reports[0] {
assert!(!v.community_report.resolved);
assert_eq!(data.jessica.name, v.creator.name);
assert_eq!(community_report.reason, v.community_report.reason);
assert_eq!(data.community.name, v.community.name);
assert_eq!(data.community.title, v.community.title);
} else {
panic!("wrong type");
}
// admin resolves the report (after taking appropriate action)
CommunityReport::resolve(pool, community_report.id, data.admin_view.person.id).await?;
let reports = ReportCombinedQuery::default()
.list(pool, &data.admin_view)
.await?;
assert_length!(1, reports);
if let ReportCombinedView::Community(v) = &reports[0] {
assert!(v.community_report.resolved);
assert!(v.resolver.is_some());
assert_eq!(
Some(&data.admin_view.person.name),
v.resolver.as_ref().map(|r| &r.name)
);
} else {
panic!("wrong type");
}
cleanup(data, pool).await?;
Ok(())
}
}

View file

@ -3,11 +3,18 @@ use diesel::Queryable;
#[cfg(feature = "full")]
use diesel::{deserialize::FromSqlRow, expression::AsExpression, sql_types};
use lemmy_db_schema::{
aggregates::structs::{CommentAggregates, PersonAggregates, PostAggregates, SiteAggregates},
aggregates::structs::{
CommentAggregates,
CommunityAggregates,
PersonAggregates,
PostAggregates,
SiteAggregates,
},
source::{
comment::Comment,
comment_report::CommentReport,
community::Community,
community_report::CommunityReport,
custom_emoji::CustomEmoji,
custom_emoji_keyword::CustomEmojiKeyword,
images::{ImageDetails, LocalImage},
@ -80,6 +87,22 @@ pub struct CommentView {
pub my_vote: Option<i16>,
}
#[skip_serializing_none]
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
/// A community report view.
pub struct CommunityReportView {
pub community_report: CommunityReport,
pub community: Community,
pub creator: Person,
pub counts: CommunityAggregates,
pub subscribed: SubscribedType,
#[cfg_attr(feature = "full", ts(optional))]
pub resolver: Option<Person>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
@ -283,9 +306,12 @@ pub struct ReportCombinedViewInternal {
// Private-message-specific
pub private_message_report: Option<PrivateMessageReport>,
pub private_message: Option<PrivateMessage>,
// Community-specific
pub community_report: Option<CommunityReport>,
pub community_counts: Option<CommunityAggregates>,
// Shared
pub report_creator: Person,
pub item_creator: Person,
pub item_creator: Option<Person>,
pub community: Option<Community>,
pub subscribed: SubscribedType,
pub resolver: Option<Person>,
@ -304,6 +330,7 @@ pub enum ReportCombinedView {
Post(PostReportView),
Comment(CommentReportView),
PrivateMessage(PrivateMessageReportView),
Community(CommunityReportView),
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)]

View file

@ -710,7 +710,6 @@ mod test {
let data = json!({
"actor": "http://ds9.lemmy.ml/u/lemmy_alpha",
"object": "http://ds9.lemmy.ml/comment/1",
"audience": "https://enterprise.lemmy.ml/c/tenforward",
"type": "Like",
"id": format!("http://ds9.lemmy.ml/activities/like/{}", uuid::Uuid::new_v4()),
});

View file

@ -165,8 +165,6 @@ pub enum LemmyErrorType {
#[cfg_attr(feature = "full", ts(export))]
#[non_exhaustive]
pub enum FederationError {
// TODO: merge into a single NotFound error
CouldntFindActivity,
InvalidCommunity,
CannotCreatePostOrCommentInDeletedOrRemovedCommunity,
CannotReceivePage,
@ -247,6 +245,9 @@ cfg_if! {
if self.error_type == LemmyErrorType::IncorrectLogin {
return actix_web::http::StatusCode::UNAUTHORIZED;
}
if self.error_type == LemmyErrorType::NotFound {
return actix_web::http::StatusCode::NOT_FOUND;
}
match self.inner.downcast_ref::<diesel::result::Error>() {
Some(diesel::result::Error::NotFound) => actix_web::http::StatusCode::NOT_FOUND,
_ => actix_web::http::StatusCode::BAD_REQUEST,

View file

@ -3,12 +3,12 @@ use anyhow::{anyhow, Context};
use deser_hjson::from_str;
use regex::Regex;
use std::{env, fs, io::Error, sync::LazyLock};
use structs::{PictrsConfig, PictrsImageMode, Settings};
use structs::{PictrsConfig, Settings};
use url::Url;
pub mod structs;
const DEFAULT_CONFIG_FILE: &str = "config/config.hjson";
static DEFAULT_CONFIG_FILE: &str = "config/config.hjson";
#[allow(clippy::expect_used)]
pub static SETTINGS: LazyLock<Settings> = LazyLock::new(|| {
@ -104,21 +104,6 @@ impl Settings {
.ok_or_else(|| anyhow!("images_disabled").into())
}
}
impl PictrsConfig {
pub fn image_mode(&self) -> PictrsImageMode {
if let Some(cache_external_link_previews) = self.cache_external_link_previews {
if cache_external_link_previews {
PictrsImageMode::StoreLinkPreviews
} else {
PictrsImageMode::None
}
} else {
self.image_mode.clone()
}
}
}
#[allow(clippy::expect_used)]
/// Necessary to avoid URL expect failures
fn pictrs_placeholder_url() -> Url {

View file

@ -77,16 +77,10 @@ pub struct PictrsConfig {
#[default(None)]
pub api_key: Option<String>,
/// Backwards compatibility with 0.18.1. False is equivalent to `image_mode: None`, true is
/// equivalent to `image_mode: StoreLinkPreviews`.
///
/// To be removed in 0.20
pub(super) cache_external_link_previews: Option<bool>,
/// Specifies how to handle remote images, so that users don't have to connect directly to remote
/// servers.
#[default(PictrsImageMode::StoreLinkPreviews)]
pub(super) image_mode: PictrsImageMode,
#[default(PictrsImageMode::ProxyAllImages)]
pub image_mode: PictrsImageMode,
/// Allows bypassing proxy for specific image hosts when using ProxyAllImages.
///
@ -130,8 +124,7 @@ pub enum PictrsImageMode {
/// ensures that they can be reliably retrieved and can be resized using pict-rs APIs. However
/// it also increases storage usage.
///
/// This is the default behaviour, and also matches Lemmy 0.18.
#[default]
/// This behaviour matches Lemmy 0.18.
StoreLinkPreviews,
/// If enabled, all images from remote domains are rewritten to pass through
/// `/api/v4/image/proxy`, including embedded images in markdown. Images are stored temporarily
@ -140,6 +133,7 @@ pub enum PictrsImageMode {
/// local server.
///
/// Requires pict-rs 0.5
#[default]
ProxyAllImages,
}

View file

@ -12,5 +12,6 @@
}
pictrs: {
api_key: "my-pictrs-key"
image_mode: StoreLinkPreviews
}
}

View file

@ -12,5 +12,6 @@
}
pictrs: {
api_key: "my-pictrs-key"
image_mode: StoreLinkPreviews
}
}

View file

@ -0,0 +1,14 @@
DELETE FROM report_combined
WHERE community_report_id IS NOT NULL;
ALTER TABLE report_combined
DROP CONSTRAINT report_combined_check,
ADD CHECK (num_nonnulls (post_report_id, comment_report_id, private_message_report_id) = 1),
DROP COLUMN community_report_id;
DROP TABLE community_report CASCADE;
ALTER TABLE community_aggregates
DROP COLUMN report_count,
DROP COLUMN unresolved_report_count;

View file

@ -0,0 +1,29 @@
CREATE TABLE community_report (
id serial PRIMARY KEY,
creator_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
community_id int REFERENCES community ON UPDATE CASCADE ON DELETE CASCADE NOT NULL,
original_community_name text NOT NULL,
original_community_title text NOT NULL,
original_community_description text,
original_community_sidebar text,
original_community_icon text,
original_community_banner text,
reason text NOT NULL,
resolved bool NOT NULL DEFAULT FALSE,
resolver_id int REFERENCES person ON UPDATE CASCADE ON DELETE CASCADE,
published timestamptz NOT NULL DEFAULT now(),
updated timestamptz NULL,
UNIQUE (community_id, creator_id)
);
CREATE INDEX idx_community_report_published ON community_report (published DESC);
ALTER TABLE report_combined
ADD COLUMN community_report_id int UNIQUE REFERENCES community_report ON UPDATE CASCADE ON DELETE CASCADE,
DROP CONSTRAINT report_combined_check,
ADD CHECK (num_nonnulls (post_report_id, comment_report_id, private_message_report_id, community_report_id) = 1);
ALTER TABLE community_aggregates
ADD COLUMN report_count smallint NOT NULL DEFAULT 0,
ADD COLUMN unresolved_report_count smallint NOT NULL DEFAULT 0;

View file

@ -3,6 +3,7 @@ use chrono::{DateTime, TimeZone, Utc};
use clokwerk::{AsyncScheduler, TimeUnits as CTimeUnits};
use diesel::{
dsl::{exists, not, IntervalDsl},
query_builder::AsQuery,
sql_query,
sql_types::{Integer, Timestamptz},
BoolExpressionMethods,
@ -37,7 +38,15 @@ use lemmy_db_schema::{
post::{Post, PostUpdateForm},
},
traits::Crud,
utils::{find_action, functions::coalesce, get_conn, now, DbPool, DELETED_REPLACEMENT_TEXT},
utils::{
find_action,
functions::coalesce,
get_conn,
now,
uplete,
DbPool,
DELETED_REPLACEMENT_TEXT,
},
};
use lemmy_routes::nodeinfo::{NodeInfo, NodeInfoWellKnown};
use lemmy_utils::error::{LemmyErrorType, LemmyResult};
@ -389,11 +398,12 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) -> LemmyResult<()> {
.execute(&mut conn)
.await?;
diesel::delete(
community_actions::table.filter(community_actions::ban_expires.lt(now().nullable())),
)
.execute(&mut conn)
.await?;
uplete::new(community_actions::table.filter(community_actions::ban_expires.lt(now().nullable())))
.set_null(community_actions::received_ban)
.set_null(community_actions::ban_expires)
.as_query()
.execute(&mut conn)
.await?;
Ok(())
}