mirror of
https://git.alemi.dev/fedimut.git
synced 2024-11-14 12:59:20 +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