cli.rs
use clap::{Arg, Command};

pub struct CliArgs {
    pub input: Option<String>,
    pub output: Option<String>,
    pub concurrency: usize,
    pub timeout: u64,
    pub verbose: bool,
    pub prefixes: Vec<String>,
    pub suffixes: Vec<String>,
    pub random_strings: usize,
}

pub fn parse_args() -> clap::ArgMatches {
    Command::new("Roblox Channel Checker")
        .version("2.0")
        .author("Your Name <youremail@example.com>")
        .about("Checks for valid Roblox channels based on wordlist combinations and analyzes channel patterns")
        .arg(
            Arg::new("input")
                .short('i')
                .long("input")
                .value_name("FILE")
                .help("Sets a custom input wordlist file")
        )
        .arg(
            Arg::new("output")
                .short('o')
                .long("output")
                .value_name("FILE")
                .help("Sets an output file to save results in JSON format")
        )
        .arg(
            Arg::new("concurrency")
                .short('c')
                .long("concurrency")
                .value_name("NUMBER")
                .help("Sets the level of concurrency for HTTP requests")
                .default_value("8"),
        )
        .arg(
            Arg::new("timeout")
                .short('t')
                .long("timeout")
                .value_name("SECONDS")
                .help("Sets the timeout for HTTP requests in seconds")
                .default_value("10"),
        )
        .arg(
            Arg::new("verbose")
                .short('v')
                .long("verbose")
                .help("Increases verbosity level")
        )
        .arg(
            Arg::new("prefixes")
                .short('p')
                .long("prefixes")
                .value_name("PREFIXES")
                .help("Comma-separated list of prefixes to add to words")
        )
        .arg(
            Arg::new("suffixes")
                .short('s')
                .long("suffixes")
                .value_name("SUFFIXES")
                .help("Comma-separated list of suffixes to add to words")
        )
        .arg(
            Arg::new("random-strings")
                .short('r')
                .long("random-strings")
                .value_name("NUMBER")
                .help("Number of random alphanumeric strings to generate and add to the wordlist")
        )
        .get_matches()
}

checker.rs
use crate::{
    errors::AppError,
    models::ChannelInfo,
};
use colored::ColoredString;
use colored::Colorize;
use reqwest::blocking::Client;
use reqwest::StatusCode;
use retry::{delay::Exponential, retry};
use serde_json::Value;
use tracing::{debug, error, info};
use regex::Regex;

pub fn check_channel(client: &Client, channel_name: &str) -> Option<ChannelInfo> {
    let base_url = format!("http://setup.rbxcdn.com/channel/z{}", channel_name);
    let endpoints = [
        ("Roblox.exe", format!("{}/Roblox.exe", base_url)),
        ("DeployHistory.txt", format!("{}/DeployHistory.txt", base_url)),
        (
            "Mac DeployHistory.txt",
            format!("{}/mac/DeployHistory.txt", base_url),
        ),
        (
            "ClientSettings",
            format!(
                "http://clientsettings.roblox.com/v2/settings/application/RCCBootstrapper/bucket/z{}",
                channel_name
            ),
        ),
    ];

    let mut results = vec![];

    for (name, url) in &endpoints {
        let result = retry(Exponential::from_millis(500).take(3), || {
            debug!("Checking {} at {}", name, url);
            match client.get(url).send() {
                Ok(response) => {
                    if response.status() == StatusCode::OK {
                        if *name == "ClientSettings" {
                            // Check ClientSettings response body
                            match response.json::<Value>() {
                                Ok(json) => {
                                    // Assuming valid if "applicationSettings" is not null
                                    let is_valid = !json
                                        .get("applicationSettings")
                                        .map(|v| v.is_null())
                                        .unwrap_or(true);
                                    Ok(is_valid)
                                }
                                Err(e) => {
                                    error!("Failed to parse JSON from {}: {}", url, e);
                                    Ok(false)
                                }
                            }
                        } else {
                            Ok(true)
                        }
                    } else {
                        Ok(false)
                    }
                }
                Err(e) => {
                    error!("Error checking {}: {}", url, e);
                    Err(e)
                }
            }
        })
        .unwrap_or(false);
        results.push(result);
    }

    if results[0] {
        Some(ChannelInfo {
            colored_channel: color_code_channel(channel_name),
            original_channel: channel_name.to_string(),
            roblox_exe: results[0],
            deploy_history: results[1],
            mac_deploy_history: results[2],
            client_settings: results[3],
        })
    } else {
        None
    }
}

fn color_code_channel(channel: &str) -> ColoredString {
    if channel.to_lowercase().contains("feature")
        || channel.to_lowercase().contains("project")
        || channel.to_lowercase().contains("bugfix")
    {
        channel.bold().blue()
    } else if channel.to_lowercase().contains("canary") {
        channel.bold().yellow()
    } else {
        channel.red()
    }
}

errors.rs
use thiserror::Error;

#[derive(Debug, Error)]
pub enum AppError {
    #[error("IO error occurred: {0}")]
    Io(#[from] std::io::Error),

    #[error("HTTP request error: {0}")]
    Http(#[from] reqwest::Error),

    #[error("Serialization error: {0}")]
    Serde(#[from] serde_json::Error),

    #[error("Regex error: {0}")]
    Regex(#[from] regex::Error),
    
    #[error("Other error: {0}")]
    Other(String),
}

main.rs

use crate::models::ChannelInfo;
use crate::models::PatternGroup;
use colored::ColoredString;
use colored::Colorize;
use crossterm::style::Stylize;
use prettytable::{row, Table};
use tui::text::Span;
use tui::text::Spans;
use std::io;
use tui::{
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    widgets::{Block, Borders, Cell, Paragraph, Row as TuiRow, Table as TuiTable, Tabs},
    Frame, Terminal,
};

pub mod checker;
pub mod cli;
pub mod errors;
pub mod models;
pub mod pattern;
pub mod ui;

use checker::check_channel;
use cli::parse_args;
use errors::AppError;
use indicatif::{ProgressBar, ProgressStyle};
use itertools::Itertools;
use rayon::prelude::*;
use reqwest::blocking::Client;
use std::collections::HashSet;
use std::fs;
use std::io::{BufRead};
use std::path::Path;
use std::time::Duration;
use tracing::{debug, error, info};
use tracing_subscriber::fmt::format::FmtSpan;
use ui::run_ui;
use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;

fn main() -> Result<(), AppError> {
    // Initialize logging
    tracing_subscriber::fmt()
        .with_span_events(FmtSpan::CLOSE) // Log span end events
        .with_max_level(tracing::Level::INFO)
        .init();

    // Parse command-line arguments
    let matches = parse_args();

    // Set verbosity
    if matches.get_one::<String>("verbose").is_some() {
        info!("Verbose mode enabled");
    }

    // Read wordlist from file or stdin
    let wordlist = if let Some(input_path) = matches.get_one::<String>("input") {
        read_wordlist_from_file(input_path)?
    } else {
        info!("Reading wordlist from stdin. Enter words followed by Enter. Press Ctrl+D (Unix) or Ctrl+Z (Windows) to finish:");
        read_wordlist_from_stdin()?
    };

    // Deduplicate wordlist
    let mut unique_wordlist: Vec<String> = wordlist.into_iter().unique().collect();
    unique_wordlist.sort();

    // Wordlist Augmentation
    if let Some(prefixes_str) = matches.get_one::<String>("prefixes") {
        let prefixes: Vec<&str> = prefixes_str.split(',').collect();
        augment_wordlist_with_prefixes(&mut unique_wordlist, &prefixes);
        info!("Augmented wordlist with prefixes.");
    }

    if let Some(suffixes_str) = matches.get_one::<String>("suffixes") {
        let suffixes: Vec<&str> = suffixes_str.split(',').collect();
        augment_wordlist_with_suffixes(&mut unique_wordlist, &suffixes);
        info!("Augmented wordlist with suffixes.");
    }

    if let Some(num_random_str) = matches.get_one::<String>("random-strings") {
        let num_random: usize = num_random_str.parse().unwrap_or(0);
        augment_wordlist_with_random_strings(&mut unique_wordlist, num_random);
        info!("Augmented wordlist with {} random strings.", num_random);
    }

    if unique_wordlist.is_empty() {
        error!("Wordlist is empty. Please provide at least one word.");
        return Ok(());
    }

    info!("Generating combinations...");
    let combinations = generate_combinations(&unique_wordlist);
    info!("Total combinations generated: {}", combinations.len());

    // Configure HTTP client
    let timeout = matches
        .get_one::<String>("timeout")
        .unwrap()
        .parse::<u64>()
        .unwrap_or(10);
    let client = Client::builder()
        .timeout(Duration::from_secs(timeout))
        .build()?;

    // Setup progress bar
    let pb = ProgressBar::new(combinations.len() as u64);
    pb.set_style(
        ProgressStyle::default_bar()
            .template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
            .unwrap()
            .progress_chars("#>-"),
    );

    let concurrency = matches
        .get_one::<String>("concurrency")
        .unwrap()
        .parse::<usize>()
        .unwrap_or(8);
    rayon::ThreadPoolBuilder::new()
        .num_threads(concurrency)
        .build_global()
        .unwrap();

    info!("Starting channel checks with concurrency level: {}", concurrency);

    // Process combinations in parallel
    let results: Vec<ChannelInfo> = combinations
        .par_iter()
        .map(|combination| {
            let result = check_channel(&client, combination);
            pb.inc(1);
            result
        })
        .filter_map(|res| res)
        .collect();

    pb.finish_with_message("Channel checks completed.");

    info!("Analyzing channel patterns...");
    let patterns = pattern::analyze_patterns(&results.iter().map(|c| c.original_channel.clone()).collect::<Vec<_>>());

    // Display results in advanced UI
    run_ui(patterns.clone(), results.clone())?;

    // Save results to file if output path is provided
    if let Some(output_path) = matches.get_one::<String>("output") {
        info!("Saving results to {}", output_path);
        save_results_to_file(&results, output_path)?;
    }

    Ok(())
}

// Reads wordlist from a file
fn read_wordlist_from_file<P: AsRef<Path>>(path: P) -> Result<Vec<String>, AppError> {
    info!("Reading wordlist from file: {:?}", path.as_ref());
    let file = fs::File::open(path)?;
    let reader = io::BufReader::new(file);
    let wordlist = reader
        .lines()
        .filter_map(Result::ok)
        .filter(|line| !line.trim().is_empty())
        .collect();
    Ok(wordlist)
}

// Reads wordlist from standard input
fn read_wordlist_from_stdin() -> Result<Vec<String>, AppError> {
    let stdin = io::stdin();
    let wordlist = stdin
        .lock()
        .lines()
        .filter_map(Result::ok)
        .filter(|line| !line.trim().is_empty())
        .collect();
    Ok(wordlist)
}

// Generates combinations of words
fn generate_combinations(wordlist: &[String]) -> HashSet<String> {
    let mut combinations = HashSet::new();
    let max_combination_length = 3; // Adjust as needed

    for length in 1..=max_combination_length {
        for combo in wordlist.iter().permutations(length) {
            let concatenated = combo.into_iter().join("");
            combinations.insert(concatenated);
        }
    }

    combinations
}

// Augments the wordlist with prefixes
fn augment_wordlist_with_prefixes(wordlist: &mut Vec<String>, prefixes: &[&str]) {
    let mut new_words = Vec::new();
    for prefix in prefixes {
        for word in wordlist.iter() {
            new_words.push(format!("{}{}", prefix, word));
        }
    }
    wordlist.append(&mut new_words);
}

// Augments the wordlist with suffixes
fn augment_wordlist_with_suffixes(wordlist: &mut Vec<String>, suffixes: &[&str]) {
    let mut new_words = Vec::new();
    for suffix in suffixes {
        for word in wordlist.iter() {
            new_words.push(format!("{}{}", word, suffix));
        }
    }
    wordlist.append(&mut new_words);
}

// Augments the wordlist with random strings
fn augment_wordlist_with_random_strings(wordlist: &mut Vec<String>, num_strings: usize) {
    let rng = thread_rng();
    for _ in 0..num_strings {
        let random_string: String = rng.clone()
            .sample_iter(&Alphanumeric)
            .take(8) // Adjust the length of random strings as needed
            .map(char::from)
            .collect();
        wordlist.push(random_string);
    }
}

// Saves results to a JSON file
fn save_results_to_file(results: &[ChannelInfo], path: &str) -> Result<(), AppError> {
    let json = serde_json::to_string_pretty(results)?;
    fs::write(path, json)?;
    Ok(())
}

models.rs:
use colored::ColoredString;
use serde::Serialize;

#[derive(Debug, Serialize, Clone)]
pub struct ChannelInfo {
    #[serde(skip_serializing)]
    pub colored_channel: ColoredString,
    #[serde(rename = "channel")]
    pub original_channel: String,
    pub roblox_exe: bool,
    pub deploy_history: bool,
    pub mac_deploy_history: bool,
    pub client_settings: bool,
}

#[derive(Debug, Clone, Serialize)]
pub struct PatternGroup {
    pub pattern: String,
    pub channels: Vec<String>,
}

pattern.rs:
use crate::models::PatternGroup;
use regex::Regex;
use std::collections::HashMap;

pub fn analyze_patterns(channels: &[String]) -> Vec<PatternGroup> {
    // Define regex patterns to match
    let patterns = vec![
        regex_patterns("zfeature\\w+"),
        regex_patterns("zproject\\w+"),
        regex_patterns("zbugfix\\w+"),
        regex_patterns("zcanary\\w*"),
        // Add more patterns as needed
    ];

    let mut pattern_map: HashMap<String, Vec<String>> = HashMap::new();

    for channel in channels {
        let mut matched = false;
        for (pattern_str, regex) in &patterns {
            if regex.is_match(channel) {
                pattern_map
                    .entry(pattern_str.clone())
                    .or_insert_with(Vec::new)
                    .push(channel.clone());
                matched = true;
                break;
            }
        }
        if !matched {
            pattern_map
                .entry("Others".to_string())
                .or_insert_with(Vec::new)
                .push(channel.clone());
        }
    }

    // Convert map to vector of PatternGroup
    let mut grouped_patterns = Vec::new();
    for (pattern, channels) in pattern_map {
        grouped_patterns.push(PatternGroup { pattern, channels });
    }

    grouped_patterns
}

fn regex_patterns(pattern: &str) -> (String, Regex) {
    (pattern.to_string(), Regex::new(&format!("(?i)^{}", pattern)).unwrap())
}

ui.rs:
use crate::models::{ChannelInfo, PatternGroup};
use colored::ColoredString;
use colored::Colorize;
use tui::text::Span;
use tui::text::Spans;
use std::io;
use tui::{
    backend::CrosstermBackend,
    layout::{Constraint, Direction, Layout, Rect},
    style::{Color, Modifier, Style},
    widgets::{Block, Borders, Cell, Paragraph, Row as TuiRow, Table as TuiTable, Tabs},
    Frame, Terminal,
};

use tracing::debug;

pub enum Page {
    Overview,
    Channels,
    Patterns,
}

impl std::fmt::Debug for Page {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Page::Overview => write!(f, "Overview"),
            Page::Channels => write!(f, "Channels"),
            Page::Patterns => write!(f, "Patterns"),
        }
    }
}

pub struct App<'a> {
    pub page: Page,
    pub patterns: Vec<PatternGroup>,
    pub channels: Vec<ChannelInfo>,
    pub selected: usize,
    pub input: &'a str,
}

impl<'a> App<'a> {
    pub fn new(patterns: Vec<PatternGroup>, channels: Vec<ChannelInfo>) -> App<'a> {
        App {
            page: Page::Overview,
            patterns,
            channels,
            selected: 0,
            input: "",
        }
    }

    pub fn next(&mut self) {
        self.selected = (self.selected + 1) % 3;
        self.update_page();
    }

    pub fn previous(&mut self) {
        if self.selected > 0 {
            self.selected -= 1;
        } else {
            self.selected = 2; // Wrap around to the last page
        }
        self.update_page();
    }

    fn update_page(&mut self) {
        self.page = match self.selected {
            0 => Page::Overview,
            1 => Page::Channels,
            2 => Page::Patterns,
            _ => Page::Overview,
        };
        debug!("Page updated to: {:?}", self.page);
    }
}

pub fn run_ui(patterns: Vec<PatternGroup>, channels: Vec<ChannelInfo>) -> Result<(), io::Error> {
    // Set up terminal
    let stdout = io::stdout();
    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    // Create app and run it
    let mut app = App::new(patterns, channels);
    let res = tui_run(&mut terminal, &mut app);

    // Restore terminal
    terminal.show_cursor()?;

    res
}

fn tui_run<B: tui::backend::Backend + std::io::Write>(
    terminal: &mut Terminal<B>,
    app: &mut App,
) -> Result<(), io::Error> {
    use crossterm::{
        event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
        execute,
        terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
    };
    use std::time::{Duration, Instant};

    enable_raw_mode()?;
    execute!(terminal.backend_mut(), EnterAlternateScreen, EnableMouseCapture)?;

    let tick_rate = Duration::from_millis(250);
    let mut last_tick = Instant::now();

    loop {
        terminal.draw(|f| ui(f, app))?;

        let timeout = tick_rate
            .checked_sub(last_tick.elapsed())
            .unwrap_or_else(|| Duration::from_secs(0));

        if event::poll(timeout)? {
            if let Event::Key(key) = event::read()? {
                match key.code {
                    KeyCode::Char('q') => {
                        disable_raw_mode()?;
                        execute!(
                            terminal.backend_mut(),
                            LeaveAlternateScreen,
                            DisableMouseCapture
                        )?;
                        break;
                    }
                    KeyCode::Tab => {
                        app.next();
                    }
                    KeyCode::BackTab => {
                        app.previous();
                    }
                    _ => {}
                }
            }
        }

        if last_tick.elapsed() >= tick_rate {
            last_tick = Instant::now();
        }
    }

    Ok(())
}

fn ui<B: tui::backend::Backend>(f: &mut Frame<B>, app: &App) {
    // Define layout
    let chunks = Layout::default()
        .direction(Direction::Vertical)
        .margin(1)
        .constraints(
            [
                Constraint::Length(3),
                Constraint::Min(2),
            ]
            .as_ref(),
        )
        .split(f.size());

    // Render tabs
    let titles = vec!["Overview", "Channels", "Patterns"]
        .iter()
        .map(|t| (*t).to_string())
        .collect::<Vec<_>>();
    let tabs = Tabs::new(titles.iter().map(|t| Spans::from(t.clone())).collect())
        .select(match app.page {
            Page::Overview => 0,
            Page::Channels => 1,
            Page::Patterns => 2,
        })
        .block(Block::default().borders(Borders::ALL).title("Pages"))
        .highlight_style(Style::default().fg(Color::Yellow))
        .style(Style::default().fg(Color::White));

    f.render_widget(tabs, chunks[0]);

    // Render content based on selected page
    match app.page {
        Page::Overview => render_overview(f, app, chunks[1]),
        Page::Channels => render_channels(f, app, chunks[1]),
        Page::Patterns => render_patterns(f, app, chunks[1]),
    }
}

fn render_overview<B: tui::backend::Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
    let total_channels = app.channels.len();
    let feature_channels = app.patterns
        .iter()
        .find(|p| p.pattern.to_lowercase().contains("feature"))
        .map(|p| p.channels.len())
        .unwrap_or(0);

    let paragraph = Paragraph::new(format!(
        "Total Valid Channels: {}\nChannels with 'feature' in their name: {}",
        total_channels, feature_channels
    ))
    .block(Block::default().borders(Borders::ALL).title("Overview"));

    f.render_widget(paragraph, area);
}

fn render_channels<B: tui::backend::Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
    if app.channels.is_empty() {
        let message = Paragraph::new("No channels available.")
            .block(Block::default().borders(Borders::ALL).title("Channels"));
        f.render_widget(message, area);
        return;
    }

    // Create a table
    let header = ["Channel", "Roblox.exe", "DeployHistory.txt", "Mac DeployHistory.txt", "ClientSettings"]
        .iter()
        .cloned()
        .map(Cell::from)
        .collect::<Vec<_>>();
    
    let rows = app.channels.iter().map(|c| {
        TuiRow::new(vec![
            c.colored_channel.clone().to_string().into(),
            bool_to_cell(c.roblox_exe),
            bool_to_cell(c.deploy_history),
            bool_to_cell(c.mac_deploy_history),
            bool_to_cell(c.client_settings),
        ])
    });

    let table = TuiTable::new(rows)
        .header(
            TuiRow::new(header)
                .style(Style::default().fg(Color::Yellow))
                .bottom_margin(1),
        )
        .block(Block::default().borders(Borders::ALL).title("Channels"))
        .widths(&[
            Constraint::Percentage(25),
            Constraint::Percentage(15),
            Constraint::Percentage(15),
            Constraint::Percentage(20),
            Constraint::Percentage(25),
        ]);
    
    f.render_widget(table, area);
}

fn bool_to_cell(value: bool) -> Cell<'static> {
    if value {
        Cell::from("✅")
    } else {
        Cell::from("❌")
    }
}

fn render_patterns<B: tui::backend::Backend>(f: &mut Frame<B>, app: &App, area: Rect) {
    let block = Block::default().borders(Borders::ALL).title("Pattern Groups");
    let inner = block.inner(area);
    f.render_widget(block, area);

    let text = app.patterns.iter().fold(String::new(), |acc, pattern| {
        format!(
            "{}Pattern: {}\nChannels: {}\n\n",
            acc,
            pattern.pattern.clone(),
            pattern.channels.join(", ")
        )
    });

    let paragraph = Paragraph::new(text)
        .block(Block::default())
        .wrap(tui::widgets::Wrap { trim: true });

    f.render_widget(paragraph, inner);
}

Remove the UI and replace it with a CLI, not TUI or crossterm just sorta idk