mirror of
https://github.com/ChuckNorrison/LightningTipBot.git
synced 2026-08-13 12:33:14 +02:00
Onlysats (#181)
* speed up tx lock * add shops * shop * scrolling * 🚀 add state stateCallbackMessage 🚀 * no shop * scroll * add shop browser * scroll bug * add buttons.go * scrolling * shop * lnurl print after serving * shop deletion * adding and removing shops * display photo captions * refactor * shops * shops * files work * rename shop * error handling * fixes * payments * confirm delete all shops * adding new items fix * own bunt db for shop * unlock transaction * remove double print * add transaction.Lock(id) * unlock getShop * add default interceptors to shop * more files * go sleep * add fileStateResetTicker * async sleep * add runtime.ResettableFunctionTicker * remove default duration * generalized concurrent mutex * simplify function ticker * status message ticker queue * add start index to shopViewDeleteAllStatusMsgs Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> Co-authored-by: lngohumble <lngohumble@gmail.com>
This commit is contained in:
parent
6b5e30dbe4
commit
f8fadb1729
20 changed files with 2335 additions and 87 deletions
1
go.mod
1
go.mod
|
|
@ -12,6 +12,7 @@ require (
|
|||
github.com/jinzhu/configor v1.2.1
|
||||
github.com/makiuchi-d/gozxing v0.0.2
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.2
|
||||
github.com/orcaman/concurrent-map v1.0.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/sethvargo/go-limiter v0.7.2
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -397,6 +397,8 @@ github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxS
|
|||
github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw=
|
||||
github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4=
|
||||
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
|
||||
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
|
|
@ -742,6 +744,8 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep
|
|||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs=
|
||||
gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk=
|
||||
gopkg.in/tucnak/telebot.v2 v2.4.1 h1:bUOFHtHhuhPekjHGe1Q1BmITvtBLdQI4yjSMC405KcU=
|
||||
gopkg.in/tucnak/telebot.v2 v2.4.1/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type TelegramConfiguration struct {
|
|||
}
|
||||
type DatabaseConfiguration struct {
|
||||
DbPath string `yaml:"db_path"`
|
||||
ShopBuntDbPath string `yaml:"shop_buntdb_path"`
|
||||
BuntDbPath string `yaml:"buntdb_path"`
|
||||
TransactionsPath string `yaml:"transactions_path"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,13 @@ const (
|
|||
UserHasEnteredAmount
|
||||
UserEnterUser
|
||||
UserHasEnteredUser
|
||||
UserEnterShopTitle
|
||||
UserStateShopItemSendPhoto
|
||||
UserStateShopItemSendTitle
|
||||
UserStateShopItemSendDescription
|
||||
UserStateShopItemSendPrice
|
||||
UserStateShopItemSendItemFile
|
||||
UserEnterShopsDescription
|
||||
)
|
||||
|
||||
type UserStateKey int
|
||||
|
|
|
|||
76
internal/runtime/function.go
Normal file
76
internal/runtime/function.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package runtime
|
||||
|
||||
import (
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
"time"
|
||||
)
|
||||
|
||||
var tickerMap cmap.ConcurrentMap
|
||||
|
||||
func init() {
|
||||
tickerMap = cmap.New()
|
||||
}
|
||||
|
||||
var defaultTickerCoolDown = time.Second * 10
|
||||
|
||||
// ResettableFunctionTicker will reset the user state as soon as tick is delivered.
|
||||
type ResettableFunctionTicker struct {
|
||||
Ticker *time.Ticker
|
||||
ResetChan chan struct{} // channel used to reset the ticker
|
||||
duration time.Duration
|
||||
Started bool
|
||||
name string
|
||||
}
|
||||
type ResettableFunctionTickerOption func(*ResettableFunctionTicker)
|
||||
|
||||
func WithDuration(d time.Duration) ResettableFunctionTickerOption {
|
||||
return func(a *ResettableFunctionTicker) {
|
||||
a.duration = d
|
||||
}
|
||||
}
|
||||
func RemoveTicker(name string) {
|
||||
tickerMap.Remove(name)
|
||||
}
|
||||
func GetTicker(name string, option ...ResettableFunctionTickerOption) *ResettableFunctionTicker {
|
||||
|
||||
if t, ok := tickerMap.Get(name); ok {
|
||||
return t.(*ResettableFunctionTicker)
|
||||
} else {
|
||||
t := NewResettableFunctionTicker(name, option...)
|
||||
tickerMap.Set(name, t)
|
||||
return t
|
||||
}
|
||||
}
|
||||
func NewResettableFunctionTicker(name string, option ...ResettableFunctionTickerOption) *ResettableFunctionTicker {
|
||||
t := &ResettableFunctionTicker{
|
||||
ResetChan: make(chan struct{}, 1),
|
||||
name: name,
|
||||
}
|
||||
|
||||
for _, opt := range option {
|
||||
opt(t)
|
||||
}
|
||||
if t.duration == 0 {
|
||||
t.duration = defaultTickerCoolDown
|
||||
}
|
||||
t.Ticker = time.NewTicker(t.duration)
|
||||
return t
|
||||
}
|
||||
|
||||
func (t *ResettableFunctionTicker) Do(f func()) {
|
||||
t.Started = true
|
||||
tickerMap.Set(t.name, t)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-t.Ticker.C:
|
||||
// ticker delivered signal. do function f
|
||||
f()
|
||||
return
|
||||
case <-t.ResetChan:
|
||||
// reset signal received. creating new ticker.
|
||||
t.Ticker = time.NewTicker(t.duration)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
31
internal/runtime/mutex.go
Normal file
31
internal/runtime/mutex.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
package runtime
|
||||
|
||||
import (
|
||||
cmap "github.com/orcaman/concurrent-map"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var mutexMap cmap.ConcurrentMap
|
||||
|
||||
func init() {
|
||||
mutexMap = cmap.New()
|
||||
}
|
||||
|
||||
func Lock(s string) {
|
||||
if m, ok := mutexMap.Get(s); ok {
|
||||
m.(*sync.Mutex).Lock()
|
||||
} else {
|
||||
m := &sync.Mutex{}
|
||||
m.Lock()
|
||||
mutexMap.Set(s, m)
|
||||
}
|
||||
log.Tracef("[Mutex] Lock %s", s)
|
||||
}
|
||||
|
||||
func Unlock(s string) {
|
||||
if m, ok := mutexMap.Get(s); ok {
|
||||
log.Tracef("[Mutex] Unlock %s", s)
|
||||
m.(*sync.Mutex).Unlock()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ package storage
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/runtime"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/buntdb"
|
||||
)
|
||||
|
|
@ -78,23 +78,31 @@ func (db *DB) Set(object Storable) error {
|
|||
}
|
||||
|
||||
// Delete a storable item.
|
||||
// todo -- not ascend users index
|
||||
func (db *DB) Delete(index string, object Storable) error {
|
||||
return db.Update(func(tx *buntdb.Tx) error {
|
||||
var delkeys []string
|
||||
runtime.IgnoreError(
|
||||
tx.Ascend(index, func(key, value string) bool {
|
||||
if key == object.Key() {
|
||||
delkeys = append(delkeys, key)
|
||||
}
|
||||
return true
|
||||
}),
|
||||
)
|
||||
for _, k := range delkeys {
|
||||
if _, err := tx.Delete(k); err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := tx.Get(object.Key())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Delete(object.Key()); err != nil {
|
||||
return err
|
||||
}
|
||||
// OLD: from gohumble:
|
||||
// todo -- not ascend users index
|
||||
// var delkeys []string
|
||||
// runtime.IgnoreError(
|
||||
// tx.Ascend(index, func(key, value string) bool {
|
||||
// if key == object.Key() {
|
||||
// delkeys = append(delkeys, key)
|
||||
// }
|
||||
// return true
|
||||
// }),
|
||||
// )
|
||||
// for _, k := range delkeys {
|
||||
// if _, err := tx.Delete(k); err != nil {
|
||||
// return err
|
||||
// }
|
||||
// }
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ package transaction
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/runtime"
|
||||
"time"
|
||||
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/storage"
|
||||
|
|
@ -17,14 +17,6 @@ type Base struct {
|
|||
UpdatedAt time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
transactionMutex = make(map[string]*sync.Mutex, 0)
|
||||
transactionMapMutex = &sync.Mutex{}
|
||||
}
|
||||
|
||||
var transactionMutex map[string]*sync.Mutex
|
||||
var transactionMapMutex *sync.Mutex
|
||||
|
||||
type Option func(b *Base)
|
||||
|
||||
func ID(id string) Option {
|
||||
|
|
@ -60,14 +52,12 @@ func (tx *Base) Lock(s storage.Storable, db *storage.DB) error {
|
|||
log.Debugf("[Lock] %s", tx.ID)
|
||||
return nil
|
||||
}
|
||||
func unlock(id string) {
|
||||
transactionMapMutex.Lock()
|
||||
if transactionMutex[id] != nil {
|
||||
transactionMutex[id].Unlock()
|
||||
log.Tracef("[TX mutex] Release %s", id)
|
||||
}
|
||||
transactionMapMutex.Unlock()
|
||||
func Unlock(id string) {
|
||||
runtime.Unlock(id)
|
||||
}
|
||||
|
||||
func Lock(id string) {
|
||||
runtime.Lock(id)
|
||||
}
|
||||
|
||||
func (tx *Base) Release(s storage.Storable, db *storage.DB) error {
|
||||
|
|
@ -79,7 +69,7 @@ func (tx *Base) Release(s storage.Storable, db *storage.DB) error {
|
|||
return err
|
||||
}
|
||||
log.Debugf("[Bunt Release] %s", tx.ID)
|
||||
unlock(tx.ID)
|
||||
Unlock(tx.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -95,17 +85,12 @@ func (tx *Base) Inactivate(s storage.Storable, db *storage.DB) error {
|
|||
}
|
||||
|
||||
func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error) {
|
||||
transactionMapMutex.Lock()
|
||||
if transactionMutex[tx.ID] == nil {
|
||||
transactionMutex[tx.ID] = &sync.Mutex{}
|
||||
}
|
||||
transactionMapMutex.Unlock()
|
||||
transactionMutex[tx.ID].Lock()
|
||||
Lock(tx.ID)
|
||||
log.Tracef("[TX mutex] Lock %s", tx.ID)
|
||||
|
||||
err := db.Get(s)
|
||||
if err != nil {
|
||||
unlock(tx.ID)
|
||||
Unlock(tx.ID)
|
||||
return s, err
|
||||
}
|
||||
// to avoid race conditions, we block the call if there is
|
||||
|
|
@ -114,7 +99,7 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error
|
|||
for tx.InTransaction {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
unlock(tx.ID)
|
||||
Unlock(tx.ID)
|
||||
return nil, fmt.Errorf("[Bunt Lock] transaction timeout %s", tx.ID)
|
||||
default:
|
||||
time.Sleep(time.Duration(75) * time.Millisecond)
|
||||
|
|
@ -122,7 +107,7 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error
|
|||
}
|
||||
}
|
||||
if err != nil {
|
||||
unlock(tx.ID)
|
||||
Unlock(tx.ID)
|
||||
return nil, fmt.Errorf("could not get transaction")
|
||||
}
|
||||
|
||||
|
|
@ -133,3 +118,8 @@ func (tx *Base) Set(s storage.Storable, db *storage.DB) error {
|
|||
tx.UpdatedAt = time.Now()
|
||||
return db.Set(s)
|
||||
}
|
||||
|
||||
func (tx *Base) Delete(s storage.Storable, db *storage.DB) error {
|
||||
tx.UpdatedAt = time.Now()
|
||||
return db.Delete(s.Key(), s)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import (
|
|||
"github.com/LightningTipBot/LightningTipBot/internal/storage"
|
||||
gocache "github.com/patrickmn/go-cache"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"gopkg.in/lightningtipbot/telebot.v2"
|
||||
tb "gopkg.in/lightningtipbot/telebot.v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
|
@ -22,8 +21,9 @@ import (
|
|||
type TipBot struct {
|
||||
Database *gorm.DB
|
||||
Bunt *storage.DB
|
||||
ShopBunt *storage.DB
|
||||
logger *gorm.DB
|
||||
Telegram *telebot.Bot
|
||||
Telegram *tb.Bot
|
||||
Client *lnbits.Client
|
||||
limiter map[string]limiter.Limiter
|
||||
Cache
|
||||
|
|
@ -48,7 +48,8 @@ func NewBot() TipBot {
|
|||
Database: db,
|
||||
Client: lnbits.NewClient(internal.Configuration.Lnbits.AdminKey, internal.Configuration.Lnbits.Url),
|
||||
logger: txLogger,
|
||||
Bunt: createBunt(),
|
||||
Bunt: createBunt(internal.Configuration.Database.BuntDbPath),
|
||||
ShopBunt: createBunt(internal.Configuration.Database.ShopBuntDbPath),
|
||||
Telegram: newTelegramBot(),
|
||||
Cache: Cache{GoCacheStore: gocacheStore},
|
||||
}
|
||||
|
|
@ -90,5 +91,6 @@ func (bot *TipBot) Start() {
|
|||
}
|
||||
bot.registerTelegramHandlers()
|
||||
initInvoiceEventCallbacks(bot)
|
||||
initializeStateCallbackMessage(bot)
|
||||
bot.Telegram.Start()
|
||||
}
|
||||
|
|
|
|||
24
internal/telegram/buttons.go
Normal file
24
internal/telegram/buttons.go
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
package telegram
|
||||
|
||||
import tb "gopkg.in/lightningtipbot/telebot.v2"
|
||||
|
||||
// buttonWrapper wrap buttons slice in rows of length i
|
||||
func buttonWrapper(buttons []tb.Btn, markup *tb.ReplyMarkup, length int) []tb.Row {
|
||||
buttonLength := len(buttons)
|
||||
rows := make([]tb.Row, 0)
|
||||
|
||||
if buttonLength > length {
|
||||
for i := 0; i < buttonLength; i = i + length {
|
||||
buttonRow := make([]tb.Btn, length)
|
||||
if i+length < buttonLength {
|
||||
buttonRow = buttons[i : i+length]
|
||||
} else {
|
||||
buttonRow = buttons[i:]
|
||||
}
|
||||
rows = append(rows, markup.Row(buttonRow...))
|
||||
}
|
||||
return rows
|
||||
}
|
||||
rows = append(rows, markup.Row(buttons...))
|
||||
return rows
|
||||
}
|
||||
|
|
@ -26,9 +26,9 @@ const (
|
|||
TipTooltipKeyPattern = "tip-tool-tip:*"
|
||||
)
|
||||
|
||||
func createBunt() *storage.DB {
|
||||
func createBunt(file string) *storage.DB {
|
||||
// create bunt database
|
||||
bunt := storage.NewBunt(internal.Configuration.Database.BuntDbPath)
|
||||
bunt := storage.NewBunt(file)
|
||||
// create bunt database index for ascending (searching) TipTooltips
|
||||
err := bunt.CreateIndex(MessageOrderedByReplyToFrom, TipTooltipKeyPattern, buntdb.IndexJSON(MessageOrderedByReplyToFrom))
|
||||
if err != nil {
|
||||
|
|
|
|||
32
internal/telegram/files.go
Normal file
32
internal/telegram/files.go
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/runtime"
|
||||
tb "gopkg.in/lightningtipbot/telebot.v2"
|
||||
)
|
||||
|
||||
func (bot *TipBot) fileHandler(ctx context.Context, m *tb.Message) {
|
||||
if m.Chat.Type != tb.ChatPrivate {
|
||||
return
|
||||
}
|
||||
user := LoadUser(ctx)
|
||||
if c := stateCallbackMessage[user.StateKey]; c != nil {
|
||||
// found handler for this state
|
||||
// now looking for user state reset ticker
|
||||
ticker := runtime.GetTicker(user.ID)
|
||||
if !ticker.Started {
|
||||
ticker.Do(func() {
|
||||
ResetUserState(user, bot)
|
||||
// removing ticker asap done
|
||||
bot.shopViewDeleteAllStatusMsgs(ctx, user, 0)
|
||||
runtime.RemoveTicker(user.ID)
|
||||
})
|
||||
} else {
|
||||
ticker.ResetChan <- struct{}{}
|
||||
}
|
||||
|
||||
c(ctx, m)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -168,6 +168,37 @@ func (bot TipBot) getHandler() []Handler {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{"/shops"},
|
||||
Handler: bot.shopsHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: MessageInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.logMessageInterceptor,
|
||||
bot.requireUserInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{"/shop"},
|
||||
Handler: bot.shopHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: MessageInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.logMessageInterceptor,
|
||||
bot.requireUserInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{"/balance"},
|
||||
Handler: bot.balanceHandler,
|
||||
|
|
@ -346,6 +377,16 @@ func (bot TipBot) getHandler() []Handler {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{tb.OnDocument, tb.OnVideo, tb.OnAnimation, tb.OnVoice, tb.OnAudio, tb.OnSticker, tb.OnVideoNote},
|
||||
Handler: bot.fileHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: MessageInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.requirePrivateChatInterceptor,
|
||||
bot.logMessageInterceptor,
|
||||
bot.loadUserInterceptor}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{tb.OnText},
|
||||
Handler: bot.anyTextHandler,
|
||||
|
|
@ -592,5 +633,379 @@ func (bot TipBot) getHandler() []Handler {
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopNewShopButton},
|
||||
Handler: bot.shopNewShopHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopAddItemButton},
|
||||
Handler: bot.shopNewItemHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopBuyitemButton},
|
||||
Handler: bot.shopGetItemFilesHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopNextitemButton},
|
||||
Handler: bot.shopNextItemButtonHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&browseShopButton},
|
||||
Handler: bot.shopsBrowser,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopSelectButton},
|
||||
Handler: bot.shopSelect,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
// button that opens selection of shops to delete
|
||||
{
|
||||
Endpoints: []interface{}{&shopDeleteShopButton},
|
||||
Handler: bot.shopsDeleteShopBrowser,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
// button that selects which shop to delete
|
||||
{
|
||||
Endpoints: []interface{}{&shopDeleteSelectButton},
|
||||
Handler: bot.shopSelectDelete,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
// button that opens selection of shops to get links of
|
||||
{
|
||||
Endpoints: []interface{}{&shopLinkShopButton},
|
||||
Handler: bot.shopsLinkShopBrowser,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
// button that selects which shop to link
|
||||
{
|
||||
Endpoints: []interface{}{&shopLinkSelectButton},
|
||||
Handler: bot.shopSelectLink,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
// button that opens selection of shops to rename
|
||||
{
|
||||
Endpoints: []interface{}{&shopRenameShopButton},
|
||||
Handler: bot.shopsRenameShopBrowser,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
// button that selects which shop to rename
|
||||
{
|
||||
Endpoints: []interface{}{&shopRenameSelectButton},
|
||||
Handler: bot.shopSelectRename,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
// button that opens shops settings buttons view
|
||||
{
|
||||
Endpoints: []interface{}{&shopSettingsButton},
|
||||
Handler: bot.shopSettingsHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
// button that lets user enter description for shops
|
||||
{
|
||||
Endpoints: []interface{}{&shopDescriptionShopButton},
|
||||
Handler: bot.shopsDescriptionHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
// button that resets user shops
|
||||
{
|
||||
Endpoints: []interface{}{&shopResetShopButton},
|
||||
Handler: bot.shopsResetHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopResetShopAskButton},
|
||||
Handler: bot.shopsAskDeleteAllShopsHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopPrevitemButton},
|
||||
Handler: bot.shopPrevItemButtonHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopShopsButton},
|
||||
Handler: bot.shopsHandlerCallback,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
// shop item settings buttons
|
||||
{
|
||||
Endpoints: []interface{}{&shopItemSettingsButton},
|
||||
Handler: bot.shopItemSettingsHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopItemSettingsBackButton},
|
||||
Handler: bot.displayShopItemHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopItemDeleteButton},
|
||||
Handler: bot.shopItemDeleteHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopItemPriceButton},
|
||||
Handler: bot.shopItemPriceHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopItemTitleButton},
|
||||
Handler: bot.shopItemTitleHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopItemAddFileButton},
|
||||
Handler: bot.shopItemAddItemHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopItemBuyButton},
|
||||
Handler: bot.shopConfirmBuyHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
{
|
||||
Endpoints: []interface{}{&shopItemCancelBuyButton},
|
||||
Handler: bot.displayShopItemHandler,
|
||||
Interceptor: &Interceptor{
|
||||
Type: CallbackInterceptor,
|
||||
Before: []intercept.Func{
|
||||
bot.localizerInterceptor,
|
||||
bot.loadUserInterceptor,
|
||||
bot.lockInterceptor,
|
||||
},
|
||||
OnDefer: []intercept.Func{
|
||||
bot.unlockInterceptor,
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@ package telegram
|
|||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/i18n"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/runtime"
|
||||
i18n2 "github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"strconv"
|
||||
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/lnbits"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept"
|
||||
|
|
@ -22,11 +22,6 @@ const (
|
|||
QueryInterceptor
|
||||
)
|
||||
|
||||
func init() {
|
||||
handlerMutex = make(map[int64]*sync.Mutex)
|
||||
handlerMapMutex = &sync.Mutex{}
|
||||
}
|
||||
|
||||
var invalidTypeError = fmt.Errorf("invalid type")
|
||||
|
||||
type Interceptor struct {
|
||||
|
|
@ -36,22 +31,12 @@ type Interceptor struct {
|
|||
OnDefer []intercept.Func
|
||||
}
|
||||
|
||||
// handlerMapMutex to prevent concurrent map read / writes on HandlerMutex map
|
||||
var handlerMapMutex *sync.Mutex
|
||||
|
||||
// handlerMutex map holds mutex for every telegram user. Mutex locket as first before interceptor and unlocked on defer intercept
|
||||
var handlerMutex map[int64]*sync.Mutex
|
||||
|
||||
// unlockInterceptor invoked as onDefer interceptor
|
||||
func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context.Context, error) {
|
||||
user := getTelegramUserFromInterface(i)
|
||||
if user != nil {
|
||||
handlerMapMutex.Lock()
|
||||
if handlerMutex[user.ID] != nil {
|
||||
handlerMutex[user.ID].Unlock()
|
||||
}
|
||||
handlerMapMutex.Unlock()
|
||||
log.Debugf("[mutex] Unlocked user %d", user.ID)
|
||||
runtime.Unlock(strconv.FormatInt(user.ID, 10))
|
||||
log.Tracef("[User mutex] Unlocked user %d", user.ID)
|
||||
}
|
||||
return ctx, nil
|
||||
}
|
||||
|
|
@ -60,13 +45,8 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context
|
|||
func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.Context, error) {
|
||||
user := getTelegramUserFromInterface(i)
|
||||
if user != nil {
|
||||
handlerMapMutex.Lock()
|
||||
if handlerMutex[user.ID] == nil {
|
||||
handlerMutex[user.ID] = &sync.Mutex{}
|
||||
}
|
||||
handlerMapMutex.Unlock()
|
||||
handlerMutex[user.ID].Lock()
|
||||
log.Debugf("[mutex] Locked user %d", user.ID)
|
||||
runtime.Lock(strconv.FormatInt(user.ID, 10))
|
||||
log.Tracef("[User mutex] Locked user %d", user.ID)
|
||||
return ctx, nil
|
||||
}
|
||||
return nil, invalidTypeError
|
||||
|
|
|
|||
|
|
@ -34,13 +34,19 @@ func TryRecognizeQrCode(img image.Image) (*gozxing.Result, error) {
|
|||
}
|
||||
|
||||
// photoHandler is the handler function for every photo from a private chat that the bot receives
|
||||
func (bot TipBot) photoHandler(ctx context.Context, m *tb.Message) {
|
||||
func (bot *TipBot) photoHandler(ctx context.Context, m *tb.Message) {
|
||||
if m.Chat.Type != tb.ChatPrivate {
|
||||
return
|
||||
}
|
||||
if m.Photo == nil {
|
||||
return
|
||||
}
|
||||
user := LoadUser(ctx)
|
||||
if c := stateCallbackMessage[user.StateKey]; c != nil {
|
||||
c(ctx, m)
|
||||
ResetUserState(user, bot)
|
||||
return
|
||||
}
|
||||
|
||||
// get file reader closer from Telegram api
|
||||
reader, err := bot.Telegram.GetFile(m.Photo.MediaFile())
|
||||
|
|
|
|||
|
|
@ -273,7 +273,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) {
|
|||
// bot.trySendMessage(c.Sender, sendErrorMessage)
|
||||
errmsg := fmt.Sprintf("[/send] Error: Transaction failed. %s", err)
|
||||
log.Errorln(errmsg)
|
||||
bot.tryEditMessage(c.Message, fmt.Sprintf("%s %s", i18n.Translate(sendData.LanguageCode, "sendErrorMessage"), err), &tb.ReplyMarkup{})
|
||||
bot.tryEditMessage(c.Message, i18n.Translate(sendData.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{})
|
||||
return
|
||||
}
|
||||
sendData.Inactivate(sendData, bot.Bunt)
|
||||
|
|
|
|||
1364
internal/telegram/shop.go
Normal file
1364
internal/telegram/shop.go
Normal file
File diff suppressed because it is too large
Load diff
291
internal/telegram/shop_helpers.go
Normal file
291
internal/telegram/shop_helpers.go
Normal file
|
|
@ -0,0 +1,291 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/lnbits"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/runtime"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/storage/transaction"
|
||||
"github.com/eko/gocache/store"
|
||||
log "github.com/sirupsen/logrus"
|
||||
tb "gopkg.in/lightningtipbot/telebot.v2"
|
||||
)
|
||||
|
||||
func (bot TipBot) shopsMainMenu(ctx context.Context, shops *Shops) *tb.ReplyMarkup {
|
||||
browseShopButton := shopKeyboard.Data("🛍 Browse shops", "shops_browse", shops.ID)
|
||||
shopNewShopButton := shopKeyboard.Data("✅ New Shop", "shops_newshop", shops.ID)
|
||||
shopSettingsButton := shopKeyboard.Data("⚙️ Settings", "shops_settings", shops.ID)
|
||||
user := LoadUser(ctx)
|
||||
|
||||
buttons := []tb.Row{}
|
||||
if len(shops.Shops) > 0 {
|
||||
buttons = append(buttons, shopKeyboard.Row(browseShopButton))
|
||||
}
|
||||
if user.Telegram.ID == shops.Owner.Telegram.ID {
|
||||
buttons = append(buttons, shopKeyboard.Row(shopNewShopButton, shopSettingsButton))
|
||||
}
|
||||
shopKeyboard.Inline(
|
||||
buttons...,
|
||||
)
|
||||
return shopKeyboard
|
||||
}
|
||||
|
||||
func (bot TipBot) shopsSettingsMenu(ctx context.Context, shops *Shops) *tb.ReplyMarkup {
|
||||
shopShopsButton := shopKeyboard.Data("⬅️ Back", "shops_shops", shops.ID)
|
||||
shopLinkShopButton := shopKeyboard.Data("🔗 Shop links", "shops_linkshop", shops.ID)
|
||||
shopRenameShopButton := shopKeyboard.Data("⌨️ Rename a shop", "shops_renameshop", shops.ID)
|
||||
shopDeleteShopButton := shopKeyboard.Data("🚫 Delete shops", "shops_deleteshop", shops.ID)
|
||||
shopDescriptionShopButton := shopKeyboard.Data("💬 Description", "shops_description", shops.ID)
|
||||
// // shopResetShopButton := shopKeyboard.Data("⚠️ Delete all shops", "shops_reset", shops.ID)
|
||||
// buttons := []tb.Row{
|
||||
// shopKeyboard.Row(shopLinkShopButton),
|
||||
// shopKeyboard.Row(shopDescriptionShopButton),
|
||||
// shopKeyboard.Row(shopRenameShopButton),
|
||||
// shopKeyboard.Row(shopDeleteShopButton),
|
||||
// // shopKeyboard.Row(shopResetShopButton),
|
||||
// shopKeyboard.Row(shopShopsButton),
|
||||
// }
|
||||
// shopKeyboard.Inline(
|
||||
// buttons...,
|
||||
// )
|
||||
|
||||
button := []tb.Btn{
|
||||
shopLinkShopButton,
|
||||
shopDescriptionShopButton,
|
||||
shopRenameShopButton,
|
||||
shopDeleteShopButton,
|
||||
shopShopsButton,
|
||||
}
|
||||
shopKeyboard.Inline(buttonWrapper(button, shopKeyboard, 2)...)
|
||||
return shopKeyboard
|
||||
}
|
||||
|
||||
// shopItemSettingsMenu builds the buttons of the item settings
|
||||
func (bot TipBot) shopItemSettingsMenu(ctx context.Context, shop *Shop, item *ShopItem) *tb.ReplyMarkup {
|
||||
shopItemPriceButton = shopKeyboard.Data("💯 Set price", "shop_itemprice", item.ID)
|
||||
shopItemDeleteButton = shopKeyboard.Data("🚫 Delete item", "shop_itemdelete", item.ID)
|
||||
shopItemTitleButton = shopKeyboard.Data("⌨️ Set title", "shop_itemtitle", item.ID)
|
||||
shopItemAddFileButton = shopKeyboard.Data("💾 Add file", "shop_itemaddfile", item.ID)
|
||||
shopItemSettingsBackButton = shopKeyboard.Data("⬅️ Back", "shop_itemsettingsback", item.ID)
|
||||
user := LoadUser(ctx)
|
||||
buttons := []tb.Row{}
|
||||
if user.Telegram.ID == shop.Owner.Telegram.ID {
|
||||
buttons = append(buttons, shopKeyboard.Row(shopItemDeleteButton, shopItemSettingsBackButton))
|
||||
buttons = append(buttons, shopKeyboard.Row(shopItemTitleButton, shopItemPriceButton))
|
||||
buttons = append(buttons, shopKeyboard.Row(shopItemAddFileButton))
|
||||
}
|
||||
shopKeyboard.Inline(
|
||||
buttons...,
|
||||
)
|
||||
return shopKeyboard
|
||||
}
|
||||
|
||||
// shopItemConfirmBuyMenu builds the buttons to confirm a purchase
|
||||
func (bot TipBot) shopItemConfirmBuyMenu(ctx context.Context, shop *Shop, item *ShopItem) *tb.ReplyMarkup {
|
||||
shopItemBuyButton = shopKeyboard.Data(fmt.Sprintf("💸 Pay %d sat", item.Price), "shop_itembuy", item.ID)
|
||||
shopItemCancelBuyButton = shopKeyboard.Data("⬅️ Back", "shop_itemcancelbuy", item.ID)
|
||||
buttons := []tb.Row{}
|
||||
buttons = append(buttons, shopKeyboard.Row(shopItemBuyButton))
|
||||
buttons = append(buttons, shopKeyboard.Row(shopItemCancelBuyButton))
|
||||
shopKeyboard.Inline(
|
||||
buttons...,
|
||||
)
|
||||
return shopKeyboard
|
||||
}
|
||||
|
||||
// shopMenu builds the buttons in the item browser
|
||||
func (bot TipBot) shopMenu(ctx context.Context, shop *Shop, item *ShopItem) *tb.ReplyMarkup {
|
||||
user := LoadUser(ctx)
|
||||
shopView, err := bot.getUserShopview(ctx, user)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
shopShopsButton := shopKeyboard.Data("⬅️ Back", "shops_shops", shop.ShopsID)
|
||||
shopAddItemButton = shopKeyboard.Data("✅ New item", "shop_additem", shop.ID)
|
||||
shopItemSettingsButton = shopKeyboard.Data("⚙️ Settings", "shop_itemsettings", item.ID)
|
||||
shopNextitemButton = shopKeyboard.Data(">", "shop_nextitem", shop.ID)
|
||||
shopPrevitemButton = shopKeyboard.Data("<", "shop_previtem", shop.ID)
|
||||
buyButtonText := "📩 Get"
|
||||
if item.Price > 0 {
|
||||
buyButtonText = fmt.Sprintf("Buy (%d sat)", item.Price)
|
||||
}
|
||||
shopBuyitemButton = shopKeyboard.Data(buyButtonText, "shop_buyitem", item.ID)
|
||||
|
||||
buttons := []tb.Row{}
|
||||
if user.Telegram.ID == shop.Owner.Telegram.ID {
|
||||
if len(shop.Items) == 0 {
|
||||
buttons = append(buttons, shopKeyboard.Row(shopAddItemButton))
|
||||
} else {
|
||||
buttons = append(buttons, shopKeyboard.Row(shopAddItemButton, shopItemSettingsButton))
|
||||
}
|
||||
}
|
||||
// publicButtons := []tb.Row{}
|
||||
if len(shop.Items) > 0 {
|
||||
if shopView.Page == len(shop.Items)-1 {
|
||||
// last page
|
||||
shopNextitemButton = shopKeyboard.Data("x", "shop_nextitem", shop.ID)
|
||||
}
|
||||
buttons = append(buttons, shopKeyboard.Row(shopPrevitemButton, shopBuyitemButton, shopNextitemButton))
|
||||
}
|
||||
buttons = append(buttons, shopKeyboard.Row(shopShopsButton))
|
||||
shopKeyboard.Inline(
|
||||
buttons...,
|
||||
)
|
||||
return shopKeyboard
|
||||
}
|
||||
|
||||
// makseShopSelectionButtons produces a list of all buttons with a uniqueString ID
|
||||
func (bot *TipBot) makseShopSelectionButtons(shops []*Shop, uniqueString string) []tb.Btn {
|
||||
var buttons []tb.Btn
|
||||
for _, shop := range shops {
|
||||
buttons = append(buttons, shopKeyboard.Data(shop.Title, uniqueString, shop.ID))
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
// -------------- ShopView --------------
|
||||
|
||||
// getUserShopview returns ShopView object from cache that holds information about the user's current browsing view
|
||||
func (bot *TipBot) getUserShopview(ctx context.Context, user *lnbits.User) (shopView ShopView, err error) {
|
||||
sv, err := bot.Cache.Get(fmt.Sprintf("shopview-%d", user.Telegram.ID))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
shopView = sv.(ShopView)
|
||||
return
|
||||
}
|
||||
func (bot *TipBot) shopViewDeleteAllStatusMsgs(ctx context.Context, user *lnbits.User, start int) (shopView ShopView, err error) {
|
||||
runtime.Lock(fmt.Sprintf("shopview-delete-%d", user.Telegram.ID))
|
||||
shopView, err = bot.getUserShopview(ctx, user)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
statusMessages := shopView.StatusMessages
|
||||
// delete all status messages from cache
|
||||
shopView.StatusMessages = append([]*tb.Message{}, statusMessages[0:start]...)
|
||||
bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour})
|
||||
|
||||
deleteStatusMessages(start, statusMessages, bot)
|
||||
runtime.Unlock(fmt.Sprintf("shopview-delete-%d", user.Telegram.ID))
|
||||
return
|
||||
}
|
||||
|
||||
func deleteStatusMessages(start int, messages []*tb.Message, bot *TipBot) {
|
||||
// delete all status messages from telegram
|
||||
for _, msg := range messages[start:] {
|
||||
bot.tryDeleteMessage(msg)
|
||||
}
|
||||
}
|
||||
|
||||
// sendStatusMessage adds a status message to the shopVoew.statusMessages
|
||||
// slide and sends a status message to the user.
|
||||
func (bot *TipBot) sendStatusMessage(ctx context.Context, to tb.Recipient, what interface{}, options ...interface{}) (msg *tb.Message) {
|
||||
user := LoadUser(ctx)
|
||||
id := fmt.Sprintf("shopview-delete-%d", user.Telegram.ID)
|
||||
|
||||
// write into cache
|
||||
runtime.Lock(id)
|
||||
shopView, err := bot.getUserShopview(ctx, user)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
statusMsg := bot.trySendMessage(to, what, options...)
|
||||
shopView.StatusMessages = append(shopView.StatusMessages, statusMsg)
|
||||
bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour})
|
||||
runtime.Unlock(id)
|
||||
return statusMsg
|
||||
}
|
||||
|
||||
// sendStatusMessageAndDelete invokes sendStatusMessage and creates
|
||||
// a ticker to delete all status messages after 5 seconds.
|
||||
func (bot *TipBot) sendStatusMessageAndDelete(ctx context.Context, to tb.Recipient, what interface{}, options ...interface{}) (msg *tb.Message) {
|
||||
user := LoadUser(ctx)
|
||||
id := fmt.Sprintf("shopview-delete-%d", user.Telegram.ID)
|
||||
statusMsg := bot.sendStatusMessage(ctx, to, what, options...)
|
||||
// kick off ticker to remove all messages
|
||||
ticker := runtime.GetTicker(id, runtime.WithDuration(5*time.Second))
|
||||
if !ticker.Started {
|
||||
ticker.Do(func() {
|
||||
bot.shopViewDeleteAllStatusMsgs(ctx, user, 1)
|
||||
// removing ticker asap done
|
||||
runtime.RemoveTicker(id)
|
||||
})
|
||||
} else {
|
||||
ticker.ResetChan <- struct{}{}
|
||||
}
|
||||
return statusMsg
|
||||
}
|
||||
|
||||
// --------------- Shop ---------------
|
||||
|
||||
// initUserShops is a helper function for creating a Shops for the user in the database
|
||||
func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops, error) {
|
||||
id := fmt.Sprintf("shops-%d", user.Telegram.ID)
|
||||
shops := &Shops{
|
||||
Base: transaction.New(transaction.ID(id)),
|
||||
ID: id,
|
||||
Owner: user,
|
||||
Shops: []string{},
|
||||
MaxShops: MAX_SHOPS,
|
||||
}
|
||||
runtime.IgnoreError(shops.Set(shops, bot.ShopBunt))
|
||||
return shops, nil
|
||||
}
|
||||
|
||||
// getUserShops returns the Shops for the user
|
||||
func (bot *TipBot) getUserShops(ctx context.Context, user *lnbits.User) (*Shops, error) {
|
||||
tx := &Shops{Base: transaction.New(transaction.ID(fmt.Sprintf("shops-%d", user.Telegram.ID)))}
|
||||
sn, err := tx.Get(tx, bot.ShopBunt)
|
||||
if err != nil {
|
||||
log.Errorf("[getUserShops] User: %s (%d): %s", GetUserStr(user.Telegram), user.Telegram.ID, err)
|
||||
return &Shops{}, err
|
||||
}
|
||||
transaction.Unlock(tx.ID)
|
||||
shops := sn.(*Shops)
|
||||
return shops, nil
|
||||
}
|
||||
|
||||
// addUserShop adds a new Shop to the Shops of a user
|
||||
func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, error) {
|
||||
shops, err := bot.getUserShops(ctx, user)
|
||||
if err != nil {
|
||||
return &Shop{}, err
|
||||
}
|
||||
shopId := fmt.Sprintf("shop-%s", RandStringRunes(10))
|
||||
shop := &Shop{
|
||||
Base: transaction.New(transaction.ID(shopId)),
|
||||
ID: shopId,
|
||||
Title: fmt.Sprintf("Shop %d (%s)", len(shops.Shops)+1, shopId),
|
||||
Owner: user,
|
||||
Type: "photo",
|
||||
Items: make(map[string]ShopItem),
|
||||
LanguageCode: ctx.Value("publicLanguageCode").(string),
|
||||
ShopsID: shops.ID,
|
||||
MaxItems: MAX_ITEMS_PER_SHOP,
|
||||
}
|
||||
runtime.IgnoreError(shop.Set(shop, bot.ShopBunt))
|
||||
shops.Shops = append(shops.Shops, shopId)
|
||||
runtime.IgnoreError(shops.Set(shops, bot.ShopBunt))
|
||||
return shop, nil
|
||||
}
|
||||
|
||||
// getShop returns the Shop for the given ID
|
||||
func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) {
|
||||
tx := &Shop{Base: transaction.New(transaction.ID(shopId))}
|
||||
sn, err := tx.Get(tx, bot.ShopBunt)
|
||||
// immediatelly set intransaction to block duplicate calls
|
||||
if err != nil {
|
||||
log.Errorf("[getShop] %s", err)
|
||||
return &Shop{}, err
|
||||
}
|
||||
transaction.Unlock(tx.ID)
|
||||
shop := sn.(*Shop)
|
||||
if shop.Owner == nil {
|
||||
return &Shop{}, fmt.Errorf("shop has no owner")
|
||||
}
|
||||
return shop, nil
|
||||
}
|
||||
25
internal/telegram/state.go
Normal file
25
internal/telegram/state.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/lnbits"
|
||||
tb "gopkg.in/lightningtipbot/telebot.v2"
|
||||
)
|
||||
|
||||
type StateCallbackMessage map[lnbits.UserStateKey]func(ctx context.Context, m *tb.Message)
|
||||
|
||||
var stateCallbackMessage StateCallbackMessage
|
||||
|
||||
func initializeStateCallbackMessage(bot *TipBot) {
|
||||
stateCallbackMessage = StateCallbackMessage{
|
||||
lnbits.UserStateLNURLEnterAmount: bot.enterAmountHandler,
|
||||
lnbits.UserEnterAmount: bot.enterAmountHandler,
|
||||
lnbits.UserEnterUser: bot.enterUserHandler,
|
||||
lnbits.UserEnterShopTitle: bot.enterShopTitleHandler,
|
||||
lnbits.UserStateShopItemSendPhoto: bot.addShopItemPhoto,
|
||||
lnbits.UserStateShopItemSendPrice: bot.enterShopItemPriceHandler,
|
||||
lnbits.UserStateShopItemSendTitle: bot.enterShopItemTitleHandler,
|
||||
lnbits.UserStateShopItemSendItemFile: bot.addItemFileHandler,
|
||||
lnbits.UserEnterShopsDescription: bot.enterShopsDescriptionHandler,
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import (
|
|||
tb "gopkg.in/lightningtipbot/telebot.v2"
|
||||
)
|
||||
|
||||
func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) {
|
||||
func (bot *TipBot) anyTextHandler(ctx context.Context, m *tb.Message) {
|
||||
if m.Chat.Type != tb.ChatPrivate {
|
||||
return
|
||||
}
|
||||
|
|
@ -36,18 +36,10 @@ func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) {
|
|||
bot.lnurlHandler(ctx, m)
|
||||
return
|
||||
}
|
||||
|
||||
// could be a LNURL
|
||||
// var lnurlregex = regexp.MustCompile(`.*?((lnurl)([0-9]{1,}[a-z0-9]+){1})`)
|
||||
|
||||
// inputs asked for
|
||||
if user.StateKey == lnbits.UserStateLNURLEnterAmount || user.StateKey == lnbits.UserEnterAmount {
|
||||
bot.enterAmountHandler(ctx, m)
|
||||
if c := stateCallbackMessage[user.StateKey]; c != nil {
|
||||
c(ctx, m)
|
||||
//ResetUserState(user, bot)
|
||||
}
|
||||
if user.StateKey == lnbits.UserEnterUser {
|
||||
bot.enterUserHandler(ctx, m)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
type EnterUserStateData struct {
|
||||
|
|
@ -114,7 +106,6 @@ func (bot *TipBot) enterUserHandler(ctx context.Context, m *tb.Message) {
|
|||
switch EnterUserStateData.Type {
|
||||
case "CreateSendState":
|
||||
m.Text = fmt.Sprintf("/send %s", userstr)
|
||||
SetUserState(user, bot, lnbits.UserHasEnteredAmount, "")
|
||||
bot.sendHandler(ctx, m)
|
||||
return
|
||||
default:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue