xmaster 1.6.3

Enterprise-grade X/Twitter CLI — post, reply, like, retweet, DM, search, and more
use crate::cli::parse_tweet_id;
use crate::context::AppContext;
use crate::errors::XmasterError;
use crate::output::{self, CsvRenderable, OutputFormat, Tableable};
use crate::providers::xapi::XApi;
use serde::Serialize;
use std::sync::Arc;

#[derive(Serialize)]
struct RepliesResult {
    #[serde(rename = "id")]
    tweet_id: String,
    replies: Vec<ReplyItem>,
    total: usize,
}

#[derive(Serialize)]
struct ReplyItem {
    id: String,
    author: String,
    text: String,
    likes: u64,
    #[serde(skip_serializing_if = "Option::is_none")]
    date: Option<String>,
}

impl Tableable for RepliesResult {
    fn to_table(&self) -> comfy_table::Table {
        let mut table = comfy_table::Table::new();
        table.set_header(vec!["Author", "Text", "Likes"]);
        for r in &self.replies {
            let text_preview = if r.text.chars().count() > 120 {
                let truncated: String = r.text.chars().take(120).collect();
                format!("{truncated}...")
            } else {
                r.text.clone()
            };
            table.add_row(vec![
                format!("@{}", r.author),
                text_preview,
                r.likes.to_string(),
            ]);
        }
        table
    }
}

impl CsvRenderable for RepliesResult {
    fn csv_headers() -> Vec<&'static str> {
        vec!["id", "author", "text", "likes", "date"]
    }
    fn csv_rows(&self) -> Vec<Vec<String>> {
        self.replies
            .iter()
            .map(|r| {
                vec![
                    r.id.clone(),
                    r.author.clone(),
                    r.text.clone(),
                    r.likes.to_string(),
                    r.date.clone().unwrap_or_default(),
                ]
            })
            .collect()
    }
}

pub async fn execute(
    ctx: Arc<AppContext>,
    format: OutputFormat,
    id: &str,
    count: usize,
) -> Result<(), XmasterError> {
    let api = XApi::new(ctx.clone());
    let tweet_id = parse_tweet_id(id);

    let tweets = api.get_replies(&tweet_id, count).await?;

    let replies: Vec<ReplyItem> = tweets
        .into_iter()
        .map(|t| {
            let likes = t
                .public_metrics
                .as_ref()
                .map(|m| m.like_count)
                .unwrap_or(0);
            ReplyItem {
                id: t.id,
                author: t.author_username.unwrap_or_else(|| "unknown".into()),
                text: t.text,
                likes,
                date: t.created_at,
            }
        })
        .collect();

    let total = replies.len();
    let result = RepliesResult {
        tweet_id,
        replies,
        total,
    };
    output::render_csv(format, &result, None);
    Ok(())
}