scroll-chat 0.1.0

A secure terminal chat over SSH - host or join chatrooms with end-to-end encryption
//! Scroll - Secure SSH-based terminal chat
//!
//! A lightweight Rust-based CLI tool for secure, real-time communication
//! that allows users to join and host chatrooms directly over SSH.

mod client;
mod error;
mod room;
mod server;
mod tui;
mod tunnel;

use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

const BANNER: &str = r#"
   _____  _____ _____   ____  _      _      
  / ____|/ ____|  __ \ / __ \| |    | |     
 | (___ | |    | |__) | |  | | |    | |     
  \___ \| |    |  _  /| |  | | |    | |     
  ____) | |____| | \ \| |__| | |____| |____ 
 |_____/ \_____|_|  \_\\____/|______|______|
                                            
  Secure SSH Chat - Because privacy matters 🔐
"#;

#[derive(Parser, Debug)]
#[command(
    name = "scroll",
    version,
    about = "A lightweight Rust-based CLI tool for secure, real-time communication over SSH",
    long_about = BANNER
)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
    
    /// Enable verbose logging
    #[arg(short, long, global = true)]
    verbose: bool,
}

#[derive(Subcommand, Debug)]
enum Commands {
    /// Create and host a new chat room
    Create {
        /// Port to listen on
        #[arg(short, long, default_value = "2222")]
        port: u16,
        
        /// Room password (required)
        #[arg(long, short = 'w')]
        pw: String,
        
        /// Your display name
        #[arg(short, long, default_value = "host")]
        user: String,
        
        /// Enable cloudflared tunnel for public access
        #[arg(short, long)]
        tunnel: bool,
        
        /// Room name/title
        #[arg(short, long, default_value = "Scroll Chat")]
        name: String,
    },
    
    /// Join an existing chat room
    Join {
        /// Server address (IP:PORT or tunnel URL)
        address: String,
        
        /// Room password
        #[arg(long, short = 'w')]
        pw: String,
        
        /// Your display name (required)
        #[arg(short, long)]
        user: String,
    },
}

#[tokio::main]
async fn main() -> Result<()> {
    let cli = Cli::parse();
    
    // Initialize logging
    let filter = if cli.verbose {
        EnvFilter::new("scroll=debug,russh=debug")
    } else {
        EnvFilter::new("scroll=info")
    };
    
    tracing_subscriber::registry()
        .with(fmt::layer().with_target(false).with_level(true))
        .with(filter)
        .init();
    
    match cli.command {
        Commands::Create { port, pw, user, tunnel, name } => {
            create_room(port, pw, user, tunnel, name).await
        }
        Commands::Join { address, pw, user } => {
            join_room(address, pw, user).await
        }
    }
}

/// Create and host a chat room
async fn create_room(
    port: u16,
    password: String,
    _username: String,  // Unused - creator is server-only
    enable_tunnel: bool,
    room_name: String,
) -> Result<()> {
    eprintln!("{}", BANNER);
    
    // Hash the password
    let password_hash = room::ChatRoom::hash_password(&password)
        .context("Failed to hash password")?;
    
    // Create the room
    let room = room::ChatRoom::new(room_name.clone(), password_hash);
    
    eprintln!("📜 Room: {}", room_name);
    eprintln!("🔒 Password protected: Yes");
    eprintln!("🌐 Local: scroll join 127.0.0.1:{} --pw <password> --user <name>", port);
    
    // Start bore tunnel if requested (free, no signup)
    let _tunnel = if enable_tunnel {
        eprintln!("\n🌍 Starting bore tunnel (free, no signup needed)...");
        match tunnel::BoreTunnel::start(port).await {
            Ok(t) => {
                let connection = t.connection_string();
                room.set_tunnel_url(connection.clone()).await;
                eprintln!("🔗 Public: scroll join {} --pw <password> --user <name>", connection);
                eprintln!("   Share this with others to join!");
                Some(t)
            }
            Err(e) => {
                eprintln!("⚠️  Tunnel failed: {}", e);
                eprintln!("   Continuing with local-only access...");
                None
            }
        }
    } else {
        None
    };
    
    eprintln!("\n✨ Room is ready! Waiting for connections...");
    eprintln!("   Press Ctrl+C to close the room.\n");
    
    // Set up signal handler for graceful shutdown
    tokio::spawn(async move {
        tokio::signal::ctrl_c().await.ok();
        eprintln!("\n\n👋 Shutting down room...");
    });
    
    // Start the SSH server
    let server = server::ChatServer::new(room);
    server.run(port).await?;
    
    Ok(())
}

/// Join an existing chat room
async fn join_room(address: String, password: String, username: String) -> Result<()> {
    eprintln!("{}", BANNER);
    
    let config = client::ClientConfig {
        address,
        password,
        username,
    };
    
    client::connect(config).await
}