package main import ( "context" "log/slog" "os" "os/signal" "regexp" "syscall" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/redis/go-redis/v9" "github.com/paramah/gw_telegram/internal/application/usecase" "github.com/paramah/gw_telegram/internal/config" "github.com/paramah/gw_telegram/internal/domain/entity" "github.com/paramah/gw_telegram/internal/infrastructure/n8n" "github.com/paramah/gw_telegram/internal/infrastructure/router" "github.com/paramah/gw_telegram/internal/infrastructure/speech" "github.com/paramah/gw_telegram/internal/infrastructure/storage" infratelegram "github.com/paramah/gw_telegram/internal/infrastructure/telegram" "github.com/paramah/gw_telegram/internal/interfaces" ) func main() { cfg := config.MustLoad() logger := newLogger(cfg.Log) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() // Telegram bot bot, err := tgbotapi.NewBotAPI(cfg.Bot.Token) if err != nil { logger.Error("failed to create telegram bot", "error", err) os.Exit(1) } bot.Debug = cfg.Bot.Debug logger.Info("telegram bot authorized", "username", bot.Self.UserName) // Redis redisOpts, err := redis.ParseURL(cfg.Redis.URL) if err != nil { logger.Error("invalid redis url", "error", err) os.Exit(1) } redisClient := redis.NewClient(redisOpts) // Infrastructure gateway := infratelegram.NewBotGateway(bot, logger) downloader := infratelegram.NewTelegramFileDownloader(bot) sessionStore := storage.NewRedisSessionStore(redisClient, cfg.Redis.TTLHours) // n8n workflows (configure via env: N8N_WORKFLOW_=) workflows := map[string]n8n.WorkflowConfig{ "default": { ID: "default", WebhookURL: cfg.N8n.BaseURL + "/webhook/default", AuthToken: cfg.N8n.AuthToken, }, } dispatcher := n8n.NewWebhookDispatcher(workflows, logger) // Intent router with default rules rules := []router.Rule{ { Pattern: regexp.MustCompile(`(?i)^/start`), IntentName: "start", Target: entity.RouteTarget{Type: entity.RouteTargetBuiltin, WorkflowID: "start"}, Priority: 100, }, { Pattern: regexp.MustCompile(`(?i)^/help`), IntentName: "help", Target: entity.RouteTarget{Type: entity.RouteTargetBuiltin, WorkflowID: "help"}, Priority: 100, }, { Pattern: regexp.MustCompile(`.*`), IntentName: "general_query", Target: entity.RouteTarget{Type: entity.RouteTargetN8n, WorkflowID: "default"}, Priority: 0, }, } intentRouter := router.NewRuleBasedRouter(rules) // Speech transcriber := speech.NewOpenAIWhisper(cfg.Speech.OpenAIKey, cfg.Speech.WhisperModel, cfg.Speech.Language) converter := speech.NewFFmpegConverter(cfg.Speech.FFmpegPath, "") voiceFileStore, err := speech.NewLocalVoiceFileStore(cfg.Speech.VoiceStorePath) if err != nil { logger.Error("failed to initialise voice file store", "error", err) os.Exit(1) } // Use cases textUC := usecase.NewHandleTextMessage(intentRouter, dispatcher, sessionStore, gateway, logger) voiceUC := usecase.NewHandleVoiceMessage(downloader, converter, transcriber, voiceFileStore, textUC, gateway, logger) // Handler + poller handler := interfaces.NewTelegramHandler(textUC, voiceUC, logger) poller := infratelegram.NewUpdatePoller(bot, handler, logger) logger.Info("starting bot", "mode", cfg.Bot.Mode) if err := poller.Start(ctx); err != nil && err != context.Canceled { logger.Error("poller stopped with error", "error", err) os.Exit(1) } logger.Info("bot stopped gracefully") } func newLogger(cfg config.LogConfig) *slog.Logger { var level slog.Level switch cfg.Level { case "debug": level = slog.LevelDebug case "warn": level = slog.LevelWarn case "error": level = slog.LevelError default: level = slog.LevelInfo } opts := &slog.HandlerOptions{Level: level} if cfg.Format == "text" { return slog.New(slog.NewTextHandler(os.Stdout, opts)) } return slog.New(slog.NewJSONHandler(os.Stdout, opts)) }