package main import ( "context" "errors" "fmt" "net/http" "os" "os/signal" "strings" "time" "fergalla.com/dockerbot/internal/auth" "fergalla.com/dockerbot/internal/cache" "fergalla.com/dockerbot/internal/config" "fergalla.com/dockerbot/internal/docker" "fergalla.com/dockerbot/internal/i18n" "fergalla.com/dockerbot/internal/logger" "fergalla.com/dockerbot/internal/ui" "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" ) type App struct { logger *logger.Logger store *auth.Store i18n *i18n.I18n dockerInfo *docker.DockerInfo renderer *ui.Renderer config *config.Config containerCache *cache.ContainerCache } func main() { log := logger.New("DEBUG", "Bot Telegram") cfg := config.Load() i, err := i18n.New("./conf/locales", "en") if err != nil { log.Error( "Failed to load locale file", "module", "Bot", "error", err) os.Exit(1) } i.SetLocale(cfg.Locale) store, err := auth.New("./conf/users.json") if err != nil { log.Error( "Failed to load users file", "module", "Auth", "error", err, ) } d, err := docker.New() if err != nil { log.Error( "Failed to create client docker", "module", "Bot", "error", err) os.Exit(1) } ttl := time.Duration(cfg.CacheTTL) * time.Second containerCache := cache.NewContainerCache(ttl, func() ([]docker.ContainerBasicInfo, error) { return d.ListContainers() }) appCtx := App{ logger: log, store: store, i18n: i, dockerInfo: d, config: cfg, containerCache: containerCache, } renderer := &ui.Renderer{ T: appCtx.T, } appCtx.renderer = renderer ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) defer cancel() if appCtx.config.Token == "" { appCtx.logger.Error( "Bot token not found", "module", "Bot", ) os.Exit(1) } opts := []bot.Option{ bot.WithMiddlewares( appCtx.AuthMiddleware(), appCtx.RecoveryMiddleware(), appCtx.LoggingMiddleware(), ), bot.WithDefaultHandler(appCtx.defaultHandler), } b, err := bot.New(appCtx.config.Token, opts...) if err != nil { appCtx.logger.Error( "Bot created failed", "module", "Bot", "error", err, ) os.Exit(1) } if err := appCtx.setConfigBot(ctx, b); err != nil { appCtx.logger.Error("Bot setup failed", "error", err) } b.RegisterHandler(bot.HandlerTypeMessageText, "/start", bot.MatchTypeExact, appCtx.startHandler) b.RegisterHandler(bot.HandlerTypeMessageText, "/help", bot.MatchTypeExact, appCtx.helpHandler) b.RegisterHandler(bot.HandlerTypeMessageText, "/docker", bot.MatchTypePrefix, appCtx.dockerHandler) b.RegisterHandler(bot.HandlerTypeCallbackQueryData, "dockerList_page", bot.MatchTypePrefix, appCtx.callbackDockerPagination) b.RegisterHandler(bot.HandlerTypeCallbackQueryData, "dockerList_refresh", bot.MatchTypePrefix, appCtx.callbackDockerListRefresh) b.RegisterHandler(bot.HandlerTypeCallbackQueryData, "dockerList_infoDocker", bot.MatchTypePrefix, appCtx.callbackDockerInfo) b.RegisterHandler(bot.HandlerTypeCallbackQueryData, "docker_action", bot.MatchTypePrefix, appCtx.callbackHandlerContainer) b.RegisterHandler(bot.HandlerTypeCallbackQueryData, "docker_back_list", bot.MatchTypePrefix, appCtx.callbackDockerBack) b.RegisterHandler(bot.HandlerTypeCallbackQueryData, "logs_back_detail", bot.MatchTypePrefix, appCtx.callbackDockerBackDetail) b.RegisterHandler(bot.HandlerTypeCallbackQueryData, "logs_refresh", bot.MatchTypePrefix, appCtx.callbackDockerLogsRefresh) b.RegisterHandler(bot.HandlerTypeCallbackQueryData, "logs", bot.MatchTypePrefix, appCtx.callbackDockerLogs) if appCtx.config.Webhookurl != "" { _, err := b.SetWebhook(ctx, &bot.SetWebhookParams{ URL: appCtx.config.Webhookurl, SecretToken: appCtx.config.Webhooktoken, }) if err != nil { appCtx.logger.Error( "Failed to set webhook", "module", "Bot", "url", appCtx.config.Webhookurl, "error", err, ) os.Exit(1) } go b.StartWebhook(ctx) appCtx.logger.Info( "Starting app", "module", "Bot", "mode", "webhook", "url", appCtx.config.Webhookurl, ) err = http.ListenAndServe(":8080", b.WebhookHandler()) if err != nil { appCtx.logger.Error( "HTTP server failed", "module", "Bot", "error", err, ) os.Exit(1) } } else { b.DeleteWebhook(ctx, &bot.DeleteWebhookParams{DropPendingUpdates: true}) appCtx.logger.Info( "Starting app", "module", "Bot", "mode", "polling", ) b.Start(ctx) } } func (a *App) T(key string, vars ...map[string]string) string { var values map[string]string if len(vars) > 0 { values = vars[0] } else { values = nil } return a.i18n.T(key, values) } func (appCtx *App) setConfigBot(ctx context.Context, b *bot.Bot) error { var errs []error commands := []models.BotCommand{ { Command: "start", Description: appCtx.T("i.cmd.start"), }, { Command: "docker", Description: appCtx.T("i.cmd.docker.detailed"), }, { Command: "help", Description: appCtx.T("i.cmd.help"), }, } if _, err := b.SetMyCommands(ctx, &bot.SetMyCommandsParams{ Commands: commands, }); err != nil { errs = append(errs, fmt.Errorf("set commands: %w", err)) } /* if _, err := b.SetMyName(ctx, &bot.SetMyNameParams{ Name: "DockerBot 🤖", }); err != nil { errs = append(errs, fmt.Errorf("set name: %w", err)) } if _, err := b.SetMyShortDescription(ctx, &bot.SetMyShortDescriptionParams{ ShortDescription: appCtx.T("app.short"), }); err != nil { errs = append(errs, fmt.Errorf("set short description: %w", err)) } if _, err := b.SetMyDescription(ctx, &bot.SetMyDescriptionParams{ Description: appCtx.T("app.about"), }); err != nil { errs = append(errs, fmt.Errorf("set description: %w", err)) } if err := setProfileImage(ctx, b); err != nil { errs = append(errs, fmt.Errorf("set profile image: %w", err)) } */ if len(errs) > 0 { return errors.Join(errs...) } return nil } func (appCtx *App) defaultHandler(ctx context.Context, b *bot.Bot, update *models.Update) { chatID := update.Message.Chat.ID appCtx.sendMessage(ctx, b, chatID, appCtx.T("e.unknown_command"), false, nil) } func (appCtx *App) startHandler(ctx context.Context, b *bot.Bot, update *models.Update) { appCtx.sendMessage(ctx, b, update.Message.Chat.ID, appCtx.T("app.welcome", map[string]string{"name": update.Message.From.FirstName}), true, nil) } func (appCtx *App) helpHandler(ctx context.Context, b *bot.Bot, update *models.Update) { appCtx.sendMessage(ctx, b, update.Message.Chat.ID, appCtx.T("app.help", nil), true, nil) } func (appCtx *App) dockerHandler(ctx context.Context, b *bot.Bot, update *models.Update) { chatID := update.Message.Chat.ID args := strings.Fields(strings.TrimSpace(update.Message.Text[len("/docker"):])) if len(args) == 0 || args[0] == "list" { appCtx.handleDockerList(ctx, b, chatID) return } subcommand := strings.ToLower(args[0]) params := args[1:] appCtx.handleDockerSubcommand(ctx, b, chatID, subcommand, params) } func (appCtx *App) handleDockerSubcommand(ctx context.Context, b *bot.Bot, chatID int64, subcommand string, params []string) { switch subcommand { case "info": appCtx.handleDockerInfo(ctx, b, chatID, params) case "start", "stop", "pause", "unpause", "restart": appCtx.handleDockerAction(ctx, b, chatID, subcommand, params) case "logs": appCtx.handleDockerLogs(ctx, b, chatID, params) case "help": appCtx.sendDockerHelp(ctx, b, chatID) default: appCtx.sendMessage(ctx, b, chatID, appCtx.T("e.unknown_command"), true, nil) } } func (appCtx *App) sendDockerHelp(ctx context.Context, b *bot.Bot, chatID int64) { appCtx.sendMessage(ctx, b, chatID, appCtx.T("app.help", nil), true, nil) } func (appCtx *App) sendMessage(ctx context.Context, b *bot.Bot, chatID int64, text string, parseHTML bool, keyboard *models.InlineKeyboardMarkup) { params := &bot.SendMessageParams{ ChatID: chatID, Text: text, } if parseHTML { params.ParseMode = "html" params.LinkPreviewOptions = &models.LinkPreviewOptions{IsDisabled: bot.True()} } else { params.ParseMode = "markdown" } if keyboard != nil { params.ReplyMarkup = keyboard } _, err := b.SendMessage(ctx, params) if err != nil { appCtx.logger.Error( "Failed to send message", "module", "Bot", "chat_id", chatID, "error", err, ) return } }