matrix-bridge-telegram 0.1.0

A bridge between Matrix and Telegram written in Rust
Documentation
use std::path::PathBuf;

use clap::{Parser, Subcommand};
use serde_json::json;

#[derive(Parser, Debug)]
#[command(name = "matrix-telegram-bridge")]
#[command(about = "Matrix-Telegram Bridge", long_about = None)]
pub struct Cli {
    #[command(subcommand)]
    pub command: Option<Commands>,

    #[arg(short, long, env = "CONFIG_PATH", default_value = "config.yaml")]
    pub config: PathBuf,

    #[arg(short, long, env = "REGISTRATION_PATH")]
    pub registration: Option<PathBuf>,
}

#[derive(Subcommand, Debug)]
pub enum Commands {
    #[command(about = "Generate a registration file for the Matrix homeserver")]
    GenerateRegistration {
        #[arg(short, long, default_value = "telegram-registration.yaml")]
        output: PathBuf,

        #[arg(long, default_value = "telegram")]
        id: String,

        #[arg(long, default_value = "http://localhost:8008")]
        homeserver_url: String,

        #[arg(long, default_value = "example.org")]
        domain: String,
    },

    #[command(about = "Grant admin privileges to a Matrix user")]
    Adminme {
        #[arg(short, long)]
        user: String,

        #[arg(short, long)]
        room: Option<String>,

        #[arg(short, long, default_value = "100")]
        power_level: i64,
    },

    #[command(about = "Migrate data between database backends")]
    Migrate {
        #[arg(long, help = "Source database URL")]
        from: String,

        #[arg(long, help = "Destination database URL")]
        to: String,

        #[arg(short, long, help = "Dry run without making changes")]
        dry_run: bool,
    },

    #[command(about = "List all bridged rooms")]
    ListRooms {
        #[arg(short, long, help = "Filter by chat type (user, group, channel)")]
        chat_type: Option<String>,

        #[arg(short, long, default_value = "100")]
        limit: i64,
    },

    #[command(about = "Unbridge a room")]
    Unbridge {
        #[arg(short, long, help = "Matrix room ID")]
        room: String,

        #[arg(short, long, help = "Also leave the Matrix room")]
        leave: bool,
    },

    #[command(about = "Validate the configuration file")]
    ValidateConfig,

    #[command(about = "Show bridge status")]
    Status,
}

pub fn generate_registration(id: &str, homeserver_url: &str, domain: &str) -> String {
    let as_token = generate_token();
    let hs_token = generate_token();

    let registration = json!({
        "id": id,
        "url": homeserver_url,
        "as_token": as_token,
        "hs_token": hs_token,
        "sender_localpart": "_telegram_",
        "rate_limited": false,
        "protocols": ["telegram"],
        "namespaces": {
            "users": [{
                "exclusive": true,
                "regex": format!("@_telegram_.*:{}", domain)
            }],
            "aliases": [{
                "exclusive": true,
                "regex": format!("#_telegram_.*:{}", domain)
            }],
            "rooms": []
        }
    });

    serde_yaml::to_string(&registration).unwrap_or_default()
}

fn generate_token() -> String {
    use std::collections::hash_map::RandomState;
    use std::hash::{BuildHasher, Hasher};

    let state = RandomState::new();
    let mut hasher = state.build_hasher();
    hasher.write_u64(
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap_or_default()
            .as_nanos() as u64,
    );
    format!("{:x}", hasher.finish())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn generate_registration_produces_valid_yaml() {
        let yaml = generate_registration("test", "http://localhost:8008", "example.org");
        assert!(yaml.contains("id: test"));
        assert!(yaml.contains("as_token:"));
        assert!(yaml.contains("hs_token:"));
        assert!(yaml.contains("protocols:"));
    }
}