diff --git a/internal/runtime/once/once.go b/internal/runtime/once/once.go new file mode 100644 index 0000000..680a174 --- /dev/null +++ b/internal/runtime/once/once.go @@ -0,0 +1,48 @@ +package once + +import ( + "fmt" + + cmap "github.com/orcaman/concurrent-map" + log "github.com/sirupsen/logrus" +) + +var onceMap cmap.ConcurrentMap + +func init() { + onceMap = cmap.New() +} + +func New(objectKey string) { + onceMap.Set(objectKey, cmap.New()) +} + +// Once creates a map of keys k1 with a map of keys k2. +// The idea is that an object with ID k1 can create a list of users k2 +// that have already interacted with the object. If the user k2 is in the list, +// the object is not allowed to accessed again. +func Once(k1, k2 string) error { + i, ok := onceMap.Get(k1) + if ok { + return setOrReturn(i.(cmap.ConcurrentMap), k2) + } + userMap := cmap.New() + onceMap.Set(k1, userMap) + return setOrReturn(userMap, k2) +} + +// setOrReturn sets the key k2 in the map i if it is not already set. +func setOrReturn(objectMap cmap.ConcurrentMap, k2 string) error { + if _, ok := objectMap.Get(k2); ok { + return fmt.Errorf("%s already consumed object", k2) + } + objectMap.Set(k2, true) + return nil +} + +// Remove removes the key k1 from the map. Should be called after Once was called and +// the object k1 finished. +func Remove(k1 string) { + onceMap.Remove(k1) + log.Tracef("Removed key %s from onceMap (len=%d)", k1, len(onceMap.Keys())) +} diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 67b7d48..d36279e 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -549,6 +549,7 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ + bot.singletonCallbackInterceptor, bot.localizerInterceptor, bot.loadUserInterceptor, bot.lockInterceptor, diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 307e1ae..49d8fec 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -8,6 +8,7 @@ import ( "time" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/once" "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/eko/gocache/store" @@ -366,6 +367,7 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor inlineFaucet.Canceled = true runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) log.Debugf("[faucet] Faucet %s canceled.", inlineFaucet.ID) + once.Remove(inlineFaucet.ID) } return } @@ -378,6 +380,7 @@ func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFauce bot.tryEditMessage(c.Message, inlineFaucet.Message, &tb.ReplyMarkup{}) inlineFaucet.Active = false log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) + once.Remove(inlineFaucet.ID) } func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 51a7748..750a1d7 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -3,10 +3,12 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "reflect" "strconv" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/once" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" @@ -33,6 +35,17 @@ type Interceptor struct { OnDefer []intercept.Func } +// singletonClickInterceptor uses the onceMap to determine whether the object k1 already interacted +// with the user k2. If so, it will return an error. +func (bot TipBot) singletonCallbackInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + switch i.(type) { + case *tb.Callback: + c := i.(*tb.Callback) + return ctx, once.Once(c.Data, strconv.FormatInt(c.Sender.ID, 10)) + } + return ctx, invalidTypeError +} + // unlockInterceptor invoked as onDefer interceptor func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { user := getTelegramUserFromInterface(i)