mirror of
https://github.com/sartoopjj/thefeed.git
synced 2026-05-18 07:44:45 +03:00
277 lines
9.4 KiB
Go
277 lines
9.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/signal"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"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")
|
|
fetchIntervalMin := flag.Int("fetch-interval", 10, "Fetch cycle interval in minutes (min 3, default 10)")
|
|
allowManage := flag.Bool("allow-manage", false, "Allow remote channel management and sending via DNS")
|
|
debug := flag.Bool("debug", false, "Log every decoded DNS query")
|
|
dnsMediaEnabled := flag.Bool("dns-media-enabled", false, "Serve media via DNS (slow relay)")
|
|
dnsMediaMaxSizeKB := flag.Int("dns-media-max-size", 100, "Per-file cap for the DNS relay in KB (0 = no cap)")
|
|
dnsMediaCacheTTLMin := flag.Int("dns-media-cache-ttl", 600, "TTL for DNS-relay cached media, in minutes")
|
|
dnsMediaCompression := flag.String("dns-media-compression", "gzip", "Compression for DNS-relay media bytes: none|gzip|deflate")
|
|
ghEnabled := flag.Bool("github-relay-enabled", false, "Serve media via GitHub (fast relay)")
|
|
ghToken := flag.String("github-relay-token", "", "GitHub PAT with contents:write on the relay repo")
|
|
ghRepo := flag.String("github-relay-repo", "", "GitHub repo for the fast relay, e.g. owner/repo")
|
|
ghBranch := flag.String("github-relay-branch", "main", "Default branch to commit to (e.g. main, master)")
|
|
ghMaxSizeKB := flag.Int("github-relay-max-size", 15*1024, "Per-file cap for the GitHub relay in KB (0 = no cap)")
|
|
ghCacheTTLMin := flag.Int("github-relay-ttl", 600, "TTL for GitHub-relay objects in minutes")
|
|
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)
|
|
}
|
|
|
|
// Catch the common --bool true mistake: Go's flag package stops parsing at
|
|
// the first positional, so any flags after it are silently dropped. Bool
|
|
// flags must use --foo or --foo=true (no space).
|
|
if flag.NArg() > 0 {
|
|
fmt.Fprintf(os.Stderr, "Error: unexpected positional argument(s): %v\n", flag.Args())
|
|
fmt.Fprintln(os.Stderr, "Hint: bool flags must be written as --flag or --flag=true (NOT --flag true).")
|
|
os.Exit(1)
|
|
}
|
|
|
|
// 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 *fetchIntervalMin == 10 {
|
|
if v := os.Getenv("THEFEED_FETCH_INTERVAL"); v != "" {
|
|
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
|
*fetchIntervalMin = n
|
|
}
|
|
}
|
|
}
|
|
if *fetchIntervalMin < 3 {
|
|
fmt.Fprintf(os.Stderr, "Error: --fetch-interval must be at least 3 minutes (got %d)\n", *fetchIntervalMin)
|
|
os.Exit(1)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
if env := os.Getenv("THEFEED_DNS_MEDIA_ENABLED"); env == "0" {
|
|
*dnsMediaEnabled = false
|
|
} else if env == "1" {
|
|
*dnsMediaEnabled = true
|
|
}
|
|
if env := os.Getenv("THEFEED_DNS_MEDIA_MAX_SIZE_KB"); env != "" {
|
|
if n, err := strconv.Atoi(env); err == nil {
|
|
*dnsMediaMaxSizeKB = n
|
|
}
|
|
}
|
|
if env := os.Getenv("THEFEED_DNS_MEDIA_CACHE_TTL_MIN"); env != "" {
|
|
if n, err := strconv.Atoi(env); err == nil {
|
|
*dnsMediaCacheTTLMin = n
|
|
}
|
|
}
|
|
if env := os.Getenv("THEFEED_DNS_MEDIA_COMPRESSION"); env != "" {
|
|
*dnsMediaCompression = env
|
|
}
|
|
if !*ghEnabled && os.Getenv("THEFEED_GITHUB_RELAY_ENABLED") == "1" {
|
|
*ghEnabled = true
|
|
}
|
|
if *ghToken == "" {
|
|
*ghToken = os.Getenv("THEFEED_GITHUB_RELAY_TOKEN")
|
|
}
|
|
if *ghRepo == "" {
|
|
*ghRepo = os.Getenv("THEFEED_GITHUB_RELAY_REPO")
|
|
}
|
|
if *ghBranch == "main" {
|
|
if v := os.Getenv("THEFEED_GITHUB_RELAY_BRANCH"); v != "" {
|
|
*ghBranch = v
|
|
}
|
|
}
|
|
if env := os.Getenv("THEFEED_GITHUB_RELAY_MAX_SIZE_KB"); env != "" {
|
|
if n, err := strconv.Atoi(env); err == nil {
|
|
*ghMaxSizeKB = n
|
|
}
|
|
}
|
|
if env := os.Getenv("THEFEED_GITHUB_RELAY_TTL_MIN"); env != "" {
|
|
if n, err := strconv.Atoi(env); err == nil {
|
|
*ghCacheTTLMin = n
|
|
}
|
|
}
|
|
|
|
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,
|
|
DNSMediaEnabled: *dnsMediaEnabled,
|
|
DNSMediaMaxSize: int64(*dnsMediaMaxSizeKB) * 1024,
|
|
DNSMediaCacheTTL: *dnsMediaCacheTTLMin,
|
|
DNSMediaCompression: *dnsMediaCompression,
|
|
FetchInterval: time.Duration(*fetchIntervalMin) * time.Minute,
|
|
GitHubRelay: server.GitHubRelayConfig{
|
|
Enabled: *ghEnabled,
|
|
Token: *ghToken,
|
|
Repo: *ghRepo,
|
|
Branch: *ghBranch,
|
|
StatePath: filepath.Join(*dataDir, "gh_relay_state.json"),
|
|
MaxBytes: int64(*ghMaxSizeKB) * 1024,
|
|
TTLMinutes: *ghCacheTTLMin,
|
|
},
|
|
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")
|
|
}
|