tail-fin-cli 0.4.0

Multi-site browser automation CLI — attaches to Chrome or auto-launches a stealth browser to drive 14+ sites
use clap::Subcommand;
use tail_fin_common::TailFinError;

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

#[derive(Subcommand)]
pub enum RedditAction {
    /// Display trending posts
    Hot {
        #[arg(long, default_value_t = 25)]
        limit: usize,
        #[arg(long)]
        after: Option<String>,
    },
    /// Show frontpage feed
    Frontpage {
        #[arg(long, default_value_t = 25)]
        limit: usize,
        #[arg(long)]
        after: Option<String>,
    },
    /// View popular content
    Popular {
        #[arg(long, default_value_t = 25)]
        limit: usize,
        #[arg(long)]
        after: Option<String>,
    },
    /// Find posts and content
    Search {
        query: String,
        #[arg(long, default_value_t = 25)]
        limit: usize,
        #[arg(long)]
        after: Option<String>,
    },
    /// Access specific community
    Subreddit {
        name: String,
        #[arg(long, default_value_t = 25)]
        limit: usize,
        #[arg(long)]
        after: Option<String>,
    },
    /// Open post thread
    Read {
        /// Post ID (e.g. "abc123" or "t3_abc123")
        post_id: String,
        #[arg(long, default_value_t = 5)]
        depth: u32,
    },
    /// View user profile
    User { username: String },
    /// List user's submissions
    UserPosts {
        username: String,
        #[arg(long, default_value_t = 25)]
        limit: usize,
        #[arg(long)]
        after: Option<String>,
    },
    /// List user's replies
    UserComments {
        username: String,
        #[arg(long, default_value_t = 25)]
        limit: usize,
        #[arg(long)]
        after: Option<String>,
    },
    /// Display bookmarks (requires login)
    Saved {
        #[arg(long, default_value_t = 25)]
        limit: usize,
        #[arg(long)]
        after: Option<String>,
    },
    /// Show liked posts (requires login)
    Upvoted {
        #[arg(long, default_value_t = 25)]
        limit: usize,
        #[arg(long)]
        after: Option<String>,
    },
    /// Like a post
    Upvote { post_id: String },
    /// Bookmark content
    Save { post_id: String },
    /// Reply to post
    Comment { post_id: String, text: String },
    /// Follow community
    Subscribe {
        /// Subreddit name (without r/)
        subreddit: String,
    },
}

pub async fn run(action: RedditAction, ctx: &Ctx) -> Result<(), TailFinError> {
    let session = require_browser_session(ctx, "reddit").await?;
    let client = tail_fin_reddit::RedditClient::new(session);

    match action {
        RedditAction::Hot { limit, after } => {
            let resp = client.hot(limit, after.as_deref()).await?;
            print_json(&resp)?;
        }
        RedditAction::Frontpage { limit, after } => {
            let resp = client.frontpage(limit, after.as_deref()).await?;
            print_json(&resp)?;
        }
        RedditAction::Popular { limit, after } => {
            let resp = client.popular(limit, after.as_deref()).await?;
            print_json(&resp)?;
        }
        RedditAction::Search {
            query,
            limit,
            after,
        } => {
            let resp = client.search(&query, limit, after.as_deref()).await?;
            print_json(&resp)?;
        }
        RedditAction::Subreddit { name, limit, after } => {
            let resp = client.subreddit(&name, limit, after.as_deref()).await?;
            print_json(&resp)?;
        }
        RedditAction::Read { post_id, depth } => {
            let thread = client.read(&post_id, depth).await?;
            print_json(&thread)?;
        }
        RedditAction::User { username } => {
            let user = client.user(&username).await?;
            print_json(&user)?;
        }
        RedditAction::UserPosts {
            username,
            limit,
            after,
        } => {
            let resp = client
                .user_posts(&username, limit, after.as_deref())
                .await?;
            print_json(&resp)?;
        }
        RedditAction::UserComments {
            username,
            limit,
            after,
        } => {
            let resp = client
                .user_comments(&username, limit, after.as_deref())
                .await?;
            print_json(&resp)?;
        }
        RedditAction::Saved { limit, after } => {
            let resp = client.saved(limit, after.as_deref()).await?;
            print_json(&resp)?;
        }
        RedditAction::Upvoted { limit, after } => {
            let resp = client.upvoted(limit, after.as_deref()).await?;
            print_json(&resp)?;
        }
        RedditAction::Upvote { post_id } => {
            let result = client.upvote(&post_id).await?;
            print_json(&result)?;
        }
        RedditAction::Save { post_id } => {
            let result = client.save_post(&post_id).await?;
            print_json(&result)?;
        }
        RedditAction::Comment { post_id, text } => {
            let result = client.comment(&post_id, &text).await?;
            print_json(&result)?;
        }
        RedditAction::Subscribe { subreddit } => {
            let result = client.subscribe(&subreddit).await?;
            print_json(&result)?;
        }
    }
    Ok(())
}

pub struct Adapter;

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

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

    fn command(&self) -> clap::Command {
        <RedditAction as clap::Subcommand>::augment_subcommands(
            clap::Command::new("reddit").about("Reddit 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 = <RedditAction as clap::FromArgMatches>::from_arg_matches(matches)
                .map_err(|e| tail_fin_common::TailFinError::Api(e.to_string()))?;
            run(action, ctx).await
        })
    }
}