mirror of
https://git.alemi.dev/fedimut.git
synced 2024-11-23 16:54:47 +01:00
feat: initial mutuals add/remove impl
This commit is contained in:
parent
f8113e0a27
commit
ad4f0610c1
2 changed files with 330 additions and 0 deletions
164
src/main.rs
Normal file
164
src/main.rs
Normal file
|
@ -0,0 +1,164 @@
|
|||
pub mod model;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
struct CliArgs {
|
||||
homeserver: String,
|
||||
|
||||
token: String,
|
||||
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
pub struct FediClient {
|
||||
http: reqwest::Client,
|
||||
homeserver: String,
|
||||
token: String,
|
||||
user_id: String,
|
||||
}
|
||||
|
||||
impl FediClient {
|
||||
pub fn new(homeserver: String, user_id: String, token: String) -> Self {
|
||||
let http = reqwest::Client::default();
|
||||
FediClient {
|
||||
http, homeserver, token, user_id,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url(&self, endpoint: &str) -> String {
|
||||
format!("https://{}/api/v1/{}", self.homeserver, endpoint)
|
||||
}
|
||||
|
||||
pub fn headers(&self) -> reqwest::header::HeaderMap {
|
||||
let mut headers = reqwest::header::HeaderMap::new();
|
||||
headers.insert("Authorization", format!("Bearer {}", self.token).parse().unwrap());
|
||||
headers
|
||||
}
|
||||
|
||||
pub async fn lists(&self) -> reqwest::Result<Vec<model::FediList>> {
|
||||
self.http
|
||||
.get(self.url("lists"))
|
||||
.headers(self.headers())
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_accounts(&self, list: &str) -> reqwest::Result<Vec<model::FediUser>> {
|
||||
self.http
|
||||
.get(self.url(&format!("lists/{list}/accounts")))
|
||||
.headers(self.headers())
|
||||
.query(&[("with_relationships", true)]) // TODO ?? without &[] it panics: "unsupported pair"
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_add_accounts(&self, list: &str, account_ids: Vec<String>) -> reqwest::Result<()> {
|
||||
self.http
|
||||
.post(self.url(&format!("lists/{list}/accounts")))
|
||||
.headers(self.headers())
|
||||
.json(&model::FediAccountIds { account_ids })
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_remove_accounts(&self, list: &str, account_ids: Vec<String>) -> reqwest::Result<()> {
|
||||
self.http
|
||||
.delete(self.url(&format!("lists/{list}/accounts")))
|
||||
.headers(self.headers())
|
||||
.json(&model::FediAccountIds { account_ids })
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_related_users(&self, user_id: &str, relation: &str) -> reqwest::Result<Vec<model::FediUser>> {
|
||||
let mut out = Vec::new();
|
||||
let limit = 40; // TODO make it customizable? idk
|
||||
loop {
|
||||
let mut page : Vec<model::FediUser> = self.http
|
||||
.get(self.url(&format!("accounts/{user_id}/{relation}")))
|
||||
.headers(self.headers())
|
||||
.query(&[("limit", limit)])
|
||||
.query(&[("with_relationships", true)])
|
||||
.send()
|
||||
.await?
|
||||
.error_for_status()?
|
||||
.json()
|
||||
.await?;
|
||||
let count = page.len();
|
||||
out.append(&mut page); // TODO can i just concat these vecs at the end?
|
||||
if count != limit { break } // end of pages
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
pub async fn followers(&self) -> reqwest::Result<Vec<model::FediUser>> {
|
||||
self.get_related_users(&self.user_id, "followers").await
|
||||
}
|
||||
|
||||
pub async fn following(&self) -> reqwest::Result<Vec<model::FediUser>> {
|
||||
self.get_related_users(&self.user_id, "following").await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
tracing_subscriber::fmt()
|
||||
.compact()
|
||||
.init();
|
||||
|
||||
let args = CliArgs::parse();
|
||||
|
||||
let client = FediClient::new(args.homeserver, args.user_id, args.token);
|
||||
|
||||
let mut mutuals = std::collections::BTreeMap::new();
|
||||
|
||||
client.followers()
|
||||
.await.unwrap()
|
||||
.into_iter()
|
||||
.filter(|u| match u.pleroma.relationship {
|
||||
model::EmptyOption::Some(ref relation) => relation.following,
|
||||
model::EmptyOption::None {} => {
|
||||
tracing::warn!("missing relationships for user '{}', skipping", u.fqn);
|
||||
false
|
||||
},
|
||||
}).for_each(|u| { mutuals.insert(u.fqn.clone(), u); });
|
||||
|
||||
let lists = client.lists().await.unwrap();
|
||||
// TODO create mutuals list if not present
|
||||
let mutuals_list = lists.iter().find(|l| l.title == "mutuals").expect("no mutuals list!");
|
||||
let current_mutuals = client.list_accounts(&mutuals_list.id).await.unwrap();
|
||||
|
||||
// first remove all users that aren't mutuals anymore
|
||||
let mut user_ids = Vec::new();
|
||||
for usr in current_mutuals {
|
||||
if mutuals.contains_key(&usr.fqn) {
|
||||
tracing::debug!("user '{}' is still a mutual", usr.fqn);
|
||||
mutuals.remove(&usr.fqn);
|
||||
} else {
|
||||
tracing::info!("removing mutual '{}'", usr.fqn);
|
||||
user_ids.push(usr.id.clone());
|
||||
}
|
||||
}
|
||||
client.list_remove_accounts(&mutuals_list.id, user_ids).await.unwrap();
|
||||
|
||||
// then add all new mutuals
|
||||
let mut user_ids = Vec::new();
|
||||
for (_id, usr) in mutuals {
|
||||
tracing::info!("adding new mutual '{}'", usr.fqn);
|
||||
user_ids.push(usr.id.clone());
|
||||
}
|
||||
client.list_add_accounts(&mutuals_list.id, user_ids).await.unwrap();
|
||||
|
||||
}
|
166
src/model.rs
Normal file
166
src/model.rs
Normal file
|
@ -0,0 +1,166 @@
|
|||
#[derive(Debug, Clone, serde::Serialize)]
|
||||
pub struct FediAccountIds {
|
||||
pub account_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct FediList {
|
||||
pub id: String, // TODO fuck you pleroma these are NUMBERS!!!!
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct FediUserField {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct FediEmoji {
|
||||
pub url: String,
|
||||
pub shortcode: String,
|
||||
pub static_url: String,
|
||||
pub visible_in_picker: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct FediUser {
|
||||
pub id: String,
|
||||
pub header: String,
|
||||
pub fields: Vec<FediUserField>,
|
||||
pub source: FediUserSource,
|
||||
pub pleroma: FediUserProperties,
|
||||
pub url: String,
|
||||
pub username: String,
|
||||
pub created_at: String, // chrono::DateTime<chrono::Utc>,
|
||||
pub emojis: Vec<FediEmoji>,
|
||||
pub akkoma: Option<FediUserAkkomaExtra>,
|
||||
pub following_count: u32,
|
||||
pub note: String,
|
||||
pub last_status_at: Option<String>, // Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub acct: String,
|
||||
pub avatar: String,
|
||||
pub avatar_static: String,
|
||||
pub bot: bool,
|
||||
pub display_name: String,
|
||||
pub followers_count: u32,
|
||||
pub fqn: String,
|
||||
pub header_static: String,
|
||||
pub locked: bool,
|
||||
pub statuses_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct FediUserProperties {
|
||||
pub background_image: Option<String>,
|
||||
pub skip_thread_containment: bool,
|
||||
pub tags: Vec<String>,
|
||||
pub ap_id: String,
|
||||
pub also_known_as: Vec<String>,
|
||||
pub hide_follows: bool,
|
||||
pub hide_follows_count: bool,
|
||||
pub hide_followers: bool,
|
||||
pub hide_followers_count: bool,
|
||||
pub is_confirmed: bool,
|
||||
pub is_suggested: bool,
|
||||
pub is_moderator: bool,
|
||||
pub is_admin: bool,
|
||||
pub hide_favorites: bool,
|
||||
pub favicon: Option<String>,
|
||||
pub relationship: EmptyOption<FediUserRelationship>,
|
||||
pub deactivated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct FediUserRelationship {
|
||||
pub id: String,
|
||||
pub following: bool,
|
||||
pub note: String,
|
||||
pub blocked_by: bool,
|
||||
pub blocking: bool,
|
||||
pub domain_blocking: bool,
|
||||
pub endorsed: bool,
|
||||
pub followed_by: bool,
|
||||
pub muting: bool,
|
||||
pub muting_notifications: bool,
|
||||
pub notifying: bool,
|
||||
pub requested: bool,
|
||||
pub requested_by: bool,
|
||||
pub showing_reblogs: bool,
|
||||
pub subscribing: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct FediUserSource {
|
||||
pub sensitive: bool,
|
||||
pub fields: Vec<FediUserField>,
|
||||
pub pleroma: FediUserSourceType,
|
||||
pub note: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct FediUserSourceType {
|
||||
pub actor_type: String,
|
||||
pub discoverable: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct FediUserAkkomaExtra {
|
||||
pub instance: FediUserAkkomaExtraInstance,
|
||||
pub status_ttl_days: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct FediUserAkkomaExtraInstance {
|
||||
pub name: String,
|
||||
pub favicon: Option<String>,
|
||||
pub nodeinfo: Option<NodeInfo>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct NodeInfo {
|
||||
pub openRegistrations: Option<bool>,
|
||||
pub protocols: Option<Vec<String>>,
|
||||
pub services: Option<NodeInfoServices>,
|
||||
pub software: Option<NodeInfoSoftware>,
|
||||
pub version: Option<String>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct NodeInfoServices {
|
||||
pub inbound: Vec<String>,
|
||||
pub outbound: Vec<String>,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct NodeInfoSoftware {
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct NodeInfoUsage {
|
||||
pub localPosts: u32,
|
||||
pub users: NodeInfoUsageUsers,
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
pub struct NodeInfoUsageUsers {
|
||||
pub activeHalfYear: u32,
|
||||
pub activeMonth: u32,
|
||||
pub total: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
#[serde(untagged)]
|
||||
pub enum EmptyOption<T> { // pleroma sets "relationship" to an empty object.... WHYYYY
|
||||
Some(T),
|
||||
None {},
|
||||
}
|
Loading…
Reference in a new issue