Files
2026-05-27 09:55:11 +09:00

175 lines
4.3 KiB
Go

package main
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"log/slog"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
maxbot "github.com/max-messenger/max-bot-api-client-go"
)
func mustEnv(key string) string {
v := os.Getenv(key)
if v == "" {
fmt.Fprintf(os.Stderr, "Environment variable %s is not set\n", key)
os.Exit(1)
}
return v
}
func envOr(key, fallback string) string {
if v := os.Getenv(key); v != "" {
return v
}
return fallback
}
func genKey() string {
b := make([]byte, 8)
rand.Read(b)
return hex.EncodeToString(b)
}
func logLevel() slog.Level {
switch strings.ToLower(os.Getenv("LOG_LEVEL")) {
case "debug":
return slog.LevelDebug
case "warn":
return slog.LevelWarn
case "error":
return slog.LevelError
default:
return slog.LevelInfo
}
}
func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: logLevel()})))
cfg := Config{
MaxToken: mustEnv("MAX_TOKEN"),
TgBotURL: envOr("TG_BOT_URL", "https://t.me/MaxTelegramBridgeBot"),
MaxBotURL: envOr("MAX_BOT_URL", "https://max.ru/id710708943262_bot"),
WebhookURL: os.Getenv("WEBHOOK_URL"),
WebhookPort: envOr("WEBHOOK_PORT", "8443"),
TgAPIURL: os.Getenv("TG_API_URL"),
}
// Parse ALLOWED_USERS whitelist
if v := os.Getenv("ALLOWED_USERS"); v != "" {
for _, s := range strings.Split(v, ",") {
s = strings.TrimSpace(s)
if s == "" {
continue
}
id, err := strconv.ParseInt(s, 10, 64)
if err != nil {
slog.Error("Invalid ALLOWED_USERS value", "value", s, "err", err)
os.Exit(1)
}
cfg.AllowedUsers = append(cfg.AllowedUsers, id)
}
slog.Info("User whitelist enabled", "count", len(cfg.AllowedUsers))
}
// Parse file size limits
if v := os.Getenv("TG_MAX_FILE_SIZE_MB"); v != "" {
if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n >= 0 {
cfg.TgMaxFileSizeMB = n
} else {
slog.Error("Invalid TG_MAX_FILE_SIZE_MB value", "value", v)
os.Exit(1)
}
}
if v := os.Getenv("MAX_MAX_FILE_SIZE_MB"); v != "" {
if n, err := strconv.Atoi(strings.TrimSpace(v)); err == nil && n >= 0 {
cfg.MaxMaxFileSizeMB = n
} else {
slog.Error("Invalid MAX_MAX_FILE_SIZE_MB value", "value", v)
os.Exit(1)
}
}
// Parse MAX_ALLOWED_EXTENSIONS whitelist (e.g. "pdf,docx,zip")
// Если не задан — расширения не проверяются локально (ошибка придёт от CDN).
if v := os.Getenv("MAX_ALLOWED_EXTENSIONS"); v != "" {
cfg.MaxAllowedExts = make(map[string]struct{})
for _, ext := range strings.Split(v, ",") {
ext = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(ext, ".")))
if ext != "" {
cfg.MaxAllowedExts[ext] = struct{}{}
}
}
slog.Info("MAX file extension whitelist enabled", "count", len(cfg.MaxAllowedExts))
}
// MESSAGE_FORMAT=newline — текст с новой строки после имени: "Имя:\nтекст"
// По умолчанию (или MESSAGE_FORMAT=inline) — "Имя: текст"
if strings.ToLower(os.Getenv("MESSAGE_FORMAT")) == "newline" {
cfg.MessageNewline = true
slog.Info("Message format: newline")
}
dbPath := envOr("DB_PATH", "bridge.db")
var repo Repository
var err error
if dsn := os.Getenv("DATABASE_URL"); dsn != "" {
repo, err = NewPostgresRepo(dsn)
if err != nil {
slog.Error("PostgreSQL error", "err", err)
os.Exit(1)
}
slog.Info("DB: PostgreSQL")
} else {
repo, err = NewSQLiteRepo(dbPath)
if err != nil {
slog.Error("SQLite error", "err", err)
os.Exit(1)
}
slog.Info("DB: SQLite", "path", dbPath)
}
defer repo.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
tgToken := mustEnv("TG_TOKEN")
tg, err := NewTGBotSender(ctx, tgToken, cfg.TgAPIURL)
if err != nil {
slog.Error("TG bot error", "err", err)
os.Exit(1)
}
maxApi, err := maxbot.New(cfg.MaxToken)
if err != nil {
slog.Error("MAX bot error", "err", err)
os.Exit(1)
}
maxInfo, err := maxApi.Bots.GetBot(context.Background())
if err != nil {
slog.Error("MAX bot info error", "err", err)
os.Exit(1)
}
slog.Info("MAX bot started", "name", maxInfo.Name)
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigCh
slog.Info("Shutting down...")
cancel()
}()
bridge := NewBridge(cfg, repo, tg, maxApi)
bridge.Run(ctx)
slog.Info("Bridge stopped")
}