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>,
output: Option,
concurrency: usize,
timeout: u64,
verbosity: u64,
prefixes: Option<Vec>,
suffixes: Option<Vec>,
random_strings: Option,
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, 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>();
Ok(wordlist)
}

async fn read_wordlist_from_stdin() -> Result<Vec, 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>();
Ok(wordlist)
}

fn augment_wordlist(
wordlist: &mut Vec,
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 {
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 {
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
zslowerterrain
zchunkexperimentbinary
 zchunkexperimentbinarycontrol
zuserinfoacquiredlog
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.