package middlewares import ( "context" "fmt" "log/slog" "os" "sync" "time" "git.maximotejeda.com/maximo/telegram-base-bot/config" "git.maximotejeda.com/maximo/telegram-base-bot/internal/application/helpers" "git.maximotejeda.com/maximo/telegram-base-bot/internal/ports" "github.com/go-telegram/bot" "github.com/go-telegram/bot/models" ) var log = slog.New(slog.NewJSONHandler(os.Stderr, nil)) func ShowMessageWithUserID(next bot.HandlerFunc) bot.HandlerFunc { return func(ctx context.Context, bot *bot.Bot, update *models.Update) { if update.Message != nil { log.Info("User new message", fmt.Sprintf("%d", update.Message.From.ID), update.Message.Text) } next(ctx, bot, update) } } // singleFlight is a middleware that ensures that only one callback query is processed at a time. // example from https://github.com/go-telegram/bot/blob/main/examples/middleware/main.go func SingleFlight(next bot.HandlerFunc) bot.HandlerFunc { sf := sync.Map{} return func(ctx context.Context, b *bot.Bot, update *models.Update) { if update.CallbackQuery != nil { key := update.CallbackQuery.Message.Message.ID if _, loaded := sf.LoadOrStore(key, struct{}{}); loaded { b.SendMessage(ctx, &bot.SendMessageParams{ChatID: update.CallbackQuery.From.ID, Text: "Query on flight, please wait"}) return } defer sf.Delete(key) next(ctx, b, update) } } } func LogMessage(next bot.HandlerFunc) bot.HandlerFunc { return func(ctx context.Context, b *bot.Bot, update *models.Update) { txt := "" user := &models.User{} if update.CallbackQuery == nil { txt = update.Message.Text user = update.Message.From } else { txt = fmt.Sprintf("%#v", update.CallbackQuery) user = &update.CallbackQuery.From } log.Info(txt, "user", user.Username) next(ctx, b, update) } } func NotifyAdmin(next bot.HandlerFunc) bot.HandlerFunc { return func(ctx context.Context, b *bot.Bot, update *models.Update) { next(ctx, b, update) } } // SetAuthRequired // set authorization middleware // authorization will be set to an amount of time in minutes func SetAuthRequired(svc ports.UserService, log *slog.Logger) func(bot.HandlerFunc) bot.HandlerFunc { userLastAuth := sync.Map{} return func(next bot.HandlerFunc) bot.HandlerFunc { return func(ctx context.Context, b *bot.Bot, update *models.Update) { var key int64 if update.CallbackQuery == nil { if update.MessageReaction == nil { key = update.Message.From.ID } } else { key = update.CallbackQuery.From.ID } slog.Debug("executing auth func","user", key) authMe := func() { k := helpers.Authenticate(ctx, log, b, update, svc) if !k { log.Info("user not Authenticated", "user", key) return } log.Debug("storing user last auth to map") userLastAuth.Store(key, time.Now()) log.Info("user Authenticated", "user", key, "time", time.Now()) next(ctx, b, update) } if _, loaded := userLastAuth.LoadOrStore(key, time.Now()); loaded { when, _ := userLastAuth.Load(key) switch { case time.Since(when.(time.Time)).Minutes() < 3: // the time user will remain auth on the bot log.Info("user on cache available", "user", key, "time", when) next(ctx, b, update) return default: log.Debug("user last auth is more than x min, auth again") authMe() } } else { log.Debug("user not auth racently, authenticating") authMe() } } } } func RateLimitUser(next bot.HandlerFunc) bot.HandlerFunc { rl := sync.Map{} type data struct { when time.Time amount int64 } return func(ctx context.Context, b *bot.Bot, update *models.Update) { var ( key int64 rLimitTime = config.GetRateLimitSec() rLimitAmnt = config.GetRateLimitAmount() ) if update.CallbackQuery == nil { key = update.Message.From.ID } else { key = update.CallbackQuery.From.ID } log.Info("got key ", "key ", key) if _, loaded := rl.LoadOrStore(key, data{when: time.Now(), amount: 0}); loaded { log.Info("user loaded on map") dt, _ := rl.Load(key) dtl := dt.(data) dtl.amount++ amnt := dtl.amount rl.Store(key, data{when: dtl.when, amount: amnt}) switch time.Since(dtl.when).Seconds() < rLimitTime { case true: switch dtl.amount > rLimitAmnt { case true: log.Info("user rl execeed", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount) rl.Store(key, data{when: time.Now(), amount: amnt}) b.SendMessage(ctx, &bot.SendMessageParams{ChatID: key, Text: "Rate Limit Exeded"}) return case false: log.Info("user rl not execeed", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount) } case false: rl.Store(key, data{when: time.Now(), amount: 1}) log.Info("user time", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount) } } else { rl.Store(key, data{when: time.Now(), amount: 1}) log.Info("user not loaded", "user", key) } next(ctx, b, update) } } // CreateRateLimitUser // Create an specific rate limiting with distincts values of time and hits func CreateRateLimitUser(ctx context.Context, log *slog.Logger, delay float64, hits int64) func(bot.HandlerFunc) bot.HandlerFunc { return func(next bot.HandlerFunc) bot.HandlerFunc { rl := sync.Map{} type data struct { when time.Time amount int64 } return func(ctx context.Context, b *bot.Bot, update *models.Update) { var ( key int64 rLimitTime = delay // time in secs to check rLimitAmnt = hits // number of hits ) if update.CallbackQuery == nil { key = update.Message.From.ID } else { key = update.CallbackQuery.From.ID } log.Info("got key ", "key ", key) if _, loaded := rl.LoadOrStore(key, data{when: time.Now(), amount: 0}); loaded { log.Info("user loaded on map") dt, _ := rl.Load(key) dtl := dt.(data) dtl.amount++ amnt := dtl.amount rl.Store(key, data{when: dtl.when, amount: amnt}) switch time.Since(dtl.when).Seconds() < rLimitTime { case true: switch dtl.amount >= rLimitAmnt { case true: log.Info("user rl execeed", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount) rl.Store(key, data{when: time.Now(), amount: amnt}) b.SendMessage(ctx, &bot.SendMessageParams{ChatID: key, Text: "Rate Limit Exeded"}) return case false: log.Info("user rl not execeed", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount) } case false: rl.Store(key, data{when: time.Now(), amount: 1}) log.Info("user time", "since", time.Since(dtl.when).Seconds(), "amount", dtl.amount) } } else { log.Info("user not loaded", "user", key) } next(ctx, b, update) } } } func CreateLogMiddleWare(ctx context.Context, log *slog.Logger) func(bot.HandlerFunc) bot.HandlerFunc{ return func(next bot.HandlerFunc)bot.HandlerFunc{ return func(ctx context.Context, b *bot.Bot, update *models.Update) { start := time.Now() txt := "" user := &models.User{} if update.CallbackQuery == nil { if update.MessageReaction == nil { txt = update.Message.Text user = update.Message.From }else { user = update.MessageReaction.User txt = "reaction" log.Info("reaction", "react", update.MessageReaction) } } else { txt = fmt.Sprintf("%#v", update.CallbackQuery) user = &update.CallbackQuery.From } log.Info(txt, "user", user.Username) log.Debug("reponse", "user", user.Username, "id", user.ID, "elapsed ms", time.Since(start).Milliseconds()) next(ctx, b, update) } } }