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, print_list, require_browser_session, Ctx};

#[derive(Subcommand)]
pub enum InstagramAction {
    /// Get user profile info
    Profile { username: String },
    /// Search users
    Search {
        query: String,
        #[arg(long, default_value_t = 10)]
        count: usize,
    },
    /// Get recent posts from a user
    User {
        username: String,
        #[arg(long, default_value_t = 12)]
        count: usize,
    },
    /// Discover trending posts
    Explore {
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
    /// List followers
    Followers {
        username: String,
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
    /// List following
    Following {
        username: String,
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
    /// Get saved posts (requires your login)
    Saved {
        #[arg(long, default_value_t = 20)]
        count: usize,
    },
    /// Like a post by shortcode
    Like { shortcode: String },
    /// Unlike a post by shortcode
    Unlike { shortcode: String },
    /// Follow a user
    Follow { username: String },
    /// Unfollow a user
    Unfollow { username: String },
    /// Save (bookmark) a post
    Save { shortcode: String },
    /// Unsave a post
    Unsave { shortcode: String },
    /// Comment on a post
    Comment { shortcode: String, text: String },
}

pub async fn run(action: InstagramAction, ctx: &Ctx) -> Result<(), TailFinError> {
    let session = require_browser_session(ctx, "instagram").await?;
    let client = tail_fin_instagram::InstagramClient::new(session);

    match action {
        InstagramAction::Profile { username } => {
            let result = client.profile(&username).await?;
            print_json(&result)?;
        }
        InstagramAction::Search { query, count } => {
            let users = client.search(&query, count).await?;
            print_list("users", &users, users.len())?;
        }
        InstagramAction::User { username, count } => {
            let posts = client.user_posts(&username, count).await?;
            print_list("posts", &posts, posts.len())?;
        }
        InstagramAction::Explore { count } => {
            let posts = client.explore(count).await?;
            print_list("posts", &posts, posts.len())?;
        }
        InstagramAction::Followers { username, count } => {
            let users = client.followers(&username, count).await?;
            print_list("followers", &users, users.len())?;
        }
        InstagramAction::Following { username, count } => {
            let users = client.following(&username, count).await?;
            print_list("following", &users, users.len())?;
        }
        InstagramAction::Saved { count } => {
            let posts = client.saved(count).await?;
            print_list("saved", &posts, posts.len())?;
        }
        InstagramAction::Like { shortcode } => {
            let result = client.like(&shortcode).await?;
            print_json(&result)?;
        }
        InstagramAction::Unlike { shortcode } => {
            let result = client.unlike(&shortcode).await?;
            print_json(&result)?;
        }
        InstagramAction::Follow { username } => {
            let result = client.follow(&username).await?;
            print_json(&result)?;
        }
        InstagramAction::Unfollow { username } => {
            let result = client.unfollow(&username).await?;
            print_json(&result)?;
        }
        InstagramAction::Save { shortcode } => {
            let result = client.save(&shortcode).await?;
            print_json(&result)?;
        }
        InstagramAction::Unsave { shortcode } => {
            let result = client.unsave(&shortcode).await?;
            print_json(&result)?;
        }
        InstagramAction::Comment { shortcode, text } => {
            let result = client.comment(&shortcode, &text).await?;
            print_json(&result)?;
        }
    }
    Ok(())
}

pub struct Adapter;

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

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

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