From f6de3b03888a36927af243a9c008bd10ce25e9cb Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 28 Nov 2024 23:03:14 +0000 Subject: [PATCH 1/3] Enable site languages for new user (fixes #5234) (#5235) * Enable site languages for new user (fixes #5234) * test coverage --- api_tests/src/user.spec.ts | 14 ++++++++++++-- crates/api_crud/src/user/create.rs | 27 ++++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/api_tests/src/user.spec.ts b/api_tests/src/user.spec.ts index 2edcf54ea..0cc747d2b 100644 --- a/api_tests/src/user.spec.ts +++ b/api_tests/src/user.spec.ts @@ -23,7 +23,12 @@ import { unfollows, saveUserSettingsBio, } from "./shared"; -import { LemmyHttp, SaveUserSettings, UploadImage } from "lemmy-js-client"; +import { + EditSite, + LemmyHttp, + SaveUserSettings, + UploadImage, +} from "lemmy-js-client"; import { GetPosts } from "lemmy-js-client/dist/types/GetPosts"; beforeAll(setupLogins); @@ -149,9 +154,14 @@ test("Create user with Arabic name", async () => { }); test("Create user with accept-language", async () => { + const edit: EditSite = { + discussion_languages: [32], + }; + await alpha.editSite(edit); + let lemmy_http = new LemmyHttp(alphaUrl, { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language#syntax - headers: { "Accept-Language": "fr-CH, en;q=0.8, de;q=0.7, *;q=0.5" }, + headers: { "Accept-Language": "fr-CH, en;q=0.8, *;q=0.5" }, }); let user = await registerUser(lemmy_http, alphaUrl); diff --git a/crates/api_crud/src/user/create.rs b/crates/api_crud/src/user/create.rs index 3bd372937..deb65ec38 100644 --- a/crates/api_crud/src/user/create.rs +++ b/crates/api_crud/src/user/create.rs @@ -21,8 +21,9 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ aggregates::structs::PersonAggregates, - newtypes::{InstanceId, OAuthProviderId}, + newtypes::{InstanceId, OAuthProviderId, SiteId}, source::{ + actor_language::SiteLanguage, captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, language::Language, local_site::LocalSite, @@ -145,7 +146,13 @@ pub async fn register( ..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string())) }; - let inserted_local_user = create_local_user(&context, language_tags, &local_user_form).await?; + let inserted_local_user = create_local_user( + &context, + language_tags, + &local_user_form, + local_site.site_id, + ) + .await?; if local_site.site_setup && require_registration_application { if let Some(answer) = data.answer.clone() { @@ -358,7 +365,13 @@ pub async fn authenticate_with_oauth( ..LocalUserInsertForm::new(person.id, None) }; - local_user = create_local_user(&context, language_tags, &local_user_form).await?; + local_user = create_local_user( + &context, + language_tags, + &local_user_form, + local_site.site_id, + ) + .await?; // Create the oauth account let oauth_account_form = @@ -449,15 +462,23 @@ async fn create_local_user( context: &Data, language_tags: Vec, local_user_form: &LocalUserInsertForm, + local_site_id: SiteId, ) -> Result { let all_languages = Language::read_all(&mut context.pool()).await?; // use hashset to avoid duplicates let mut language_ids = HashSet::new(); + + // Enable languages from `Accept-Language` header for l in language_tags { if let Some(found) = all_languages.iter().find(|all| all.code == l) { language_ids.insert(found.id); } } + + // Enable site languages. Ignored if all languages are enabled. + let discussion_languages = SiteLanguage::read(&mut context.pool(), local_site_id).await?; + language_ids.extend(discussion_languages); + let language_ids = language_ids.into_iter().collect(); let inserted_local_user = From 66a63df15216d92538d108911fb98dbfe266cb9a Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 28 Nov 2024 23:21:43 +0000 Subject: [PATCH 2/3] Instance blocks with mod log entry and expiration (fixes #2506) (#5214) * Instance blocks with mod log entry and expiration (fixes #2506) * separate table for instance block mod log * fix tests * fix ts * modlog entry for allow instance * fix test cleanup * add back test * clippy * fix check * more changes * move files * update * sql fmt * partly working * fix setup * cleanup * fixes * prettier * try catch * address comments --- api_tests/package.json | 2 +- api_tests/pnpm-lock.yaml | 10 +- api_tests/src/community.spec.ts | 21 +- api_tests/src/post.spec.ts | 21 +- api_tests/src/shared.ts | 70 ++-- crates/api/src/community/add_mod.rs | 2 +- crates/api/src/community/ban.rs | 2 +- crates/api/src/community/hide.rs | 2 +- crates/api/src/community/transfer.rs | 2 +- crates/api/src/lib.rs | 2 +- crates/api/src/local_user/add_admin.rs | 2 +- crates/api/src/local_user/ban_person.rs | 2 +- crates/api/src/post/feature.rs | 2 +- crates/api/src/post/lock.rs | 2 +- crates/api/src/site/admin_allow_instance.rs | 53 +++ crates/api/src/site/admin_block_instance.rs | 56 ++++ crates/api/src/site/leave_admin.rs | 2 +- crates/api/src/site/mod.rs | 4 +- crates/api/src/site/mod_log.rs | 18 + crates/api/src/site/purge/comment.rs | 2 +- crates/api/src/site/purge/community.rs | 2 +- crates/api/src/site/purge/person.rs | 2 +- crates/api/src/site/purge/post.rs | 2 +- .../site/{block.rs => user_block_instance.rs} | 15 +- crates/api_common/src/site.rs | 38 ++- crates/api_common/src/utils.rs | 7 +- crates/api_crud/src/comment/remove.rs | 2 +- crates/api_crud/src/community/remove.rs | 2 +- crates/api_crud/src/post/remove.rs | 2 +- crates/api_crud/src/site/update.rs | 8 - .../apub/src/activities/block/block_user.rs | 2 +- .../src/activities/block/undo_block_user.rs | 2 +- .../activities/community/collection_add.rs | 2 +- .../activities/community/collection_remove.rs | 2 +- .../src/activities/community/lock_page.rs | 2 +- crates/apub/src/activities/deletion/delete.rs | 2 +- .../src/activities/deletion/undo_delete.rs | 2 +- crates/apub/src/api/user_settings_backup.rs | 10 +- .../src/impls/federation_allowlist.rs | 113 +++---- .../src/impls/federation_blocklist.rs | 69 ++-- crates/db_schema/src/impls/mod.rs | 2 +- crates/db_schema/src/impls/mod_log/admin.rs | 132 ++++++++ crates/db_schema/src/impls/mod_log/mod.rs | 2 + .../src/impls/{ => mod_log}/moderator.rs | 149 +-------- crates/db_schema/src/lib.rs | 2 + crates/db_schema/src/schema.rs | 30 ++ .../src/source/federation_blocklist.rs | 11 +- crates/db_schema/src/source/mod.rs | 2 +- crates/db_schema/src/source/mod_log/admin.rs | 176 ++++++++++ crates/db_schema/src/source/mod_log/mod.rs | 2 + .../src/source/{ => mod_log}/moderator.rs | 96 ------ .../src/admin_allow_instance.rs | 52 +++ .../src/admin_block_instance.rs | 52 +++ crates/db_views_moderator/src/lib.rs | 4 + crates/db_views_moderator/src/structs.rs | 65 +++- crates/federate/src/lib.rs | 35 +- crates/utils/src/error.rs | 1 + .../down.sql | 7 + .../up.sql | 22 ++ src/api_routes_http.rs | 308 +++++++++--------- src/scheduled_tasks.rs | 24 ++ 61 files changed, 1089 insertions(+), 648 deletions(-) create mode 100644 crates/api/src/site/admin_allow_instance.rs create mode 100644 crates/api/src/site/admin_block_instance.rs rename crates/api/src/site/{block.rs => user_block_instance.rs} (79%) create mode 100644 crates/db_schema/src/impls/mod_log/admin.rs create mode 100644 crates/db_schema/src/impls/mod_log/mod.rs rename crates/db_schema/src/impls/{ => mod_log}/moderator.rs (82%) create mode 100644 crates/db_schema/src/source/mod_log/admin.rs create mode 100644 crates/db_schema/src/source/mod_log/mod.rs rename crates/db_schema/src/source/{ => mod_log}/moderator.rs (75%) create mode 100644 crates/db_views_moderator/src/admin_allow_instance.rs create mode 100644 crates/db_views_moderator/src/admin_block_instance.rs create mode 100644 migrations/2024-11-28-142005_instance-block-mod-log/down.sql create mode 100644 migrations/2024-11-28-142005_instance-block-mod-log/up.sql diff --git a/api_tests/package.json b/api_tests/package.json index 57173595a..ef47bf192 100644 --- a/api_tests/package.json +++ b/api_tests/package.json @@ -28,7 +28,7 @@ "eslint": "^9.14.0", "eslint-plugin-prettier": "^5.1.3", "jest": "^29.5.0", - "lemmy-js-client": "0.20.0-alpha.18", + "lemmy-js-client": "0.20.0-instance-blocks.5", "prettier": "^3.2.5", "ts-jest": "^29.1.0", "typescript": "^5.5.4", diff --git a/api_tests/pnpm-lock.yaml b/api_tests/pnpm-lock.yaml index 01d4a8e74..a95e80726 100644 --- a/api_tests/pnpm-lock.yaml +++ b/api_tests/pnpm-lock.yaml @@ -30,8 +30,8 @@ importers: specifier: ^29.5.0 version: 29.7.0(@types/node@22.9.0) lemmy-js-client: - specifier: 0.20.0-alpha.18 - version: 0.20.0-alpha.18 + specifier: 0.20.0-instance-blocks.5 + version: 0.20.0-instance-blocks.5 prettier: specifier: ^3.2.5 version: 3.3.3 @@ -1167,8 +1167,8 @@ packages: resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} engines: {node: '>=6'} - lemmy-js-client@0.20.0-alpha.18: - resolution: {integrity: sha512-oZy8DboTWfUar4mPWpi7SYrOEjTBJxkvd1e6QaVwoA5UhqQV1WhxEYbzrpi/gXnEokaVQ0i5sjtL/Y2PHMO3MQ==} + lemmy-js-client@0.20.0-instance-blocks.5: + resolution: {integrity: sha512-wDuRFzg32lbbJr4cNmd+cbzjgw+okw2/d5AujYjAm4gv0OEFfsYhP3QQ2WscwUR5HJTdzsR7IIyiBnvmaEUzUw==} leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} @@ -3077,7 +3077,7 @@ snapshots: kleur@3.0.3: {} - lemmy-js-client@0.20.0-alpha.18: {} + lemmy-js-client@0.20.0-instance-blocks.5: {} leven@3.1.0: {} diff --git a/api_tests/src/community.spec.ts b/api_tests/src/community.spec.ts index 77b68e2fc..d75b711fc 100644 --- a/api_tests/src/community.spec.ts +++ b/api_tests/src/community.spec.ts @@ -25,16 +25,16 @@ import { getComments, createComment, getCommunityByName, - blockInstance, waitUntil, alphaUrl, delta, - betaAllowedInstances, searchPostLocal, longDelay, editCommunity, unfollows, + userBlockInstance, } from "./shared"; +import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams"; import { EditCommunity, EditSite } from "lemmy-js-client"; beforeAll(setupLogins); @@ -363,7 +363,7 @@ test("User blocks instance, communities are hidden", async () => { expect(listing_ids).toContain(postRes.post_view.post.ap_id); // block the beta instance - await blockInstance(alpha, alphaPost.community.instance_id, true); + await userBlockInstance(alpha, alphaPost.community.instance_id, true); // after blocking, post should not be in listing let listing2 = await getPosts(alpha, "All"); @@ -371,7 +371,7 @@ test("User blocks instance, communities are hidden", async () => { expect(listing_ids2.indexOf(postRes.post_view.post.ap_id)).toBe(-1); // unblock instance again - await blockInstance(alpha, alphaPost.community.instance_id, false); + await userBlockInstance(alpha, alphaPost.community.instance_id, false); // post should be included in listing let listing3 = await getPosts(alpha, "All"); @@ -455,9 +455,12 @@ test("Dont receive community activities after unsubscribe", async () => { expect(communityRes1.community_view.counts.subscribers).toBe(2); // temporarily block alpha, so that it doesn't know about unfollow - let editSiteForm: EditSite = {}; - editSiteForm.allowed_instances = ["lemmy-epsilon"]; - await beta.editSite(editSiteForm); + var allow_instance_params: AdminAllowInstanceParams = { + instance: "lemmy-alpha", + allow: false, + reason: undefined, + }; + await beta.adminAllowInstance(allow_instance_params); await longDelay(); // unfollow @@ -471,8 +474,8 @@ test("Dont receive community activities after unsubscribe", async () => { expect(communityRes2.community_view.counts.subscribers).toBe(2); // unblock alpha - editSiteForm.allowed_instances = betaAllowedInstances; - await beta.editSite(editSiteForm); + allow_instance_params.allow = true; + await beta.adminAllowInstance(allow_instance_params); await longDelay(); // create a post, it shouldnt reach beta diff --git a/api_tests/src/post.spec.ts b/api_tests/src/post.spec.ts index 59e3d774e..a6063e0a2 100644 --- a/api_tests/src/post.spec.ts +++ b/api_tests/src/post.spec.ts @@ -40,6 +40,7 @@ import { createCommunity, } from "./shared"; import { PostView } from "lemmy-js-client/dist/types/PostView"; +import { AdminBlockInstanceParams } from "lemmy-js-client/dist/types/AdminBlockInstanceParams"; import { EditSite, ResolveObject } from "lemmy-js-client"; let betaCommunity: CommunityView | undefined; @@ -87,12 +88,12 @@ async function assertPostFederation( } test("Create a post", async () => { - // Setup some allowlists and blocklists - const editSiteForm: EditSite = {}; - - editSiteForm.allowed_instances = []; - editSiteForm.blocked_instances = ["lemmy-alpha"]; - await epsilon.editSite(editSiteForm); + // Block alpha + var block_instance_params: AdminBlockInstanceParams = { + instance: "lemmy-alpha", + block: true, + }; + await epsilon.adminBlockInstance(block_instance_params); if (!betaCommunity) { throw "Missing beta community"; @@ -132,11 +133,9 @@ test("Create a post", async () => { resolvePost(epsilon, postRes.post_view.post), ).rejects.toStrictEqual(Error("not_found")); - // remove added allow/blocklists - editSiteForm.allowed_instances = []; - editSiteForm.blocked_instances = []; - await delta.editSite(editSiteForm); - await epsilon.editSite(editSiteForm); + // remove blocked instance + block_instance_params.block = false; + await epsilon.adminBlockInstance(block_instance_params); }); test("Create a post in a non-existent community", async () => { diff --git a/api_tests/src/shared.ts b/api_tests/src/shared.ts index 0b0a9862c..9b0662959 100644 --- a/api_tests/src/shared.ts +++ b/api_tests/src/shared.ts @@ -1,9 +1,8 @@ import { + AdminBlockInstanceParams, ApproveCommunityPendingFollower, BlockCommunity, BlockCommunityResponse, - BlockInstance, - BlockInstanceResponse, CommunityId, CommunityVisibility, CreatePrivateMessageReport, @@ -21,11 +20,13 @@ import { PostView, PrivateMessageReportResponse, SuccessResponse, + UserBlockInstanceParams, } from "lemmy-js-client"; import { CreatePost } from "lemmy-js-client/dist/types/CreatePost"; import { DeletePost } from "lemmy-js-client/dist/types/DeletePost"; import { EditPost } from "lemmy-js-client/dist/types/EditPost"; import { EditSite } from "lemmy-js-client/dist/types/EditSite"; +import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams"; import { FeaturePost } from "lemmy-js-client/dist/types/FeaturePost"; import { GetComments } from "lemmy-js-client/dist/types/GetComments"; import { GetCommentsResponse } from "lemmy-js-client/dist/types/GetCommentsResponse"; @@ -104,13 +105,6 @@ export const gamma = new LemmyHttp(gammaUrl, { fetchFunction }); export const delta = new LemmyHttp(deltaUrl, { fetchFunction }); export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction }); -export const betaAllowedInstances = [ - "lemmy-alpha", - "lemmy-gamma", - "lemmy-delta", - "lemmy-epsilon", -]; - const password = "lemmylemmy"; export async function setupLogins() { @@ -168,30 +162,29 @@ export async function setupLogins() { rate_limit_comment: 999, rate_limit_search: 999, }; - - // Set the blocks and auths for each - editSiteForm.allowed_instances = [ - "lemmy-beta", - "lemmy-gamma", - "lemmy-delta", - "lemmy-epsilon", - ]; await alpha.editSite(editSiteForm); - - editSiteForm.allowed_instances = betaAllowedInstances; await beta.editSite(editSiteForm); - - editSiteForm.allowed_instances = [ - "lemmy-alpha", - "lemmy-beta", - "lemmy-delta", - "lemmy-epsilon", - ]; await gamma.editSite(editSiteForm); - - // Setup delta allowed instance - editSiteForm.allowed_instances = ["lemmy-beta"]; await delta.editSite(editSiteForm); + await epsilon.editSite(editSiteForm); + + // Set the blocks for each + await allowInstance(alpha, "lemmy-beta"); + await allowInstance(alpha, "lemmy-gamma"); + await allowInstance(alpha, "lemmy-delta"); + await allowInstance(alpha, "lemmy-epsilon"); + + await allowInstance(beta, "lemmy-alpha"); + await allowInstance(beta, "lemmy-gamma"); + await allowInstance(beta, "lemmy-delta"); + await allowInstance(beta, "lemmy-epsilon"); + + await allowInstance(gamma, "lemmy-alpha"); + await allowInstance(gamma, "lemmy-beta"); + await allowInstance(gamma, "lemmy-delta"); + await allowInstance(gamma, "lemmy-epsilon"); + + await allowInstance(delta, "lemmy-beta"); // Create the main alpha/beta communities // Ignore thrown errors of duplicates @@ -208,6 +201,17 @@ export async function setupLogins() { } } +async function allowInstance(api: LemmyHttp, instance: string) { + const params: AdminAllowInstanceParams = { + instance, + allow: true, + }; + // Ignore errors from duplicate allows (because setup gets called for each test file) + try { + await api.adminAllowInstance(params); + } catch {} +} + export async function createPost( api: LemmyHttp, community_id: number, @@ -854,16 +858,16 @@ export function getPosts( return api.getPosts(form); } -export function blockInstance( +export function userBlockInstance( api: LemmyHttp, instance_id: InstanceId, block: boolean, -): Promise { - let form: BlockInstance = { +): Promise { + let form: UserBlockInstanceParams = { instance_id, block, }; - return api.blockInstance(form); + return api.userBlockInstance(form); } export function blockCommunity( diff --git a/crates/api/src/community/add_mod.rs b/crates/api/src/community/add_mod.rs index 9e85788ea..4c5b4eae5 100644 --- a/crates/api/src/community/add_mod.rs +++ b/crates/api/src/community/add_mod.rs @@ -10,7 +10,7 @@ use lemmy_db_schema::{ source::{ community::{Community, CommunityModerator, CommunityModeratorForm}, local_user::LocalUser, - moderator::{ModAddCommunity, ModAddCommunityForm}, + mod_log::moderator::{ModAddCommunity, ModAddCommunityForm}, }, traits::{Crud, Joinable}, }; diff --git a/crates/api/src/community/ban.rs b/crates/api/src/community/ban.rs index a0e57061b..8689d2563 100644 --- a/crates/api/src/community/ban.rs +++ b/crates/api/src/community/ban.rs @@ -20,7 +20,7 @@ use lemmy_db_schema::{ CommunityPersonBanForm, }, local_user::LocalUser, - moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, }, traits::{Bannable, Crud, Followable}, }; diff --git a/crates/api/src/community/hide.rs b/crates/api/src/community/hide.rs index 077ed1c5e..f494ad732 100644 --- a/crates/api/src/community/hide.rs +++ b/crates/api/src/community/hide.rs @@ -10,7 +10,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityUpdateForm}, - moderator::{ModHideCommunity, ModHideCommunityForm}, + mod_log::moderator::{ModHideCommunity, ModHideCommunityForm}, }, traits::Crud, }; diff --git a/crates/api/src/community/transfer.rs b/crates/api/src/community/transfer.rs index a5255e5e1..e60b50aa2 100644 --- a/crates/api/src/community/transfer.rs +++ b/crates/api/src/community/transfer.rs @@ -8,7 +8,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityModerator, CommunityModeratorForm}, - moderator::{ModTransferCommunity, ModTransferCommunityForm}, + mod_log::moderator::{ModTransferCommunity, ModTransferCommunityForm}, }, traits::{Crud, Joinable}, }; diff --git a/crates/api/src/lib.rs b/crates/api/src/lib.rs index 83979212d..6a2c94332 100644 --- a/crates/api/src/lib.rs +++ b/crates/api/src/lib.rs @@ -19,7 +19,7 @@ use lemmy_db_schema::{ CommunityPersonBanForm, }, local_site::LocalSite, - moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, person::Person, }, traits::{Bannable, Crud, Followable}, diff --git a/crates/api/src/local_user/add_admin.rs b/crates/api/src/local_user/add_admin.rs index 1e515952e..1e821bf3e 100644 --- a/crates/api/src/local_user/add_admin.rs +++ b/crates/api/src/local_user/add_admin.rs @@ -7,7 +7,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ local_user::{LocalUser, LocalUserUpdateForm}, - moderator::{ModAdd, ModAddForm}, + mod_log::moderator::{ModAdd, ModAddForm}, }, traits::Crud, }; diff --git a/crates/api/src/local_user/ban_person.rs b/crates/api/src/local_user/ban_person.rs index 9349cc632..f929433f0 100644 --- a/crates/api/src/local_user/ban_person.rs +++ b/crates/api/src/local_user/ban_person.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ source::{ local_user::LocalUser, login_token::LoginToken, - moderator::{ModBan, ModBanForm}, + mod_log::moderator::{ModBan, ModBanForm}, person::{Person, PersonUpdateForm}, }, traits::Crud, diff --git a/crates/api/src/post/feature.rs b/crates/api/src/post/feature.rs index 6fc2f443c..8ede8c31c 100644 --- a/crates/api/src/post/feature.rs +++ b/crates/api/src/post/feature.rs @@ -10,7 +10,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::Community, - moderator::{ModFeaturePost, ModFeaturePostForm}, + mod_log::moderator::{ModFeaturePost, ModFeaturePostForm}, post::{Post, PostUpdateForm}, }, traits::Crud, diff --git a/crates/api/src/post/lock.rs b/crates/api/src/post/lock.rs index 011770c2e..ad7fa7264 100644 --- a/crates/api/src/post/lock.rs +++ b/crates/api/src/post/lock.rs @@ -9,7 +9,7 @@ use lemmy_api_common::{ }; use lemmy_db_schema::{ source::{ - moderator::{ModLockPost, ModLockPostForm}, + mod_log::moderator::{ModLockPost, ModLockPostForm}, post::{Post, PostUpdateForm}, }, traits::Crud, diff --git a/crates/api/src/site/admin_allow_instance.rs b/crates/api/src/site/admin_allow_instance.rs new file mode 100644 index 000000000..81879ecae --- /dev/null +++ b/crates/api/src/site/admin_allow_instance.rs @@ -0,0 +1,53 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + site::AdminAllowInstanceParams, + utils::is_admin, + LemmyErrorType, + SuccessResponse, +}; +use lemmy_db_schema::source::{ + federation_allowlist::{FederationAllowList, FederationAllowListForm}, + instance::Instance, + mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn admin_allow_instance( + data: Json, + local_user_view: LocalUserView, + context: Data, +) -> LemmyResult> { + is_admin(&local_user_view)?; + + let blocklist = Instance::blocklist(&mut context.pool()).await?; + if !blocklist.is_empty() { + Err(LemmyErrorType::CannotCombineFederationBlocklistAndAllowlist)?; + } + + let instance_id = Instance::read_or_create(&mut context.pool(), data.instance.clone()) + .await? + .id; + let form = FederationAllowListForm { + instance_id, + updated: None, + }; + if data.allow { + FederationAllowList::allow(&mut context.pool(), &form).await?; + } else { + FederationAllowList::unallow(&mut context.pool(), instance_id).await?; + } + + let mod_log_form = AdminAllowInstanceForm { + instance_id, + admin_person_id: local_user_view.person.id, + reason: data.reason.clone(), + allowed: data.allow, + }; + AdminAllowInstance::insert(&mut context.pool(), &mod_log_form).await?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/site/admin_block_instance.rs b/crates/api/src/site/admin_block_instance.rs new file mode 100644 index 000000000..54962ccf3 --- /dev/null +++ b/crates/api/src/site/admin_block_instance.rs @@ -0,0 +1,56 @@ +use activitypub_federation::config::Data; +use actix_web::web::Json; +use lemmy_api_common::{ + context::LemmyContext, + site::AdminBlockInstanceParams, + utils::is_admin, + LemmyErrorType, + SuccessResponse, +}; +use lemmy_db_schema::source::{ + federation_blocklist::{FederationBlockList, FederationBlockListForm}, + instance::Instance, + mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm}, +}; +use lemmy_db_views::structs::LocalUserView; +use lemmy_utils::error::LemmyResult; + +#[tracing::instrument(skip(context))] +pub async fn admin_block_instance( + data: Json, + local_user_view: LocalUserView, + context: Data, +) -> LemmyResult> { + is_admin(&local_user_view)?; + + let allowlist = Instance::allowlist(&mut context.pool()).await?; + if !allowlist.is_empty() { + Err(LemmyErrorType::CannotCombineFederationBlocklistAndAllowlist)?; + } + + let instance_id = Instance::read_or_create(&mut context.pool(), data.instance.clone()) + .await? + .id; + let form = FederationBlockListForm { + instance_id, + expires: data.expires, + updated: None, + }; + + if data.block { + FederationBlockList::block(&mut context.pool(), &form).await?; + } else { + FederationBlockList::unblock(&mut context.pool(), instance_id).await?; + } + + let mod_log_form = AdminBlockInstanceForm { + instance_id, + admin_person_id: local_user_view.person.id, + blocked: data.block, + reason: data.reason.clone(), + when_: data.expires, + }; + AdminBlockInstance::insert(&mut context.pool(), &mod_log_form).await?; + + Ok(Json(SuccessResponse::default())) +} diff --git a/crates/api/src/site/leave_admin.rs b/crates/api/src/site/leave_admin.rs index 97ad7e2e5..86b80be3e 100644 --- a/crates/api/src/site/leave_admin.rs +++ b/crates/api/src/site/leave_admin.rs @@ -6,7 +6,7 @@ use lemmy_db_schema::{ language::Language, local_site_url_blocklist::LocalSiteUrlBlocklist, local_user::{LocalUser, LocalUserUpdateForm}, - moderator::{ModAdd, ModAddForm}, + mod_log::moderator::{ModAdd, ModAddForm}, oauth_provider::OAuthProvider, tagline::Tagline, }, diff --git a/crates/api/src/site/mod.rs b/crates/api/src/site/mod.rs index f18dea3d0..52e882bb3 100644 --- a/crates/api/src/site/mod.rs +++ b/crates/api/src/site/mod.rs @@ -1,7 +1,9 @@ -pub mod block; +pub mod admin_allow_instance; +pub mod admin_block_instance; pub mod federated_instances; pub mod leave_admin; pub mod list_all_media; pub mod mod_log; pub mod purge; pub mod registration_applications; +pub mod user_block_instance; diff --git a/crates/api/src/site/mod_log.rs b/crates/api/src/site/mod_log.rs index 8f5538566..bbf147666 100644 --- a/crates/api/src/site/mod_log.rs +++ b/crates/api/src/site/mod_log.rs @@ -7,6 +7,8 @@ use lemmy_api_common::{ use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType}; use lemmy_db_views::structs::LocalUserView; use lemmy_db_views_moderator::structs::{ + AdminAllowInstanceView, + AdminBlockInstanceView, AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView, @@ -121,6 +123,8 @@ pub async fn get_mod_log( admin_purged_communities, admin_purged_posts, admin_purged_comments, + admin_block_instance, + admin_allow_instance, ) = if data.community_id.is_none() { ( match type_ { @@ -161,6 +165,18 @@ pub async fn get_mod_log( } _ => Default::default(), }, + match type_ { + All | AdminBlockInstance if other_person_id.is_none() => { + AdminBlockInstanceView::list(&mut context.pool(), params).await? + } + _ => Default::default(), + }, + match type_ { + All | AdminAllowInstance if other_person_id.is_none() => { + AdminAllowInstanceView::list(&mut context.pool(), params).await? + } + _ => Default::default(), + }, ) } else { Default::default() @@ -183,5 +199,7 @@ pub async fn get_mod_log( admin_purged_posts, admin_purged_comments, hidden_communities, + admin_block_instance, + admin_allow_instance, })) } diff --git a/crates/api/src/site/purge/comment.rs b/crates/api/src/site/purge/comment.rs index ae79a835a..5208cc397 100644 --- a/crates/api/src/site/purge/comment.rs +++ b/crates/api/src/site/purge/comment.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ source::{ comment::Comment, local_user::LocalUser, - moderator::{AdminPurgeComment, AdminPurgeCommentForm}, + mod_log::admin::{AdminPurgeComment, AdminPurgeCommentForm}, }, traits::Crud, }; diff --git a/crates/api/src/site/purge/community.rs b/crates/api/src/site/purge/community.rs index f0252e303..c55f753dc 100644 --- a/crates/api/src/site/purge/community.rs +++ b/crates/api/src/site/purge/community.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ source::{ community::Community, local_user::LocalUser, - moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm}, + mod_log::admin::{AdminPurgeCommunity, AdminPurgeCommunityForm}, }, traits::Crud, }; diff --git a/crates/api/src/site/purge/person.rs b/crates/api/src/site/purge/person.rs index 6dad4ce65..0f15e7726 100644 --- a/crates/api/src/site/purge/person.rs +++ b/crates/api/src/site/purge/person.rs @@ -11,7 +11,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ local_user::LocalUser, - moderator::{AdminPurgePerson, AdminPurgePersonForm}, + mod_log::admin::{AdminPurgePerson, AdminPurgePersonForm}, person::{Person, PersonUpdateForm}, }, traits::Crud, diff --git a/crates/api/src/site/purge/post.rs b/crates/api/src/site/purge/post.rs index f808269e7..e726945f5 100644 --- a/crates/api/src/site/purge/post.rs +++ b/crates/api/src/site/purge/post.rs @@ -11,7 +11,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ local_user::LocalUser, - moderator::{AdminPurgePost, AdminPurgePostForm}, + mod_log::admin::{AdminPurgePost, AdminPurgePostForm}, post::Post, }, traits::Crud, diff --git a/crates/api/src/site/block.rs b/crates/api/src/site/user_block_instance.rs similarity index 79% rename from crates/api/src/site/block.rs rename to crates/api/src/site/user_block_instance.rs index 823dda612..940538833 100644 --- a/crates/api/src/site/block.rs +++ b/crates/api/src/site/user_block_instance.rs @@ -1,9 +1,6 @@ use activitypub_federation::config::Data; use actix_web::web::Json; -use lemmy_api_common::{ - context::LemmyContext, - site::{BlockInstance, BlockInstanceResponse}, -}; +use lemmy_api_common::{context::LemmyContext, site::UserBlockInstanceParams, SuccessResponse}; use lemmy_db_schema::{ source::instance_block::{InstanceBlock, InstanceBlockForm}, traits::Blockable, @@ -12,11 +9,11 @@ use lemmy_db_views::structs::LocalUserView; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; #[tracing::instrument(skip(context))] -pub async fn block_instance( - data: Json, +pub async fn user_block_instance( + data: Json, local_user_view: LocalUserView, context: Data, -) -> LemmyResult> { +) -> LemmyResult> { let instance_id = data.instance_id; let person_id = local_user_view.person.id; if local_user_view.person.instance_id == instance_id { @@ -38,7 +35,5 @@ pub async fn block_instance( .with_lemmy_type(LemmyErrorType::InstanceBlockAlreadyExists)?; } - Ok(Json(BlockInstanceResponse { - blocked: data.block, - })) + Ok(Json(SuccessResponse::default())) } diff --git a/crates/api_common/src/site.rs b/crates/api_common/src/site.rs index 91c6151d7..9babe423c 100644 --- a/crates/api_common/src/site.rs +++ b/crates/api_common/src/site.rs @@ -43,6 +43,8 @@ use lemmy_db_views_actor::structs::{ PersonView, }; use lemmy_db_views_moderator::structs::{ + AdminAllowInstanceView, + AdminBlockInstanceView, AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView, @@ -183,6 +185,8 @@ pub struct GetModlogResponse { pub admin_purged_posts: Vec, pub admin_purged_comments: Vec, pub hidden_communities: Vec, + pub admin_block_instance: Vec, + pub admin_allow_instance: Vec, } #[skip_serializing_none] @@ -265,10 +269,6 @@ pub struct CreateSite { #[cfg_attr(feature = "full", ts(optional))] pub captcha_difficulty: Option, #[cfg_attr(feature = "full", ts(optional))] - pub allowed_instances: Option>, - #[cfg_attr(feature = "full", ts(optional))] - pub blocked_instances: Option>, - #[cfg_attr(feature = "full", ts(optional))] pub registration_mode: Option, #[cfg_attr(feature = "full", ts(optional))] pub oauth_registration: Option, @@ -394,12 +394,6 @@ pub struct EditSite { /// The captcha difficulty. Can be easy, medium, or hard #[cfg_attr(feature = "full", ts(optional))] pub captcha_difficulty: Option, - /// A list of allowed instances. If none are set, federation is open. - #[cfg_attr(feature = "full", ts(optional))] - pub allowed_instances: Option>, - /// A list of blocked instances. - #[cfg_attr(feature = "full", ts(optional))] - pub blocked_instances: Option>, /// A list of blocked URLs #[cfg_attr(feature = "full", ts(optional))] pub blocked_urls: Option>, @@ -648,15 +642,29 @@ pub struct GetUnreadRegistrationApplicationCountResponse { #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] /// Block an instance as user -pub struct BlockInstance { +pub struct UserBlockInstanceParams { pub instance_id: InstanceId, pub block: bool, } -#[skip_serializing_none] -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] #[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", ts(export))] -pub struct BlockInstanceResponse { - pub blocked: bool, +pub struct AdminBlockInstanceParams { + pub instance: String, + pub block: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub expires: Option>, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] +#[cfg_attr(feature = "full", derive(TS))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminAllowInstanceParams { + pub instance: String, + pub allow: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, } diff --git a/crates/api_common/src/utils.rs b/crates/api_common/src/utils.rs index d46e57749..3b8a00197 100644 --- a/crates/api_common/src/utils.rs +++ b/crates/api_common/src/utils.rs @@ -23,7 +23,12 @@ use lemmy_db_schema::{ local_site::LocalSite, local_site_rate_limit::LocalSiteRateLimit, local_site_url_blocklist::LocalSiteUrlBlocklist, - moderator::{ModRemoveComment, ModRemoveCommentForm, ModRemovePost, ModRemovePostForm}, + mod_log::moderator::{ + ModRemoveComment, + ModRemoveCommentForm, + ModRemovePost, + ModRemovePostForm, + }, oauth_account::OAuthAccount, password_reset_request::PasswordResetRequest, person::{Person, PersonUpdateForm}, diff --git a/crates/api_crud/src/comment/remove.rs b/crates/api_crud/src/comment/remove.rs index 4e8a1871a..1ac6201e8 100644 --- a/crates/api_crud/src/comment/remove.rs +++ b/crates/api_crud/src/comment/remove.rs @@ -12,7 +12,7 @@ use lemmy_db_schema::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, local_user::LocalUser, - moderator::{ModRemoveComment, ModRemoveCommentForm}, + mod_log::moderator::{ModRemoveComment, ModRemoveCommentForm}, }, traits::{Crud, Reportable}, }; diff --git a/crates/api_crud/src/community/remove.rs b/crates/api_crud/src/community/remove.rs index c506bde1b..7dc78a37a 100644 --- a/crates/api_crud/src/community/remove.rs +++ b/crates/api_crud/src/community/remove.rs @@ -10,7 +10,7 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ community::{Community, CommunityUpdateForm}, - moderator::{ModRemoveCommunity, ModRemoveCommunityForm}, + mod_log::moderator::{ModRemoveCommunity, ModRemoveCommunityForm}, }, traits::Crud, }; diff --git a/crates/api_crud/src/post/remove.rs b/crates/api_crud/src/post/remove.rs index 7e3261e6f..95aa5fc56 100644 --- a/crates/api_crud/src/post/remove.rs +++ b/crates/api_crud/src/post/remove.rs @@ -11,7 +11,7 @@ use lemmy_db_schema::{ source::{ community::Community, local_user::LocalUser, - moderator::{ModRemovePost, ModRemovePostForm}, + mod_log::moderator::{ModRemovePost, ModRemovePostForm}, post::{Post, PostUpdateForm}, post_report::PostReport, }, diff --git a/crates/api_crud/src/site/update.rs b/crates/api_crud/src/site/update.rs index 6c23adfb4..d2585ea43 100644 --- a/crates/api_crud/src/site/update.rs +++ b/crates/api_crud/src/site/update.rs @@ -19,8 +19,6 @@ use lemmy_api_common::{ use lemmy_db_schema::{ source::{ actor_language::SiteLanguage, - federation_allowlist::FederationAllowList, - federation_blocklist::FederationBlockList, local_site::{LocalSite, LocalSiteUpdateForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, local_site_url_blocklist::LocalSiteUrlBlocklist, @@ -152,12 +150,6 @@ pub async fn update_site( .await .ok(); - // Replace the blocked and allowed instances - let allowed = data.allowed_instances.clone(); - FederationAllowList::replace(&mut context.pool(), allowed).await?; - let blocked = data.blocked_instances.clone(); - FederationBlockList::replace(&mut context.pool(), blocked).await?; - if let Some(url_blocklist) = data.blocked_urls.clone() { let parsed_urls = check_urls_are_valid(&url_blocklist)?; LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?; diff --git a/crates/apub/src/activities/block/block_user.rs b/crates/apub/src/activities/block/block_user.rs index 866e1cc6c..64c402482 100644 --- a/crates/apub/src/activities/block/block_user.rs +++ b/crates/apub/src/activities/block/block_user.rs @@ -36,7 +36,7 @@ use lemmy_db_schema::{ CommunityPersonBan, CommunityPersonBanForm, }, - moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, person::{Person, PersonUpdateForm}, }, traits::{Bannable, Crud, Followable}, diff --git a/crates/apub/src/activities/block/undo_block_user.rs b/crates/apub/src/activities/block/undo_block_user.rs index 29fc22f0c..122eae429 100644 --- a/crates/apub/src/activities/block/undo_block_user.rs +++ b/crates/apub/src/activities/block/undo_block_user.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::{CommunityPersonBan, CommunityPersonBanForm}, - moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, + mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, person::{Person, PersonUpdateForm}, }, traits::{Bannable, Crud}, diff --git a/crates/apub/src/activities/community/collection_add.rs b/crates/apub/src/activities/community/collection_add.rs index ae508c2c5..1014229c8 100644 --- a/crates/apub/src/activities/community/collection_add.rs +++ b/crates/apub/src/activities/community/collection_add.rs @@ -31,7 +31,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::{Community, CommunityModerator, CommunityModeratorForm}, - moderator::{ModAddCommunity, ModAddCommunityForm}, + mod_log::moderator::{ModAddCommunity, ModAddCommunityForm}, person::Person, post::{Post, PostUpdateForm}, }, diff --git a/crates/apub/src/activities/community/collection_remove.rs b/crates/apub/src/activities/community/collection_remove.rs index 6c08735ed..c94286703 100644 --- a/crates/apub/src/activities/community/collection_remove.rs +++ b/crates/apub/src/activities/community/collection_remove.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::{Community, CommunityModerator, CommunityModeratorForm}, - moderator::{ModAddCommunity, ModAddCommunityForm}, + mod_log::moderator::{ModAddCommunity, ModAddCommunityForm}, post::{Post, PostUpdateForm}, }, traits::{Crud, Joinable}, diff --git a/crates/apub/src/activities/community/lock_page.rs b/crates/apub/src/activities/community/lock_page.rs index a9bacea8a..af6a5796f 100644 --- a/crates/apub/src/activities/community/lock_page.rs +++ b/crates/apub/src/activities/community/lock_page.rs @@ -27,7 +27,7 @@ use lemmy_db_schema::{ source::{ activity::ActivitySendTargets, community::Community, - moderator::{ModLockPost, ModLockPostForm}, + mod_log::moderator::{ModLockPost, ModLockPostForm}, person::Person, post::{Post, PostUpdateForm}, }, diff --git a/crates/apub/src/activities/deletion/delete.rs b/crates/apub/src/activities/deletion/delete.rs index 064f0bc82..4ad24d966 100644 --- a/crates/apub/src/activities/deletion/delete.rs +++ b/crates/apub/src/activities/deletion/delete.rs @@ -14,7 +14,7 @@ use lemmy_db_schema::{ comment::{Comment, CommentUpdateForm}, comment_report::CommentReport, community::{Community, CommunityUpdateForm}, - moderator::{ + mod_log::moderator::{ ModRemoveComment, ModRemoveCommentForm, ModRemoveCommunity, diff --git a/crates/apub/src/activities/deletion/undo_delete.rs b/crates/apub/src/activities/deletion/undo_delete.rs index f4a7bb9b9..b30b22fd4 100644 --- a/crates/apub/src/activities/deletion/undo_delete.rs +++ b/crates/apub/src/activities/deletion/undo_delete.rs @@ -13,7 +13,7 @@ use lemmy_db_schema::{ source::{ comment::{Comment, CommentUpdateForm}, community::{Community, CommunityUpdateForm}, - moderator::{ + mod_log::moderator::{ ModRemoveComment, ModRemoveCommentForm, ModRemoveCommunity, diff --git a/crates/apub/src/api/user_settings_backup.rs b/crates/apub/src/api/user_settings_backup.rs index d5a864bec..6184df7d3 100644 --- a/crates/apub/src/api/user_settings_backup.rs +++ b/crates/apub/src/api/user_settings_backup.rs @@ -322,7 +322,7 @@ pub(crate) mod tests { CommunityFollowerState, CommunityInsertForm, }, - local_user::LocalUser, + person::Person, }, traits::{Crud, Followable}, }; @@ -376,8 +376,8 @@ pub(crate) mod tests { assert_eq!(follows.len(), 1); assert_eq!(follows[0].community.actor_id, community.actor_id); - LocalUser::delete(pool, export_user.local_user.id).await?; - LocalUser::delete(pool, import_user.local_user.id).await?; + Person::delete(pool, export_user.person.id).await?; + Person::delete(pool, import_user.person.id).await?; Ok(()) } @@ -412,8 +412,8 @@ pub(crate) mod tests { Some(LemmyErrorType::TooManyItems) ); - LocalUser::delete(pool, export_user.local_user.id).await?; - LocalUser::delete(pool, import_user.local_user.id).await?; + Person::delete(pool, export_user.person.id).await?; + Person::delete(pool, import_user.person.id).await?; Ok(()) } diff --git a/crates/db_schema/src/impls/federation_allowlist.rs b/crates/db_schema/src/impls/federation_allowlist.rs index 099e0b231..41ced26f7 100644 --- a/crates/db_schema/src/impls/federation_allowlist.rs +++ b/crates/db_schema/src/impls/federation_allowlist.rs @@ -1,60 +1,51 @@ use crate::{ - schema::federation_allowlist, + newtypes::InstanceId, + schema::{admin_allow_instance, federation_allowlist}, source::{ federation_allowlist::{FederationAllowList, FederationAllowListForm}, - instance::Instance, + mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm}, }, utils::{get_conn, DbPool}, }; -use diesel::{dsl::insert_into, result::Error}; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use diesel::{delete, dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; -impl FederationAllowList { - pub async fn replace(pool: &mut DbPool<'_>, list_opt: Option>) -> Result<(), Error> { +impl AdminAllowInstance { + pub async fn insert(pool: &mut DbPool<'_>, form: &AdminAllowInstanceForm) -> Result<(), Error> { let conn = &mut get_conn(pool).await?; - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - if let Some(list) = list_opt { - Self::clear(conn).await?; - - for domain in list { - // Upsert all of these as instances - let instance = Instance::read_or_create(&mut conn.into(), domain).await?; - - let form = FederationAllowListForm { - instance_id: instance.id, - updated: None, - }; - insert_into(federation_allowlist::table) - .values(form) - .get_result::(conn) - .await?; - } - Ok(()) - } else { - Ok(()) - } - }) as _ - }) - .await - } - - async fn clear(conn: &mut AsyncPgConnection) -> Result { - diesel::delete(federation_allowlist::table) + insert_into(admin_allow_instance::table) + .values(form) .execute(conn) - .await + .await?; + + Ok(()) } } + +impl FederationAllowList { + pub async fn allow(pool: &mut DbPool<'_>, form: &FederationAllowListForm) -> Result<(), Error> { + let conn = &mut get_conn(pool).await?; + insert_into(federation_allowlist::table) + .values(form) + .execute(conn) + .await?; + Ok(()) + } + pub async fn unallow(pool: &mut DbPool<'_>, instance_id_: InstanceId) -> Result<(), Error> { + use federation_allowlist::dsl::instance_id; + let conn = &mut get_conn(pool).await?; + delete(federation_allowlist::table.filter(instance_id.eq(instance_id_))) + .execute(conn) + .await?; + Ok(()) + } +} + #[cfg(test)] mod tests { - use crate::{ - source::{federation_allowlist::FederationAllowList, instance::Instance}, - utils::build_db_pool_for_tests, - }; - use diesel::result::Error; + use super::*; + use crate::{source::instance::Instance, utils::build_db_pool_for_tests}; use pretty_assertions::assert_eq; use serial_test::serial; @@ -63,31 +54,33 @@ mod tests { async fn test_allowlist_insert_and_clear() -> Result<(), Error> { let pool = &build_db_pool_for_tests(); let pool = &mut pool.into(); - let domains = vec![ - "tld1.xyz".to_string(), - "tld2.xyz".to_string(), - "tld3.xyz".to_string(), + let instances = vec![ + Instance::read_or_create(pool, "tld1.xyz".to_string()).await?, + Instance::read_or_create(pool, "tld2.xyz".to_string()).await?, + Instance::read_or_create(pool, "tld3.xyz".to_string()).await?, ]; + let forms: Vec<_> = instances + .iter() + .map(|i| FederationAllowListForm { + instance_id: i.id, + updated: None, + }) + .collect(); - let allowed = Some(domains.clone()); - - FederationAllowList::replace(pool, allowed).await?; + for f in &forms { + FederationAllowList::allow(pool, f).await?; + } let allows = Instance::allowlist(pool).await?; - let allows_domains = allows - .iter() - .map(|i| i.domain.clone()) - .collect::>(); assert_eq!(3, allows.len()); - assert_eq!(domains, allows_domains); + assert_eq!(instances, allows); - // Now test clearing them via Some(empty vec) - let clear_allows = Some(Vec::new()); - - FederationAllowList::replace(pool, clear_allows).await?; + // Now test clearing them + for f in forms { + FederationAllowList::unallow(pool, f.instance_id).await?; + } let allows = Instance::allowlist(pool).await?; - assert_eq!(0, allows.len()); Instance::delete_all(pool).await?; diff --git a/crates/db_schema/src/impls/federation_blocklist.rs b/crates/db_schema/src/impls/federation_blocklist.rs index 2a6e0671d..4a42e81b6 100644 --- a/crates/db_schema/src/impls/federation_blocklist.rs +++ b/crates/db_schema/src/impls/federation_blocklist.rs @@ -1,49 +1,42 @@ use crate::{ - schema::federation_blocklist, + newtypes::InstanceId, + schema::{admin_block_instance, federation_blocklist}, source::{ federation_blocklist::{FederationBlockList, FederationBlockListForm}, - instance::Instance, + mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm}, }, utils::{get_conn, DbPool}, }; -use diesel::{dsl::insert_into, result::Error}; -use diesel_async::{AsyncPgConnection, RunQueryDsl}; +use diesel::{delete, dsl::insert_into, result::Error, ExpressionMethods, QueryDsl}; +use diesel_async::RunQueryDsl; -impl FederationBlockList { - pub async fn replace(pool: &mut DbPool<'_>, list_opt: Option>) -> Result<(), Error> { +impl AdminBlockInstance { + pub async fn insert(pool: &mut DbPool<'_>, form: &AdminBlockInstanceForm) -> Result<(), Error> { let conn = &mut get_conn(pool).await?; - conn - .build_transaction() - .run(|conn| { - Box::pin(async move { - if let Some(list) = list_opt { - Self::clear(conn).await?; - - for domain in list { - // Upsert all of these as instances - let instance = Instance::read_or_create(&mut conn.into(), domain).await?; - - let form = FederationBlockListForm { - instance_id: instance.id, - updated: None, - }; - insert_into(federation_blocklist::table) - .values(form) - .get_result::(conn) - .await?; - } - Ok(()) - } else { - Ok(()) - } - }) as _ - }) - .await - } - - async fn clear(conn: &mut AsyncPgConnection) -> Result { - diesel::delete(federation_blocklist::table) + insert_into(admin_block_instance::table) + .values(form) .execute(conn) - .await + .await?; + + Ok(()) + } +} + +impl FederationBlockList { + pub async fn block(pool: &mut DbPool<'_>, form: &FederationBlockListForm) -> Result<(), Error> { + let conn = &mut get_conn(pool).await?; + insert_into(federation_blocklist::table) + .values(form) + .execute(conn) + .await?; + Ok(()) + } + pub async fn unblock(pool: &mut DbPool<'_>, instance_id_: InstanceId) -> Result<(), Error> { + use federation_blocklist::dsl::instance_id; + let conn = &mut get_conn(pool).await?; + delete(federation_blocklist::table.filter(instance_id.eq(instance_id_))) + .execute(conn) + .await?; + Ok(()) } } diff --git a/crates/db_schema/src/impls/mod.rs b/crates/db_schema/src/impls/mod.rs index f115a101f..d4ea47800 100644 --- a/crates/db_schema/src/impls/mod.rs +++ b/crates/db_schema/src/impls/mod.rs @@ -21,7 +21,7 @@ pub mod local_site_url_blocklist; pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; -pub mod moderator; +pub mod mod_log; pub mod oauth_account; pub mod oauth_provider; pub mod password_reset_request; diff --git a/crates/db_schema/src/impls/mod_log/admin.rs b/crates/db_schema/src/impls/mod_log/admin.rs new file mode 100644 index 000000000..c1b2bf69f --- /dev/null +++ b/crates/db_schema/src/impls/mod_log/admin.rs @@ -0,0 +1,132 @@ +use crate::{ + source::mod_log::admin::{ + AdminPurgeComment, + AdminPurgeCommentForm, + AdminPurgeCommunity, + AdminPurgeCommunityForm, + AdminPurgePerson, + AdminPurgePersonForm, + AdminPurgePost, + AdminPurgePostForm, + }, + traits::Crud, + utils::{get_conn, DbPool}, +}; +use diesel::{dsl::insert_into, result::Error, QueryDsl}; +use diesel_async::RunQueryDsl; + +#[async_trait] +impl Crud for AdminPurgePerson { + type InsertForm = AdminPurgePersonForm; + type UpdateForm = AdminPurgePersonForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_person::dsl::admin_purge_person; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_person) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_person::dsl::admin_purge_person; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_person.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminPurgeCommunity { + type InsertForm = AdminPurgeCommunityForm; + type UpdateForm = AdminPurgeCommunityForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_community::dsl::admin_purge_community; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_community) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_community::dsl::admin_purge_community; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_community.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminPurgePost { + type InsertForm = AdminPurgePostForm; + type UpdateForm = AdminPurgePostForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_post::dsl::admin_purge_post; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_post) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_post::dsl::admin_purge_post; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_post.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} + +#[async_trait] +impl Crud for AdminPurgeComment { + type InsertForm = AdminPurgeCommentForm; + type UpdateForm = AdminPurgeCommentForm; + type IdType = i32; + + async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { + use crate::schema::admin_purge_comment::dsl::admin_purge_comment; + let conn = &mut get_conn(pool).await?; + insert_into(admin_purge_comment) + .values(form) + .get_result::(conn) + .await + } + + async fn update( + pool: &mut DbPool<'_>, + from_id: i32, + form: &Self::InsertForm, + ) -> Result { + use crate::schema::admin_purge_comment::dsl::admin_purge_comment; + let conn = &mut get_conn(pool).await?; + diesel::update(admin_purge_comment.find(from_id)) + .set(form) + .get_result::(conn) + .await + } +} diff --git a/crates/db_schema/src/impls/mod_log/mod.rs b/crates/db_schema/src/impls/mod_log/mod.rs new file mode 100644 index 000000000..54341c69a --- /dev/null +++ b/crates/db_schema/src/impls/mod_log/mod.rs @@ -0,0 +1,2 @@ +pub mod admin; +pub mod moderator; diff --git a/crates/db_schema/src/impls/moderator.rs b/crates/db_schema/src/impls/mod_log/moderator.rs similarity index 82% rename from crates/db_schema/src/impls/moderator.rs rename to crates/db_schema/src/impls/mod_log/moderator.rs index 8deb56258..37b66480d 100644 --- a/crates/db_schema/src/impls/moderator.rs +++ b/crates/db_schema/src/impls/mod_log/moderator.rs @@ -1,13 +1,5 @@ use crate::{ - source::moderator::{ - AdminPurgeComment, - AdminPurgeCommentForm, - AdminPurgeCommunity, - AdminPurgeCommunityForm, - AdminPurgePerson, - AdminPurgePersonForm, - AdminPurgePost, - AdminPurgePostForm, + source::mod_log::moderator::{ ModAdd, ModAddCommunity, ModAddCommunityForm, @@ -376,157 +368,20 @@ impl Crud for ModAdd { } } -#[async_trait] -impl Crud for AdminPurgePerson { - type InsertForm = AdminPurgePersonForm; - type UpdateForm = AdminPurgePersonForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_person::dsl::admin_purge_person; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_person) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_person::dsl::admin_purge_person; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_person.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - -#[async_trait] -impl Crud for AdminPurgeCommunity { - type InsertForm = AdminPurgeCommunityForm; - type UpdateForm = AdminPurgeCommunityForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_community::dsl::admin_purge_community; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_community) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_community::dsl::admin_purge_community; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_community.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - -#[async_trait] -impl Crud for AdminPurgePost { - type InsertForm = AdminPurgePostForm; - type UpdateForm = AdminPurgePostForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_post::dsl::admin_purge_post; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_post) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_post::dsl::admin_purge_post; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_post.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - -#[async_trait] -impl Crud for AdminPurgeComment { - type InsertForm = AdminPurgeCommentForm; - type UpdateForm = AdminPurgeCommentForm; - type IdType = i32; - - async fn create(pool: &mut DbPool<'_>, form: &Self::InsertForm) -> Result { - use crate::schema::admin_purge_comment::dsl::admin_purge_comment; - let conn = &mut get_conn(pool).await?; - insert_into(admin_purge_comment) - .values(form) - .get_result::(conn) - .await - } - - async fn update( - pool: &mut DbPool<'_>, - from_id: i32, - form: &Self::InsertForm, - ) -> Result { - use crate::schema::admin_purge_comment::dsl::admin_purge_comment; - let conn = &mut get_conn(pool).await?; - diesel::update(admin_purge_comment.find(from_id)) - .set(form) - .get_result::(conn) - .await - } -} - #[cfg(test)] mod tests { + use super::*; use crate::{ source::{ comment::{Comment, CommentInsertForm}, community::{Community, CommunityInsertForm}, instance::Instance, - moderator::{ - ModAdd, - ModAddCommunity, - ModAddCommunityForm, - ModAddForm, - ModBan, - ModBanForm, - ModBanFromCommunity, - ModBanFromCommunityForm, - ModFeaturePost, - ModFeaturePostForm, - ModLockPost, - ModLockPostForm, - ModRemoveComment, - ModRemoveCommentForm, - ModRemoveCommunity, - ModRemoveCommunityForm, - ModRemovePost, - ModRemovePostForm, - }, person::{Person, PersonInsertForm}, post::{Post, PostInsertForm}, }, - traits::Crud, utils::build_db_pool_for_tests, }; - use diesel::result::Error; use pretty_assertions::assert_eq; use serial_test::serial; diff --git a/crates/db_schema/src/lib.rs b/crates/db_schema/src/lib.rs index 7ee60cc1e..ad6e93619 100644 --- a/crates/db_schema/src/lib.rs +++ b/crates/db_schema/src/lib.rs @@ -215,6 +215,8 @@ pub enum ModlogActionType { AdminPurgeCommunity, AdminPurgePost, AdminPurgeComment, + AdminBlockInstance, + AdminAllowInstance, } #[derive( diff --git a/crates/db_schema/src/schema.rs b/crates/db_schema/src/schema.rs index f2b186d35..0bc046ece 100644 --- a/crates/db_schema/src/schema.rs +++ b/crates/db_schema/src/schema.rs @@ -42,6 +42,29 @@ pub mod sql_types { pub struct RegistrationModeEnum; } +diesel::table! { + admin_allow_instance (id) { + id -> Int4, + instance_id -> Int4, + admin_person_id -> Int4, + allowed -> Bool, + reason -> Nullable, + when_ -> Timestamptz, + } +} + +diesel::table! { + admin_block_instance (id) { + id -> Int4, + instance_id -> Int4, + admin_person_id -> Int4, + blocked -> Bool, + reason -> Nullable, + expires -> Nullable, + when_ -> Timestamptz, + } +} + diesel::table! { admin_purge_comment (id) { id -> Int4, @@ -284,6 +307,7 @@ diesel::table! { instance_id -> Int4, published -> Timestamptz, updated -> Nullable, + expires -> Nullable, } } @@ -935,6 +959,10 @@ diesel::table! { } } +diesel::joinable!(admin_allow_instance -> instance (instance_id)); +diesel::joinable!(admin_allow_instance -> person (admin_person_id)); +diesel::joinable!(admin_block_instance -> instance (instance_id)); +diesel::joinable!(admin_block_instance -> person (admin_person_id)); diesel::joinable!(admin_purge_comment -> person (admin_person_id)); diesel::joinable!(admin_purge_comment -> post (post_id)); diesel::joinable!(admin_purge_community -> person (admin_person_id)); @@ -1012,6 +1040,8 @@ diesel::joinable!(site_language -> language (language_id)); diesel::joinable!(site_language -> site (site_id)); diesel::allow_tables_to_appear_in_same_query!( + admin_allow_instance, + admin_block_instance, admin_purge_comment, admin_purge_community, admin_purge_person, diff --git a/crates/db_schema/src/source/federation_blocklist.rs b/crates/db_schema/src/source/federation_blocklist.rs index 2176ce42d..df877facf 100644 --- a/crates/db_schema/src/source/federation_blocklist.rs +++ b/crates/db_schema/src/source/federation_blocklist.rs @@ -1,14 +1,14 @@ use crate::newtypes::InstanceId; -#[cfg(feature = "full")] -use crate::schema::federation_blocklist; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fmt::Debug; +#[cfg(feature = "full")] +use {crate::schema::federation_blocklist, ts_rs::TS}; #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[cfg_attr( feature = "full", - derive(Queryable, Selectable, Associations, Identifiable) + derive(TS, Queryable, Selectable, Associations, Identifiable) )] #[cfg_attr( feature = "full", @@ -17,10 +17,14 @@ use std::fmt::Debug; #[cfg_attr(feature = "full", diesel(table_name = federation_blocklist))] #[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] pub struct FederationBlockList { pub instance_id: InstanceId, pub published: DateTime, + #[cfg_attr(feature = "full", ts(optional))] pub updated: Option>, + #[cfg_attr(feature = "full", ts(optional))] + pub expires: Option>, } #[derive(Clone, Default)] @@ -29,4 +33,5 @@ pub struct FederationBlockList { pub struct FederationBlockListForm { pub instance_id: InstanceId, pub updated: Option>, + pub expires: Option>, } diff --git a/crates/db_schema/src/source/mod.rs b/crates/db_schema/src/source/mod.rs index 5082ddbd1..86def9691 100644 --- a/crates/db_schema/src/source/mod.rs +++ b/crates/db_schema/src/source/mod.rs @@ -26,7 +26,7 @@ pub mod local_site_url_blocklist; pub mod local_user; pub mod local_user_vote_display_mode; pub mod login_token; -pub mod moderator; +pub mod mod_log; pub mod oauth_account; pub mod oauth_provider; pub mod password_reset_request; diff --git a/crates/db_schema/src/source/mod_log/admin.rs b/crates/db_schema/src/source/mod_log/admin.rs new file mode 100644 index 000000000..d6e48b8ee --- /dev/null +++ b/crates/db_schema/src/source/mod_log/admin.rs @@ -0,0 +1,176 @@ +use crate::newtypes::{CommunityId, InstanceId, PersonId, PostId}; +#[cfg(feature = "full")] +use crate::schema::{ + admin_allow_instance, + admin_block_instance, + admin_purge_comment, + admin_purge_community, + admin_purge_person, + admin_purge_post, +}; +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(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a person. +pub struct AdminPurgePerson { + pub id: i32, + pub admin_person_id: PersonId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] +pub struct AdminPurgePersonForm { + pub admin_person_id: PersonId, + pub reason: Option, +} + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a community. +pub struct AdminPurgeCommunity { + pub id: i32, + pub admin_person_id: PersonId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] +pub struct AdminPurgeCommunityForm { + pub admin_person_id: PersonId, + pub reason: Option, +} + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a post. +pub struct AdminPurgePost { + pub id: i32, + pub admin_person_id: PersonId, + pub community_id: CommunityId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] +pub struct AdminPurgePostForm { + pub admin_person_id: PersonId, + pub community_id: CommunityId, + pub reason: Option, +} + +#[skip_serializing_none] +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +/// When an admin purges a comment. +pub struct AdminPurgeComment { + pub id: i32, + pub admin_person_id: PersonId, + pub post_id: PostId, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] +pub struct AdminPurgeCommentForm { + pub admin_person_id: PersonId, + pub post_id: PostId, + pub reason: Option, +} + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr( + feature = "full", + derive(TS, Queryable, Selectable, Associations, Identifiable) +)] +#[cfg_attr( + feature = "full", + diesel(belongs_to(crate::source::instance::Instance)) +)] +#[cfg_attr(feature = "full", diesel(table_name = admin_allow_instance))] +#[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminAllowInstance { + pub id: i32, + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub allowed: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + pub when_: DateTime, +} + +#[derive(Clone, Default)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_allow_instance))] +pub struct AdminAllowInstanceForm { + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub allowed: bool, + pub reason: Option, +} + +#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] +#[cfg_attr( + feature = "full", + derive(TS, Queryable, Selectable, Associations, Identifiable) +)] +#[cfg_attr( + feature = "full", + diesel(belongs_to(crate::source::instance::Instance)) +)] +#[cfg_attr(feature = "full", diesel(table_name = admin_block_instance))] +#[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] +#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] +#[cfg_attr(feature = "full", ts(export))] +pub struct AdminBlockInstance { + pub id: i32, + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub blocked: bool, + #[cfg_attr(feature = "full", ts(optional))] + pub reason: Option, + #[cfg_attr(feature = "full", ts(optional))] + pub expires: Option>, + pub when_: DateTime, +} + +#[derive(Clone, Default)] +#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] +#[cfg_attr(feature = "full", diesel(table_name = admin_block_instance))] +pub struct AdminBlockInstanceForm { + pub instance_id: InstanceId, + pub admin_person_id: PersonId, + pub blocked: bool, + pub reason: Option, + pub when_: Option>, +} diff --git a/crates/db_schema/src/source/mod_log/mod.rs b/crates/db_schema/src/source/mod_log/mod.rs new file mode 100644 index 000000000..54341c69a --- /dev/null +++ b/crates/db_schema/src/source/mod_log/mod.rs @@ -0,0 +1,2 @@ +pub mod admin; +pub mod moderator; diff --git a/crates/db_schema/src/source/moderator.rs b/crates/db_schema/src/source/mod_log/moderator.rs similarity index 75% rename from crates/db_schema/src/source/moderator.rs rename to crates/db_schema/src/source/mod_log/moderator.rs index b4fdcc676..470b643a5 100644 --- a/crates/db_schema/src/source/moderator.rs +++ b/crates/db_schema/src/source/mod_log/moderator.rs @@ -1,10 +1,6 @@ use crate::newtypes::{CommentId, CommunityId, PersonId, PostId}; #[cfg(feature = "full")] use crate::schema::{ - admin_purge_comment, - admin_purge_community, - admin_purge_person, - admin_purge_post, mod_add, mod_add_community, mod_ban, @@ -300,95 +296,3 @@ pub struct ModAddForm { pub other_person_id: PersonId, pub removed: Option, } - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a person. -pub struct AdminPurgePerson { - pub id: i32, - pub admin_person_id: PersonId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_person))] -pub struct AdminPurgePersonForm { - pub admin_person_id: PersonId, - pub reason: Option, -} - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a community. -pub struct AdminPurgeCommunity { - pub id: i32, - pub admin_person_id: PersonId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_community))] -pub struct AdminPurgeCommunityForm { - pub admin_person_id: PersonId, - pub reason: Option, -} - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a post. -pub struct AdminPurgePost { - pub id: i32, - pub admin_person_id: PersonId, - pub community_id: CommunityId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_post))] -pub struct AdminPurgePostForm { - pub admin_person_id: PersonId, - pub community_id: CommunityId, - pub reason: Option, -} - -#[skip_serializing_none] -#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] -#[cfg_attr(feature = "full", derive(Queryable, Selectable, Identifiable, TS))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] -#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] -#[cfg_attr(feature = "full", ts(export))] -/// When an admin purges a comment. -pub struct AdminPurgeComment { - pub id: i32, - pub admin_person_id: PersonId, - pub post_id: PostId, - #[cfg_attr(feature = "full", ts(optional))] - pub reason: Option, - pub when_: DateTime, -} - -#[cfg_attr(feature = "full", derive(Insertable, AsChangeset))] -#[cfg_attr(feature = "full", diesel(table_name = admin_purge_comment))] -pub struct AdminPurgeCommentForm { - pub admin_person_id: PersonId, - pub post_id: PostId, - pub reason: Option, -} diff --git a/crates/db_views_moderator/src/admin_allow_instance.rs b/crates/db_views_moderator/src/admin_allow_instance.rs new file mode 100644 index 000000000..2a0aaad14 --- /dev/null +++ b/crates/db_views_moderator/src/admin_allow_instance.rs @@ -0,0 +1,52 @@ +use crate::structs::{AdminAllowInstanceView, ModlogListParams}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::PersonId, + schema::{admin_allow_instance, instance, person}, + utils::{get_conn, limit_and_offset, DbPool}, +}; + +impl AdminAllowInstanceView { + pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + + let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); + let show_mod_names = !params.hide_modlog_names; + let show_mod_names_expr = show_mod_names.as_sql::(); + + let admin_names_join = admin_allow_instance::admin_person_id + .eq(person::id) + .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); + let mut query = admin_allow_instance::table + .left_join(person::table.on(admin_names_join)) + .inner_join(instance::table) + .select(( + admin_allow_instance::all_columns, + instance::all_columns, + person::all_columns.nullable(), + )) + .into_boxed(); + + if let Some(admin_person_id) = params.mod_person_id { + query = query.filter(admin_allow_instance::admin_person_id.eq(admin_person_id)); + }; + + let (limit, offset) = limit_and_offset(params.page, params.limit)?; + + query + .limit(limit) + .offset(offset) + .order_by(admin_allow_instance::when_.desc()) + .load::(conn) + .await + } +} diff --git a/crates/db_views_moderator/src/admin_block_instance.rs b/crates/db_views_moderator/src/admin_block_instance.rs new file mode 100644 index 000000000..e9d7c8b0d --- /dev/null +++ b/crates/db_views_moderator/src/admin_block_instance.rs @@ -0,0 +1,52 @@ +use crate::structs::{AdminBlockInstanceView, ModlogListParams}; +use diesel::{ + result::Error, + BoolExpressionMethods, + ExpressionMethods, + IntoSql, + JoinOnDsl, + NullableExpressionMethods, + QueryDsl, +}; +use diesel_async::RunQueryDsl; +use lemmy_db_schema::{ + newtypes::PersonId, + schema::{admin_block_instance, instance, person}, + utils::{get_conn, limit_and_offset, DbPool}, +}; + +impl AdminBlockInstanceView { + pub async fn list(pool: &mut DbPool<'_>, params: ModlogListParams) -> Result, Error> { + let conn = &mut get_conn(pool).await?; + + let admin_person_id_join = params.mod_person_id.unwrap_or(PersonId(-1)); + let show_mod_names = !params.hide_modlog_names; + let show_mod_names_expr = show_mod_names.as_sql::(); + + let admin_names_join = admin_block_instance::admin_person_id + .eq(person::id) + .and(show_mod_names_expr.or(person::id.eq(admin_person_id_join))); + let mut query = admin_block_instance::table + .left_join(person::table.on(admin_names_join)) + .inner_join(instance::table) + .select(( + admin_block_instance::all_columns, + instance::all_columns, + person::all_columns.nullable(), + )) + .into_boxed(); + + if let Some(admin_person_id) = params.mod_person_id { + query = query.filter(admin_block_instance::admin_person_id.eq(admin_person_id)); + }; + + let (limit, offset) = limit_and_offset(params.page, params.limit)?; + + query + .limit(limit) + .offset(offset) + .order_by(admin_block_instance::when_.desc()) + .load::(conn) + .await + } +} diff --git a/crates/db_views_moderator/src/lib.rs b/crates/db_views_moderator/src/lib.rs index d3e7efffd..5748707c6 100644 --- a/crates/db_views_moderator/src/lib.rs +++ b/crates/db_views_moderator/src/lib.rs @@ -1,4 +1,8 @@ #[cfg(feature = "full")] +pub mod admin_allow_instance; +#[cfg(feature = "full")] +pub mod admin_block_instance; +#[cfg(feature = "full")] pub mod admin_purge_comment_view; #[cfg(feature = "full")] pub mod admin_purge_community_view; diff --git a/crates/db_views_moderator/src/structs.rs b/crates/db_views_moderator/src/structs.rs index 27ee82522..06e9f099a 100644 --- a/crates/db_views_moderator/src/structs.rs +++ b/crates/db_views_moderator/src/structs.rs @@ -5,22 +5,29 @@ use lemmy_db_schema::{ source::{ comment::Comment, community::Community, - moderator::{ - AdminPurgeComment, - AdminPurgeCommunity, - AdminPurgePerson, - AdminPurgePost, - ModAdd, - ModAddCommunity, - ModBan, - ModBanFromCommunity, - ModFeaturePost, - ModHideCommunity, - ModLockPost, - ModRemoveComment, - ModRemoveCommunity, - ModRemovePost, - ModTransferCommunity, + instance::Instance, + mod_log::{ + admin::{ + AdminAllowInstance, + AdminBlockInstance, + AdminPurgeComment, + AdminPurgeCommunity, + AdminPurgePerson, + AdminPurgePost, + }, + moderator::{ + ModAdd, + ModAddCommunity, + ModBan, + ModBanFromCommunity, + ModFeaturePost, + ModHideCommunity, + ModLockPost, + ModRemoveComment, + ModRemoveCommunity, + ModRemovePost, + ModTransferCommunity, + }, }, person::Person, post::Post, @@ -233,6 +240,32 @@ pub struct AdminPurgePostView { pub community: Community, } +#[skip_serializing_none] +#[derive(Debug, 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))] +/// When an admin purges a post. +pub struct AdminBlockInstanceView { + pub admin_block_instance: AdminBlockInstance, + pub instance: Instance, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, +} + +#[skip_serializing_none] +#[derive(Debug, 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))] +/// When an admin purges a post. +pub struct AdminAllowInstanceView { + pub admin_block_instance: AdminAllowInstance, + pub instance: Instance, + #[cfg_attr(feature = "full", ts(optional))] + pub admin: Option, +} + #[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[cfg_attr(feature = "full", derive(TS, Queryable))] diff --git a/crates/federate/src/lib.rs b/crates/federate/src/lib.rs index 983749de3..dbb92949e 100644 --- a/crates/federate/src/lib.rs +++ b/crates/federate/src/lib.rs @@ -199,10 +199,14 @@ mod test { use super::*; use activitypub_federation::config::Data; use chrono::DateTime; - use lemmy_db_schema::source::{ - federation_allowlist::FederationAllowList, - federation_blocklist::FederationBlockList, - instance::InstanceForm, + use lemmy_db_schema::{ + source::{ + federation_allowlist::{FederationAllowList, FederationAllowListForm}, + federation_blocklist::{FederationBlockList, FederationBlockListForm}, + instance::InstanceForm, + person::{Person, PersonInsertForm}, + }, + traits::Crud, }; use lemmy_utils::error::LemmyError; use serial_test::serial; @@ -318,14 +322,22 @@ mod test { async fn test_send_manager_blocked() -> LemmyResult<()> { let mut data = TestData::init(1, 1).await?; - let domain = data.instances[0].domain.clone(); - FederationBlockList::replace(&mut data.context.pool(), Some(vec![domain])).await?; + let instance_id = data.instances[0].id; + let form = PersonInsertForm::new("tim".to_string(), String::new(), instance_id); + let person = Person::create(&mut data.context.pool(), &form).await?; + let form = FederationBlockListForm { + instance_id, + updated: None, + expires: None, + }; + FederationBlockList::block(&mut data.context.pool(), &form).await?; data.run().await?; let workers = &data.send_manager.workers; assert_eq!(2, workers.len()); assert!(workers.contains_key(&data.instances[1].id)); assert!(workers.contains_key(&data.instances[2].id)); + Person::delete(&mut data.context.pool(), person.id).await?; data.cleanup().await?; Ok(()) } @@ -336,13 +348,20 @@ mod test { async fn test_send_manager_allowed() -> LemmyResult<()> { let mut data = TestData::init(1, 1).await?; - let domain = data.instances[0].domain.clone(); - FederationAllowList::replace(&mut data.context.pool(), Some(vec![domain])).await?; + let instance_id = data.instances[0].id; + let form = PersonInsertForm::new("tim".to_string(), String::new(), instance_id); + let person = Person::create(&mut data.context.pool(), &form).await?; + let form = FederationAllowListForm { + instance_id: data.instances[0].id, + updated: None, + }; + FederationAllowList::allow(&mut data.context.pool(), &form).await?; data.run().await?; let workers = &data.send_manager.workers; assert_eq!(1, workers.len()); assert!(workers.contains_key(&data.instances[0].id)); + Person::delete(&mut data.context.pool(), person.id).await?; data.cleanup().await?; Ok(()) } diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs index 906a9006d..40f878747 100644 --- a/crates/utils/src/error.rs +++ b/crates/utils/src/error.rs @@ -151,6 +151,7 @@ pub enum LemmyErrorType { CommunityHasNoFollowers, PostScheduleTimeMustBeInFuture, TooManyScheduledPosts, + CannotCombineFederationBlocklistAndAllowlist, FederationError { #[cfg_attr(feature = "full", ts(optional))] error: Option, diff --git a/migrations/2024-11-28-142005_instance-block-mod-log/down.sql b/migrations/2024-11-28-142005_instance-block-mod-log/down.sql new file mode 100644 index 000000000..7936cfe7c --- /dev/null +++ b/migrations/2024-11-28-142005_instance-block-mod-log/down.sql @@ -0,0 +1,7 @@ +ALTER TABLE federation_blocklist + DROP expires; + +DROP TABLE admin_block_instance; + +DROP TABLE admin_allow_instance; + diff --git a/migrations/2024-11-28-142005_instance-block-mod-log/up.sql b/migrations/2024-11-28-142005_instance-block-mod-log/up.sql new file mode 100644 index 000000000..f537f5d32 --- /dev/null +++ b/migrations/2024-11-28-142005_instance-block-mod-log/up.sql @@ -0,0 +1,22 @@ +ALTER TABLE federation_blocklist + ADD COLUMN expires timestamptz; + +CREATE TABLE admin_block_instance ( + id serial PRIMARY KEY, + instance_id int NOT NULL REFERENCES instance (id) ON UPDATE CASCADE ON DELETE CASCADE, + admin_person_id int NOT NULL REFERENCES person (id) ON UPDATE CASCADE ON DELETE CASCADE, + blocked bool NOT NULL, + reason text, + expires timestamptz, + when_ timestamptz NOT NULL DEFAULT now() +); + +CREATE TABLE admin_allow_instance ( + id serial PRIMARY KEY, + instance_id int NOT NULL REFERENCES instance (id) ON UPDATE CASCADE ON DELETE CASCADE, + admin_person_id int NOT NULL REFERENCES person (id) ON UPDATE CASCADE ON DELETE CASCADE, + allowed bool NOT NULL, + reason text, + when_ timestamptz NOT NULL DEFAULT now() +); + diff --git a/src/api_routes_http.rs b/src/api_routes_http.rs index 2f431419c..4a648da00 100644 --- a/src/api_routes_http.rs +++ b/src/api_routes_http.rs @@ -1,4 +1,4 @@ -use actix_web::{guard, web}; +use actix_web::{guard, web::*}; use lemmy_api::{ comment::{ distinguish::distinguish_comment, @@ -76,7 +76,8 @@ use lemmy_api::{ resolve::resolve_pm_report, }, site::{ - block::block_instance, + admin_allow_instance::admin_allow_instance, + admin_block_instance::admin_block_instance, federated_instances::get_federated_instances, leave_admin::leave_admin, list_all_media::list_all_media, @@ -93,6 +94,7 @@ use lemmy_api::{ list::list_registration_applications, unread_count::get_unread_registration_application_count, }, + user_block_instance::user_block_instance, }, sitemap::get_sitemap, }; @@ -159,146 +161,146 @@ use lemmy_apub::api::{ use lemmy_routes::images::image_proxy; use lemmy_utils::rate_limit::RateLimitCell; -pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { +pub fn config(cfg: &mut ServiceConfig, rate_limit: &RateLimitCell) { cfg.service( - web::scope("/api/v3") - .route("/image_proxy", web::get().to(image_proxy)) + scope("/api/v3") + .route("/image_proxy", get().to(image_proxy)) // Site .service( - web::scope("/site") + scope("/site") .wrap(rate_limit.message()) - .route("", web::get().to(get_site)) + .route("", get().to(get_site)) // Admin Actions - .route("", web::post().to(create_site)) - .route("", web::put().to(update_site)) - .route("/block", web::post().to(block_instance)), + .route("", post().to(create_site)) + .route("", put().to(update_site)) + .route("/block", post().to(user_block_instance)), ) .service( - web::resource("/modlog") + resource("/modlog") .wrap(rate_limit.message()) - .route(web::get().to(get_mod_log)), + .route(get().to(get_mod_log)), ) .service( - web::resource("/search") + resource("/search") .wrap(rate_limit.search()) - .route(web::get().to(search)), + .route(get().to(search)), ) .service( - web::resource("/resolve_object") + resource("/resolve_object") .wrap(rate_limit.message()) - .route(web::get().to(resolve_object)), + .route(get().to(resolve_object)), ) // Community .service( - web::resource("/community") + resource("/community") .guard(guard::Post()) .wrap(rate_limit.register()) - .route(web::post().to(create_community)), + .route(post().to(create_community)), ) .service( - web::scope("/community") + scope("/community") .wrap(rate_limit.message()) - .route("", web::get().to(get_community)) - .route("", web::put().to(update_community)) - .route("/random", web::get().to(get_random_community)) - .route("/hide", web::put().to(hide_community)) - .route("/list", web::get().to(list_communities)) - .route("/follow", web::post().to(follow_community)) - .route("/block", web::post().to(block_community)) - .route("/delete", web::post().to(delete_community)) + .route("", get().to(get_community)) + .route("", put().to(update_community)) + .route("/random", get().to(get_random_community)) + .route("/hide", put().to(hide_community)) + .route("/list", get().to(list_communities)) + .route("/follow", post().to(follow_community)) + .route("/block", post().to(block_community)) + .route("/delete", post().to(delete_community)) // Mod Actions - .route("/remove", web::post().to(remove_community)) - .route("/transfer", web::post().to(transfer_community)) - .route("/ban_user", web::post().to(ban_from_community)) - .route("/mod", web::post().to(add_mod_to_community)) + .route("/remove", post().to(remove_community)) + .route("/transfer", post().to(transfer_community)) + .route("/ban_user", post().to(ban_from_community)) + .route("/mod", post().to(add_mod_to_community)) .service( - web::scope("/pending_follows") + scope("/pending_follows") .wrap(rate_limit.message()) - .route("/count", web::get().to(get_pending_follows_count)) - .route("/list", web::get().to(get_pending_follows_list)) - .route("/approve", web::post().to(post_pending_follows_approve)), + .route("/count", get().to(get_pending_follows_count)) + .route("/list", get().to(get_pending_follows_list)) + .route("/approve", post().to(post_pending_follows_approve)), ), ) .service( - web::scope("/federated_instances") + scope("/federated_instances") .wrap(rate_limit.message()) - .route("", web::get().to(get_federated_instances)), + .route("", get().to(get_federated_instances)), ) // Post .service( // Handle POST to /post separately to add the post() rate limitter - web::resource("/post") + resource("/post") .guard(guard::Post()) .wrap(rate_limit.post()) - .route(web::post().to(create_post)), + .route(post().to(create_post)), ) .service( - web::scope("/post") + scope("/post") .wrap(rate_limit.message()) - .route("", web::get().to(get_post)) - .route("", web::put().to(update_post)) - .route("/delete", web::post().to(delete_post)) - .route("/remove", web::post().to(remove_post)) - .route("/mark_as_read", web::post().to(mark_post_as_read)) - .route("/mark_many_as_read", web::post().to(mark_posts_as_read)) - .route("/hide", web::post().to(hide_post)) - .route("/lock", web::post().to(lock_post)) - .route("/feature", web::post().to(feature_post)) - .route("/list", web::get().to(list_posts)) - .route("/like", web::post().to(like_post)) - .route("/like/list", web::get().to(list_post_likes)) - .route("/save", web::put().to(save_post)) - .route("/report", web::post().to(create_post_report)) - .route("/report/resolve", web::put().to(resolve_post_report)) - .route("/report/list", web::get().to(list_post_reports)) - .route("/site_metadata", web::get().to(get_link_metadata)), + .route("", get().to(get_post)) + .route("", put().to(update_post)) + .route("/delete", post().to(delete_post)) + .route("/remove", post().to(remove_post)) + .route("/mark_as_read", post().to(mark_post_as_read)) + .route("/mark_many_as_read", post().to(mark_posts_as_read)) + .route("/hide", post().to(hide_post)) + .route("/lock", post().to(lock_post)) + .route("/feature", post().to(feature_post)) + .route("/list", get().to(list_posts)) + .route("/like", post().to(like_post)) + .route("/like/list", get().to(list_post_likes)) + .route("/save", put().to(save_post)) + .route("/report", post().to(create_post_report)) + .route("/report/resolve", put().to(resolve_post_report)) + .route("/report/list", get().to(list_post_reports)) + .route("/site_metadata", get().to(get_link_metadata)), ) // Comment .service( // Handle POST to /comment separately to add the comment() rate limitter - web::resource("/comment") + resource("/comment") .guard(guard::Post()) .wrap(rate_limit.comment()) - .route(web::post().to(create_comment)), + .route(post().to(create_comment)), ) .service( - web::scope("/comment") + scope("/comment") .wrap(rate_limit.message()) - .route("", web::get().to(get_comment)) - .route("", web::put().to(update_comment)) - .route("/delete", web::post().to(delete_comment)) - .route("/remove", web::post().to(remove_comment)) - .route("/mark_as_read", web::post().to(mark_reply_as_read)) - .route("/distinguish", web::post().to(distinguish_comment)) - .route("/like", web::post().to(like_comment)) - .route("/like/list", web::get().to(list_comment_likes)) - .route("/save", web::put().to(save_comment)) - .route("/list", web::get().to(list_comments)) - .route("/report", web::post().to(create_comment_report)) - .route("/report/resolve", web::put().to(resolve_comment_report)) - .route("/report/list", web::get().to(list_comment_reports)), + .route("", get().to(get_comment)) + .route("", put().to(update_comment)) + .route("/delete", post().to(delete_comment)) + .route("/remove", post().to(remove_comment)) + .route("/mark_as_read", post().to(mark_reply_as_read)) + .route("/distinguish", post().to(distinguish_comment)) + .route("/like", post().to(like_comment)) + .route("/like/list", get().to(list_comment_likes)) + .route("/save", put().to(save_comment)) + .route("/list", get().to(list_comments)) + .route("/report", post().to(create_comment_report)) + .route("/report/resolve", put().to(resolve_comment_report)) + .route("/report/list", get().to(list_comment_reports)), ) // Private Message .service( - web::scope("/private_message") + scope("/private_message") .wrap(rate_limit.message()) - .route("/list", web::get().to(get_private_message)) - .route("", web::post().to(create_private_message)) - .route("", web::put().to(update_private_message)) - .route("/delete", web::post().to(delete_private_message)) - .route("/mark_as_read", web::post().to(mark_pm_as_read)) - .route("/report", web::post().to(create_pm_report)) - .route("/report/resolve", web::put().to(resolve_pm_report)) - .route("/report/list", web::get().to(list_pm_reports)), + .route("/list", get().to(get_private_message)) + .route("", post().to(create_private_message)) + .route("", put().to(update_private_message)) + .route("/delete", post().to(delete_private_message)) + .route("/mark_as_read", post().to(mark_pm_as_read)) + .route("/report", post().to(create_pm_report)) + .route("/report/resolve", put().to(resolve_pm_report)) + .route("/report/list", get().to(list_pm_reports)), ) // User .service( // Account action, I don't like that it's in /user maybe /accounts // Handle /user/register separately to add the register() rate limiter - web::resource("/user/register") + resource("/user/register") .guard(guard::Post()) .wrap(rate_limit.register()) - .route(web::post().to(register)), + .route(post().to(register)), ) // User .service( @@ -306,138 +308,134 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimitCell) { // TODO: pretty annoying way to apply rate limits for register and login, we should // group them under a common path so that rate limit is only applied once (eg under // /account). - web::resource("/user/login") + resource("/user/login") .guard(guard::Post()) .wrap(rate_limit.register()) - .route(web::post().to(login)), + .route(post().to(login)), ) .service( - web::resource("/user/password_reset") + resource("/user/password_reset") .wrap(rate_limit.register()) - .route(web::post().to(reset_password)), + .route(post().to(reset_password)), ) .service( // Handle captcha separately - web::resource("/user/get_captcha") + resource("/user/get_captcha") .wrap(rate_limit.post()) - .route(web::get().to(get_captcha)), + .route(get().to(get_captcha)), ) .service( - web::resource("/user/export_settings") + resource("/user/export_settings") .wrap(rate_limit.import_user_settings()) - .route(web::get().to(export_settings)), + .route(get().to(export_settings)), ) .service( - web::resource("/user/import_settings") + resource("/user/import_settings") .wrap(rate_limit.import_user_settings()) - .route(web::post().to(import_settings)), + .route(post().to(import_settings)), ) // TODO, all the current account related actions under /user need to get moved here eventually .service( - web::scope("/account") + scope("/account") .wrap(rate_limit.message()) - .route("/list_media", web::get().to(list_media)), + .route("/list_media", get().to(list_media)), ) // User actions .service( - web::scope("/user") + scope("/user") .wrap(rate_limit.message()) - .route("", web::get().to(read_person)) - .route("/mention", web::get().to(list_mentions)) + .route("", get().to(read_person)) + .route("/mention", get().to(list_mentions)) .route( "/mention/mark_as_read", - web::post().to(mark_person_mention_as_read), + post().to(mark_person_mention_as_read), ) - .route("/replies", web::get().to(list_replies)) + .route("/replies", get().to(list_replies)) // Admin action. I don't like that it's in /user - .route("/ban", web::post().to(ban_from_site)) - .route("/banned", web::get().to(list_banned_users)) - .route("/block", web::post().to(block_person)) + .route("/ban", post().to(ban_from_site)) + .route("/banned", get().to(list_banned_users)) + .route("/block", post().to(block_person)) // TODO Account actions. I don't like that they're in /user maybe /accounts - .route("/logout", web::post().to(logout)) - .route("/delete_account", web::post().to(delete_account)) - .route( - "/password_change", - web::post().to(change_password_after_reset), - ) + .route("/logout", post().to(logout)) + .route("/delete_account", post().to(delete_account)) + .route("/password_change", post().to(change_password_after_reset)) // TODO mark_all_as_read feels off being in this section as well - .route( - "/mark_all_as_read", - web::post().to(mark_all_notifications_read), - ) - .route("/save_user_settings", web::put().to(save_user_settings)) - .route("/change_password", web::put().to(change_password)) - .route("/report_count", web::get().to(report_count)) - .route("/unread_count", web::get().to(unread_count)) - .route("/verify_email", web::post().to(verify_email)) - .route("/leave_admin", web::post().to(leave_admin)) - .route("/totp/generate", web::post().to(generate_totp_secret)) - .route("/totp/update", web::post().to(update_totp)) - .route("/list_logins", web::get().to(list_logins)) - .route("/validate_auth", web::get().to(validate_auth)), + .route("/mark_all_as_read", post().to(mark_all_notifications_read)) + .route("/save_user_settings", put().to(save_user_settings)) + .route("/change_password", put().to(change_password)) + .route("/report_count", get().to(report_count)) + .route("/unread_count", get().to(unread_count)) + .route("/verify_email", post().to(verify_email)) + .route("/leave_admin", post().to(leave_admin)) + .route("/totp/generate", post().to(generate_totp_secret)) + .route("/totp/update", post().to(update_totp)) + .route("/list_logins", get().to(list_logins)) + .route("/validate_auth", get().to(validate_auth)), ) // Admin Actions .service( - web::scope("/admin") + scope("/admin") .wrap(rate_limit.message()) - .route("/add", web::post().to(add_admin)) + .route("/add", post().to(add_admin)) .route( "/registration_application/count", - web::get().to(get_unread_registration_application_count), + get().to(get_unread_registration_application_count), ) .route( "/registration_application/list", - web::get().to(list_registration_applications), + get().to(list_registration_applications), ) .route( "/registration_application/approve", - web::put().to(approve_registration_application), + put().to(approve_registration_application), ) .route( "/registration_application", - web::get().to(get_registration_application), + get().to(get_registration_application), ) - .route("/list_all_media", web::get().to(list_all_media)) + .route("/list_all_media", get().to(list_all_media)) .service( - web::scope("/purge") - .route("/person", web::post().to(purge_person)) - .route("/community", web::post().to(purge_community)) - .route("/post", web::post().to(purge_post)) - .route("/comment", web::post().to(purge_comment)), + scope("/purge") + .route("/person", post().to(purge_person)) + .route("/community", post().to(purge_community)) + .route("/post", post().to(purge_post)) + .route("/comment", post().to(purge_comment)), ) .service( - web::scope("/tagline") + scope("/tagline") .wrap(rate_limit.message()) - .route("", web::post().to(create_tagline)) - .route("", web::put().to(update_tagline)) - .route("/delete", web::post().to(delete_tagline)) - .route("/list", web::get().to(list_taglines)), - ), + .route("", post().to(create_tagline)) + .route("", put().to(update_tagline)) + .route("/delete", post().to(delete_tagline)) + .route("/list", get().to(list_taglines)), + ) + .route("block_instance", post().to(admin_block_instance)) + .route("allow_instance", post().to(admin_allow_instance)), ) .service( - web::scope("/custom_emoji") + scope("/custom_emoji") .wrap(rate_limit.message()) - .route("", web::post().to(create_custom_emoji)) - .route("", web::put().to(update_custom_emoji)) - .route("/delete", web::post().to(delete_custom_emoji)) - .route("/list", web::get().to(list_custom_emojis)), + .route("", post().to(create_custom_emoji)) + .route("", put().to(update_custom_emoji)) + .route("/delete", post().to(delete_custom_emoji)) + .route("/list", get().to(list_custom_emojis)), ) .service( - web::scope("/oauth_provider") + scope("/oauth_provider") .wrap(rate_limit.message()) - .route("", web::post().to(create_oauth_provider)) - .route("", web::put().to(update_oauth_provider)) - .route("/delete", web::post().to(delete_oauth_provider)), + .route("", post().to(create_oauth_provider)) + .route("", put().to(update_oauth_provider)) + .route("/delete", post().to(delete_oauth_provider)), ) .service( - web::scope("/oauth") + scope("/oauth") .wrap(rate_limit.register()) - .route("/authenticate", web::post().to(authenticate_with_oauth)), + .route("/authenticate", post().to(authenticate_with_oauth)), ), ); cfg.service( - web::scope("/sitemap.xml") + scope("/sitemap.xml") .wrap(rate_limit.message()) - .route("", web::get().to(get_sitemap)), + .route("", get().to(get_sitemap)), ); } diff --git a/src/scheduled_tasks.rs b/src/scheduled_tasks.rs index 5900fe39f..52962877f 100644 --- a/src/scheduled_tasks.rs +++ b/src/scheduled_tasks.rs @@ -23,6 +23,7 @@ use lemmy_db_schema::{ comment, community, community_actions, + federation_blocklist, instance, person, post, @@ -58,6 +59,7 @@ pub async fn setup(context: Data) -> LemmyResult<()> { async move { active_counts(&mut context.pool()).await; update_banned_when_expired(&mut context.pool()).await; + delete_instance_block_when_expired(&mut context.pool()).await; } }); @@ -113,6 +115,7 @@ async fn startup_jobs(pool: &mut DbPool<'_>) { active_counts(pool).await; update_hot_ranks(pool).await; update_banned_when_expired(pool).await; + delete_instance_block_when_expired(pool).await; clear_old_activities(pool).await; overwrite_deleted_posts_and_comments(pool).await; delete_old_denied_users(pool).await; @@ -446,6 +449,27 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) { } } +/// Set banned to false after ban expires +async fn delete_instance_block_when_expired(pool: &mut DbPool<'_>) { + info!("Delete instance blocks when expired ..."); + let conn = get_conn(pool).await; + + match conn { + Ok(mut conn) => { + diesel::delete( + federation_blocklist::table.filter(federation_blocklist::expires.lt(now().nullable())), + ) + .execute(&mut conn) + .await + .inspect_err(|e| error!("Failed to remove federation_blocklist expired rows: {e}")) + .ok(); + } + Err(e) => { + error!("Failed to get connection from pool: {e}"); + } + } +} + /// Find all unpublished posts with scheduled date in the future, and publish them. async fn publish_scheduled_posts(context: &Data) { let pool = &mut context.pool(); From 3d7fbde091d59ef7258e0dcb177240519a2e1874 Mon Sep 17 00:00:00 2001 From: Nutomic Date: Thu, 28 Nov 2024 23:23:18 +0000 Subject: [PATCH 3/3] Allow bypassing image proxy for specific domains (#5223) * Allow bypassing proxy for some domains with ProxyAllImages * remove web:: * remove expect * bypass imgur by default * correct imgur domain * restore processing, cleanup --------- Co-authored-by: sunaurus --- config/defaults.hjson | 9 ++++ crates/routes/src/images.rs | 66 +++++++++++++++------------- crates/utils/src/settings/structs.rs | 9 ++++ 3 files changed, 54 insertions(+), 30 deletions(-) diff --git a/config/defaults.hjson b/config/defaults.hjson index c12f879c7..282b7957d 100644 --- a/config/defaults.hjson +++ b/config/defaults.hjson @@ -73,6 +73,15 @@ # # Requires pict-rs 0.5 "ProxyAllImages" + # Allows bypassing proxy for specific image hosts when using ProxyAllImages. + # + # imgur.com is bypassed by default to avoid rate limit errors. When specifying any bypass + # in the config, this default is ignored and you need to list imgur explicitly. To proxy imgur + # requests, specify a noop bypass list, eg `proxy_bypass_domains ["example.org"]`. + proxy_bypass_domains: [ + "i.imgur.com" + /* ... */ + ] # Timeout for uploading images to pictrs (in seconds) upload_timeout: 30 # Resize post thumbnails to this maximum width/height. diff --git a/crates/routes/src/images.rs b/crates/routes/src/images.rs index fe91fa42c..50897b95d 100644 --- a/crates/routes/src/images.rs +++ b/crates/routes/src/images.rs @@ -1,13 +1,14 @@ use actix_web::{ - body::BodyStream, + body::{BodyStream, BoxBody}, http::{ header::{HeaderName, ACCEPT_ENCODING, HOST}, Method, StatusCode, }, - web::{self, Query}, + web::*, HttpRequest, HttpResponse, + Responder, }; use futures::stream::{Stream, StreamExt}; use http::HeaderValue; @@ -24,22 +25,18 @@ use serde::Deserialize; use std::time::Duration; use url::Url; -pub fn config( - cfg: &mut web::ServiceConfig, - client: ClientWithMiddleware, - rate_limit: &RateLimitCell, -) { +pub fn config(cfg: &mut ServiceConfig, client: ClientWithMiddleware, rate_limit: &RateLimitCell) { cfg - .app_data(web::Data::new(client)) + .app_data(Data::new(client)) .service( - web::resource("/pictrs/image") + resource("/pictrs/image") .wrap(rate_limit.image()) - .route(web::post().to(upload)), + .route(post().to(upload)), ) // 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/delete/{token}/{filename}").route(web::get().to(delete))) - .service(web::resource("/pictrs/healthz").route(web::get().to(healthz))); + .service(resource("/pictrs/image/{filename}").route(get().to(full_res))) + .service(resource("/pictrs/image/delete/{token}/{filename}").route(get().to(delete))) + .service(resource("/pictrs/healthz").route(get().to(healthz))); } trait ProcessUrl { @@ -129,11 +126,11 @@ fn adapt_request( async fn upload( req: HttpRequest, - body: web::Payload, + body: Payload, // require login local_user_view: LocalUserView, - client: web::Data, - context: web::Data, + client: Data, + context: Data, ) -> LemmyResult { // TODO: check rate limit here let pictrs_config = context.settings().pictrs_config()?; @@ -173,11 +170,11 @@ async fn upload( } async fn full_res( - filename: web::Path, - web::Query(params): web::Query, + filename: Path, + Query(params): Query, req: HttpRequest, - client: web::Data, - context: web::Data, + client: Data, + context: Data, local_user_view: Option, ) -> LemmyResult { // block access to images if instance is private and unauthorized, public @@ -226,10 +223,10 @@ async fn image( } async fn delete( - components: web::Path<(String, String)>, + components: Path<(String, String)>, req: HttpRequest, - client: web::Data, - context: web::Data, + client: Data, + context: Data, // require login _local_user_view: LocalUserView, ) -> LemmyResult { @@ -253,8 +250,8 @@ async fn delete( async fn healthz( req: HttpRequest, - client: web::Data, - context: web::Data, + client: Data, + context: Data, ) -> LemmyResult { let pictrs_config = context.settings().pictrs_config()?; let url = format!("{}healthz", pictrs_config.url); @@ -273,9 +270,9 @@ async fn healthz( pub async fn image_proxy( Query(params): Query, req: HttpRequest, - client: web::Data, - context: web::Data, -) -> LemmyResult { + client: Data, + context: Data, +) -> LemmyResult, HttpResponse>> { let url = Url::parse(¶ms.url)?; // Check that url corresponds to a federated image so that this can't be abused as a proxy @@ -283,10 +280,19 @@ pub async fn image_proxy( RemoteImage::validate(&mut context.pool(), url.clone().into()).await?; let pictrs_config = context.settings().pictrs_config()?; - let processed_url = params.process_url(¶ms.url, &pictrs_config.url); - image(processed_url, req, &client).await + let bypass_proxy = pictrs_config + .proxy_bypass_domains + .iter() + .any(|s| url.domain().is_some_and(|d| d == s)); + if bypass_proxy { + // Bypass proxy and redirect user to original image + Ok(Either::Left(Redirect::to(url.to_string()).respond_to(&req))) + } else { + // Proxy the image data through Lemmy + Ok(Either::Right(image(processed_url, req, &client).await?)) + } } fn make_send(mut stream: S) -> impl Stream + Send + Unpin + 'static diff --git a/crates/utils/src/settings/structs.rs b/crates/utils/src/settings/structs.rs index fdbec4a95..ccc0da45b 100644 --- a/crates/utils/src/settings/structs.rs +++ b/crates/utils/src/settings/structs.rs @@ -88,6 +88,15 @@ pub struct PictrsConfig { #[default(PictrsImageMode::StoreLinkPreviews)] pub(super) image_mode: PictrsImageMode, + /// Allows bypassing proxy for specific image hosts when using ProxyAllImages. + /// + /// imgur.com is bypassed by default to avoid rate limit errors. When specifying any bypass + /// in the config, this default is ignored and you need to list imgur explicitly. To proxy imgur + /// requests, specify a noop bypass list, eg `proxy_bypass_domains ["example.org"]`. + #[default(vec!["i.imgur.com".to_string()])] + #[doku(example = "i.imgur.com")] + pub proxy_bypass_domains: Vec, + /// Timeout for uploading images to pictrs (in seconds) #[default(30)] pub upload_timeout: u64,