Files
DockerBot/main.go
2026-04-13 21:42:04 +02:00

304 lines
8.3 KiB
Go

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
}
}