mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-19 10:24:35 +03:00
188 lines
5.8 KiB
Go
188 lines
5.8 KiB
Go
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., https://nitter.net,http://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")
|
|
}
|