feat: nuke cli task

not awesome but eh acceptable i think
This commit is contained in:
əlemi 2024-06-21 02:32:31 +02:00
parent 11edbba1ae
commit f466016c01
Signed by: alemi
GPG key ID: A4895B84D311642C
2 changed files with 99 additions and 0 deletions

View file

@ -16,6 +16,9 @@ pub use register::*;
mod update;
pub use update::*;
mod nuke;
pub use nuke::*;
#[derive(Debug, Clone, clap::Subcommand)]
pub enum CliCommand {
/// generate fake user, note and activity
@ -90,6 +93,17 @@ pub enum CliCommand {
/// url for banner image of new user
#[arg(long = "banner")]
banner_url: Option<String>,
},
/// break all user relations so that instance can be shut down
Nuke {
/// unless this is set, nuke will be a dry run
#[arg(long, default_value_t = false)]
for_real: bool,
/// also send Delete activities for all local objects
#[arg(long, default_value_t = false)]
delete_objects: bool,
}
}
@ -108,5 +122,7 @@ pub async fn run(ctx: upub::Context, command: CliCommand) -> Result<(), Box<dyn
Ok(update_users(ctx, days).await?),
CliCommand::Register { username, password, display_name, summary, avatar_url, banner_url } =>
Ok(register(ctx, username, password, display_name, summary, avatar_url, banner_url).await?),
CliCommand::Nuke { for_real, delete_objects } =>
Ok(nuke(ctx, for_real, delete_objects).await?),
}
}

83
upub/cli/src/nuke.rs Normal file
View file

@ -0,0 +1,83 @@
use std::collections::HashSet;
use apb::{ActivityMut, BaseMut, ObjectMut};
use futures::TryStreamExt;
use sea_orm::{ActiveValue::{Set, NotSet}, ColumnTrait, EntityTrait, QueryFilter, QuerySelect, SelectColumns};
pub async fn nuke(ctx: upub::Context, for_real: bool, delete_posts: bool) -> Result<(), sea_orm::DbErr> {
if !for_real {
tracing::warn!("THIS IS A DRY RUN! pass --for-real to actually nuke this instance");
}
let mut to_undo = Vec::new();
// TODO rather expensive to find all local users with a LIKE query, should add an isLocal flag
let local_users_vec = upub::model::actor::Entity::find()
.filter(upub::model::actor::Column::Id.like(format!("{}%", ctx.base())))
.select_only()
.select_column(upub::model::actor::Column::Internal)
.into_tuple::<i64>()
.all(ctx.db())
.await?;
let local_users : HashSet<i64> = HashSet::from_iter(local_users_vec);
{
let mut stream = upub::model::relation::Entity::find().stream(ctx.db()).await?;
while let Some(like) = stream.try_next().await? {
if local_users.contains(&like.follower) {
to_undo.push(like.activity);
} else if local_users.contains(&like.following) {
if let Some(accept) = like.accept {
to_undo.push(accept);
}
}
}
}
for internal in to_undo {
let Some(activity) = upub::model::activity::Entity::find_by_id(internal)
.one(ctx.db())
.await?
else {
tracing::error!("could not load activity #{internal}");
continue;
};
let Some(oid) = activity.object
else {
tracing::error!("can't undo activity without object");
continue;
};
let aid = ctx.aid(&upub::Context::new_id());
let undo_activity = apb::new()
.set_id(Some(&aid))
.set_activity_type(Some(apb::ActivityType::Undo))
.set_actor(apb::Node::link(activity.actor.clone()))
.set_object(apb::Node::link(oid))
.set_published(Some(chrono::Utc::now()));
let job = upub::model::job::ActiveModel {
internal: NotSet,
activity: Set(aid.clone()),
job_type: Set(upub::model::job::JobType::Outbound),
actor: Set(activity.actor),
target: Set(None),
published: Set(chrono::Utc::now()),
not_before: Set(chrono::Utc::now()),
attempt: Set(0),
payload: Set(Some(undo_activity)),
};
tracing::debug!("undoing {}", activity.id);
if for_real {
upub::model::job::Entity::insert(job).exec(ctx.db()).await?;
}
}
Ok(())
}