package main import ( "bytes" "crypto/rand" "encoding/hex" "flag" "fmt" "log" "os" "os/signal" "syscall" "local/sneedchatbridge/config" "local/sneedchatbridge/cookie" "local/sneedchatbridge/matrix" "local/sneedchatbridge/sneed" ) func main() { // ------------------------------------------------------------ // COMMAND-LINE FLAG HANDLING // ------------------------------------------------------------ genReg := flag.Bool("generate-registration", false, "Generate registration.yaml and exit") outPath := flag.String("out", "registration.yaml", "Output path for generated registration.yaml") flag.Parse() // ------------------------------------------------------------ // LOAD CONFIG // ------------------------------------------------------------ cfg, err := config.Load(".env") if err != nil { log.Fatalf("❌ Failed to load configuration: %v", err) } // If --generate-registration was passed, output registration.yaml and exit. if *genReg { err := generateRegistrationFile(cfg, *outPath) if err != nil { log.Fatalf("❌ Failed to generate registration file: %v", err) } log.Printf("🟢 Registration file written to %s", *outPath) return } // ------------------------------------------------------------ // NORMAL BRIDGE STARTUP (APPSERVICE MODE ONLY) // ------------------------------------------------------------ fmt.Println("===============================================") fmt.Println(" Matrix Appservice ↔ Sneedchat Bridge") fmt.Println("===============================================") log.Printf("Using Sneedchat room ID: %d", cfg.SneedchatRoomID) log.Printf("Matrix Appservice listen address: %s", cfg.AppserviceListenAddr) log.Printf("Ghost MXID domain: %s (prefix=%q)", cfg.MatrixGhostUserDomain, cfg.MatrixGhostUserPrefix) // ------------------------------------------------------------ // COOKIE REFRESH SERVICE // ------------------------------------------------------------ ck, err := cookie.NewCookieRefreshServiceWithDebug( cfg.BridgeUsername, cfg.BridgePassword, kiwiDomain(), cfg.Debug, ) if err != nil { log.Fatalf("❌ Cannot create cookie refresh service: %v", err) } ck.Start() ck.WaitForCookie() log.Println("🟢 Initial XenForo session cookie acquired.") // ------------------------------------------------------------ // SNEEDCHAT CLIENT INIT // ------------------------------------------------------------ sneedClient := sneed.NewClient(cfg.SneedchatRoomID, ck) // ------------------------------------------------------------ // MATRIX BRIDGE INIT (APPSERVICE) // ------------------------------------------------------------ bridge := matrix.NewBridge(cfg, sneedClient) // ------------------------------------------------------------ // START COMPONENTS // ------------------------------------------------------------ // Start Sneedchat WebSocket client if err := sneedClient.Connect(); err != nil { log.Fatalf("❌ Sneedchat initial connect failed: %v", err) } // Start Matrix Appservice HTTP server go func() { if err := bridge.StartAppserviceServer(cfg); err != nil { log.Fatalf("❌ Appservice HTTP server error: %v", err) } }() // ------------------------------------------------------------ // WAIT FOR SHUTDOWN SIGNAL // ------------------------------------------------------------ sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) s := <-sigCh log.Printf("🔻 Shutdown signal received: %v", s) // ------------------------------------------------------------ // SHUT DOWN GRACEFULLY // ------------------------------------------------------------ // If Bridge implements Stop() we call it. (Optional) if stopper, ok := interface{}(bridge).(interface{ Stop() }); ok { stopper.Stop() } sneedClient.Disconnect() ck.Stop() log.Println("🟡 Bridge stopped.") } // ------------------------------------------------------------ // REGISTRATION GENERATOR // ------------------------------------------------------------ // generateRegistrationFile writes the appservice registration.yaml // based on values loaded from .env. func generateRegistrationFile(cfg *config.Config, outPath string) error { template := `id: sneedchat url: "http://127.0.0.1:29333" as_token: "%s" hs_token: "%s" sender_localpart: "sneedbridge" rate_limited: false namespaces: users: - regex: "^@sneedbridge:%s$" exclusive: true - regex: "^@.*:%s$" exclusive: true aliases: [] rooms: [] push_ephemeral: true de.sorunome.msc2409.push_ephemeral: true ` var buf bytes.Buffer hsToken := randomHex(64) // Synapse expects strong token buf.WriteString(fmt.Sprintf( template, cfg.MatrixAppserviceToken, hsToken, cfg.MatrixGhostUserDomain, cfg.MatrixGhostUserDomain, )) return os.WriteFile(outPath, buf.Bytes(), 0600) } // randomHex generates a cryptographically random hex token. func randomHex(n int) string { b := make([]byte, n/2) _, err := rand.Read(b) if err != nil { panic(err) } return hex.EncodeToString(b) } // kiwiDomain returns the XenForo/Sneedchat domain name. func kiwiDomain() string { return "kiwifarms.st" }