2025-02-23 09:56:01 -04:00

253 lines
7.4 KiB
Go

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
}
log.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)
}
}
}