Instance blocks with mod log entry and expiration (fixes #2506)

This commit is contained in:
Felix Ableitner 2024-11-19 16:07:53 +01:00
parent 417e18e819
commit 2a1d3c9db8
15 changed files with 201 additions and 110 deletions

View file

@ -0,0 +1,37 @@
use activitypub_federation::config::Data;
use actix_web::web::Json;
use lemmy_api_common::{
context::LemmyContext,
site::{AdminBlockInstance, AdminBlockInstanceResponse},
utils::is_admin,
};
use lemmy_db_schema::source::federation_blocklist::{FederationBlockList, FederationBlockListForm};
use lemmy_db_views::structs::LocalUserView;
use lemmy_utils::error::LemmyResult;
#[tracing::instrument(skip(context))]
pub async fn block_instance(
data: Json<AdminBlockInstance>,
local_user_view: LocalUserView,
context: Data<LemmyContext>,
) -> LemmyResult<Json<AdminBlockInstanceResponse>> {
is_admin(&local_user_view)?;
let instance_block_form = FederationBlockListForm {
instance_id: data.instance_id,
admin_person_id: Some(local_user_view.person.id),
reason: data.reason.clone(),
expires: data.expires,
updated: None,
};
if data.block {
FederationBlockList::block(&mut context.pool(), &instance_block_form).await?;
} else {
FederationBlockList::unblock(&mut context.pool(), &instance_block_form).await?;
}
Ok(Json(AdminBlockInstanceResponse {
blocked: data.block,
}))
}

View file

@ -1,3 +1,4 @@
pub mod admin_block_instance;
pub mod block;
pub mod federated_instances;
pub mod leave_admin;

View file

@ -7,21 +7,10 @@ use lemmy_api_common::{
use lemmy_db_schema::{source::local_site::LocalSite, ModlogActionType};
use lemmy_db_views::structs::LocalUserView;
use lemmy_db_views_moderator::structs::{
AdminPurgeCommentView,
AdminPurgeCommunityView,
AdminPurgePersonView,
AdminPurgePostView,
ModAddCommunityView,
ModAddView,
ModBanFromCommunityView,
ModBanView,
ModFeaturePostView,
ModHideCommunityView,
ModLockPostView,
ModRemoveCommentView,
ModRemoveCommunityView,
ModRemovePostView,
ModTransferCommunityView,
AdminBlockInstanceView, AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView,
AdminPurgePostView, ModAddCommunityView, ModAddView,
ModBanFromCommunityView, ModBanView, ModFeaturePostView, ModHideCommunityView, ModLockPostView,
ModRemoveCommentView, ModRemoveCommunityView, ModRemovePostView, ModTransferCommunityView,
ModlogListParams,
};
use lemmy_utils::error::LemmyResult;
@ -121,6 +110,7 @@ pub async fn get_mod_log(
admin_purged_communities,
admin_purged_posts,
admin_purged_comments,
admin_block_instance,
) = if data.community_id.is_none() {
(
match type_ {
@ -161,6 +151,12 @@ pub async fn get_mod_log(
}
_ => Default::default(),
},
match type_ {
All | AdminBlockInstance if other_person_id.is_none() => {
AdminBlockInstanceView::list(&mut context.pool(), params).await?
}
_ => Default::default(),
},
)
} else {
Default::default()
@ -183,5 +179,6 @@ pub async fn get_mod_log(
admin_purged_posts,
admin_purged_comments,
hidden_communities,
admin_block_instance,
}))
}

View file

@ -2,13 +2,7 @@ use crate::federate_retry_sleep_duration;
use chrono::{DateTime, Utc};
use lemmy_db_schema::{
newtypes::{
CommentId,
CommunityId,
InstanceId,
LanguageId,
PersonId,
PostId,
RegistrationApplicationId,
CommentId, CommunityId, InstanceId, LanguageId, PersonId, PostId, RegistrationApplicationId,
},
source::{
community::Community,
@ -20,44 +14,20 @@ use lemmy_db_schema::{
person::Person,
tagline::Tagline,
},
CommentSortType,
FederationMode,
ListingType,
ModlogActionType,
PostListingMode,
PostSortType,
RegistrationMode,
SearchType,
CommentSortType, FederationMode, ListingType, ModlogActionType, PostListingMode, PostSortType,
RegistrationMode, SearchType,
};
use lemmy_db_views::structs::{
CommentView,
LocalUserView,
PostView,
RegistrationApplicationView,
SiteView,
CommentView, LocalUserView, PostView, RegistrationApplicationView, SiteView,
};
use lemmy_db_views_actor::structs::{
CommunityFollowerView,
CommunityModeratorView,
CommunityView,
PersonView,
CommunityFollowerView, CommunityModeratorView, CommunityView, PersonView,
};
use lemmy_db_views_moderator::structs::{
AdminPurgeCommentView,
AdminPurgeCommunityView,
AdminPurgePersonView,
AdminPurgePostView,
ModAddCommunityView,
ModAddView,
ModBanFromCommunityView,
ModBanView,
ModFeaturePostView,
ModHideCommunityView,
ModLockPostView,
ModRemoveCommentView,
ModRemoveCommunityView,
ModRemovePostView,
ModTransferCommunityView,
AdminBlockInstanceView, AdminPurgeCommentView, AdminPurgeCommunityView, AdminPurgePersonView,
AdminPurgePostView, ModAddCommunityView, ModAddView, ModBanFromCommunityView, ModBanView,
ModFeaturePostView, ModHideCommunityView, ModLockPostView, ModRemoveCommentView,
ModRemoveCommunityView, ModRemovePostView, ModTransferCommunityView,
};
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
@ -183,6 +153,7 @@ pub struct GetModlogResponse {
pub admin_purged_posts: Vec<AdminPurgePostView>,
pub admin_purged_comments: Vec<AdminPurgeCommentView>,
pub hidden_communities: Vec<ModHideCommunityView>,
pub admin_block_instance: Vec<AdminBlockInstanceView>,
}
#[skip_serializing_none]
@ -660,3 +631,20 @@ pub struct BlockInstance {
pub struct BlockInstanceResponse {
pub blocked: bool,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct AdminBlockInstance {
pub instance_id: InstanceId,
pub block: bool,
pub reason: Option<String>,
pub expires: Option<DateTime<Utc>>,
}
#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "full", derive(TS))]
#[cfg_attr(feature = "full", ts(export))]
pub struct AdminBlockInstanceResponse {
pub blocked: bool,
}

View file

@ -19,8 +19,6 @@ use lemmy_api_common::{
use lemmy_db_schema::{
source::{
actor_language::SiteLanguage,
federation_allowlist::FederationAllowList,
federation_blocklist::FederationBlockList,
local_site::{LocalSite, LocalSiteUpdateForm},
local_site_rate_limit::{LocalSiteRateLimit, LocalSiteRateLimitUpdateForm},
local_site_url_blocklist::LocalSiteUrlBlocklist,
@ -152,12 +150,6 @@ pub async fn update_site(
.await
.ok();
// Replace the blocked and allowed instances
let allowed = data.allowed_instances.clone();
FederationAllowList::replace(&mut context.pool(), allowed).await?;
let blocked = data.blocked_instances.clone();
FederationBlockList::replace(&mut context.pool(), blocked).await?;
if let Some(url_blocklist) = data.blocked_urls.clone() {
let parsed_urls = check_urls_are_valid(&url_blocklist)?;
LocalSiteUrlBlocklist::replace(&mut context.pool(), parsed_urls).await?;

View file

@ -1,49 +1,30 @@
use crate::{
schema::federation_blocklist,
source::{
federation_blocklist::{FederationBlockList, FederationBlockListForm},
instance::Instance,
},
source::federation_blocklist::{FederationBlockList, FederationBlockListForm},
utils::{get_conn, DbPool},
};
use diesel::{delete, ExpressionMethods, QueryDsl};
use diesel::{dsl::insert_into, result::Error};
use diesel_async::{AsyncPgConnection, RunQueryDsl};
use diesel_async::RunQueryDsl;
impl FederationBlockList {
pub async fn replace(pool: &mut DbPool<'_>, list_opt: Option<Vec<String>>) -> Result<(), Error> {
pub async fn block(pool: &mut DbPool<'_>, form: &FederationBlockListForm) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
conn
.build_transaction()
.run(|conn| {
Box::pin(async move {
if let Some(list) = list_opt {
Self::clear(conn).await?;
for domain in list {
// Upsert all of these as instances
let instance = Instance::read_or_create(&mut conn.into(), domain).await?;
let form = FederationBlockListForm {
instance_id: instance.id,
updated: None,
};
insert_into(federation_blocklist::table)
.values(form)
.get_result::<Self>(conn)
.await?;
}
Ok(())
} else {
Ok(())
}
}) as _
})
insert_into(federation_blocklist::table)
.values(form)
.get_result::<Self>(conn)
.await
}
async fn clear(conn: &mut AsyncPgConnection) -> Result<usize, Error> {
diesel::delete(federation_blocklist::table)
.execute(conn)
.await
pub async fn unblock(
pool: &mut DbPool<'_>,
form: &FederationBlockListForm,
) -> Result<Self, Error> {
let conn = &mut get_conn(pool).await?;
delete(
federation_blocklist::table
.filter(federation_blocklist::dsl::instance_id.eq(form.instance_id)),
)
.get_result(conn)
.await
}
}

View file

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

View file

@ -282,6 +282,9 @@ diesel::table! {
instance_id -> Int4,
published -> Timestamptz,
updated -> Nullable<Timestamptz>,
admin_person_id -> Nullable<Int4>,
reason -> Nullable<Text>,
expires -> Nullable<Timestamptz>,
}
}
@ -955,6 +958,7 @@ diesel::joinable!(custom_emoji_keyword -> custom_emoji (custom_emoji_id));
diesel::joinable!(email_verification -> local_user (local_user_id));
diesel::joinable!(federation_allowlist -> instance (instance_id));
diesel::joinable!(federation_blocklist -> instance (instance_id));
diesel::joinable!(federation_blocklist -> person (admin_person_id));
diesel::joinable!(federation_queue_state -> instance (instance_id));
diesel::joinable!(instance_actions -> instance (instance_id));
diesel::joinable!(instance_actions -> person (person_id));

View file

@ -1,14 +1,15 @@
use crate::newtypes::InstanceId;
use crate::newtypes::{InstanceId, PersonId};
#[cfg(feature = "full")]
use crate::schema::federation_blocklist;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use ts_rs::TS;
#[derive(Clone, PartialEq, Eq, Debug, Serialize, Deserialize)]
#[cfg_attr(
feature = "full",
derive(Queryable, Selectable, Associations, Identifiable)
derive(TS, Queryable, Selectable, Associations, Identifiable)
)]
#[cfg_attr(
feature = "full",
@ -17,10 +18,15 @@ use std::fmt::Debug;
#[cfg_attr(feature = "full", diesel(table_name = federation_blocklist))]
#[cfg_attr(feature = "full", diesel(primary_key(instance_id)))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
pub struct FederationBlockList {
pub instance_id: InstanceId,
pub published: DateTime<Utc>,
pub updated: Option<DateTime<Utc>>,
// TODO: would be good to make this mandatory but value doesnt exist for old entries
pub admin_person_id: Option<PersonId>,
pub reason: Option<String>,
pub expires: Option<DateTime<Utc>>,
}
#[derive(Clone, Default)]
@ -29,4 +35,7 @@ pub struct FederationBlockList {
pub struct FederationBlockListForm {
pub instance_id: InstanceId,
pub updated: Option<DateTime<Utc>>,
pub admin_person_id: Option<PersonId>,
pub reason: Option<String>,
pub expires: Option<DateTime<Utc>>,
}

View file

@ -0,0 +1,57 @@
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::{federation_blocklist, instance, person},
utils::{functions::coalesce, 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 = coalesce(federation_blocklist::admin_person_id, 0)
.eq(person::id)
.and(show_mod_names_expr.or(person::id.eq(admin_person_id_join)));
let mut query = federation_blocklist::table
.left_join(person::table.on(admin_names_join))
.inner_join(instance::table)
.select((
federation_blocklist::all_columns,
instance::all_columns,
person::all_columns.nullable(),
))
.into_boxed();
if let Some(admin_person_id) = params.mod_person_id {
query = query.filter(federation_blocklist::admin_person_id.eq(admin_person_id));
};
// If a post or comment ID is given, then don't find any results
if params.post_id.is_some() || params.comment_id.is_some() {
return Ok(vec![]);
}
let (limit, offset) = limit_and_offset(params.page, params.limit)?;
query
.limit(limit)
.offset(offset)
.order_by(federation_blocklist::published.desc())
.load::<AdminBlockInstanceView>(conn)
.await
}
}

View file

@ -1,4 +1,6 @@
#[cfg(feature = "full")]
pub mod admin_block_instance;
#[cfg(feature = "full")]
pub mod admin_purge_comment_view;
#[cfg(feature = "full")]
pub mod admin_purge_community_view;

View file

@ -5,6 +5,8 @@ use lemmy_db_schema::{
source::{
comment::Comment,
community::Community,
federation_blocklist::FederationBlockList,
instance::Instance,
moderator::{
AdminPurgeComment,
AdminPurgeCommunity,
@ -233,6 +235,19 @@ pub struct AdminPurgePostView {
pub community: Community,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]
#[cfg_attr(feature = "full", diesel(check_for_backend(diesel::pg::Pg)))]
#[cfg_attr(feature = "full", ts(export))]
/// When an admin purges a post.
pub struct AdminBlockInstanceView {
pub blocklist_entry: FederationBlockList,
pub instance: Instance,
#[cfg_attr(feature = "full", ts(optional))]
pub admin: Option<Person>,
}
#[skip_serializing_none]
#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
#[cfg_attr(feature = "full", derive(TS, Queryable))]

View file

@ -0,0 +1,3 @@
alter table federation_blocklist drop column reason;
alter table federation_blocklist drop column expires;
alter table federation_blocklist drop column admin_person_id;

View file

@ -0,0 +1,3 @@
alter table federation_blocklist add column admin_person_id int REFERENCES person(id) ON UPDATE CASCADE ON DELETE CASCADE;
alter table federation_blocklist add column reason text;
alter table federation_blocklist add column expires timestamptz;

View file

@ -19,15 +19,7 @@ use lemmy_api_common::{
use lemmy_api_crud::post::create::send_webmention;
use lemmy_db_schema::{
schema::{
captcha_answer,
comment,
community,
community_actions,
instance,
person,
post,
received_activity,
sent_activity,
captcha_answer, comment, community, community_actions, federation_blocklist, instance, person, post, received_activity, sent_activity
},
source::{
community::Community,
@ -439,6 +431,15 @@ async fn update_banned_when_expired(pool: &mut DbPool<'_>) {
.await
.inspect_err(|e| error!("Failed to remove community_ban expired rows: {e}"))
.ok();
// TODO: need separate table for modlog, otherwise entry will disappear
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}");