feat: initial commit

This commit is contained in:
2026-04-16 19:39:02 +02:00
commit 50d1686b1e
44 changed files with 2089 additions and 0 deletions

125
cmd/bot/main.go Normal file
View File

@@ -0,0 +1,125 @@
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_<INTENT>=<url>)
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, "")
// Use cases
textUC := usecase.NewHandleTextMessage(intentRouter, dispatcher, sessionStore, gateway, logger)
voiceUC := usecase.NewHandleVoiceMessage(downloader, converter, transcriber, 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))
}