Improve this program massively. Improve the word generation algorithm.

use clap::{Arg, ArgAction, Command as ClapCommand};
use colored::*;
use config::{Config, File};
use futures::stream::{FuturesUnordered, StreamExt};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use itertools::Itertools;
use prettytable::{row, Table};
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use reqwest::StatusCode;
use serde::Serialize;
use std::{
    collections::HashSet,
    fs,
    io::{self, BufRead},
    path::Path,
    sync::Arc,
};
use thiserror::Error;
use tokio::sync::Semaphore;
use tracing::{debug, error, info, warn};
use tracing_subscriber::fmt::{self, format::FmtSpan};

// Custom Types and Structures

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

#[derive(Debug, Serialize)]
struct Summary {
    total_combinations: usize,
    valid_channels: usize,
    invalid_channels: usize,
}

#[derive(Debug, Error)]
enum AppError {
    #[error("IO error occurred: {0}")]
    Io(#[from] io::Error),
    #[error("HTTP request error: {0}")]
    Http(#[from] reqwest::Error),
    #[error("Serialization error: {0}")]
    Serde(#[from] serde_json::Error),
    #[error("Configuration error: {0}")]
    Config(#[from] config::ConfigError),
}

// Configuration Structure

#[derive(Debug, Deserialize)]
struct Settings {
    input_files: Option<Vec<String>>,
    output: Option<String>,
    concurrency: usize,
    timeout: u64,
    verbosity: u64,
    prefixes: Option<Vec<String>>,
    suffixes: Option<Vec<String>>,
    random_strings: Option<usize>,
    max_combination_length: usize,
    retry_attempts: usize,
    retry_base_delay_ms: u64,
    rate_limit_per_sec: usize,
    output_format: String, // json, csv, yaml
}

impl Settings {
    fn new(args: &clap::ArgMatches) -> Result<Self, AppError> {
        // Initialize default configuration
        let mut settings = Config::default();

        // Merge configuration from file if provided
        if let Some(config_path) = args.get_one::<String>("config") {
            settings.merge(File::with_name(config_path))?;
        }

        // Override with command-line arguments
        settings.merge(config::Environment::with_prefix("APP"))?;

        // Deserialize into Settings struct
        let cfg = settings.try_into::<Settings>()?;
        Ok(cfg)
    }
}

// Custom Logger Initialization

fn init_logger(verbosity: u64) {
    let log_level = match verbosity {
        0 => "info",
        1 => "debug",
        2 => "trace",
        _ => "trace",
    };

    tracing_subscriber::fmt()
        .with_max_level(log_level.parse().unwrap_or(tracing::Level::INFO))
        .with_span_events(FmtSpan::CLOSE)
        .with_target(false)
        .with_timer(fmt::time::ChronoLocal::rfc3339())
        .init();

    info!(
        "[ 🌃 ] intelligence : Logger initialized with level {}",
        log_level
    );
}

// Wordlist Handling Functions

async fn read_wordlist_from_file(path: &str) -> Result<Vec<String>, AppError> {
    info!("[ 🌃 ] intelligence : Reading wordlist from file: {}", path);
    let file = fs::File::open(path)?;
    let reader = io::BufReader::new(file);
    let wordlist = reader
        .lines()
        .filter_map(|line| line.ok())
        .map(|line| line.trim().to_string())
        .filter(|line| !line.is_empty())
        .collect::<Vec<String>>();
    Ok(wordlist)
}

async fn read_wordlist_from_stdin() -> Result<Vec<String>, AppError> {
    info!("[ 🌃 ] intelligence : Reading wordlist from standard input");
    let stdin = io::stdin();
    let wordlist = stdin
        .lock()
        .lines()
        .filter_map(|line| line.ok())
        .map(|line| line.trim().to_string())
        .filter(|line| !line.is_empty())
        .collect::<Vec<String>>();
    Ok(wordlist)
}

fn augment_wordlist(
    wordlist: &mut Vec<String>,
    prefixes: &[String],
    suffixes: &[String],
    num_random: usize,
) {
    info!("[ 🌃 ] intelligence : Augmenting wordlist with prefixes, suffixes, and random strings");

    // Add prefixes
    if !prefixes.is_empty() {
        let prefixed = prefixes
            .iter()
            .cartesian_product(wordlist.iter())
            .map(|(p, w)| format!("{}{}", p, w))
            .collect::<Vec<String>>();
        wordlist.extend(prefixed);
    }

    // Add suffixes
    if !suffixes.is_empty() {
        let suffixed = suffixes
            .iter()
            .cartesian_product(wordlist.iter())
            .map(|(s, w)| format!("{}{}", w, s))
            .collect::<Vec<String>>();
        wordlist.extend(suffixed);
    }

    // Add random strings
    if num_random > 0 {
        let mut rng = thread_rng();
        let random_strings: Vec<String> = (0..num_random)
            .map(|_| {
                rng.sample_iter(&Alphanumeric)
                    .take(8)
                    .map(char::from)
                    .collect()
            })
            .collect();
        wordlist.extend(random_strings);
    }

    // Deduplicate and sort
    let unique_wordlist: HashSet<String> = wordlist.clone().into_iter().collect();
    let mut unique_vec: Vec<String> = unique_wordlist.into_iter().collect();
    unique_vec.sort();
    wordlist.clear();
    wordlist.extend(unique_vec);

    info!(
        "[ 🌃 ] intelligence : Wordlist augmented. Total unique words: {}",
        wordlist.len()
    );
}

// Combination Generation

fn generate_combinations(wordlist: &[String], max_length: usize) -> HashSet<String> {
    info!(
        "[ 🌃 ] intelligence : Generating combinations up to length {}",
        max_length
    );
    let mut combinations = HashSet::new();

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

    info!(
        "[ 🌃 ] intelligence : Total combinations generated: {}",
        combinations.len()
    );
    combinations
}

// Channel Checking Function

async fn check_channel(
    client: &reqwest::Client,
    channel_name: &str,
    retry_attempts: usize,
    base_delay_ms: u64,
) -> 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 mut attempt = 0;
        loop {
            attempt += 1;
            match client.get(url).send().await {
                Ok(response) => {
                    if response.status() == StatusCode::OK {
                        if *name == "ClientSettings" {
                            match response.text().await {
                                Ok(body) => {
                                    results.push(!body.contains(r#"{"applicationSettings":null}"#));
                                }
                                Err(e) => {
                                    warn!(
                                        "[ 🌃 ] intelligence : Failed to read body for {}: {}",
                                        url, e
                                    );
                                    results.push(false);
                                }
                            }
                        } else {
                            results.push(true);
                        }
                        break;
                    } else {
                        results.push(false);
                        break;
                    }
                }
                Err(e) => {
                    if attempt >= retry_attempts {
                        error!(
                            "[ 🌃 ] intelligence : Failed to fetch {} after {} attempts: {}",
                            url, attempt, e
                        );
                        results.push(false);
                        break;
                    } else {
                        warn!(
                            "[ 🌃 ] intelligence : Error fetching {}: {}. Retrying... (attempt {})",
                            url, e, attempt
                        );
                        tokio::time::sleep(
                            tokio::time::Duration::from_millis(base_delay_ms * 2_u64.pow(attempt)),
                        )
                        .await;
                        continue;
                    }
                }
            }
        }
    }

    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.contains("feature")
        || channel.contains("project")
        || channel.contains("bugfix")
        || channel.contains("hotfix")
    {
        channel.bold().blue()
    } else if channel.contains("release") || channel.contains("stable") {
        channel.bold().green()
    } else {
        channel.red()
    }
}

// Reporting Functions

fn display_results(results: &[ChannelInfo]) {
    info!("[ 🌃 ] intelligence : Displaying results in table format");

    let mut table = Table::new();
    table.add_row(row![
        "Channel",
        "Roblox.exe",
        "DeployHistory.txt",
        "Mac DeployHistory.txt",
        "ClientSettings",
    ]);

    for info in results {
        table.add_row(row![
            info.colored_channel,
            status_to_mark(info.roblox_exe),
            status_to_mark(info.deploy_history),
            status_to_mark(info.mac_deploy_history),
            status_to_mark(info.client_settings),
        ]);
    }

    table.printstd();
}

fn status_to_mark(status: bool) -> ColoredString {
    if status {
        "✅".green()
    } else {
        "❌".red()
    }
}

fn save_results(
    results: &[ChannelInfo],
    summary: &Summary,
    path: &str,
    format: &str,
) -> Result<(), AppError> {
    info!("[ 🌃 ] intelligence : Saving results to {}", path);

    match format.to_lowercase().as_str() {
        "json" => {
            let output = serde_json::json!({
                "summary": summary,
                "results": results,
            });
            fs::write(path, serde_json::to_string_pretty(&output)?)?;
        }
        "csv" => {
            let mut wtr = csv::Writer::from_path(path)?;
            wtr.write_record(&[
                "Channel",
                "Roblox.exe",
                "DeployHistory.txt",
                "Mac DeployHistory.txt",
                "ClientSettings",
            ])?;
            for info in results {
                wtr.write_record(&[
                    &info.original_channel,
                    &info.roblox_exe.to_string(),
                    &info.deploy_history.to_string(),
                    &info.mac_deploy_history.to_string(),
                    &info.client_settings.to_string(),
                ])?;
            }
            wtr.flush()?;
        }
        "yaml" => {
            let output = serde_yaml::to_string(&serde_json::json!({
                "summary": summary,
                "results": results,
            }))?;
            fs::write(path, output)?;
        }
        _ => {
            warn!(
                "[ 🌃 ] intelligence : Unsupported output format '{}'. Defaulting to JSON.",
                format
            );
            let output = serde_json::json!({
                "summary": summary,
                "results": results,
            });
            fs::write(path, serde_json::to_string_pretty(&output)?)?;
        }
    }

    Ok(())
}

// Main Execution Function

#[tokio::main]
async fn main() -> Result<(), AppError> {
    // Define command-line arguments with Clap
    let matches = ClapCommand::new("Roblox Channel Checker Plus")
        .version("2.0")
        .author("Your Name <youremail@example.com>")
        .about("Advanced checker for valid Roblox channels based on wordlist combinations")
        .arg(
            Arg::new("config")
                .short('f')
                .long("config")
                .value_name("FILE")
                .help("Sets a custom configuration file")
                .takes_value(true),
        )
        .arg(
            Arg::new("input_files")
                .short('i')
                .long("input")
                .value_name("FILES")
                .help("Sets input wordlist files (comma-separated)")
                .action(ArgAction::Append)
                .multiple_values(true),
        )
        .arg(
            Arg::new("output")
                .short('o')
                .long("output")
                .value_name("FILE")
                .help("Sets an output file to save results")
                .takes_value(true),
        )
        .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("verbosity")
                .short('v')
                .long("verbose")
                .help("Increases verbosity level")
                .action(ArgAction::Count),
        )
        .arg(
            Arg::new("prefixes")
                .short('p')
                .long("prefixes")
                .value_name("PREFIXES")
                .help("Comma-separated list of prefixes to add to words")
                .takes_value(true),
        )
        .arg(
            Arg::new("suffixes")
                .short('s')
                .long("suffixes")
                .value_name("SUFFIXES")
                .help("Comma-separated list of suffixes to add to words")
                .takes_value(true),
        )
        .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")
                .default_value("0"),
        )
        .arg(
            Arg::new("max_combination_length")
                .short('m')
                .long("max-combination-length")
                .value_name("NUMBER")
                .help("Maximum length of word combinations")
                .default_value("3"),
        )
        .arg(
            Arg::new("retry_attempts")
                .long("retry-attempts")
                .value_name("NUMBER")
                .help("Number of retry attempts for failed HTTP requests")
                .default_value("3"),
        )
        .arg(
            Arg::new("retry_base_delay_ms")
                .long("retry-delay")
                .value_name("MILLISECONDS")
                .help("Base delay in ms for retries with exponential backoff")
                .default_value("500"),
        )
        .arg(
            Arg::new("rate_limit_per_sec")
                .long("rate-limit")
                .value_name("NUMBER")
                .help("Number of allowed requests per second")
                .default_value("50"),
        )
        .arg(
            Arg::new("output_format")
                .long("output-format")
                .value_name("FORMAT")
                .help("Output format: json, csv, yaml")
                .default_value("json"),
        )
        .get_matches();

    // Initialize configuration
    let settings = Settings::new(&matches)?;

    // Initialize logging
    init_logger(settings.verbosity);

    // Read and prepare wordlist
    let mut wordlist = Vec::new();

    if let Some(files) = &settings.input_files {
        for file in files {
            let words = read_wordlist_from_file(file).await?;
            wordlist.extend(words);
        }
    } else {
        let words = read_wordlist_from_stdin().await?;
        wordlist.extend(words);
    }

    // Augment wordlist
    let prefixes = settings
        .prefixes
        .unwrap_or_else(|| vec![])
        .iter()
        .map(|s| s.to_string())
        .collect::<Vec<String>>();
    let suffixes = settings
        .suffixes
        .unwrap_or_else(|| vec![])
        .iter()
        .map(|s| s.to_string())
        .collect::<Vec<String>>();
    let random_strings = settings.random_strings.unwrap_or(0);

    augment_wordlist(&mut wordlist, &prefixes, &suffixes, random_strings);

    if wordlist.is_empty() {
        error!("[ 🌃 ] intelligence : Wordlist is empty. Please provide at least one word.");
        return Ok(());
    }

    // Generate combinations
    let combinations = generate_combinations(&wordlist, settings.max_combination_length);
    let total_combinations = combinations.len();

    // Initialize HTTP client
    let client = reqwest::Client::builder()
        .timeout(tokio::time::Duration::from_secs(settings.timeout))
        .build()?;

    // Initialize rate limiter
    let semaphore = Arc::new(Semaphore::new(settings.rate_limit_per_sec));

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

    // Initialize result storage
    let results = Arc::new(tokio::sync::Mutex::new(Vec::new()));

    // Initialize a futures unordered collection
    let futures = combinations.into_iter().map(|combination| {
        let client = client.clone();
        let semaphore = semaphore.clone();
        let results = results.clone();
        let pb = pb.clone();
        let output_format = settings.output_format.clone();
        let retry_attempts = settings.retry_attempts;
        let retry_base_delay_ms = settings.retry_base_delay_ms;

        tokio::spawn(async move {
            let _permit = semaphore.acquire().await;
            if let Some(info) = check_channel(
                &client,
                &combination,
                retry_attempts,
                retry_base_delay_ms,
            )
            .await
            {
                results.lock().await.push(info);
            }
            pb.inc(1);
        })
    });

    // Execute all futures concurrently with rate limiting
    let fut_unordered = FuturesUnordered::from_iter(futures);

    tokio::spawn(async move {
        mp.join().unwrap();
    });

    while fut_unordered.next().await.is_some() {}

    // Gather results
    let results = Arc::try_unwrap(results)
        .expect("Arc has multiple owners")
        .into_inner();

    // Display results
    if !results.is_empty() {
        display_results(&results);
    } else {
        info!("[ 🌃 ] intelligence : No valid channels found.");
    }

    // Prepare summary
    let summary = Summary {
        total_combinations,
        valid_channels: results.len(),
        invalid_channels: total_combinations - results.len(),
    };

    // Save results if output path is provided
    if let Some(output_path) = &settings.output {
        save_results(&results, &summary, output_path, &settings.output_format)?;
    }

    // Display summary
    info!("[ 🌃 ] intelligence : Execution Summary:");
    println!("Total Combinations Generated: {}", summary.total_combinations);
    println!("Valid Channels Found: {}", summary.valid_channels);
    println!("Invalid Channels: {}", summary.invalid_channels);

    Ok(())
}

Actual known channels:

zfeatureharmony
zintegration
zintegrationtest
zfeaturerail
zprojecteliu
zprojecteliurcc
zmoregranulartelemetryprofiling
zrenderinvalidateopttest
zhermeticbuilds
zunifiedhangmonitor
zfallthroughcleanup
zfixdockpanelcrash
zfcthreadsafe
zinstanceclusterdeferredmeshrebind
as you can see, they get crazier weirdly enough

However, none seem to have numbers unless its another iteration of the channel or a project/bugfix

zcompatibilitytelemetrychannelflagonly
zassetcacheinitprod
zbasercu

Add some advanced algorithm to know where words are normally
Like
{first_word} {second_word} {third_word} , etc.

z{first_word}{second_word}
zfeature{first_word}{second_word?}{number?/testing}
etc and beyond

We are trying to find `new` channels here. Not existing ones.