feat: initial mutuals add/remove impl

This commit is contained in:
əlemi 2024-02-28 17:09:27 +01:00
parent f8113e0a27
commit ad4f0610c1
Signed by: alemi
GPG key ID: A4895B84D311642C
2 changed files with 330 additions and 0 deletions

164
src/main.rs Normal file
View 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
View 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 {},
}