Merge remote-tracking branch 'origin/main' into combined_tables_2

This commit is contained in:
Dessalines 2024-11-28 18:25:55 -05:00
commit a9f28af554
66 changed files with 1180 additions and 687 deletions

View file

@ -28,7 +28,7 @@
"eslint": "^9.14.0", "eslint": "^9.14.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"jest": "^29.5.0", "jest": "^29.5.0",
"lemmy-js-client": "0.20.0-alpha.18", "lemmy-js-client": "0.20.0-instance-blocks.5",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"ts-jest": "^29.1.0", "ts-jest": "^29.1.0",
"typescript": "^5.5.4", "typescript": "^5.5.4",

View file

@ -30,8 +30,8 @@ importers:
specifier: ^29.5.0 specifier: ^29.5.0
version: 29.7.0(@types/node@22.9.0) version: 29.7.0(@types/node@22.9.0)
lemmy-js-client: lemmy-js-client:
specifier: 0.20.0-alpha.18 specifier: 0.20.0-instance-blocks.5
version: 0.20.0-alpha.18 version: 0.20.0-instance-blocks.5
prettier: prettier:
specifier: ^3.2.5 specifier: ^3.2.5
version: 3.3.3 version: 3.3.3
@ -1167,8 +1167,8 @@ packages:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==} resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'} engines: {node: '>=6'}
lemmy-js-client@0.20.0-alpha.18: lemmy-js-client@0.20.0-instance-blocks.5:
resolution: {integrity: sha512-oZy8DboTWfUar4mPWpi7SYrOEjTBJxkvd1e6QaVwoA5UhqQV1WhxEYbzrpi/gXnEokaVQ0i5sjtL/Y2PHMO3MQ==} resolution: {integrity: sha512-wDuRFzg32lbbJr4cNmd+cbzjgw+okw2/d5AujYjAm4gv0OEFfsYhP3QQ2WscwUR5HJTdzsR7IIyiBnvmaEUzUw==}
leven@3.1.0: leven@3.1.0:
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
@ -3077,7 +3077,7 @@ snapshots:
kleur@3.0.3: {} 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: {} leven@3.1.0: {}

View file

@ -25,16 +25,16 @@ import {
getComments, getComments,
createComment, createComment,
getCommunityByName, getCommunityByName,
blockInstance,
waitUntil, waitUntil,
alphaUrl, alphaUrl,
delta, delta,
betaAllowedInstances,
searchPostLocal, searchPostLocal,
longDelay, longDelay,
editCommunity, editCommunity,
unfollows, unfollows,
userBlockInstance,
} from "./shared"; } from "./shared";
import { AdminAllowInstanceParams } from "lemmy-js-client/dist/types/AdminAllowInstanceParams";
import { EditCommunity, EditSite } from "lemmy-js-client"; import { EditCommunity, EditSite } from "lemmy-js-client";
beforeAll(setupLogins); beforeAll(setupLogins);
@ -363,7 +363,7 @@ test("User blocks instance, communities are hidden", async () => {
expect(listing_ids).toContain(postRes.post_view.post.ap_id); expect(listing_ids).toContain(postRes.post_view.post.ap_id);
// block the beta instance // 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 // after blocking, post should not be in listing
let listing2 = await getPosts(alpha, "All"); 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); expect(listing_ids2.indexOf(postRes.post_view.post.ap_id)).toBe(-1);
// unblock instance again // 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 // post should be included in listing
let listing3 = await getPosts(alpha, "All"); 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); expect(communityRes1.community_view.counts.subscribers).toBe(2);
// temporarily block alpha, so that it doesn't know about unfollow // temporarily block alpha, so that it doesn't know about unfollow
let editSiteForm: EditSite = {}; var allow_instance_params: AdminAllowInstanceParams = {
editSiteForm.allowed_instances = ["lemmy-epsilon"]; instance: "lemmy-alpha",
await beta.editSite(editSiteForm); allow: false,
reason: undefined,
};
await beta.adminAllowInstance(allow_instance_params);
await longDelay(); await longDelay();
// unfollow // unfollow
@ -471,8 +474,8 @@ test("Dont receive community activities after unsubscribe", async () => {
expect(communityRes2.community_view.counts.subscribers).toBe(2); expect(communityRes2.community_view.counts.subscribers).toBe(2);
// unblock alpha // unblock alpha
editSiteForm.allowed_instances = betaAllowedInstances; allow_instance_params.allow = true;
await beta.editSite(editSiteForm); await beta.adminAllowInstance(allow_instance_params);
await longDelay(); await longDelay();
// create a post, it shouldnt reach beta // create a post, it shouldnt reach beta

View file

@ -40,6 +40,7 @@ import {
createCommunity, createCommunity,
} from "./shared"; } from "./shared";
import { PostView } from "lemmy-js-client/dist/types/PostView"; 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"; import { EditSite, ResolveObject } from "lemmy-js-client";
let betaCommunity: CommunityView | undefined; let betaCommunity: CommunityView | undefined;
@ -87,12 +88,12 @@ async function assertPostFederation(
} }
test("Create a post", async () => { test("Create a post", async () => {
// Setup some allowlists and blocklists // Block alpha
const editSiteForm: EditSite = {}; var block_instance_params: AdminBlockInstanceParams = {
instance: "lemmy-alpha",
editSiteForm.allowed_instances = []; block: true,
editSiteForm.blocked_instances = ["lemmy-alpha"]; };
await epsilon.editSite(editSiteForm); await epsilon.adminBlockInstance(block_instance_params);
if (!betaCommunity) { if (!betaCommunity) {
throw "Missing beta community"; throw "Missing beta community";
@ -132,11 +133,9 @@ test("Create a post", async () => {
resolvePost(epsilon, postRes.post_view.post), resolvePost(epsilon, postRes.post_view.post),
).rejects.toStrictEqual(Error("not_found")); ).rejects.toStrictEqual(Error("not_found"));
// remove added allow/blocklists // remove blocked instance
editSiteForm.allowed_instances = []; block_instance_params.block = false;
editSiteForm.blocked_instances = []; await epsilon.adminBlockInstance(block_instance_params);
await delta.editSite(editSiteForm);
await epsilon.editSite(editSiteForm);
}); });
test("Create a post in a non-existent community", async () => { test("Create a post in a non-existent community", async () => {

View file

@ -1,9 +1,8 @@
import { import {
AdminBlockInstanceParams,
ApproveCommunityPendingFollower, ApproveCommunityPendingFollower,
BlockCommunity, BlockCommunity,
BlockCommunityResponse, BlockCommunityResponse,
BlockInstance,
BlockInstanceResponse,
CommunityId, CommunityId,
CommunityVisibility, CommunityVisibility,
CreatePrivateMessageReport, CreatePrivateMessageReport,
@ -21,11 +20,13 @@ import {
PostView, PostView,
PrivateMessageReportResponse, PrivateMessageReportResponse,
SuccessResponse, SuccessResponse,
UserBlockInstanceParams,
} from "lemmy-js-client"; } from "lemmy-js-client";
import { CreatePost } from "lemmy-js-client/dist/types/CreatePost"; import { CreatePost } from "lemmy-js-client/dist/types/CreatePost";
import { DeletePost } from "lemmy-js-client/dist/types/DeletePost"; import { DeletePost } from "lemmy-js-client/dist/types/DeletePost";
import { EditPost } from "lemmy-js-client/dist/types/EditPost"; import { EditPost } from "lemmy-js-client/dist/types/EditPost";
import { EditSite } from "lemmy-js-client/dist/types/EditSite"; 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 { FeaturePost } from "lemmy-js-client/dist/types/FeaturePost";
import { GetComments } from "lemmy-js-client/dist/types/GetComments"; import { GetComments } from "lemmy-js-client/dist/types/GetComments";
import { GetCommentsResponse } from "lemmy-js-client/dist/types/GetCommentsResponse"; 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 delta = new LemmyHttp(deltaUrl, { fetchFunction });
export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction }); export const epsilon = new LemmyHttp(epsilonUrl, { fetchFunction });
export const betaAllowedInstances = [
"lemmy-alpha",
"lemmy-gamma",
"lemmy-delta",
"lemmy-epsilon",
];
const password = "lemmylemmy"; const password = "lemmylemmy";
export async function setupLogins() { export async function setupLogins() {
@ -168,30 +162,29 @@ export async function setupLogins() {
rate_limit_comment: 999, rate_limit_comment: 999,
rate_limit_search: 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); await alpha.editSite(editSiteForm);
editSiteForm.allowed_instances = betaAllowedInstances;
await beta.editSite(editSiteForm); await beta.editSite(editSiteForm);
editSiteForm.allowed_instances = [
"lemmy-alpha",
"lemmy-beta",
"lemmy-delta",
"lemmy-epsilon",
];
await gamma.editSite(editSiteForm); await gamma.editSite(editSiteForm);
// Setup delta allowed instance
editSiteForm.allowed_instances = ["lemmy-beta"];
await delta.editSite(editSiteForm); 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 // Create the main alpha/beta communities
// Ignore thrown errors of duplicates // 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( export async function createPost(
api: LemmyHttp, api: LemmyHttp,
community_id: number, community_id: number,
@ -854,16 +858,16 @@ export function getPosts(
return api.getPosts(form); return api.getPosts(form);
} }
export function blockInstance( export function userBlockInstance(
api: LemmyHttp, api: LemmyHttp,
instance_id: InstanceId, instance_id: InstanceId,
block: boolean, block: boolean,
): Promise<BlockInstanceResponse> { ): Promise<SuccessResponse> {
let form: BlockInstance = { let form: UserBlockInstanceParams = {
instance_id, instance_id,
block, block,
}; };
return api.blockInstance(form); return api.userBlockInstance(form);
} }
export function blockCommunity( export function blockCommunity(

View file

@ -23,7 +23,12 @@ import {
unfollows, unfollows,
saveUserSettingsBio, saveUserSettingsBio,
} from "./shared"; } 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"; import { GetPosts } from "lemmy-js-client/dist/types/GetPosts";
beforeAll(setupLogins); beforeAll(setupLogins);
@ -149,9 +154,14 @@ test("Create user with Arabic name", async () => {
}); });
test("Create user with accept-language", async () => { test("Create user with accept-language", async () => {
const edit: EditSite = {
discussion_languages: [32],
};
await alpha.editSite(edit);
let lemmy_http = new LemmyHttp(alphaUrl, { let lemmy_http = new LemmyHttp(alphaUrl, {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language#syntax // 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); let user = await registerUser(lemmy_http, alphaUrl);

View file

@ -73,6 +73,15 @@
# #
# Requires pict-rs 0.5 # Requires pict-rs 0.5
"ProxyAllImages" "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) # Timeout for uploading images to pictrs (in seconds)
upload_timeout: 30 upload_timeout: 30
# Resize post thumbnails to this maximum width/height. # Resize post thumbnails to this maximum width/height.

View file

@ -10,7 +10,7 @@ use lemmy_db_schema::{
source::{ source::{
community::{Community, CommunityModerator, CommunityModeratorForm}, community::{Community, CommunityModerator, CommunityModeratorForm},
local_user::LocalUser, local_user::LocalUser,
moderator::{ModAddCommunity, ModAddCommunityForm}, mod_log::moderator::{ModAddCommunity, ModAddCommunityForm},
}, },
traits::{Crud, Joinable}, traits::{Crud, Joinable},
}; };

View file

@ -20,7 +20,7 @@ use lemmy_db_schema::{
CommunityPersonBanForm, CommunityPersonBanForm,
}, },
local_user::LocalUser, local_user::LocalUser,
moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
}, },
traits::{Bannable, Crud, Followable}, traits::{Bannable, Crud, Followable},
}; };

View file

@ -10,7 +10,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::{Community, CommunityUpdateForm}, community::{Community, CommunityUpdateForm},
moderator::{ModHideCommunity, ModHideCommunityForm}, mod_log::moderator::{ModHideCommunity, ModHideCommunityForm},
}, },
traits::Crud, traits::Crud,
}; };

View file

@ -8,7 +8,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::{Community, CommunityModerator, CommunityModeratorForm}, community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModTransferCommunity, ModTransferCommunityForm}, mod_log::moderator::{ModTransferCommunity, ModTransferCommunityForm},
}, },
traits::{Crud, Joinable}, traits::{Crud, Joinable},
}; };

View file

@ -19,7 +19,7 @@ use lemmy_db_schema::{
CommunityPersonBanForm, CommunityPersonBanForm,
}, },
local_site::LocalSite, local_site::LocalSite,
moderator::{ModBanFromCommunity, ModBanFromCommunityForm}, mod_log::moderator::{ModBanFromCommunity, ModBanFromCommunityForm},
person::Person, person::Person,
}, },
traits::{Bannable, Crud, Followable}, traits::{Bannable, Crud, Followable},

View file

@ -7,7 +7,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
local_user::{LocalUser, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserUpdateForm},
moderator::{ModAdd, ModAddForm}, mod_log::moderator::{ModAdd, ModAddForm},
}, },
traits::Crud, traits::Crud,
}; };

View file

@ -11,7 +11,7 @@ use lemmy_db_schema::{
source::{ source::{
local_user::LocalUser, local_user::LocalUser,
login_token::LoginToken, login_token::LoginToken,
moderator::{ModBan, ModBanForm}, mod_log::moderator::{ModBan, ModBanForm},
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},
}, },
traits::Crud, traits::Crud,

View file

@ -10,7 +10,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::Community, community::Community,
moderator::{ModFeaturePost, ModFeaturePostForm}, mod_log::moderator::{ModFeaturePost, ModFeaturePostForm},
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
}, },
traits::Crud, traits::Crud,

View file

@ -9,7 +9,7 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
moderator::{ModLockPost, ModLockPostForm}, mod_log::moderator::{ModLockPost, ModLockPostForm},
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
}, },
traits::Crud, traits::Crud,

View file

@ -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<AdminAllowInstanceParams>,
local_user_view: LocalUserView,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
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()))
}

View file

@ -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<AdminBlockInstanceParams>,
local_user_view: LocalUserView,
context: Data<LemmyContext>,
) -> LemmyResult<Json<SuccessResponse>> {
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()))
}

View file

@ -6,7 +6,7 @@ use lemmy_db_schema::{
language::Language, language::Language,
local_site_url_blocklist::LocalSiteUrlBlocklist, local_site_url_blocklist::LocalSiteUrlBlocklist,
local_user::{LocalUser, LocalUserUpdateForm}, local_user::{LocalUser, LocalUserUpdateForm},
moderator::{ModAdd, ModAddForm}, mod_log::moderator::{ModAdd, ModAddForm},
oauth_provider::OAuthProvider, oauth_provider::OAuthProvider,
tagline::Tagline, tagline::Tagline,
}, },

View file

@ -1,7 +1,9 @@
pub mod block; pub mod admin_allow_instance;
pub mod admin_block_instance;
pub mod federated_instances; pub mod federated_instances;
pub mod leave_admin; pub mod leave_admin;
pub mod list_all_media; pub mod list_all_media;
pub mod mod_log; pub mod mod_log;
pub mod purge; pub mod purge;
pub mod registration_applications; pub mod registration_applications;
pub mod user_block_instance;

View file

@ -7,6 +7,8 @@ use lemmy_api_common::{
use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType}; use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType};
use lemmy_db_views::structs::LocalUserView; use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_moderator::structs::{ use lemmy_db_views_moderator::structs::{
AdminAllowInstanceView,
AdminBlockInstanceView,
AdminPurgeCommentView, AdminPurgeCommentView,
AdminPurgeCommunityView, AdminPurgeCommunityView,
AdminPurgePersonView, AdminPurgePersonView,
@ -121,6 +123,8 @@ pub async fn get_mod_log(
admin_purged_communities, admin_purged_communities,
admin_purged_posts, admin_purged_posts,
admin_purged_comments, admin_purged_comments,
admin_block_instance,
admin_allow_instance,
) = if data.community_id.is_none() { ) = if data.community_id.is_none() {
( (
match type_ { match type_ {
@ -161,6 +165,18 @@ pub async fn get_mod_log(
} }
_ => Default::default(), _ => 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 { } else {
Default::default() Default::default()
@ -183,5 +199,7 @@ pub async fn get_mod_log(
admin_purged_posts, admin_purged_posts,
admin_purged_comments, admin_purged_comments,
hidden_communities, hidden_communities,
admin_block_instance,
admin_allow_instance,
})) }))
} }

View file

@ -11,7 +11,7 @@ use lemmy_db_schema::{
source::{ source::{
comment::Comment, comment::Comment,
local_user::LocalUser, local_user::LocalUser,
moderator::{AdminPurgeComment, AdminPurgeCommentForm}, mod_log::admin::{AdminPurgeComment, AdminPurgeCommentForm},
}, },
traits::Crud, traits::Crud,
}; };

View file

@ -13,7 +13,7 @@ use lemmy_db_schema::{
source::{ source::{
community::Community, community::Community,
local_user::LocalUser, local_user::LocalUser,
moderator::{AdminPurgeCommunity, AdminPurgeCommunityForm}, mod_log::admin::{AdminPurgeCommunity, AdminPurgeCommunityForm},
}, },
traits::Crud, traits::Crud,
}; };

View file

@ -11,7 +11,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
local_user::LocalUser, local_user::LocalUser,
moderator::{AdminPurgePerson, AdminPurgePersonForm}, mod_log::admin::{AdminPurgePerson, AdminPurgePersonForm},
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},
}, },
traits::Crud, traits::Crud,

View file

@ -11,7 +11,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
local_user::LocalUser, local_user::LocalUser,
moderator::{AdminPurgePost, AdminPurgePostForm}, mod_log::admin::{AdminPurgePost, AdminPurgePostForm},
post::Post, post::Post,
}, },
traits::Crud, traits::Crud,

View file

@ -1,9 +1,6 @@
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use actix_web::web::Json; use actix_web::web::Json;
use lemmy_api_common::{ use lemmy_api_common::{context::LemmyContext, site::UserBlockInstanceParams, SuccessResponse};
context::LemmyContext,
site::{BlockInstance, BlockInstanceResponse},
};
use lemmy_db_schema::{ use lemmy_db_schema::{
source::instance_block::{InstanceBlock, InstanceBlockForm}, source::instance_block::{InstanceBlock, InstanceBlockForm},
traits::Blockable, traits::Blockable,
@ -12,11 +9,11 @@ use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult}; use lemmy_utils::error::{LemmyErrorExt, LemmyErrorType, LemmyResult};
#[tracing::instrument(skip(context))] #[tracing::instrument(skip(context))]
pub async fn block_instance( pub async fn user_block_instance(
data: Json<BlockInstance>, data: Json<UserBlockInstanceParams>,
local_user_view: LocalUserView, local_user_view: LocalUserView,
context: Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<Json<BlockInstanceResponse>> { ) -> LemmyResult<Json<SuccessResponse>> {
let instance_id = data.instance_id; let instance_id = data.instance_id;
let person_id = local_user_view.person.id; let person_id = local_user_view.person.id;
if local_user_view.person.instance_id == instance_id { if local_user_view.person.instance_id == instance_id {
@ -38,7 +35,5 @@ pub async fn block_instance(
.with_lemmy_type(LemmyErrorType::InstanceBlockAlreadyExists)?; .with_lemmy_type(LemmyErrorType::InstanceBlockAlreadyExists)?;
} }
Ok(Json(BlockInstanceResponse { Ok(Json(SuccessResponse::default()))
blocked: data.block,
}))
} }

View file

@ -43,6 +43,8 @@ use lemmy_db_views_actor::structs::{
PersonView, PersonView,
}; };
use lemmy_db_views_moderator::structs::{ use lemmy_db_views_moderator::structs::{
AdminAllowInstanceView,
AdminBlockInstanceView,
AdminPurgeCommentView, AdminPurgeCommentView,
AdminPurgeCommunityView, AdminPurgeCommunityView,
AdminPurgePersonView, AdminPurgePersonView,
@ -183,6 +185,8 @@ pub struct GetModlogResponse {
pub admin_purged_posts: Vec<AdminPurgePostView>, pub admin_purged_posts: Vec<AdminPurgePostView>,
pub admin_purged_comments: Vec<AdminPurgeCommentView>, pub admin_purged_comments: Vec<AdminPurgeCommentView>,
pub hidden_communities: Vec<ModHideCommunityView>, pub hidden_communities: Vec<ModHideCommunityView>,
pub admin_block_instance: Vec<AdminBlockInstanceView>,
pub admin_allow_instance: Vec<AdminAllowInstanceView>,
} }
#[skip_serializing_none] #[skip_serializing_none]
@ -265,10 +269,6 @@ pub struct CreateSite {
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub captcha_difficulty: Option<String>, pub captcha_difficulty: Option<String>,
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub allowed_instances: Option<Vec<String>>,
#[cfg_attr(feature = "full", ts(optional))]
pub blocked_instances: Option<Vec<String>>,
#[cfg_attr(feature = "full", ts(optional))]
pub registration_mode: Option<RegistrationMode>, pub registration_mode: Option<RegistrationMode>,
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub oauth_registration: Option<bool>, pub oauth_registration: Option<bool>,
@ -394,12 +394,6 @@ pub struct EditSite {
/// The captcha difficulty. Can be easy, medium, or hard /// The captcha difficulty. Can be easy, medium, or hard
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub captcha_difficulty: Option<String>, pub captcha_difficulty: Option<String>,
/// A list of allowed instances. If none are set, federation is open.
#[cfg_attr(feature = "full", ts(optional))]
pub allowed_instances: Option<Vec<String>>,
/// A list of blocked instances.
#[cfg_attr(feature = "full", ts(optional))]
pub blocked_instances: Option<Vec<String>>,
/// A list of blocked URLs /// A list of blocked URLs
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
pub blocked_urls: Option<Vec<String>>, pub blocked_urls: Option<Vec<String>>,
@ -648,15 +642,29 @@ pub struct GetUnreadRegistrationApplicationCountResponse {
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
/// Block an instance as user /// Block an instance as user
pub struct BlockInstance { pub struct UserBlockInstanceParams {
pub instance_id: InstanceId, pub instance_id: InstanceId,
pub block: bool, pub block: bool,
} }
#[skip_serializing_none] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS))] #[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))] #[cfg_attr(feature = "full", ts(export))]
pub struct BlockInstanceResponse { pub struct AdminBlockInstanceParams {
pub blocked: bool, pub instance: String,
pub block: bool,
#[cfg_attr(feature = "full", ts(optional))]
pub reason: Option<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub expires: Option<DateTime<Utc>>,
}
#[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<String>,
} }

View file

@ -23,7 +23,12 @@ use lemmy_db_schema::{
local_site::LocalSite, local_site::LocalSite,
local_site_rate_limit::LocalSiteRateLimit, local_site_rate_limit::LocalSiteRateLimit,
local_site_url_blocklist::LocalSiteUrlBlocklist, local_site_url_blocklist::LocalSiteUrlBlocklist,
moderator::{ModRemoveComment, ModRemoveCommentForm, ModRemovePost, ModRemovePostForm}, mod_log::moderator::{
ModRemoveComment,
ModRemoveCommentForm,
ModRemovePost,
ModRemovePostForm,
},
oauth_account::OAuthAccount, oauth_account::OAuthAccount,
password_reset_request::PasswordResetRequest, password_reset_request::PasswordResetRequest,
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},

View file

@ -12,7 +12,7 @@ use lemmy_db_schema::{
comment::{Comment, CommentUpdateForm}, comment::{Comment, CommentUpdateForm},
comment_report::CommentReport, comment_report::CommentReport,
local_user::LocalUser, local_user::LocalUser,
moderator::{ModRemoveComment, ModRemoveCommentForm}, mod_log::moderator::{ModRemoveComment, ModRemoveCommentForm},
}, },
traits::{Crud, Reportable}, traits::{Crud, Reportable},
}; };

View file

@ -10,7 +10,7 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
community::{Community, CommunityUpdateForm}, community::{Community, CommunityUpdateForm},
moderator::{ModRemoveCommunity, ModRemoveCommunityForm}, mod_log::moderator::{ModRemoveCommunity, ModRemoveCommunityForm},
}, },
traits::Crud, traits::Crud,
}; };

View file

@ -11,7 +11,7 @@ use lemmy_db_schema::{
source::{ source::{
community::Community, community::Community,
local_user::LocalUser, local_user::LocalUser,
moderator::{ModRemovePost, ModRemovePostForm}, mod_log::moderator::{ModRemovePost, ModRemovePostForm},
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
post_report::PostReport, post_report::PostReport,
}, },

View file

@ -19,8 +19,6 @@ use lemmy_api_common::{
use lemmy_db_schema::{ use lemmy_db_schema::{
source::{ source::{
actor_language::SiteLanguage, actor_language::SiteLanguage,
federation_allowlist::FederationAllowList,
federation_blocklist::FederationBlockList,
local_site::{LocalSite, LocalSiteUpdateForm}, local_site::{LocalSite, LocalSiteUpdateForm},
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm}, local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
local_site_url_blocklist::LocalSiteUrlBlocklist, local_site_url_blocklist::LocalSiteUrlBlocklist,
@ -152,12 +150,6 @@ pub async fn update_site(
.await .await
.ok(); .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() { if let Some(url_blocklist) = data.blocked_urls.clone() {
let parsed_urls = check_urls_are_valid(&url_blocklist)?; let parsed_urls = check_urls_are_valid(&url_blocklist)?;
LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?; LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?;

View file

@ -21,8 +21,9 @@ use lemmy_api_common::{
}; };
use lemmy_db_schema::{ use lemmy_db_schema::{
aggregates::structs::PersonAggregates, aggregates::structs::PersonAggregates,
newtypes::{InstanceId, OAuthProviderId}, newtypes::{InstanceId, OAuthProviderId, SiteId},
source::{ source::{
actor_language::SiteLanguage,
captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer}, captcha_answer::{CaptchaAnswer, CheckCaptchaAnswer},
language::Language, language::Language,
local_site::LocalSite, local_site::LocalSite,
@ -145,7 +146,13 @@ pub async fn register(
..LocalUserInsertForm::new(inserted_person.id, Some(data.password.to_string())) ..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 local_site.site_setup && require_registration_application {
if let Some(answer) = data.answer.clone() { if let Some(answer) = data.answer.clone() {
@ -358,7 +365,13 @@ pub async fn authenticate_with_oauth(
..LocalUserInsertForm::new(person.id, None) ..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 // Create the oauth account
let oauth_account_form = let oauth_account_form =
@ -449,15 +462,23 @@ async fn create_local_user(
context: &Data<LemmyContext>, context: &Data<LemmyContext>,
language_tags: Vec<String>, language_tags: Vec<String>,
local_user_form: &LocalUserInsertForm, local_user_form: &LocalUserInsertForm,
local_site_id: SiteId,
) -> Result<LocalUser, LemmyError> { ) -> Result<LocalUser, LemmyError> {
let all_languages = Language::read_all(&mut context.pool()).await?; let all_languages = Language::read_all(&mut context.pool()).await?;
// use hashset to avoid duplicates // use hashset to avoid duplicates
let mut language_ids = HashSet::new(); let mut language_ids = HashSet::new();
// Enable languages from `Accept-Language` header
for l in language_tags { for l in language_tags {
if let Some(found) = all_languages.iter().find(|all| all.code == l) { if let Some(found) = all_languages.iter().find(|all| all.code == l) {
language_ids.insert(found.id); 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 language_ids = language_ids.into_iter().collect();
let inserted_local_user = let inserted_local_user =

View file

@ -36,7 +36,7 @@ use lemmy_db_schema::{
CommunityPersonBan, CommunityPersonBan,
CommunityPersonBanForm, CommunityPersonBanForm,
}, },
moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm},
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},
}, },
traits::{Bannable, Crud, Followable}, traits::{Bannable, Crud, Followable},

View file

@ -27,7 +27,7 @@ use lemmy_db_schema::{
source::{ source::{
activity::ActivitySendTargets, activity::ActivitySendTargets,
community::{CommunityPersonBan, CommunityPersonBanForm}, community::{CommunityPersonBan, CommunityPersonBanForm},
moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm}, mod_log::moderator::{ModBan, ModBanForm, ModBanFromCommunity, ModBanFromCommunityForm},
person::{Person, PersonUpdateForm}, person::{Person, PersonUpdateForm},
}, },
traits::{Bannable, Crud}, traits::{Bannable, Crud},

View file

@ -31,7 +31,7 @@ use lemmy_db_schema::{
source::{ source::{
activity::ActivitySendTargets, activity::ActivitySendTargets,
community::{Community, CommunityModerator, CommunityModeratorForm}, community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModAddCommunity, ModAddCommunityForm}, mod_log::moderator::{ModAddCommunity, ModAddCommunityForm},
person::Person, person::Person,
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
}, },

View file

@ -27,7 +27,7 @@ use lemmy_db_schema::{
source::{ source::{
activity::ActivitySendTargets, activity::ActivitySendTargets,
community::{Community, CommunityModerator, CommunityModeratorForm}, community::{Community, CommunityModerator, CommunityModeratorForm},
moderator::{ModAddCommunity, ModAddCommunityForm}, mod_log::moderator::{ModAddCommunity, ModAddCommunityForm},
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
}, },
traits::{Crud, Joinable}, traits::{Crud, Joinable},

View file

@ -27,7 +27,7 @@ use lemmy_db_schema::{
source::{ source::{
activity::ActivitySendTargets, activity::ActivitySendTargets,
community::Community, community::Community,
moderator::{ModLockPost, ModLockPostForm}, mod_log::moderator::{ModLockPost, ModLockPostForm},
person::Person, person::Person,
post::{Post, PostUpdateForm}, post::{Post, PostUpdateForm},
}, },

View file

@ -14,7 +14,7 @@ use lemmy_db_schema::{
comment::{Comment, CommentUpdateForm}, comment::{Comment, CommentUpdateForm},
comment_report::CommentReport, comment_report::CommentReport,
community::{Community, CommunityUpdateForm}, community::{Community, CommunityUpdateForm},
moderator::{ mod_log::moderator::{
ModRemoveComment, ModRemoveComment,
ModRemoveCommentForm, ModRemoveCommentForm,
ModRemoveCommunity, ModRemoveCommunity,

View file

@ -13,7 +13,7 @@ use lemmy_db_schema::{
source::{ source::{
comment::{Comment, CommentUpdateForm}, comment::{Comment, CommentUpdateForm},
community::{Community, CommunityUpdateForm}, community::{Community, CommunityUpdateForm},
moderator::{ mod_log::moderator::{
ModRemoveComment, ModRemoveComment,
ModRemoveCommentForm, ModRemoveCommentForm,
ModRemoveCommunity, ModRemoveCommunity,

View file

@ -322,7 +322,7 @@ pub(crate) mod tests {
CommunityFollowerState, CommunityFollowerState,
CommunityInsertForm, CommunityInsertForm,
}, },
local_user::LocalUser, person::Person,
}, },
traits::{Crud, Followable}, traits::{Crud, Followable},
}; };
@ -376,8 +376,8 @@ pub(crate) mod tests {
assert_eq!(follows.len(), 1); assert_eq!(follows.len(), 1);
assert_eq!(follows[0].community.actor_id, community.actor_id); assert_eq!(follows[0].community.actor_id, community.actor_id);
LocalUser::delete(pool, export_user.local_user.id).await?; Person::delete(pool, export_user.person.id).await?;
LocalUser::delete(pool, import_user.local_user.id).await?; Person::delete(pool, import_user.person.id).await?;
Ok(()) Ok(())
} }
@ -412,8 +412,8 @@ pub(crate) mod tests {
Some(LemmyErrorType::TooManyItems) Some(LemmyErrorType::TooManyItems)
); );
LocalUser::delete(pool, export_user.local_user.id).await?; Person::delete(pool, export_user.person.id).await?;
LocalUser::delete(pool, import_user.local_user.id).await?; Person::delete(pool, import_user.person.id).await?;
Ok(()) Ok(())
} }

View file

@ -1,60 +1,51 @@
use crate::{ use crate::{
schema::federation_allowlist, newtypes::InstanceId,
schema::{admin_allow_instance, federation_allowlist},
source::{ source::{
federation_allowlist::{FederationAllowList, FederationAllowListForm}, federation_allowlist::{FederationAllowList, FederationAllowListForm},
instance::Instance, mod_log::admin::{AdminAllowInstance, AdminAllowInstanceForm},
}, },
utils::{get_conn, DbPool}, utils::{get_conn, DbPool},
}; };
use diesel::{dsl::insert_into, result::Error}; use diesel::{delete, dsl::insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel_async::{AsyncPgConnection, RunQueryDsl}; use diesel_async::RunQueryDsl;
impl FederationAllowList { impl AdminAllowInstance {
pub async fn replace(pool: &mut DbPool<'_>, list_opt: Option<Vec<String>>) -> Result<(), Error> { pub async fn insert(pool: &mut DbPool<'_>, form: &AdminAllowInstanceForm) -> Result<(), Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
conn insert_into(admin_allow_instance::table)
.build_transaction() .values(form)
.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::<Self>(conn)
.await?;
}
Ok(())
} else {
Ok(())
}
}) as _
})
.await
}
async fn clear(conn: &mut AsyncPgConnection) -> Result<usize, Error> {
diesel::delete(federation_allowlist::table)
.execute(conn) .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)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use super::*;
source::{federation_allowlist::FederationAllowList, instance::Instance}, use crate::{source::instance::Instance, utils::build_db_pool_for_tests};
utils::build_db_pool_for_tests,
};
use diesel::result::Error;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serial_test::serial; use serial_test::serial;
@ -63,31 +54,33 @@ mod tests {
async fn test_allowlist_insert_and_clear() -> Result<(), Error> { async fn test_allowlist_insert_and_clear() -> Result<(), Error> {
let pool = &build_db_pool_for_tests(); let pool = &build_db_pool_for_tests();
let pool = &mut pool.into(); let pool = &mut pool.into();
let domains = vec![ let instances = vec![
"tld1.xyz".to_string(), Instance::read_or_create(pool, "tld1.xyz".to_string()).await?,
"tld2.xyz".to_string(), Instance::read_or_create(pool, "tld2.xyz".to_string()).await?,
"tld3.xyz".to_string(), 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()); for f in &forms {
FederationAllowList::allow(pool, f).await?;
FederationAllowList::replace(pool, allowed).await?; }
let allows = Instance::allowlist(pool).await?; let allows = Instance::allowlist(pool).await?;
let allows_domains = allows
.iter()
.map(|i| i.domain.clone())
.collect::<Vec<String>>();
assert_eq!(3, allows.len()); assert_eq!(3, allows.len());
assert_eq!(domains, allows_domains); assert_eq!(instances, allows);
// Now test clearing them via Some(empty vec) // Now test clearing them
let clear_allows = Some(Vec::new()); for f in forms {
FederationAllowList::unallow(pool, f.instance_id).await?;
FederationAllowList::replace(pool, clear_allows).await?; }
let allows = Instance::allowlist(pool).await?; let allows = Instance::allowlist(pool).await?;
assert_eq!(0, allows.len()); assert_eq!(0, allows.len());
Instance::delete_all(pool).await?; Instance::delete_all(pool).await?;

View file

@ -1,49 +1,42 @@
use crate::{ use crate::{
schema::federation_blocklist, newtypes::InstanceId,
schema::{admin_block_instance, federation_blocklist},
source::{ source::{
federation_blocklist::{FederationBlockList, FederationBlockListForm}, federation_blocklist::{FederationBlockList, FederationBlockListForm},
instance::Instance, mod_log::admin::{AdminBlockInstance, AdminBlockInstanceForm},
}, },
utils::{get_conn, DbPool}, utils::{get_conn, DbPool},
}; };
use diesel::{dsl::insert_into, result::Error}; use diesel::{delete, dsl::insert_into, result::Error, ExpressionMethods, QueryDsl};
use diesel_async::{AsyncPgConnection, RunQueryDsl}; use diesel_async::RunQueryDsl;
impl FederationBlockList { impl AdminBlockInstance {
pub async fn replace(pool: &mut DbPool<'_>, list_opt: Option<Vec<String>>) -> Result<(), Error> { pub async fn insert(pool: &mut DbPool<'_>, form: &AdminBlockInstanceForm) -> Result<(), Error> {
let conn = &mut get_conn(pool).await?; let conn = &mut get_conn(pool).await?;
conn insert_into(admin_block_instance::table)
.build_transaction() .values(form)
.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::<Self>(conn)
.await?;
}
Ok(())
} else {
Ok(())
}
}) as _
})
.await
}
async fn clear(conn: &mut AsyncPgConnection) -> Result<usize, Error> {
diesel::delete(federation_blocklist::table)
.execute(conn) .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(())
} }
} }

View file

@ -21,7 +21,7 @@ pub mod local_site_url_blocklist;
pub mod local_user; pub mod local_user;
pub mod local_user_vote_display_mode; pub mod local_user_vote_display_mode;
pub mod login_token; pub mod login_token;
pub mod moderator; pub mod mod_log;
pub mod oauth_account; pub mod oauth_account;
pub mod oauth_provider; pub mod oauth_provider;
pub mod password_reset_request; pub mod password_reset_request;

View file

@ -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<Self, Error> {
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::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
from_id: i32,
form: &Self::InsertForm,
) -> Result<Self, Error> {
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::<Self>(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<Self, Error> {
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::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
from_id: i32,
form: &Self::InsertForm,
) -> Result<Self, Error> {
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::<Self>(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<Self, Error> {
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::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
from_id: i32,
form: &Self::InsertForm,
) -> Result<Self, Error> {
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::<Self>(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<Self, Error> {
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::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
from_id: i32,
form: &Self::InsertForm,
) -> Result<Self, Error> {
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::<Self>(conn)
.await
}
}

View file

@ -0,0 +1,2 @@
pub mod admin;
pub mod moderator;

View file

@ -1,13 +1,5 @@
use crate::{ use crate::{
source::moderator::{ source::mod_log::moderator::{
AdminPurgeComment,
AdminPurgeCommentForm,
AdminPurgeCommunity,
AdminPurgeCommunityForm,
AdminPurgePerson,
AdminPurgePersonForm,
AdminPurgePost,
AdminPurgePostForm,
ModAdd, ModAdd,
ModAddCommunity, ModAddCommunity,
ModAddCommunityForm, 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<Self, Error> {
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::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
from_id: i32,
form: &Self::InsertForm,
) -> Result<Self, Error> {
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::<Self>(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<Self, Error> {
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::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
from_id: i32,
form: &Self::InsertForm,
) -> Result<Self, Error> {
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::<Self>(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<Self, Error> {
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::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
from_id: i32,
form: &Self::InsertForm,
) -> Result<Self, Error> {
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::<Self>(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<Self, Error> {
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::<Self>(conn)
.await
}
async fn update(
pool: &mut DbPool<'_>,
from_id: i32,
form: &Self::InsertForm,
) -> Result<Self, Error> {
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::<Self>(conn)
.await
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::{ use crate::{
source::{ source::{
comment::{Comment, CommentInsertForm}, comment::{Comment, CommentInsertForm},
community::{Community, CommunityInsertForm}, community::{Community, CommunityInsertForm},
instance::Instance, instance::Instance,
moderator::{
ModAdd,
ModAddCommunity,
ModAddCommunityForm,
ModAddForm,
ModBan,
ModBanForm,
ModBanFromCommunity,
ModBanFromCommunityForm,
ModFeaturePost,
ModFeaturePostForm,
ModLockPost,
ModLockPostForm,
ModRemoveComment,
ModRemoveCommentForm,
ModRemoveCommunity,
ModRemoveCommunityForm,
ModRemovePost,
ModRemovePostForm,
},
person::{Person, PersonInsertForm}, person::{Person, PersonInsertForm},
post::{Post, PostInsertForm}, post::{Post, PostInsertForm},
}, },
traits::Crud,
utils::build_db_pool_for_tests, utils::build_db_pool_for_tests,
}; };
use diesel::result::Error;
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use serial_test::serial; use serial_test::serial;

View file

@ -215,6 +215,8 @@ pub enum ModlogActionType {
AdminPurgeCommunity, AdminPurgeCommunity,
AdminPurgePost, AdminPurgePost,
AdminPurgeComment, AdminPurgeComment,
AdminBlockInstance,
AdminAllowInstance,
} }
#[derive( #[derive(

View file

@ -42,6 +42,29 @@ pub mod sql_types {
pub struct RegistrationModeEnum; pub struct RegistrationModeEnum;
} }
diesel::table! {
admin_allow_instance (id) {
id -> Int4,
instance_id -> Int4,
admin_person_id -> Int4,
allowed -> Bool,
reason -> Nullable<Text>,
when_ -> Timestamptz,
}
}
diesel::table! {
admin_block_instance (id) {
id -> Int4,
instance_id -> Int4,
admin_person_id -> Int4,
blocked -> Bool,
reason -> Nullable<Text>,
expires -> Nullable<Timestamptz>,
when_ -> Timestamptz,
}
}
diesel::table! { diesel::table! {
admin_purge_comment (id) { admin_purge_comment (id) {
id -> Int4, id -> Int4,
@ -284,6 +307,7 @@ diesel::table! {
instance_id -> Int4, instance_id -> Int4,
published -> Timestamptz, published -> Timestamptz,
updated -> Nullable<Timestamptz>, updated -> Nullable<Timestamptz>,
expires -> Nullable<Timestamptz>,
} }
} }
@ -945,6 +969,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 -> person (admin_person_id));
diesel::joinable!(admin_purge_comment -> post (post_id)); diesel::joinable!(admin_purge_comment -> post (post_id));
diesel::joinable!(admin_purge_community -> person (admin_person_id)); diesel::joinable!(admin_purge_community -> person (admin_person_id));
@ -1025,6 +1053,8 @@ diesel::joinable!(site_language -> language (language_id));
diesel::joinable!(site_language -> site (site_id)); diesel::joinable!(site_language -> site (site_id));
diesel::allow_tables_to_appear_in_same_query!( diesel::allow_tables_to_appear_in_same_query!(
admin_allow_instance,
admin_block_instance,
admin_purge_comment, admin_purge_comment,
admin_purge_community, admin_purge_community,
admin_purge_person, admin_purge_person,

View file

@ -1,14 +1,14 @@
use crate::newtypes::InstanceId; use crate::newtypes::InstanceId;
#[cfg(feature = "full")]
use crate::schema::federation_blocklist;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
#[cfg(feature = "full")]
use {crate::schema::federation_blocklist, ts_rs::TS};
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)] #[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr( #[cfg_attr(
feature = "full", feature = "full",
derive(Queryable, Selectable, Associations, Identifiable) derive(TS, Queryable, Selectable, Associations, Identifiable)
)] )]
#[cfg_attr( #[cfg_attr(
feature = "full", feature = "full",
@ -17,10 +17,14 @@ use std::fmt::Debug;
#[cfg_attr(feature = "full", diesel(table_name = federation_blocklist))] #[cfg_attr(feature = "full", diesel(table_name = federation_blocklist))]
#[cfg_attr(feature = "full", diesel(primary_key(instance_id)))] #[cfg_attr(feature = "full", diesel(primary_key(instance_id)))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))] #[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
pub struct FederationBlockList { pub struct FederationBlockList {
pub instance_id: InstanceId, pub instance_id: InstanceId,
pub published: DateTime<Utc>, pub published: DateTime<Utc>,
#[cfg_attr(feature = "full", ts(optional))]
pub updated: Option<DateTime<Utc>>, pub updated: Option<DateTime<Utc>>,
#[cfg_attr(feature = "full", ts(optional))]
pub expires: Option<DateTime<Utc>>,
} }
#[derive(Clone, Default)] #[derive(Clone, Default)]
@ -29,4 +33,5 @@ pub struct FederationBlockList {
pub struct FederationBlockListForm { pub struct FederationBlockListForm {
pub instance_id: InstanceId, pub instance_id: InstanceId,
pub updated: Option<DateTime<Utc>>, pub updated: Option<DateTime<Utc>>,
pub expires: Option<DateTime<Utc>>,
} }

View file

@ -27,7 +27,7 @@ pub mod local_site_url_blocklist;
pub mod local_user; pub mod local_user;
pub mod local_user_vote_display_mode; pub mod local_user_vote_display_mode;
pub mod login_token; pub mod login_token;
pub mod moderator; pub mod mod_log;
pub mod oauth_account; pub mod oauth_account;
pub mod oauth_provider; pub mod oauth_provider;
pub mod password_reset_request; pub mod password_reset_request;

View file

@ -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<String>,
pub when_: DateTime<Utc>,
}
#[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<String>,
}
#[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<String>,
pub when_: DateTime<Utc>,
}
#[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<String>,
}
#[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<String>,
pub when_: DateTime<Utc>,
}
#[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<String>,
}
#[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<String>,
pub when_: DateTime<Utc>,
}
#[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<String>,
}
#[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<String>,
pub when_: DateTime<Utc>,
}
#[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<String>,
}
#[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<String>,
#[cfg_attr(feature = "full", ts(optional))]
pub expires: Option<DateTime<Utc>>,
pub when_: DateTime<Utc>,
}
#[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<String>,
pub when_: Option<DateTime<Utc>>,
}

View file

@ -0,0 +1,2 @@
pub mod admin;
pub mod moderator;

View file

@ -1,10 +1,6 @@
use crate::newtypes::{CommentId, CommunityId, PersonId, PostId}; use crate::newtypes::{CommentId, CommunityId, PersonId, PostId};
#[cfg(feature = "full")] #[cfg(feature = "full")]
use crate::schema::{ use crate::schema::{
admin_purge_comment,
admin_purge_community,
admin_purge_person,
admin_purge_post,
mod_add, mod_add,
mod_add_community, mod_add_community,
mod_ban, mod_ban,
@ -300,95 +296,3 @@ pub struct ModAddForm {
pub other_person_id: PersonId, pub other_person_id: PersonId,
pub removed: Option<bool>, pub removed: Option<bool>,
} }
#[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<String>,
pub when_: DateTime<Utc>,
}
#[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<String>,
}
#[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<String>,
pub when_: DateTime<Utc>,
}
#[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<String>,
}
#[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<String>,
pub when_: DateTime<Utc>,
}
#[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<String>,
}
#[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<String>,
pub when_: DateTime<Utc>,
}
#[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<String>,
}

View file

@ -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<Vec<Self>, 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::<diesel::sql_types::Bool>();
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::<Self>(conn)
.await
}
}

View file

@ -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<Vec<Self>, 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::<diesel::sql_types::Bool>();
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::<Self>(conn)
.await
}
}

View file

@ -1,4 +1,8 @@
#[cfg(feature = "full")] #[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; pub mod admin_purge_comment_view;
#[cfg(feature = "full")] #[cfg(feature = "full")]
pub mod admin_purge_community_view; pub mod admin_purge_community_view;

View file

@ -5,22 +5,29 @@ use lemmy_db_schema::{
source::{ source::{
comment::Comment, comment::Comment,
community::Community, community::Community,
moderator::{ instance::Instance,
AdminPurgeComment, mod_log::{
AdminPurgeCommunity, admin::{
AdminPurgePerson, AdminAllowInstance,
AdminPurgePost, AdminBlockInstance,
ModAdd, AdminPurgeComment,
ModAddCommunity, AdminPurgeCommunity,
ModBan, AdminPurgePerson,
ModBanFromCommunity, AdminPurgePost,
ModFeaturePost, },
ModHideCommunity, moderator::{
ModLockPost, ModAdd,
ModRemoveComment, ModAddCommunity,
ModRemoveCommunity, ModBan,
ModRemovePost, ModBanFromCommunity,
ModTransferCommunity, ModFeaturePost,
ModHideCommunity,
ModLockPost,
ModRemoveComment,
ModRemoveCommunity,
ModRemovePost,
ModTransferCommunity,
},
}, },
person::Person, person::Person,
post::Post, post::Post,
@ -233,6 +240,32 @@ pub struct AdminPurgePostView {
pub community: Community, 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<Person>,
}
#[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<Person>,
}
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[cfg_attr(feature = "full", derive(TS, Queryable))] #[cfg_attr(feature = "full", derive(TS, Queryable))]

View file

@ -199,10 +199,14 @@ mod test {
use super::*; use super::*;
use activitypub_federation::config::Data; use activitypub_federation::config::Data;
use chrono::DateTime; use chrono::DateTime;
use lemmy_db_schema::source::{ use lemmy_db_schema::{
federation_allowlist::FederationAllowList, source::{
federation_blocklist::FederationBlockList, federation_allowlist::{FederationAllowList, FederationAllowListForm},
instance::InstanceForm, federation_blocklist::{FederationBlockList, FederationBlockListForm},
instance::InstanceForm,
person::{Person, PersonInsertForm},
},
traits::Crud,
}; };
use lemmy_utils::error::LemmyError; use lemmy_utils::error::LemmyError;
use serial_test::serial; use serial_test::serial;
@ -318,14 +322,22 @@ mod test {
async fn test_send_manager_blocked() -> LemmyResult<()> { async fn test_send_manager_blocked() -> LemmyResult<()> {
let mut data = TestData::init(1, 1).await?; let mut data = TestData::init(1, 1).await?;
let domain = data.instances[0].domain.clone(); let instance_id = data.instances[0].id;
FederationBlockList::replace(&mut data.context.pool(), Some(vec![domain])).await?; 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?; data.run().await?;
let workers = &data.send_manager.workers; let workers = &data.send_manager.workers;
assert_eq!(2, workers.len()); assert_eq!(2, workers.len());
assert!(workers.contains_key(&data.instances[1].id)); assert!(workers.contains_key(&data.instances[1].id));
assert!(workers.contains_key(&data.instances[2].id)); assert!(workers.contains_key(&data.instances[2].id));
Person::delete(&mut data.context.pool(), person.id).await?;
data.cleanup().await?; data.cleanup().await?;
Ok(()) Ok(())
} }
@ -336,13 +348,20 @@ mod test {
async fn test_send_manager_allowed() -> LemmyResult<()> { async fn test_send_manager_allowed() -> LemmyResult<()> {
let mut data = TestData::init(1, 1).await?; let mut data = TestData::init(1, 1).await?;
let domain = data.instances[0].domain.clone(); let instance_id = data.instances[0].id;
FederationAllowList::replace(&mut data.context.pool(), Some(vec![domain])).await?; 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?; data.run().await?;
let workers = &data.send_manager.workers; let workers = &data.send_manager.workers;
assert_eq!(1, workers.len()); assert_eq!(1, workers.len());
assert!(workers.contains_key(&data.instances[0].id)); assert!(workers.contains_key(&data.instances[0].id));
Person::delete(&mut data.context.pool(), person.id).await?;
data.cleanup().await?; data.cleanup().await?;
Ok(()) Ok(())
} }

View file

@ -1,13 +1,14 @@
use actix_web::{ use actix_web::{
body::BodyStream, body::{BodyStream, BoxBody},
http::{ http::{
header::{HeaderName, ACCEPT_ENCODING, HOST}, header::{HeaderName, ACCEPT_ENCODING, HOST},
Method, Method,
StatusCode, StatusCode,
}, },
web::{self, Query}, web::*,
HttpRequest, HttpRequest,
HttpResponse, HttpResponse,
Responder,
}; };
use futures::stream::{Stream, StreamExt}; use futures::stream::{Stream, StreamExt};
use http::HeaderValue; use http::HeaderValue;
@ -24,22 +25,18 @@ use serde::Deserialize;
use std::time::Duration; use std::time::Duration;
use url::Url; use url::Url;
pub fn config( pub fn config(cfg: &mut ServiceConfig, client: ClientWithMiddleware, rate_limit: &RateLimitCell) {
cfg: &mut web::ServiceConfig,
client: ClientWithMiddleware,
rate_limit: &RateLimitCell,
) {
cfg cfg
.app_data(web::Data::new(client)) .app_data(Data::new(client))
.service( .service(
web::resource("/pictrs/image") resource("/pictrs/image")
.wrap(rate_limit.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 // This has optional query params: /image/{filename}?format=jpg&thumbnail=256
.service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res))) .service(resource("/pictrs/image/{filename}").route(get().to(full_res)))
.service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete))) .service(resource("/pictrs/image/delete/{token}/{filename}").route(get().to(delete)))
.service(web::resource("/pictrs/healthz").route(web::get().to(healthz))); .service(resource("/pictrs/healthz").route(get().to(healthz)));
} }
trait ProcessUrl { trait ProcessUrl {
@ -129,11 +126,11 @@ fn adapt_request(
async fn upload( async fn upload(
req: HttpRequest, req: HttpRequest,
body: web::Payload, body: Payload,
// require login // require login
local_user_view: LocalUserView, local_user_view: LocalUserView,
client: web::Data<ClientWithMiddleware>, client: Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
// TODO: check rate limit here // TODO: check rate limit here
let pictrs_config = context.settings().pictrs_config()?; let pictrs_config = context.settings().pictrs_config()?;
@ -173,11 +170,11 @@ async fn upload(
} }
async fn full_res( async fn full_res(
filename: web::Path<String>, filename: Path<String>,
web::Query(params): web::Query<PictrsGetParams>, Query(params): Query<PictrsGetParams>,
req: HttpRequest, req: HttpRequest,
client: web::Data<ClientWithMiddleware>, client: Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>, context: Data<LemmyContext>,
local_user_view: Option<LocalUserView>, local_user_view: Option<LocalUserView>,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
// block access to images if instance is private and unauthorized, public // block access to images if instance is private and unauthorized, public
@ -226,10 +223,10 @@ async fn image(
} }
async fn delete( async fn delete(
components: web::Path<(String, String)>, components: Path<(String, String)>,
req: HttpRequest, req: HttpRequest,
client: web::Data<ClientWithMiddleware>, client: Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>, context: Data<LemmyContext>,
// require login // require login
_local_user_view: LocalUserView, _local_user_view: LocalUserView,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
@ -253,8 +250,8 @@ async fn delete(
async fn healthz( async fn healthz(
req: HttpRequest, req: HttpRequest,
client: web::Data<ClientWithMiddleware>, client: Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<HttpResponse> {
let pictrs_config = context.settings().pictrs_config()?; let pictrs_config = context.settings().pictrs_config()?;
let url = format!("{}healthz", pictrs_config.url); let url = format!("{}healthz", pictrs_config.url);
@ -273,9 +270,9 @@ async fn healthz(
pub async fn image_proxy( pub async fn image_proxy(
Query(params): Query<ImageProxyParams>, Query(params): Query<ImageProxyParams>,
req: HttpRequest, req: HttpRequest,
client: web::Data<ClientWithMiddleware>, client: Data<ClientWithMiddleware>,
context: web::Data<LemmyContext>, context: Data<LemmyContext>,
) -> LemmyResult<HttpResponse> { ) -> LemmyResult<Either<HttpResponse<()>, HttpResponse<BoxBody>>> {
let url = Url::parse(&params.url)?; let url = Url::parse(&params.url)?;
// Check that url corresponds to a federated image so that this can't be abused as a proxy // 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?; RemoteImage::validate(&mut context.pool(), url.clone().into()).await?;
let pictrs_config = context.settings().pictrs_config()?; let pictrs_config = context.settings().pictrs_config()?;
let processed_url = params.process_url(&params.url, &pictrs_config.url); let processed_url = params.process_url(&params.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<S>(mut stream: S) -> impl Stream<Item = S::Item> + Send + Unpin + 'static fn make_send<S>(mut stream: S) -> impl Stream<Item = S::Item> + Send + Unpin + 'static

View file

@ -151,6 +151,7 @@ pub enum LemmyErrorType {
CommunityHasNoFollowers, CommunityHasNoFollowers,
PostScheduleTimeMustBeInFuture, PostScheduleTimeMustBeInFuture,
TooManyScheduledPosts, TooManyScheduledPosts,
CannotCombineFederationBlocklistAndAllowlist,
FederationError { FederationError {
#[cfg_attr(feature = "full", ts(optional))] #[cfg_attr(feature = "full", ts(optional))]
error: Option<FederationError>, error: Option<FederationError>,

View file

@ -88,6 +88,15 @@ pub struct PictrsConfig {
#[default(PictrsImageMode::StoreLinkPreviews)] #[default(PictrsImageMode::StoreLinkPreviews)]
pub(super) image_mode: PictrsImageMode, 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<String>,
/// Timeout for uploading images to pictrs (in seconds) /// Timeout for uploading images to pictrs (in seconds)
#[default(30)] #[default(30)]
pub upload_timeout: u64, pub upload_timeout: u64,

View file

@ -0,0 +1,7 @@
ALTER TABLE federation_blocklist
DROP expires;
DROP TABLE admin_block_instance;
DROP TABLE admin_allow_instance;

View file

@ -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()
);

View file

@ -1,4 +1,4 @@
use actix_web::{guard, web}; use actix_web::{guard, web::*};
use lemmy_api::{ use lemmy_api::{
comment::{ comment::{
distinguish::distinguish_comment, distinguish::distinguish_comment,
@ -79,7 +79,8 @@ use lemmy_api::{
report_combined::list::list_reports, report_combined::list::list_reports,
}, },
site::{ site::{
block::block_instance, admin_allow_instance::admin_allow_instance,
admin_block_instance::admin_block_instance,
federated_instances::get_federated_instances, federated_instances::get_federated_instances,
leave_admin::leave_admin, leave_admin::leave_admin,
list_all_media::list_all_media, list_all_media::list_all_media,
@ -96,6 +97,7 @@ use lemmy_api::{
list::list_registration_applications, list::list_registration_applications,
unread_count::get_unread_registration_application_count, unread_count::get_unread_registration_application_count,
}, },
user_block_instance::user_block_instance,
}, },
sitemap::get_sitemap, sitemap::get_sitemap,
}; };
@ -162,154 +164,151 @@ use lemmy_apub::api::{
use lemmy_routes::images::image_proxy; use lemmy_routes::images::image_proxy;
use lemmy_utils::rate_limit::RateLimitCell; 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( cfg.service(
web::scope("/api/v3") scope("/api/v3")
.route("/image_proxy", web::get().to(image_proxy)) .route("/image_proxy", get().to(image_proxy))
// Site // Site
.service( .service(
web::scope("/site") scope("/site")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::get().to(get_site)) .route("", get().to(get_site))
// Admin Actions // Admin Actions
.route("", web::post().to(create_site)) .route("", post().to(create_site))
.route("", web::put().to(update_site)) .route("", put().to(update_site))
.route("/block", web::post().to(block_instance)), .route("/block", post().to(user_block_instance)),
) )
.service( .service(
web::resource("/modlog") resource("/modlog")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route(web::get().to(get_mod_log)), .route(get().to(get_mod_log)),
) )
.service( .service(
web::resource("/search") resource("/search")
.wrap(rate_limit.search()) .wrap(rate_limit.search())
.route(web::get().to(search)), .route(get().to(search)),
) )
.service( .service(
web::resource("/resolve_object") resource("/resolve_object")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route(web::get().to(resolve_object)), .route(get().to(resolve_object)),
) )
// Community // Community
.service( .service(
web::resource("/community") resource("/community")
.guard(guard::Post()) .guard(guard::Post())
.wrap(rate_limit.register()) .wrap(rate_limit.register())
.route(web::post().to(create_community)), .route(post().to(create_community)),
) )
.service( .service(
web::scope("/community") scope("/community")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::get().to(get_community)) .route("", get().to(get_community))
.route("", web::put().to(update_community)) .route("", put().to(update_community))
.route("/random", web::get().to(get_random_community)) .route("/random", get().to(get_random_community))
.route("/hide", web::put().to(hide_community)) .route("/hide", put().to(hide_community))
.route("/list", web::get().to(list_communities)) .route("/list", get().to(list_communities))
.route("/follow", web::post().to(follow_community)) .route("/follow", post().to(follow_community))
.route("/block", web::post().to(block_community)) .route("/block", post().to(block_community))
.route("/delete", web::post().to(delete_community)) .route("/delete", post().to(delete_community))
// Mod Actions // Mod Actions
.route("/remove", web::post().to(remove_community)) .route("/remove", post().to(remove_community))
.route("/transfer", web::post().to(transfer_community)) .route("/transfer", post().to(transfer_community))
.route("/ban_user", web::post().to(ban_from_community)) .route("/ban_user", post().to(ban_from_community))
.route("/mod", web::post().to(add_mod_to_community)) .route("/mod", post().to(add_mod_to_community))
.service( .service(
web::scope("/pending_follows") scope("/pending_follows")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("/count", web::get().to(get_pending_follows_count)) .route("/count", get().to(get_pending_follows_count))
.route("/list", web::get().to(get_pending_follows_list)) .route("/list", get().to(get_pending_follows_list))
.route("/approve", web::post().to(post_pending_follows_approve)), .route("/approve", post().to(post_pending_follows_approve)),
), ),
) )
.service( .service(
web::scope("/federated_instances") scope("/federated_instances")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::get().to(get_federated_instances)), .route("", get().to(get_federated_instances)),
) )
// Post // Post
.service( .service(
// Handle POST to /post separately to add the post() rate limitter // Handle POST to /post separately to add the post() rate limitter
web::resource("/post") resource("/post")
.guard(guard::Post()) .guard(guard::Post())
.wrap(rate_limit.post()) .wrap(rate_limit.post())
.route(web::post().to(create_post)), .route(post().to(create_post)),
) )
.service( .service(
web::scope("/post") scope("/post")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::get().to(get_post)) .route("", get().to(get_post))
.route("", web::put().to(update_post)) .route("", put().to(update_post))
.route("/delete", web::post().to(delete_post)) .route("/delete", post().to(delete_post))
.route("/remove", web::post().to(remove_post)) .route("/remove", post().to(remove_post))
.route("/mark_as_read", web::post().to(mark_post_as_read)) .route("/mark_as_read", post().to(mark_post_as_read))
.route("/mark_many_as_read", web::post().to(mark_posts_as_read)) .route("/mark_many_as_read", post().to(mark_posts_as_read))
.route("/hide", web::post().to(hide_post)) .route("/hide", post().to(hide_post))
.route("/lock", web::post().to(lock_post)) .route("/lock", post().to(lock_post))
.route("/feature", web::post().to(feature_post)) .route("/feature", post().to(feature_post))
.route("/list", web::get().to(list_posts)) .route("/list", get().to(list_posts))
.route("/like", web::post().to(like_post)) .route("/like", post().to(like_post))
.route("/like/list", web::get().to(list_post_likes)) .route("/like/list", get().to(list_post_likes))
.route("/save", web::put().to(save_post)) .route("/save", put().to(save_post))
// TODO should these be moved into the new report heading? .route("/report", post().to(create_post_report))
.route("/report", web::post().to(create_post_report)) .route("/report/resolve", put().to(resolve_post_report))
.route("/report/resolve", web::put().to(resolve_post_report)) .route("/report/list", get().to(list_post_reports))
.route("/report/list", web::get().to(list_post_reports)) .route("/site_metadata", get().to(get_link_metadata)),
.route("/site_metadata", web::get().to(get_link_metadata)),
) )
// Comment // Comment
.service( .service(
// Handle POST to /comment separately to add the comment() rate limitter // Handle POST to /comment separately to add the comment() rate limitter
web::resource("/comment") resource("/comment")
.guard(guard::Post()) .guard(guard::Post())
.wrap(rate_limit.comment()) .wrap(rate_limit.comment())
.route(web::post().to(create_comment)), .route(post().to(create_comment)),
) )
.service( .service(
web::scope("/comment") scope("/comment")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::get().to(get_comment)) .route("", get().to(get_comment))
.route("", web::put().to(update_comment)) .route("", put().to(update_comment))
.route("/delete", web::post().to(delete_comment)) .route("/delete", post().to(delete_comment))
.route("/remove", web::post().to(remove_comment)) .route("/remove", post().to(remove_comment))
.route("/mark_as_read", web::post().to(mark_reply_as_read)) .route("/mark_as_read", post().to(mark_reply_as_read))
.route("/distinguish", web::post().to(distinguish_comment)) .route("/distinguish", post().to(distinguish_comment))
.route("/like", web::post().to(like_comment)) .route("/like", post().to(like_comment))
.route("/like/list", web::get().to(list_comment_likes)) .route("/like/list", get().to(list_comment_likes))
.route("/save", web::put().to(save_comment)) .route("/save", put().to(save_comment))
.route("/list", web::get().to(list_comments)) .route("/list", get().to(list_comments))
// TODO should these be moved into the new report heading? .route("/report", post().to(create_comment_report))
.route("/report", web::post().to(create_comment_report)) .route("/report/resolve", put().to(resolve_comment_report))
.route("/report/resolve", web::put().to(resolve_comment_report)) .route("/report/list", get().to(list_comment_reports)),
.route("/report/list", web::get().to(list_comment_reports)),
) )
.service( .service(
web::scope("report") scope("report")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("/list", web::get().to(list_reports)), .route("/list", web::get().to(list_reports)),
) )
// Private Message // Private Message
.service( .service(
web::scope("/private_message") scope("/private_message")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("/list", web::get().to(get_private_message)) .route("/list", get().to(get_private_message))
.route("", web::post().to(create_private_message)) .route("", post().to(create_private_message))
.route("", web::put().to(update_private_message)) .route("", put().to(update_private_message))
.route("/delete", web::post().to(delete_private_message)) .route("/delete", post().to(delete_private_message))
.route("/mark_as_read", web::post().to(mark_pm_as_read)) .route("/mark_as_read", post().to(mark_pm_as_read))
// TODO should these be moved into the new report heading? .route("/report", post().to(create_pm_report))
.route("/report", web::post().to(create_pm_report)) .route("/report/resolve", put().to(resolve_pm_report))
.route("/report/resolve", web::put().to(resolve_pm_report)) .route("/report/list", get().to(list_pm_reports)),
.route("/report/list", web::get().to(list_pm_reports)),
) )
// User // User
.service( .service(
// Account action, I don't like that it's in /user maybe /accounts // Account action, I don't like that it's in /user maybe /accounts
// Handle /user/register separately to add the register() rate limiter // Handle /user/register separately to add the register() rate limiter
web::resource("/user/register") resource("/user/register")
.guard(guard::Post()) .guard(guard::Post())
.wrap(rate_limit.register()) .wrap(rate_limit.register())
.route(web::post().to(register)), .route(post().to(register)),
) )
// User // User
.service( .service(
@ -317,138 +316,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 // 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 // group them under a common path so that rate limit is only applied once (eg under
// /account). // /account).
web::resource("/user/login") resource("/user/login")
.guard(guard::Post()) .guard(guard::Post())
.wrap(rate_limit.register()) .wrap(rate_limit.register())
.route(web::post().to(login)), .route(post().to(login)),
) )
.service( .service(
web::resource("/user/password_reset") resource("/user/password_reset")
.wrap(rate_limit.register()) .wrap(rate_limit.register())
.route(web::post().to(reset_password)), .route(post().to(reset_password)),
) )
.service( .service(
// Handle captcha separately // Handle captcha separately
web::resource("/user/get_captcha") resource("/user/get_captcha")
.wrap(rate_limit.post()) .wrap(rate_limit.post())
.route(web::get().to(get_captcha)), .route(get().to(get_captcha)),
) )
.service( .service(
web::resource("/user/export_settings") resource("/user/export_settings")
.wrap(rate_limit.import_user_settings()) .wrap(rate_limit.import_user_settings())
.route(web::get().to(export_settings)), .route(get().to(export_settings)),
) )
.service( .service(
web::resource("/user/import_settings") resource("/user/import_settings")
.wrap(rate_limit.import_user_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 // TODO, all the current account related actions under /user need to get moved here eventually
.service( .service(
web::scope("/account") scope("/account")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("/list_media", web::get().to(list_media)), .route("/list_media", get().to(list_media)),
) )
// User actions // User actions
.service( .service(
web::scope("/user") scope("/user")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::get().to(read_person)) .route("", get().to(read_person))
.route("/mention", web::get().to(list_mentions)) .route("/mention", get().to(list_mentions))
.route( .route(
"/mention/mark_as_read", "/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 // Admin action. I don't like that it's in /user
.route("/ban", web::post().to(ban_from_site)) .route("/ban", post().to(ban_from_site))
.route("/banned", web::get().to(list_banned_users)) .route("/banned", get().to(list_banned_users))
.route("/block", web::post().to(block_person)) .route("/block", post().to(block_person))
// TODO Account actions. I don't like that they're in /user maybe /accounts // TODO Account actions. I don't like that they're in /user maybe /accounts
.route("/logout", web::post().to(logout)) .route("/logout", post().to(logout))
.route("/delete_account", web::post().to(delete_account)) .route("/delete_account", post().to(delete_account))
.route( .route("/password_change", post().to(change_password_after_reset))
"/password_change",
web::post().to(change_password_after_reset),
)
// TODO mark_all_as_read feels off being in this section as well // TODO mark_all_as_read feels off being in this section as well
.route( .route("/mark_all_as_read", post().to(mark_all_notifications_read))
"/mark_all_as_read", .route("/save_user_settings", put().to(save_user_settings))
web::post().to(mark_all_notifications_read), .route("/change_password", put().to(change_password))
) .route("/report_count", get().to(report_count))
.route("/save_user_settings", web::put().to(save_user_settings)) .route("/unread_count", get().to(unread_count))
.route("/change_password", web::put().to(change_password)) .route("/verify_email", post().to(verify_email))
.route("/report_count", web::get().to(report_count)) .route("/leave_admin", post().to(leave_admin))
.route("/unread_count", web::get().to(unread_count)) .route("/totp/generate", post().to(generate_totp_secret))
.route("/verify_email", web::post().to(verify_email)) .route("/totp/update", post().to(update_totp))
.route("/leave_admin", web::post().to(leave_admin)) .route("/list_logins", get().to(list_logins))
.route("/totp/generate", web::post().to(generate_totp_secret)) .route("/validate_auth", get().to(validate_auth)),
.route("/totp/update", web::post().to(update_totp))
.route("/list_logins", web::get().to(list_logins))
.route("/validate_auth", web::get().to(validate_auth)),
) )
// Admin Actions // Admin Actions
.service( .service(
web::scope("/admin") scope("/admin")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("/add", web::post().to(add_admin)) .route("/add", post().to(add_admin))
.route( .route(
"/registration_application/count", "/registration_application/count",
web::get().to(get_unread_registration_application_count), get().to(get_unread_registration_application_count),
) )
.route( .route(
"/registration_application/list", "/registration_application/list",
web::get().to(list_registration_applications), get().to(list_registration_applications),
) )
.route( .route(
"/registration_application/approve", "/registration_application/approve",
web::put().to(approve_registration_application), put().to(approve_registration_application),
) )
.route( .route(
"/registration_application", "/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( .service(
web::scope("/purge") scope("/purge")
.route("/person", web::post().to(purge_person)) .route("/person", post().to(purge_person))
.route("/community", web::post().to(purge_community)) .route("/community", post().to(purge_community))
.route("/post", web::post().to(purge_post)) .route("/post", post().to(purge_post))
.route("/comment", web::post().to(purge_comment)), .route("/comment", post().to(purge_comment)),
) )
.service( .service(
web::scope("/tagline") scope("/tagline")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::post().to(create_tagline)) .route("", post().to(create_tagline))
.route("", web::put().to(update_tagline)) .route("", put().to(update_tagline))
.route("/delete", web::post().to(delete_tagline)) .route("/delete", post().to(delete_tagline))
.route("/list", web::get().to(list_taglines)), .route("/list", get().to(list_taglines)),
), )
.route("block_instance", post().to(admin_block_instance))
.route("allow_instance", post().to(admin_allow_instance)),
) )
.service( .service(
web::scope("/custom_emoji") scope("/custom_emoji")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::post().to(create_custom_emoji)) .route("", post().to(create_custom_emoji))
.route("", web::put().to(update_custom_emoji)) .route("", put().to(update_custom_emoji))
.route("/delete", web::post().to(delete_custom_emoji)) .route("/delete", post().to(delete_custom_emoji))
.route("/list", web::get().to(list_custom_emojis)), .route("/list", get().to(list_custom_emojis)),
) )
.service( .service(
web::scope("/oauth_provider") scope("/oauth_provider")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::post().to(create_oauth_provider)) .route("", post().to(create_oauth_provider))
.route("", web::put().to(update_oauth_provider)) .route("", put().to(update_oauth_provider))
.route("/delete", web::post().to(delete_oauth_provider)), .route("/delete", post().to(delete_oauth_provider)),
) )
.service( .service(
web::scope("/oauth") scope("/oauth")
.wrap(rate_limit.register()) .wrap(rate_limit.register())
.route("/authenticate", web::post().to(authenticate_with_oauth)), .route("/authenticate", post().to(authenticate_with_oauth)),
), ),
); );
cfg.service( cfg.service(
web::scope("/sitemap.xml") scope("/sitemap.xml")
.wrap(rate_limit.message()) .wrap(rate_limit.message())
.route("", web::get().to(get_sitemap)), .route("", get().to(get_sitemap)),
); );
} }

View file

@ -23,6 +23,7 @@ use lemmy_db_schema::{
comment, comment,
community, community,
community_actions, community_actions,
federation_blocklist,
instance, instance,
person, person,
post, post,
@ -58,6 +59,7 @@ pub async fn setup(context: Data<LemmyContext>) -> LemmyResult<()> {
async move { async move {
active_counts(&mut context.pool()).await; active_counts(&mut context.pool()).await;
update_banned_when_expired(&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; active_counts(pool).await;
update_hot_ranks(pool).await; update_hot_ranks(pool).await;
update_banned_when_expired(pool).await; update_banned_when_expired(pool).await;
delete_instance_block_when_expired(pool).await;
clear_old_activities(pool).await; clear_old_activities(pool).await;
overwrite_deleted_posts_and_comments(pool).await; overwrite_deleted_posts_and_comments(pool).await;
delete_old_denied_users(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. /// Find all unpublished posts with scheduled date in the future, and publish them.
async fn publish_scheduled_posts(context: &Data<LemmyContext>) { async fn publish_scheduled_posts(context: &Data<LemmyContext>) {
let pool = &mut context.pool(); let pool = &mut context.pool();