package main import ( "bufio" "context" "flag" "fmt" "log" "os" "os/signal" "path/filepath" "strconv" "strings" "syscall" "golang.org/x/term" "github.com/sartoopjj/thefeed/internal/server" "github.com/sartoopjj/thefeed/internal/version" ) func main() { dataDir := flag.String("data-dir", "./data", "Data directory for channels, session, and config") listen := flag.String("listen", ":53", "DNS listen address (host:port)") domain := flag.String("domain", "", "DNS domain (e.g., t.example.com)") key := flag.String("key", "", "Encryption passphrase") channelsFile := flag.String("channels", "", "Path to channels file (default: {data-dir}/channels.txt)") xAccountsFile := flag.String("x-accounts", "", "Path to X accounts file (default: {data-dir}/x_accounts.txt)") xRSSInstances := flag.String("x-rss-instances", "", "Comma-separated X RSS base URLs (e.g., http://nitter.net,https://nitter.net)") apiID := flag.String("api-id", "", "Telegram API ID (optional if --no-telegram)") apiHash := flag.String("api-hash", "", "Telegram API Hash (optional if --no-telegram)") phone := flag.String("phone", "", "Telegram phone number (optional if --no-telegram)") loginOnly := flag.Bool("login-only", false, "Authenticate to Telegram, save session, and exit") noTelegram := flag.Bool("no-telegram", false, "Fetch public channels without Telegram login") sessionPath := flag.String("session", "", "Path to Telegram session file (default: {data-dir}/session.json)") maxPadding := flag.Int("padding", 32, "Max random padding bytes in DNS responses (anti-DPI, 0=disabled)") msgLimit := flag.Int("msg-limit", 15, "Maximum messages to fetch per Telegram channel") allowManage := flag.Bool("allow-manage", false, "Allow remote channel management and sending via DNS") debug := flag.Bool("debug", false, "Log every decoded DNS query") showVersion := flag.Bool("version", false, "Show version and exit") flag.Usage = func() { fmt.Fprintf(os.Stderr, "thefeed-server %s\n\nServes Telegram/X feed content over encrypted DNS for censorship-resistant access.\n\nUsage:\n thefeed-server [flags]\n\nFlags:\n", version.Version) flag.PrintDefaults() } flag.Parse() if *showVersion { fmt.Printf("thefeed-server %s (commit: %s, built: %s)\n", version.Version, version.Commit, version.Date) os.Exit(0) } // Create data directory if err := os.MkdirAll(*dataDir, 0700); err != nil { log.Fatalf("Create data dir: %v", err) } // Default paths relative to data directory if *channelsFile == "" { *channelsFile = filepath.Join(*dataDir, "channels.txt") } if *xAccountsFile == "" { *xAccountsFile = filepath.Join(*dataDir, "x_accounts.txt") } if *sessionPath == "" { *sessionPath = filepath.Join(*dataDir, "session.json") } if *domain == "" { *domain = os.Getenv("THEFEED_DOMAIN") } if *key == "" { *key = os.Getenv("THEFEED_KEY") } if !*allowManage && os.Getenv("THEFEED_ALLOW_MANAGE") == "1" { *allowManage = true } // THEFEED_ALLOW_MANAGE=0 explicitly disables, even if flag was set if os.Getenv("THEFEED_ALLOW_MANAGE") == "0" { *allowManage = false } if *msgLimit == 15 { if v := os.Getenv("THEFEED_MSG_LIMIT"); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { *msgLimit = n } } } if *apiID == "" { *apiID = os.Getenv("TELEGRAM_API_ID") } if *apiHash == "" { *apiHash = os.Getenv("TELEGRAM_API_HASH") } if *phone == "" { *phone = os.Getenv("TELEGRAM_PHONE") } if *xRSSInstances == "" { *xRSSInstances = os.Getenv("THEFEED_X_RSS_INSTANCES") } if *domain == "" || *key == "" { fmt.Fprintln(os.Stderr, "Error: --domain and --key are required") flag.Usage() os.Exit(1) } // Telegram credentials are required unless --no-telegram needTelegram := !*noTelegram if needTelegram { if *apiID == "" || *apiHash == "" || *phone == "" { fmt.Fprintln(os.Stderr, "Error: --api-id, --api-hash, and --phone are required (use --no-telegram to skip)") flag.Usage() os.Exit(1) } } var id int if *apiID != "" { var err error id, err = strconv.Atoi(*apiID) if err != nil { log.Fatalf("Invalid API ID: %v", err) } } // Interactive 2FA password prompt — only when Telegram is enabled password := os.Getenv("TELEGRAM_PASSWORD") if password == "" && needTelegram { hasSession := false if info, statErr := os.Stat(*sessionPath); statErr == nil && info.Size() > 0 { hasSession = true } if *loginOnly || !hasSession { fmt.Print("Telegram 2FA password (press Enter if none): ") pwBytes, err := term.ReadPassword(int(syscall.Stdin)) fmt.Println() if err == nil && len(pwBytes) > 0 { password = string(pwBytes) } } } cfg := server.Config{ ListenAddr: *listen, Domain: *domain, Passphrase: *key, ChannelsFile: *channelsFile, XAccountsFile: *xAccountsFile, XRSSInstances: *xRSSInstances, MaxPadding: *maxPadding, MsgLimit: *msgLimit, NoTelegram: *noTelegram, AllowManage: *allowManage, Debug: *debug, Telegram: server.TelegramConfig{ APIID: id, APIHash: *apiHash, Phone: *phone, Password: password, SessionPath: *sessionPath, LoginOnly: *loginOnly, CodePrompt: func(ctx context.Context) (string, error) { fmt.Print("Enter Telegram auth code: ") reader := bufio.NewReader(os.Stdin) code, err := reader.ReadString('\n') if err != nil { return "", err } return strings.TrimSpace(code), nil }, }, } srv, err := server.New(cfg) if err != nil { log.Fatalf("Create server: %v", err) } ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) defer cancel() log.Printf("Starting thefeed server %s on %s for domain %s", version.Version, cfg.ListenAddr, cfg.Domain) if err := srv.Run(ctx); err != nil && ctx.Err() == nil { log.Fatalf("Server error: %v", err) } log.Println("Server stopped") }