Initial version
This commit is contained in:
commit
fb89412448
6 changed files with 3009 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
config.toml
|
||||
2722
Cargo.lock
generated
Normal file
2722
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
13
Cargo.toml
Normal file
13
Cargo.toml
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
[package]
|
||||
name = "banPropagator"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
toml = "1.0.3+spec-1.1.0"
|
||||
lemmy-client = "1.0.5"
|
||||
serde = { version = "1.0.228", features = ["derive"] }
|
||||
rpassword = "7.4.0"
|
||||
anyhow = "1.0.102"
|
||||
tokio = { version = "1.49.0", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
reqwest = "0.12.24"
|
||||
29
README.md
Normal file
29
README.md
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
Propagate Lemmy Bans
|
||||
====================
|
||||
|
||||
`config.toml`:
|
||||
```toml
|
||||
instance = "instance.com"
|
||||
admin_name = "Admin"
|
||||
exclusions = ["admin1@instance.com", "admin2@instance.com"]
|
||||
admin_password = "[ADMIN_PASSWORD]"
|
||||
|
||||
[[lemmy_modlog_instances]]
|
||||
instance = "lemmy.blahaj.zone"
|
||||
|
||||
[[lemmy_modlog_instances]]
|
||||
instance = "piefed.blahaj.zone"
|
||||
|
||||
[[lemmy_modlog_instances]]
|
||||
instance = "lemmy.dbzer0.com"
|
||||
```
|
||||
|
||||
Gets a list of the 10 most recent PurgeUser events from the Lemmy instances.
|
||||
Bans those users locally. Repeats every 60 seconds.
|
||||
|
||||
Could do with a lot of work and features, but this is a straw-man for further development.
|
||||
|
||||
# Features missing:
|
||||
|
||||
- Piefed support
|
||||
- Kbin/Mbin/other support
|
||||
34
src/config.rs
Normal file
34
src/config.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub(crate) struct LemmyModlogInstance {
|
||||
pub(crate) instance: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub(crate) struct Config {
|
||||
/// The instance to ban users from
|
||||
pub(crate) instance: String,
|
||||
/// The admin user to use
|
||||
pub(crate) admin_name: String,
|
||||
/// The admin password to use, if not provided, will be prompted for
|
||||
pub(crate) admin_password: Option<String>,
|
||||
/// Any users to exclude from banning
|
||||
pub(crate) exclusions: Vec<String>,
|
||||
/// The lemmy instances to get modlogs from
|
||||
pub(crate) lemmy_modlog_instances: Vec<LemmyModlogInstance>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn load(path: &Path) -> anyhow::Result<Self> {
|
||||
let file_contents = std::fs::read_to_string(path)?;
|
||||
let mut config: Config = toml::from_str(&file_contents)?;
|
||||
if config.admin_password.is_none() {
|
||||
let password = rpassword::prompt_password("Enter your password: ")?;
|
||||
config.admin_password = Some(password);
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
}
|
||||
|
||||
209
src/main.rs
Normal file
209
src/main.rs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
mod config;
|
||||
|
||||
use crate::config::Config;
|
||||
use anyhow::{Context, bail};
|
||||
use lemmy_client::lemmy_api_common::lemmy_db_schema::ModlogActionType;
|
||||
use lemmy_client::lemmy_api_common::lemmy_db_schema::newtypes::{InstanceId, PersonId};
|
||||
use lemmy_client::lemmy_api_common::lemmy_db_schema::sensitive::SensitiveString;
|
||||
use lemmy_client::lemmy_api_common::person::{BanPerson, GetPersonDetails, Login};
|
||||
use lemmy_client::lemmy_api_common::site::{
|
||||
FederatedInstances, GetFederatedInstancesResponse, GetModlog, PurgePerson,
|
||||
};
|
||||
use lemmy_client::{ClientOptions, LemmyClient, LemmyRequest, lemmy_api_common};
|
||||
use reqwest;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::hash::Hash;
|
||||
use std::path::Path;
|
||||
use std::time::Duration;
|
||||
use tokio::time::sleep;
|
||||
|
||||
/// Bans a user from a given instance
|
||||
async fn ban_user(id: i32, client: &LemmyClient, token: &SensitiveString) {
|
||||
client
|
||||
.ban_from_site(LemmyRequest {
|
||||
body: BanPerson {
|
||||
person_id: PersonId(id),
|
||||
ban: true,
|
||||
remove_data: Some(false),
|
||||
reason: Some(String::from("Propagated ban")),
|
||||
expires: None,
|
||||
},
|
||||
jwt: Some(token.to_string()),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Gets the modlog for a given instance
|
||||
async fn get_modlog(url: &str) -> anyhow::Result<Vec<(i32, String)>> {
|
||||
let client = LemmyClient::new(ClientOptions {
|
||||
domain: String::from(url),
|
||||
secure: true,
|
||||
});
|
||||
|
||||
let modlog_result = client
|
||||
.get_modlog(GetModlog {
|
||||
mod_person_id: None,
|
||||
community_id: None,
|
||||
page: None,
|
||||
limit: Some(10),
|
||||
type_: Some(ModlogActionType::AdminPurgePerson),
|
||||
other_person_id: None,
|
||||
post_id: None,
|
||||
comment_id: None,
|
||||
})
|
||||
.await;
|
||||
let modlog = match modlog_result {
|
||||
Ok(modlog) => modlog,
|
||||
Err(e) => {
|
||||
bail!("Getting modlog failed: {:?}", e);
|
||||
}
|
||||
};
|
||||
let purged_people = modlog
|
||||
.admin_purged_persons
|
||||
.iter()
|
||||
.map(|appv| {
|
||||
let reason = appv
|
||||
.admin_purge_person
|
||||
.reason
|
||||
.clone()
|
||||
.unwrap_or_else(|| String::from("No reason provided"));
|
||||
let userid = appv.admin_purge_person.id;
|
||||
(userid, reason)
|
||||
})
|
||||
.collect::<Vec<(i32, String)>>();
|
||||
|
||||
Ok(purged_people)
|
||||
}
|
||||
|
||||
async fn get_all_instances(
|
||||
instance_domain: &str,
|
||||
token: &SensitiveString,
|
||||
) -> anyhow::Result<HashMap<i32, String>> {
|
||||
let mut instances: HashMap<i32, String> = HashMap::new();
|
||||
|
||||
let response = reqwest::get(format!(
|
||||
"https://{}/api/v3/federated_instances",
|
||||
instance_domain
|
||||
))
|
||||
.await?;
|
||||
let response = response.json::<GetFederatedInstancesResponse>().await?;
|
||||
if let Some(federated_instances) = response.federated_instances {
|
||||
federated_instances.allowed.iter().for_each(|i| {
|
||||
let id = i.instance.id;
|
||||
let name = i.instance.domain.clone();
|
||||
instances.insert(id.0, name);
|
||||
});
|
||||
federated_instances.blocked.iter().for_each(|i| {
|
||||
let id = i.instance.id;
|
||||
let name = i.instance.domain.clone();
|
||||
instances.insert(id.0, name);
|
||||
});
|
||||
federated_instances.linked.iter().for_each(|i| {
|
||||
let id = i.instance.id;
|
||||
let name = i.instance.domain.clone();
|
||||
instances.insert(id.0, name);
|
||||
});
|
||||
} else {
|
||||
bail!("No federated instances found");
|
||||
}
|
||||
|
||||
Ok(instances)
|
||||
}
|
||||
|
||||
async fn get_username(
|
||||
user_id: i32,
|
||||
client: &LemmyClient,
|
||||
token: &SensitiveString,
|
||||
instances: &HashMap<i32, String>,
|
||||
) -> anyhow::Result<String> {
|
||||
let response = match client
|
||||
.get_person(GetPersonDetails {
|
||||
person_id: Some(PersonId(user_id)),
|
||||
username: None,
|
||||
sort: None,
|
||||
page: None,
|
||||
limit: None,
|
||||
community_id: None,
|
||||
saved_only: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(response) => response,
|
||||
Err(e) => {
|
||||
bail!("Failed to get person details for user ID: {user_id} - {e}");
|
||||
}
|
||||
};
|
||||
|
||||
let username = response.person_view.person.name;
|
||||
let instance_id = response.person_view.person.instance_id;
|
||||
let Some(instance) = instances.get(&instance_id.0) else {
|
||||
bail!("No instance found for instanceid ID: {}", instance_id.0);
|
||||
};
|
||||
|
||||
println!(
|
||||
"User ID {} belongs to username: {}@{}",
|
||||
user_id, username, instance
|
||||
);
|
||||
|
||||
Ok(format!("{}@{}", username, instance))
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let config = Config::load(Path::new("config.toml"))?;
|
||||
|
||||
let client = LemmyClient::new(ClientOptions {
|
||||
domain: String::from(&config.instance),
|
||||
secure: true,
|
||||
});
|
||||
let Some(admin_password) = &config.admin_password else {
|
||||
bail!("No admin_password provided")
|
||||
};
|
||||
let auth = client
|
||||
.login(Login {
|
||||
username_or_email: SensitiveString::from(config.admin_name),
|
||||
password: SensitiveString::from(admin_password.clone()),
|
||||
totp_2fa_token: None,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let Some(token) = auth.jwt else {
|
||||
bail!("No JWT returned from login")
|
||||
};
|
||||
|
||||
let instances = get_all_instances(&config.instance, &token).await?;
|
||||
|
||||
let mut previously_banned: HashSet<i32> = HashSet::new();
|
||||
|
||||
loop {
|
||||
for instance in &config.lemmy_modlog_instances {
|
||||
println!("Checking modlog for instance: {}", instance.instance);
|
||||
let Ok(modlog) = get_modlog(&instance.instance).await else {
|
||||
println!("Failed to get modlog for instance: {}", instance.instance);
|
||||
continue;
|
||||
};
|
||||
for (userid, reason) in modlog {
|
||||
if previously_banned.contains(&userid) {
|
||||
continue;
|
||||
}
|
||||
let username = match get_username(userid, &client, &token, &instances).await {
|
||||
Ok(u) => u,
|
||||
Err(e) => {
|
||||
println!("Error: {e}");
|
||||
previously_banned.insert(userid);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
println!("{}: {}", userid, reason);
|
||||
if !config.exclusions.contains(&username) {
|
||||
// println!("Propagating Ban for User: {}", username);
|
||||
// ban_user(userid, &client, &token).await;
|
||||
}
|
||||
previously_banned.insert(userid);
|
||||
}
|
||||
}
|
||||
|
||||
sleep(Duration::from_secs(60)).await;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue