tail-fin-cli 0.3.0

Multi-site browser automation CLI — Twitter, Reddit, Bloomberg, Coupang, PCC, Instagram, YouTube, Grok, SeekingAlpha, Xiaohongshu, 591, Nansen
use clap::Subcommand;
use tail_fin_common::TailFinError;

use crate::session::{print_json, print_list, require_browser_session, Ctx};

#[derive(Subcommand)]
pub enum XhsAction {
    /// Get a note by ID or URL
    Note { note: String },
    /// Search for notes
    Search {
        query: String,
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
    /// Get comments on a note
    Comments {
        /// Note ID or URL
        note: String,
        #[arg(long, default_value_t = 20)]
        count: usize,
        /// Include nested replies
        #[arg(long, default_value_t = false)]
        with_replies: bool,
    },
    /// Get a user's published notes
    User {
        user_id: String,
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
    /// Get media URLs from a note (images + video)
    Media { note: String },
    /// Get homepage recommendation feed
    Feed {
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
    /// Get notifications (requires login)
    Notifications {
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
}

pub async fn run(action: XhsAction, ctx: &Ctx) -> Result<(), TailFinError> {
    let session = require_browser_session(ctx, "xhs").await?;
    let client = tail_fin_xhs::XhsClient::new(session);

    match action {
        XhsAction::Note { note } => {
            let result = client.note(&note).await?;
            print_json(&result)?;
        }
        XhsAction::Search { query, count } => {
            let notes = client.search(&query, count).await?;
            print_list("notes", &notes, notes.len())?;
        }
        XhsAction::Comments {
            note,
            count,
            with_replies,
        } => {
            let comments = client.comments(&note, count, with_replies).await?;
            print_list("comments", &comments, comments.len())?;
        }
        XhsAction::User { user_id, count } => {
            let notes = client.user_notes(&user_id, count).await?;
            print_list("notes", &notes, notes.len())?;
        }
        XhsAction::Media { note } => {
            let media = client.media(&note).await?;
            print_list("media", &media, media.len())?;
        }
        XhsAction::Feed { count } => {
            let items = client.feed(count).await?;
            print_list("feed", &items, items.len())?;
        }
        XhsAction::Notifications { count } => {
            let notifs = client.notifications(count).await?;
            print_list("notifications", &notifs, notifs.len())?;
        }
    }
    Ok(())
}

pub struct Adapter;

impl crate::adapter::CliAdapter for Adapter {
    fn name(&self) -> &'static str {
        "xhs"
    }

    fn about(&self) -> &'static str {
        "Xiaohongshu operations"
    }

    fn command(&self) -> clap::Command {
        <XhsAction as clap::Subcommand>::augment_subcommands(
            clap::Command::new("xhs").about("Xiaohongshu operations"),
        )
    }

    fn dispatch<'a>(
        &'a self,
        matches: &'a clap::ArgMatches,
        ctx: &'a crate::session::Ctx,
    ) -> std::pin::Pin<
        Box<
            dyn std::future::Future<Output = Result<(), tail_fin_common::TailFinError>> + Send + 'a,
        >,
    > {
        Box::pin(async move {
            let action = <XhsAction as clap::FromArgMatches>::from_arg_matches(matches)
                .map_err(|e| tail_fin_common::TailFinError::Api(e.to_string()))?;
            run(action, ctx).await
        })
    }
}