From fbbe18d7a7ab0b967c68a65e6bf5c1ab9386a7d4 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Tue, 21 Sep 2021 21:35:10 +0200 Subject: [PATCH 001/541] add interceptor and handler context (#69) * add interceptor for handler context * remove user load * remove UserInitializedWallet * remove getuser invocation * add lnbits user to inlineFaucet * set wallet client * lnbits user to inline receive * lnbits user to inline send * add context to handleInlineReceiveQuery * add handler.go * fix help message * fix handler registration * rename * check if reply to is nil * remove unwanted stuff * error handlers * remove client from wallet * rename force to require * string fixes * add intercept.Func * add PrivateChat check * remove private chat requirement for anytexthandler * fix callback * errors fixed * fix callback * loadUser * do not require * check type in interceptor * check type in interceptor * check for user wallets * allow send for toUser with wallet but not initialized * fix reply to context * mdescape * strfux * set state columns only * update str * set user now * anytext should not load user outside pm * rename endpoint to donationEndpoint * register uppercase endpoints and add comments * fix photos in logMessageInterceptor * reduce log level to trace Co-authored-by: LightningTipBot --- balance.go | 15 +- bot.go | 77 +------ database.go | 4 +- donate.go | 25 +-- handler.go | 272 ++++++++++++++++++++++++ help.go | 21 +- inline_faucet.go | 91 ++++---- inline_query.go | 9 +- inline_receive.go | 49 +++-- inline_send.go | 66 +++--- interceptor.go | 112 ++++++++++ internal/lnbits/lnbits.go | 11 +- internal/lnbits/types.go | 1 - internal/lnbits/webhook.go | 1 - internal/lnurl/lnurl.go | 4 +- internal/telegram/intercept/callback.go | 64 ++++++ internal/telegram/intercept/message.go | 68 ++++++ internal/telegram/intercept/query.go | 64 ++++++ invoice.go | 13 +- link.go | 11 +- lnurl.go | 43 ++-- pay.go | 41 ++-- photo.go | 17 +- send.go | 90 ++++---- start.go | 36 ++-- text.go | 16 +- tip.go | 46 ++-- transaction.go | 81 +++---- users.go | 44 ++-- 29 files changed, 927 insertions(+), 465 deletions(-) create mode 100644 handler.go create mode 100644 interceptor.go create mode 100644 internal/telegram/intercept/callback.go create mode 100644 internal/telegram/intercept/message.go create mode 100644 internal/telegram/intercept/query.go diff --git a/balance.go b/balance.go index 17c7f7a4..36454023 100644 --- a/balance.go +++ b/balance.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" log "github.com/sirupsen/logrus" @@ -13,27 +14,27 @@ const ( balanceErrorMessage = "🚫 Error fetching your balance. Please try again later." ) -func (bot TipBot) balanceHandler(m *tb.Message) { +func (bot TipBot) balanceHandler(ctx context.Context, m *tb.Message) { // check and print all commands - bot.anyTextHandler(m) + bot.anyTextHandler(ctx, m) // reply only in private message if m.Chat.Type != tb.ChatPrivate { // delete message NewMessage(m, WithDuration(0, bot.telegram)) } // first check whether the user is initialized - fromUser, err := GetUser(m.Sender, bot) - if err != nil { - log.Errorf("[/balance] Error: %s", err) + user := LoadUser(ctx) + if user.Wallet == nil { return } - if !fromUser.Initialized { + + if !user.Initialized { bot.startHandler(m) return } usrStr := GetUserStr(m.Sender) - balance, err := bot.GetUserBalance(m.Sender) + balance, err := bot.GetUserBalance(user) if err != nil { log.Errorf("[/balance] Error fetching %s's balance: %s", usrStr, err) bot.trySendMessage(m.Sender, balanceErrorMessage) diff --git a/bot.go b/bot.go index 7c4abb19..718673ae 100644 --- a/bot.go +++ b/bot.go @@ -2,21 +2,15 @@ package main import ( "fmt" - "strings" - "sync" - "time" - - "github.com/LightningTipBot/LightningTipBot/internal/storage" - + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" - + "github.com/LightningTipBot/LightningTipBot/internal/storage" log "github.com/sirupsen/logrus" - - "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "gopkg.in/tucnak/telebot.v2" tb "gopkg.in/tucnak/telebot.v2" - "gorm.io/gorm" + "sync" + "time" ) type TipBot struct { @@ -66,7 +60,7 @@ func newTelegramBot() *tb.Bot { // todo -- may want to derive user wallets from this specific bot wallet (master wallet), since lnbits usermanager extension is able to do that. func (bot TipBot) initBotWallet() error { botWalletInitialisation.Do(func() { - err := bot.initWallet(bot.telegram.Me) + _, err := bot.initWallet(bot.telegram.Me) if err != nil { log.Errorln(fmt.Sprintf("[initBotWallet] Could not initialize bot wallet: %s", err.Error())) return @@ -75,67 +69,6 @@ func (bot TipBot) initBotWallet() error { return nil } -// registerTelegramHandlers will register all telegram handlers. -func (bot TipBot) registerTelegramHandlers() { - telegramHandlerRegistration.Do(func() { - // Set up handlers - var endpointHandler = map[string]interface{}{ - "/tip": bot.tipHandler, - "/pay": bot.confirmPaymentHandler, - "/invoice": bot.invoiceHandler, - "/balance": bot.balanceHandler, - "/start": bot.startHandler, - "/send": bot.confirmSendHandler, - "/help": bot.helpHandler, - "/basics": bot.basicsHandler, - "/donate": bot.donationHandler, - "/advanced": bot.advancedHelpHandler, - "/link": bot.lndhubHandler, - "/lnurl": bot.lnurlHandler, - "/faucet": bot.faucetHandler, - "/zapfhahn": bot.faucetHandler, - "/kraan": bot.faucetHandler, - tb.OnPhoto: bot.privatePhotoHandler, - tb.OnText: bot.anyTextHandler, - tb.OnQuery: bot.anyQueryHandler, - tb.OnChosenInlineResult: bot.anyChosenInlineHandler, - } - // assign handler to endpoint - for endpoint, handler := range endpointHandler { - log.Debugf("Registering: %s", endpoint) - bot.telegram.Handle(endpoint, handler) - - // if the endpoint is a string command (not photo etc) - if strings.HasPrefix(endpoint, "/") { - // register upper case versions as well - bot.telegram.Handle(strings.ToUpper(endpoint), handler) - } - } - - // button handlers - // for /pay - bot.telegram.Handle(&btnPay, bot.payHandler) - bot.telegram.Handle(&btnCancelPay, bot.cancelPaymentHandler) - // for /send - bot.telegram.Handle(&btnSend, bot.sendHandler) - bot.telegram.Handle(&btnCancelSend, bot.cancelSendHandler) - - // register inline button handlers - // button for inline send - bot.telegram.Handle(&btnAcceptInlineSend, bot.acceptInlineSendHandler) - bot.telegram.Handle(&btnCancelInlineSend, bot.cancelInlineSendHandler) - - // button for inline receive - bot.telegram.Handle(&btnAcceptInlineReceive, bot.acceptInlineReceiveHandler) - bot.telegram.Handle(&btnCancelInlineReceive, bot.cancelInlineReceiveHandler) - - // // button for inline faucet - bot.telegram.Handle(&btnAcceptInlineFaucet, bot.accpetInlineFaucetHandler) - bot.telegram.Handle(&btnCancelInlineFaucet, bot.cancelInlineFaucetHandler) - - }) -} - // Start will initialize the telegram bot and lnbits. func (bot TipBot) Start() { // set up lnbits api diff --git a/database.go b/database.go index 13f6392d..896048bc 100644 --- a/database.go +++ b/database.go @@ -42,11 +42,9 @@ func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { if tx.Error != nil { errmsg := fmt.Sprintf("[GetUser] Couldn't fetch %s's info from database.", GetUserStr(u)) log.Warnln(errmsg) + user.Telegram = u return user, tx.Error } - defer func() { - user.Wallet.Client = bot.client - }() var err error go func() { userCopy := bot.copyLowercaseUser(u) diff --git a/donate.go b/donate.go index b0e5987f..76d940d0 100644 --- a/donate.go +++ b/donate.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "io" "io/ioutil" @@ -27,7 +28,7 @@ var ( donateHelpText = "📖 Oops, that didn't work. %s\n\n" + "*Usage:* `/donate `\n" + "*Example:* `/donate 1000`" - endpoint string + donationEndpoint string ) func helpDonateUsage(errormsg string) string { @@ -38,9 +39,9 @@ func helpDonateUsage(errormsg string) string { } } -func (bot TipBot) donationHandler(m *tb.Message) { +func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { // check and print all commands - bot.anyTextHandler(m) + bot.anyTextHandler(ctx, m) if len(strings.Split(m.Text, " ")) < 2 { bot.trySendMessage(m.Sender, helpDonateUsage(donateEnterAmountMessage)) @@ -58,7 +59,7 @@ func (bot TipBot) donationHandler(m *tb.Message) { // command is valid msg := bot.trySendMessage(m.Sender, donationProgressMessage) // get invoice - resp, err := http.Get(fmt.Sprintf(endpoint, amount, GetUserStr(m.Sender), GetUserStr(bot.telegram.Me))) + resp, err := http.Get(fmt.Sprintf(donationEndpoint, amount, GetUserStr(m.Sender), GetUserStr(bot.telegram.Me))) if err != nil { log.Errorln(err) bot.tryEditMessage(msg, donationErrorMessage) @@ -72,13 +73,9 @@ func (bot TipBot) donationHandler(m *tb.Message) { } // send donation invoice - user, err := GetUser(m.Sender, bot) - if err != nil { - return - } - + user := LoadUser(ctx) // bot.trySendMessage(user.Telegram, string(body)) - _, err = user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: string(body)}, *user.Wallet) + _, err = user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: string(body)}, bot.client) if err != nil { userStr := GetUserStr(m.Sender) errmsg := fmt.Sprintf("[/donate] Donation failed for user %s: %s", userStr, err) @@ -96,7 +93,7 @@ func init() { if err != nil { panic(err) } - endpoint = sb.String() + donationEndpoint = sb.String() } type rot13Reader struct { @@ -124,7 +121,7 @@ func (rot13 rot13Reader) Read(b []byte) (int, error) { return n, err } -func (bot TipBot) parseCmdDonHandler(m *tb.Message) error { +func (bot TipBot) parseCmdDonHandler(ctx context.Context, m *tb.Message) error { arg := "" if strings.HasPrefix(strings.ToLower(m.Text), "/send") { arg, _ = getArgumentFromCommand(m.Text, 2) @@ -154,9 +151,9 @@ func (bot TipBot) parseCmdDonHandler(m *tb.Message) error { } donationInterceptMessage := sb.String() - bot.trySendMessage(m.Sender, MarkdownEscape(donationInterceptMessage)) + bot.trySendMessage(m.Sender, MarkdownEscape(donationInterceptMessage)) m.Text = fmt.Sprintf("/donate %d", amount) - bot.donationHandler(m) + bot.donationHandler(ctx, m) // returning nil here will abort the parent handler (/pay or /tip) return nil } diff --git a/handler.go b/handler.go new file mode 100644 index 00000000..47e51244 --- /dev/null +++ b/handler.go @@ -0,0 +1,272 @@ +package main + +import ( + "context" + "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + tb "gopkg.in/tucnak/telebot.v2" + "strings" +) + +type Handler struct { + Endpoints []interface{} + Handler interface{} + Interceptor *Interceptor +} + +// registerTelegramHandlers will register all telegram handlers. +func (bot TipBot) registerTelegramHandlers() { + telegramHandlerRegistration.Do(func() { + // Set up handlers + for _, h := range bot.getHandler() { + fmt.Println("registering", h.Endpoints) + bot.register(h) + } + + }) +} + +// registerHandlerWithInterceptor will register a handler with all the predefined interceptors, based on the interceptor type +func (bot TipBot) registerHandlerWithInterceptor(h Handler) { + switch h.Interceptor.Type { + case MessageInterceptor: + for _, endpoint := range h.Endpoints { + bot.handle(endpoint, intercept.HandlerWithMessage(h.Handler.(func(ctx context.Context, query *tb.Message)), + intercept.WithBeforeMessage(h.Interceptor.Before...), + intercept.WithAfterMessage(h.Interceptor.After...))) + } + case QueryInterceptor: + for _, endpoint := range h.Endpoints { + bot.handle(endpoint, intercept.HandlerWithQuery(h.Handler.(func(ctx context.Context, query *tb.Query)), + intercept.WithBeforeQuery(h.Interceptor.Before...), + intercept.WithAfterQuery(h.Interceptor.After...))) + } + case CallbackInterceptor: + for _, endpoint := range h.Endpoints { + bot.handle(endpoint, intercept.HandlerWithCallback(h.Handler.(func(ctx context.Context, callback *tb.Callback)), + intercept.WithBeforeCallback(h.Interceptor.Before...), + intercept.WithAfterCallback(h.Interceptor.After...))) + } + } +} + +// handle accepts an endpoint and handler for telegram handler registration. +// function will automatically register string handlers as uppercase and first letter uppercase. +func (bot TipBot) handle(endpoint interface{}, handler interface{}) { + // register the endpoint + bot.telegram.Handle(endpoint, handler) + switch endpoint.(type) { + case string: + // check if this is a string endpoint + sEndpoint := endpoint.(string) + if strings.HasPrefix(sEndpoint, "/") { + // Uppercase endpoint registration, because starting with slash + bot.telegram.Handle(strings.ToUpper(sEndpoint), handler) + if len(sEndpoint) > 2 { + // Also register endpoint with first letter uppercase + bot.telegram.Handle(fmt.Sprintf("/%s%s", strings.ToUpper(string(sEndpoint[1])), sEndpoint[2:]), handler) + } + } + } +} + +// register registers a handler, so that telegram can handle the endpoint correctly. +func (bot TipBot) register(h Handler) { + if h.Interceptor != nil { + bot.registerHandlerWithInterceptor(h) + } else { + for _, endpoint := range h.Endpoints { + bot.handle(endpoint, h.Handler) + } + } +} + +// getHandler returns a list of all handlers, that need to be registered with telegram +func (bot TipBot) getHandler() []Handler { + return []Handler{ + { + Endpoints: []interface{}{"/start"}, + Handler: bot.startHandler, + }, + { + Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan"}, + Handler: bot.faucetHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.requireUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/tip"}, + Handler: bot.tipHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.loadUserInterceptor, + bot.loadReplyToInterceptor, + }}, + }, + { + Endpoints: []interface{}{"/pay"}, + Handler: bot.confirmPaymentHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/invoice"}, + Handler: bot.invoiceHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/balance"}, + Handler: bot.balanceHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/send"}, + Handler: bot.confirmSendHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/help"}, + Handler: bot.helpHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/basics"}, + Handler: bot.basicsHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/donate"}, + Handler: bot.donationHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/advanced"}, + Handler: bot.advancedHelpHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/link"}, + Handler: bot.lndhubHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/lnurl"}, + Handler: bot.lnurlHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{tb.OnPhoto}, + Handler: bot.photoHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.requirePrivateChatInterceptor, + bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{tb.OnText}, + Handler: bot.anyTextHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.requirePrivateChatInterceptor, + bot.loadUserInterceptor, + }}, + }, + { + Endpoints: []interface{}{tb.OnQuery}, + Handler: bot.anyQueryHandler, + Interceptor: &Interceptor{ + Type: QueryInterceptor, + Before: []intercept.Func{bot.requireUserInterceptor}}, + }, + { + Endpoints: []interface{}{tb.OnChosenInlineResult}, + Handler: bot.anyChosenInlineHandler, + }, + { + Endpoints: []interface{}{&btnPay}, + Handler: bot.payHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{&btnCancelPay}, + Handler: bot.cancelPaymentHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{&btnSend}, + Handler: bot.sendHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{&btnCancelSend}, + Handler: bot.cancelSendHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{&btnAcceptInlineSend}, + Handler: bot.acceptInlineSendHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{&btnCancelInlineSend}, + Handler: bot.cancelInlineSendHandler, + }, + { + Endpoints: []interface{}{&btnAcceptInlineReceive}, + Handler: bot.acceptInlineReceiveHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{&btnCancelInlineReceive}, + Handler: bot.cancelInlineReceiveHandler, + }, + { + Endpoints: []interface{}{&btnAcceptInlineFaucet}, + Handler: bot.accpetInlineFaucetHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{&btnCancelInlineFaucet}, + Handler: bot.cancelInlineFaucetHandler, + }, + } +} diff --git a/help.go b/help.go index ceca3833..7880f96b 100644 --- a/help.go +++ b/help.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" tb "gopkg.in/tucnak/telebot.v2" @@ -71,9 +72,9 @@ func (bot TipBot) makeHelpMessage(m *tb.Message) string { return fmt.Sprintf(helpMessage, dynamicHelpMessage) } -func (bot TipBot) helpHandler(m *tb.Message) { +func (bot TipBot) helpHandler(ctx context.Context, m *tb.Message) { // check and print all commands - bot.anyTextHandler(m) + bot.anyTextHandler(ctx, m) if !m.Private() { // delete message NewMessage(m, WithDuration(0, bot.telegram)) @@ -82,9 +83,9 @@ func (bot TipBot) helpHandler(m *tb.Message) { return } -func (bot TipBot) basicsHandler(m *tb.Message) { +func (bot TipBot) basicsHandler(ctx context.Context, m *tb.Message) { // check and print all commands - bot.anyTextHandler(m) + bot.anyTextHandler(ctx, m) if !m.Private() { // delete message NewMessage(m, WithDuration(0, bot.telegram)) @@ -93,7 +94,7 @@ func (bot TipBot) basicsHandler(m *tb.Message) { return } -func (bot TipBot) makeadvancedHelpMessage(m *tb.Message) string { +func (bot TipBot) makeAdvancedHelpMessage(m *tb.Message) string { dynamicHelpMessage := "" // user has no username set if len(m.Sender.Username) == 0 { @@ -108,7 +109,7 @@ func (bot TipBot) makeadvancedHelpMessage(m *tb.Message) string { dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("Your Lightning Address:\n`%s`\n", lnaddr) } - lnurl, err := bot.UserGetLNURL(m.Sender) + lnurl, err := UserGetLNURL(m.Sender) if err != nil { dynamicHelpMessage = "" } else { @@ -117,16 +118,16 @@ func (bot TipBot) makeadvancedHelpMessage(m *tb.Message) string { } // this is so stupid: - return fmt.Sprintf(advancedMessage, dynamicHelpMessage, GetUserStrMd(bot.telegram.Me), GetUserStrMd(bot.telegram.Me), GetUserStrMd(bot.telegram.Me)) + return fmt.Sprintf(advancedMessage, dynamicHelpMessage, GetUserStr(bot.telegram.Me), GetUserStr(bot.telegram.Me), GetUserStr(bot.telegram.Me)) } -func (bot TipBot) advancedHelpHandler(m *tb.Message) { +func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) { // check and print all commands - bot.anyTextHandler(m) + bot.anyTextHandler(ctx, m) if !m.Private() { // delete message NewMessage(m, WithDuration(0, bot.telegram)) } - bot.trySendMessage(m.Sender, bot.makeadvancedHelpMessage(m), tb.NoPreview) + bot.trySendMessage(m.Sender, bot.makeAdvancedHelpMessage(m), tb.NoPreview) return } diff --git a/inline_faucet.go b/inline_faucet.go index 99e997e2..69f66b57 100644 --- a/inline_faucet.go +++ b/inline_faucet.go @@ -1,10 +1,13 @@ package main import ( + "context" "fmt" "strconv" "time" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" @@ -20,7 +23,7 @@ const ( inlineFaucetInvalidAmountMessage = "🚫 Invalid amount." inlineFaucetSentMessage = "🚰 %d sat sent to %s." inlineFaucetReceivedMessage = "🚰 %s sent you %d sat." - inlineFaucetHelpFaucetInGroup = "Create a faucet in a group with the bot inside or use 👉 inline commands (/advanced for more)." + inlineFaucetHelpFaucetInGroup = "Create a faucet in a group with the bot inside or use 👉 inline command (/advanced for more)." inlineFaucetHelpText = "📖 Oops, that didn't work. %s\n\n" + "*Usage:* `/faucet `\n" + "*Example:* `/faucet 210 21`" @@ -40,19 +43,19 @@ var ( ) type InlineFaucet struct { - Message string `json:"inline_faucet_message"` - Amount int `json:"inline_faucet_amount"` - RemainingAmount int `json:"inline_faucet_remainingamount"` - PerUserAmount int `json:"inline_faucet_peruseramount"` - From *tb.User `json:"inline_faucet_from"` - To []*tb.User `json:"inline_faucet_to"` - Memo string `json:"inline_faucet_memo"` - ID string `json:"inline_faucet_id"` - Active bool `json:"inline_faucet_active"` - NTotal int `json:"inline_faucet_ntotal"` - NTaken int `json:"inline_faucet_ntaken"` - UserNeedsWallet bool `json:"inline_faucet_userneedswallet"` - InTransaction bool `json:"inline_faucet_intransaction"` + Message string `json:"inline_faucet_message"` + Amount int `json:"inline_faucet_amount"` + RemainingAmount int `json:"inline_faucet_remainingamount"` + PerUserAmount int `json:"inline_faucet_peruseramount"` + From *lnbits.User `json:"inline_faucet_from"` + To []*tb.User `json:"inline_faucet_to"` + Memo string `json:"inline_faucet_memo"` + ID string `json:"inline_faucet_id"` + Active bool `json:"inline_faucet_active"` + NTotal int `json:"inline_faucet_ntotal"` + NTaken int `json:"inline_faucet_ntaken"` + UserNeedsWallet bool `json:"inline_faucet_userneedswallet"` + InTransaction bool `json:"inline_faucet_intransaction"` } func NewInlineFaucet() *InlineFaucet { @@ -127,7 +130,7 @@ func (bot *TipBot) getInlineFaucet(c *tb.Callback) (*InlineFaucet, error) { } -func (bot TipBot) faucetHandler(m *tb.Message) { +func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { if m.Private() { bot.trySendMessage(m.Sender, fmt.Sprintf(inlineFaucetHelpText, inlineFaucetHelpFaucetInGroup)) return @@ -159,9 +162,9 @@ func (bot TipBot) faucetHandler(m *tb.Message) { return } inlineFaucet.NTotal = inlineFaucet.Amount / inlineFaucet.PerUserAmount - + fromUser := LoadUser(ctx) fromUserStr := GetUserStr(m.Sender) - balance, err := bot.GetUserBalance(m.Sender) + balance, err := bot.GetUserBalance(fromUser) if err != nil { errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) log.Errorln(errmsg) @@ -192,14 +195,14 @@ func (bot TipBot) faucetHandler(m *tb.Message) { bot.trySendMessage(m.Chat, inlineMessage, inlineFaucetMenu) log.Infof("[faucet] %s created faucet %s: %d sat (%d per user)", fromUserStr, inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) inlineFaucet.Message = inlineMessage - inlineFaucet.From = m.Sender + inlineFaucet.From = fromUser inlineFaucet.Memo = memo inlineFaucet.RemainingAmount = inlineFaucet.Amount runtime.IgnoreError(bot.bunt.Set(inlineFaucet)) } -func (bot TipBot) handleInlineFaucetQuery(q *tb.Query) { +func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { inlineFaucet := NewInlineFaucet() var err error inlineFaucet.Amount, err = decodeAmountFromCommand(q.Text) @@ -228,9 +231,9 @@ func (bot TipBot) handleInlineFaucetQuery(q *tb.Query) { return } inlineFaucet.NTotal = inlineFaucet.Amount / inlineFaucet.PerUserAmount - + fromUser := LoadUser(ctx) fromUserStr := GetUserStr(&q.From) - balance, err := bot.GetUserBalance(&q.From) + balance, err := bot.GetUserBalance(fromUser) if err != nil { errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) log.Errorln(errmsg) @@ -276,7 +279,7 @@ func (bot TipBot) handleInlineFaucetQuery(q *tb.Query) { // create persistend inline send struct inlineFaucet.Message = inlineMessage inlineFaucet.ID = id - inlineFaucet.From = &q.From + inlineFaucet.From = fromUser inlineFaucet.RemainingAmount = inlineFaucet.Amount inlineFaucet.Memo = memo runtime.IgnoreError(bot.bunt.Set(inlineFaucet)) @@ -292,12 +295,15 @@ func (bot TipBot) handleInlineFaucetQuery(q *tb.Query) { } } -func (bot *TipBot) accpetInlineFaucetHandler(c *tb.Callback) { +func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback) { + to := LoadUser(ctx) + inlineFaucet, err := bot.getInlineFaucet(c) if err != nil { log.Errorf("[faucet] %s", err) return } + from := inlineFaucet.From err = bot.LockFaucet(inlineFaucet) if err != nil { log.Errorf("[faucet] %s", err) @@ -310,32 +316,29 @@ func (bot *TipBot) accpetInlineFaucetHandler(c *tb.Callback) { // release faucet no matter what defer bot.ReleaseFaucet(inlineFaucet) - to := c.Sender - from := inlineFaucet.From - - if from.ID == to.ID { - bot.trySendMessage(from, sendYourselfMessage) + if from.Telegram.ID == to.Telegram.ID { + bot.trySendMessage(from.Telegram, sendYourselfMessage) return } // check if to user has already taken from the faucet for _, a := range inlineFaucet.To { - if a.ID == to.ID { + if a.ID == to.Telegram.ID { // to user is already in To slice, has taken from facuet - log.Infof("[faucet] %s already took from faucet %s", GetUserStr(to), inlineFaucet.ID) + log.Infof("[faucet] %s already took from faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) return } } if inlineFaucet.RemainingAmount >= inlineFaucet.PerUserAmount { - toUserStrMd := GetUserStrMd(to) - fromUserStrMd := GetUserStrMd(from) - toUserStr := GetUserStr(to) - fromUserStr := GetUserStr(from) + toUserStrMd := GetUserStrMd(to.Telegram) + fromUserStrMd := GetUserStrMd(from.Telegram) + toUserStr := GetUserStr(to.Telegram) + fromUserStr := GetUserStr(from.Telegram) // check if user exists and create a wallet if not - _, exists := bot.UserExists(to) + _, exists := bot.UserExists(to.Telegram) if !exists { log.Infof("[faucet] User %s has no wallet.", toUserStr) - err = bot.CreateWalletForTelegramUser(to) + to, err = bot.CreateWalletForTelegramUser(to.Telegram) if err != nil { errmsg := fmt.Errorf("[faucet] Error: Could not create wallet for %s", toUserStr) log.Errorln(errmsg) @@ -343,7 +346,7 @@ func (bot *TipBot) accpetInlineFaucetHandler(c *tb.Callback) { } } - if !bot.UserInitializedWallet(to) { + if !to.Initialized { inlineFaucet.UserNeedsWallet = true } @@ -354,11 +357,7 @@ func (bot *TipBot) accpetInlineFaucetHandler(c *tb.Callback) { success, err := t.Send() if !success { - if err != nil { - bot.trySendMessage(from, fmt.Sprintf(tipErrorMessage, err)) - } else { - bot.trySendMessage(from, fmt.Sprintf(tipErrorMessage, tipUndefinedErrorMsg)) - } + bot.trySendMessage(from.Telegram, sendErrorMessage) errMsg := fmt.Sprintf("[faucet] Transaction failed: %s", err) log.Errorln(errMsg) return @@ -366,11 +365,11 @@ func (bot *TipBot) accpetInlineFaucetHandler(c *tb.Callback) { log.Infof("[faucet] faucet %s: %d sat from %s to %s ", inlineFaucet.ID, inlineFaucet.PerUserAmount, fromUserStr, toUserStr) inlineFaucet.NTaken += 1 - inlineFaucet.To = append(inlineFaucet.To, to) + inlineFaucet.To = append(inlineFaucet.To, to.Telegram) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount - _, err = bot.telegram.Send(to, fmt.Sprintf(inlineFaucetReceivedMessage, fromUserStrMd, inlineFaucet.PerUserAmount)) - _, err = bot.telegram.Send(from, fmt.Sprintf(inlineFaucetSentMessage, inlineFaucet.PerUserAmount, toUserStrMd)) + _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(inlineFaucetReceivedMessage, fromUserStrMd, inlineFaucet.PerUserAmount)) + _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(inlineFaucetSentMessage, inlineFaucet.PerUserAmount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[faucet] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -414,7 +413,7 @@ func (bot *TipBot) cancelInlineFaucetHandler(c *tb.Callback) { log.Errorf("[cancelInlineSendHandler] %s", err) return } - if c.Sender.ID == inlineFaucet.From.ID { + if c.Sender.ID == inlineFaucet.From.Telegram.ID { bot.tryEditMessage(c.Message, inlineFaucetCancelledMessage, &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false diff --git a/inline_query.go b/inline_query.go index 515fb926..d68680c7 100644 --- a/inline_query.go +++ b/inline_query.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "strconv" "strings" @@ -87,7 +88,7 @@ func (bot TipBot) anyChosenInlineHandler(q *tb.ChosenInlineResult) { fmt.Printf(q.Query) } -func (bot TipBot) anyQueryHandler(q *tb.Query) { +func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) { if q.Text == "" { bot.inlineQueryInstructions(q) return @@ -98,14 +99,14 @@ func (bot TipBot) anyQueryHandler(q *tb.Query) { q.Text = strings.TrimPrefix(q.Text, "/") } if strings.HasPrefix(q.Text, "send") || strings.HasPrefix(q.Text, "pay") { - bot.handleInlineSendQuery(q) + bot.handleInlineSendQuery(ctx, q) } if strings.HasPrefix(q.Text, "faucet") || strings.HasPrefix(q.Text, "giveaway") || strings.HasPrefix(q.Text, "zapfhahn") || strings.HasPrefix(q.Text, "kraan") { - bot.handleInlineFaucetQuery(q) + bot.handleInlineFaucetQuery(ctx, q) } if strings.HasPrefix(q.Text, "receive") || strings.HasPrefix(q.Text, "get") || strings.HasPrefix(q.Text, "payme") || strings.HasPrefix(q.Text, "request") { - bot.handleInlineReceiveQuery(q) + bot.handleInlineReceiveQuery(ctx, q) } } diff --git a/inline_receive.go b/inline_receive.go index 56f202c3..3138508a 100644 --- a/inline_receive.go +++ b/inline_receive.go @@ -1,9 +1,12 @@ package main import ( + "context" "fmt" "time" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" @@ -16,6 +19,7 @@ const ( inlineReceiveCreateWalletMessage = "Chat with %s 👈 to manage your wallet." inlineReceiveYourselfMessage = "📖 You can't pay to yourself." inlineReceiveFailedMessage = "🚫 Receive failed." + inlineReceiveCancelledMessage = "🚫 Receive cancelled." ) var ( @@ -29,10 +33,10 @@ var ( ) type InlineReceive struct { - Message string `json:"inline_receive_message"` - Amount int `json:"inline_receive_amount"` - From *tb.User `json:"inline_receive_from"` - To *tb.User `json:"inline_receive_to"` + Message string `json:"inline_receive_message"` + Amount int `json:"inline_receive_amount"` + From *lnbits.User `json:"inline_receive_from"` + To *lnbits.User `json:"inline_receive_to"` Memo string ID string `json:"inline_receive_id"` Active bool `json:"inline_receive_active"` @@ -108,7 +112,8 @@ func (bot *TipBot) getInlineReceive(c *tb.Callback) (*InlineReceive, error) { } -func (bot TipBot) handleInlineReceiveQuery(q *tb.Query) { +func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { + from := LoadUser(ctx) inlineReceive := NewInlineReceive() var err error inlineReceive.Amount, err = decodeAmountFromCommand(q.Text) @@ -160,7 +165,7 @@ func (bot TipBot) handleInlineReceiveQuery(q *tb.Query) { // create persistend inline send struct // add data to persistent object inlineReceive.ID = id - inlineReceive.To = &q.From // The user who wants to receive + inlineReceive.To = from // The user who wants to receive // add result to persistent struct inlineReceive.Message = inlineMessage runtime.IgnoreError(bot.bunt.Set(inlineReceive)) @@ -177,7 +182,7 @@ func (bot TipBot) handleInlineReceiveQuery(q *tb.Query) { } } -func (bot *TipBot) acceptInlineReceiveHandler(c *tb.Callback) { +func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) { inlineReceive, err := bot.getInlineReceive(c) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -199,18 +204,17 @@ func (bot *TipBot) acceptInlineReceiveHandler(c *tb.Callback) { // user `from` is the one who is SENDING // user `to` is the one who is RECEIVING - from := c.Sender + from := LoadUser(ctx) to := inlineReceive.To - toUserStrMd := GetUserStrMd(to) - fromUserStrMd := GetUserStrMd(from) - toUserStr := GetUserStr(to) - fromUserStr := GetUserStr(from) + toUserStrMd := GetUserStrMd(to.Telegram) + fromUserStrMd := GetUserStrMd(from.Telegram) + toUserStr := GetUserStr(to.Telegram) + fromUserStr := GetUserStr(from.Telegram) if from.ID == to.ID { - bot.trySendMessage(from, sendYourselfMessage) + bot.trySendMessage(from.Telegram, sendYourselfMessage) return } - // balance check of the user balance, err := bot.GetUserBalance(from) if err != nil { @@ -221,7 +225,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(c *tb.Callback) { // check if fromUser has balance if balance < inlineReceive.Amount { log.Errorln("[acceptInlineReceiveHandler] balance of user %s too low", fromUserStr) - bot.trySendMessage(from, fmt.Sprintf(inlineSendBalanceLowMessage, balance)) + bot.trySendMessage(from.Telegram, fmt.Sprintf(inlineSendBalanceLowMessage, balance)) return } @@ -234,11 +238,6 @@ func (bot *TipBot) acceptInlineReceiveHandler(c *tb.Callback) { t.Memo = transactionMemo success, err := t.Send() if !success { - if err != nil { - bot.trySendMessage(from, fmt.Sprintf(tipErrorMessage, err)) - } else { - bot.trySendMessage(from, fmt.Sprintf(tipErrorMessage, tipUndefinedErrorMsg)) - } errMsg := fmt.Sprintf("[acceptInlineReceiveHandler] Transaction failed: %s", err) log.Errorln(errMsg) bot.tryEditMessage(c.Message, inlineReceiveFailedMessage, &tb.ReplyMarkup{}) @@ -253,14 +252,14 @@ func (bot *TipBot) acceptInlineReceiveHandler(c *tb.Callback) { inlineReceive.Message = inlineReceive.Message + fmt.Sprintf(inlineReceiveAppendMemo, memo) } - if !bot.UserInitializedWallet(to) { + if !to.Initialized { inlineReceive.Message += "\n\n" + fmt.Sprintf(inlineSendCreateWalletMessage, GetUserStrMd(bot.telegram.Me)) } bot.tryEditMessage(c.Message, inlineReceive.Message, &tb.ReplyMarkup{}) // notify users - _, err = bot.telegram.Send(to, fmt.Sprintf(sendReceivedMessage, fromUserStrMd, inlineReceive.Amount)) - _, err = bot.telegram.Send(from, fmt.Sprintf(tipSentMessage, inlineReceive.Amount, toUserStrMd)) + _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(sendReceivedMessage, fromUserStrMd, inlineReceive.Amount)) + _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(tipSentMessage, inlineReceive.Amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[acceptInlineReceiveHandler] Error: Receive message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -274,8 +273,8 @@ func (bot *TipBot) cancelInlineReceiveHandler(c *tb.Callback) { log.Errorf("[cancelInlineReceiveHandler] %s", err) return } - if c.Sender.ID == inlineReceive.To.ID { - bot.tryEditMessage(c.Message, sendCancelledMessage, &tb.ReplyMarkup{}) + if c.Sender.ID == inlineReceive.To.Telegram.ID { + bot.tryEditMessage(c.Message, inlineReceiveCancelledMessage, &tb.ReplyMarkup{}) // set the inlineReceive inactive inlineReceive.Active = false inlineReceive.InTransaction = false diff --git a/inline_send.go b/inline_send.go index 80d54192..3dd60702 100644 --- a/inline_send.go +++ b/inline_send.go @@ -1,9 +1,12 @@ package main import ( + "context" "fmt" "time" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" @@ -31,14 +34,14 @@ var ( ) type InlineSend struct { - Message string `json:"inline_send_message"` - Amount int `json:"inline_send_amount"` - From *tb.User `json:"inline_send_from"` - To *tb.User `json:"inline_send_to"` - Memo string `json:"inline_send_memo"` - ID string `json:"inline_send_id"` - Active bool `json:"inline_send_active"` - InTransaction bool `json:"inline_send_intransaction"` + Message string `json:"inline_send_message"` + Amount int `json:"inline_send_amount"` + From *lnbits.User `json:"inline_send_from"` + To *tb.User `json:"inline_send_to"` + Memo string `json:"inline_send_memo"` + ID string `json:"inline_send_id"` + Active bool `json:"inline_send_active"` + InTransaction bool `json:"inline_send_intransaction"` } func NewInlineSend() *InlineSend { @@ -112,7 +115,7 @@ func (bot *TipBot) getInlineSend(c *tb.Callback) (*InlineSend, error) { } -func (bot TipBot) handleInlineSendQuery(q *tb.Query) { +func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { inlineSend := NewInlineSend() var err error inlineSend.Amount, err = decodeAmountFromCommand(q.Text) @@ -124,8 +127,9 @@ func (bot TipBot) handleInlineSendQuery(q *tb.Query) { bot.inlineQueryReplyWithError(q, inlineSendInvalidAmountMessage, fmt.Sprintf(inlineQuerySendDescription, bot.telegram.Me.Username)) return } + fromUser := LoadUser(ctx) fromUserStr := GetUserStr(&q.From) - balance, err := bot.GetUserBalance(&q.From) + balance, err := bot.GetUserBalance(fromUser) if err != nil { errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) log.Errorln(errmsg) @@ -175,7 +179,7 @@ func (bot TipBot) handleInlineSendQuery(q *tb.Query) { // add data to persistent object inlineSend.Message = inlineMessage inlineSend.ID = id - inlineSend.From = &q.From + inlineSend.From = fromUser // add result to persistent struct runtime.IgnoreError(bot.bunt.Set(inlineSend)) } @@ -190,12 +194,15 @@ func (bot TipBot) handleInlineSendQuery(q *tb.Query) { } } -func (bot *TipBot) acceptInlineSendHandler(c *tb.Callback) { +func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) { + to := LoadUser(ctx) + inlineSend, err := bot.getInlineSend(c) if err != nil { log.Errorf("[acceptInlineSendHandler] %s", err) return } + fromUser := inlineSend.From // immediatelly set intransaction to block duplicate calls err = bot.LockSend(inlineSend) if err != nil { @@ -210,26 +217,24 @@ func (bot *TipBot) acceptInlineSendHandler(c *tb.Callback) { defer bot.ReleaseSend(inlineSend) amount := inlineSend.Amount - to := c.Sender - from := inlineSend.From - inlineSend.To = to + inlineSend.To = to.Telegram - if from.ID == to.ID { - bot.trySendMessage(from, sendYourselfMessage) + if fromUser.Telegram.ID == to.Telegram.ID { + bot.trySendMessage(fromUser.Telegram, sendYourselfMessage) return } - toUserStrMd := GetUserStrMd(to) - fromUserStrMd := GetUserStrMd(from) - toUserStr := GetUserStr(to) - fromUserStr := GetUserStr(from) + toUserStrMd := GetUserStrMd(to.Telegram) + fromUserStrMd := GetUserStrMd(fromUser.Telegram) + toUserStr := GetUserStr(to.Telegram) + fromUserStr := GetUserStr(fromUser.Telegram) // check if user exists and create a wallet if not - _, exists := bot.UserExists(to) + _, exists := bot.UserExists(to.Telegram) if !exists { log.Infof("[sendInline] User %s has no wallet.", toUserStr) - err = bot.CreateWalletForTelegramUser(to) + to, err = bot.CreateWalletForTelegramUser(to.Telegram) if err != nil { errmsg := fmt.Errorf("[sendInline] Error: Could not create wallet for %s", toUserStr) log.Errorln(errmsg) @@ -241,15 +246,10 @@ func (bot *TipBot) acceptInlineSendHandler(c *tb.Callback) { // todo: user new get username function to get userStrings transactionMemo := fmt.Sprintf("Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount) - t := NewTransaction(bot, from, to, amount, TransactionType("inline send")) + t := NewTransaction(bot, fromUser, to, amount, TransactionType("inline send")) t.Memo = transactionMemo success, err := t.Send() if !success { - if err != nil { - bot.trySendMessage(from, fmt.Sprintf(tipErrorMessage, err)) - } else { - bot.trySendMessage(from, fmt.Sprintf(tipErrorMessage, tipUndefinedErrorMsg)) - } errMsg := fmt.Sprintf("[sendInline] Transaction failed: %s", err) log.Errorln(errMsg) bot.tryEditMessage(c.Message, inlineSendFailedMessage, &tb.ReplyMarkup{}) @@ -264,14 +264,14 @@ func (bot *TipBot) acceptInlineSendHandler(c *tb.Callback) { inlineSend.Message = inlineSend.Message + fmt.Sprintf(inlineSendAppendMemo, memo) } - if !bot.UserInitializedWallet(to) { + if !to.Initialized { inlineSend.Message += "\n\n" + fmt.Sprintf(inlineSendCreateWalletMessage, GetUserStrMd(bot.telegram.Me)) } bot.tryEditMessage(c.Message, inlineSend.Message, &tb.ReplyMarkup{}) // notify users - _, err = bot.telegram.Send(to, fmt.Sprintf(sendReceivedMessage, fromUserStrMd, amount)) - _, err = bot.telegram.Send(from, fmt.Sprintf(tipSentMessage, amount, toUserStrMd)) + _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(sendReceivedMessage, fromUserStrMd, amount)) + _, err = bot.telegram.Send(fromUser.Telegram, fmt.Sprintf(tipSentMessage, amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[sendInline] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -285,7 +285,7 @@ func (bot *TipBot) cancelInlineSendHandler(c *tb.Callback) { log.Errorf("[cancelInlineSendHandler] %s", err) return } - if c.Sender.ID == inlineSend.From.ID { + if c.Sender.ID == inlineSend.From.Telegram.ID { bot.tryEditMessage(c.Message, sendCancelledMessage, &tb.ReplyMarkup{}) // set the inlineSend inactive inlineSend.Active = false diff --git a/interceptor.go b/interceptor.go new file mode 100644 index 00000000..293ece4c --- /dev/null +++ b/interceptor.go @@ -0,0 +1,112 @@ +package main + +import ( + "context" + "fmt" + + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + log "github.com/sirupsen/logrus" + tb "gopkg.in/tucnak/telebot.v2" +) + +type InterceptorType int + +const ( + MessageInterceptor InterceptorType = iota + CallbackInterceptor + QueryInterceptor +) + +var invalidTypeError = fmt.Errorf("invalid type") + +type Interceptor struct { + Type InterceptorType + Before []intercept.Func + After []intercept.Func +} + +func (bot TipBot) requireUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + switch i.(type) { + case *tb.Query: + user, err := GetUser(&i.(*tb.Query).From, bot) + return context.WithValue(ctx, "user", user), err + case *tb.Callback: + c := i.(*tb.Callback) + m := *c.Message + m.Sender = c.Sender + user, err := GetUser(i.(*tb.Callback).Sender, bot) + return context.WithValue(ctx, "user", user), err + case *tb.Message: + user, err := GetUser(i.(*tb.Message).Sender, bot) + return context.WithValue(ctx, "user", user), err + } + return nil, invalidTypeError +} +func (bot TipBot) loadUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + ctx, _ = bot.requireUserInterceptor(ctx, i) + return ctx, nil +} + +// loadReplyToInterceptor Loading the telegram user with message intercept +func (bot TipBot) loadReplyToInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + switch i.(type) { + case *tb.Message: + m := i.(*tb.Message) + if m.ReplyTo != nil { + if m.ReplyTo.Sender != nil { + user, _ := GetUser(m.ReplyTo.Sender, bot) + user.Telegram = m.ReplyTo.Sender + return context.WithValue(ctx, "reply_to_user", user), nil + } + } + return ctx, nil + } + return ctx, invalidTypeError +} + +func (bot TipBot) requirePrivateChatInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + switch i.(type) { + case *tb.Message: + m := i.(*tb.Message) + if m.Chat.Type != tb.ChatPrivate { + return nil, fmt.Errorf("no private chat") + } + return ctx, nil + } + return nil, invalidTypeError +} + +const photoTag = "" + +func (bot TipBot) logMessageInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + switch i.(type) { + case *tb.Message: + m := i.(*tb.Message) + if m.Text != "" { + log.Infof("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, m.Text) + } else if m.Photo != nil { + log.Infof("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, photoTag) + } + return ctx, nil + } + return nil, invalidTypeError +} + +// LoadUser from context +func LoadUser(ctx context.Context) *lnbits.User { + u := ctx.Value("user") + if u != nil { + return u.(*lnbits.User) + } + return nil +} + +// LoadReplyToUser from context +func LoadReplyToUser(ctx context.Context) *lnbits.User { + u := ctx.Value("reply_to_user") + if u != nil { + return u.(*lnbits.User) + } + return nil +} diff --git a/internal/lnbits/lnbits.go b/internal/lnbits/lnbits.go index 164b7563..27e106dd 100644 --- a/internal/lnbits/lnbits.go +++ b/internal/lnbits/lnbits.go @@ -74,14 +74,13 @@ func (c *Client) CreateWallet(userId, walletName, adminId string) (wal Wallet, e return } err = resp.ToJSON(&wal) - wal.Client = c return } // Invoice creates an invoice associated with this wallet. -func (c Client) Invoice(params InvoiceParams, w Wallet) (lntx BitInvoice, err error) { +func (w Wallet) Invoice(params InvoiceParams, c *Client) (lntx BitInvoice, err error) { c.header["X-Api-Key"] = w.Adminkey - resp, err := req.Post(c.url+"/api/v1/payments", w.header, req.BodyJSON(¶ms)) + resp, err := req.Post(c.url+"/api/v1/payments", c.header, req.BodyJSON(¶ms)) if err != nil { return } @@ -100,7 +99,7 @@ func (c Client) Invoice(params InvoiceParams, w Wallet) (lntx BitInvoice, err er // Info returns wallet information func (c Client) Info(w Wallet) (wtx Wallet, err error) { c.header["X-Api-Key"] = w.Adminkey - resp, err := req.Get(w.url+"/api/v1/wallet", w.header, nil) + resp, err := req.Get(c.url+"/api/v1/wallet", c.header, nil) if err != nil { return } @@ -135,9 +134,9 @@ func (c Client) Wallets(w User) (wtx []Wallet, err error) { } // Pay pays a given invoice with funds from the wallet. -func (c Client) Pay(params PaymentParams, w Wallet) (wtx BitInvoice, err error) { +func (w Wallet) Pay(params PaymentParams, c *Client) (wtx BitInvoice, err error) { c.header["X-Api-Key"] = w.Adminkey - resp, err := req.Post(c.url+"/api/v1/payments", w.header, req.BodyJSON(¶ms)) + resp, err := req.Post(c.url+"/api/v1/payments", c.header, req.BodyJSON(¶ms)) if err != nil { return } diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 47277ce9..b6968a09 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -74,7 +74,6 @@ func (err Error) Error() string { } type Wallet struct { - *Client `gorm:"-"` ID string `json:"id" gorm:"id"` Adminkey string `json:"adminkey"` Inkey string `json:"inkey"` diff --git a/internal/lnbits/webhook.go b/internal/lnbits/webhook.go index 3bf3f2f3..d47f27bd 100644 --- a/internal/lnbits/webhook.go +++ b/internal/lnbits/webhook.go @@ -50,7 +50,6 @@ func (w *WebhookServer) GetUserByWalletId(walletId string) (*User, error) { if tx.Error != nil { return user, tx.Error } - user.Wallet.Client = w.c return user, nil } diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index c2e6bf9b..7318e4f4 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -103,7 +103,7 @@ func (w Server) serveLNURLpSecond(username string, amount int64) (*lnurl.LNURLPa } // set wallet lnbits client - user.Wallet.Client = w.c + var resp *lnurl.LNURLPayResponse2 // the same description_hash needs to be built in the second request @@ -118,7 +118,7 @@ func (w Server) serveLNURLpSecond(username string, amount int64) (*lnurl.LNURLPa Out: false, DescriptionHash: descriptionHash, Webhook: w.WebhookServer}, - *user.Wallet) + w.c) if err != nil { err = fmt.Errorf("[serveLNURLpSecond] Couldn't create invoice: %v", err) resp = &lnurl.LNURLPayResponse2{ diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go new file mode 100644 index 00000000..62413558 --- /dev/null +++ b/internal/telegram/intercept/callback.go @@ -0,0 +1,64 @@ +package intercept + +import ( + "context" + log "github.com/sirupsen/logrus" + tb "gopkg.in/tucnak/telebot.v2" +) + +type CallbackFuncHandler func(ctx context.Context, message *tb.Callback) +type Func func(ctx context.Context, message interface{}) (context.Context, error) + +type handlerCallbackInterceptor struct { + handler CallbackFuncHandler + before CallbackChain + after CallbackChain +} +type CallbackChain []Func +type CallbackInterceptOption func(*handlerCallbackInterceptor) + +func WithBeforeCallback(chain ...Func) CallbackInterceptOption { + return func(a *handlerCallbackInterceptor) { + a.before = chain + } +} +func WithAfterCallback(chain ...Func) CallbackInterceptOption { + return func(a *handlerCallbackInterceptor) { + a.after = chain + } +} + +func interceptCallback(ctx context.Context, message *tb.Callback, hm CallbackChain) (context.Context, error) { + if ctx == nil { + ctx = context.Background() + } + if hm != nil { + var err error + for _, m := range hm { + ctx, err = m(ctx, message) + if err != nil { + return ctx, err + } + } + } + return ctx, nil +} + +func HandlerWithCallback(handler CallbackFuncHandler, option ...CallbackInterceptOption) func(Callback *tb.Callback) { + hm := &handlerCallbackInterceptor{handler: handler} + for _, opt := range option { + opt(hm) + } + return func(c *tb.Callback) { + ctx, err := interceptCallback(context.Background(), c, hm.before) + if err != nil { + log.Traceln(err) + return + } + hm.handler(ctx, c) + _, err = interceptCallback(ctx, c, hm.after) + if err != nil { + log.Traceln(err) + } + } +} diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go new file mode 100644 index 00000000..9068c411 --- /dev/null +++ b/internal/telegram/intercept/message.go @@ -0,0 +1,68 @@ +package intercept + +import ( + "context" + log "github.com/sirupsen/logrus" + tb "gopkg.in/tucnak/telebot.v2" +) + +type MessageInterface interface { + a() func(ctx context.Context, message *tb.Message) +} +type MessageFuncHandler func(ctx context.Context, message *tb.Message) + +type handlerMessageInterceptor struct { + handler MessageFuncHandler + before MessageChain + after MessageChain +} +type MessageChain []Func +type MessageInterceptOption func(*handlerMessageInterceptor) + +func WithBeforeMessage(chain ...Func) MessageInterceptOption { + return func(a *handlerMessageInterceptor) { + a.before = chain + } +} +func WithAfterMessage(chain ...Func) MessageInterceptOption { + return func(a *handlerMessageInterceptor) { + a.after = chain + } +} + +func interceptMessage(ctx context.Context, message *tb.Message, hm MessageChain) (context.Context, error) { + if ctx == nil { + ctx = context.Background() + } + if hm != nil { + var err error + for _, m := range hm { + ctx, err = m(ctx, message) + if err != nil { + return ctx, err + } + } + } + return ctx, nil +} + +func HandlerWithMessage(handler MessageFuncHandler, option ...MessageInterceptOption) func(message *tb.Message) { + + hm := &handlerMessageInterceptor{handler: handler} + for _, opt := range option { + opt(hm) + } + return func(message *tb.Message) { + ctx, err := interceptMessage(context.Background(), message, hm.before) + if err != nil { + log.Traceln(err) + return + } + hm.handler(ctx, message) + _, err = interceptMessage(ctx, message, hm.after) + if err != nil { + log.Traceln(err) + return + } + } +} diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go new file mode 100644 index 00000000..16a50668 --- /dev/null +++ b/internal/telegram/intercept/query.go @@ -0,0 +1,64 @@ +package intercept + +import ( + "context" + log "github.com/sirupsen/logrus" + tb "gopkg.in/tucnak/telebot.v2" +) + +type QueryFuncHandler func(ctx context.Context, message *tb.Query) + +type handlerQueryInterceptor struct { + handler QueryFuncHandler + before QueryChain + after QueryChain +} +type QueryChain []Func +type QueryInterceptOption func(*handlerQueryInterceptor) + +func WithBeforeQuery(chain ...Func) QueryInterceptOption { + return func(a *handlerQueryInterceptor) { + a.before = chain + } +} +func WithAfterQuery(chain ...Func) QueryInterceptOption { + return func(a *handlerQueryInterceptor) { + a.after = chain + } +} + +func interceptQuery(ctx context.Context, message *tb.Query, hm QueryChain) (context.Context, error) { + if ctx == nil { + ctx = context.Background() + } + if hm != nil { + var err error + for _, m := range hm { + ctx, err = m(ctx, message) + if err != nil { + return ctx, err + } + } + } + return ctx, nil +} + +func HandlerWithQuery(handler QueryFuncHandler, option ...QueryInterceptOption) func(message *tb.Query) { + hm := &handlerQueryInterceptor{handler: handler} + for _, opt := range option { + opt(hm) + } + return func(message *tb.Query) { + ctx, err := interceptQuery(context.Background(), message, hm.before) + if err != nil { + log.Traceln(err) + return + } + hm.handler(ctx, message) + _, err = interceptQuery(ctx, message, hm.after) + if err != nil { + log.Traceln(err) + return + } + } +} diff --git a/invoice.go b/invoice.go index 6ba0c3f6..77cc6024 100644 --- a/invoice.go +++ b/invoice.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "fmt" "strings" @@ -28,9 +29,9 @@ func helpInvoiceUsage(errormsg string) string { } } -func (bot TipBot) invoiceHandler(m *tb.Message) { +func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { // check and print all commands - bot.anyTextHandler(m) + bot.anyTextHandler(ctx, m) if m.Chat.Type != tb.ChatPrivate { // delete message NewMessage(m, WithDuration(0, bot.telegram)) @@ -41,7 +42,11 @@ func (bot TipBot) invoiceHandler(m *tb.Message) { return } - user, err := GetUser(m.Sender, bot) + user := LoadUser(ctx) + if user.Wallet == nil { + return + } + userStr := GetUserStr(m.Sender) amount, err := decodeAmountFromCommand(m.Text) if err != nil { @@ -73,7 +78,7 @@ func (bot TipBot) invoiceHandler(m *tb.Message) { Amount: int64(amount), Memo: memo, Webhook: Configuration.Lnbits.WebhookServer}, - *user.Wallet) + bot.client) if err != nil { errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) log.Errorln(errmsg) diff --git a/link.go b/link.go index fd378790..d2a25e74 100644 --- a/link.go +++ b/link.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "fmt" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" @@ -16,24 +17,20 @@ var ( couldNotLinkMessage = "🚫 Couldn't link your wallet. Please try again later." ) -func (bot TipBot) lndhubHandler(m *tb.Message) { +func (bot TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { if Configuration.Lnbits.LnbitsPublicUrl == "" { bot.trySendMessage(m.Sender, couldNotLinkMessage) return } // check and print all commands - bot.anyTextHandler(m) + bot.anyTextHandler(ctx, m) // reply only in private message if m.Chat.Type != tb.ChatPrivate { // delete message NewMessage(m, WithDuration(0, bot.telegram)) } // first check whether the user is initialized - fromUser, err := GetUser(m.Sender, bot) - if err != nil { - log.Errorf("[/balance] Error: %s", err) - return - } + fromUser := LoadUser(ctx) bot.trySendMessage(m.Sender, walletConnectMessage) lndhubUrl := fmt.Sprintf("lndhub://admin:%s@%slndhub/ext/", fromUser.Wallet.Adminkey, Configuration.Lnbits.LnbitsPublicUrl) diff --git a/lnurl.go b/lnurl.go index bb487d00..dcf7a03f 100644 --- a/lnurl.go +++ b/lnurl.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "encoding/json" "errors" "fmt" @@ -34,7 +35,7 @@ const ( ) // lnurlHandler is invoked on /lnurl command -func (bot TipBot) lnurlHandler(m *tb.Message) { +func (bot TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { // commands: // /lnurl // /lnurl @@ -68,10 +69,8 @@ func (bot TipBot) lnurlHandler(m *tb.Message) { // bot.trySendMessage(m.Sender, err.Error()) return } - user, err := GetUser(m.Sender, bot) - if err != nil { - log.Errorln(err) - bot.tryEditMessage(msg, fmt.Sprintf(lnurlPaymentFailed, "database error.")) + user := LoadUser(ctx) + if user.Wallet == nil { return } @@ -104,7 +103,7 @@ func (bot TipBot) lnurlHandler(m *tb.Message) { SetUserState(user, bot, lnbits.UserStateConfirmLNURLPay, string(paramsJson)) bot.tryDeleteMessage(msg) // directly go to confirm - bot.lnurlPayHandler(m) + bot.lnurlPayHandler(ctx, m) } } @@ -116,7 +115,7 @@ func (bot *TipBot) UserGetLightningAddress(user *tb.User) (string, error) { } } -func (bot *TipBot) UserGetLNURL(user *tb.User) (string, error) { +func UserGetLNURL(user *tb.User) (string, error) { name := strings.ToLower(strings.ToLower(user.Username)) if len(name) == 0 { return "", fmt.Errorf("user has no username.") @@ -133,7 +132,7 @@ func (bot *TipBot) UserGetLNURL(user *tb.User) (string, error) { // lnurlReceiveHandler outputs the LNURL of the user func (bot TipBot) lnurlReceiveHandler(m *tb.Message) { - lnurlEncode, err := bot.UserGetLNURL(m.Sender) + lnurlEncode, err := UserGetLNURL(m.Sender) if err != nil { errmsg := fmt.Sprintf("[lnurlReceiveHandler] Failed to get LNURL: %s", err) log.Errorln(errmsg) @@ -152,14 +151,12 @@ func (bot TipBot) lnurlReceiveHandler(m *tb.Message) { bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lnurlEncode)}) } -func (bot TipBot) lnurlEnterAmountHandler(m *tb.Message) { - user, err := GetUser(m.Sender, bot) - if err != nil { - log.Errorln(err) - // bot.trySendMessage(m.Sender, err.Error()) - ResetUserState(user, bot) +func (bot TipBot) lnurlEnterAmountHandler(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + if user.Wallet == nil { return } + if user.StateKey == lnbits.UserStateLNURLEnterAmount { a, err := strconv.Atoi(m.Text) if err != nil { @@ -192,7 +189,7 @@ func (bot TipBot) lnurlEnterAmountHandler(m *tb.Message) { return } SetUserState(user, bot, lnbits.UserStateConfirmLNURLPay, string(state)) - bot.lnurlPayHandler(m) + bot.lnurlPayHandler(ctx, m) } } @@ -203,16 +200,14 @@ type LnurlStateResponse struct { } // lnurlPayHandler is invoked when the user has delivered an amount and is ready to pay -func (bot TipBot) lnurlPayHandler(c *tb.Message) { +func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { msg := bot.trySendMessage(c.Sender, lnurlGettingUserMessage) - user, err := GetUser(c.Sender, bot) - if err != nil { - log.Errorln(err) - // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(lnurlPaymentFailed, "database error.")) + user := LoadUser(ctx) + if user.Wallet == nil { return } + if user.StateKey == lnbits.UserStateConfirmLNURLPay { client, err := getHttpClient() if err != nil { @@ -263,7 +258,7 @@ func (bot TipBot) lnurlPayHandler(c *tb.Message) { } bot.telegram.Delete(msg) c.Text = fmt.Sprintf("/pay %s", response2.PR) - bot.confirmPaymentHandler(c) + bot.confirmPaymentHandler(ctx, c) } } @@ -365,7 +360,7 @@ func HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error) { } } -func (bot *TipBot) sendToLightningAddress(m *tb.Message, address string, amount int) error { +func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, address string, amount int) error { split := strings.Split(address, "@") if len(split) != 2 { return fmt.Errorf("lightning address format wrong") @@ -388,6 +383,6 @@ func (bot *TipBot) sendToLightningAddress(m *tb.Message, address string, amount } else { m.Text = fmt.Sprintf("/lnurl %s", lnurl) } - bot.lnurlHandler(m) + bot.lnurlHandler(ctx, m) return nil } diff --git a/pay.go b/pay.go index fd672a71..62f3b0af 100644 --- a/pay.go +++ b/pay.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "strings" @@ -36,9 +37,14 @@ func helpPayInvoiceUsage(errormsg string) string { } // confirmPaymentHandler invoked on "/pay lnbc..." command -func (bot TipBot) confirmPaymentHandler(m *tb.Message) { +func (bot TipBot) confirmPaymentHandler(ctx context.Context, m *tb.Message) { // check and print all commands - bot.anyTextHandler(m) + bot.anyTextHandler(ctx, m) + user := LoadUser(ctx) + if user.Wallet == nil { + return + } + if m.Chat.Type != tb.ChatPrivate { // delete message NewMessage(m, WithDuration(0, bot.telegram)) @@ -50,13 +56,6 @@ func (bot TipBot) confirmPaymentHandler(m *tb.Message) { bot.trySendMessage(m.Sender, helpPayInvoiceUsage("")) return } - user, err := GetUser(m.Sender, bot) - if err != nil { - NewMessage(m, WithDuration(0, bot.telegram)) - errmsg := fmt.Sprintf("[/pay] Error: Could not GetUser: %s", err) - log.Errorln(errmsg) - return - } userStr := GetUserStr(m.Sender) paymentRequest, err := getArgumentFromCommand(m.Text, 1) if err != nil { @@ -88,7 +87,7 @@ func (bot TipBot) confirmPaymentHandler(m *tb.Message) { } // check user balance first - balance, err := bot.GetUserBalance(m.Sender) + balance, err := bot.GetUserBalance(user) if err != nil { NewMessage(m, WithDuration(0, bot.telegram)) errmsg := fmt.Sprintf("[/pay] Error: Could not get user balance: %s", err) @@ -119,16 +118,14 @@ func (bot TipBot) confirmPaymentHandler(m *tb.Message) { } // cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot TipBot) cancelPaymentHandler(c *tb.Callback) { +func (bot TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { // reset state immediately - user, err := GetUser(c.Sender, bot) - if err != nil { - return - } + user := LoadUser(ctx) + ResetUserState(user, bot) bot.tryDeleteMessage(c.Message) - _, err = bot.telegram.Send(c.Sender, paymentCancelledMessage) + _, err := bot.telegram.Send(c.Sender, paymentCancelledMessage) if err != nil { log.WithField("message", paymentCancelledMessage).WithField("user", c.Sender.ID).Printf("[Send] %s", err.Error()) return @@ -137,22 +134,22 @@ func (bot TipBot) cancelPaymentHandler(c *tb.Callback) { } // payHandler when user clicked pay "X" on payment confirmation -func (bot TipBot) payHandler(c *tb.Callback) { +func (bot TipBot) payHandler(ctx context.Context, c *tb.Callback) { bot.tryEditMessage(c.Message, c.Message.Text, &tb.ReplyMarkup{}) - user, err := GetUser(c.Sender, bot) - if err != nil { - log.Printf("[GetUser] User: %d: %s", c.Sender.ID, err.Error()) + user := LoadUser(ctx) + if user.Wallet == nil { return } + if user.StateKey == lnbits.UserStateConfirmPayment { invoiceString := user.StateData - // reset state immediatelly + // reset state immediately ResetUserState(user, bot) userStr := GetUserStr(c.Sender) // pay invoice - invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoiceString}, *user.Wallet) + invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoiceString}, bot.client) if err != nil { errmsg := fmt.Sprintf("[/pay] Could not pay invoice of user %s: %s", userStr, err) bot.trySendMessage(c.Sender, fmt.Sprintf(invoicePaymentFailedMessage, err)) diff --git a/photo.go b/photo.go index b334f16e..98b53a95 100644 --- a/photo.go +++ b/photo.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "image" "image/jpeg" @@ -37,30 +38,30 @@ func TryRecognizeQrCode(img image.Image) (*gozxing.Result, error) { return nil, fmt.Errorf("no codes found") } -// privatePhotoHandler is the handler function for every photo from a private chat that the bot receives -func (bot TipBot) privatePhotoHandler(m *tb.Message) { +// 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) { if m.Chat.Type != tb.ChatPrivate { return } if m.Photo == nil { return } - log.Infof("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, "") + // get file reader closer from telegram api reader, err := bot.telegram.GetFile(m.Photo.MediaFile()) if err != nil { - log.Errorf("Getfile error: %v\n", err) + log.Errorf("[photoHandler] getfile error: %v\n", err) return } // decode to jpeg image img, err := jpeg.Decode(reader) if err != nil { - log.Errorf("image.Decode error: %v\n", err) + log.Errorf("[photoHandler] image.Decode error: %v\n", err) return } data, err := TryRecognizeQrCode(img) if err != nil { - log.Errorf("tryRecognizeQrCodes error: %v\n", err) + log.Errorf("[photoHandler] tryRecognizeQrCodes error: %v\n", err) bot.trySendMessage(m.Sender, photoQrNotRecognizedMessage) return } @@ -69,11 +70,11 @@ func (bot TipBot) privatePhotoHandler(m *tb.Message) { // invoke payment handler if lightning.IsInvoice(data.String()) { m.Text = fmt.Sprintf("/pay %s", data.String()) - bot.confirmPaymentHandler(m) + bot.confirmPaymentHandler(ctx, m) return } else if lightning.IsLnurl(data.String()) { m.Text = fmt.Sprintf("/lnurl %s", data.String()) - bot.lnurlHandler(m) + bot.lnurlHandler(ctx, m) return } } diff --git a/send.go b/send.go index 06873c83..82c1a666 100644 --- a/send.go +++ b/send.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "strconv" "strings" @@ -18,11 +19,11 @@ const ( sendUserHasNoWalletMessage = "🚫 User %s hasn't created a wallet yet." sendSentMessage = "💸 %d sat sent to %s." sendReceivedMessage = "🏅 %s sent you %d sat." - sendErrorMessage = "🚫 Transaction failed: %s" + sendErrorMessage = "🚫 Send failed." confirmSendInvoiceMessage = "Do you want to pay to %s?\n\n💸 Amount: %d sat" confirmSendAppendMemo = "\n✉️ %s" sendCancelledMessage = "🚫 Send cancelled." - errorTryLaterMessage = "🚫 Internal error. Please try again later.." + errorTryLaterMessage = "🚫 Error. Please try again later." sendHelpText = "📖 Oops, that didn't work. %s\n\n" + "*Usage:* `/send []`\n" + "*Example:* `/send 1000 @LightningTipBot I just like the bot ❤️`\n" + @@ -40,7 +41,7 @@ func helpSendUsage(errormsg string) string { func (bot *TipBot) SendCheckSyntax(m *tb.Message) (bool, string) { arguments := strings.Split(m.Text, " ") if len(arguments) < 2 { - return false, fmt.Sprintf("Did you enter an amount and a recipient? You can use the /send command to either send to Telegram users like @%s or to a Lightning address like LightningTipBot@ln.tips.", bot.telegram.Me.Username) + return false, fmt.Sprintf("Did you enter an amount and a recipient? You can use the /send command to either send to Telegram users like %s or to a Lightning address like LightningTipBot@ln.tips.", GetUserStrMd(bot.telegram.Me)) } // if len(arguments) < 3 { // return false, "Did you enter a recipient?" @@ -52,19 +53,21 @@ func (bot *TipBot) SendCheckSyntax(m *tb.Message) (bool, string) { } // confirmPaymentHandler invoked on "/send 123 @user" command -func (bot *TipBot) confirmSendHandler(m *tb.Message) { - // reset state immediately - user, err := GetUser(m.Sender, *bot) - if err != nil { +func (bot *TipBot) confirmSendHandler(ctx context.Context, m *tb.Message) { + bot.anyTextHandler(ctx, m) + user := LoadUser(ctx) + if user.Wallet == nil { return } + + // reset state immediately ResetUserState(user, *bot) // check and print all commands - bot.anyTextHandler(m) + // If the send is a reply, then trigger /tip handler if m.IsReply() { - bot.tipHandler(m) + bot.tipHandler(ctx, m) return } @@ -88,8 +91,8 @@ func (bot *TipBot) confirmSendHandler(m *tb.Message) { } if err == nil { if lightning.IsLightningAddress(arg) { - // if the second argument is a lightning address, then send to that address - err = bot.sendToLightningAddress(m, arg, amount) + // lightning address, send to that address + err = bot.sendToLightningAddress(ctx, m, arg, amount) if err != nil { log.Errorln(err.Error()) return @@ -98,6 +101,9 @@ func (bot *TipBot) confirmSendHandler(m *tb.Message) { } } + // todo: this error might have been overwritten by the functions above + // we should only check for a valid amount here, instead of error and amount + // ASSUME INTERNAL SEND TO TELEGRAM USER if err != nil || amount < 1 { errmsg := fmt.Sprintf("[/send] Error: Send amount not valid.") @@ -144,14 +150,14 @@ func (bot *TipBot) confirmSendHandler(m *tb.Message) { toUserStrMention := m.Text[m.Entities[1].Offset : m.Entities[1].Offset+m.Entities[1].Length] toUserStrWithoutAt := strings.TrimPrefix(toUserStrMention, "@") - err = bot.parseCmdDonHandler(m) + err = bot.parseCmdDonHandler(ctx, m) if err == nil { return } toUserDb := &lnbits.User{} tx := bot.database.Where("telegram_username = ?", strings.ToLower(toUserStrWithoutAt)).First(toUserDb) - if tx.Error != nil || toUserDb.Wallet == nil || toUserDb.Initialized == false { + if tx.Error != nil || toUserDb.Wallet == nil { NewMessage(m, WithDuration(0, bot.telegram)) err = fmt.Errorf(sendUserHasNoWalletMessage, MarkdownEscape(toUserStrMention)) bot.trySendMessage(m.Sender, err.Error()) @@ -171,13 +177,6 @@ func (bot *TipBot) confirmSendHandler(m *tb.Message) { // save the send data to the database log.Debug(sendData) - user, err = GetUser(m.Sender, *bot) - if err != nil { - NewMessage(m, WithDuration(0, bot.telegram)) - log.Printf("[/send] Error: %s\n", err.Error()) - bot.trySendMessage(m.Sender, fmt.Sprint(errorTryLaterMessage)) - return - } SetUserState(user, *bot, lnbits.UserStateConfirmSend, sendData) @@ -194,17 +193,13 @@ func (bot *TipBot) confirmSendHandler(m *tb.Message) { } // cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot *TipBot) cancelSendHandler(c *tb.Callback) { +func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { // reset state immediately - user, err := GetUser(c.Sender, *bot) - if err != nil { - log.Errorln(err.Error()) - return - } + user := LoadUser(ctx) ResetUserState(user, *bot) // delete the confirmation message - err = bot.telegram.Delete(c.Message) + err := bot.telegram.Delete(c.Message) if err != nil { log.Errorln("[cancelSendHandler] " + err.Error()) } @@ -217,7 +212,7 @@ func (bot *TipBot) cancelSendHandler(c *tb.Callback) { } // sendHandler invoked when user clicked send on payment confirmation -func (bot *TipBot) sendHandler(c *tb.Callback) { +func (bot *TipBot) sendHandler(ctx context.Context, c *tb.Callback) { // remove buttons from confirmation message _, err := bot.telegram.Edit(c.Message, MarkdownEscape(c.Message.Text), &tb.ReplyMarkup{}) if err != nil { @@ -225,21 +220,17 @@ func (bot *TipBot) sendHandler(c *tb.Callback) { } // decode callback data // log.Debug("[sendHandler] Callback: %s", c.Data) - user, err := GetUser(c.Sender, *bot) - if err != nil { - log.Printf("[GetUser] User: %d: %s", c.Sender.ID, err.Error()) - return - } - if user.StateKey != lnbits.UserStateConfirmSend { - log.Errorf("[sendHandler] User StateKey does not match! User: %d: StateKey: %d", c.Sender.ID, user.StateKey) + from := LoadUser(ctx) + if from.StateKey != lnbits.UserStateConfirmSend { + log.Errorf("[sendHandler] User StateKey does not match! User: %d: StateKey: %d", c.Sender.ID, from.StateKey) return } // decode StateData in which we have information about the send payment - splits := strings.Split(user.StateData, "|") + splits := strings.Split(from.StateData, "|") if len(splits) < 3 { log.Error("[sendHandler] Not enough arguments in callback data") - log.Errorf("user.StateData: %s", user.StateData) + log.Errorf("user.StateData: %s", from.StateData) return } toId, err := strconv.Atoi(splits[0]) @@ -257,15 +248,18 @@ func (bot *TipBot) sendHandler(c *tb.Callback) { } // reset state - ResetUserState(user, *bot) + ResetUserState(from, *bot) // we can now get the wallets of both users - to := &tb.User{ID: toId, Username: toUserStrWithoutAt} - from := c.Sender - toUserStrMd := GetUserStrMd(to) - fromUserStrMd := GetUserStrMd(from) - toUserStr := GetUserStr(to) - fromUserStr := GetUserStr(from) + to, err := GetUser(&tb.User{ID: toId, Username: toUserStrWithoutAt}, *bot) + if err != nil { + log.Errorln(err.Error()) + return + } + toUserStrMd := GetUserStrMd(to.Telegram) + fromUserStrMd := GetUserStrMd(from.Telegram) + toUserStr := GetUserStr(to.Telegram) + fromUserStr := GetUserStr(from.Telegram) transactionMemo := fmt.Sprintf("Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount) t := NewTransaction(bot, from, to, amount, TransactionType("send")) @@ -274,17 +268,17 @@ func (bot *TipBot) sendHandler(c *tb.Callback) { success, err := t.Send() if !success || err != nil { // NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(c.Sender, fmt.Sprintf(sendErrorMessage, err)) + bot.trySendMessage(c.Sender, sendErrorMessage) errmsg := fmt.Sprintf("[/send] Error: Transaction failed. %s", err) log.Errorln(errmsg) return } - bot.trySendMessage(from, fmt.Sprintf(sendSentMessage, amount, toUserStrMd)) - bot.trySendMessage(to, fmt.Sprintf(sendReceivedMessage, fromUserStrMd, amount)) + bot.trySendMessage(from.Telegram, fmt.Sprintf(sendSentMessage, amount, toUserStrMd)) + bot.trySendMessage(to.Telegram, fmt.Sprintf(sendReceivedMessage, fromUserStrMd, amount)) // send memo if it was present if len(sendMemo) > 0 { - bot.trySendMessage(to, fmt.Sprintf("✉️ %s", MarkdownEscape(sendMemo))) + bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", MarkdownEscape(sendMemo))) } return diff --git a/start.go b/start.go index d15a8adc..7139c757 100644 --- a/start.go +++ b/start.go @@ -1,6 +1,7 @@ package main import ( + "context" "errors" "fmt" "strconv" @@ -29,17 +30,17 @@ func (bot TipBot) startHandler(m *tb.Message) { // bot.helpHandler(m) log.Printf("[/start] User: %s (%d)\n", m.Sender.Username, m.Sender.ID) walletCreationMsg, err := bot.telegram.Send(m.Sender, startSettingWalletMessage) - err = bot.initWallet(m.Sender) + user, err := bot.initWallet(m.Sender) if err != nil { log.Errorln(fmt.Sprintf("[startHandler] Error with initWallet: %s", err.Error())) bot.tryEditMessage(walletCreationMsg, startWalletErrorMessage) return } bot.tryDeleteMessage(walletCreationMsg) - - bot.helpHandler(m) + userContext := context.WithValue(context.Background(), "user", user) + bot.helpHandler(userContext, m) bot.trySendMessage(m.Sender, startWalletReadyMessage) - bot.balanceHandler(m) + bot.balanceHandler(userContext, m) // send the user a warning about the fact that they need to set a username if len(m.Sender.Username) == 0 { @@ -48,19 +49,19 @@ func (bot TipBot) startHandler(m *tb.Message) { return } -func (bot TipBot) initWallet(tguser *tb.User) error { +func (bot TipBot) initWallet(tguser *tb.User) (*lnbits.User, error) { user, err := GetUser(tguser, bot) if errors.Is(err, gorm.ErrRecordNotFound) { - u := &lnbits.User{Telegram: tguser} - err = bot.createWallet(u) + user = &lnbits.User{Telegram: tguser} + err = bot.createWallet(user) if err != nil { - return err + return user, err } - u.Initialized = true - err = UpdateUserRecord(u, bot) + user.Initialized = true + err = UpdateUserRecord(user, bot) if err != nil { log.Errorln(fmt.Sprintf("[initWallet] error updating user: %s", err.Error())) - return err + return user, err } } else if !user.Initialized { // update all tip tooltips (with the "initialize me" message) that this user might have received before @@ -69,16 +70,16 @@ func (bot TipBot) initWallet(tguser *tb.User) error { err = UpdateUserRecord(user, bot) if err != nil { log.Errorln(fmt.Sprintf("[initWallet] error updating user: %s", err.Error())) - return err + return user, err } } else if user.Initialized { // wallet is already initialized - return nil + return user, nil } else { err = fmt.Errorf("could not initialize wallet") - return err + return user, err } - return nil + return user, nil } func (bot TipBot) createWallet(user *lnbits.User) error { @@ -92,17 +93,16 @@ func (bot TipBot) createWallet(user *lnbits.User) error { log.Errorln(errormsg) return err } - user.Wallet = &lnbits.Wallet{Client: bot.client} + user.Wallet = &lnbits.Wallet{} user.ID = u.ID user.Name = u.Name - wallet, err := user.Wallet.Wallets(*user) + wallet, err := bot.client.Wallets(*user) if err != nil { errormsg := fmt.Sprintf("[createWallet] Get wallet error: %s", err) log.Errorln(errormsg) return err } user.Wallet = &wallet[0] - user.Wallet.Client = bot.client user.Initialized = false err = UpdateUserRecord(user, bot) if err != nil { diff --git a/text.go b/text.go index 5e7c8e94..38cf8376 100644 --- a/text.go +++ b/text.go @@ -1,10 +1,9 @@ package main import ( + "context" "strings" - log "github.com/sirupsen/logrus" - "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" tb "gopkg.in/tucnak/telebot.v2" @@ -14,15 +13,14 @@ const ( initWalletMessage = "You don't have a wallet yet. Enter */start*" ) -func (bot TipBot) anyTextHandler(m *tb.Message) { - log.Infof("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, m.Text) +func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { if m.Chat.Type != tb.ChatPrivate { return } // check if user is in database, if not, initialize wallet - user, exists := bot.UserExists(m.Sender) - if !exists || !user.Initialized { + user := LoadUser(ctx) + if user.Wallet == nil || !user.Initialized { bot.startHandler(m) return } @@ -31,19 +29,19 @@ func (bot TipBot) anyTextHandler(m *tb.Message) { anyText := strings.ToLower(m.Text) if lightning.IsInvoice(anyText) { m.Text = "/pay " + anyText - bot.confirmPaymentHandler(m) + bot.confirmPaymentHandler(ctx, m) return } if lightning.IsLnurl(anyText) { m.Text = "/lnurl " + anyText - bot.lnurlHandler(m) + bot.lnurlHandler(ctx, m) return } // could be a LNURL // var lnurlregex = regexp.MustCompile(`.*?((lnurl)([0-9]{1,}[a-z0-9]+){1})`) if user.StateKey == lnbits.UserStateLNURLEnterAmount { - bot.lnurlEnterAmountHandler(m) + bot.lnurlEnterAmountHandler(ctx, m) } } diff --git a/tip.go b/tip.go index fb0b8f5a..e25cb43b 100644 --- a/tip.go +++ b/tip.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "strings" "time" @@ -17,7 +18,7 @@ const ( tipYourselfMessage = "📖 You can't tip yourself." tipSentMessage = "💸 %d sat sent to %s." tipReceivedMessage = "🏅 %s has tipped you %d sat." - tipErrorMessage = "🚫 Transaction failed: %s" + tipErrorMessage = "🚫 Tip failed." tipUndefinedErrorMsg = "please try again later" tipHelpText = "📖 Oops, that didn't work. %s\n\n" + "*Usage:* `/tip []`\n" + @@ -40,11 +41,16 @@ func TipCheckSyntax(m *tb.Message) (bool, string) { return true, "" } -func (bot *TipBot) tipHandler(m *tb.Message) { +func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { // delete the tip message after a few seconds, this is default behaviour defer NewMessage(m, WithDuration(time.Second*time.Duration(Configuration.Telegram.MessageDisposeDuration), bot.telegram)) // check and print all commands - bot.anyTextHandler(m) + bot.anyTextHandler(ctx, m) + user := LoadUser(ctx) + if user.Wallet == nil { + return + } + // only if message is a reply if !m.IsReply() { NewMessage(m, WithDuration(0, bot.telegram)) @@ -70,14 +76,14 @@ func (bot *TipBot) tipHandler(m *tb.Message) { return } - err = bot.parseCmdDonHandler(m) + err = bot.parseCmdDonHandler(ctx, m) if err == nil { return } // TIP COMMAND IS VALID + from := LoadUser(ctx) - to := m.ReplyTo.Sender - from := m.Sender + to := LoadReplyToUser(ctx) if from.ID == to.ID { NewMessage(m, WithDuration(0, bot.telegram)) @@ -85,14 +91,14 @@ func (bot *TipBot) tipHandler(m *tb.Message) { return } - toUserStrMd := GetUserStrMd(m.ReplyTo.Sender) - fromUserStrMd := GetUserStrMd(from) - toUserStr := GetUserStr(m.ReplyTo.Sender) - fromUserStr := GetUserStr(from) + toUserStrMd := GetUserStrMd(to.Telegram) + fromUserStrMd := GetUserStrMd(from.Telegram) + toUserStr := GetUserStr(to.Telegram) + fromUserStr := GetUserStr(from.Telegram) - if _, exists := bot.UserExists(to); !exists { + if _, exists := bot.UserExists(to.Telegram); !exists { log.Infof("[/tip] User %s has no wallet.", toUserStr) - err = bot.CreateWalletForTelegramUser(to) + to, err = bot.CreateWalletForTelegramUser(to.Telegram) if err != nil { errmsg := fmt.Errorf("[/tip] Error: Could not create wallet for %s", toUserStr) log.Errorln(errmsg) @@ -117,23 +123,19 @@ func (bot *TipBot) tipHandler(m *tb.Message) { success, err := t.Send() if !success { NewMessage(m, WithDuration(0, bot.telegram)) - if err != nil { - bot.trySendMessage(m.Sender, fmt.Sprintf(tipErrorMessage, err)) - } else { - bot.trySendMessage(m.Sender, fmt.Sprintf(tipErrorMessage, tipUndefinedErrorMsg)) - } + bot.trySendMessage(m.Sender, tipErrorMessage) errMsg := fmt.Sprintf("[/tip] Transaction failed: %s", err) log.Errorln(errMsg) return } // update tooltip if necessary - messageHasTip := tipTooltipHandler(m, bot, amount, bot.UserInitializedWallet(to)) + messageHasTip := tipTooltipHandler(m, bot, amount, to.Initialized) log.Infof("[tip] %d sat from %s to %s", amount, fromUserStr, toUserStr) // notify users - _, err = bot.telegram.Send(from, fmt.Sprintf(tipSentMessage, amount, toUserStrMd)) + _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(tipSentMessage, amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[/tip] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -142,12 +144,12 @@ func (bot *TipBot) tipHandler(m *tb.Message) { // forward tipped message to user once if !messageHasTip { - bot.tryForwardMessage(to, m.ReplyTo, tb.Silent) + bot.tryForwardMessage(to.Telegram, m.ReplyTo, tb.Silent) } - bot.trySendMessage(to, fmt.Sprintf(tipReceivedMessage, fromUserStrMd, amount)) + bot.trySendMessage(to.Telegram, fmt.Sprintf(tipReceivedMessage, fromUserStrMd, amount)) if len(tipMemo) > 0 { - bot.trySendMessage(to, fmt.Sprintf("✉️ %s", MarkdownEscape(tipMemo))) + bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", MarkdownEscape(tipMemo))) } return } diff --git a/transaction.go b/transaction.go index 8cd16251..5c337e8e 100644 --- a/transaction.go +++ b/transaction.go @@ -15,25 +15,25 @@ const ( ) type Transaction struct { - ID uint `gorm:"primarykey"` - Time time.Time `json:"time"` - Bot *TipBot `gorm:"-"` - From *tb.User `json:"from" gorm:"-"` - To *tb.User `json:"to" gorm:"-"` - FromId int `json:"from_id" ` - ToId int `json:"to_id" ` - FromUser string `json:"from_user"` - ToUser string `json:"to_user"` - Type string `json:"type"` - Amount int `json:"amount"` - ChatID int64 `json:"chat_id"` - ChatName string `json:"chat_name"` - Memo string `json:"memo"` - Success bool `json:"success"` - FromWallet string `json:"from_wallet"` - ToWallet string `json:"to_wallet"` - FromLNbitsID string `json:"from_lnbits"` - ToLNbitsID string `json:"to_lnbits"` + ID uint `gorm:"primarykey"` + Time time.Time `json:"time"` + Bot *TipBot `gorm:"-"` + From *lnbits.User `json:"from" gorm:"-"` + To *lnbits.User `json:"to" gorm:"-"` + FromId int `json:"from_id" ` + ToId int `json:"to_id" ` + FromUser string `json:"from_user"` + ToUser string `json:"to_user"` + Type string `json:"type"` + Amount int `json:"amount"` + ChatID int64 `json:"chat_id"` + ChatName string `json:"chat_name"` + Memo string `json:"memo"` + Success bool `json:"success"` + FromWallet string `json:"from_wallet"` + ToWallet string `json:"to_wallet"` + FromLNbitsID string `json:"from_lnbits"` + ToLNbitsID string `json:"to_lnbits"` } type TransactionOption func(t *Transaction) @@ -51,15 +51,15 @@ func TransactionType(transactionType string) TransactionOption { } } -func NewTransaction(bot *TipBot, from *tb.User, to *tb.User, amount int, opts ...TransactionOption) *Transaction { +func NewTransaction(bot *TipBot, from *lnbits.User, to *lnbits.User, amount int, opts ...TransactionOption) *Transaction { t := &Transaction{ Bot: bot, From: from, To: to, - FromUser: GetUserStr(from), - ToUser: GetUserStr(to), - FromId: from.ID, - ToId: to.ID, + FromUser: GetUserStr(from.Telegram), + ToUser: GetUserStr(to.Telegram), + FromId: from.Telegram.ID, + ToId: to.Telegram.ID, Amount: amount, Memo: "Powered by @LightningTipBot", Time: time.Now(), @@ -97,19 +97,12 @@ func (t *Transaction) Send() (success bool, err error) { return success, err } -func (t *Transaction) SendTransaction(bot *TipBot, from *tb.User, to *tb.User, amount int, memo string) (bool, error) { - fromUserStr := GetUserStr(from) - toUserStr := GetUserStr(to) +func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits.User, amount int, memo string) (bool, error) { + fromUserStr := GetUserStr(from.Telegram) + toUserStr := GetUserStr(to.Telegram) - // from := m.Sender - fromUser, err := GetUser(from, *bot) - if err != nil { - errmsg := fmt.Sprintf("could not get user %s", fromUserStr) - log.Errorln(errmsg) - return false, err - } - t.FromWallet = fromUser.Wallet.ID - t.FromLNbitsID = fromUser.ID + t.FromWallet = from.Wallet.ID + t.FromLNbitsID = from.ID // check if fromUser has balance balance, err := bot.GetUserBalance(from) if err != nil { @@ -124,29 +117,23 @@ func (t *Transaction) SendTransaction(bot *TipBot, from *tb.User, to *tb.User, a return false, fmt.Errorf(errmsg) } - toUser, err := GetUser(to, *bot) - if err != nil { - errmsg := fmt.Sprintf("[SendTransaction] Error: ToUser %s not found: %s", toUserStr, err) - log.Errorln(errmsg) - return false, err - } - t.ToWallet = toUser.Wallet.ID - t.ToLNbitsID = toUser.ID + t.ToWallet = to.ID + t.ToLNbitsID = to.ID // generate invoice - invoice, err := toUser.Wallet.Invoice( + invoice, err := to.Wallet.Invoice( lnbits.InvoiceParams{ Amount: int64(amount), Out: false, Memo: memo}, - *toUser.Wallet) + bot.client) if err != nil { errmsg := fmt.Sprintf("[SendTransaction] Error: Could not create invoice for user %s", toUserStr) log.Errorln(errmsg) return false, err } // pay invoice - _, err = fromUser.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoice.PaymentRequest}, *fromUser.Wallet) + _, err = from.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoice.PaymentRequest}, bot.client) if err != nil { errmsg := fmt.Sprintf("[SendTransaction] Error: Payment from %s to %s of %d sat failed", fromUserStr, toUserStr, amount) log.Errorln(errmsg) diff --git a/users.go b/users.go index d3b02b3e..b7696b02 100644 --- a/users.go +++ b/users.go @@ -15,20 +15,12 @@ import ( func SetUserState(user *lnbits.User, bot TipBot, stateKey lnbits.UserStateKey, stateData string) { user.StateKey = stateKey user.StateData = stateData - err := UpdateUserRecord(user, bot) - if err != nil { - log.Errorln(err.Error()) - return - } + bot.database.Table("users").Where("name = ?", user.Name).Update("state_key", user.StateKey).Update("state_data", user.StateData) } func ResetUserState(user *lnbits.User, bot TipBot) { user.ResetState() - err := UpdateUserRecord(user, bot) - if err != nil { - log.Errorln(err.Error()) - return - } + bot.database.Table("users").Where("name = ?", user.Name).Update("state_key", 0).Update("state_data", "") } var markdownV2Escapes = []string{"_", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"} @@ -87,34 +79,22 @@ func appendUinqueUsersToSlice(slice []*tb.User, i *tb.User) []*tb.User { return append(slice, i) } -func (bot *TipBot) UserInitializedWallet(user *tb.User) bool { - toUser, err := GetUser(user, *bot) - if err != nil { - return false - } - return toUser.Initialized -} +func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int, err error) { -func (bot *TipBot) GetUserBalance(user *tb.User) (amount int, err error) { - // get user - fromUser, err := GetUser(user, *bot) - if err != nil { - return - } - wallet, err := fromUser.Wallet.Info(*fromUser.Wallet) + wallet, err := bot.client.Info(*user.Wallet) if err != nil { - errmsg := fmt.Sprintf("[GetUserBalance] Error: Couldn't fetch user %s's info from LNbits: %s", GetUserStr(user), err) + errmsg := fmt.Sprintf("[GetUserBalance] Error: Couldn't fetch user %s's info from LNbits: %s", GetUserStr(user.Telegram), err) log.Errorln(errmsg) return } - fromUser.Wallet.Balance = wallet.Balance - err = UpdateUserRecord(fromUser, *bot) + user.Wallet.Balance = wallet.Balance + err = UpdateUserRecord(user, *bot) if err != nil { return } // msat to sat amount = int(wallet.Balance) / 1000 - log.Infof("[GetUserBalance] %s's balance: %d sat\n", GetUserStr(user), amount) + log.Infof("[GetUserBalance] %s's balance: %d sat\n", GetUserStr(user.Telegram), amount) return } @@ -125,7 +105,7 @@ func (bot *TipBot) copyLowercaseUser(u *tb.User) *tb.User { return &userCopy } -func (bot *TipBot) CreateWalletForTelegramUser(tbUser *tb.User) error { +func (bot *TipBot) CreateWalletForTelegramUser(tbUser *tb.User) (*lnbits.User, error) { userCopy := bot.copyLowercaseUser(tbUser) user := &lnbits.User{Telegram: userCopy} userStr := GetUserStr(tbUser) @@ -134,14 +114,14 @@ func (bot *TipBot) CreateWalletForTelegramUser(tbUser *tb.User) error { if err != nil { errmsg := fmt.Sprintf("[CreateWalletForTelegramUser] Error: Could not create wallet for user %s", userStr) log.Errorln(errmsg) - return err + return user, err } tx := bot.database.Save(user) if tx.Error != nil { - return tx.Error + return nil, tx.Error } log.Printf("[CreateWalletForTelegramUser] Wallet created for user %s. ", userStr) - return nil + return user, nil } func (bot *TipBot) UserExists(user *tb.User) (*lnbits.User, bool) { From 2c51662a75ec04ef7b46d72d250c05be83594658 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 22 Sep 2021 21:23:53 +0200 Subject: [PATCH 002/541] Send and pay data persistent (#72) * new lookup function * use persitant object * rename to avoid collisions * send with persistent structs * add persistant pay struct * finish up * clean comments * revert * fix ID check * less logging * error logging fixed * payment error verbose * reply to interceptor for send * reply to send command if channel --- bot.go | 12 +- database.go | 14 ++ handler.go | 83 ++++++++---- inline_faucet.go | 4 +- inline_receive.go | 4 +- inline_send.go | 25 ++-- lnurl.go | 2 +- pay.go | 253 ++++++++++++++++++++++++++++-------- photo.go | 2 +- send.go | 321 ++++++++++++++++++++++++++++++---------------- text.go | 2 +- tip.go | 2 +- transaction.go | 2 +- 13 files changed, 509 insertions(+), 217 deletions(-) diff --git a/bot.go b/bot.go index 718673ae..7b4b7f95 100644 --- a/bot.go +++ b/bot.go @@ -2,6 +2,9 @@ package main import ( "fmt" + "sync" + "time" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -9,8 +12,6 @@ import ( "gopkg.in/tucnak/telebot.v2" tb "gopkg.in/tucnak/telebot.v2" "gorm.io/gorm" - "sync" - "time" ) type TipBot struct { @@ -22,13 +23,6 @@ type TipBot struct { } var ( - paymentConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} - btnCancelPay = paymentConfirmationMenu.Data("🚫 Cancel", "cancel_pay") - btnPay = paymentConfirmationMenu.Data("✅ Pay", "confirm_pay") - sendConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} - btnCancelSend = sendConfirmationMenu.Data("🚫 Cancel", "cancel_send") - btnSend = sendConfirmationMenu.Data("✅ Send", "confirm_send") - botWalletInitialisation = sync.Once{} telegramHandlerRegistration = sync.Once{} ) diff --git a/database.go b/database.go index 896048bc..6ebf3916 100644 --- a/database.go +++ b/database.go @@ -4,6 +4,7 @@ import ( "fmt" "reflect" "strconv" + "strings" log "github.com/sirupsen/logrus" @@ -35,6 +36,19 @@ func migration() (db *gorm.DB, txLogger *gorm.DB) { return orm, txLogger } +func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.User, error) { + toUserDb := &lnbits.User{} + tx := bot.database.Where("telegram_username = ?", strings.ToLower(toUserStrWithoutAt)).First(toUserDb) + if tx.Error != nil || toUserDb.Wallet == nil { + err := tx.Error + if toUserDb.Wallet == nil { + err = fmt.Errorf("%s | user @%s has no wallet", tx.Error, toUserStrWithoutAt) + } + return nil, err + } + return toUserDb, nil +} + // GetUser from telegram user. Update the user if user information changed. func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { user := &lnbits.User{Name: strconv.Itoa(u.ID)} diff --git a/handler.go b/handler.go index 47e51244..f979c1fa 100644 --- a/handler.go +++ b/handler.go @@ -3,9 +3,10 @@ package main import ( "context" "fmt" + "strings" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" tb "gopkg.in/tucnak/telebot.v2" - "strings" ) type Handler struct { @@ -101,79 +102,109 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.logMessageInterceptor, bot.loadUserInterceptor, bot.loadReplyToInterceptor, }}, }, { Endpoints: []interface{}{"/pay"}, - Handler: bot.confirmPaymentHandler, + Handler: bot.payHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor, + }}, }, { Endpoints: []interface{}{"/invoice"}, Handler: bot.invoiceHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor, + }}, }, { Endpoints: []interface{}{"/balance"}, Handler: bot.balanceHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor, + }}, }, { Endpoints: []interface{}{"/send"}, - Handler: bot.confirmSendHandler, + Handler: bot.sendHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor, + bot.loadReplyToInterceptor, + }}, }, { Endpoints: []interface{}{"/help"}, Handler: bot.helpHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor, + }}, }, { Endpoints: []interface{}{"/basics"}, Handler: bot.basicsHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor, + }}, }, { Endpoints: []interface{}{"/donate"}, Handler: bot.donationHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor, + }}, }, { Endpoints: []interface{}{"/advanced"}, Handler: bot.advancedHelpHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor, + }}, }, { Endpoints: []interface{}{"/link"}, Handler: bot.lndhubHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor}}, }, { Endpoints: []interface{}{"/lnurl"}, Handler: bot.lnurlHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor}}, }, { Endpoints: []interface{}{tb.OnPhoto}, @@ -191,8 +222,8 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.logMessageInterceptor, bot.requirePrivateChatInterceptor, + bot.logMessageInterceptor, // Log message only if private chat bot.loadUserInterceptor, }}, }, @@ -209,7 +240,7 @@ func (bot TipBot) getHandler() []Handler { }, { Endpoints: []interface{}{&btnPay}, - Handler: bot.payHandler, + Handler: bot.confirmPayHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{bot.loadUserInterceptor}}, @@ -223,7 +254,7 @@ func (bot TipBot) getHandler() []Handler { }, { Endpoints: []interface{}{&btnSend}, - Handler: bot.sendHandler, + Handler: bot.confirmSendHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{bot.loadUserInterceptor}}, diff --git a/inline_faucet.go b/inline_faucet.go index 69f66b57..1e7cfa58 100644 --- a/inline_faucet.go +++ b/inline_faucet.go @@ -173,7 +173,7 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { } // check if fromUser has balance if balance < inlineFaucet.Amount { - log.Errorln("Balance of user %s too low", fromUserStr) + log.Errorf("Balance of user %s too low", fromUserStr) bot.trySendMessage(m.Sender, fmt.Sprintf(inlineSendBalanceLowMessage, balance)) bot.tryDeleteMessage(m) return @@ -241,7 +241,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { } // check if fromUser has balance if balance < inlineFaucet.Amount { - log.Errorln("Balance of user %s too low", fromUserStr) + log.Errorf("Balance of user %s too low", fromUserStr) bot.inlineQueryReplyWithError(q, fmt.Sprintf(inlineSendBalanceLowMessage, balance), fmt.Sprintf(inlineQueryFaucetDescription, bot.telegram.Me.Username)) return } diff --git a/inline_receive.go b/inline_receive.go index 3138508a..97c565c1 100644 --- a/inline_receive.go +++ b/inline_receive.go @@ -211,7 +211,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac toUserStr := GetUserStr(to.Telegram) fromUserStr := GetUserStr(from.Telegram) - if from.ID == to.ID { + if from.Telegram.ID == to.Telegram.ID { bot.trySendMessage(from.Telegram, sendYourselfMessage) return } @@ -224,7 +224,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac } // check if fromUser has balance if balance < inlineReceive.Amount { - log.Errorln("[acceptInlineReceiveHandler] balance of user %s too low", fromUserStr) + log.Errorf("[acceptInlineReceiveHandler] balance of user %s too low", fromUserStr) bot.trySendMessage(from.Telegram, fmt.Sprintf(inlineSendBalanceLowMessage, balance)) return } diff --git a/inline_send.go b/inline_send.go index 3dd60702..471c5e14 100644 --- a/inline_send.go +++ b/inline_send.go @@ -23,14 +23,17 @@ const ( inlineSendBalanceLowMessage = "🚫 Your balance is too low (👑 %d sat)." ) -var ( +const ( inlineQuerySendTitle = "💸 Send payment to a chat." inlineQuerySendDescription = "Usage: @%s send []" inlineResultSendTitle = "💸 Send %d sat." inlineResultSendDescription = "👉 Click to send %d sat to this chat." - inlineSendMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} - btnCancelInlineSend = inlineSendMenu.Data("🚫 Cancel", "cancel_send_inline") - btnAcceptInlineSend = inlineSendMenu.Data("✅ Receive", "confirm_send_inline") +) + +var ( + inlineSendMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + btnCancelInlineSend = inlineSendMenu.Data("🚫 Cancel", "cancel_send_inline") + btnAcceptInlineSend = inlineSendMenu.Data("✅ Receive", "confirm_send_inline") ) type InlineSend struct { @@ -58,7 +61,7 @@ func (msg InlineSend) Key() string { return msg.ID } -func (bot *TipBot) LockSend(tx *InlineSend) error { +func (bot *TipBot) LockInlineSend(tx *InlineSend) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = true err := bot.bunt.Set(tx) @@ -68,7 +71,7 @@ func (bot *TipBot) LockSend(tx *InlineSend) error { return nil } -func (bot *TipBot) ReleaseSend(tx *InlineSend) error { +func (bot *TipBot) ReleaseInlineSend(tx *InlineSend) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = false err := bot.bunt.Set(tx) @@ -78,7 +81,7 @@ func (bot *TipBot) ReleaseSend(tx *InlineSend) error { return nil } -func (bot *TipBot) inactivateSend(tx *InlineSend) error { +func (bot *TipBot) InactivateInlineSend(tx *InlineSend) error { tx.Active = false err := bot.bunt.Set(tx) if err != nil { @@ -137,7 +140,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { } // check if fromUser has balance if balance < inlineSend.Amount { - log.Errorln("Balance of user %s too low", fromUserStr) + log.Errorf("Balance of user %s too low", fromUserStr) bot.inlineQueryReplyWithError(q, fmt.Sprintf(inlineSendBalanceLowMessage, balance), fmt.Sprintf(inlineQuerySendDescription, bot.telegram.Me.Username)) return } @@ -204,7 +207,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) } fromUser := inlineSend.From // immediatelly set intransaction to block duplicate calls - err = bot.LockSend(inlineSend) + err = bot.LockInlineSend(inlineSend) if err != nil { log.Errorf("[getInlineSend] %s", err) return @@ -214,7 +217,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) return } - defer bot.ReleaseSend(inlineSend) + defer bot.ReleaseInlineSend(inlineSend) amount := inlineSend.Amount @@ -242,7 +245,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) } } // set inactive to avoid double-sends - bot.inactivateSend(inlineSend) + bot.InactivateInlineSend(inlineSend) // todo: user new get username function to get userStrings transactionMemo := fmt.Sprintf("Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount) diff --git a/lnurl.go b/lnurl.go index dcf7a03f..4f718b86 100644 --- a/lnurl.go +++ b/lnurl.go @@ -258,7 +258,7 @@ func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { } bot.telegram.Delete(msg) c.Text = fmt.Sprintf("/pay %s", response2.PR) - bot.confirmPaymentHandler(ctx, c) + bot.payHandler(ctx, c) } } diff --git a/pay.go b/pay.go index 62f3b0af..e74a7dea 100644 --- a/pay.go +++ b/pay.go @@ -4,30 +4,40 @@ import ( "context" "fmt" "strings" + "time" log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" decodepay "github.com/fiatjaf/ln-decodepay" tb "gopkg.in/tucnak/telebot.v2" ) const ( - paymentCancelledMessage = "🚫 Payment cancelled." - invoicePaidMessage = "⚡️ Payment sent." - invoicePrivateChatOnlyErrorMessage = "You can pay invoices only in the private chat with the bot." - invalidInvoiceHelpMessage = "Did you enter a valid Lightning invoice? Try /send if you want to send to a Telegram user or Lightning address." - invoiceNoAmountMessage = "🚫 Can't pay invoices without an amount." - insufficientFundsMessage = "🚫 Insufficient funds. You have %d sat but you need at least %d sat." - feeReserveMessage = "⚠️ Sending your entire balance might fail because of network fees. If it fails, try sending a bit less." - invoicePaymentFailedMessage = "🚫 Payment failed: %s" - confirmPayInvoiceMessage = "Do you want to send this payment?\n\n💸 Amount: %d sat" - confirmPayAppendMemo = "\n✉️ %s" - payHelpText = "📖 Oops, that didn't work. %s\n\n" + + paymentCancelledMessage = "🚫 Payment cancelled." + invoicePaidMessage = "⚡️ Payment sent." + invoicePublicPaidMessage = "⚡️ Payment sent by %s." + // invoicePrivateChatOnlyErrorMessage = "You can pay invoices only in the private chat with the bot." + invalidInvoiceHelpMessage = "Did you enter a valid Lightning invoice? Try /send if you want to send to a Telegram user or Lightning address." + invoiceNoAmountMessage = "🚫 Can't pay invoices without an amount." + insufficientFundsMessage = "🚫 Insufficient funds. You have %d sat but you need at least %d sat." + feeReserveMessage = "⚠️ Sending your entire balance might fail because of network fees. If it fails, try sending a bit less." + invoicePaymentFailedMessage = "🚫 Payment failed: %s" + invoiceUndefinedErrorMessage = "Could not pay invoice." + confirmPayInvoiceMessage = "Do you want to send this payment?\n\n💸 Amount: %d sat" + confirmPayAppendMemo = "\n✉️ %s" + payHelpText = "📖 Oops, that didn't work. %s\n\n" + "*Usage:* `/pay `\n" + "*Example:* `/pay lnbc20n1psscehd...`" ) +var ( + paymentConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + btnCancelPay = paymentConfirmationMenu.Data("🚫 Cancel", "cancel_pay") + btnPay = paymentConfirmationMenu.Data("✅ Pay", "confirm_pay") +) + func helpPayInvoiceUsage(errormsg string) string { if len(errormsg) > 0 { return fmt.Sprintf(payHelpText, fmt.Sprintf("%s", errormsg)) @@ -36,8 +46,90 @@ func helpPayInvoiceUsage(errormsg string) string { } } -// confirmPaymentHandler invoked on "/pay lnbc..." command -func (bot TipBot) confirmPaymentHandler(ctx context.Context, m *tb.Message) { +type PayData struct { + From *lnbits.User `json:"from"` + ID string `json:"id"` + Invoice string `json:"invoice"` + Hash string `json:"hash"` + Proof string `json:"proof"` + Memo string `json:"memo"` + Message string `json:"message"` + Amount int64 `json:"amount"` + InTransaction bool `json:"intransaction"` + Active bool `json:"active"` +} + +func NewPay() *PayData { + payData := &PayData{ + Active: true, + InTransaction: false, + } + return payData +} + +func (msg PayData) Key() string { + return msg.ID +} + +func (bot *TipBot) LockPay(tx *PayData) error { + // immediatelly set intransaction to block duplicate calls + tx.InTransaction = true + err := bot.bunt.Set(tx) + if err != nil { + return err + } + return nil +} + +func (bot *TipBot) ReleasePay(tx *PayData) error { + // immediatelly set intransaction to block duplicate calls + tx.InTransaction = false + err := bot.bunt.Set(tx) + if err != nil { + return err + } + return nil +} + +func (bot *TipBot) InactivatePay(tx *PayData) error { + tx.Active = false + err := bot.bunt.Set(tx) + if err != nil { + return err + } + return nil +} + +func (bot *TipBot) getPay(c *tb.Callback) (*PayData, error) { + payData := NewPay() + payData.ID = c.Data + + err := bot.bunt.Get(payData) + + // to avoid race conditions, we block the call if there is + // already an active transaction by loop until InTransaction is false + ticker := time.NewTicker(time.Second * 10) + + for payData.InTransaction { + select { + case <-ticker.C: + return nil, fmt.Errorf("pay timeout") + default: + log.Infoln("[pay] in transaction") + time.Sleep(time.Duration(500) * time.Millisecond) + err = bot.bunt.Get(payData) + } + } + if err != nil { + return nil, fmt.Errorf("could not get payData") + } + + return payData, nil + +} + +// payHandler invoked on "/pay lnbc..." command +func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { // check and print all commands bot.anyTextHandler(ctx, m) user := LoadUser(ctx) @@ -45,12 +137,12 @@ func (bot TipBot) confirmPaymentHandler(ctx context.Context, m *tb.Message) { return } - if m.Chat.Type != tb.ChatPrivate { - // delete message - NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(m.Sender, helpPayInvoiceUsage(invoicePrivateChatOnlyErrorMessage)) - return - } + // if m.Chat.Type != tb.ChatPrivate { + // // delete message + // NewMessage(m, WithDuration(0, bot.telegram)) + // bot.trySendMessage(m.Sender, helpPayInvoiceUsage(invoicePrivateChatOnlyErrorMessage)) + // return + // } if len(strings.Split(m.Text, " ")) < 2 { NewMessage(m, WithDuration(0, bot.telegram)) bot.trySendMessage(m.Sender, helpPayInvoiceUsage("")) @@ -104,61 +196,118 @@ func (bot TipBot) confirmPaymentHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(m.Sender, feeReserveMessage) } + confirmText := fmt.Sprintf(confirmPayInvoiceMessage, amount) + if len(bolt11.Description) > 0 { + confirmText = confirmText + fmt.Sprintf(confirmPayAppendMemo, MarkdownEscape(bolt11.Description)) + } + log.Printf("[/pay] User: %s, amount: %d sat.", userStr, amount) + // object that holds all information about the send payment + id := fmt.Sprintf("pay-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) + payData := PayData{ + From: user, + Invoice: paymentRequest, + Active: true, + InTransaction: false, + ID: id, + Amount: int64(amount), + Memo: bolt11.Description, + Message: confirmText, + } + // add result to persistent struct + runtime.IgnoreError(bot.bunt.Set(payData)) + SetUserState(user, bot, lnbits.UserStateConfirmPayment, paymentRequest) // // // create inline buttons + btnPay.Data = id + btnCancelPay.Data = id paymentConfirmationMenu.Inline(paymentConfirmationMenu.Row(btnPay, btnCancelPay)) - confirmText := fmt.Sprintf(confirmPayInvoiceMessage, amount) - if len(bolt11.Description) > 0 { - confirmText = confirmText + fmt.Sprintf(confirmPayAppendMemo, MarkdownEscape(bolt11.Description)) - } - bot.trySendMessage(m.Sender, confirmText, paymentConfirmationMenu) + bot.trySendMessage(m.Chat, confirmText, paymentConfirmationMenu) } -// cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { - // reset state immediately - user := LoadUser(ctx) - - ResetUserState(user, bot) - - bot.tryDeleteMessage(c.Message) - _, err := bot.telegram.Send(c.Sender, paymentCancelledMessage) +// confirmPayHandler when user clicked pay on payment confirmation +func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { + payData, err := bot.getPay(c) + if err != nil { + log.Errorf("[acceptSendHandler] %s", err) + return + } + // onnly the correct user can press + if payData.From.Telegram.ID != c.Sender.ID { + return + } + // immediatelly set intransaction to block duplicate calls + err = bot.LockPay(payData) if err != nil { - log.WithField("message", paymentCancelledMessage).WithField("user", c.Sender.ID).Printf("[Send] %s", err.Error()) + log.Errorf("[acceptSendHandler] %s", err) + bot.tryDeleteMessage(c.Message) + return + } + if !payData.Active { + log.Errorf("[acceptSendHandler] send not active anymore") + bot.tryDeleteMessage(c.Message) return } + defer bot.ReleasePay(payData) -} + // remove buttons from confirmation message + // bot.tryEditMessage(c.Message, MarkdownEscape(payData.Message), &tb.ReplyMarkup{}) -// payHandler when user clicked pay "X" on payment confirmation -func (bot TipBot) payHandler(ctx context.Context, c *tb.Callback) { - bot.tryEditMessage(c.Message, c.Message.Text, &tb.ReplyMarkup{}) user := LoadUser(ctx) if user.Wallet == nil { + bot.tryDeleteMessage(c.Message) return } - if user.StateKey == lnbits.UserStateConfirmPayment { - invoiceString := user.StateData + invoiceString := payData.Invoice - // reset state immediately - ResetUserState(user, bot) + // reset state immediately + ResetUserState(user, bot) - userStr := GetUserStr(c.Sender) - // pay invoice - invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoiceString}, bot.client) - if err != nil { - errmsg := fmt.Sprintf("[/pay] Could not pay invoice of user %s: %s", userStr, err) - bot.trySendMessage(c.Sender, fmt.Sprintf(invoicePaymentFailedMessage, err)) - log.Errorln(errmsg) - return + userStr := GetUserStr(c.Sender) + // pay invoice + invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoiceString}, bot.client) + if err != nil { + errmsg := fmt.Sprintf("[/pay] Could not pay invoice of user %s: %s", userStr, err) + if len(err.Error()) == 0 { + err = fmt.Errorf(invoiceUndefinedErrorMessage) } - bot.trySendMessage(c.Sender, invoicePaidMessage) - log.Printf("[/pay] User %s paid invoice %s", userStr, invoice.PaymentHash) + // bot.trySendMessage(c.Sender, fmt.Sprintf(invoicePaymentFailedMessage, err)) + bot.tryEditMessage(c.Message, fmt.Sprintf(invoicePaymentFailedMessage, err), &tb.ReplyMarkup{}) + log.Errorln(errmsg) return } + payData.Hash = invoice.PaymentHash + payData.InTransaction = false + + if c.Message.Private() { + bot.tryEditMessage(c.Message, invoicePaidMessage, &tb.ReplyMarkup{}) + } else { + bot.trySendMessage(c.Sender, invoicePaidMessage) + bot.tryEditMessage(c.Message, fmt.Sprintf(invoicePublicPaidMessage, userStr), &tb.ReplyMarkup{}) + } + log.Printf("[/pay] User %s paid invoice %s", userStr, invoice.PaymentHash) + return +} + +// cancelPaymentHandler invoked when user clicked cancel on payment confirmation +func (bot TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { + // reset state immediately + user := LoadUser(ctx) + ResetUserState(user, bot) + payData, err := bot.getPay(c) + if err != nil { + log.Errorf("[acceptSendHandler] %s", err) + return + } + // onnly the correct user can press + if payData.From.Telegram.ID != c.Sender.ID { + return + } + bot.tryEditMessage(c.Message, paymentCancelledMessage, &tb.ReplyMarkup{}) + payData.InTransaction = false + bot.InactivatePay(payData) } diff --git a/photo.go b/photo.go index 98b53a95..88b00ed3 100644 --- a/photo.go +++ b/photo.go @@ -70,7 +70,7 @@ func (bot TipBot) photoHandler(ctx context.Context, m *tb.Message) { // invoke payment handler if lightning.IsInvoice(data.String()) { m.Text = fmt.Sprintf("/pay %s", data.String()) - bot.confirmPaymentHandler(ctx, m) + bot.payHandler(ctx, m) return } else if lightning.IsLnurl(data.String()) { m.Text = fmt.Sprintf("/lnurl %s", data.String()) diff --git a/send.go b/send.go index 82c1a666..c45e000e 100644 --- a/send.go +++ b/send.go @@ -2,11 +2,13 @@ package main import ( "context" + "encoding/json" "fmt" - "strconv" "strings" + "time" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" @@ -14,13 +16,12 @@ import ( const ( sendValidAmountMessage = "Did you enter a valid amount?" - sendUserNotFoundMessage = "User %s could not be found. You can /send only to Telegram tags like @%s." - sendIsNotAUsser = "🚫 %s is not a username. You can /send only to Telegram tags like @%s." sendUserHasNoWalletMessage = "🚫 User %s hasn't created a wallet yet." sendSentMessage = "💸 %d sat sent to %s." + sendPublicSentMessage = "💸 %d sat sent from %s to %s." sendReceivedMessage = "🏅 %s sent you %d sat." sendErrorMessage = "🚫 Send failed." - confirmSendInvoiceMessage = "Do you want to pay to %s?\n\n💸 Amount: %d sat" + confirmSendMessage = "Do you want to pay to %s?\n\n💸 Amount: %d sat" confirmSendAppendMemo = "\n✉️ %s" sendCancelledMessage = "🚫 Send cancelled." errorTryLaterMessage = "🚫 Error. Please try again later." @@ -30,6 +31,12 @@ const ( "*Example:* `/send 1234 LightningTipBot@ln.tips`" ) +var ( + sendConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + btnCancelSend = sendConfirmationMenu.Data("🚫 Cancel", "cancel_send") + btnSend = sendConfirmationMenu.Data("✅ Send", "confirm_send") +) + func helpSendUsage(errormsg string) string { if len(errormsg) > 0 { return fmt.Sprintf(sendHelpText, fmt.Sprintf("%s", errormsg)) @@ -43,17 +50,92 @@ func (bot *TipBot) SendCheckSyntax(m *tb.Message) (bool, string) { if len(arguments) < 2 { return false, fmt.Sprintf("Did you enter an amount and a recipient? You can use the /send command to either send to Telegram users like %s or to a Lightning address like LightningTipBot@ln.tips.", GetUserStrMd(bot.telegram.Me)) } - // if len(arguments) < 3 { - // return false, "Did you enter a recipient?" - // } - // if !strings.HasPrefix(arguments[0], "/send") { - // return false, "Did you enter a valid command?" - // } return true, "" } -// confirmPaymentHandler invoked on "/send 123 @user" command -func (bot *TipBot) confirmSendHandler(ctx context.Context, m *tb.Message) { +type SendData struct { + ID string `json:"id"` + From *lnbits.User `json:"from"` + ToTelegramId int `json:"to_telegram_id"` + ToTelegramUser string `json:"to_telegram_user"` + Memo string `json:"memo"` + Message string `json:"message"` + Amount int64 `json:"amount"` + InTransaction bool `json:"intransaction"` + Active bool `json:"active"` +} + +func NewSend() *SendData { + sendData := &SendData{ + Active: true, + InTransaction: false, + } + return sendData +} + +func (msg SendData) Key() string { + return msg.ID +} + +func (bot *TipBot) LockSend(tx *SendData) error { + // immediatelly set intransaction to block duplicate calls + tx.InTransaction = true + err := bot.bunt.Set(tx) + if err != nil { + return err + } + return nil +} + +func (bot *TipBot) ReleaseSend(tx *SendData) error { + // immediatelly set intransaction to block duplicate calls + tx.InTransaction = false + err := bot.bunt.Set(tx) + if err != nil { + return err + } + return nil +} + +func (bot *TipBot) InactivateSend(tx *SendData) error { + tx.Active = false + err := bot.bunt.Set(tx) + if err != nil { + return err + } + return nil +} + +func (bot *TipBot) getSend(c *tb.Callback) (*SendData, error) { + sendData := NewSend() + sendData.ID = c.Data + + err := bot.bunt.Get(sendData) + + // to avoid race conditions, we block the call if there is + // already an active transaction by loop until InTransaction is false + ticker := time.NewTicker(time.Second * 10) + + for sendData.InTransaction { + select { + case <-ticker.C: + return nil, fmt.Errorf("send timeout") + default: + log.Infoln("[send] in transaction") + time.Sleep(time.Duration(500) * time.Millisecond) + err = bot.bunt.Get(sendData) + } + } + if err != nil { + return nil, fmt.Errorf("could not get sendData") + } + + return sendData, nil + +} + +// sendHandler invoked on "/send 123 @user" command +func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) user := LoadUser(ctx) if user.Wallet == nil { @@ -118,142 +200,123 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, m *tb.Message) { // check for memo in command sendMemo := GetMemoFromCommand(m.Text, 3) - if len(m.Entities) < 2 { - arg, err := getArgumentFromCommand(m.Text, 2) - if err != nil { - log.Errorln(err.Error()) - return - } - arg = MarkdownEscape(arg) - NewMessage(m, WithDuration(0, bot.telegram)) - errmsg := fmt.Sprintf("Error: User %s could not be found", arg) - bot.trySendMessage(m.Sender, helpSendUsage(fmt.Sprintf(sendUserNotFoundMessage, arg, bot.telegram.Me.Username))) - log.Errorln(errmsg) + toUserStrMention := "" + toUserStrWithoutAt := "" - return - } - if m.Entities[1].Type != "mention" { - arg, err := getArgumentFromCommand(m.Text, 2) + // check for user in command, accepts user mention or plan username without @ + if len(m.Entities) > 1 && m.Entities[1].Type == "mention" { + toUserStrMention = m.Text[m.Entities[1].Offset : m.Entities[1].Offset+m.Entities[1].Length] + toUserStrWithoutAt = strings.TrimPrefix(toUserStrMention, "@") + } else { + toUserStrWithoutAt, err = getArgumentFromCommand(m.Text, 2) if err != nil { - NewMessage(m, WithDuration(0, bot.telegram)) log.Errorln(err.Error()) return } - arg = MarkdownEscape(arg) - NewMessage(m, WithDuration(0, bot.telegram)) - errmsg := fmt.Sprintf("Error: %s is not a user", arg) - bot.trySendMessage(m.Sender, fmt.Sprintf(sendIsNotAUsser, arg, bot.telegram.Me.Username)) - log.Errorln(errmsg) - return + toUserStrMention = "@" + toUserStrWithoutAt + toUserStrWithoutAt = strings.TrimPrefix(toUserStrWithoutAt, "@") } - toUserStrMention := m.Text[m.Entities[1].Offset : m.Entities[1].Offset+m.Entities[1].Length] - toUserStrWithoutAt := strings.TrimPrefix(toUserStrMention, "@") - err = bot.parseCmdDonHandler(ctx, m) if err == nil { return } - toUserDb := &lnbits.User{} - tx := bot.database.Where("telegram_username = ?", strings.ToLower(toUserStrWithoutAt)).First(toUserDb) - if tx.Error != nil || toUserDb.Wallet == nil { + toUserDb, err := GetUserByTelegramUsername(toUserStrWithoutAt, *bot) + if err != nil { NewMessage(m, WithDuration(0, bot.telegram)) - err = fmt.Errorf(sendUserHasNoWalletMessage, MarkdownEscape(toUserStrMention)) - bot.trySendMessage(m.Sender, err.Error()) - if tx.Error != nil { - log.Printf("[/send] Error: %v %v", err, tx.Error) - return - } - log.Printf("[/send] Error: %v", err) + bot.trySendMessage(m.Sender, fmt.Sprintf(sendUserHasNoWalletMessage, toUserStrMention)) return } - // string that holds all information about the send payment - sendData := strconv.Itoa(toUserDb.Telegram.ID) + "|" + toUserStrWithoutAt + "|" + - strconv.Itoa(amount) - if len(sendMemo) > 0 { - sendData = sendData + "|" + sendMemo - } - // save the send data to the database - log.Debug(sendData) - - SetUserState(user, *bot, lnbits.UserStateConfirmSend, sendData) - - sendConfirmationMenu.Inline(sendConfirmationMenu.Row(btnSend, btnCancelSend)) - confirmText := fmt.Sprintf(confirmSendInvoiceMessage, MarkdownEscape(toUserStrMention), amount) + // entire text of the inline object + confirmText := fmt.Sprintf(confirmSendMessage, MarkdownEscape(toUserStrMention), amount) if len(sendMemo) > 0 { confirmText = confirmText + fmt.Sprintf(confirmSendAppendMemo, MarkdownEscape(sendMemo)) } - _, err = bot.telegram.Send(m.Sender, confirmText, sendConfirmationMenu) + // object that holds all information about the send payment + id := fmt.Sprintf("send-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) + sendData := SendData{ + From: user, + Active: true, + InTransaction: false, + ID: id, + Amount: int64(amount), + ToTelegramId: toUserDb.Telegram.ID, + ToTelegramUser: toUserStrWithoutAt, + Memo: sendMemo, + Message: confirmText, + } + // add result to persistent struct + runtime.IgnoreError(bot.bunt.Set(sendData)) + + sendDataJson, err := json.Marshal(sendData) if err != nil { - log.Error("[confirmSendHandler]" + err.Error()) + NewMessage(m, WithDuration(0, bot.telegram)) + log.Printf("[/send] Error: %s\n", err.Error()) + bot.trySendMessage(m.Sender, fmt.Sprint(errorTryLaterMessage)) return } -} + // save the send data to the database + // log.Debug(sendData) + SetUserState(user, *bot, lnbits.UserStateConfirmSend, string(sendDataJson)) -// cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { - // reset state immediately - user := LoadUser(ctx) - ResetUserState(user, *bot) + btnSend.Data = id + btnCancelSend.Data = id - // delete the confirmation message - err := bot.telegram.Delete(c.Message) - if err != nil { - log.Errorln("[cancelSendHandler] " + err.Error()) - } - // notify the user - _, err = bot.telegram.Send(c.Sender, sendCancelledMessage) - if err != nil { - log.WithField("message", sendCancelledMessage).WithField("user", c.Sender.ID).Printf("[Send] %s", err.Error()) - return + sendConfirmationMenu.Inline(sendConfirmationMenu.Row(btnSend, btnCancelSend)) + + if m.Private() { + bot.trySendMessage(m.Chat, confirmText, sendConfirmationMenu) + } else { + bot.tryReplyMessage(m, confirmText, sendConfirmationMenu) } } // sendHandler invoked when user clicked send on payment confirmation -func (bot *TipBot) sendHandler(ctx context.Context, c *tb.Callback) { - // remove buttons from confirmation message - _, err := bot.telegram.Edit(c.Message, MarkdownEscape(c.Message.Text), &tb.ReplyMarkup{}) +func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { + sendData, err := bot.getSend(c) if err != nil { - log.Errorln("[sendHandler] " + err.Error()) - } - // decode callback data - // log.Debug("[sendHandler] Callback: %s", c.Data) - from := LoadUser(ctx) - if from.StateKey != lnbits.UserStateConfirmSend { - log.Errorf("[sendHandler] User StateKey does not match! User: %d: StateKey: %d", c.Sender.ID, from.StateKey) + log.Errorf("[acceptSendHandler] %s", err) return } - - // decode StateData in which we have information about the send payment - splits := strings.Split(from.StateData, "|") - if len(splits) < 3 { - log.Error("[sendHandler] Not enough arguments in callback data") - log.Errorf("user.StateData: %s", from.StateData) + // onnly the correct user can press + if sendData.From.Telegram.ID != c.Sender.ID { return } - toId, err := strconv.Atoi(splits[0]) + // immediatelly set intransaction to block duplicate calls + err = bot.LockSend(sendData) if err != nil { - log.Errorln("[sendHandler] " + err.Error()) - } - toUserStrWithoutAt := splits[1] - amount, err := strconv.Atoi(splits[2]) - if err != nil { - log.Errorln("[sendHandler] " + err.Error()) + log.Errorf("[acceptSendHandler] %s", err) + bot.tryDeleteMessage(c.Message) + return } - sendMemo := "" - if len(splits) > 3 { - sendMemo = strings.Join(splits[3:], "|") + if !sendData.Active { + log.Errorf("[acceptSendHandler] send not active anymore") + bot.tryDeleteMessage(c.Message) + return } + defer bot.ReleaseSend(sendData) - // reset state - ResetUserState(from, *bot) + // // remove buttons from confirmation message + // bot.tryEditMessage(c.Message, MarkdownEscape(sendData.Message), &tb.ReplyMarkup{}) + + // decode callback data + // log.Debug("[send] Callback: %s", c.Data) + from := LoadUser(ctx) + ResetUserState(from, *bot) // we don't need to check the statekey anymore like we did earlier + + // information about the send + toId := sendData.ToTelegramId + toUserStrWithoutAt := sendData.ToTelegramUser + amount := sendData.Amount + sendMemo := sendData.Memo // we can now get the wallets of both users to, err := GetUser(&tb.User{ID: toId, Username: toUserStrWithoutAt}, *bot) if err != nil { log.Errorln(err.Error()) + bot.tryDeleteMessage(c.Message) return } toUserStrMd := GetUserStrMd(to.Telegram) @@ -262,20 +325,28 @@ func (bot *TipBot) sendHandler(ctx context.Context, c *tb.Callback) { fromUserStr := GetUserStr(from.Telegram) transactionMemo := fmt.Sprintf("Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount) - t := NewTransaction(bot, from, to, amount, TransactionType("send")) + t := NewTransaction(bot, from, to, int(amount), TransactionType("send")) t.Memo = transactionMemo success, err := t.Send() if !success || err != nil { - // NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(c.Sender, sendErrorMessage) + // bot.trySendMessage(c.Sender, sendErrorMessage) errmsg := fmt.Sprintf("[/send] Error: Transaction failed. %s", err) log.Errorln(errmsg) + bot.tryEditMessage(c.Message, sendErrorMessage, &tb.ReplyMarkup{}) return } - bot.trySendMessage(from.Telegram, fmt.Sprintf(sendSentMessage, amount, toUserStrMd)) + sendData.InTransaction = false + bot.trySendMessage(to.Telegram, fmt.Sprintf(sendReceivedMessage, fromUserStrMd, amount)) + // bot.trySendMessage(from.Telegram, fmt.Sprintf(sendSentMessage, amount, toUserStrMd)) + if c.Message.Private() { + bot.tryEditMessage(c.Message, fmt.Sprintf(sendSentMessage, amount, toUserStrMd), &tb.ReplyMarkup{}) + } else { + bot.trySendMessage(c.Sender, fmt.Sprintf(sendSentMessage, amount, toUserStrMd)) + bot.tryEditMessage(c.Message, fmt.Sprintf(sendPublicSentMessage, amount, fromUserStrMd, toUserStrMd), &tb.ReplyMarkup{}) + } // send memo if it was present if len(sendMemo) > 0 { bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", MarkdownEscape(sendMemo))) @@ -283,3 +354,33 @@ func (bot *TipBot) sendHandler(ctx context.Context, c *tb.Callback) { return } + +// cancelPaymentHandler invoked when user clicked cancel on payment confirmation +func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { + // reset state immediately + user := LoadUser(ctx) + ResetUserState(user, *bot) + sendData, err := bot.getSend(c) + if err != nil { + log.Errorf("[acceptSendHandler] %s", err) + return + } + // onnly the correct user can press + if sendData.From.Telegram.ID != c.Sender.ID { + return + } + // remove buttons from confirmation message + bot.tryEditMessage(c.Message, sendCancelledMessage, &tb.ReplyMarkup{}) + sendData.InTransaction = false + bot.InactivateSend(sendData) + // // delete the confirmation message + // bot.tryDeleteMessage(c.Message) + // // notify the user + // bot.trySendMessage(c.Sender, sendCancelledMessage) + + // // set the inlineSend inactive + // sendData.Active = false + // sendData.InTransaction = false + // runtime.IgnoreError(bot.bunt.Set(sendData)) + +} diff --git a/text.go b/text.go index 38cf8376..81e9afdd 100644 --- a/text.go +++ b/text.go @@ -29,7 +29,7 @@ func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { anyText := strings.ToLower(m.Text) if lightning.IsInvoice(anyText) { m.Text = "/pay " + anyText - bot.confirmPaymentHandler(ctx, m) + bot.payHandler(ctx, m) return } if lightning.IsLnurl(anyText) { diff --git a/tip.go b/tip.go index e25cb43b..8fb9529b 100644 --- a/tip.go +++ b/tip.go @@ -85,7 +85,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { to := LoadReplyToUser(ctx) - if from.ID == to.ID { + if from.Telegram.ID == to.Telegram.ID { NewMessage(m, WithDuration(0, bot.telegram)) bot.trySendMessage(m.Sender, tipYourselfMessage) return diff --git a/transaction.go b/transaction.go index 5c337e8e..c74e9993 100644 --- a/transaction.go +++ b/transaction.go @@ -113,7 +113,7 @@ func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits // check if fromUser has balance if balance < amount { errmsg := fmt.Sprintf(balanceTooLowMessage) - log.Errorln("Balance of user %s too low", fromUserStr) + log.Errorf("Balance of user %s too low", fromUserStr) return false, fmt.Errorf(errmsg) } From 167b581d3efad2e6507256929c8801aaf9891443 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Sep 2021 14:07:29 +0200 Subject: [PATCH 003/541] translations (#71) * squash * balance too low message * add context to start * add translations * translation readme * translations for all * equalize * new faucet empty message * commands * localize * add more commands * adjust * add language code * add languageCode context * cancel handler with struct language * inline objects use their language * button translations * send language * add GetLnbitsUser * different localizers * fix publicLanguageCode * translate balance and donate * whatever bro * translateUser * pull bro * asd * Create nl.toml (#74) * Create nl.toml * Update nl.toml * I18n (#75) * Whatever brah * Update es.toml * Whatever brah * Update nl.toml (#76) * clean comments * fix amount Co-authored-by: lngohumble Co-authored-by: maxmerlin78 <91363235+maxmerlin78@users.noreply.github.com> Co-authored-by: Koty <89604768+kotyauditore@users.noreply.github.com> --- balance.go | 11 +- bot.go | 4 + database.go | 16 ++- donate.go | 29 ++-- go.mod | 3 + go.sum | 6 + handler.go | 21 ++- help.go | 80 +++-------- inline_faucet.go | 137 +++++++++--------- inline_query.go | 16 +-- inline_receive.go | 85 ++++++----- inline_send.go | 73 +++++----- interceptor.go | 72 ++++++++++ internal/i18n/localize.go | 23 +++ invoice.go | 18 +-- link.go | 13 +- lnurl.go | 48 +++---- pay.go | 80 +++++------ photo.go | 9 +- send.go | 92 +++++------- start.go | 18 +-- text.go | 6 +- tip.go | 45 ++---- translate.go | 32 +++++ translations/README.md | 41 ++++++ translations/de.toml | 296 ++++++++++++++++++++++++++++++++++++++ translations/en.toml | 296 ++++++++++++++++++++++++++++++++++++++ translations/es.toml | 296 ++++++++++++++++++++++++++++++++++++++ translations/nl.toml | 296 ++++++++++++++++++++++++++++++++++++++ 29 files changed, 1693 insertions(+), 469 deletions(-) create mode 100644 internal/i18n/localize.go create mode 100644 translate.go create mode 100644 translations/README.md create mode 100644 translations/de.toml create mode 100644 translations/en.toml create mode 100644 translations/es.toml create mode 100644 translations/nl.toml diff --git a/balance.go b/balance.go index 36454023..22fbdb2d 100644 --- a/balance.go +++ b/balance.go @@ -9,11 +9,6 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - balanceMessage = "👑 *Your balance:* %d sat" - balanceErrorMessage = "🚫 Error fetching your balance. Please try again later." -) - func (bot TipBot) balanceHandler(ctx context.Context, m *tb.Message) { // check and print all commands bot.anyTextHandler(ctx, m) @@ -29,7 +24,7 @@ func (bot TipBot) balanceHandler(ctx context.Context, m *tb.Message) { } if !user.Initialized { - bot.startHandler(m) + bot.startHandler(ctx, m) return } @@ -37,11 +32,11 @@ func (bot TipBot) balanceHandler(ctx context.Context, m *tb.Message) { balance, err := bot.GetUserBalance(user) if err != nil { log.Errorf("[/balance] Error fetching %s's balance: %s", usrStr, err) - bot.trySendMessage(m.Sender, balanceErrorMessage) + bot.trySendMessage(m.Sender, Translate(ctx, "balanceErrorMessage")) return } log.Infof("[/balance] %s's balance: %d sat\n", usrStr, balance) - bot.trySendMessage(m.Sender, fmt.Sprintf(balanceMessage, balance)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "balanceMessage"), balance)) return } diff --git a/bot.go b/bot.go index 7b4b7f95..f7960065 100644 --- a/bot.go +++ b/bot.go @@ -5,9 +5,11 @@ import ( "sync" "time" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/storage" + i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" log "github.com/sirupsen/logrus" "gopkg.in/tucnak/telebot.v2" tb "gopkg.in/tucnak/telebot.v2" @@ -20,6 +22,7 @@ type TipBot struct { logger *gorm.DB telegram *telebot.Bot client *lnbits.Client + bundle *i18n2.Bundle } var ( @@ -34,6 +37,7 @@ func NewBot() TipBot { database: db, logger: txLogger, bunt: storage.NewBunt(Configuration.Database.BuntDbPath), + bundle: i18n.RegisterLanguages(), } } diff --git a/database.go b/database.go index 6ebf3916..9b70216f 100644 --- a/database.go +++ b/database.go @@ -49,8 +49,13 @@ func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.U return toUserDb, nil } -// GetUser from telegram user. Update the user if user information changed. -func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { +// GetLnbitsUser will not update the user in database. +// this is required, because fetching lnbits.User from a incomplete tb.User +// will update the incomplete (partial) user in storage. +// this function will accept users like this: +// &tb.User{ID: toId, Username: username} +// without updating the user in storage. +func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { user := &lnbits.User{Name: strconv.Itoa(u.ID)} tx := bot.database.First(user) if tx.Error != nil { @@ -59,7 +64,12 @@ func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { user.Telegram = u return user, tx.Error } - var err error + return user, nil +} + +// GetUser from telegram user. Update the user if user information changed. +func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { + user, err := GetLnbitsUser(u, bot) go func() { userCopy := bot.copyLowercaseUser(u) if !reflect.DeepEqual(userCopy, user.Telegram) { diff --git a/donate.go b/donate.go index 76d940d0..d0498e21 100644 --- a/donate.go +++ b/donate.go @@ -19,23 +19,14 @@ import ( // IF YOU USE THIS PROJECT, LEAVE THIS CODE ALONE var ( - donationSuccess = "🙏 Thank you for your donation." - donationErrorMessage = "🚫 Oh no. Donation failed." - donationProgressMessage = "🧮 Preparing your donation..." - donationFailedMessage = "🚫 Donation failed: %s" - donateEnterAmountMessage = "Did you enter an amount?" - donateValidAmountMessage = "Did you enter a valid amount?" - donateHelpText = "📖 Oops, that didn't work. %s\n\n" + - "*Usage:* `/donate `\n" + - "*Example:* `/donate 1000`" donationEndpoint string ) -func helpDonateUsage(errormsg string) string { +func helpDonateUsage(ctx context.Context, errormsg string) string { if len(errormsg) > 0 { - return fmt.Sprintf(donateHelpText, fmt.Sprintf("%s", errormsg)) + return fmt.Sprintf(Translate(ctx, "donateHelpText"), fmt.Sprintf("%s", errormsg)) } else { - return fmt.Sprintf(donateHelpText, "") + return fmt.Sprintf(Translate(ctx, "donateHelpText"), "") } } @@ -44,7 +35,7 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) if len(strings.Split(m.Text, " ")) < 2 { - bot.trySendMessage(m.Sender, helpDonateUsage(donateEnterAmountMessage)) + bot.trySendMessage(m.Sender, helpDonateUsage(ctx, Translate(ctx, "donateEnterAmountMessage"))) return } amount, err := decodeAmountFromCommand(m.Text) @@ -52,23 +43,23 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { return } if amount < 1 { - bot.trySendMessage(m.Sender, helpDonateUsage(donateValidAmountMessage)) + bot.trySendMessage(m.Sender, helpDonateUsage(ctx, Translate(ctx, "donateValidAmountMessage"))) return } // command is valid - msg := bot.trySendMessage(m.Sender, donationProgressMessage) + msg := bot.trySendMessage(m.Sender, Translate(ctx, "donationProgressMessage")) // get invoice resp, err := http.Get(fmt.Sprintf(donationEndpoint, amount, GetUserStr(m.Sender), GetUserStr(bot.telegram.Me))) if err != nil { log.Errorln(err) - bot.tryEditMessage(msg, donationErrorMessage) + bot.tryEditMessage(msg, Translate(ctx, "donationErrorMessage")) return } body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Errorln(err) - bot.tryEditMessage(msg, donationErrorMessage) + bot.tryEditMessage(msg, Translate(ctx, "donationErrorMessage")) return } @@ -80,10 +71,10 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { userStr := GetUserStr(m.Sender) errmsg := fmt.Sprintf("[/donate] Donation failed for user %s: %s", userStr, err) log.Errorln(errmsg) - bot.tryEditMessage(msg, fmt.Sprintf(donationFailedMessage, err)) + bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "donationFailedMessage"), err)) return } - bot.tryEditMessage(msg, donationSuccess) + bot.tryEditMessage(msg, Translate(ctx, "donationSuccess")) } diff --git a/go.mod b/go.mod index e21cd11a..00cbca72 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,21 @@ module github.com/LightningTipBot/LightningTipBot go 1.15 require ( + github.com/BurntSushi/toml v0.3.1 github.com/fiatjaf/go-lnurl v1.4.0 github.com/fiatjaf/ln-decodepay v1.1.0 github.com/gorilla/mux v1.8.0 github.com/imroc/req v0.3.0 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/sirupsen/logrus v1.2.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tidwall/btree v0.6.1 // indirect github.com/tidwall/buntdb v1.2.6 github.com/tidwall/gjson v1.8.1 github.com/tidwall/pretty v1.2.0 // indirect + golang.org/x/text v0.3.5 gopkg.in/tucnak/telebot.v2 v2.3.5 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.12 diff --git a/go.sum b/go.sum index e3fad934..4b68dded 100644 --- a/go.sum +++ b/go.sum @@ -160,6 +160,9 @@ github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KV github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc= +github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c= +github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= @@ -270,6 +273,7 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -304,6 +308,8 @@ gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bl gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= diff --git a/handler.go b/handler.go index f979c1fa..7990c647 100644 --- a/handler.go +++ b/handler.go @@ -31,18 +31,21 @@ func (bot TipBot) registerTelegramHandlers() { func (bot TipBot) registerHandlerWithInterceptor(h Handler) { switch h.Interceptor.Type { case MessageInterceptor: + h.Interceptor.Before = append(h.Interceptor.Before, bot.localizerInterceptor) for _, endpoint := range h.Endpoints { bot.handle(endpoint, intercept.HandlerWithMessage(h.Handler.(func(ctx context.Context, query *tb.Message)), intercept.WithBeforeMessage(h.Interceptor.Before...), intercept.WithAfterMessage(h.Interceptor.After...))) } case QueryInterceptor: + h.Interceptor.Before = append(h.Interceptor.Before, bot.localizerInterceptor) for _, endpoint := range h.Endpoints { bot.handle(endpoint, intercept.HandlerWithQuery(h.Handler.(func(ctx context.Context, query *tb.Query)), intercept.WithBeforeQuery(h.Interceptor.Before...), intercept.WithAfterQuery(h.Interceptor.After...))) } case CallbackInterceptor: + h.Interceptor.Before = append(h.Interceptor.Before, bot.localizerInterceptor) for _, endpoint := range h.Endpoints { bot.handle(endpoint, intercept.HandlerWithCallback(h.Handler.(func(ctx context.Context, callback *tb.Callback)), intercept.WithBeforeCallback(h.Interceptor.Before...), @@ -86,8 +89,9 @@ func (bot TipBot) register(h Handler) { func (bot TipBot) getHandler() []Handler { return []Handler{ { - Endpoints: []interface{}{"/start"}, - Handler: bot.startHandler, + Endpoints: []interface{}{"/start"}, + Handler: bot.startHandler, + Interceptor: &Interceptor{Type: MessageInterceptor}, }, { Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan"}, @@ -212,8 +216,8 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.logMessageInterceptor, bot.requirePrivateChatInterceptor, + bot.logMessageInterceptor, bot.loadUserInterceptor}}, }, { @@ -232,7 +236,7 @@ func (bot TipBot) getHandler() []Handler { Handler: bot.anyQueryHandler, Interceptor: &Interceptor{ Type: QueryInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor, bot.localizerInterceptor}}, }, { Endpoints: []interface{}{tb.OnChosenInlineResult}, @@ -276,6 +280,9 @@ func (bot TipBot) getHandler() []Handler { { Endpoints: []interface{}{&btnCancelInlineSend}, Handler: bot.cancelInlineSendHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, }, { Endpoints: []interface{}{&btnAcceptInlineReceive}, @@ -287,6 +294,9 @@ func (bot TipBot) getHandler() []Handler { { Endpoints: []interface{}{&btnCancelInlineReceive}, Handler: bot.cancelInlineReceiveHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, }, { Endpoints: []interface{}{&btnAcceptInlineFaucet}, @@ -298,6 +308,9 @@ func (bot TipBot) getHandler() []Handler { { Endpoints: []interface{}{&btnCancelInlineFaucet}, Handler: bot.cancelInlineFaucetHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, }, } } diff --git a/help.go b/help.go index 7880f96b..3b02246c 100644 --- a/help.go +++ b/help.go @@ -7,68 +7,21 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - helpMessage = "⚡️ *Wallet*\n_This bot is a Bitcoin Lightning wallet that can sends tips on Telegram. To tip, add the bot to a group chat. The basic unit of tips are Satoshis (sat). 100,000,000 sat = 1 Bitcoin. Type 📚 /basics for more._\n\n" + - "❤️ *Donate*\n" + - "_This bot charges no fees but costs satoshis to operate. If you like the bot, please consider supporting this project with a donation. To donate, use_ `/donate 1000`\n\n" + - "%s" + - "⚙️ *Commands*\n" + - "*/tip* 🏅 Reply to a message to tip: `/tip []`\n" + - "*/balance* 👑 Check your balance: `/balance`\n" + - "*/send* 💸 Send funds to a user: `/send @user or user@ln.tips []`\n" + - "*/invoice* ⚡️ Receive with Lightning: `/invoice []`\n" + - "*/pay* ⚡️ Pay with Lightning: `/pay `\n" + - "*/donate* ❤️ Donate to the project: `/donate 1000`\n" + - "*/advanced* 🤖 Advanced features.\n" + - "*/help* 📖 Read this help." - - infoMessage = "🧡 *Bitcoin*\n" + - "_Bitcoin is the currency of the internet. It is permissionless and decentralized and has no masters and no controling authority. Bitcoin is sound money that is faster, more secure, and more inclusive than the legacy financial system._\n\n" + - "🧮 *Economnics*\n" + - "_The smallest unit of Bitcoin are Satoshis (sat) and 100,000,000 sat = 1 Bitcoin. There will only ever be 21 Million Bitcoin. The fiat currency value of Bitcoin can change daily. However, if you live on a Bitcoin standard 1 sat will always equal 1 sat._\n\n" + - "⚡️ *The Lightning Network*\n" + - "_The Lightning Network is a payment protocol that enables fast and cheap Bitcoin payments that require almost no energy. It is what scales Bitcoin to the billions of people around the world._\n\n" + - "📲 *Lightning Wallets*\n" + - "_Your funds on this bot can be sent to any other Lightning wallet and vice versa. Recommended Lightning wallets for your phone are_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (non-custodial), or_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(easy)_.\n\n" + - "📄 *Open Source*\n" + - "_This bot is free and_ [open source](https://github.com/LightningTipBot/LightningTipBot) _software. You can run it on your own computer and use it in your own community._\n\n" + - "✈️ *Telegram*\n" + - "_Add this bot to your Telegram group chat to /tip posts. If you make the bot admin of the group it will also clean up commands to keep the chat tidy._\n\n" + - "🏛 *Terms*\n" + - "_We are not custodian of your funds. We will act in your best interest but we're also aware that the situation without KYC is tricky until we figure something out. Any amount you load onto your wallet will be considered a donation. Do not give us all your money. Be aware that this bot is in beta development. Use at your own risk._\n\n" + - "❤️ *Donate*\n" + - "_This bot charges no fees but costs satoshis to operate. If you like the bot, please consider supporting this project with a donation. To donate, use_ `/donate 1000`" - - helpNoUsernameMessage = "ℹ️ Please set a Telegram username." - - advancedMessage = "%s\n\n" + - "👉 *Inline commands*\n" + - "*send* 💸 Send sats to chat: `%s send []`\n" + - "*receive* 🏅 Request a payment: `%s receive []`\n" + - "*faucet* 🚰 Create a faucet: `%s faucet `\n\n" + - "📖 You can use inline commands in every chat, even in private conversations. Wait a second after entering an inline command and *click* the result, don't press enter.\n\n" + - "⚙️ *Advanced commands*\n" + - "*/link* 🔗 Link your wallet to [BlueWallet](https://bluewallet.io/) or [Zeus](https://zeusln.app/)\n" + - "*/lnurl* ⚡️ Lnurl receive or pay: `/lnurl` or `/lnurl `\n" + - "*/faucet* 🚰 Create a faucet `/faucet `" -) - -func (bot TipBot) makeHelpMessage(m *tb.Message) string { +func (bot TipBot) makeHelpMessage(ctx context.Context, m *tb.Message) string { dynamicHelpMessage := "" // user has no username set if len(m.Sender.Username) == 0 { // return fmt.Sprintf(helpMessage, fmt.Sprintf("%s\n\n", helpNoUsernameMessage)) - dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("%s\n", helpNoUsernameMessage) - } else { - dynamicHelpMessage = "ℹ️ *Info*\n" - lnaddr, err := bot.UserGetLightningAddress(m.Sender) - if err != nil { - dynamicHelpMessage = "" - } else { - dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("Your Lightning Address is `%s`\n", lnaddr) - } + dynamicHelpMessage = dynamicHelpMessage + "\n" + Translate(ctx, "helpNoUsernameMessage") } - dynamicHelpMessage = dynamicHelpMessage + "\n" + lnaddr, _ := bot.UserGetLightningAddress(m.Sender) + if len(lnaddr) > 0 { + dynamicHelpMessage = dynamicHelpMessage + "\n" + fmt.Sprintf(Translate(ctx, "infoYourLightningAddress"), lnaddr) + } + if len(dynamicHelpMessage) > 0 { + dynamicHelpMessage = Translate(ctx, "infoHelpMessage") + dynamicHelpMessage + } + helpMessage := Translate(ctx, "helpMessage") return fmt.Sprintf(helpMessage, dynamicHelpMessage) } @@ -79,7 +32,7 @@ func (bot TipBot) helpHandler(ctx context.Context, m *tb.Message) { // delete message NewMessage(m, WithDuration(0, bot.telegram)) } - bot.trySendMessage(m.Sender, bot.makeHelpMessage(m), tb.NoPreview) + bot.trySendMessage(m.Sender, bot.makeHelpMessage(ctx, m), tb.NoPreview) return } @@ -90,16 +43,17 @@ func (bot TipBot) basicsHandler(ctx context.Context, m *tb.Message) { // delete message NewMessage(m, WithDuration(0, bot.telegram)) } - bot.trySendMessage(m.Sender, infoMessage, tb.NoPreview) + bot.trySendMessage(m.Sender, Translate(ctx, "basicsMessage"), tb.NoPreview) return } -func (bot TipBot) makeAdvancedHelpMessage(m *tb.Message) string { +func (bot TipBot) makeAdvancedHelpMessage(ctx context.Context, m *tb.Message) string { + dynamicHelpMessage := "" // user has no username set if len(m.Sender.Username) == 0 { // return fmt.Sprintf(helpMessage, fmt.Sprintf("%s\n\n", helpNoUsernameMessage)) - dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("%s", helpNoUsernameMessage) + dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("%s", Translate(ctx, "helpNoUsernameMessage")) } else { dynamicHelpMessage = "ℹ️ *Info*\n" lnaddr, err := bot.UserGetLightningAddress(m.Sender) @@ -118,7 +72,7 @@ func (bot TipBot) makeAdvancedHelpMessage(m *tb.Message) string { } // this is so stupid: - return fmt.Sprintf(advancedMessage, dynamicHelpMessage, GetUserStr(bot.telegram.Me), GetUserStr(bot.telegram.Me), GetUserStr(bot.telegram.Me)) + return fmt.Sprintf(Translate(ctx, "advancedMessage"), dynamicHelpMessage, GetUserStr(bot.telegram.Me), GetUserStr(bot.telegram.Me), GetUserStr(bot.telegram.Me)) } func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) { @@ -128,6 +82,6 @@ func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) { // delete message NewMessage(m, WithDuration(0, bot.telegram)) } - bot.trySendMessage(m.Sender, bot.makeAdvancedHelpMessage(m), tb.NoPreview) + bot.trySendMessage(m.Sender, bot.makeAdvancedHelpMessage(ctx, m), tb.NoPreview) return } diff --git a/inline_faucet.go b/inline_faucet.go index 1e7cfa58..6486ec8e 100644 --- a/inline_faucet.go +++ b/inline_faucet.go @@ -13,29 +13,6 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - inlineFaucetMessage = "Press ✅ to collect %d sat from this faucet.\n\n🚰 Remaining: %d/%d sat (given to %d/%d users)\n%s" - inlineFaucetEndedMessage = "🏅 Faucet empty 🏅\n\n🚰 %d sat given to %d users." - inlineFaucetAppendMemo = "\n✉️ %s" - inlineFaucetCreateWalletMessage = "Chat with %s 👈 to manage your wallet." - inlineFaucetCancelledMessage = "🚫 Faucet cancelled." - inlineFaucetInvalidPeruserAmountMessage = "🚫 Peruser amount not divisor of capacity." - inlineFaucetInvalidAmountMessage = "🚫 Invalid amount." - inlineFaucetSentMessage = "🚰 %d sat sent to %s." - inlineFaucetReceivedMessage = "🚰 %s sent you %d sat." - inlineFaucetHelpFaucetInGroup = "Create a faucet in a group with the bot inside or use 👉 inline command (/advanced for more)." - inlineFaucetHelpText = "📖 Oops, that didn't work. %s\n\n" + - "*Usage:* `/faucet `\n" + - "*Example:* `/faucet 210 21`" -) - -const ( - inlineQueryFaucetTitle = "🚰 Create a faucet." - inlineQueryFaucetDescription = "Usage: @%s faucet " - inlineResultFaucetTitle = "💸 Create a %d sat faucet." - inlineResultFaucetDescription = "👉 Click here to create a faucet worth %d sat in this chat." -) - var ( inlineFaucetMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} btnCancelInlineFaucet = inlineFaucetMenu.Data("🚫 Cancel", "cancel_faucet_inline") @@ -56,6 +33,7 @@ type InlineFaucet struct { NTaken int `json:"inline_faucet_ntaken"` UserNeedsWallet bool `json:"inline_faucet_userneedswallet"` InTransaction bool `json:"inline_faucet_intransaction"` + LanguageCode string `json:"languagecode"` } func NewInlineFaucet() *InlineFaucet { @@ -116,48 +94,49 @@ func (bot *TipBot) getInlineFaucet(c *tb.Callback) (*InlineFaucet, error) { for inlineFaucet.InTransaction { select { case <-ticker.C: - return nil, fmt.Errorf("[faucet] faucet %s timeout", inlineFaucet.ID) + return nil, fmt.Errorf("faucet %s timeout", inlineFaucet.ID) default: - log.Infof("[faucet] faucet %s already in transaction", inlineFaucet.ID) + log.Warnf("[getInlineFaucet] %s in transaction", inlineFaucet.ID) time.Sleep(time.Duration(500) * time.Millisecond) err = bot.bunt.Get(inlineFaucet) } } if err != nil { - return nil, fmt.Errorf("could not get inline faucet: %s", err) + return nil, fmt.Errorf("could not get inline faucet %s: %s", inlineFaucet.ID, err) } return inlineFaucet, nil } func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { + bot.anyTextHandler(ctx, m) if m.Private() { - bot.trySendMessage(m.Sender, fmt.Sprintf(inlineFaucetHelpText, inlineFaucetHelpFaucetInGroup)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetHelpFaucetInGroup"))) return } inlineFaucet := NewInlineFaucet() var err error inlineFaucet.Amount, err = decodeAmountFromCommand(m.Text) if err != nil { - bot.trySendMessage(m.Sender, fmt.Sprintf(inlineFaucetHelpText, inlineFaucetInvalidAmountMessage)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetInvalidAmountMessage"))) bot.tryDeleteMessage(m) return } peruserStr, err := getArgumentFromCommand(m.Text, 2) if err != nil { - bot.trySendMessage(m.Sender, fmt.Sprintf(inlineFaucetHelpText, "")) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), "")) bot.tryDeleteMessage(m) return } inlineFaucet.PerUserAmount, err = strconv.Atoi(peruserStr) if err != nil { - bot.trySendMessage(m.Sender, fmt.Sprintf(inlineFaucetHelpText, inlineFaucetInvalidAmountMessage)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetInvalidAmountMessage"))) bot.tryDeleteMessage(m) return } // peruser amount must be >1 and a divisor of amount if inlineFaucet.PerUserAmount < 1 || inlineFaucet.Amount%inlineFaucet.PerUserAmount != 0 { - bot.trySendMessage(m.Sender, fmt.Sprintf(inlineFaucetHelpText, inlineFaucetInvalidPeruserAmountMessage)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetInvalidPeruserAmountMessage"))) bot.tryDeleteMessage(m) return } @@ -173,8 +152,8 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { } // check if fromUser has balance if balance < inlineFaucet.Amount { - log.Errorf("Balance of user %s too low", fromUserStr) - bot.trySendMessage(m.Sender, fmt.Sprintf(inlineSendBalanceLowMessage, balance)) + log.Errorf("[faucet] Balance of user %s too low", fromUserStr) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineSendBalanceLowMessage"), balance)) bot.tryDeleteMessage(m) return } @@ -182,22 +161,29 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { // // check for memo in command memo := GetMemoFromCommand(m.Text, 3) - inlineMessage := fmt.Sprintf(inlineFaucetMessage, inlineFaucet.PerUserAmount, inlineFaucet.Amount, inlineFaucet.Amount, 0, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.Amount, inlineFaucet.Amount)) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, inlineFaucet.Amount, inlineFaucet.Amount, 0, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.Amount, inlineFaucet.Amount)) if len(memo) > 0 { - inlineMessage = inlineMessage + fmt.Sprintf(inlineFaucetAppendMemo, memo) + inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineFaucetAppendMemo"), memo) } inlineFaucet.ID = fmt.Sprintf("inl-faucet-%d-%d-%s", m.Sender.ID, inlineFaucet.Amount, RandStringRunes(5)) - - btnAcceptInlineFaucet.Data = inlineFaucet.ID - btnCancelInlineFaucet.Data = inlineFaucet.ID - inlineFaucetMenu.Inline(inlineFaucetMenu.Row(btnAcceptInlineFaucet, btnCancelInlineFaucet)) + acceptInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "collectButtonMessage"), "confirm_faucet_inline") + cancelInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_faucet_inline") + acceptInlineFaucetButton.Data = inlineFaucet.ID + cancelInlineFaucetButton.Data = inlineFaucet.ID + + inlineFaucetMenu.Inline( + inlineFaucetMenu.Row( + acceptInlineFaucetButton, + cancelInlineFaucetButton), + ) bot.trySendMessage(m.Chat, inlineMessage, inlineFaucetMenu) log.Infof("[faucet] %s created faucet %s: %d sat (%d per user)", fromUserStr, inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) inlineFaucet.Message = inlineMessage inlineFaucet.From = fromUser inlineFaucet.Memo = memo inlineFaucet.RemainingAmount = inlineFaucet.Amount + inlineFaucet.LanguageCode = ctx.Value("publicLanguageCode").(string) runtime.IgnoreError(bot.bunt.Set(inlineFaucet)) } @@ -207,27 +193,27 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { var err error inlineFaucet.Amount, err = decodeAmountFromCommand(q.Text) if err != nil { - bot.inlineQueryReplyWithError(q, inlineQueryFaucetTitle, fmt.Sprintf(inlineQueryFaucetDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } if inlineFaucet.Amount < 1 { - bot.inlineQueryReplyWithError(q, inlineSendInvalidAmountMessage, fmt.Sprintf(inlineQueryFaucetDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } peruserStr, err := getArgumentFromCommand(q.Text, 2) if err != nil { - bot.inlineQueryReplyWithError(q, inlineQueryFaucetTitle, fmt.Sprintf(inlineQueryFaucetDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } inlineFaucet.PerUserAmount, err = strconv.Atoi(peruserStr) if err != nil { - bot.inlineQueryReplyWithError(q, inlineQueryFaucetTitle, fmt.Sprintf(inlineQueryFaucetDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } // peruser amount must be >1 and a divisor of amount if inlineFaucet.PerUserAmount < 1 || inlineFaucet.Amount%inlineFaucet.PerUserAmount != 0 { - bot.inlineQueryReplyWithError(q, inlineFaucetInvalidPeruserAmountMessage, fmt.Sprintf(inlineQueryFaucetDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineFaucetInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } inlineFaucet.NTotal = inlineFaucet.Amount / inlineFaucet.PerUserAmount @@ -242,7 +228,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { // check if fromUser has balance if balance < inlineFaucet.Amount { log.Errorf("Balance of user %s too low", fromUserStr) - bot.inlineQueryReplyWithError(q, fmt.Sprintf(inlineSendBalanceLowMessage, balance), fmt.Sprintf(inlineQueryFaucetDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, fmt.Sprintf(TranslateUser(ctx, "inlineSendBalanceLowMessage"), balance), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } @@ -254,22 +240,29 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { } results := make(tb.Results, len(urls)) // []tb.Result for i, url := range urls { - inlineMessage := fmt.Sprintf(inlineFaucetMessage, inlineFaucet.PerUserAmount, inlineFaucet.Amount, inlineFaucet.Amount, 0, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.Amount, inlineFaucet.Amount)) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, inlineFaucet.Amount, inlineFaucet.Amount, 0, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.Amount, inlineFaucet.Amount)) if len(memo) > 0 { - inlineMessage = inlineMessage + fmt.Sprintf(inlineFaucetAppendMemo, memo) + inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineFaucetAppendMemo"), memo) } result := &tb.ArticleResult{ // URL: url, Text: inlineMessage, - Title: fmt.Sprintf(inlineResultFaucetTitle, inlineFaucet.Amount), - Description: fmt.Sprintf(inlineResultFaucetDescription, inlineFaucet.Amount), + Title: fmt.Sprintf(TranslateUser(ctx, "inlineResultFaucetTitle"), inlineFaucet.Amount), + Description: TranslateUser(ctx, "inlineResultFaucetDescription"), // required for photos ThumbURL: url, } id := fmt.Sprintf("inl-faucet-%d-%d-%s", q.From.ID, inlineFaucet.Amount, RandStringRunes(5)) - btnAcceptInlineFaucet.Data = id - btnCancelInlineFaucet.Data = id - inlineFaucetMenu.Inline(inlineFaucetMenu.Row(btnAcceptInlineFaucet, btnCancelInlineFaucet)) + acceptInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "collectButtonMessage"), "confirm_faucet_inline") + cancelInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_faucet_inline") + acceptInlineFaucetButton.Data = id + cancelInlineFaucetButton.Data = id + + inlineFaucetMenu.Inline( + inlineFaucetMenu.Row( + acceptInlineFaucetButton, + cancelInlineFaucetButton), + ) result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: inlineFaucetMenu.InlineKeyboard} results[i] = result @@ -282,6 +275,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { inlineFaucet.From = fromUser inlineFaucet.RemainingAmount = inlineFaucet.Amount inlineFaucet.Memo = memo + inlineFaucet.LanguageCode = ctx.Value("publicLanguageCode").(string) runtime.IgnoreError(bot.bunt.Set(inlineFaucet)) } @@ -306,18 +300,18 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback from := inlineFaucet.From err = bot.LockFaucet(inlineFaucet) if err != nil { - log.Errorf("[faucet] %s", err) + log.Errorf("[faucet] LockFaucet %s error: %s", inlineFaucet.ID, err) return } if !inlineFaucet.Active { - log.Errorf("[faucet] inline send not active anymore") + log.Errorf(fmt.Sprintf("[faucet] faucet %s inactive.", inlineFaucet.ID)) return } // release faucet no matter what defer bot.ReleaseFaucet(inlineFaucet) if from.Telegram.ID == to.Telegram.ID { - bot.trySendMessage(from.Telegram, sendYourselfMessage) + bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) return } // check if to user has already taken from the faucet @@ -357,7 +351,7 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback success, err := t.Send() if !success { - bot.trySendMessage(from.Telegram, sendErrorMessage) + bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) errMsg := fmt.Sprintf("[faucet] Transaction failed: %s", err) log.Errorln(errMsg) return @@ -368,8 +362,8 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.To = append(inlineFaucet.To, to.Telegram) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount - _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(inlineFaucetReceivedMessage, fromUserStrMd, inlineFaucet.PerUserAmount)) - _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(inlineFaucetSentMessage, inlineFaucet.PerUserAmount, toUserStrMd)) + _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(bot.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) + _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(bot.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[faucet] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -377,29 +371,36 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback } // build faucet message - inlineFaucet.Message = fmt.Sprintf(inlineFaucetMessage, inlineFaucet.PerUserAmount, inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) + inlineFaucet.Message = fmt.Sprintf(bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) memo := inlineFaucet.Memo if len(memo) > 0 { - inlineFaucet.Message = inlineFaucet.Message + fmt.Sprintf(inlineFaucetAppendMemo, memo) + inlineFaucet.Message = inlineFaucet.Message + fmt.Sprintf(bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetAppendMemo"), memo) } if inlineFaucet.UserNeedsWallet { - inlineFaucet.Message += "\n\n" + fmt.Sprintf(inlineFaucetCreateWalletMessage, GetUserStrMd(bot.telegram.Me)) + inlineFaucet.Message += "\n\n" + fmt.Sprintf(bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) } // register new inline buttons inlineFaucetMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} - btnCancelInlineFaucet.Data = inlineFaucet.ID - btnAcceptInlineFaucet.Data = inlineFaucet.ID - inlineFaucetMenu.Inline(inlineFaucetMenu.Row(btnAcceptInlineFaucet, btnCancelInlineFaucet)) + acceptInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "collectButtonMessage"), "confirm_faucet_inline") + cancelInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_faucet_inline") + acceptInlineFaucetButton.Data = inlineFaucet.ID + cancelInlineFaucetButton.Data = inlineFaucet.ID + + inlineFaucetMenu.Inline( + inlineFaucetMenu.Row( + acceptInlineFaucetButton, + cancelInlineFaucetButton), + ) // update message log.Infoln(inlineFaucet.Message) bot.tryEditMessage(c.Message, inlineFaucet.Message, inlineFaucetMenu) } if inlineFaucet.RemainingAmount < inlineFaucet.PerUserAmount { // faucet is depleted - inlineFaucet.Message = fmt.Sprintf(inlineFaucetEndedMessage, inlineFaucet.Amount, inlineFaucet.NTaken) + inlineFaucet.Message = fmt.Sprintf(bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetEndedMessage"), inlineFaucet.Amount, inlineFaucet.NTaken) if inlineFaucet.UserNeedsWallet { - inlineFaucet.Message += "\n\n" + fmt.Sprintf(inlineFaucetCreateWalletMessage, GetUserStrMd(bot.telegram.Me)) + inlineFaucet.Message += "\n\n" + fmt.Sprintf(bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) } bot.tryEditMessage(c.Message, inlineFaucet.Message) inlineFaucet.Active = false @@ -407,14 +408,14 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback } -func (bot *TipBot) cancelInlineFaucetHandler(c *tb.Callback) { +func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { inlineFaucet, err := bot.getInlineFaucet(c) if err != nil { log.Errorf("[cancelInlineSendHandler] %s", err) return } if c.Sender.ID == inlineFaucet.From.Telegram.ID { - bot.tryEditMessage(c.Message, inlineFaucetCancelledMessage, &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false inlineFaucet.InTransaction = false diff --git a/inline_query.go b/inline_query.go index d68680c7..59d351cf 100644 --- a/inline_query.go +++ b/inline_query.go @@ -12,7 +12,7 @@ import ( const queryImage = "https://avatars.githubusercontent.com/u/88730856?v=4" -func (bot TipBot) inlineQueryInstructions(q *tb.Query) { +func (bot TipBot) inlineQueryInstructions(ctx context.Context, q *tb.Query) { instructions := []struct { url string title string @@ -20,18 +20,18 @@ func (bot TipBot) inlineQueryInstructions(q *tb.Query) { }{ { url: queryImage, - title: inlineQuerySendTitle, - description: fmt.Sprintf(inlineQuerySendDescription, bot.telegram.Me.Username), + title: TranslateUser(ctx, "inlineQuerySendTitle"), + description: fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.telegram.Me.Username), }, { url: queryImage, - title: inlineQueryReceiveTitle, - description: fmt.Sprintf(inlineQueryReceiveDescription, bot.telegram.Me.Username), + title: TranslateUser(ctx, "inlineQueryReceiveTitle"), + description: fmt.Sprintf(TranslateUser(ctx, "inlineQueryReceiveDescription"), bot.telegram.Me.Username), }, { url: queryImage, - title: inlineQueryFaucetTitle, - description: fmt.Sprintf(inlineQueryFaucetDescription, bot.telegram.Me.Username), + title: TranslateUser(ctx, "inlineQueryFaucetTitle"), + description: fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username), }, } results := make(tb.Results, len(instructions)) // []tb.Result @@ -90,7 +90,7 @@ func (bot TipBot) anyChosenInlineHandler(q *tb.ChosenInlineResult) { func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) { if q.Text == "" { - bot.inlineQueryInstructions(q) + bot.inlineQueryInstructions(ctx, q) return } diff --git a/inline_receive.go b/inline_receive.go index 97c565c1..e1523ae5 100644 --- a/inline_receive.go +++ b/inline_receive.go @@ -12,24 +12,10 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - inlineReceiveMessage = "Press 💸 to pay to %s.\n\n💸 Amount: %d sat" - inlineReceiveAppendMemo = "\n✉️ %s" - inlineReceiveUpdateMessageAccept = "💸 %d sat sent from %s to %s." - inlineReceiveCreateWalletMessage = "Chat with %s 👈 to manage your wallet." - inlineReceiveYourselfMessage = "📖 You can't pay to yourself." - inlineReceiveFailedMessage = "🚫 Receive failed." - inlineReceiveCancelledMessage = "🚫 Receive cancelled." -) - var ( - inlineQueryReceiveTitle = "🏅 Request a payment in a chat." - inlineQueryReceiveDescription = "Usage: @%s receive []" - inlineResultReceiveTitle = "🏅 Receive %d sat." - inlineResultReceiveDescription = "👉 Click to request a payment of %d sat." - inlineReceiveMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} - btnCancelInlineReceive = inlineReceiveMenu.Data("🚫 Cancel", "cancel_receive_inline") - btnAcceptInlineReceive = inlineReceiveMenu.Data("💸 Pay", "confirm_receive_inline") + inlineReceiveMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + btnCancelInlineReceive = inlineReceiveMenu.Data("🚫 Cancel", "cancel_receive_inline") + btnAcceptInlineReceive = inlineReceiveMenu.Data("💸 Pay", "confirm_receive_inline") ) type InlineReceive struct { @@ -37,10 +23,11 @@ type InlineReceive struct { Amount int `json:"inline_receive_amount"` From *lnbits.User `json:"inline_receive_from"` To *lnbits.User `json:"inline_receive_to"` - Memo string - ID string `json:"inline_receive_id"` - Active bool `json:"inline_receive_active"` - InTransaction bool `json:"inline_receive_intransaction"` + Memo string `json:"inline_receive_memo"` + ID string `json:"inline_receive_id"` + Active bool `json:"inline_receive_active"` + InTransaction bool `json:"inline_receive_intransaction"` + LanguageCode string `json:"languagecode"` } func NewInlineReceive() *InlineReceive { @@ -98,15 +85,15 @@ func (bot *TipBot) getInlineReceive(c *tb.Callback) (*InlineReceive, error) { for inlineReceive.InTransaction { select { case <-ticker.C: - return nil, fmt.Errorf("inline send timeout") + return nil, fmt.Errorf("inline receive %s timeout", inlineReceive.ID) default: - log.Infoln("in transaction") + log.Warnf("[getInlineReceive] %s in transaction", inlineReceive.ID) time.Sleep(time.Duration(500) * time.Millisecond) err = bot.bunt.Get(inlineReceive) } } if err != nil { - return nil, fmt.Errorf("could not get inline receive message") + return nil, fmt.Errorf("could not get inline receive %s", inlineReceive.ID) } return inlineReceive, nil @@ -118,11 +105,11 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { var err error inlineReceive.Amount, err = decodeAmountFromCommand(q.Text) if err != nil { - bot.inlineQueryReplyWithError(q, inlineQueryReceiveTitle, fmt.Sprintf(inlineQueryReceiveDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineQueryReceiveTitle"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.telegram.Me.Username)) return } if inlineReceive.Amount < 1 { - bot.inlineQueryReplyWithError(q, inlineSendInvalidAmountMessage, fmt.Sprintf(inlineQueryReceiveDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.telegram.Me.Username)) return } @@ -137,24 +124,31 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { results := make(tb.Results, len(urls)) // []tb.Result for i, url := range urls { - inlineMessage := fmt.Sprintf(inlineReceiveMessage, fromUserStr, inlineReceive.Amount) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineReceiveMessage"), fromUserStr, inlineReceive.Amount) if len(inlineReceive.Memo) > 0 { - inlineMessage = inlineMessage + fmt.Sprintf(inlineReceiveAppendMemo, inlineReceive.Memo) + inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineReceiveAppendMemo"), inlineReceive.Memo) } result := &tb.ArticleResult{ // URL: url, Text: inlineMessage, - Title: fmt.Sprintf(inlineResultReceiveTitle, inlineReceive.Amount), - Description: fmt.Sprintf(inlineResultReceiveDescription, inlineReceive.Amount), + Title: fmt.Sprintf(TranslateUser(ctx, "inlineResultReceiveTitle"), inlineReceive.Amount), + Description: fmt.Sprintf(TranslateUser(ctx, "inlineResultReceiveDescription"), inlineReceive.Amount), // required for photos ThumbURL: url, } id := fmt.Sprintf("inl-receive-%d-%d-%s", q.From.ID, inlineReceive.Amount, RandStringRunes(5)) - btnAcceptInlineReceive.Data = id - btnCancelInlineReceive.Data = id - inlineReceiveMenu.Inline(inlineReceiveMenu.Row(btnAcceptInlineReceive, btnCancelInlineReceive)) + acceptInlineReceiveButton := inlineReceiveMenu.Data(Translate(ctx, "payReceiveButtonMessage"), "confirm_receive_inline") + cancelInlineReceiveButton := inlineReceiveMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_receive_inline") + acceptInlineReceiveButton.Data = id + cancelInlineReceiveButton.Data = id + + inlineReceiveMenu.Inline( + inlineReceiveMenu.Row( + acceptInlineReceiveButton, + cancelInlineReceiveButton), + ) result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: inlineReceiveMenu.InlineKeyboard} results[i] = result @@ -168,6 +162,7 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { inlineReceive.To = from // The user who wants to receive // add result to persistent struct inlineReceive.Message = inlineMessage + inlineReceive.LanguageCode = ctx.Value("publicLanguageCode").(string) runtime.IgnoreError(bot.bunt.Set(inlineReceive)) } @@ -212,7 +207,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac fromUserStr := GetUserStr(from.Telegram) if from.Telegram.ID == to.Telegram.ID { - bot.trySendMessage(from.Telegram, sendYourselfMessage) + bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) return } // balance check of the user @@ -225,7 +220,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac // check if fromUser has balance if balance < inlineReceive.Amount { log.Errorf("[acceptInlineReceiveHandler] balance of user %s too low", fromUserStr) - bot.trySendMessage(from.Telegram, fmt.Sprintf(inlineSendBalanceLowMessage, balance)) + bot.trySendMessage(from.Telegram, fmt.Sprintf(Translate(ctx, "inlineSendBalanceLowMessage"), balance)) return } @@ -233,33 +228,33 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac bot.inactivateReceive(inlineReceive) // todo: user new get username function to get userStrings - transactionMemo := fmt.Sprintf("Send from %s to %s (%d sat).", fromUserStr, toUserStr, inlineReceive.Amount) - t := NewTransaction(bot, from, to, inlineReceive.Amount, TransactionType("inline send")) + transactionMemo := fmt.Sprintf("InlineReceive from %s to %s (%d sat).", fromUserStr, toUserStr, inlineReceive.Amount) + t := NewTransaction(bot, from, to, inlineReceive.Amount, TransactionType("inline receive")) t.Memo = transactionMemo success, err := t.Send() if !success { errMsg := fmt.Sprintf("[acceptInlineReceiveHandler] Transaction failed: %s", err) log.Errorln(errMsg) - bot.tryEditMessage(c.Message, inlineReceiveFailedMessage, &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, bot.Translate(inlineReceive.LanguageCode, "inlineReceiveFailedMessage"), &tb.ReplyMarkup{}) return } log.Infof("[acceptInlineReceiveHandler] %d sat from %s to %s", inlineReceive.Amount, fromUserStr, toUserStr) - inlineReceive.Message = fmt.Sprintf("%s", fmt.Sprintf(inlineSendUpdateMessageAccept, inlineReceive.Amount, fromUserStrMd, toUserStrMd)) + inlineReceive.Message = fmt.Sprintf("%s", fmt.Sprintf(bot.Translate(inlineReceive.LanguageCode, "inlineSendUpdateMessageAccept"), inlineReceive.Amount, fromUserStrMd, toUserStrMd)) memo := inlineReceive.Memo if len(memo) > 0 { - inlineReceive.Message = inlineReceive.Message + fmt.Sprintf(inlineReceiveAppendMemo, memo) + inlineReceive.Message = inlineReceive.Message + fmt.Sprintf(bot.Translate(inlineReceive.LanguageCode, "inlineReceiveAppendMemo"), memo) } if !to.Initialized { - inlineReceive.Message += "\n\n" + fmt.Sprintf(inlineSendCreateWalletMessage, GetUserStrMd(bot.telegram.Me)) + inlineReceive.Message += "\n\n" + fmt.Sprintf(bot.Translate(inlineReceive.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) } bot.tryEditMessage(c.Message, inlineReceive.Message, &tb.ReplyMarkup{}) // notify users - _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(sendReceivedMessage, fromUserStrMd, inlineReceive.Amount)) - _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(tipSentMessage, inlineReceive.Amount, toUserStrMd)) + _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(bot.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, inlineReceive.Amount)) + _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(bot.Translate(from.Telegram.LanguageCode, "sendSentMessage"), inlineReceive.Amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[acceptInlineReceiveHandler] Error: Receive message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -267,14 +262,14 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac } } -func (bot *TipBot) cancelInlineReceiveHandler(c *tb.Callback) { +func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { inlineReceive, err := bot.getInlineReceive(c) if err != nil { log.Errorf("[cancelInlineReceiveHandler] %s", err) return } if c.Sender.ID == inlineReceive.To.Telegram.ID { - bot.tryEditMessage(c.Message, inlineReceiveCancelledMessage, &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, bot.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineReceive inactive inlineReceive.Active = false inlineReceive.InTransaction = false diff --git a/inline_send.go b/inline_send.go index 471c5e14..029ec156 100644 --- a/inline_send.go +++ b/inline_send.go @@ -12,24 +12,6 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - inlineSendMessage = "Press ✅ to receive payment from %s.\n\n💸 Amount: %d sat" - inlineSendAppendMemo = "\n✉️ %s" - inlineSendUpdateMessageAccept = "💸 %d sat sent from %s to %s." - inlineSendCreateWalletMessage = "Chat with %s 👈 to manage your wallet." - sendYourselfMessage = "📖 You can't pay to yourself." - inlineSendFailedMessage = "🚫 Send failed." - inlineSendInvalidAmountMessage = "🚫 Amount must be larger than 0." - inlineSendBalanceLowMessage = "🚫 Your balance is too low (👑 %d sat)." -) - -const ( - inlineQuerySendTitle = "💸 Send payment to a chat." - inlineQuerySendDescription = "Usage: @%s send []" - inlineResultSendTitle = "💸 Send %d sat." - inlineResultSendDescription = "👉 Click to send %d sat to this chat." -) - var ( inlineSendMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} btnCancelInlineSend = inlineSendMenu.Data("🚫 Cancel", "cancel_send_inline") @@ -45,6 +27,7 @@ type InlineSend struct { ID string `json:"inline_send_id"` Active bool `json:"inline_send_active"` InTransaction bool `json:"inline_send_intransaction"` + LanguageCode string `json:"languagecode"` } func NewInlineSend() *InlineSend { @@ -103,15 +86,15 @@ func (bot *TipBot) getInlineSend(c *tb.Callback) (*InlineSend, error) { for inlineSend.InTransaction { select { case <-ticker.C: - return nil, fmt.Errorf("inline send timeout") + return nil, fmt.Errorf("inline send %s timeout", inlineSend.ID) default: - log.Infoln("in transaction") + log.Warnf("[getInlineSend] %s in transaction", inlineSend.ID) time.Sleep(time.Duration(500) * time.Millisecond) err = bot.bunt.Get(inlineSend) } } if err != nil { - return nil, fmt.Errorf("could not get inline send message") + return nil, fmt.Errorf("could not get inline send %s", inlineSend.ID) } return inlineSend, nil @@ -123,11 +106,11 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { var err error inlineSend.Amount, err = decodeAmountFromCommand(q.Text) if err != nil { - bot.inlineQueryReplyWithError(q, inlineQuerySendTitle, fmt.Sprintf(inlineQuerySendDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQuerySendTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.telegram.Me.Username)) return } if inlineSend.Amount < 1 { - bot.inlineQueryReplyWithError(q, inlineSendInvalidAmountMessage, fmt.Sprintf(inlineQuerySendDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQuerySendDescription"), bot.telegram.Me.Username)) return } fromUser := LoadUser(ctx) @@ -141,7 +124,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { // check if fromUser has balance if balance < inlineSend.Amount { log.Errorf("Balance of user %s too low", fromUserStr) - bot.inlineQueryReplyWithError(q, fmt.Sprintf(inlineSendBalanceLowMessage, balance), fmt.Sprintf(inlineQuerySendDescription, bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, fmt.Sprintf(TranslateUser(ctx, "inlineSendBalanceLowMessage"), balance), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.telegram.Me.Username)) return } @@ -154,24 +137,31 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { results := make(tb.Results, len(urls)) // []tb.Result for i, url := range urls { - inlineMessage := fmt.Sprintf(inlineSendMessage, fromUserStr, inlineSend.Amount) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineSendMessage"), fromUserStr, inlineSend.Amount) if len(inlineSend.Memo) > 0 { - inlineMessage = inlineMessage + fmt.Sprintf(inlineSendAppendMemo, inlineSend.Memo) + inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineSendAppendMemo"), inlineSend.Memo) } result := &tb.ArticleResult{ // URL: url, Text: inlineMessage, - Title: fmt.Sprintf(inlineResultSendTitle, inlineSend.Amount), - Description: fmt.Sprintf(inlineResultSendDescription, inlineSend.Amount), + Title: fmt.Sprintf(TranslateUser(ctx, "inlineResultSendTitle"), inlineSend.Amount), + Description: fmt.Sprintf(TranslateUser(ctx, "inlineResultSendDescription"), inlineSend.Amount), // required for photos ThumbURL: url, } id := fmt.Sprintf("inl-send-%d-%d-%s", q.From.ID, inlineSend.Amount, RandStringRunes(5)) - btnAcceptInlineSend.Data = id - btnCancelInlineSend.Data = id - inlineSendMenu.Inline(inlineSendMenu.Row(btnAcceptInlineSend, btnCancelInlineSend)) + acceptInlineSendButton := inlineSendMenu.Data(Translate(ctx, "receiveButtonMessage"), "confirm_send_inline") + cancelInlineSendButton := inlineSendMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_send_inline") + acceptInlineSendButton.Data = id + cancelInlineSendButton.Data = id + + inlineSendMenu.Inline( + inlineSendMenu.Row( + acceptInlineSendButton, + cancelInlineSendButton), + ) result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: inlineSendMenu.InlineKeyboard} results[i] = result @@ -183,6 +173,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { inlineSend.Message = inlineMessage inlineSend.ID = id inlineSend.From = fromUser + inlineSend.LanguageCode = ctx.Value("publicLanguageCode").(string) // add result to persistent struct runtime.IgnoreError(bot.bunt.Set(inlineSend)) } @@ -224,7 +215,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) inlineSend.To = to.Telegram if fromUser.Telegram.ID == to.Telegram.ID { - bot.trySendMessage(fromUser.Telegram, sendYourselfMessage) + bot.trySendMessage(fromUser.Telegram, Translate(ctx, "sendYourselfMessage")) return } @@ -248,33 +239,33 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) bot.InactivateInlineSend(inlineSend) // todo: user new get username function to get userStrings - transactionMemo := fmt.Sprintf("Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount) + transactionMemo := fmt.Sprintf("InlineSend from %s to %s (%d sat).", fromUserStr, toUserStr, amount) t := NewTransaction(bot, fromUser, to, amount, TransactionType("inline send")) t.Memo = transactionMemo success, err := t.Send() if !success { errMsg := fmt.Sprintf("[sendInline] Transaction failed: %s", err) log.Errorln(errMsg) - bot.tryEditMessage(c.Message, inlineSendFailedMessage, &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, bot.Translate(inlineSend.LanguageCode, "inlineSendFailedMessage"), &tb.ReplyMarkup{}) return } log.Infof("[sendInline] %d sat from %s to %s", amount, fromUserStr, toUserStr) - inlineSend.Message = fmt.Sprintf("%s", fmt.Sprintf(inlineSendUpdateMessageAccept, amount, fromUserStrMd, toUserStrMd)) + inlineSend.Message = fmt.Sprintf("%s", fmt.Sprintf(bot.Translate(inlineSend.LanguageCode, "inlineSendUpdateMessageAccept"), amount, fromUserStrMd, toUserStrMd)) memo := inlineSend.Memo if len(memo) > 0 { - inlineSend.Message = inlineSend.Message + fmt.Sprintf(inlineSendAppendMemo, memo) + inlineSend.Message = inlineSend.Message + fmt.Sprintf(bot.Translate(inlineSend.LanguageCode, "inlineSendAppendMemo"), memo) } if !to.Initialized { - inlineSend.Message += "\n\n" + fmt.Sprintf(inlineSendCreateWalletMessage, GetUserStrMd(bot.telegram.Me)) + inlineSend.Message += "\n\n" + fmt.Sprintf(bot.Translate(inlineSend.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) } bot.tryEditMessage(c.Message, inlineSend.Message, &tb.ReplyMarkup{}) // notify users - _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(sendReceivedMessage, fromUserStrMd, amount)) - _, err = bot.telegram.Send(fromUser.Telegram, fmt.Sprintf(tipSentMessage, amount, toUserStrMd)) + _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(bot.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) + _, err = bot.telegram.Send(fromUser.Telegram, fmt.Sprintf(bot.Translate(fromUser.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[sendInline] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -282,14 +273,14 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) } } -func (bot *TipBot) cancelInlineSendHandler(c *tb.Callback) { +func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) { inlineSend, err := bot.getInlineSend(c) if err != nil { log.Errorf("[cancelInlineSendHandler] %s", err) return } if c.Sender.ID == inlineSend.From.Telegram.ID { - bot.tryEditMessage(c.Message, sendCancelledMessage, &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, bot.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineSend inactive inlineSend.Active = false inlineSend.InTransaction = false diff --git a/interceptor.go b/interceptor.go index 293ece4c..a7486cc3 100644 --- a/interceptor.go +++ b/interceptor.go @@ -4,6 +4,8 @@ import ( "context" "fmt" + "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" @@ -65,6 +67,45 @@ func (bot TipBot) loadReplyToInterceptor(ctx context.Context, i interface{}) (co return ctx, invalidTypeError } +func (bot TipBot) localizerInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + var userLanguageCodeContext context.Context + var publicLanguageCodeContext context.Context + var userLocalizerContext context.Context + var publicLocalizerContext context.Context + var userLocalizer *i18n.Localizer + var publicLocalizer *i18n.Localizer + + // default language is english + publicLocalizer = i18n.NewLocalizer(bot.bundle, "en") + publicLanguageCodeContext = context.WithValue(ctx, "publicLanguageCode", "en") + publicLocalizerContext = context.WithValue(publicLanguageCodeContext, "publicLocalizer", publicLocalizer) + + switch i.(type) { + case *tb.Message: + m := i.(*tb.Message) + userLocalizer = i18n.NewLocalizer(bot.bundle, m.Sender.LanguageCode) + userLanguageCodeContext = context.WithValue(publicLocalizerContext, "userLanguageCode", m.Sender.LanguageCode) + userLocalizerContext = context.WithValue(userLanguageCodeContext, "userLocalizer", userLocalizer) + if m.Private() { + // in pm overwrite public localizer with user localizer + publicLanguageCodeContext = context.WithValue(userLocalizerContext, "publicLanguageCode", m.Sender.LanguageCode) + publicLocalizerContext = context.WithValue(publicLanguageCodeContext, "publicLocalizer", userLocalizer) + } + return publicLocalizerContext, nil + case *tb.Callback: + m := i.(*tb.Callback) + userLocalizer = i18n.NewLocalizer(bot.bundle, m.Sender.LanguageCode) + userLanguageCodeContext = context.WithValue(publicLocalizerContext, "userLanguageCode", m.Sender.LanguageCode) + return context.WithValue(userLanguageCodeContext, "userLocalizer", userLocalizer), nil + case *tb.Query: + m := i.(*tb.Query) + userLocalizer = i18n.NewLocalizer(bot.bundle, m.From.LanguageCode) + userLanguageCodeContext = context.WithValue(publicLocalizerContext, "userLanguageCode", m.From.LanguageCode) + return context.WithValue(userLanguageCodeContext, "userLocalizer", userLocalizer), nil + } + return ctx, nil +} + func (bot TipBot) requirePrivateChatInterceptor(ctx context.Context, i interface{}) (context.Context, error) { switch i.(type) { case *tb.Message: @@ -89,10 +130,41 @@ func (bot TipBot) logMessageInterceptor(ctx context.Context, i interface{}) (con log.Infof("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, photoTag) } return ctx, nil + case *tb.Callback: + m := i.(*tb.Callback) + log.Infof("[Callback %s:%d] Data: %s", GetUserStr(m.Sender), m.Sender.ID, m.Data) + return ctx, nil } return nil, invalidTypeError } +// LoadUser from context +func LoadUserLocalizer(ctx context.Context) *i18n.Localizer { + u := ctx.Value("userLocalizer") + if u != nil { + return u.(*i18n.Localizer) + } + return nil +} + +// LoadUser from context +func LoadPublicLocalizer(ctx context.Context) *i18n.Localizer { + u := ctx.Value("publicLocalizer") + if u != nil { + return u.(*i18n.Localizer) + } + return nil +} + +// // LoadUser from context +// func LoadLocalizer(ctx context.Context) *i18n.Localizer { +// u := ctx.Value("localizer") +// if u != nil { +// return u.(*i18n.Localizer) +// } +// return nil +// } + // LoadUser from context func LoadUser(ctx context.Context) *lnbits.User { u := ctx.Value("user") diff --git a/internal/i18n/localize.go b/internal/i18n/localize.go new file mode 100644 index 00000000..50d7d9bc --- /dev/null +++ b/internal/i18n/localize.go @@ -0,0 +1,23 @@ +package i18n + +import ( + "github.com/BurntSushi/toml" + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" +) + +func init() { + +} + +func RegisterLanguages() *i18n.Bundle { + + bundle := i18n.NewBundle(language.English) + bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + bundle.MustLoadMessageFile("translations/en.toml") + bundle.LoadMessageFile("translations/de.toml") + bundle.LoadMessageFile("translations/es.toml") + bundle.LoadMessageFile("translations/nl.toml") + + return bundle +} diff --git a/invoice.go b/invoice.go index 77cc6024..26d259d1 100644 --- a/invoice.go +++ b/invoice.go @@ -13,19 +13,11 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - invoiceEnterAmountMessage = "Did you enter an amount?" - invoiceValidAmountMessage = "Did you enter a valid amount?" - invoiceHelpText = "📖 Oops, that didn't work. %s\n\n" + - "*Usage:* `/invoice []`\n" + - "*Example:* `/invoice 1000 Thank you!`" -) - -func helpInvoiceUsage(errormsg string) string { +func helpInvoiceUsage(ctx context.Context, errormsg string) string { if len(errormsg) > 0 { - return fmt.Sprintf(invoiceHelpText, fmt.Sprintf("%s", errormsg)) + return fmt.Sprintf(Translate(ctx, "invoiceHelpText"), fmt.Sprintf("%s", errormsg)) } else { - return fmt.Sprintf(invoiceHelpText, "") + return fmt.Sprintf(Translate(ctx, "invoiceHelpText"), "") } } @@ -38,7 +30,7 @@ func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { return } if len(strings.Split(m.Text, " ")) < 2 { - bot.trySendMessage(m.Sender, helpInvoiceUsage(invoiceEnterAmountMessage)) + bot.trySendMessage(m.Sender, helpInvoiceUsage(ctx, Translate(ctx, "invoiceEnterAmountMessage"))) return } @@ -54,7 +46,7 @@ func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { } if amount > 0 { } else { - bot.trySendMessage(m.Sender, helpInvoiceUsage(invoiceValidAmountMessage)) + bot.trySendMessage(m.Sender, helpInvoiceUsage(ctx, Translate(ctx, "invoiceValidAmountMessage"))) return } diff --git a/link.go b/link.go index d2a25e74..033780e2 100644 --- a/link.go +++ b/link.go @@ -4,22 +4,15 @@ import ( "bytes" "context" "fmt" + log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" tb "gopkg.in/tucnak/telebot.v2" ) -var ( - walletConnectMessage = "🔗 *Link your wallet*\n\n" + - "⚠️ Never share the URL or the QR code with anyone or they will be able to access your funds.\n\n" + - "- *BlueWallet:* Press *New wallet*, *Import wallet*, *Scan or import a file*, and scan the QR code.\n" + - "- *Zeus:* Copy the URL below, press *Add a new node*, *Import* (the URL), *Save Node Config*." - couldNotLinkMessage = "🚫 Couldn't link your wallet. Please try again later." -) - func (bot TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { if Configuration.Lnbits.LnbitsPublicUrl == "" { - bot.trySendMessage(m.Sender, couldNotLinkMessage) + bot.trySendMessage(m.Sender, Translate(ctx, "couldNotLinkMessage")) return } // check and print all commands @@ -31,7 +24,7 @@ func (bot TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { } // first check whether the user is initialized fromUser := LoadUser(ctx) - bot.trySendMessage(m.Sender, walletConnectMessage) + bot.trySendMessage(m.Sender, Translate(ctx, "walletConnectMessage")) lndhubUrl := fmt.Sprintf("lndhub://admin:%s@%slndhub/ext/", fromUser.Wallet.Adminkey, Configuration.Lnbits.LnbitsPublicUrl) diff --git a/lnurl.go b/lnurl.go index 4f718b86..d6976851 100644 --- a/lnurl.go +++ b/lnurl.go @@ -20,20 +20,6 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - lnurlReceiveInfoText = "👇 You can use this LNURL to receive payments." - lnurlResolvingUrlMessage = "🧮 Resolving address..." - lnurlGettingUserMessage = "🧮 Preparing payment..." - lnurlPaymentFailed = "🚫 Payment failed: %s" - lnurlInvalidAmountMessage = "🚫 Invalid amount." - lnurlInvalidAmountRangeMessage = "🚫 Amount must be between %d and %d sat." - lnurlNoUsernameMessage = "🚫 You need to set a Telegram username to receive via LNURL." - lnurlEnterAmountMessage = "⌨️ Enter an amount between %d and %d sat." - lnurlHelpText = "📖 Oops, that didn't work. %s\n\n" + - "*Usage:* `/lnurl [amount] `\n" + - "*Example:* `/lnurl LNURL1DP68GUR...`" -) - // lnurlHandler is invoked on /lnurl command func (bot TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { // commands: @@ -44,16 +30,16 @@ func (bot TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { // if only /lnurl is entered, show the lnurl of the user if m.Text == "/lnurl" { - bot.lnurlReceiveHandler(m) + bot.lnurlReceiveHandler(ctx, m) return } // assume payment // HandleLNURL by fiatjaf/go-lnurl - msg := bot.trySendMessage(m.Sender, lnurlResolvingUrlMessage) + msg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlResolvingUrlMessage")) _, params, err := HandleLNURL(m.Text) if err != nil { - bot.tryEditMessage(msg, fmt.Sprintf(lnurlPaymentFailed, "could not resolve LNURL.")) + bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not resolve LNURL.")) log.Errorln(err) return } @@ -65,7 +51,7 @@ func (bot TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { default: err := fmt.Errorf("invalid LNURL type.") log.Errorln(err) - bot.tryEditMessage(msg, fmt.Sprintf(lnurlPaymentFailed, err)) + bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) // bot.trySendMessage(m.Sender, err.Error()) return } @@ -88,7 +74,7 @@ func (bot TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { bot.tryDeleteMessage(msg) // Let the user enter an amount and return - bot.trySendMessage(m.Sender, fmt.Sprintf(lnurlEnterAmountMessage, payParams.MinSendable/1000, payParams.MaxSendable/1000), tb.ForceReply) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlEnterAmountMessage"), payParams.MinSendable/1000, payParams.MaxSendable/1000), tb.ForceReply) } else { // amount is already present in the command // set also amount in the state of the user @@ -131,12 +117,12 @@ func UserGetLNURL(user *tb.User) (string, error) { } // lnurlReceiveHandler outputs the LNURL of the user -func (bot TipBot) lnurlReceiveHandler(m *tb.Message) { +func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { lnurlEncode, err := UserGetLNURL(m.Sender) if err != nil { errmsg := fmt.Sprintf("[lnurlReceiveHandler] Failed to get LNURL: %s", err) log.Errorln(errmsg) - bot.telegram.Send(m.Sender, lnurlNoUsernameMessage) + bot.telegram.Send(m.Sender, Translate(ctx, "lnurlNoUsernameMessage")) } // create qr code qr, err := qrcode.Encode(lnurlEncode, qrcode.Medium, 256) @@ -146,7 +132,7 @@ func (bot TipBot) lnurlReceiveHandler(m *tb.Message) { return } - bot.trySendMessage(m.Sender, lnurlReceiveInfoText) + bot.trySendMessage(m.Sender, Translate(ctx, "lnurlReceiveInfoText")) // send the lnurl data to user bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lnurlEncode)}) } @@ -161,7 +147,7 @@ func (bot TipBot) lnurlEnterAmountHandler(ctx context.Context, m *tb.Message) { a, err := strconv.Atoi(m.Text) if err != nil { log.Errorln(err) - bot.trySendMessage(m.Sender, lnurlInvalidAmountMessage) + bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) ResetUserState(user, bot) return } @@ -177,7 +163,7 @@ func (bot TipBot) lnurlEnterAmountHandler(ctx context.Context, m *tb.Message) { if amount > (stateResponse.MaxSendable/1000) || amount < (stateResponse.MinSendable/1000) { err = fmt.Errorf("amount not in range") log.Errorln(err) - bot.trySendMessage(m.Sender, fmt.Sprintf(lnurlInvalidAmountRangeMessage, stateResponse.MinSendable/1000, stateResponse.MaxSendable/1000)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), stateResponse.MinSendable/1000, stateResponse.MaxSendable/1000)) ResetUserState(user, bot) return } @@ -201,7 +187,7 @@ type LnurlStateResponse struct { // lnurlPayHandler is invoked when the user has delivered an amount and is ready to pay func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { - msg := bot.trySendMessage(c.Sender, lnurlGettingUserMessage) + msg := bot.trySendMessage(c.Sender, Translate(ctx, "lnurlGettingUserMessage")) user := LoadUser(ctx) if user.Wallet == nil { @@ -213,7 +199,7 @@ func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { if err != nil { log.Errorln(err) // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(lnurlPaymentFailed, err)) + bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) return } var stateResponse LnurlStateResponse @@ -221,14 +207,14 @@ func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { if err != nil { log.Errorln(err) // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(lnurlPaymentFailed, err)) + bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) return } callbackUrl, err := url.Parse(stateResponse.Callback) if err != nil { log.Errorln(err) // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(lnurlPaymentFailed, err)) + bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) return } qs := callbackUrl.Query() @@ -239,7 +225,7 @@ func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { if err != nil { log.Errorln(err) // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(lnurlPaymentFailed, err)) + bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) return } var response2 lnurl.LNURLPayResponse2 @@ -247,13 +233,13 @@ func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { if err != nil { log.Errorln(err) // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(lnurlPaymentFailed, err)) + bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) return } json.Unmarshal(body, &response2) if len(response2.PR) < 1 { - bot.tryEditMessage(msg, fmt.Sprintf(lnurlPaymentFailed, "could not receive invoice (wrong address?).")) + bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not receive invoice (wrong address?).")) return } bot.telegram.Delete(msg) diff --git a/pay.go b/pay.go index e74a7dea..1f70d9aa 100644 --- a/pay.go +++ b/pay.go @@ -14,35 +14,17 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - paymentCancelledMessage = "🚫 Payment cancelled." - invoicePaidMessage = "⚡️ Payment sent." - invoicePublicPaidMessage = "⚡️ Payment sent by %s." - // invoicePrivateChatOnlyErrorMessage = "You can pay invoices only in the private chat with the bot." - invalidInvoiceHelpMessage = "Did you enter a valid Lightning invoice? Try /send if you want to send to a Telegram user or Lightning address." - invoiceNoAmountMessage = "🚫 Can't pay invoices without an amount." - insufficientFundsMessage = "🚫 Insufficient funds. You have %d sat but you need at least %d sat." - feeReserveMessage = "⚠️ Sending your entire balance might fail because of network fees. If it fails, try sending a bit less." - invoicePaymentFailedMessage = "🚫 Payment failed: %s" - invoiceUndefinedErrorMessage = "Could not pay invoice." - confirmPayInvoiceMessage = "Do you want to send this payment?\n\n💸 Amount: %d sat" - confirmPayAppendMemo = "\n✉️ %s" - payHelpText = "📖 Oops, that didn't work. %s\n\n" + - "*Usage:* `/pay `\n" + - "*Example:* `/pay lnbc20n1psscehd...`" -) - var ( paymentConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} btnCancelPay = paymentConfirmationMenu.Data("🚫 Cancel", "cancel_pay") btnPay = paymentConfirmationMenu.Data("✅ Pay", "confirm_pay") ) -func helpPayInvoiceUsage(errormsg string) string { +func helpPayInvoiceUsage(ctx context.Context, errormsg string) string { if len(errormsg) > 0 { - return fmt.Sprintf(payHelpText, fmt.Sprintf("%s", errormsg)) + return fmt.Sprintf(Translate(ctx, "payHelpText"), fmt.Sprintf("%s", errormsg)) } else { - return fmt.Sprintf(payHelpText, "") + return fmt.Sprintf(Translate(ctx, "payHelpText"), "") } } @@ -57,6 +39,7 @@ type PayData struct { Amount int64 `json:"amount"` InTransaction bool `json:"intransaction"` Active bool `json:"active"` + LanguageCode string `json:"languagecode"` } func NewPay() *PayData { @@ -136,23 +119,16 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { if user.Wallet == nil { return } - - // if m.Chat.Type != tb.ChatPrivate { - // // delete message - // NewMessage(m, WithDuration(0, bot.telegram)) - // bot.trySendMessage(m.Sender, helpPayInvoiceUsage(invoicePrivateChatOnlyErrorMessage)) - // return - // } if len(strings.Split(m.Text, " ")) < 2 { NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(m.Sender, helpPayInvoiceUsage("")) + bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, "")) return } userStr := GetUserStr(m.Sender) paymentRequest, err := getArgumentFromCommand(m.Text, 1) if err != nil { NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(m.Sender, helpPayInvoiceUsage(invalidInvoiceHelpMessage)) + bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) errmsg := fmt.Sprintf("[/pay] Error: Could not getArgumentFromCommand: %s", err) log.Errorln(errmsg) return @@ -164,7 +140,7 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { // decode invoice bolt11, err := decodepay.Decodepay(paymentRequest) if err != nil { - bot.trySendMessage(m.Sender, helpPayInvoiceUsage(invalidInvoiceHelpMessage)) + bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) errmsg := fmt.Sprintf("[/pay] Error: Could not decode invoice: %s", err) log.Errorln(errmsg) return @@ -172,7 +148,7 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { amount := int(bolt11.MSatoshi / 1000) if amount <= 0 { - bot.trySendMessage(m.Sender, invoiceNoAmountMessage) + bot.trySendMessage(m.Sender, Translate(ctx, "invoiceNoAmountMessage")) errmsg := fmt.Sprint("[/pay] Error: invoice without amount") log.Errorln(errmsg) return @@ -188,17 +164,17 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { } if amount > balance { NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(m.Sender, fmt.Sprintf(insufficientFundsMessage, balance, amount)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "insufficientFundsMessage"), balance, amount)) return } // send warning that the invoice might fail due to missing fee reserve if float64(amount) > float64(balance)*0.99 { - bot.trySendMessage(m.Sender, feeReserveMessage) + bot.trySendMessage(m.Sender, Translate(ctx, "feeReserveMessage")) } - confirmText := fmt.Sprintf(confirmPayInvoiceMessage, amount) + confirmText := fmt.Sprintf(Translate(ctx, "confirmPayInvoiceMessage"), amount) if len(bolt11.Description) > 0 { - confirmText = confirmText + fmt.Sprintf(confirmPayAppendMemo, MarkdownEscape(bolt11.Description)) + confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmPayAppendMemo"), MarkdownEscape(bolt11.Description)) } log.Printf("[/pay] User: %s, amount: %d sat.", userStr, amount) @@ -214,6 +190,7 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { Amount: int64(amount), Memo: bolt11.Description, Message: confirmText, + LanguageCode: ctx.Value("publicLanguageCode").(string), } // add result to persistent struct runtime.IgnoreError(bot.bunt.Set(payData)) @@ -221,9 +198,16 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { SetUserState(user, bot, lnbits.UserStateConfirmPayment, paymentRequest) // // // create inline buttons - btnPay.Data = id - btnCancelPay.Data = id - paymentConfirmationMenu.Inline(paymentConfirmationMenu.Row(btnPay, btnCancelPay)) + payButton := paymentConfirmationMenu.Data(Translate(ctx, "payButtonMessage"), "confirm_pay") + cancelButton := paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_pay") + payButton.Data = id + cancelButton.Data = id + + paymentConfirmationMenu.Inline( + paymentConfirmationMenu.Row( + payButton, + cancelButton), + ) bot.trySendMessage(m.Chat, confirmText, paymentConfirmationMenu) } @@ -270,12 +254,12 @@ func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { // pay invoice invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoiceString}, bot.client) if err != nil { - errmsg := fmt.Sprintf("[/pay] Could not pay invoice of user %s: %s", userStr, err) + errmsg := fmt.Sprintf("[/pay] Could not pay invoice of %s: %s", userStr, err) if len(err.Error()) == 0 { - err = fmt.Errorf(invoiceUndefinedErrorMessage) + err = fmt.Errorf(bot.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) } // bot.trySendMessage(c.Sender, fmt.Sprintf(invoicePaymentFailedMessage, err)) - bot.tryEditMessage(c.Message, fmt.Sprintf(invoicePaymentFailedMessage, err), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, fmt.Sprintf(bot.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), err), &tb.ReplyMarkup{}) log.Errorln(errmsg) return } @@ -283,12 +267,14 @@ func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { payData.InTransaction = false if c.Message.Private() { - bot.tryEditMessage(c.Message, invoicePaidMessage, &tb.ReplyMarkup{}) + // if the command was invoked in private chat + bot.tryEditMessage(c.Message, bot.Translate(payData.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) } else { - bot.trySendMessage(c.Sender, invoicePaidMessage) - bot.tryEditMessage(c.Message, fmt.Sprintf(invoicePublicPaidMessage, userStr), &tb.ReplyMarkup{}) + // if the command was invoked in group chat + bot.trySendMessage(c.Sender, bot.Translate(payData.LanguageCode, "invoicePaidMessage")) + bot.tryEditMessage(c.Message, fmt.Sprintf(bot.Translate(payData.LanguageCode, "invoicePublicPaidMessage"), userStr), &tb.ReplyMarkup{}) } - log.Printf("[/pay] User %s paid invoice %s", userStr, invoice.PaymentHash) + log.Printf("[pay] User %s paid invoice %d (%d sat)", userStr, payData.ID, payData.Amount) return } @@ -307,7 +293,7 @@ func (bot TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { if payData.From.Telegram.ID != c.Sender.ID { return } - bot.tryEditMessage(c.Message, paymentCancelledMessage, &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, bot.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) payData.InTransaction = false bot.InactivatePay(payData) } diff --git a/photo.go b/photo.go index 88b00ed3..67cb6298 100644 --- a/photo.go +++ b/photo.go @@ -14,11 +14,6 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -var ( - photoQrNotRecognizedMessage = "🚫 Could not regocognize a Lightning invoice. Try to center the QR code, crop the photo, or zoom in." - photoQrRecognizedMessage = "✅ QR code:\n`%s`" -) - // TryRecognizeInvoiceFromQrCode will try to read an invoice string from a qr code and invoke the payment handler. func TryRecognizeQrCode(img image.Image) (*gozxing.Result, error) { // check for qr code @@ -62,11 +57,11 @@ func (bot TipBot) photoHandler(ctx context.Context, m *tb.Message) { data, err := TryRecognizeQrCode(img) if err != nil { log.Errorf("[photoHandler] tryRecognizeQrCodes error: %v\n", err) - bot.trySendMessage(m.Sender, photoQrNotRecognizedMessage) + bot.trySendMessage(m.Sender, Translate(ctx, "photoQrNotRecognizedMessage")) return } - bot.trySendMessage(m.Sender, fmt.Sprintf(photoQrRecognizedMessage, data.String())) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "photoQrRecognizedMessage"), data.String())) // invoke payment handler if lightning.IsInvoice(data.String()) { m.Text = fmt.Sprintf("/pay %s", data.String()) diff --git a/send.go b/send.go index c45e000e..d1245db0 100644 --- a/send.go +++ b/send.go @@ -14,41 +14,24 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - sendValidAmountMessage = "Did you enter a valid amount?" - sendUserHasNoWalletMessage = "🚫 User %s hasn't created a wallet yet." - sendSentMessage = "💸 %d sat sent to %s." - sendPublicSentMessage = "💸 %d sat sent from %s to %s." - sendReceivedMessage = "🏅 %s sent you %d sat." - sendErrorMessage = "🚫 Send failed." - confirmSendMessage = "Do you want to pay to %s?\n\n💸 Amount: %d sat" - confirmSendAppendMemo = "\n✉️ %s" - sendCancelledMessage = "🚫 Send cancelled." - errorTryLaterMessage = "🚫 Error. Please try again later." - sendHelpText = "📖 Oops, that didn't work. %s\n\n" + - "*Usage:* `/send []`\n" + - "*Example:* `/send 1000 @LightningTipBot I just like the bot ❤️`\n" + - "*Example:* `/send 1234 LightningTipBot@ln.tips`" -) - var ( sendConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} btnCancelSend = sendConfirmationMenu.Data("🚫 Cancel", "cancel_send") btnSend = sendConfirmationMenu.Data("✅ Send", "confirm_send") ) -func helpSendUsage(errormsg string) string { +func helpSendUsage(ctx context.Context, errormsg string) string { if len(errormsg) > 0 { - return fmt.Sprintf(sendHelpText, fmt.Sprintf("%s", errormsg)) + return fmt.Sprintf(Translate(ctx, "sendHelpText"), fmt.Sprintf("%s", errormsg)) } else { - return fmt.Sprintf(sendHelpText, "") + return fmt.Sprintf(Translate(ctx, "sendHelpText"), "") } } -func (bot *TipBot) SendCheckSyntax(m *tb.Message) (bool, string) { +func (bot *TipBot) SendCheckSyntax(ctx context.Context, m *tb.Message) (bool, string) { arguments := strings.Split(m.Text, " ") if len(arguments) < 2 { - return false, fmt.Sprintf("Did you enter an amount and a recipient? You can use the /send command to either send to Telegram users like %s or to a Lightning address like LightningTipBot@ln.tips.", GetUserStrMd(bot.telegram.Me)) + return false, fmt.Sprintf(Translate(ctx, "sendSyntaxErrorMessage"), GetUserStrMd(bot.telegram.Me)) } return true, "" } @@ -63,6 +46,7 @@ type SendData struct { Amount int64 `json:"amount"` InTransaction bool `json:"intransaction"` Active bool `json:"active"` + LanguageCode string `json:"languagecode"` } func NewSend() *SendData { @@ -153,8 +137,8 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { return } - if ok, errstr := bot.SendCheckSyntax(m); !ok { - bot.trySendMessage(m.Sender, helpSendUsage(errstr)) + if ok, errstr := bot.SendCheckSyntax(ctx, m); !ok { + bot.trySendMessage(m.Sender, helpSendUsage(ctx, errstr)) NewMessage(m, WithDuration(0, bot.telegram)) return } @@ -192,7 +176,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { log.Errorln(errmsg) // immediately delete if the amount is bullshit NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(m.Sender, helpSendUsage(sendValidAmountMessage)) + bot.trySendMessage(m.Sender, helpSendUsage(ctx, Translate(ctx, "sendValidAmountMessage"))) return } @@ -225,14 +209,14 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { toUserDb, err := GetUserByTelegramUsername(toUserStrWithoutAt, *bot) if err != nil { NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(m.Sender, fmt.Sprintf(sendUserHasNoWalletMessage, toUserStrMention)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), toUserStrMention)) return } // entire text of the inline object - confirmText := fmt.Sprintf(confirmSendMessage, MarkdownEscape(toUserStrMention), amount) + confirmText := fmt.Sprintf(Translate(ctx, "confirmSendMessage"), MarkdownEscape(toUserStrMention), amount) if len(sendMemo) > 0 { - confirmText = confirmText + fmt.Sprintf(confirmSendAppendMemo, MarkdownEscape(sendMemo)) + confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmSendAppendMemo"), MarkdownEscape(sendMemo)) } // object that holds all information about the send payment id := fmt.Sprintf("send-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) @@ -246,26 +230,31 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { ToTelegramUser: toUserStrWithoutAt, Memo: sendMemo, Message: confirmText, + LanguageCode: ctx.Value("publicLanguageCode").(string), } - // add result to persistent struct + // save persistent struct runtime.IgnoreError(bot.bunt.Set(sendData)) sendDataJson, err := json.Marshal(sendData) if err != nil { NewMessage(m, WithDuration(0, bot.telegram)) log.Printf("[/send] Error: %s\n", err.Error()) - bot.trySendMessage(m.Sender, fmt.Sprint(errorTryLaterMessage)) + bot.trySendMessage(m.Sender, fmt.Sprint(Translate(ctx, "errorTryLaterMessage"))) return } // save the send data to the database // log.Debug(sendData) SetUserState(user, *bot, lnbits.UserStateConfirmSend, string(sendDataJson)) - - btnSend.Data = id - btnCancelSend.Data = id - - sendConfirmationMenu.Inline(sendConfirmationMenu.Row(btnSend, btnCancelSend)) - + sendButton := sendConfirmationMenu.Data(Translate(ctx, "sendButtonMessage"), "confirm_send") + cancelButton := sendConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_send") + sendButton.Data = id + cancelButton.Data = id + + sendConfirmationMenu.Inline( + sendConfirmationMenu.Row( + sendButton, + cancelButton), + ) if m.Private() { bot.trySendMessage(m.Chat, confirmText, sendConfirmationMenu) } else { @@ -313,7 +302,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { sendMemo := sendData.Memo // we can now get the wallets of both users - to, err := GetUser(&tb.User{ID: toId, Username: toUserStrWithoutAt}, *bot) + to, err := GetLnbitsUser(&tb.User{ID: toId, Username: toUserStrWithoutAt}, *bot) if err != nil { log.Errorln(err.Error()) bot.tryDeleteMessage(c.Message) @@ -333,19 +322,24 @@ 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, sendErrorMessage, &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, fmt.Sprintf("%s %s", bot.Translate(sendData.LanguageCode, "sendErrorMessage"), err), &tb.ReplyMarkup{}) return } + log.Infof("[send] Transaction sent from %s to %s (%d sat).", fromUserStr, toUserStr, amount) + sendData.InTransaction = false - bot.trySendMessage(to.Telegram, fmt.Sprintf(sendReceivedMessage, fromUserStrMd, amount)) - // bot.trySendMessage(from.Telegram, fmt.Sprintf(sendSentMessage, amount, toUserStrMd)) + // notify to user + bot.trySendMessage(to.Telegram, fmt.Sprintf(bot.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) + // bot.trySendMessage(from.Telegram, fmt.Sprintf(Translate(ctx, "sendSentMessage"), amount, toUserStrMd)) if c.Message.Private() { - bot.tryEditMessage(c.Message, fmt.Sprintf(sendSentMessage, amount, toUserStrMd), &tb.ReplyMarkup{}) + // if the command was invoked in private chat + bot.tryEditMessage(c.Message, fmt.Sprintf(bot.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd), &tb.ReplyMarkup{}) } else { - bot.trySendMessage(c.Sender, fmt.Sprintf(sendSentMessage, amount, toUserStrMd)) - bot.tryEditMessage(c.Message, fmt.Sprintf(sendPublicSentMessage, amount, fromUserStrMd, toUserStrMd), &tb.ReplyMarkup{}) + // if the command was invoked in group chat + bot.trySendMessage(c.Sender, fmt.Sprintf(bot.Translate(from.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) + bot.tryEditMessage(c.Message, fmt.Sprintf(bot.Translate(sendData.LanguageCode, "sendPublicSentMessage"), amount, fromUserStrMd, toUserStrMd), &tb.ReplyMarkup{}) } // send memo if it was present if len(sendMemo) > 0 { @@ -370,17 +364,7 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { return } // remove buttons from confirmation message - bot.tryEditMessage(c.Message, sendCancelledMessage, &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, bot.Translate(sendData.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) sendData.InTransaction = false bot.InactivateSend(sendData) - // // delete the confirmation message - // bot.tryDeleteMessage(c.Message) - // // notify the user - // bot.trySendMessage(c.Sender, sendCancelledMessage) - - // // set the inlineSend inactive - // sendData.Active = false - // sendData.InTransaction = false - // runtime.IgnoreError(bot.bunt.Set(sendData)) - } diff --git a/start.go b/start.go index 7139c757..6101c310 100644 --- a/start.go +++ b/start.go @@ -13,15 +13,7 @@ import ( "gorm.io/gorm" ) -const ( - startSettingWalletMessage = "🧮 Setting up your wallet..." - startWalletCreatedMessage = "🧮 Wallet created." - startWalletReadyMessage = "✅ *Your wallet is ready.*" - startWalletErrorMessage = "🚫 Error initializing your wallet. Try again later." - startNoUsernameMessage = "☝️ It looks like you don't have a Telegram @username yet. That's ok, you don't need one to use this bot. However, to make better use of your wallet, set up a username in the Telegram settings. Then, enter /balance so the bot can update its record of you." -) - -func (bot TipBot) startHandler(m *tb.Message) { +func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) { if !m.Private() { return } @@ -29,22 +21,22 @@ func (bot TipBot) startHandler(m *tb.Message) { // WILL RESULT IN AN ENDLESS LOOP OTHERWISE // bot.helpHandler(m) log.Printf("[/start] User: %s (%d)\n", m.Sender.Username, m.Sender.ID) - walletCreationMsg, err := bot.telegram.Send(m.Sender, startSettingWalletMessage) + walletCreationMsg, err := bot.telegram.Send(m.Sender, Translate(ctx, "startSettingWalletMessage")) user, err := bot.initWallet(m.Sender) if err != nil { log.Errorln(fmt.Sprintf("[startHandler] Error with initWallet: %s", err.Error())) - bot.tryEditMessage(walletCreationMsg, startWalletErrorMessage) + bot.tryEditMessage(walletCreationMsg, Translate(ctx, "startWalletErrorMessage")) return } bot.tryDeleteMessage(walletCreationMsg) userContext := context.WithValue(context.Background(), "user", user) bot.helpHandler(userContext, m) - bot.trySendMessage(m.Sender, startWalletReadyMessage) + bot.trySendMessage(m.Sender, Translate(ctx, "startWalletReadyMessage")) bot.balanceHandler(userContext, m) // send the user a warning about the fact that they need to set a username if len(m.Sender.Username) == 0 { - bot.trySendMessage(m.Sender, startNoUsernameMessage, tb.NoPreview) + bot.trySendMessage(m.Sender, Translate(ctx, "startNoUsernameMessage"), tb.NoPreview) } return } diff --git a/text.go b/text.go index 81e9afdd..34166f0a 100644 --- a/text.go +++ b/text.go @@ -9,10 +9,6 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - initWalletMessage = "You don't have a wallet yet. Enter */start*" -) - func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { if m.Chat.Type != tb.ChatPrivate { return @@ -21,7 +17,7 @@ func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { // check if user is in database, if not, initialize wallet user := LoadUser(ctx) if user.Wallet == nil || !user.Initialized { - bot.startHandler(m) + bot.startHandler(ctx, m) return } diff --git a/tip.go b/tip.go index 8fb9529b..7408336a 100644 --- a/tip.go +++ b/tip.go @@ -10,33 +10,18 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - tipDidYouReplyMessage = "Did you reply to a message to tip? To reply to any message, right-click -> Reply on your computer or swipe the message on your phone. If you want to send directly to another user, use the /send command." - tipInviteGroupMessage = "ℹ️ By the way, you can invite this bot to any group to start tipping there." - tipEnterAmountMessage = "Did you enter an amount?" - tipValidAmountMessage = "Did you enter a valid amount?" - tipYourselfMessage = "📖 You can't tip yourself." - tipSentMessage = "💸 %d sat sent to %s." - tipReceivedMessage = "🏅 %s has tipped you %d sat." - tipErrorMessage = "🚫 Tip failed." - tipUndefinedErrorMsg = "please try again later" - tipHelpText = "📖 Oops, that didn't work. %s\n\n" + - "*Usage:* `/tip []`\n" + - "*Example:* `/tip 1000 Dank meme!`" -) - -func helpTipUsage(errormsg string) string { +func helpTipUsage(ctx context.Context, errormsg string) string { if len(errormsg) > 0 { - return fmt.Sprintf(tipHelpText, fmt.Sprintf("%s", errormsg)) + return fmt.Sprintf(Translate(ctx, "tipHelpText"), fmt.Sprintf("%s", errormsg)) } else { - return fmt.Sprintf(tipHelpText, "") + return fmt.Sprintf(Translate(ctx, "tipHelpText"), "") } } -func TipCheckSyntax(m *tb.Message) (bool, string) { +func TipCheckSyntax(ctx context.Context, m *tb.Message) (bool, string) { arguments := strings.Split(m.Text, " ") if len(arguments) < 2 { - return false, tipEnterAmountMessage + return false, Translate(ctx, "tipEnterAmountMessage") } return true, "" } @@ -54,13 +39,13 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { // only if message is a reply if !m.IsReply() { NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(m.Sender, helpTipUsage(fmt.Sprintf(tipDidYouReplyMessage))) - bot.trySendMessage(m.Sender, tipInviteGroupMessage) + bot.trySendMessage(m.Sender, helpTipUsage(ctx, Translate(ctx, "tipDidYouReplyMessage"))) + bot.trySendMessage(m.Sender, Translate(ctx, "tipInviteGroupMessage")) return } - if ok, err := TipCheckSyntax(m); !ok { - bot.trySendMessage(m.Sender, helpTipUsage(err)) + if ok, err := TipCheckSyntax(ctx, m); !ok { + bot.trySendMessage(m.Sender, helpTipUsage(ctx, err)) NewMessage(m, WithDuration(0, bot.telegram)) return } @@ -71,7 +56,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { errmsg := fmt.Sprintf("[/tip] Error: Tip amount not valid.") // immediately delete if the amount is bullshit NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(m.Sender, helpTipUsage(tipValidAmountMessage)) + bot.trySendMessage(m.Sender, helpTipUsage(ctx, Translate(ctx, "tipValidAmountMessage"))) log.Errorln(errmsg) return } @@ -87,7 +72,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if from.Telegram.ID == to.Telegram.ID { NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(m.Sender, tipYourselfMessage) + bot.trySendMessage(m.Sender, Translate(ctx, "tipYourselfMessage")) return } @@ -123,7 +108,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { success, err := t.Send() if !success { NewMessage(m, WithDuration(0, bot.telegram)) - bot.trySendMessage(m.Sender, tipErrorMessage) + bot.trySendMessage(m.Sender, Translate(ctx, "tipErrorMessage")) errMsg := fmt.Sprintf("[/tip] Transaction failed: %s", err) log.Errorln(errMsg) return @@ -132,10 +117,10 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { // update tooltip if necessary messageHasTip := tipTooltipHandler(m, bot, amount, to.Initialized) - log.Infof("[tip] %d sat from %s to %s", amount, fromUserStr, toUserStr) + log.Infof("[tip] Transaction sent from %s to %s (%d sat).", fromUserStr, toUserStr, amount) // notify users - _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(tipSentMessage, amount, toUserStrMd)) + _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(Translate(ctx, "tipSentMessage"), amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[/tip] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -146,7 +131,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if !messageHasTip { bot.tryForwardMessage(to.Telegram, m.ReplyTo, tb.Silent) } - bot.trySendMessage(to.Telegram, fmt.Sprintf(tipReceivedMessage, fromUserStrMd, amount)) + bot.trySendMessage(to.Telegram, fmt.Sprintf(bot.Translate(to.Telegram.LanguageCode, "tipReceivedMessage"), fromUserStrMd, amount)) if len(tipMemo) > 0 { bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", MarkdownEscape(tipMemo))) diff --git a/translate.go b/translate.go new file mode 100644 index 00000000..fe1a4db3 --- /dev/null +++ b/translate.go @@ -0,0 +1,32 @@ +package main + +import ( + "context" + + "github.com/nicksnyder/go-i18n/v2/i18n" + log "github.com/sirupsen/logrus" +) + +func Translate(ctx context.Context, MessgeID string) string { + str, err := LoadPublicLocalizer(ctx).Localize(&i18n.LocalizeConfig{MessageID: MessgeID}) + if err != nil { + log.Warnf("Error translating message %s: %s", MessgeID, err) + } + return str +} + +func TranslateUser(ctx context.Context, MessgeID string) string { + str, err := LoadUserLocalizer(ctx).Localize(&i18n.LocalizeConfig{MessageID: MessgeID}) + if err != nil { + log.Warnf("Error translating message %s: %s", MessgeID, err) + } + return str +} + +func (bot *TipBot) Translate(languageCode string, MessgeID string) string { + str, err := i18n.NewLocalizer(bot.bundle, languageCode).Localize(&i18n.LocalizeConfig{MessageID: MessgeID}) + if err != nil { + log.Warnf("Error translating message %s: %s", MessgeID, err) + } + return str +} diff --git a/translations/README.md b/translations/README.md new file mode 100644 index 00000000..50946c7e --- /dev/null +++ b/translations/README.md @@ -0,0 +1,41 @@ +# Translation guide + +Thank you for helping to translate this bot into many different languages. If you chose to translate this bot, please try to test every possible case that you can think of. As time passes, new features will be added and your translation could become out of date. It would be great, if you could update your language's translation if you notice any weird changes. + +## General +* The bot checks the language settings of each Telegram user and translate the interaction with the user (private chats, **inline commands?**) to the user's language, if a translation is available. Otherwise, it will default to english. All messages in groups will be english. If the user does not have a language setting, it will default to english. +* For now, all `/commands` are in english. That means that all `/command` references in the help messages should remain english for now. We plan to implement localized commands, which is why you will find the strings in the translation files. Please chose simple, single-worded, lower-case, for the command translations. +* Please use a spell checker, like Google Docs to check your final translation. Thanks :) + +## Language +* Please use a "kind" and "playful" tone in your translations. We do not have to be very dry and technical in tone. Please use a respectful language. +* Please remember who the prototypical user is: a non-technical fruit-selling lady in Brazil that wants to sell Mangos for Satoshis. + +## Standards +* Please "fork" your translation from the english translation file `en.toml`. Simply copy the file, rename it to your language code (look it up on Google if you're unsure) and start editing :) +* Please use only "sat" as a denominator for amounts, do not use the plural form "sats". +* Please choose an appropriate expression for "Amount" and keep it across the entire translation. +* Please reuse all Emojis in the same location and order as the original text. +* Do not add line breaks. All translations should have the same number of lines. +* Please use english words for Bitcoin- and Lightning-native concepts like "Lightning", "Wallet", "Invoice", "Tip", and other technical terms like "Log", "Bot", etc. **IF** your language does not have a widely-used and recognized alternative for it. If most software in your language uses another word instead of "Wallet" for example, then we should also use that. +* For fixed english terms like "Tip" I recommend using the english version and giving a translation in parenthesis like "... Tips (*kleine Beträge*) senden kann". The text in *italic* is the next best translation of "Tips" + + +## Technical +* Every string should be wrapped in three quotes `"""` +* Strings can span over multiple lines. +* Every string variable found in the original english language file should be translated. If a specific string is missing in a translation, the english version will be used for that particular string. +* Every language has their own translation file. The file for english is `en.toml`. + +* Headings to many sections are **bold** starting and ending with asterix `*`. Italic starts and ends with an underscore `_`. +* Command examples are in `code format` starting end ending with ``` ` ``` + +## Pleaceholders +* Symbols like `%s`, `%d`, `%f` are meant as placeholders for other bits of text, numbers, floats. Please reuse them in every string you translate. +* Please do not change the order of the placeholders in your translation. It would break things. +* Please do not use modifiers for **bold**, *italic*, and others around the placeholders. We are using MarkdownV1 and it would break things. Do not do `_%s_` for example. + +## GitHub infos +* Please submit translations as a GitHub pull-request. This way, you can easily work with others and review each other. To submit a pull-request, you need a Github account. Then, fork the entire project (using the button in the upper-right corner). +* Then, create a new branch for your translation. Do this using the Github UI or via the terminal inside the project repository: `git checkout -b translation_es` for example. Then, create the appropriate language file and put it in the translations folder. Then, add it to the branch with by navigating to the translations folder `git add es.toml` and `git commit -m 'add spanish'`. Finally, push the branch to your fork `git push --set-upstream origin translation_es`. When done, open a pull-request in the *original github repo* and select your forked branch. +* Good luck :) \ No newline at end of file diff --git a/translations/de.toml b/translations/de.toml new file mode 100644 index 00000000..3b2f74f7 --- /dev/null +++ b/translations/de.toml @@ -0,0 +1,296 @@ +# COMMANDS + +helpCommandStr = """hilfe""" +basicsCommandStr = """grundlagen""" +tipCommandStr = """tip""" +balanceCommandStr = """guthaben""" +sendCommandStr = """sende""" +invoiceCommandStr = """invoice""" +payCommandStr = """bezahle""" +donateCommandStr = """spende""" +advancedCommandStr = """fortgeschritten""" +transactionsCommandStr = """transaktionen""" +logCommandStr = """log""" +listCommandStr = """liste""" + +linkCommandStr = """verbinde""" +lnurlCommandStr = """lnurl""" +faucetCommandStr = """zapfhahn""" + +tipjarCommandStr = """spendendose""" +receiveCommandStr = """empfange""" +hideCommandStr = """verstecke""" +volcanoCommandStr = """vulkan""" +showCommandStr = """zeige""" +optionsCommandStr = """optionen""" +settingsCommandStr = """einstellungen""" +saveCommandStr = """save""" +deleteCommandStr = """lösche""" +infoCommandStr = """info""" + +# NOTIFICATIONS + +cantDoThatMessage = """Du kannst das nicht tun.""" +cantClickMessage = """Du kannst diesen Knopf nicht drücken.""" +balanceTooLowMessage = """Dein Guthaben ist zu niedrig.""" + +# BUTTONS + +sendButtonMessage = """✅ Senden""" +payButtonMessage = """✅ Bezahlen""" +payReceiveButtonMessage = """💸 Bezahlen""" +receiveButtonMessage = """✅ Empfangen""" +cancelButtonMessage = """🚫 Abbrechen""" +collectButtonMessage = """✅ Einsammeln""" +nextButtonMessage = """Vor""" +backButtonMessage = """Zurück""" +acceptButtonMessage = """Akzeptieren""" +denyButtonMessage = """Verweigern""" +tipButtonMessage = """Tip""" +revealButtonMessage = """Aufdecken""" +showButtonMessage = """Zeigen""" +hideButtonMessage = """Verstecken""" +joinButtonMessage = """Mitmachen""" +optionsButtonMessage = """Optionen""" +settingsButtonMessage = """Einstellungen""" +saveButtonMessage = """Speichern""" +deleteButtonMessage = """Löschen""" +infoButtonMessage = """Info""" + +# HELP + +helpMessage = """⚡️ *Wallet* +_Dieser Bot ist ein Bitcoin Lightning Wallet der Tips (kleine Beträge) auf Telegram senden kann. Up einen Tip zu senden, füge den Bot in einen Gruppenchat hinzu. Die grundlegende Einheit von Tips sind Satoshis (sat). 100,000,000 sat = 1 Bitcoin. Gebe 📚 /basics ein für mehr Informationen._ + +❤️ *Spenden* +_Dieser Bot erhebt keine Gebühren, kostet aber Satoshis um betrieben zu werden. Wenn du den Bot magst, kannst das Projekt mit eine Spende unterstützen. Um zu spenden, gebe ein: _ `/donate 1000` + +%s + +⚙️ *Befehle* +*/tip* 🏅 Antworte auf eine Nachricht um einen Tip zu senden: `/tip []` +*/balance* 👑 Frage dein Guthaben ab: `/balance` +*/send* 💸 Sende an einen Benutzer: `/send @user oder user@ln.tips []` +*/invoice* ⚡️ Empfange mit Lightning: `/invoice []` +*/pay* ⚡️ Bezahle mit Lightning: `/pay ` +*/donate* ❤️ Spende an das Projekt: `/donate 1000` +*/advanced* 🤖 Fortgerschittene Funktionen. +*/help* 📖 Rufe die Hilfe auf.""" + +infoHelpMessage = """ℹ️ *Info*""" +infoYourLightningAddress = """Deine Lightning Adresse ist `%s`""" + +basicsMessage = """🧡 *Bitcoin* +_Bitcoin ist die Währung des Internets. Es ist offen für jeden, dezentral und es gibt niemanden, der Bitcoin kontrolliert. Bitcoin ist hartes Geld, was schneller, sicherer, und fairer ist, als das klassische Finanzsystem._ + +🧮 *Ökonomie* +_Die kleinste Einheit von Bitcoin sind Satoshis (sat) und 100,000,000 sat = 1 Bitcoin. Es wird niemals mehr als 21 Millionen Bitcoin geben. Der Fiatgeld-Wert von Bitcoin kann sich täglich ändern. Wenn du jedoch auf einem Bitcoin-Standard lebst, wird 1 sat für immer 1 sat Wert sein._ + +⚡️ *Das Lightning Netzwerk* +_Das Lightning Netzwerk ist ein Bezahlprotokoll, das schnelle und günstige Bitcoin Zahlungen erlaubt, die fast keine Energie kosten. Dadurch skaliert Bitcoin auf Milliarden von Menschen weltweit._ + +📲 *Lightning Wallets* +_Dein Geld auf diesem Bot kannst du an jedes andere Lightning Wallet der Welt versenden. Empfholene Lightning Wallets für dein Handy sind _ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (non-custodial), or_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(einfach)_. + +📄 *Open Source* +_Dieser Bot ist kostenlose_ [Open Source](https://github.com/LightningTipBot/LightningTipBot) _Software. Du kannst ihn auf deinem eigenen Rechner laufen lassen und für deine eigene Community betreiben._ + +✈️ *Telegram* +_Füge den Bot in deine Telegram Gruppenchats hinzu, um Nachrichten ein /tip zu senden. Wenn der Bot Admin der Gruppe ist, wird er auch den Chat aufräumen, indem er manche Befehle nach Ausführung löscht._ + +🏛 *Bedingungen* +_Wir sind keine Verwalter deines Geldes. Wir werden in deinem besten Interesse handeln, aber sind uns auch dessen bewusst, dass die Situation ohne KYC etwas kompliziert ist. Solange wir keine andere Lösung gefunden haben, werden wir alle Beträge auf diesem Bot als Spenden betrachten. Gebe uns nicht dein ganzes Geld. Sei dir dessen bewusst, dass dieser Bot immernoch in der Betaphase ist. Benutzung auf eigene Gefahr._ + +❤️ *Spenden* +_Dieser Bot erhebt keine Gebühren, kostet aber Satoshis um betrieben zu werden. Wenn du den Bot magst, kannst das Projekt mit eine Spende unterstützen. Um zu spenden, gebe ein: _ `/donate 1000`""" + +helpNoUsernameMessage = """👋 Bitte seteze einen Telegram Nutzernamen.""" + +advancedMessage = """%s + +👉 *Inline Befehle* +*send* 💸 Sende sats an einen Chat: `%s send []` +*receive* 🏅 Bitte um Zahlung: `%s receive []` +*faucet* 🚰 Erzeuge einen Zapfhahn: `%s faucet ` + +📖 Du kannst Inline Befehle in jedem Chat verwenden, sogar in privaten Nachrichten. Warte eine Sekunde, nachdem du den Befehl eingegeben hast und *klicke* auf das Ergebnis, statt Enter einzugeben. + +⚙️ *Fortgeschrittene Befehle* +*/link* 🔗 Verbinde dein Wallet mit [BlueWallet](https://bluewallet.io/) oder [Zeus](https://zeusln.app/) +*/lnurl* ⚡️ Lnurl empfangen oder senden: `/lnurl` or `/lnurl ` +*/faucet* 🚰 Erzeuge einen Zapfhahn `/faucet `""" + +# START + +startSettingWalletMessage = """🧮 Richte dein Wallet ein...""" +startWalletCreatedMessage = """🧮 Wallet erstellt.""" +startWalletReadyMessage = """✅ *Dein Wallet ist bereit.*""" +startWalletErrorMessage = """🚫 Fehler beim einrichten deines Wallets. Bitte versuche es später noch ein mal.""" +startNoUsernameMessage = """☝️ Es sieht so aus, als hättest du noch keinen Telegram @usernamen. Das ist ok, du brauchst keinen, um diesen Bot zu verwenden. Um jedoch alle Funktionen verwenden zu können, solltest du dir einen Usernamen in den Telegram Einstellungen setzen. Gebe danach ein mal /balance ein, damit der Bot seine Informationen über dich aktualisieren kann.""" + +# BALANCE + +balanceMessage = """👑 *Dein Guthaben:* %d sat""" +balanceErrorMessage = """🚫 Konnte dein Guthaben nicht abfragen. Bitte versuche es später noch ein mal.""" + +# TIP + +tipDidYouReplyMessage = """Hast du auf eine Nachricht geantwortet um ihr ein Tip zu senden? Um zu antworten, rechts-klicke die Nachricht auf deinem Computer oder wische die Nachricht auf deinem Handy. Wenn du direkt Zahlungen an einen anderen Nutzer senden möchtest, benutze den /send Befehl.""" +tipInviteGroupMessage = """ℹ️ Übrigens, du kannst diesen Bot in jeden Gruppenchat einladen und dort Tips verteilen.""" +tipEnterAmountMessage = """Hast du einen Betrag eingegeben?""" +tipValidAmountMessage = """Hast du einen gültigen Betrag eingegeben?""" +tipYourselfMessage = """📖 Du kannst dir nicht selbst Tips senden.""" +tipSentMessage = """💸 %d sat an %s gesendet.""" +tipReceivedMessage = """🏅 %s hat dir ein Tip von %d sat gesendet.""" +tipErrorMessage = """🚫 Tip fehlgeschlagen.""" +tipUndefinedErrorMsg = """bitte versuche es später noch ein mal.""" +tipHelpText = """📖 Ups, das hat nicht geklappt. %s + +*Befehl:* `/tip []` +*Beispiel:* `/tip 1000 Geiles Meme!`""" + +# SEND + +sendValidAmountMessage = """Hast du einen gültigen Betrag eingegeben?""" +sendUserHasNoWalletMessage = """🚫 Der Benutzer %s hat noch keinen Wallet erstellt.""" +sendSentMessage = """💸 %d sat gesendet an %s.""" +sendPublicSentMessage = """💸 %d sat gesendet von %s an %s.""" +sendReceivedMessage = """🏅 %s hat dir %d sat gesendet.""" +sendErrorMessage = """🚫 Senden fehlgeschlagen.""" +confirmSendMessage = """Möchtest eine Zahlung an %s senden?\n\n💸 Betrag: %d sat""" +confirmSendAppendMemo = """\n✉️ %s""" +sendCancelledMessage = """🚫 Senden abgebrochen.""" +errorTryLaterMessage = """🚫 Fehler. Bitte versuche es später noch ein mal.""" +sendSyntaxErrorMessage = """Hast du einen gültigen Betrag und Empfänger eingegeben? Du kannst den /send Befehl verwenden, um entweder an Telegram Nutzer zu senden, wie z.B. %s oder an eine Lightning Adresse, wie LightningTipBot@ln.tips.""" +sendHelpText = """📖 Ups, das hat nicht geklappt. %s + +*Befehl:* `/send []` +*Beispiel:* `/send 1000 @LightningTipBot Ich liebe diesen Bot ❤️` +*Beispiel:* `/send 1234 LightningTipBot@ln.tips`""" + +# INVOICE + +invoiceEnterAmountMessage = """Hast du einen Betrag eingegeben?""" +invoiceValidAmountMessage = """Hast du einen gültigen Betrag eingegeben?""" +invoiceHelpText = """📖 Ups, das hat nicht geklappt. %s + +*Befehl:* `/invoice []` +*Beispiel:* `/invoice 1000 Thank you!`""" + +# PAY + +paymentCancelledMessage = """🚫 Zahlung abgebrochen.""" +invoicePaidMessage = """⚡️ Zahlung gesendet.""" +invoicePublicPaidMessage = """⚡️ Zahlung gesendet von %s.""" +invalidInvoiceHelpMessage = """Hast du eine gültige Lightning Invoice eingegeben? Versuche den /send Befehl, falls du an einen Telegram Nutzer oder eine Lightning Adresse senden willst.""" +invoiceNoAmountMessage = """🚫 Kann keine Invoices ohne Betrag bezahlen.""" +insufficientFundsMessage = """🚫 Guthaben zu niedrig. Du hast %d sat aber brauchst mindestens %d sat.""" +feeReserveMessage = """⚠️ Dein gesamtes Guthaben zu senden könnte wegen den Netzwerkgebühren fehlschlagen. Falls das passiert, versuche es mit einem etwas geringeren Betrag.""" +invoicePaymentFailedMessage = """🚫 Zahlung fehlgeschlagen: %s""" +invoiceUndefinedErrorMessage = """Konnte Invoice nicht bezahlen.""" +confirmPayInvoiceMessage = """Mochtest du diese Zahlung senden?\n\n💸 Betrag: %d sat""" +confirmPayAppendMemo = """\n✉️ %s""" +payHelpText = """📖 Ups, das hat nicht geklappt. %s + +*Befehl:* `/pay ` +*Beispiel:* `/pay lnbc20n1psscehd...`""" + +# DONATE + +donationSuccess = """🙏 Danke für deine Spende.""" +donationErrorMessage = """🚫 Oh nein. Spende fehlgeschlagen.""" +donationProgressMessage = """🧮 Bereite deine Spende vor...""" +donationFailedMessage = """🚫 Spende fehlgeschlagen: %s""" +donateEnterAmountMessage = """Hast du deinen Betrag eingegeben?""" +donateValidAmountMessage = """Hast du einen gültigen Betrag eingegeben?""" +donateHelpText = """📖 Ups, das hat nicht geklappt. %s + +*Befehl:* `/donate ` +*Beispiel:* `/donate 1000`""" + +# PHOTO + +photoQrNotRecognizedMessage = """🚫 Keine Lightning Invoice oder LNURL erkannt. Versuche den QR Code zu zentrieren, beschneide ihn oder zoome mehr heran.""" +photoQrRecognizedMessage = """✅ QR Code: +`%s`""" + +# LNURL + +lnurlReceiveInfoText = """👇 Du kannst diese statische LNURL benutzen um bezahlt zu werden.""" +lnurlResolvingUrlMessage = """🧮 Löse Adresse auf...""" +lnurlGettingUserMessage = """🧮 Bereite Zahlung vor...""" +lnurlPaymentFailed = """🚫 Zahlung fehlgeschlagen: %s""" +lnurlInvalidAmountMessage = """🚫 Ungültiger Betrag.""" +lnurlInvalidAmountRangeMessage = """🚫 Betrag muss zwischen %d und %d sat liegen.""" +lnurlNoUsernameMessage = """🚫 Du musst einen Telegram Username anlegen, um per LNURL Zahlungen zu empfangen.""" +lnurlEnterAmountMessage = """⌨️ Gebe einen Betrag zwischen %d und %d sat ein.""" +lnurlHelpText = """📖 Ups, das hat nicht geklappt. %s + +*Befehl:* `/lnurl [betrag] ` +*Beispiel:* `/lnurl LNURL1DP68GUR...`""" + +# LINK + +walletConnectMessage = """🔗 *Verbinde dein Wallet* + +⚠️ Teile diese URL und den QR code mit niemandem! Jeder, der darauf Zugriff hat, hat auch Zugriff auf dein Konto. + +- *BlueWallet:* Drücke *New wallet*, *Import wallet*, *Scan or import a file*, und scanne den QR Code. +- *Zeus:* Kiere die URL unten, drücke *Add a new node*, *Import* (die URL), *Save Node Config*.""" +couldNotLinkMessage = """🚫 Konnte dein Wallet nicht verbinden. Bitte versuche es später noch ein mal.""" + +# FAUCET + +inlineQueryFaucetTitle = """🚰 Erzeuge einen Zapfhahn.""" +inlineQueryFaucetDescription = """Befehl: @%s faucet """ +inlineResultFaucetTitle = """🚰 Erzeuge einen %d sat Zapfhahn.""" +inlineResultFaucetDescription = """👉 Klicke hier um den Zapfhahn in diesen Chat zu senden.""" + +inlineFaucetMessage = """Drücke ✅ um %d sat aus diesem Zapfhahn zu zapfen. + +🚰 Verbleibend: %d/%d sat (%d/%d gezapft) +%s""" +inlineFaucetEndedMessage = """🚰 Zapfhahn leer 🍺\n\n🏅 %d sat an %d User ausgeschüttet.""" +inlineFaucetAppendMemo = """\n✉️ %s""" +inlineFaucetCreateWalletMessage = """Chatte mit %s 👈 um dein Wallet zu verwalten.""" +inlineFaucetCancelledMessage = """🚫 Zapfhahn abgebrochen.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 Die Pro-User Menge muss ein natürlicher Teiler der Gesamtmenge sein.""" +inlineFaucetInvalidAmountMessage = """🚫 Ungültige Menge.""" +inlineFaucetSentMessage = """🚰 %d sat an %s gesendet.""" +inlineFaucetReceivedMessage = """🚰 %s hat dir %d sat gesendet.""" +inlineFaucetHelpFaucetInGroup = """Erzuge einen Zapfhahn in einer Gruppe, wo der Bot eingeladen ist oder benutze einen 👉 Inline Bommand (/advanced für mehr).""" +inlineFaucetHelpText = """📖 Ups, das hat nicht geklappt. %s + +*Befehl:* `/faucet ` +*Beispiel:* `/faucet 210 21`""" + +# INLINE SEND + +inlineQuerySendTitle = """💸 Sende Zahlungen in einem Chat.""" +inlineQuerySendDescription = """Befehl: @%s send []""" +inlineResultSendTitle = """💸 Sende %d sat.""" +inlineResultSendDescription = """👉 Klicke hier um %d sat in diesen Chat zu senden.""" + +inlineSendMessage = """Klicke ✅ um eine Zahlung von %s zu erhalten.\n\n💸 Betrag: %d sat""" +inlineSendAppendMemo = """\n✉️ %s""" +inlineSendUpdateMessageAccept = """💸 %d sat gesendet von %s an %s.""" +inlineSendCreateWalletMessage = """Chatte mit %s 👈 um dein Wallet zu verwalten.""" +sendYourselfMessage = """📖 Du kannst dich nicht selbst bezahlen.""" +inlineSendFailedMessage = """🚫 Senden fehlgeschlagen.""" +inlineSendInvalidAmountMessage = """🚫 Betrag muss größer als 0 sein.""" +inlineSendBalanceLowMessage = """🚫 Dein Guthaben ist zu niedrig (%d sat).""" + +# INLINE RECEIVE + +inlineQueryReceiveTitle = """🏅 Empfange eine Zahlung in einem Chat.""" +inlineQueryReceiveDescription = """Befehl: @%s receive []""" +inlineResultReceiveTitle = """🏅 Empfange %d sat.""" +inlineResultReceiveDescription = """👉 Klicke hier um eine Zahlung von %d sat zu empfangen.""" + +inlineReceiveMessage = """Drücke 💸 um an %s zu zahlen.\n\n💸 Betrag: %d sat""" +inlineReceiveAppendMemo = """\n✉️ %s""" +inlineReceiveUpdateMessageAccept = """💸 %d sat gesendet von %s an %s.""" +inlineReceiveCreateWalletMessage = """Chatte mit %s 👈 um dein Wallet zu verwalten.""" +inlineReceiveYourselfMessage = """📖 Du kannst dich nicht selbst bezahlen.""" +inlineReceiveFailedMessage = """🚫 Empfangen fehlgeschlagen.""" +inlineReceiveCancelledMessage = """🚫 Empfangen abgebrochen.""" diff --git a/translations/en.toml b/translations/en.toml new file mode 100644 index 00000000..ed0be8cf --- /dev/null +++ b/translations/en.toml @@ -0,0 +1,296 @@ +# COMMANDS + +helpCommandStr = """help""" +basicsCommandStr = """basics""" +tipCommandStr = """tip""" +balanceCommandStr = """balance""" +sendCommandStr = """send""" +invoiceCommandStr = """invoice""" +payCommandStr = """pay""" +donateCommandStr = """donate""" +advancedCommandStr = """advanced""" +transactionsCommandStr = """transactions""" +logCommandStr = """log""" +listCommandStr = """list""" + +linkCommandStr = """link""" +lnurlCommandStr = """lnurl""" +faucetCommandStr = """faucet""" + +tipjarCommandStr = """tipjar""" +receiveCommandStr = """receive""" +hideCommandStr = """hide""" +volcanoCommandStr = """volcano""" +showCommandStr = """show""" +optionsCommandStr = """options""" +settingsCommandStr = """settings""" +saveCommandStr = """save""" +deleteCommandStr = """delete""" +infoCommandStr = """info""" + +# NOTIFICATIONS + +cantDoThatMessage = """You can't do that.""" +cantClickMessage = """You can't click this button.""" +balanceTooLowMessage = """Your balance is too low.""" + +# BUTTONS + +sendButtonMessage = """✅ Send""" +payButtonMessage = """✅ Pay""" +payReceiveButtonMessage = """💸 Pay""" +receiveButtonMessage = """✅ Receive""" +cancelButtonMessage = """🚫 Cancel""" +collectButtonMessage = """✅ Collect""" +nextButtonMessage = """Next""" +backButtonMessage = """Back""" +acceptButtonMessage = """Accept""" +denyButtonMessage = """Deny""" +tipButtonMessage = """Tip""" +revealButtonMessage = """Reveal""" +showButtonMessage = """Show""" +hideButtonMessage = """Hide""" +joinButtonMessage = """Join""" +optionsButtonMessage = """Options""" +settingsButtonMessage = """Settings""" +saveButtonMessage = """Save""" +deleteButtonMessage = """Delete""" +infoButtonMessage = """Info""" + +# HELP + +helpMessage = """⚡️ *Wallet* +_This bot is a Bitcoin Lightning wallet that can sends tips on Telegram. To tip, add the bot to a group chat. The basic unit of tips are Satoshis (sat). 100,000,000 sat = 1 Bitcoin. Type 📚 /basics for more._ + +❤️ *Donate* +_This bot charges no fees but costs Satoshis to operate. If you like the bot, please consider supporting this project with a donation. To donate, use_ `/donate 1000` + +%s + +⚙️ *Commands* +*/tip* 🏅 Reply to a message to tip: `/tip []` +*/balance* 👑 Check your balance: `/balance` +*/send* 💸 Send funds to a user: `/send @user or user@ln.tips []` +*/invoice* ⚡️ Receive with Lightning: `/invoice []` +*/pay* ⚡️ Pay with Lightning: `/pay ` +*/donate* ❤️ Donate to the project: `/donate 1000` +*/advanced* 🤖 Advanced features. +*/help* 📖 Read this help.""" + +infoHelpMessage = """ℹ️ *Info*""" +infoYourLightningAddress = """Your Lightning Address is `%s`""" + +basicsMessage = """🧡 *Bitcoin* +_Bitcoin is the currency of the internet. It is permissionless and decentralized and has no masters and no controling authority. Bitcoin is sound money that is faster, more secure, and more inclusive than the legacy financial system._ + +🧮 *Economnics* +_The smallest unit of Bitcoin are Satoshis (sat) and 100,000,000 sat = 1 Bitcoin. There will only ever be 21 Million Bitcoin. The fiat currency value of Bitcoin can change daily. However, if you live on a Bitcoin standard 1 sat will always equal 1 sat._ + +⚡️ *The Lightning Network* +_The Lightning Network is a payment protocol that enables fast and cheap Bitcoin payments that require almost no energy. It is what scales Bitcoin to the billions of people around the world._ + +📲 *Lightning Wallets* +_Your funds on this bot can be sent to any other Lightning wallet and vice versa. Recommended Lightning wallets for your phone are_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (non-custodial), or_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(easy)_. + +📄 *Open Source* +_This bot is free and_ [open source](https://github.com/LightningTipBot/LightningTipBot) _software. You can run it on your own computer and use it in your own community._ + +✈️ *Telegram* +_Add this bot to your Telegram group chat to /tip posts. If you make the bot admin of the group it will also clean up commands to keep the chat tidy._ + +🏛 *Terms* +_We are not custodian of your funds. We will act in your best interest but we're also aware that the situation without KYC is tricky until we figure something out. Any amount you load onto your wallet will be considered a donation. Do not give us all your money. Be aware that this bot is in beta development. Use at your own risk._ + +❤️ *Donate* +_This bot charges no fees but costs satoshis to operate. If you like the bot, please consider supporting this project with a donation. To donate, use_ `/donate 1000`""" + +helpNoUsernameMessage = """👋 Please set a Telegram username.""" + +advancedMessage = """%s + +👉 *Inline commands* +*send* 💸 Send sats to chat: `%s send []` +*receive* 🏅 Request a payment: `%s receive []` +*faucet* 🚰 Create a faucet: `%s faucet ` + +📖 You can use inline commands in every chat, even in private conversations. Wait a second after entering an inline command and *click* the result, don't press enter. + +⚙️ *Advanced commands* +*/link* 🔗 Link your wallet to [BlueWallet](https://bluewallet.io/) or [Zeus](https://zeusln.app/) +*/lnurl* ⚡️ Lnurl receive or pay: `/lnurl` or `/lnurl ` +*/faucet* 🚰 Create a faucet `/faucet `""" + +# START + +startSettingWalletMessage = """🧮 Setting up your wallet...""" +startWalletCreatedMessage = """🧮 Wallet created.""" +startWalletReadyMessage = """✅ *Your wallet is ready.*""" +startWalletErrorMessage = """🚫 Error initializing your wallet. Try again later.""" +startNoUsernameMessage = """☝️ It looks like you don't have a Telegram @username yet. That's ok, you don't need one to use this bot. However, to make better use of your wallet, set up a username in the Telegram settings. Then, enter /balance so the bot can update its record of you.""" + +# BALANCE + +balanceMessage = """👑 *Your balance:* %d sat""" +balanceErrorMessage = """🚫 Could not fetch your balance. Please try again later.""" + +# TIP + +tipDidYouReplyMessage = """Did you reply to a message to tip? To reply to any message, right-click -> Reply on your computer or swipe the message on your phone. If you want to send directly to another user, use the /send command.""" +tipInviteGroupMessage = """ℹ️ By the way, you can invite this bot to any group to start tipping there.""" +tipEnterAmountMessage = """Did you enter an amount?""" +tipValidAmountMessage = """Did you enter a valid amount?""" +tipYourselfMessage = """📖 You can't tip yourself.""" +tipSentMessage = """💸 %d sat sent to %s.""" +tipReceivedMessage = """🏅 %s has tipped you %d sat.""" +tipErrorMessage = """🚫 Tip failed.""" +tipUndefinedErrorMsg = """please try again later""" +tipHelpText = """📖 Oops, that didn't work. %s + +*Usage:* `/tip []` +*Example:* `/tip 1000 Dank meme!`""" + +# SEND + +sendValidAmountMessage = """Did you enter a valid amount?""" +sendUserHasNoWalletMessage = """🚫 User %s hasn't created a wallet yet.""" +sendSentMessage = """💸 %d sat sent to %s.""" +sendPublicSentMessage = """💸 %d sat sent from %s to %s.""" +sendReceivedMessage = """🏅 %s sent you %d sat.""" +sendErrorMessage = """🚫 Send failed.""" +confirmSendMessage = """Do you want to pay to %s?\n\n💸 Amount: %d sat""" +confirmSendAppendMemo = """\n✉️ %s""" +sendCancelledMessage = """🚫 Send cancelled.""" +errorTryLaterMessage = """🚫 Error. Please try again later.""" +sendSyntaxErrorMessage = """Did you enter an amount and a recipient? You can use the /send command to either send to Telegram users like %s or to a Lightning address like LightningTipBot@ln.tips.""" +sendHelpText = """📖 Oops, that didn't work. %s + +*Usage:* `/send []` +*Example:* `/send 1000 @LightningTipBot I just like the bot ❤️` +*Example:* `/send 1234 LightningTipBot@ln.tips`""" + +# INVOICE + +invoiceEnterAmountMessage = """Did you enter an amount?""" +invoiceValidAmountMessage = """Did you enter a valid amount?""" +invoiceHelpText = """📖 Oops, that didn't work. %s + +*Usage:* `/invoice []` +*Example:* `/invoice 1000 Thank you!`""" + +# PAY + +paymentCancelledMessage = """🚫 Payment cancelled.""" +invoicePaidMessage = """⚡️ Payment sent.""" +invoicePublicPaidMessage = """⚡️ Payment sent by %s.""" +invalidInvoiceHelpMessage = """Did you enter a valid Lightning invoice? Try /send if you want to send to a Telegram user or Lightning address.""" +invoiceNoAmountMessage = """🚫 Can't pay invoices without an amount.""" +insufficientFundsMessage = """🚫 Insufficient funds. You have %d sat but you need at least %d sat.""" +feeReserveMessage = """⚠️ Sending your entire balance might fail because of network fees. If it fails, try sending a bit less.""" +invoicePaymentFailedMessage = """🚫 Payment failed: %s""" +invoiceUndefinedErrorMessage = """Could not pay invoice.""" +confirmPayInvoiceMessage = """Do you want to send this payment?\n\n💸 Amount: %d sat""" +confirmPayAppendMemo = """\n✉️ %s""" +payHelpText = """📖 Oops, that didn't work. %s + +*Usage:* `/pay ` +*Example:* `/pay lnbc20n1psscehd...`""" + +# DONATE + +donationSuccess = """🙏 Thank you for your donation.""" +donationErrorMessage = """🚫 Oh no. Donation failed.""" +donationProgressMessage = """🧮 Preparing your donation...""" +donationFailedMessage = """🚫 Donation failed: %s""" +donateEnterAmountMessage = """Did you enter an amount?""" +donateValidAmountMessage = """Did you enter a valid amount?""" +donateHelpText = """📖 Oops, that didn't work. %s + +*Usage:* `/donate ` +*Example:* `/donate 1000`""" + +# PHOTO + +photoQrNotRecognizedMessage = """🚫 Could not regocognize a Lightning invoice or an LNURL. Try to center the QR code, crop the photo, or zoom in.""" +photoQrRecognizedMessage = """✅ QR code: +`%s`""" + +# LNURL + +lnurlReceiveInfoText = """👇 You can use this static LNURL to receive payments.""" +lnurlResolvingUrlMessage = """🧮 Resolving address...""" +lnurlGettingUserMessage = """🧮 Preparing payment...""" +lnurlPaymentFailed = """🚫 Payment failed: %s""" +lnurlInvalidAmountMessage = """🚫 Invalid amount.""" +lnurlInvalidAmountRangeMessage = """🚫 Amount must be between %d and %d sat.""" +lnurlNoUsernameMessage = """🚫 You need to set a Telegram username to receive payments via LNURL.""" +lnurlEnterAmountMessage = """⌨️ Enter an amount between %d and %d sat.""" +lnurlHelpText = """📖 Oops, that didn't work. %s + +*Usage:* `/lnurl [amount] ` +*Example:* `/lnurl LNURL1DP68GUR...`""" + +# LINK + +walletConnectMessage = """🔗 *Link your wallet* + +⚠️ Never share the URL or the QR code with anyone or they will be able to access your funds. + +- *BlueWallet:* Press *New wallet*, *Import wallet*, *Scan or import a file*, and scan the QR code. +- *Zeus:* Copy the URL below, press *Add a new node*, *Import* (the URL), *Save Node Config*.""" +couldNotLinkMessage = """🚫 Couldn't link your wallet. Please try again later.""" + +# FAUCET + +inlineQueryFaucetTitle = """🚰 Create a faucet.""" +inlineQueryFaucetDescription = """Usage: @%s faucet """ +inlineResultFaucetTitle = """🚰 Create a %d sat faucet.""" +inlineResultFaucetDescription = """👉 Click here to create a faucet in this chat.""" + +inlineFaucetMessage = """Press ✅ to collect %d sat from this faucet. + +🚰 Remaining: %d/%d sat (given to %d/%d users) +%s""" +inlineFaucetEndedMessage = """🚰 Faucet empty 🍺\n\n🏅 %d sat given to %d users.""" +inlineFaucetAppendMemo = """\n✉️ %s""" +inlineFaucetCreateWalletMessage = """Chat with %s 👈 to manage your wallet.""" +inlineFaucetCancelledMessage = """🚫 Faucet cancelled.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 Peruser amount not divisor of capacity.""" +inlineFaucetInvalidAmountMessage = """🚫 Invalid amount.""" +inlineFaucetSentMessage = """🚰 %d sat sent to %s.""" +inlineFaucetReceivedMessage = """🚰 %s sent you %d sat.""" +inlineFaucetHelpFaucetInGroup = """Create a faucet in a group with the bot inside or use 👉 inline command (/advanced for more).""" +inlineFaucetHelpText = """📖 Oops, that didn't work. %s + +*Usage:* `/faucet ` +*Example:* `/faucet 210 21`""" + +# INLINE SEND + +inlineQuerySendTitle = """💸 Send payment to a chat.""" +inlineQuerySendDescription = """Usage: @%s send []""" +inlineResultSendTitle = """💸 Send %d sat.""" +inlineResultSendDescription = """👉 Click to send %d sat to this chat.""" + +inlineSendMessage = """Press ✅ to receive payment from %s.\n\n💸 Amount: %d sat""" +inlineSendAppendMemo = """\n✉️ %s""" +inlineSendUpdateMessageAccept = """💸 %d sat sent from %s to %s.""" +inlineSendCreateWalletMessage = """Chat with %s 👈 to manage your wallet.""" +sendYourselfMessage = """📖 You can't pay to yourself.""" +inlineSendFailedMessage = """🚫 Send failed.""" +inlineSendInvalidAmountMessage = """🚫 Amount must be larger than 0.""" +inlineSendBalanceLowMessage = """🚫 Your balance is too low (%d sat).""" + +# INLINE RECEIVE + +inlineQueryReceiveTitle = """🏅 Request a payment in a chat.""" +inlineQueryReceiveDescription = """Usage: @%s receive []""" +inlineResultReceiveTitle = """🏅 Receive %d sat.""" +inlineResultReceiveDescription = """👉 Click to request a payment of %d sat.""" + +inlineReceiveMessage = """Press 💸 to pay to %s.\n\n💸 Amount: %d sat""" +inlineReceiveAppendMemo = """\n✉️ %s""" +inlineReceiveUpdateMessageAccept = """💸 %d sat sent from %s to %s.""" +inlineReceiveCreateWalletMessage = """Chat with %s 👈 to manage your wallet.""" +inlineReceiveYourselfMessage = """📖 You can't pay to yourself.""" +inlineReceiveFailedMessage = """🚫 Receive failed.""" +inlineReceiveCancelledMessage = """🚫 Receive cancelled.""" diff --git a/translations/es.toml b/translations/es.toml new file mode 100644 index 00000000..bd14b059 --- /dev/null +++ b/translations/es.toml @@ -0,0 +1,296 @@ +# COMMANDS + +helpCommandStr = """ayuda""" +basicsCommandStr = """basico""" +tipCommandStr = """tip""" +balanceCommandStr = """saldo""" +sendCommandStr = """enviar""" +invoiceCommandStr = """factura""" +payCommandStr = """pagar""" +donateCommandStr = """donar""" +advancedCommandStr = """avanzado""" +transactionsCommandStr = """transacciones""" +logCommandStr = """log""" +listCommandStr = """lista""" + +linkCommandStr = """enlace""" +lnurlCommandStr = """lnurl""" +faucetCommandStr = """grifo""" + +tipjarCommandStr = """bote""" +receiveCommandStr = """recibir""" +hideCommandStr = """ocultar""" +volcanoCommandStr = """volcán""" +showCommandStr = """mostrar""" +optionsCommandStr = """opciones""" +settingsCommandStr = """ajustes""" +saveCommandStr = """guardar""" +deleteCommandStr = """borrar""" +infoCommandStr = """info""" + +# NOTIFICATIONS + +cantDoThatMessage = """No puedes hacer eso.""" +cantClickMessage = """No puedes clicar este botón.""" +balanceTooLowMessage = """Tu saldo es muy bajo.""" + +# BUTTONS + +sendButtonMessage = """✅ Enviar""" +payButtonMessage = """✅ Pagar""" +payReceiveButtonMessage = """💸 Pagar""" +receiveButtonMessage = """✅ Recibir""" +cancelButtonMessage = """🚫 Cancelar""" +collectButtonMessage = """✅ Cobrar""" +nextButtonMessage = """Siguiente""" +backButtonMessage = """Volver""" +acceptButtonMessage = """Aceptar""" +denyButtonMessage = """Negar""" +tipButtonMessage = """Tip""" +revealButtonMessage = """Revelar""" +showButtonMessage = """Mostrar""" +hideButtonMessage = """Ocultar""" +joinButtonMessage = """Unir""" +optionsButtonMessage = """Opciones""" +settingsButtonMessage = """Ajustes""" +saveButtonMessage = """Guardar""" +deleteButtonMessage = """Borrar""" +infoButtonMessage = """Info""" + +# HELP + +helpMessage = """⚡️ *Monedero* +_Este bot es un monedero Bitcoin Lightning que puede enviar tips vía Telegram. Para dar tips, añade el bot a un chat de grupo. La unidad básica de los tips son los Satoshis (sat). 100.000.000 sat = 1 Bitcoin. Escribe 📚 /basics para saber más._ + +❤️ *Donar* +_Este bot no cobra ninguna comisión, pero su funcionamiento cuesta Satoshis. Si te gusta el bot, por favor considera apoyar este proyecto con una donación. Para donar, usa_ `/donate 1000`. + +%s + +⚙️ *Comandos* +*/tip* 🏅 Responder a un mensaje para dar tip: `/tip []` +*/balance* 👑 Consulta tu saldo: `/balance`. +*/send* 💸 Enviar fondos a un usuario: `/send @user o user@ln.tips []` +*/invoice* ⚡️ Recibir con Lightning: `/invoice []` +*/pay* ⚡️ Pagar con Lightning: `/pay ` +*/donate* ❤️ Donar al proyecto: `/donate 1000` +*/advanced* 🤖 Funciones avanzadas. +*/help* 📖 Leer esta ayuda.""" + +infoHelpMessage = """ℹ️ *Info*""" +infoYourLightningAddress = """Su Lightning Address _(Dirección Lightning)_ es `%s`.""" + +basicsMessage = """🧡 *Bitcoin* +_Bitcoin es la moneda de Internet. Es una moneda sin permisos y descentralizada que no tiene dueños ni autoridad que la controle. Bitcoin es un dinero sólido que es más rápido, más seguro y más inclusivo que el sistema financiero fiduciario._ + +🧮 *Economía* +_La unidad más pequeña de Bitcoin son los Satoshis (sat) y 100.000.000 sat = 1 Bitcoin. Sólo habrá 21 millones de Bitcoin. El valor de Bitcoin en moneda fiduciaria puede cambiar diariamente. Sin embargo, si usted vive en un patrón Bitcoin, 1 sat siempre será igual a 1 sat._ + +⚡️ *La Red Lightning* +_La red Lightning es un protocolo de pago que permite realizar pagos con Bitcoin de forma rápida y barata, sin apenas consumo de energía. Es lo que hace escalar Bitcoin a miles de millones de personas en todo el mundo._ + +📲 *Monederos Lightning* +_Tus fondos en este bot pueden ser enviados a cualquier otro monedero Lightning y viceversa. Los monederos Lightning recomendados para su teléfono son_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (sin custodia), o_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(fácil)_. + +📄 *Código abierto* +_Este bot es gratuito y de_ [Código abierto](https://github.com/LightningTipBot/LightningTipBot)_. Puedes ejecutarlo en tu propio ordenador y utilizarlo en tu propia comunidad._ + +✈️ *Telegram* +_Añade este bot al chat de tu grupo de Telegram para enviar fondos usando /tip. Si haces al bot administrador del grupo, también limpiará los comandos para mantener el chat ordenado._ + +🏛 *Términos* +_No somos custodios de tus fondos. Actuaremos en su mejor interés, pero también somos conscientes de que la situación sin KYC es complicada hasta que resolvamos algo. Cualquier cantidad que pongas en tu monedero se considerará una donación. No nos des todo tu dinero. Ten en cuenta que este bot está en desarrollo beta. Utilízalo bajo tu propio riesgo._ + +❤️ *Donar* +_Este bot no cobra ninguna comisión, pero su funcionamiento cuesta satoshis. Si te gusta el bot, por favor considera apoyar este proyecto con una donación. Para donar, use_ `/donate 1000`""" + +helpNoUsernameMessage = """👋 Por favor, pon un nombre de usuario de Telegram.""" + +advancedMessage = """%s + +👉 *Comandos Inline* +*send* 💸 Enviar sats al chat: `%s send []` +*receive* 🏅 Solicita un pago: `%s receive []`. +*faucet* 🚰 Crear un grifo: `%s faucet `. + +📖 Puedes usar comandos _inline_ en todos los chats, incluso en las conversaciones privadas. Espera un segundo después de introducir un comando _inline_ y *haz clic* en el resultado, no pulses enter. + +⚙️ *Comandos avanzados* +*/link* 🔗 Enlaza tu monedero a [ BlueWallet ](https://bluewallet.io/) o [ Zeus ](https://zeusln.app/) +*/lnurl* ⚡️ Lnurl recibir o pagar: `/lnurl` o `/lnurl ` +*/faucet* 🚰 Crear un grifo: `%s faucet `""" + +# START + +startSettingWalletMessage = """🧮 Configurar tu monedero...""" +startWalletCreatedMessage = """🧮 Monedero creado.""" +startWalletReadyMessage = """✅ *Tu monedero está listo.*""" +startWalletErrorMessage = """🚫 Error al iniciar tu monedero. Vuelve a intentarlo más tarde.""" +startNoUsernameMessage = """☝️ Parece que aún no tienes un @nombredeusuario en Telegram. No pasa nada, no necesitas uno para usar este bot. Sin embargo, para hacer un mejor uso de tu monedero, configura un nombre de usuario en los ajustes de Telegram. Luego, introduce /balance para que el bot pueda actualizar su registro de ti.""" + +# BALANCE + +balanceMessage = """👑 *Tu saldo:* %d sat""" +balanceErrorMessage = """🚫 No se ha podido recuperar tu saldo. Por favor, inténtalo más tarde.""" + +# TIP + +tipDidYouReplyMessage = """¿Has respondido a un mensaje para dar un tip? Para responder a cualquier mensaje, haz clic con el botón derecho del ratón -> Responder en tu ordenador o desliza el mensaje en tu teléfono. Si quieres enviar directamente a otro usuario, utiliza el comando /send.""" +tipInviteGroupMessage = """ℹ️ Por cierto, puedes invitar a este bot a cualquier grupo para empezar a dar tips allí.""" +tipEnterAmountMessage = """¿Ingresaste un monto?""" +tipValidAmountMessage = """¿Ingresaste un monto válido?""" +tipYourselfMessage = """📖 No te puedes dar tips a ti mismo.""" +tipSentMessage = """💸 %d sat enviado a %s.""" +tipReceivedMessage = """🏅 %s te ha dado un tip de %d sat.""" +tipErrorMessage = """🚫 El tip ha fallado.""" +tipUndefinedErrorMsg = """por favor, inténtalo más tarde""" +tipHelpText = """📖 Oops, eso no funcionó. %s + +*Uso:* `/tip []` +*Ejemplo:* `/tip 1000 ¡Meme de mal gusto!`""" + +# SEND + +sendValidAmountMessage = """¿Ingresaste un monto válido?""" +sendUserHasNoWalletMessage = """🚫 El usuario %s aún no ha creado un monedero.""" +sendSentMessage = """💸 %d sat enviado a %s.""" +sendPublicSentMessage = """💸 %d sat enviado(s) de %s a %s.""" +sendReceivedMessage = """🏅 %s te envió %d sat.""" +sendErrorMessage = """🚫 Envío fallido.""" +confirmSendMessage = """¿Desea pagar a %s?\n\n💸 Monto: %d sat?""" +confirmSendAppendMemo = """\n✉️ %s""" +sendCancelledMessage = """🚫 Envío cancelado.""" +errorTryLaterMessage = """🚫 Error. Por favor, inténtelo de nuevo más tarde.""" +sendSyntaxErrorMessage = """¿Has introducido un monto y un destinatario? Puedes utilizar el comando /send para enviar a usuarios de Telegram como %s o a una dirección Lightning como LightningTipBot@ln.tips.""" +sendHelpText = """📖 Oops, eso no funcionó. %s + +*Uso:* `/send []` +*Ejemplo:* `/send 1000 @LightningTipBot Me gusta el bot ❤️`. +*Ejemplo:* `/send 1234 LightningTipBot@ln.tips`""" + +# INVOICE + +invoiceEnterAmountMessage = """¿Ingresaste un monto?""" +invoiceValidAmountMessage = """¿Ingresaste un monto válido?""" +invoiceHelpText = """📖 Oops, eso no funcionó. %s + +*Uso:* `/invoice []` +*Ejemplo:* `/invoice 1000 ¡Gracias!`""" + +# PAY + +paymentCancelledMessage = """🚫 Pago cancelado.""" +invoicePaidMessage = """⚡️ Pago enviado.""" +invoicePublicPaidMessage = """⚡️ Pago enviado por %s.""" +invalidInvoiceHelpMessage = """¿Has introducido una factura Lightning válida? Prueba con /send si quieres enviar a un usuario de Telegram o a una dirección Lightning.""" +invoiceNoAmountMessage = """🚫 No se pueden pagar las facturas sin un monto.""" +insufficientFundsMessage = """🚫 Fondos insuficientes. Tienes %d sat pero necesitas al menos %d sat.""" +feeReserveMessage = """⚠️ El envío de todo el saldo puede fallar debido a las tarifas de la red. Si falla, intenta enviar un poco menos.""" +invoicePaymentFailedMessage = """🚫 Pago fallido: %s""" +invoiceUndefinedErrorMessage = """No se pudo pagar la factura.""" +confirmPayInvoiceMessage = """¿Desea pagar a %s?\n\n💸 Monto: %d sat?""" +confirmPayAppendMemo = """\n✉️ %s""" +payHelpText = """📖 Oops, eso no funcionó. %s + +*Uso:* `/pay ` +*Ejemplo:* `/pay lnbc20n1psscehd...`""" + +# DONATE + +donationSuccess = """🙏 Gracias por tu donación.""" +donationErrorMessage = """🚫 Oh, no. Donación fallida.""" +donationProgressMessage = """🧮 Preparando tu donación...""" +donationFailedMessage = """🚫 Donación fallida: %s""" +donateEnterAmountMessage = """¿Ingresaste un monto?""" +donateValidAmountMessage = """¿Ingresaste un monto válido?""" +donateHelpText = """📖 Oops, eso no funcionó. %s + +*Uso:* `/donate ` +*Ejemplo:* `/donate 1000`""" + +# PHOTO + +photoQrNotRecognizedMessage = """🚫 No se ha podido reconocer una factura Lightning o una LNURL. Intenta centrar el código QR, recortar la foto o ampliarla.""" +photoQrRecognizedMessage = """✅ QR code: +`%s`""" + +# LNURL + +lnurlReceiveInfoText = """👇 Puedes usar esta LNURL estática para recibir pagos.""" +lnurlResolvingUrlMessage = """🧮 Solventando la dirección...""" +lnurlGettingUserMessage = """🧮 Preparando el pago...""" +lnurlPaymentFailed = """🚫 Pago fallido: %s""" +lnurlInvalidAmountMessage = """🚫 Monto inválido.""" +lnurlInvalidAmountRangeMessage = """🚫 El monto debe estar entre %d y %d sat.""" +lnurlNoUsernameMessage = """🚫 Tienes que establecer un nombre de usuario de Telegram para recibir pagos a través de LNURL.""" +lnurlEnterAmountMessage = """⌨️ Introduce un monto entre %d y %d sat.""" +lnurlHelpText = """📖 Oops, eso no funcionó. %s + +*Uso:* `/lnurl [monto] ` +*Ejemplo:* `/lnurl LNURL1DP68GUR...`""" + +# LINK + +walletConnectMessage = """🔗 *Enlaza tu monedero* + +⚠️ Nunca compartas la URL o el código QR con nadie o podrán acceder a tus fondos. + +- *BlueWallet:* Pulse *Nuevo monedero*, *Importar monedero*, *Escanear o importar un archivo*, y escanea el código QR. +- *Zeus:* Copia la URL de abajo, pulsa *Añadir un nuevo nodo*, *Importar* (la URL), *Guardar configuración del nodo*.""" +couldNotLinkMessage = """🚫 No se ha podido vincular su monedero. Por favor, inténtalo más tarde.""" + +# FAUCET + +inlineQueryFaucetTitle = """🚰 Crea un grifo.""" +inlineQueryFaucetDescription = """Uso: @%s grifo """ +inlineResultFaucetTitle = """🚰 Crear un grifo %d sat.""" +inlineResultFaucetDescription = """👉 Haz clic aquí para crear un grifo en este chat.""" + +inlineFaucetMessage = """Pulsa ✅ para cobrar %d sat de este grifo. + +🚰 Restante: %d/%d sat (dado a %d/%d usuarios) +%s""" +inlineFaucetEndedMessage = """🚰 Grifo vacío 🍺\n\n🏅 %d sat dados a %d usuarios.""" +inlineFaucetAppendMemo = """\n✉️ %s""" +inlineFaucetCreateWalletMessage = """Chatea con %s 👈 para gestionar tu cartera.""" +inlineFaucetCancelledMessage = """🚫 Grifo cancelado.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 El monto del usuario no es divisor de la capacidad.""" +inlineFaucetInvalidAmountMessage = """🚫 Monto inválido.""" +inlineFaucetSentMessage = """🚰 %d sat enviado(s) a %s.""" +inlineFaucetReceivedMessage = """🚰 %s te envió %d sat.""" +inlineFaucetHelpFaucetInGroup = """Crea un grifo en un grupo con el bot dentro o utiliza el 👉 comando _inline_ (/advanced para más).""" +inlineFaucetHelpText = """📖 Oops, eso no funcionó. %s + +*Uso:* `/faucet ` +*Ejemplo:* `/faucet 210 21`""" + +# INLINE SEND + +inlineQuerySendTitle = """💸 Enviar el pago a un chat.""" +inlineQuerySendDescription = """Uso: @%s send []""" +inlineResultSendTitle = """💸 Enviar %d sat.""" +inlineResultSendDescription = """👉 Haga clic para enviar %d sat a este chat.""" + +inlineSendMessage = """Pulse ✅ para recibir el pago de %s.\n\n💸 Cantidad: %d sat""" +inlineSendAppendMemo = """\n✉️ %s""" +inlineSendUpdateMessageAccept = """💸 %d sat enviado de %s a %s.""" +inlineSendCreateWalletMessage = """Chatea con %s 👈 para gestionar tu cartera.""" +sendYourselfMessage = """📖 No puedes pagarte a ti mismo.""" +inlineSendFailedMessage = """🚫 Envío fallido.""" +inlineSendInvalidAmountMessage = """🚫 La cantidad debe ser mayor que 0.""" +inlineSendBalanceLowMessage = """🚫 Tu saldo es demasiado bajo (%d sat).""" + +# INLINE RECEIVE + +inlineQueryReceiveTitle = """🏅 Solicita un pago en un chat.""" +inlineQueryReceiveDescription = """Uso: @%s recibir []""" +inlineResultReceiveTitle = """🏅 Recibir %d sat.""" +inlineResultReceiveDescription = """👉 Haga clic para solicitar un pago de %d sat.""" + +inlineReceiveMessage = """Pulsa 💸 para pagar a %s.\n\n💸 Monto: %d sat""" +inlineReceiveAppendMemo = """\n✉️ %s""" +inlineReceiveUpdateMessageAccept = """💸 %d sat enviado de %s a %s.""" +inlineReceiveCreateWalletMessage = """Chatea con %s 👈 para gestionar tu cartera.""" +inlineReceiveYourselfMessage = """📖 No puedes pagarte a ti mismo.""" +inlineReceiveFailedMessage = """🚫 Recepción fallida.""" +inlineReceiveCancelledMessage = """🚫 Recepción cancelada.""" diff --git a/translations/nl.toml b/translations/nl.toml new file mode 100644 index 00000000..32d81189 --- /dev/null +++ b/translations/nl.toml @@ -0,0 +1,296 @@ +# COMMANDS + +helpCommandStr = """help""" +basicsCommandStr = """basis""" +tipCommandStr = """tip""" +balanceCommandStr = """saldo""" +sendCommandStr = """verzenden""" +invoiceCommandStr = """factuur""" +payCommandStr = """betalen""" +donateCommandStr = """donatie""" +advancedCommandStr = """geavanceerd""" +transactionsCommandStr = """transacties""" +logCommandStr = """log""" +listCommandStr = """lijst""" + +linkCommandStr = """connect""" +lnurlCommandStr = """lnurl""" +faucetCommandStr = """kraan""" + +tipjarCommandStr = """donatiebox""" +receiveCommandStr = """ontvangen""" +hideCommandStr = """verbergen""" +volcanoCommandStr = """vulkaan""" +showCommandStr = """toon""" +optionsCommandStr = """opties""" +settingsCommandStr = """instellingen""" +saveCommandStr = """save""" +deleteCommandStr = """verwijderen""" +infoCommandStr = """info""" + +# NOTIFICATIONS + +cantDoThatMessage = """Dat kun je niet doen.""" +cantClickMessage = """Je kunt niet op deze knop klikken.""" +balanceTooLowMessage = """Uw saldo is te laag.""" + +# BUTTONS + +sendButtonMessage = """✅ verzenden""" +payButtonMessage = """✅ betalen""" +payReceiveButtonMessage = """💸 betalen""" +receiveButtonMessage = """✅ ontvangen""" +cancelButtonMessage = """🚫 annuleren""" +collectButtonMessage = """✅ verzamelen""" +nextButtonMessage = """volgende""" +backButtonMessage = """terug""" +acceptButtonMessage = """accepteren""" +denyButtonMessage = """weigeren""" +tipButtonMessage = """tip""" +revealButtonMessage = """laat zijn""" +showButtonMessage = """toon""" +hideButtonMessage = """verbergen""" +joinButtonMessage = """deelnemen""" +optionsButtonMessage = """opties""" +settingsButtonMessage = """instellingen""" +saveButtonMessage = """save""" +deleteButtonMessage = """verwijderen""" +infoButtonMessage = """info""" + +# HELP + +helpMessage = """⚡️ *Wallet* +_Deze bot is een Bitcoin Lightning wallet die tips kan sturen op Telegram. Om tips te geven, voeg de bot toe aan een groepschat. De basiseenheid van tips zijn Satoshis (sat). 100.000.000 sat = 1 Bitcoin. Type 📚 /basics voor meer._ + +❤️ *Doneren* +_Deze bot brengt geen kosten in rekening maar kost Satoshis om te werken. Als je de bot leuk vindt, overweeg dan om dit project te steunen met een donatie. Om te doneren, gebruik_ `/donate 1000` + +%s + +⚙️ *opdrachten* +*/tip* 🏅 Antwoord op een bericht aan tip: `/tip []` +*/balance* 👑 Controleer uw saldo: `/balance` +*/send* 💸 Stuur geld naar een gebruiker: `/send @user or user@ln.tips []` +*/invoice* ⚡️ Ontvang met Lightning: `/invoice []` +*/pay* ⚡️ Betaal met Lightning: `/pay ` +*/donate* ❤️ Doneer aan het project: `/donate 1000` +*/advanced* 🤖 Geavanceerde functies. +*/help* 📖 Lees deze hulp.""" + +infoHelpMessage = """ℹ️ *Info*""" +infoYourLightningAddress = """Uw Lightning adres is `%s`""" + +basicsMessage = """🧡 *Bitcoin* +_Bitcoin is de munteenheid van het internet. Het is zonder toestemming en gedecentraliseerd en heeft geen meesters en geen controlerende autoriteit. Bitcoin is gezond geld dat sneller, veiliger en inclusiever is dan het oude financiële systeem. + +🧮 *Economie* +_De kleinste eenheid van Bitcoin zijn Satoshis (sat) en 100.000.000 sat = 1 Bitcoin. Er zullen ooit maar 21 miljoen Bitcoin zijn. De fiatvalutawaarde van Bitcoin kan dagelijks veranderen. Echter, als u leeft op een Bitcoin-standaard zal 1 sat altijd gelijk zijn aan 1 sat._ + +⚡️ *Het Lightning Network* +_Het Lightning Network is een betalingsprotocol dat snelle en goedkope Bitcoin-betalingen mogelijk maakt die bijna geen energie vergen. Het is wat Bitcoin schaalt naar de miljarden mensen over de hele wereld._ + +📲 *Lightning Wallets* +_U kunt uw geld via deze bot naar elke andere Lightning Wallet in de wereld sturen. Aanbevolen Lightning wallets voor uw telefoon zijn_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (niet-custodiaal), of_ [Wallet van Satoshi](https://www.walletofsatoshi.com/) _(gemakkelijk)_. + +📄 *Open Source* +_Deze bot is gratis en_ [open source](https://github.com/LightningTipBot/LightningTipBot) _software. U kunt het op uw eigen computer draaien en in uw eigen gemeenschap gebruiken._ + +✈️ *Telegram* +_Voeg deze bot toe aan je Telegram groep chat om berichten te /tip. Als je de bot admin maakt van de groep zal hij ook commando's opruimen om de chat netjes te houden._ + +🏛 *Voorwaarden* +_Wij zijn geen bewaarder van uw fondsen. Wij zullen in uw belang handelen, maar we zijn ons er ook van bewust dat de situatie zonder KYC lastig is totdat we iets hebben uitgezocht. Elk bedrag dat u in uw portemonnee laadt, wordt beschouwd als een donatie. Geef ons niet al uw geld. Wees ervan bewust dat deze bot in beta ontwikkeling is. Gebruik op eigen risico. + +❤️ *Doneren* +_Deze bot brengt geen kosten in rekening maar kost satoshis om te werken. Als je de bot leuk vindt, overweeg dan om dit project te steunen met een donatie. Om te doneren, gebruik_ `/donate 1000`""" + +helpNoUsernameMessage = """👋 Stel alstublieft een Telegram gebruikersnaam in.""" + +advancedMessage = """%s + +👉 *Inline commands* +*send* 💸 Stuur sats naar chat: `%s send []` +*receive* 🏅 Verzoek om betaling: `%s receive []` +*faucet* 🚰 Maak een kraan: `%s faucet ` + +📖 Je kunt inline commando's in elke chat gebruiken, zelfs in privé gesprekken. Wacht een seconde na het invoeren van een inline commando en *klik* op het resultaat, druk niet op enter. + +⚙️ *Geavanceerde opdrachten* +*/link* 🔗 Koppel uw wallet aan [BlueWallet](https://bluewallet.io/) of [Zeus](https://zeusln.app/) +*/lnurl* ⚡️ Lnurl ontvangen of betalen: `/lnurl` of `/lnurl ` +*/faucet* 🚰 Maak een kraan `/faucet `""" + +# START + +startSettingWalletMessage = """🧮 Uw wallet instellen...""" +startWalletCreatedMessage = """🧮 Wallet gemaakt.""" +startWalletReadyMessage = """✅ *Uw wallet is klaar.*""" +startWalletErrorMessage = """🚫 Fout bij het initialiseren van uw wallet. Probeer later opnieuw.""" +startNoUsernameMessage = """☝️ Het lijkt erop dat je nog geen Telegram @ gebruikersnaam hebt. Dat is niet erg, je hebt er geen nodig om deze bot te gebruiken. Echter, om beter gebruik te maken van je wallet, stel je een gebruikersnaam in bij de Telegram instellingen. Voer vervolgens /balance in, zodat de bot zijn gegevens over jou kan bijwerken.""" + +# BALANCE + +balanceMessage = """👑 *Uw saldo:* %d sat""" +balanceErrorMessage = """🚫 Kon uw saldo niet ophalen. Probeer het later nog eens.""" + +# TIP + +tipDidYouReplyMessage = """Hebt u een bericht aan tip beantwoord? Om te antwoorden op een bericht, klik met de rechtermuisknop -> Beantwoorden op uw computer of veeg over het bericht op uw telefoon. Als je direct naar een andere gebruiker wilt sturen, gebruik dan het /send commando.""" +tipInviteGroupMessage = """ℹ️ Trouwens, je kan deze bot uitnodigen voor elke groep om daar te beginnen tippen.""" +tipEnterAmountMessage = """Heb je een bedrag ingevoerd?""" +tipValidAmountMessage = """Heb je een geldig bedrag ingevoerd?""" +tipYourselfMessage = """📖 Je kunt jezelf geen tip geven.""" +tipSentMessage = """💸 %d sat gestuurd naar %s.""" +tipReceivedMessage = """🏅 %s heeft je getipt %d sat.""" +tipErrorMessage = """🚫 Tip mislukt.""" +tipUndefinedErrorMsg = """probeer het later nog eens""" +tipHelpText = """📖 Oeps, dat werkte niet. %s + +*Usage:* `/tip []` +*Example:* `/tip 1000 Dank meme!`""" + +# SEND + +sendValidAmountMessage = """Heb je een geldig bedrag ingevoerd?""" +sendUserHasNoWalletMessage = """🚫 Gebruiker %s heeft nog geen portemonnee aangemaakt.""" +sendSentMessage = """💸 %d zat verstuurd naar %s.""" +sendPublicSentMessage = """💸 %d sat verstuurd van %s naar %s.""" +sendReceivedMessage = """🏅 %s stuurde u %d sat.""" +sendErrorMessage = """🚫 Verzenden mislukt.""" +confirmSendMessage = """Wilt u betalen aan %s? bedrag: %d sat""" +confirmSendAppendMemo = """\n✉️ %s""" +sendCancelledMessage = """🚫 Geannuleerd verzenden.""" +errorTryLaterMessage = """🚫 Fout. Probeer het later nog eens.""" +sendSyntaxErrorMessage = """Heb je een bedrag en een ontvanger ingevoerd? U kunt het /send commando gebruiken om ofwel naar Telegram gebruikers te sturen zoals %s of naar een Lightning adres zoals LightningTipBot@ln.tips.""" +sendHelpText = """📖 Oeps, dat werkte niet. %s + +*Usage:* `/send []` +*Example:* `/send 1000 @LightningTipBot Ik hou gewoon van de bot ❤️` +*Example:* `/send 1234 LightningTipBot@ln.tips`""" + +# INVOICE + +invoiceEnterAmountMessage = """Heb je een bedrag ingevoerd?""" +invoiceValidAmountMessage = """Heeft u een geldig bedrag ingevoerd?""" +invoiceHelpText = """📖 Oeps, dat werkte niet. %s + +*Usage:* `/invoice []` +*Example:* `/invoice 1000 Dank je!`""" + +# PAY + +paymentCancelledMessage = """🚫 Betaling geannuleerd.""" +invoicePaidMessage = """⚡️ Betaling verzonden.""" +invoicePublicPaidMessage = """⚡️ Betaling verzonden door %s.""" +invalidInvoiceHelpMessage = """Heeft u een geldige Lightning factuur ingevoerd? Probeer /send als u wilt verzenden naar een Telegram gebruiker of Lightning address.""" +invoiceNoAmountMessage = """🚫 Kan facturen niet betalen zonder een bedrag.""" +insufficientFundsMessage = """🚫 Onvoldoende middelen. Je hebt %d sat maar je hebt minstens %d sat nodig.""" +feeReserveMessage = """⚠️ Het versturen van uw volledige saldo kan mislukken vanwege netwerkkosten. Als het mislukt, probeer dan een beetje minder te verzenden.""" +invoicePaymentFailedMessage = """🚫 Betaling mislukt: %s""" +invoiceUndefinedErrorMessage = """Kon de factuur niet betalen.""" +confirmPayInvoiceMessage = """Wilt u deze betaling sturen?\n\n💸 bedrag: %d sat""" +confirmPayAppendMemo = """\n✉️ %s""" +payHelpText = """📖 Oeps, dat werkte niet. %s + +*Usage:* `/pay ` +*Example:* `/pay lnbc20n1psscehd...`""" + +# DONATE + +donationSuccess = """🙏 Dank u voor uw donatie.""" +donationErrorMessage = """🚫 Oh nee. Donatie mislukt.""" +donationProgressMessage = """🧮 Preparing your donation...""" +donationFailedMessage = """🚫 Donatie mislukt: %s""" +donateEnterAmountMessage = """Heeft u een bedrag ingevoerd?""" +donateValidAmountMessage = """Heeft u een geldig bedrag ingevoerd?""" +donateHelpText = """📖 Oeps, dat werkte niet. %s + +*Usage:* `/donate ` +*Example:* `/donate 1000`""" + +# PHOTO + +photoQrNotRecognizedMessage = """🚫 Kon een Lightning factuur of een LNURL niet herkennen. Probeer de QR code te centreren, de foto bij te snijden, of in te zoomen.""" +photoQrRecognizedMessage = """✅ QR code: +`%s`""" + +# LNURL + +lnurlReceiveInfoText = """👇 U kunt deze statische LNURL gebruiken om betalingen te ontvangen.""" +lnurlResolvingUrlMessage = """🧮 Oplossen adres...""" +lnurlGettingUserMessage = """🧮 Voorbereiding van betaling...""" +lnurlPaymentFailed = """🚫 Betaling mislukt: %s""" +lnurlInvalidAmountMessage = """🚫 Ongeldig bedrag.""" +lnurlInvalidAmountRangeMessage = """🚫 Bedrag moet liggen tussen %d en %d zat.""" +lnurlNoUsernameMessage = """🚫 U moet een Telegram gebruikersnaam instellen om betalingen te ontvangen via LNURL.""" +lnurlEnterAmountMessage = """⌨️ Voer een bedrag in tussen %d en %d sat.""" +lnurlHelpText = """📖 Oeps, dat werkte niet. %s + +*Usage:* `/lnurl [bedrag] ` +*Example:* `/lnurl LNURL1DP68GUR...`""" + +# LINK + +walletConnectMessage = """🔗 *Koppel uw wallet* + +⚠️ Deel nooit de URL of de QR code met iemand anders of zij zullen toegang krijgen tot uw fondsen. + +- *BlueWallet:* Druk op *Nieuwe wallet*, *Import wallet*, *Scan of importeer een bestand*, en scan de QR code. +- *Zeus:* Kopieer de URL hieronder, druk op *Voeg een nieuw knooppunt toe*, *Import* (de URL), *Save Node Config*.""" +couldNotLinkMessage = """🚫 Uw wallet kon niet gelinkt worden. Probeer het later opnieuw.""" + +# FAUCET + +inlineQueryFaucetTitle = """🚰 Maak een kraan.""" +inlineQueryFaucetDescription = """Gebruik: @%s tapkrann """ +inlineResultFaucetTitle = """🚰 Maak een %d sat kraan.""" +inlineResultFaucetDescription = """👉 Klik hier om een kraan in deze chat te maken..""" + +inlineFaucetMessage = """Druk op ✅ om %d sat te verzamelen van deze kraan. + +🚰 Remaining: %d/%d sat (given to %d/%d users) +%s""" +inlineFaucetEndedMessage = """🚰 kraan leeg 🍺🏅 %d zat gegeven aan %d gebruikers.""" +inlineFaucetAppendMemo = """\n✉️ %s""" +inlineFaucetCreateWalletMessage = """Chat met %s 👈 om uw wallet te beheren.""" +inlineFaucetCancelledMessage = """🚫 kraan geannuleerd.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 Per gebruiker bedrag niet deelbaar van capaciteit.""" +inlineFaucetInvalidAmountMessage = """🚫 Ongeldig bedrag.""" +inlineFaucetSentMessage = """🚰 %d zat gestuurd naar %s.""" +inlineFaucetReceivedMessage = """🚰 %s stuurde je %d sat.""" +inlineFaucetHelpFaucetInGroup = """Maak een kraan in een groep met de bot erin of gebruik 👉 inline commando (/advanced voor meer).""" +inlineFaucetHelpText = """📖 Oeps, dat werkte niet. %s + +*Usage:* `/kraan ` +*Example:* `/kraan 210 21`""" + +# INLINE VERZENDEN + +inlineQuerySendTitle = """💸 Stuur betaling naar een chat.""" +inlineQuerySendDescription = """Gebruik: @%s send []""" +inlineResultSendTitle = """💸 Stuur %d sat.""" +inlineResultSendDescription = """👉 Klik om %d sat naar deze chat te sturen.""" + +inlineSendMessage = """Druk op ✅ om de betaling van %s te ontvangen.\n💸 Bedrag: %d sat""" +inlineSendAppendMemo = """\n✉️ %s""" +inlineSendUpdateMessageAccept = """💸 %d verzonden van %s naar %s.""" +inlineSendCreateWalletMessage = """Chat met %s 👈 om uw wallet te beheren.""" +sendYourselfMessage = """📖 Je kunt niet aan jezelf betalen.""" +inlineSendFailedMessage = """🚫 verzenden mislukt.""" +inlineSendInvalidAmountMessage = """🚫 Bedrag moet groter zijn dan 0.""" +inlineSendBalanceLowMessage = """🚫 Uw saldo is te laag (%d zat).""" + +# INLINE RECEIVE + +inlineQueryReceiveTitle = """🏅 Vraag een betaling in een chat.""" +inlineQueryReceiveDescription = """Gebruik: @%s ontvangt []""" +inlineResultReceiveTitle = """🏅 Ontvang %d zat.""" +inlineResultReceiveDescription = """👉 Klik om een betaling van %d sat aan te vragen.""" + +inlineReceiveMessage = """Druk op 💸 om te betalen aan %s.\n💸 Bedrag: %d sat""" +inlineReceiveAppendMemo = """\n✉️ %s""" +inlineReceiveUpdateMessageAccept = """💸 %d sat verstuurd van %s naar %s.""" +inlineReceiveCreateWalletMessage = """Chat met %s 👈 om uw wallet te beheren.""" +inlineReceiveYourselfMessage = """📖 Je kunt niet aan jezelf betalen.""" +inlineReceiveFailedMessage = """🚫 Ontvangst mislukt.""" +inlineReceiveCancelledMessage = """🚫 Ontvangen geannuleerd.""" From 4830928352d5434394a8de0f379d9255bd52b2b6 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Sep 2021 15:04:53 +0200 Subject: [PATCH 004/541] I18n hotfix1 (#77) * fix context for /start * fix tip from translation --- start.go | 6 +++--- tip.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/start.go b/start.go index 6101c310..e9fc611a 100644 --- a/start.go +++ b/start.go @@ -29,10 +29,10 @@ func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) { return } bot.tryDeleteMessage(walletCreationMsg) - userContext := context.WithValue(context.Background(), "user", user) - bot.helpHandler(userContext, m) + ctx = context.WithValue(ctx, "user", user) + bot.helpHandler(ctx, m) bot.trySendMessage(m.Sender, Translate(ctx, "startWalletReadyMessage")) - bot.balanceHandler(userContext, m) + bot.balanceHandler(ctx, m) // send the user a warning about the fact that they need to set a username if len(m.Sender.Username) == 0 { diff --git a/tip.go b/tip.go index 7408336a..e703b3f3 100644 --- a/tip.go +++ b/tip.go @@ -120,7 +120,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { log.Infof("[tip] Transaction sent from %s to %s (%d sat).", fromUserStr, toUserStr, amount) // notify users - _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(Translate(ctx, "tipSentMessage"), amount, toUserStrMd)) + _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(bot.Translate(from.Telegram.LanguageCode, "tipSentMessage"), amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[/tip] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) From af21ad4beaeb3ba7ce16f6c506818d3de40b2260 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Sep 2021 17:50:18 +0200 Subject: [PATCH 005/541] hotfixes (#78) --- pay.go | 2 +- tooltip.go | 4 +++- translations/de.toml | 20 ++++++++++---------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/pay.go b/pay.go index 1f70d9aa..3774fe70 100644 --- a/pay.go +++ b/pay.go @@ -259,7 +259,7 @@ func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { err = fmt.Errorf(bot.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) } // bot.trySendMessage(c.Sender, fmt.Sprintf(invoicePaymentFailedMessage, err)) - bot.tryEditMessage(c.Message, fmt.Sprintf(bot.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), err), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, fmt.Sprintf(bot.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), MarkdownEscape(err.Error())), &tb.ReplyMarkup{}) log.Errorln(errmsg) return } diff --git a/tooltip.go b/tooltip.go index 8083530e..75e572c7 100644 --- a/tooltip.go +++ b/tooltip.go @@ -132,7 +132,9 @@ func tipTooltipHandler(m *tb.Message, bot *TipBot, amount int, initializedWallet } msg, err := bot.telegram.Reply(m.ReplyTo, tipmsg, tb.Silent) if err != nil { - print(err) + log.Errorf("[tipTooltipHandler Reply] %s", err.Error()) + // todo: in case of error we should do something better than just return 0 + return false } message := NewTipTooltip(msg, TipAmount(amount), Tips(1)) message.Tippers = appendUinqueUsersToSlice(message.Tippers, m.Sender) diff --git a/translations/de.toml b/translations/de.toml index 3b2f74f7..30633398 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -60,7 +60,7 @@ infoButtonMessage = """Info""" # HELP helpMessage = """⚡️ *Wallet* -_Dieser Bot ist ein Bitcoin Lightning Wallet der Tips (kleine Beträge) auf Telegram senden kann. Up einen Tip zu senden, füge den Bot in einen Gruppenchat hinzu. Die grundlegende Einheit von Tips sind Satoshis (sat). 100,000,000 sat = 1 Bitcoin. Gebe 📚 /basics ein für mehr Informationen._ +_Dieser Bot ist ein Bitcoin Lightning Wallet der Tips (kleine Beträge) auf Telegram senden kann. Um ein Tip zu senden, füge den Bot in einen Gruppenchat hinzu. Die grundlegende Einheit von Tips sind Satoshis (sat). 100,000,000 sat = 1 Bitcoin. Gebe 📚 /basics ein für mehr Informationen._ ❤️ *Spenden* _Dieser Bot erhebt keine Gebühren, kostet aber Satoshis um betrieben zu werden. Wenn du den Bot magst, kannst das Projekt mit eine Spende unterstützen. Um zu spenden, gebe ein: _ `/donate 1000` @@ -74,7 +74,7 @@ _Dieser Bot erhebt keine Gebühren, kostet aber Satoshis um betrieben zu werden. */invoice* ⚡️ Empfange mit Lightning: `/invoice []` */pay* ⚡️ Bezahle mit Lightning: `/pay ` */donate* ❤️ Spende an das Projekt: `/donate 1000` -*/advanced* 🤖 Fortgerschittene Funktionen. +*/advanced* 🤖 Fortgeschrittene Funktionen. */help* 📖 Rufe die Hilfe auf.""" infoHelpMessage = """ℹ️ *Info*""" @@ -99,12 +99,12 @@ _Dieser Bot ist kostenlose_ [Open Source](https://github.com/LightningTipBot/Lig _Füge den Bot in deine Telegram Gruppenchats hinzu, um Nachrichten ein /tip zu senden. Wenn der Bot Admin der Gruppe ist, wird er auch den Chat aufräumen, indem er manche Befehle nach Ausführung löscht._ 🏛 *Bedingungen* -_Wir sind keine Verwalter deines Geldes. Wir werden in deinem besten Interesse handeln, aber sind uns auch dessen bewusst, dass die Situation ohne KYC etwas kompliziert ist. Solange wir keine andere Lösung gefunden haben, werden wir alle Beträge auf diesem Bot als Spenden betrachten. Gebe uns nicht dein ganzes Geld. Sei dir dessen bewusst, dass dieser Bot immernoch in der Betaphase ist. Benutzung auf eigene Gefahr._ +_Wir sind keine Verwalter deines Geldes. Wir werden in deinem besten Interesse handeln, aber sind uns auch dessen bewusst, dass die Situation ohne KYC etwas kompliziert ist. Solange wir keine andere Lösung gefunden haben, werden wir alle Beträge auf diesem Bot als Spenden betrachten. Gebe uns nicht dein ganzes Geld. Sei dir dessen bewusst, dass dieser Bot immer noch in der Betaphase ist. Benutzung auf eigene Gefahr._ ❤️ *Spenden* _Dieser Bot erhebt keine Gebühren, kostet aber Satoshis um betrieben zu werden. Wenn du den Bot magst, kannst das Projekt mit eine Spende unterstützen. Um zu spenden, gebe ein: _ `/donate 1000`""" -helpNoUsernameMessage = """👋 Bitte seteze einen Telegram Nutzernamen.""" +helpNoUsernameMessage = """👋 Bitte setze einen Telegram Benutzernamen.""" advancedMessage = """%s @@ -125,13 +125,13 @@ advancedMessage = """%s startSettingWalletMessage = """🧮 Richte dein Wallet ein...""" startWalletCreatedMessage = """🧮 Wallet erstellt.""" startWalletReadyMessage = """✅ *Dein Wallet ist bereit.*""" -startWalletErrorMessage = """🚫 Fehler beim einrichten deines Wallets. Bitte versuche es später noch ein mal.""" +startWalletErrorMessage = """🚫 Fehler beim einrichten deines Wallets. Bitte versuche es später noch einmal.""" startNoUsernameMessage = """☝️ Es sieht so aus, als hättest du noch keinen Telegram @usernamen. Das ist ok, du brauchst keinen, um diesen Bot zu verwenden. Um jedoch alle Funktionen verwenden zu können, solltest du dir einen Usernamen in den Telegram Einstellungen setzen. Gebe danach ein mal /balance ein, damit der Bot seine Informationen über dich aktualisieren kann.""" # BALANCE balanceMessage = """👑 *Dein Guthaben:* %d sat""" -balanceErrorMessage = """🚫 Konnte dein Guthaben nicht abfragen. Bitte versuche es später noch ein mal.""" +balanceErrorMessage = """🚫 Konnte dein Guthaben nicht abfragen. Bitte versuche es später noch einmal.""" # TIP @@ -143,7 +143,7 @@ tipYourselfMessage = """📖 Du kannst dir nicht selbst Tips senden.""" tipSentMessage = """💸 %d sat an %s gesendet.""" tipReceivedMessage = """🏅 %s hat dir ein Tip von %d sat gesendet.""" tipErrorMessage = """🚫 Tip fehlgeschlagen.""" -tipUndefinedErrorMsg = """bitte versuche es später noch ein mal.""" +tipUndefinedErrorMsg = """bitte versuche es später noch einmal.""" tipHelpText = """📖 Ups, das hat nicht geklappt. %s *Befehl:* `/tip []` @@ -160,7 +160,7 @@ sendErrorMessage = """🚫 Senden fehlgeschlagen.""" confirmSendMessage = """Möchtest eine Zahlung an %s senden?\n\n💸 Betrag: %d sat""" confirmSendAppendMemo = """\n✉️ %s""" sendCancelledMessage = """🚫 Senden abgebrochen.""" -errorTryLaterMessage = """🚫 Fehler. Bitte versuche es später noch ein mal.""" +errorTryLaterMessage = """🚫 Fehler. Bitte versuche es später noch einmal.""" sendSyntaxErrorMessage = """Hast du einen gültigen Betrag und Empfänger eingegeben? Du kannst den /send Befehl verwenden, um entweder an Telegram Nutzer zu senden, wie z.B. %s oder an eine Lightning Adresse, wie LightningTipBot@ln.tips.""" sendHelpText = """📖 Ups, das hat nicht geklappt. %s @@ -188,7 +188,7 @@ insufficientFundsMessage = """🚫 Guthaben zu niedrig. Du hast %d sat aber brau feeReserveMessage = """⚠️ Dein gesamtes Guthaben zu senden könnte wegen den Netzwerkgebühren fehlschlagen. Falls das passiert, versuche es mit einem etwas geringeren Betrag.""" invoicePaymentFailedMessage = """🚫 Zahlung fehlgeschlagen: %s""" invoiceUndefinedErrorMessage = """Konnte Invoice nicht bezahlen.""" -confirmPayInvoiceMessage = """Mochtest du diese Zahlung senden?\n\n💸 Betrag: %d sat""" +confirmPayInvoiceMessage = """Möchtest du diese Zahlung senden?\n\n💸 Betrag: %d sat""" confirmPayAppendMemo = """\n✉️ %s""" payHelpText = """📖 Ups, das hat nicht geklappt. %s @@ -237,7 +237,7 @@ walletConnectMessage = """🔗 *Verbinde dein Wallet* - *BlueWallet:* Drücke *New wallet*, *Import wallet*, *Scan or import a file*, und scanne den QR Code. - *Zeus:* Kiere die URL unten, drücke *Add a new node*, *Import* (die URL), *Save Node Config*.""" -couldNotLinkMessage = """🚫 Konnte dein Wallet nicht verbinden. Bitte versuche es später noch ein mal.""" +couldNotLinkMessage = """🚫 Konnte dein Wallet nicht verbinden. Bitte versuche es später noch einmal.""" # FAUCET From 5784126bbd6b989a75b52f9b7729b24e869ce3b5 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Sep 2021 18:08:23 +0200 Subject: [PATCH 006/541] Amount k (#79) * amounts with k * faucet with getAmount --- amounts.go | 14 ++++++++++++-- inline_faucet.go | 5 ++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/amounts.go b/amounts.go index 53074944..34e81852 100644 --- a/amounts.go +++ b/amounts.go @@ -20,12 +20,22 @@ func decodeAmountFromCommand(input string) (amount int, err error) { // log.Errorln(errmsg) return 0, errors.New(errmsg) } - amount, err = getAmount(input) + amount, err = getAmount(strings.Split(input, " ")[1]) return amount, err } func getAmount(input string) (amount int, err error) { - amount, err = strconv.Atoi(strings.Split(input, " ")[1]) + // convert something like 1.2k into 1200 + if strings.HasSuffix(input, "k") { + fmount, err := strconv.ParseFloat(strings.TrimSpace(input[:len(input)-1]), 64) + if err != nil { + return 0, err + } + amount = int(fmount * 1000) + return amount, err + } + + amount, err = strconv.Atoi(input) if err != nil { return 0, err } diff --git a/inline_faucet.go b/inline_faucet.go index 6486ec8e..bcd78092 100644 --- a/inline_faucet.go +++ b/inline_faucet.go @@ -3,7 +3,6 @@ package main import ( "context" "fmt" - "strconv" "time" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -128,7 +127,7 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { bot.tryDeleteMessage(m) return } - inlineFaucet.PerUserAmount, err = strconv.Atoi(peruserStr) + inlineFaucet.PerUserAmount, err = getAmount(peruserStr) if err != nil { bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetInvalidAmountMessage"))) bot.tryDeleteMessage(m) @@ -206,7 +205,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } - inlineFaucet.PerUserAmount, err = strconv.Atoi(peruserStr) + inlineFaucet.PerUserAmount, err = getAmount(peruserStr) if err != nil { bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return From 4d5731bf516dce05442daa44fb54994731d0ccae Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Sep 2021 10:19:56 +0200 Subject: [PATCH 007/541] adjust es.toml (#81) * adjust es.toml * adjust es.toml --- translations/es.toml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/translations/es.toml b/translations/es.toml index bd14b059..339becf7 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -1,7 +1,7 @@ # COMMANDS helpCommandStr = """ayuda""" -basicsCommandStr = """basico""" +basicsCommandStr = """fundamentos""" tipCommandStr = """tip""" balanceCommandStr = """saldo""" sendCommandStr = """enviar""" @@ -31,7 +31,7 @@ infoCommandStr = """info""" # NOTIFICATIONS cantDoThatMessage = """No puedes hacer eso.""" -cantClickMessage = """No puedes clicar este botón.""" +cantClickMessage = """No puedes dar click en este botón.""" balanceTooLowMessage = """Tu saldo es muy bajo.""" # BUTTONS @@ -60,7 +60,7 @@ infoButtonMessage = """Info""" # HELP helpMessage = """⚡️ *Monedero* -_Este bot es un monedero Bitcoin Lightning que puede enviar tips vía Telegram. Para dar tips, añade el bot a un chat de grupo. La unidad básica de los tips son los Satoshis (sat). 100.000.000 sat = 1 Bitcoin. Escribe 📚 /basics para saber más._ +_Este bot es un monedero Bitcoin Lightning que puede enviar tips (propinas) vía Telegram. Para dar tips, añade el bot a un chat de grupo. La unidad básica de los tips son los Satoshis (sat). 100.000.000 sat = 1 Bitcoin. Escribe 📚 /basics para saber más._ ❤️ *Donar* _Este bot no cobra ninguna comisión, pero su funcionamiento cuesta Satoshis. Si te gusta el bot, por favor considera apoyar este proyecto con una donación. Para donar, usa_ `/donate 1000`. @@ -84,10 +84,10 @@ basicsMessage = """🧡 *Bitcoin* _Bitcoin es la moneda de Internet. Es una moneda sin permisos y descentralizada que no tiene dueños ni autoridad que la controle. Bitcoin es un dinero sólido que es más rápido, más seguro y más inclusivo que el sistema financiero fiduciario._ 🧮 *Economía* -_La unidad más pequeña de Bitcoin son los Satoshis (sat) y 100.000.000 sat = 1 Bitcoin. Sólo habrá 21 millones de Bitcoin. El valor de Bitcoin en moneda fiduciaria puede cambiar diariamente. Sin embargo, si usted vive en un patrón Bitcoin, 1 sat siempre será igual a 1 sat._ +_La unidad más pequeña de Bitcoin son los Satoshis (sat) y 100.000.000 sat = 1 Bitcoin. Sólo habrá 21 millones de Bitcoin. El valor de Bitcoin en moneda fiduciaria puede cambiar diariamente. Sin embargo, si vives en el patrón Bitcoin, 1 sat siempre será igual a 1 sat._ ⚡️ *La Red Lightning* -_La red Lightning es un protocolo de pago que permite realizar pagos con Bitcoin de forma rápida y barata, sin apenas consumo de energía. Es lo que hace escalar Bitcoin a miles de millones de personas en todo el mundo._ +_La red Lightning es un protocolo de pago que permite realizar pagos con Bitcoin de forma rápida y barata, con mínimo consumo de energía. Es lo que hace escalar Bitcoin a miles de millones de personas en todo el mundo._ 📲 *Monederos Lightning* _Tus fondos en este bot pueden ser enviados a cualquier otro monedero Lightning y viceversa. Los monederos Lightning recomendados para su teléfono son_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (sin custodia), o_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(fácil)_. @@ -99,7 +99,7 @@ _Este bot es gratuito y de_ [Código abierto](https://github.com/LightningTipBot _Añade este bot al chat de tu grupo de Telegram para enviar fondos usando /tip. Si haces al bot administrador del grupo, también limpiará los comandos para mantener el chat ordenado._ 🏛 *Términos* -_No somos custodios de tus fondos. Actuaremos en su mejor interés, pero también somos conscientes de que la situación sin KYC es complicada hasta que resolvamos algo. Cualquier cantidad que pongas en tu monedero se considerará una donación. No nos des todo tu dinero. Ten en cuenta que este bot está en desarrollo beta. Utilízalo bajo tu propio riesgo._ +_No somos custodios de tus fondos. Actuaremos en su mejor interés, pero también somos conscientes de que la situación sin KYC es complicada hasta que demos con una solución óptima. Cualquier cantidad que pongas en tu monedero se considerará una donación. No nos des todo tu dinero. Ten en cuenta que este bot está en desarrollo beta. Utilízalo bajo tu propio riesgo._ ❤️ *Donar* _Este bot no cobra ninguna comisión, pero su funcionamiento cuesta satoshis. Si te gusta el bot, por favor considera apoyar este proyecto con una donación. Para donar, use_ `/donate 1000`""" @@ -242,7 +242,7 @@ couldNotLinkMessage = """🚫 No se ha podido vincular su monedero. Por favor, i # FAUCET inlineQueryFaucetTitle = """🚰 Crea un grifo.""" -inlineQueryFaucetDescription = """Uso: @%s grifo """ +inlineQueryFaucetDescription = """Uso: @%s faucet """ inlineResultFaucetTitle = """🚰 Crear un grifo %d sat.""" inlineResultFaucetDescription = """👉 Haz clic aquí para crear un grifo en este chat.""" @@ -254,7 +254,7 @@ inlineFaucetEndedMessage = """🚰 Grifo vacío 🍺\n\n🏅 %d s inlineFaucetAppendMemo = """\n✉️ %s""" inlineFaucetCreateWalletMessage = """Chatea con %s 👈 para gestionar tu cartera.""" inlineFaucetCancelledMessage = """🚫 Grifo cancelado.""" -inlineFaucetInvalidPeruserAmountMessage = """🚫 El monto del usuario no es divisor de la capacidad.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 El monto por usuario no es divisor de la capacidad.""" inlineFaucetInvalidAmountMessage = """🚫 Monto inválido.""" inlineFaucetSentMessage = """🚰 %d sat enviado(s) a %s.""" inlineFaucetReceivedMessage = """🚰 %s te envió %d sat.""" @@ -283,7 +283,7 @@ inlineSendBalanceLowMessage = """🚫 Tu saldo es demasiado bajo (%d sat).""" # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Solicita un pago en un chat.""" -inlineQueryReceiveDescription = """Uso: @%s recibir []""" +inlineQueryReceiveDescription = """Uso: @%s receive []""" inlineResultReceiveTitle = """🏅 Recibir %d sat.""" inlineResultReceiveDescription = """👉 Haga clic para solicitar un pago de %d sat.""" From 7e532f3f01c32f5f98b87c719f04723f93555575 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Sep 2021 19:22:53 +0200 Subject: [PATCH 008/541] Get user hotfix1 (#82) * database error on lookup fail * still return user skeleton on error --- database.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/database.go b/database.go index 9b70216f..be393534 100644 --- a/database.go +++ b/database.go @@ -59,7 +59,7 @@ func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { user := &lnbits.User{Name: strconv.Itoa(u.ID)} tx := bot.database.First(user) if tx.Error != nil { - errmsg := fmt.Sprintf("[GetUser] Couldn't fetch %s's info from database.", GetUserStr(u)) + errmsg := fmt.Sprintf("[GetUser] Couldn't fetch %s from database: %s", GetUserStr(u), tx.Error.Error()) log.Warnln(errmsg) user.Telegram = u return user, tx.Error @@ -70,6 +70,9 @@ func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { // GetUser from telegram user. Update the user if user information changed. func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { user, err := GetLnbitsUser(u, bot) + if err != nil { + return user, err + } go func() { userCopy := bot.copyLowercaseUser(u) if !reflect.DeepEqual(userCopy, user.Telegram) { From a7a7968d098de39e79d27576d4fd516fa29f8bfc Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Sep 2021 23:09:11 +0200 Subject: [PATCH 009/541] fix /pay message (#83) --- translations/es.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/translations/es.toml b/translations/es.toml index 339becf7..8f0b6192 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -157,7 +157,7 @@ sendSentMessage = """💸 %d sat enviado a %s.""" sendPublicSentMessage = """💸 %d sat enviado(s) de %s a %s.""" sendReceivedMessage = """🏅 %s te envió %d sat.""" sendErrorMessage = """🚫 Envío fallido.""" -confirmSendMessage = """¿Desea pagar a %s?\n\n💸 Monto: %d sat?""" +confirmSendMessage = """¿Desea pagar a %s?\n\n💸 Monto: %d sat""" confirmSendAppendMemo = """\n✉️ %s""" sendCancelledMessage = """🚫 Envío cancelado.""" errorTryLaterMessage = """🚫 Error. Por favor, inténtelo de nuevo más tarde.""" @@ -188,7 +188,7 @@ insufficientFundsMessage = """🚫 Fondos insuficientes. Tienes %d sat pero feeReserveMessage = """⚠️ El envío de todo el saldo puede fallar debido a las tarifas de la red. Si falla, intenta enviar un poco menos.""" invoicePaymentFailedMessage = """🚫 Pago fallido: %s""" invoiceUndefinedErrorMessage = """No se pudo pagar la factura.""" -confirmPayInvoiceMessage = """¿Desea pagar a %s?\n\n💸 Monto: %d sat?""" +confirmPayInvoiceMessage = """¿Quieres enviar este pago?\n\n💸 Monto: %d sat""" confirmPayAppendMemo = """\n✉️ %s""" payHelpText = """📖 Oops, eso no funcionó. %s From a9b9d0bfdd825dfe18531f9dad2076737f9f2433 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 27 Sep 2021 00:14:35 +0200 Subject: [PATCH 010/541] translate faucets (#84) --- handler.go | 2 +- inline_faucet.go | 10 ++++++++++ inline_query.go | 26 +++++++++++++++++++++++++- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/handler.go b/handler.go index 7990c647..5e347f35 100644 --- a/handler.go +++ b/handler.go @@ -94,7 +94,7 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{Type: MessageInterceptor}, }, { - Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan"}, + Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan", "/grifo"}, Handler: bot.faucetHandler, Interceptor: &Interceptor{ Type: MessageInterceptor, diff --git a/inline_faucet.go b/inline_faucet.go index bcd78092..a911cb17 100644 --- a/inline_faucet.go +++ b/inline_faucet.go @@ -3,6 +3,7 @@ package main import ( "context" "fmt" + "strings" "time" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -107,12 +108,21 @@ func (bot *TipBot) getInlineFaucet(c *tb.Callback) (*InlineFaucet, error) { } +func (bot TipBot) mapFaucetLanguage(ctx context.Context, command string) context.Context { + if len(strings.Split(command, " ")) > 1 { + c := strings.Split(command, " ")[0][1:] // cut the / + ctx = bot.commandTranslationMap(ctx, c) + } + return ctx +} + func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) if m.Private() { bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetHelpFaucetInGroup"))) return } + ctx = bot.mapFaucetLanguage(ctx, m.Text) inlineFaucet := NewInlineFaucet() var err error inlineFaucet.Amount, err = decodeAmountFromCommand(m.Text) diff --git a/inline_query.go b/inline_query.go index 59d351cf..be3df875 100644 --- a/inline_query.go +++ b/inline_query.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/nicksnyder/go-i18n/v2/i18n" log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" ) @@ -88,6 +89,25 @@ func (bot TipBot) anyChosenInlineHandler(q *tb.ChosenInlineResult) { fmt.Printf(q.Query) } +func (bot TipBot) commandTranslationMap(ctx context.Context, command string) context.Context { + switch command { + // is default, we don't have to check it + // case "faucet": + // ctx = context.WithValue(ctx, "publicLanguageCode", "en") + // ctx = context.WithValue(ctx, "publicLocalizer", i18n.NewLocalizer(bot.bundle, "en")) + case "zapfhahn": + ctx = context.WithValue(ctx, "publicLanguageCode", "de") + ctx = context.WithValue(ctx, "publicLocalizer", i18n.NewLocalizer(bot.bundle, "de")) + case "kraan": + ctx = context.WithValue(ctx, "publicLanguageCode", "nl") + ctx = context.WithValue(ctx, "publicLocalizer", i18n.NewLocalizer(bot.bundle, "nl")) + case "grifo": + ctx = context.WithValue(ctx, "publicLanguageCode", "es") + ctx = context.WithValue(ctx, "publicLocalizer", i18n.NewLocalizer(bot.bundle, "es")) + } + return ctx +} + func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) { if q.Text == "" { bot.inlineQueryInstructions(ctx, q) @@ -102,7 +122,11 @@ func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) { bot.handleInlineSendQuery(ctx, q) } - if strings.HasPrefix(q.Text, "faucet") || strings.HasPrefix(q.Text, "giveaway") || strings.HasPrefix(q.Text, "zapfhahn") || strings.HasPrefix(q.Text, "kraan") { + if strings.HasPrefix(q.Text, "faucet") || strings.HasPrefix(q.Text, "zapfhahn") || strings.HasPrefix(q.Text, "kraan") || strings.HasPrefix(q.Text, "grifo") { + if len(strings.Split(q.Text, " ")) > 1 { + c := strings.Split(q.Text, " ")[0] + ctx = bot.commandTranslationMap(ctx, c) + } bot.handleInlineFaucetQuery(ctx, q) } From 31caa37657c26d3fcb01f447310f72a7a0085d02 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 27 Sep 2021 00:57:59 +0200 Subject: [PATCH 011/541] Inline translations (#85) * translate faucets * fix faucet buttons --- inline_faucet.go | 4 ++-- translations/nl.toml | 40 ++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/inline_faucet.go b/inline_faucet.go index a911cb17..7038f661 100644 --- a/inline_faucet.go +++ b/inline_faucet.go @@ -391,8 +391,8 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback // register new inline buttons inlineFaucetMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} - acceptInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "collectButtonMessage"), "confirm_faucet_inline") - cancelInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_faucet_inline") + acceptInlineFaucetButton := inlineFaucetMenu.Data(bot.Translate(inlineFaucet.LanguageCode, "collectButtonMessage"), "confirm_faucet_inline") + cancelInlineFaucetButton := inlineFaucetMenu.Data(bot.Translate(inlineFaucet.LanguageCode, "cancelButtonMessage"), "cancel_faucet_inline") acceptInlineFaucetButton.Data = inlineFaucet.ID cancelInlineFaucetButton.Data = inlineFaucet.ID diff --git a/translations/nl.toml b/translations/nl.toml index 32d81189..4fc8d75d 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -36,26 +36,26 @@ balanceTooLowMessage = """Uw saldo is te laag.""" # BUTTONS -sendButtonMessage = """✅ verzenden""" -payButtonMessage = """✅ betalen""" -payReceiveButtonMessage = """💸 betalen""" -receiveButtonMessage = """✅ ontvangen""" -cancelButtonMessage = """🚫 annuleren""" -collectButtonMessage = """✅ verzamelen""" -nextButtonMessage = """volgende""" -backButtonMessage = """terug""" -acceptButtonMessage = """accepteren""" -denyButtonMessage = """weigeren""" -tipButtonMessage = """tip""" -revealButtonMessage = """laat zijn""" -showButtonMessage = """toon""" -hideButtonMessage = """verbergen""" -joinButtonMessage = """deelnemen""" -optionsButtonMessage = """opties""" -settingsButtonMessage = """instellingen""" -saveButtonMessage = """save""" -deleteButtonMessage = """verwijderen""" -infoButtonMessage = """info""" +sendButtonMessage = """✅ Verzenden""" +payButtonMessage = """✅ Betalen""" +payReceiveButtonMessage = """💸 Betalen""" +receiveButtonMessage = """✅ Ontvangen""" +cancelButtonMessage = """🚫 Annuleren""" +collectButtonMessage = """✅ Verzamelen""" +nextButtonMessage = """Volgende""" +backButtonMessage = """Terug""" +acceptButtonMessage = """Accepteren""" +denyButtonMessage = """Weigeren""" +tipButtonMessage = """Tip""" +revealButtonMessage = """Laat zijn""" +showButtonMessage = """Toon""" +hideButtonMessage = """Verbergen""" +joinButtonMessage = """Deelnemen""" +optionsButtonMessage = """Ppties""" +settingsButtonMessage = """Instellingen""" +saveButtonMessage = """Save""" +deleteButtonMessage = """Serwijderen""" +infoButtonMessage = """Info""" # HELP From 48fce72559c8f18cbb23115d394c50918f9552e2 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 6 Oct 2021 01:40:16 +0300 Subject: [PATCH 012/541] Italian translation (#87) * add it.toml * fix toml and load localizzazione * fix commands * update translation * fix strings * remove duplicate Co-authored-by: lngohumble --- internal/i18n/localize.go | 2 +- translations/README.md | 7 + translations/it.toml | 296 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 304 insertions(+), 1 deletion(-) create mode 100644 translations/it.toml diff --git a/internal/i18n/localize.go b/internal/i18n/localize.go index 50d7d9bc..65545846 100644 --- a/internal/i18n/localize.go +++ b/internal/i18n/localize.go @@ -16,8 +16,8 @@ func RegisterLanguages() *i18n.Bundle { bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) bundle.MustLoadMessageFile("translations/en.toml") bundle.LoadMessageFile("translations/de.toml") + bundle.LoadMessageFile("translations/it.toml") bundle.LoadMessageFile("translations/es.toml") bundle.LoadMessageFile("translations/nl.toml") - return bundle } diff --git a/translations/README.md b/translations/README.md index 50946c7e..765f84a9 100644 --- a/translations/README.md +++ b/translations/README.md @@ -2,6 +2,13 @@ Thank you for helping to translate this bot into many different languages. If you chose to translate this bot, please try to test every possible case that you can think of. As time passes, new features will be added and your translation could become out of date. It would be great, if you could update your language's translation if you notice any weird changes. +## Quick and dirty summary +* Duplicate `en.toml` to your localization and edit string by string. +* Do not translate commands in the text! I.e. `/balance` stays `/balance`. +* Pay attention to every single `"""` and `%s` or `%d`. +* Your end resiult should have exactly the same number of lines as `en.toml`. +* Start sentences with `C`apital letters, end them with a full stop`.` + ## General * The bot checks the language settings of each Telegram user and translate the interaction with the user (private chats, **inline commands?**) to the user's language, if a translation is available. Otherwise, it will default to english. All messages in groups will be english. If the user does not have a language setting, it will default to english. * For now, all `/commands` are in english. That means that all `/command` references in the help messages should remain english for now. We plan to implement localized commands, which is why you will find the strings in the translation files. Please chose simple, single-worded, lower-case, for the command translations. diff --git a/translations/it.toml b/translations/it.toml new file mode 100644 index 00000000..0d88ed0b --- /dev/null +++ b/translations/it.toml @@ -0,0 +1,296 @@ +# COMMANDS + +helpCommandStr = """aiuto""" +basicsCommandStr = """informazioni""" +tipCommandStr = """mancia""" +balanceCommandStr = """saldo""" +sendCommandStr = """invia""" +invoiceCommandStr = """invoice""" +payCommandStr = """paga""" +donateCommandStr = """dona""" +advancedCommandStr = """avanzate""" +transactionsCommandStr = """traduzioni""" +logCommandStr = """log""" +listCommandStr = """lista""" + +linkCommandStr = """link""" +lnurlCommandStr = """lnurl""" +faucetCommandStr = """distribuzione""" + +tipjarCommandStr = """salvadanaio""" +receiveCommandStr = """ricevi""" +hideCommandStr = """nascondi""" +volcanoCommandStr = """vulcano""" +showCommandStr = """mostra""" +optionsCommandStr = """opzioni""" +settingsCommandStr = """impostazioni""" +saveCommandStr = """salva""" +deleteCommandStr = """cancella""" +infoCommandStr = """info""" + +# NOTIFICATIONS + +cantDoThatMessage = """Funzione non disponibile.""" +cantClickMessage = """Pulsante non selezionabile.""" +balanceTooLowMessage = """Saldo non sufficiente.""" + +# BUTTONS + +sendButtonMessage = """✅ Invia""" +payButtonMessage = """✅ Paga""" +payReceiveButtonMessage = """💸 Paga""" +receiveButtonMessage = """✅ Ricevi""" +cancelButtonMessage = """🚫 Cancella""" +collectButtonMessage = """✅ Incassa""" +nextButtonMessage = """Prossimo""" +backButtonMessage = """Indietro""" +acceptButtonMessage = """Consenti""" +denyButtonMessage = """Rifiuta""" +tipButtonMessage = """Mancia""" +revealButtonMessage = """Rivela""" +showButtonMessage = """Mostra""" +hideButtonMessage = """Nascondi""" +joinButtonMessage = """Unisciti""" +optionsButtonMessage = """Opzioni""" +settingsButtonMessage = """Impostazioni""" +saveButtonMessage = """Salva""" +deleteButtonMessage = """Cancella""" +infoButtonMessage = """Info""" + +# HELP + +helpMessage = """⚡️ *Wallet* +_Questo bot è un Wallet Bitcoin Lightning con cui puoi inviare ricompense via Telegram. Per inviare, aggiungi il bot come partecipante a una chat di gruppo. L'unità di conto per questi invii di piccolo importo è il Satoshi (sat). 100,000,000 sat = 1 Bitcoin. Digita 📚 /basics per maggiori informazioni._ + +❤️ *Dona* +_Questo bot non applica commissioni agli utenti, ma costa alcuni Satoshi gestirlo. Se ti piace, valuta se sostenere questo progetto con una donazione. Per donare, usa il comando_ `/donate 1000` + +%s + +⚙️ *Comandi* +*/tip* 🏅 Rispondi così a un messaggio per inviare una mancia: `/tip []` +*/balance* 👑 Verifica il tuo saldo residuo: `/balance` +*/send* 💸 Invia fondi a un utente: `/send @utente o utente@ln.tips []` +*/invoice* ⚡️ Ricevi attraverso Lightning: `/invoice []` +*/pay* ⚡️ Paga attraverso Lightning: `/pay ` +*/donate* ❤️ Dona per supportare questo progetto: `/donate 1000` +*/advanced* 🤖 Funzioni avanzate. +*/help* 📖 Richiama questo elenco.""" + +infoHelpMessage = """ℹ️ *Info*""" +infoYourLightningAddress = """Il tuo indirizzo Lightning è `%s`""" + +basicsMessage = """🧡 *Bitcoin* +_Bitcoin è la valuta di internet. È una rete libera e decentralizzata a cui tutti possono partecipare, senza una autorità di controllo centrale. Bitcoin è una moneta ma anche un sistema di pagamento più veloce, sicuro e inclusivo del sistema finanziario tradizionale._ + +🧮 *Economnics* +_La più piccola unità in cui è divisibile un Bitcoin è un Satoshi (sat) e 100,000,000 sat equivalgono a 1 Bitcoin. Esistono solamente 21 milioni di Bitcoin. Il valore di Bitcoin espresso in valuta tradizionale può variare ogni giorno. In ogni caso, se la tua valuta di riferimento è Bitcoin, 1 sat varrà sempre 1 sat._ + +⚡️ *Lightning Network* +_Lightning Network è un protocollo di pagamento che consente pagamenti in Bitcoin veloci ed economici, praticamente senza consumare energia. È il sistema che consentirà di diffondere Bitcoin a miliardi di persone nel mondo._ + +📲 *Lightning Wallets* +_I tuoi fondi conservati in questo bot possono essere mandati a un altro wallet Lightning e viceversa. Altri wallet Lightning per il tuo smartphone sono_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (non-custodial), oppure_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(semplice da usare)_. + +📄 *Open Source* +_Questo bot è software libero e_ [open source](https://github.com/LightningTipBot/LightningTipBot) _. Puoi farlo girare sul tuo computer e usarlo per la tua comunità._ + +✈️ *Telegram* +_Aggiungi questo bot a una chat di gruppo Telegram per inviare una /tip. Se concedi al bot i privilegi di amministratore della chat, il bot si occuperà automaticamente di eliminare i comandi inviati per tenere la chat pulita._ + +🏛 *Termini e condizioni* +_Noi non siamo custodi dei tuoi fondi. Agiremo sempre nel tuo migliore interesse, ma siamo consapevoli che operare senza il riconoscimento formale dei Clienti (KYC) è un terreno accidentato finché non troviamo qualche soluzione alternativa. Qualsiasi ammontare caricato nel wallet sarà formalmente considerato una donazione. Non inviarci tutto il tuo denaro! Sii consapevole che questo bot è ancora in fase di sviluppo, e puoi usarlo a tuo rischio e pericolo._ + +❤️ *Dona* +_Questo bot non applica commissioni agli utenti, ma costa alcuni Satoshi gestirlo. Se ti piace, valuta se sostenere questo progetto con una donazione. Per donare, usa il comando_ `/donate 1000`""" + +helpNoUsernameMessage = """👋 Per favore imposta un nome utente Telegram.""" + +advancedMessage = """%s + +👉 *Comandi in linea* +*send* 💸 Invia alcuni sat a una chat: `%s send []` +*receive* 🏅 Richiedi un pagamento: `%s receive []` +*faucet* 🚰 Eroga fondi ai partecipanti della chat: `%s faucet ` + +📖 Puoi usare i comandi in linea in ogni chat, anche nelle conversazioni private. Attendi un secondo dopo aver inviato un comando in linea e *clicca* sull'azione desiderata, non premere invio. + +⚙️ *Comandi avanzati* +*/link* 🔗 Crea un collegamento al tuo wallet [BlueWallet](https://bluewallet.io/) o [Zeus](https://zeusln.app/) +*/lnurl* ⚡️ Ricevi o paga un Lnurl: `/lnurl` or `/lnurl ` +*/faucet* 🚰 Eroga fondi ai partecipanti della chat `/faucet `""" + +# START + +startSettingWalletMessage = """🧮 Sto creando il tuo wallet...""" +startWalletCreatedMessage = """🧮 Wallet creato.""" +startWalletReadyMessage = """✅ *Il tuo wallet è pronto.*""" +startWalletErrorMessage = """🚫 Errore di inizializzazione del wallet. Riprova più tardi.""" +startNoUsernameMessage = """☝️ Sembra che tu non abbia un nome utente Telegram @username. Non è obbligatorio per utilizzare questo bot, ma consente di abilitare ulteriori funzioni. Per usare al meglio il tuo wallet, imposta un nome utente nelle impostazioni Telegram e poi inserisci /balance in modo che il bot possa aggiornarsi.""" + +# BALANCE + +balanceMessage = """👑 *Il tuo saldo è:* %d sat""" +balanceErrorMessage = """🚫 Non riesco a recupare il tuo saldo. Per favore riprova più tardi.""" + +# TIP + +tipDidYouReplyMessage = """Hai risposto a un messaggio per inviare una mancia? Per rispondere a un messaggio, clicca con il tasto destro -> Rispondi sul tuo computer o fai swipe sul tuo smartphone. Se vuoi inviare direttamente a un altro utente, usa il comando /send.""" +tipInviteGroupMessage = """ℹ️ In ogni caso, puoi invitare questo bot in qualsiasi chat di gruppo per incominciare a inviare mance.""" +tipEnterAmountMessage = """Hai inserito un ammontare?""" +tipValidAmountMessage = """Hai inserito un ammontare valido?""" +tipYourselfMessage = """📖 Non puoi inviare una mancia a te stesso.""" +tipSentMessage = """💸 %d sat inviati a %s.""" +tipReceivedMessage = """🏅 %s ti ha inviato una mancia di %d sat.""" +tipErrorMessage = """🚫 Invio mancia non riuscito.""" +tipUndefinedErrorMsg = """Per favore riprova più tardi""" +tipHelpText = """📖 Ops, non funziona. %s + +*Usage:* `/tip []` +*Example:* `/tip 1000 meme fantastico!`""" + +# SEND + +sendValidAmountMessage = """Hai inserito un ammontare valido?""" +sendUserHasNoWalletMessage = """🚫 L'utente %s non ha ancora creato un wallet.""" +sendSentMessage = """💸 %d sat inviati a %s.""" +sendPublicSentMessage = """💸 %d sat inviati da %s a %s.""" +sendReceivedMessage = """🏅 %s ti ha inviato %d sat.""" +sendErrorMessage = """🚫 Invio non riuscito.""" +confirmSendMessage = """Vuoi inviare un pagamento a %s?\n\n💸 Ammontare: %d sat""" +confirmSendAppendMemo = """\n✉️ %s""" +sendCancelledMessage = """🚫 Invio cancellato.""" +errorTryLaterMessage = """🚫 Errore. Per favore riprova più tardi.""" +sendSyntaxErrorMessage = """Hai specificato un ammontare e un destinatario? Puoi usare il comando /send per inviare sia a utenti Telegram come %s sia a un indirizzo Lightning del tipo LightningTipBot@ln.tips.""" +sendHelpText = """📖 Ops, non ha funzionato. %s + +*Sintassi:* `/send []` +*Esempio:* `/send 1000 @LightningTipBot Amo questo bot ❤️` +*Esempio:* `/send 1234 LightningTipBot@ln.tips`""" + +# INVOICE + +invoiceEnterAmountMessage = """Hai inserito un ammontare?""" +invoiceValidAmountMessage = """Hai inserito un ammontare valido?""" +invoiceHelpText = """📖 Ops, non ha funzionato. %s + +*Sintassi:* `/invoice []` +*Esempio:* `/invoice 1000 Grazie!`""" + +# PAY + +paymentCancelledMessage = """🚫 Pagamento cancellato.""" +invoicePaidMessage = """⚡️ Pagamento inviato.""" +invoicePublicPaidMessage = """⚡️ Pagamento inviato da %s.""" +invalidInvoiceHelpMessage = """Hai inserito una invoice Lightning valida? Prova /send se vuoi inviare fondi a un utente Telegram o a un indirizzo Lightning.""" +invoiceNoAmountMessage = """🚫 Non è possibile pagare questa invoice senza specificare un ammontare.""" +insufficientFundsMessage = """🚫 Fondi insufficienti. Hai in portafoglio %d sat ma servono almeno %d sat per l'invio.""" +feeReserveMessage = """⚠️ Inviare il tuo intero saldo potrebbe non essere possibile a causa della incidenza delle commissioni di rete. Se l'invio non va a buon fine, prova a inviare un ammontare leggermente inferiore.""" +invoicePaymentFailedMessage = """🚫 Pagamento non riuscito: %s""" +invoiceUndefinedErrorMessage = """Non è stato possibile pagare questa invoice.""" +confirmPayInvoiceMessage = """Vuoi inviare questo pagamento?\n\n💸 Amount: %d sat""" +confirmPayAppendMemo = """\n✉️ %s""" +payHelpText = """📖 Ops, non ha funzionato. %s + +*Sintassi:* `/pay ` +*Esempio:* `/pay lnbc20n1psscehd...`""" + +# DONATE + +donationSuccess = """🙏 Grazie per la donazione.""" +donationErrorMessage = """🚫 Oh no, la tua donazione non è andata a buon fine.""" +donationProgressMessage = """🧮 Sto preparando la donazione...""" +donationFailedMessage = """🚫 La donazione non è andata a buon fine: %s""" +donateEnterAmountMessage = """Hai inserito un ammontare?""" +donateValidAmountMessage = """Hai inserito un ammontare valido?""" +donateHelpText = """📖 Ops, non ha funzionato. %s + +*Sintassi:* `/donate ` +*Esepio:* `/donate 1000`""" + +# PHOTO + +photoQrNotRecognizedMessage = """🚫 Non sono riuscito a riconoscere una invoice Lightning o un LNURL. Cerca cortesemente di centrare meglio il codice QR oppure prova a ritagliare o ingrandire l'immagine.""" +photoQrRecognizedMessage = """✅ Codice QR: +`%s`""" + +# LNURL + +lnurlReceiveInfoText = """👇 Puoi usare questo LNURL statico per ricevere pagamenti.""" +lnurlResolvingUrlMessage = """🧮 Recupero indirizzo...""" +lnurlGettingUserMessage = """🧮 Preparazione pagamento...""" +lnurlPaymentFailed = """🚫 Pagamento non riuscito: %s""" +lnurlInvalidAmountMessage = """🚫 Ammontare non valido.""" +lnurlInvalidAmountRangeMessage = """🚫 L'ammontare deve essere compreso tra %d e %d sat.""" +lnurlNoUsernameMessage = """🚫 Devi impostare un nome utente Telegram per ricevere pagamenti tramite un LNURL.""" +lnurlEnterAmountMessage = """⌨️ Imposta un ammontare tra %d e %d sat.""" +lnurlHelpText = """📖 Ops, non ha funzionato. %s + +*Sintassi:* `/lnurl [ammontare] ` +*Esempio:* `/lnurl LNURL1DP68GUR...`""" + +# LINK + +walletConnectMessage = """🔗 *Collega il tuo wallet* + +⚠️ Non mostrare mai la URL il codice QR, altrimenti qualcuno potrebbe avere accesso ai tuoi fondi. + +- *BlueWallet:* Premi *+ (Aggiungi Portafoglio)*, *Importa portafoglio*, *scansionare un codice QR*, e scansiona il codice QR . +- *Zeus:* Copia la URL qui sotto, premi *Add a new node*, *Import* (incolla la URL), *Save Node Config*.""" +couldNotLinkMessage = """🚫 Non sono riuscito a collegare il tuo wallet. Per favore riprova più tardi.""" + +# FAUCET + +inlineQueryFaucetTitle = """🚰 Crea una distribuzione di fondi.""" +inlineQueryFaucetDescription = """Sintassi: @%s faucet """ +inlineResultFaucetTitle = """🚰 Crea una distribuzione per un totale di %d sat.""" +inlineResultFaucetDescription = """👉 Clicca qui per creare una distribuzione di fondi in questa chat.""" + +inlineFaucetMessage = """Premi ✅ per riscuotere %d sat da questa distribuzione. + +🚰 Rimanente: %d/%d sat (distribuiti a %d/%d utenti) +%s""" +inlineFaucetEndedMessage = """🚰 Distribuzione completata 🍺\n\n🏅 %d sat distribuiti a %d utenti.""" +inlineFaucetAppendMemo = """\n✉️ %s""" +inlineFaucetCreateWalletMessage = """Chatta con %s 👈 per gestire il tuo wallet.""" +inlineFaucetCancelledMessage = """🚫 Distribuzione cancellata.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 Ammontare per utente non è una frazione intera del totale.""" +inlineFaucetInvalidAmountMessage = """🚫 Ammontare non valido.""" +inlineFaucetSentMessage = """🚰 %d sat inviati a %s.""" +inlineFaucetReceivedMessage = """🚰 %s ti ha inviato %d sat.""" +inlineFaucetHelpFaucetInGroup = """Crea una distribuzione in un gruppo in cui sia presente il bot oppure usa il 👉 comando in linea (/advanced per ulteriori funzionalità).""" +inlineFaucetHelpText = """📖 Ops, non ha funzionato. %s + +*Sintassi:* `/faucet ` +*Esempio:* `/faucet 210 21`""" + +# INLINE SEND + +inlineQuerySendTitle = """💸 Invia pagamento in una chat.""" +inlineQuerySendDescription = """Sintassi: @%s send []""" +inlineResultSendTitle = """💸 Invio %d sat.""" +inlineResultSendDescription = """👉 Clicca per inviare %d sat in questa chat.""" + +inlineSendMessage = """Premi ✅ per ricevere un pagamento da %s.\n\n💸 Ammontare: %d sat""" +inlineSendAppendMemo = """\n✉️ %s""" +inlineSendUpdateMessageAccept = """💸 %d sat inviati da %s a %s.""" +inlineSendCreateWalletMessage = """Chatta con %s 👈 per gestire il tuo wallet.""" +sendYourselfMessage = """📖 Non puoi inviare un pagamento a te stesso.""" +inlineSendFailedMessage = """🚫 Invio non riuscito.""" +inlineSendInvalidAmountMessage = """🚫 L'ammontare deve essere maggiore di 0.""" +inlineSendBalanceLowMessage = """🚫 Il tuo saldo è insufficiente (%d sat).""" + +# INLINE RECEIVE + +inlineQueryReceiveTitle = """🏅 Richiedi un pagamento in una chat.""" +inlineQueryReceiveDescription = """Sintassi: @%s receive []""" +inlineResultReceiveTitle = """🏅 Ricevi %d sat.""" +inlineResultReceiveDescription = """👉 Clicca per richiedere un pagamento di %d sat.""" + +inlineReceiveMessage = """Premi 💸 per inviare un pagamento a %s.\n\n💸 Ammontare: %d sat""" +inlineReceiveAppendMemo = """\n✉️ %s""" +inlineReceiveUpdateMessageAccept = """💸 %d sat inviati da %s a %s.""" +inlineReceiveCreateWalletMessage = """Chatta con %s 👈 per gestire il tuo wallet.""" +inlineReceiveYourselfMessage = """📖 Non puoi inviare un pagamento a te stesso.""" +inlineReceiveFailedMessage = """🚫 Pagamento non riuscito.""" +inlineReceiveCancelledMessage = """🚫 Pagamento cancellato.""" From f6ef7933f5f3a06d8d2519a6f5ba3f24574e6f46 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 6 Oct 2021 21:32:44 +0300 Subject: [PATCH 013/541] refactor translations (#88) * refactor translations * fix print error * add rest of languages * remove unnecessary package --- bot.go | 4 --- inline_faucet.go | 21 +++++++------- inline_query.go | 11 ++++---- inline_receive.go | 15 +++++----- inline_send.go | 15 +++++----- interceptor.go | 58 +++++++++++++++----------------------- internal/i18n/localize.go | 13 +++++++-- internal/lnbits/webhook.go | 11 +++++--- pay.go | 15 +++++----- send.go | 13 +++++---- tip.go | 5 ++-- translate.go | 8 ------ translations/de.toml | 1 + translations/en.toml | 1 + translations/es.toml | 1 + translations/it.toml | 1 + translations/nl.toml | 1 + 17 files changed, 97 insertions(+), 97 deletions(-) diff --git a/bot.go b/bot.go index f7960065..7b4b7f95 100644 --- a/bot.go +++ b/bot.go @@ -5,11 +5,9 @@ import ( "sync" "time" - "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/storage" - i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" log "github.com/sirupsen/logrus" "gopkg.in/tucnak/telebot.v2" tb "gopkg.in/tucnak/telebot.v2" @@ -22,7 +20,6 @@ type TipBot struct { logger *gorm.DB telegram *telebot.Bot client *lnbits.Client - bundle *i18n2.Bundle } var ( @@ -37,7 +34,6 @@ func NewBot() TipBot { database: db, logger: txLogger, bunt: storage.NewBunt(Configuration.Database.BuntDbPath), - bundle: i18n.RegisterLanguages(), } } diff --git a/inline_faucet.go b/inline_faucet.go index 7038f661..9d2fc350 100644 --- a/inline_faucet.go +++ b/inline_faucet.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" @@ -371,8 +372,8 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.To = append(inlineFaucet.To, to.Telegram) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount - _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(bot.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) - _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(bot.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) + _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) + _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[faucet] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -380,19 +381,19 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback } // build faucet message - inlineFaucet.Message = fmt.Sprintf(bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) + inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) memo := inlineFaucet.Memo if len(memo) > 0 { - inlineFaucet.Message = inlineFaucet.Message + fmt.Sprintf(bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetAppendMemo"), memo) + inlineFaucet.Message = inlineFaucet.Message + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetAppendMemo"), memo) } if inlineFaucet.UserNeedsWallet { - inlineFaucet.Message += "\n\n" + fmt.Sprintf(bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) + inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) } // register new inline buttons inlineFaucetMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} - acceptInlineFaucetButton := inlineFaucetMenu.Data(bot.Translate(inlineFaucet.LanguageCode, "collectButtonMessage"), "confirm_faucet_inline") - cancelInlineFaucetButton := inlineFaucetMenu.Data(bot.Translate(inlineFaucet.LanguageCode, "cancelButtonMessage"), "cancel_faucet_inline") + acceptInlineFaucetButton := inlineFaucetMenu.Data(i18n.Translate(inlineFaucet.LanguageCode, "collectButtonMessage"), "confirm_faucet_inline") + cancelInlineFaucetButton := inlineFaucetMenu.Data(i18n.Translate(inlineFaucet.LanguageCode, "cancelButtonMessage"), "cancel_faucet_inline") acceptInlineFaucetButton.Data = inlineFaucet.ID cancelInlineFaucetButton.Data = inlineFaucet.ID @@ -407,9 +408,9 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback } if inlineFaucet.RemainingAmount < inlineFaucet.PerUserAmount { // faucet is depleted - inlineFaucet.Message = fmt.Sprintf(bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetEndedMessage"), inlineFaucet.Amount, inlineFaucet.NTaken) + inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetEndedMessage"), inlineFaucet.Amount, inlineFaucet.NTaken) if inlineFaucet.UserNeedsWallet { - inlineFaucet.Message += "\n\n" + fmt.Sprintf(bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) + inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) } bot.tryEditMessage(c.Message, inlineFaucet.Message) inlineFaucet.Active = false @@ -424,7 +425,7 @@ func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback return } if c.Sender.ID == inlineFaucet.From.Telegram.ID { - bot.tryEditMessage(c.Message, bot.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false inlineFaucet.InTransaction = false diff --git a/inline_query.go b/inline_query.go index be3df875..7b0d24a0 100644 --- a/inline_query.go +++ b/inline_query.go @@ -6,7 +6,8 @@ import ( "strconv" "strings" - "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" + i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" ) @@ -94,16 +95,16 @@ func (bot TipBot) commandTranslationMap(ctx context.Context, command string) con // is default, we don't have to check it // case "faucet": // ctx = context.WithValue(ctx, "publicLanguageCode", "en") - // ctx = context.WithValue(ctx, "publicLocalizer", i18n.NewLocalizer(bot.bundle, "en")) + // ctx = context.WithValue(ctx, "publicLocalizer", i18n.NewLocalizer(i18n.Bundle, "en")) case "zapfhahn": ctx = context.WithValue(ctx, "publicLanguageCode", "de") - ctx = context.WithValue(ctx, "publicLocalizer", i18n.NewLocalizer(bot.bundle, "de")) + ctx = context.WithValue(ctx, "publicLocalizer", i18n2.NewLocalizer(i18n.Bundle, "de")) case "kraan": ctx = context.WithValue(ctx, "publicLanguageCode", "nl") - ctx = context.WithValue(ctx, "publicLocalizer", i18n.NewLocalizer(bot.bundle, "nl")) + ctx = context.WithValue(ctx, "publicLocalizer", i18n2.NewLocalizer(i18n.Bundle, "nl")) case "grifo": ctx = context.WithValue(ctx, "publicLanguageCode", "es") - ctx = context.WithValue(ctx, "publicLocalizer", i18n.NewLocalizer(bot.bundle, "es")) + ctx = context.WithValue(ctx, "publicLocalizer", i18n2.NewLocalizer(i18n.Bundle, "es")) } return ctx } diff --git a/inline_receive.go b/inline_receive.go index e1523ae5..81bd00ae 100644 --- a/inline_receive.go +++ b/inline_receive.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" @@ -235,26 +236,26 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac if !success { errMsg := fmt.Sprintf("[acceptInlineReceiveHandler] Transaction failed: %s", err) log.Errorln(errMsg) - bot.tryEditMessage(c.Message, bot.Translate(inlineReceive.LanguageCode, "inlineReceiveFailedMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveFailedMessage"), &tb.ReplyMarkup{}) return } log.Infof("[acceptInlineReceiveHandler] %d sat from %s to %s", inlineReceive.Amount, fromUserStr, toUserStr) - inlineReceive.Message = fmt.Sprintf("%s", fmt.Sprintf(bot.Translate(inlineReceive.LanguageCode, "inlineSendUpdateMessageAccept"), inlineReceive.Amount, fromUserStrMd, toUserStrMd)) + inlineReceive.Message = fmt.Sprintf("%s", fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendUpdateMessageAccept"), inlineReceive.Amount, fromUserStrMd, toUserStrMd)) memo := inlineReceive.Memo if len(memo) > 0 { - inlineReceive.Message = inlineReceive.Message + fmt.Sprintf(bot.Translate(inlineReceive.LanguageCode, "inlineReceiveAppendMemo"), memo) + inlineReceive.Message = inlineReceive.Message + fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveAppendMemo"), memo) } if !to.Initialized { - inlineReceive.Message += "\n\n" + fmt.Sprintf(bot.Translate(inlineReceive.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) + inlineReceive.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) } bot.tryEditMessage(c.Message, inlineReceive.Message, &tb.ReplyMarkup{}) // notify users - _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(bot.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, inlineReceive.Amount)) - _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(bot.Translate(from.Telegram.LanguageCode, "sendSentMessage"), inlineReceive.Amount, toUserStrMd)) + _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, inlineReceive.Amount)) + _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), inlineReceive.Amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[acceptInlineReceiveHandler] Error: Receive message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -269,7 +270,7 @@ func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callbac return } if c.Sender.ID == inlineReceive.To.Telegram.ID { - bot.tryEditMessage(c.Message, bot.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineReceive inactive inlineReceive.Active = false inlineReceive.InTransaction = false diff --git a/inline_send.go b/inline_send.go index 029ec156..8b39638d 100644 --- a/inline_send.go +++ b/inline_send.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" @@ -246,26 +247,26 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) if !success { errMsg := fmt.Sprintf("[sendInline] Transaction failed: %s", err) log.Errorln(errMsg) - bot.tryEditMessage(c.Message, bot.Translate(inlineSend.LanguageCode, "inlineSendFailedMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "inlineSendFailedMessage"), &tb.ReplyMarkup{}) return } log.Infof("[sendInline] %d sat from %s to %s", amount, fromUserStr, toUserStr) - inlineSend.Message = fmt.Sprintf("%s", fmt.Sprintf(bot.Translate(inlineSend.LanguageCode, "inlineSendUpdateMessageAccept"), amount, fromUserStrMd, toUserStrMd)) + inlineSend.Message = fmt.Sprintf("%s", fmt.Sprintf(i18n.Translate(inlineSend.LanguageCode, "inlineSendUpdateMessageAccept"), amount, fromUserStrMd, toUserStrMd)) memo := inlineSend.Memo if len(memo) > 0 { - inlineSend.Message = inlineSend.Message + fmt.Sprintf(bot.Translate(inlineSend.LanguageCode, "inlineSendAppendMemo"), memo) + inlineSend.Message = inlineSend.Message + fmt.Sprintf(i18n.Translate(inlineSend.LanguageCode, "inlineSendAppendMemo"), memo) } if !to.Initialized { - inlineSend.Message += "\n\n" + fmt.Sprintf(bot.Translate(inlineSend.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) + inlineSend.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineSend.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) } bot.tryEditMessage(c.Message, inlineSend.Message, &tb.ReplyMarkup{}) // notify users - _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(bot.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) - _, err = bot.telegram.Send(fromUser.Telegram, fmt.Sprintf(bot.Translate(fromUser.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) + _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) + _, err = bot.telegram.Send(fromUser.Telegram, fmt.Sprintf(i18n.Translate(fromUser.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[sendInline] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -280,7 +281,7 @@ func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) return } if c.Sender.ID == inlineSend.From.Telegram.ID { - bot.tryEditMessage(c.Message, bot.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineSend inactive inlineSend.Active = false inlineSend.InTransaction = false diff --git a/interceptor.go b/interceptor.go index a7486cc3..94b390f6 100644 --- a/interceptor.go +++ b/interceptor.go @@ -4,7 +4,8 @@ import ( "context" "fmt" - "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" + i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" @@ -68,40 +69,36 @@ func (bot TipBot) loadReplyToInterceptor(ctx context.Context, i interface{}) (co } func (bot TipBot) localizerInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - var userLanguageCodeContext context.Context - var publicLanguageCodeContext context.Context - var userLocalizerContext context.Context - var publicLocalizerContext context.Context - var userLocalizer *i18n.Localizer - var publicLocalizer *i18n.Localizer + var userLocalizer *i18n2.Localizer + var publicLocalizer *i18n2.Localizer // default language is english - publicLocalizer = i18n.NewLocalizer(bot.bundle, "en") - publicLanguageCodeContext = context.WithValue(ctx, "publicLanguageCode", "en") - publicLocalizerContext = context.WithValue(publicLanguageCodeContext, "publicLocalizer", publicLocalizer) + publicLocalizer = i18n2.NewLocalizer(i18n.Bundle, "en") + ctx = context.WithValue(ctx, "publicLanguageCode", "en") + ctx = context.WithValue(ctx, "publicLocalizer", publicLocalizer) switch i.(type) { case *tb.Message: m := i.(*tb.Message) - userLocalizer = i18n.NewLocalizer(bot.bundle, m.Sender.LanguageCode) - userLanguageCodeContext = context.WithValue(publicLocalizerContext, "userLanguageCode", m.Sender.LanguageCode) - userLocalizerContext = context.WithValue(userLanguageCodeContext, "userLocalizer", userLocalizer) + userLocalizer = i18n2.NewLocalizer(i18n.Bundle, m.Sender.LanguageCode) + ctx = context.WithValue(ctx, "userLanguageCode", m.Sender.LanguageCode) + ctx = context.WithValue(ctx, "userLocalizer", userLocalizer) if m.Private() { // in pm overwrite public localizer with user localizer - publicLanguageCodeContext = context.WithValue(userLocalizerContext, "publicLanguageCode", m.Sender.LanguageCode) - publicLocalizerContext = context.WithValue(publicLanguageCodeContext, "publicLocalizer", userLocalizer) + ctx = context.WithValue(ctx, "publicLanguageCode", m.Sender.LanguageCode) + ctx = context.WithValue(ctx, "publicLocalizer", userLocalizer) } - return publicLocalizerContext, nil + return ctx, nil case *tb.Callback: m := i.(*tb.Callback) - userLocalizer = i18n.NewLocalizer(bot.bundle, m.Sender.LanguageCode) - userLanguageCodeContext = context.WithValue(publicLocalizerContext, "userLanguageCode", m.Sender.LanguageCode) - return context.WithValue(userLanguageCodeContext, "userLocalizer", userLocalizer), nil + userLocalizer = i18n2.NewLocalizer(i18n.Bundle, m.Sender.LanguageCode) + ctx = context.WithValue(ctx, "userLanguageCode", m.Sender.LanguageCode) + return context.WithValue(ctx, "userLocalizer", userLocalizer), nil case *tb.Query: m := i.(*tb.Query) - userLocalizer = i18n.NewLocalizer(bot.bundle, m.From.LanguageCode) - userLanguageCodeContext = context.WithValue(publicLocalizerContext, "userLanguageCode", m.From.LanguageCode) - return context.WithValue(userLanguageCodeContext, "userLocalizer", userLocalizer), nil + userLocalizer = i18n2.NewLocalizer(i18n.Bundle, m.From.LanguageCode) + ctx = context.WithValue(ctx, "userLanguageCode", m.From.LanguageCode) + return context.WithValue(ctx, "userLocalizer", userLocalizer), nil } return ctx, nil } @@ -139,32 +136,23 @@ func (bot TipBot) logMessageInterceptor(ctx context.Context, i interface{}) (con } // LoadUser from context -func LoadUserLocalizer(ctx context.Context) *i18n.Localizer { +func LoadUserLocalizer(ctx context.Context) *i18n2.Localizer { u := ctx.Value("userLocalizer") if u != nil { - return u.(*i18n.Localizer) + return u.(*i18n2.Localizer) } return nil } // LoadUser from context -func LoadPublicLocalizer(ctx context.Context) *i18n.Localizer { +func LoadPublicLocalizer(ctx context.Context) *i18n2.Localizer { u := ctx.Value("publicLocalizer") if u != nil { - return u.(*i18n.Localizer) + return u.(*i18n2.Localizer) } return nil } -// // LoadUser from context -// func LoadLocalizer(ctx context.Context) *i18n.Localizer { -// u := ctx.Value("localizer") -// if u != nil { -// return u.(*i18n.Localizer) -// } -// return nil -// } - // LoadUser from context func LoadUser(ctx context.Context) *lnbits.User { u := ctx.Value("user") diff --git a/internal/i18n/localize.go b/internal/i18n/localize.go index 65545846..4b8ef68e 100644 --- a/internal/i18n/localize.go +++ b/internal/i18n/localize.go @@ -3,15 +3,17 @@ package i18n import ( "github.com/BurntSushi/toml" "github.com/nicksnyder/go-i18n/v2/i18n" + log "github.com/sirupsen/logrus" "golang.org/x/text/language" ) -func init() { +var Bundle *i18n.Bundle +func init() { + Bundle = RegisterLanguages() } func RegisterLanguages() *i18n.Bundle { - bundle := i18n.NewBundle(language.English) bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) bundle.MustLoadMessageFile("translations/en.toml") @@ -21,3 +23,10 @@ func RegisterLanguages() *i18n.Bundle { bundle.LoadMessageFile("translations/nl.toml") return bundle } +func Translate(languageCode string, MessgeID string) string { + str, err := i18n.NewLocalizer(Bundle, languageCode).Localize(&i18n.LocalizeConfig{MessageID: MessgeID}) + if err != nil { + log.Warnf("Error translating message %s: %s", MessgeID, err) + } + return str +} diff --git a/internal/lnbits/webhook.go b/internal/lnbits/webhook.go index d47f27bd..7615fbee 100644 --- a/internal/lnbits/webhook.go +++ b/internal/lnbits/webhook.go @@ -3,19 +3,22 @@ package lnbits import ( "encoding/json" "fmt" - log "github.com/sirupsen/logrus" - "gorm.io/gorm" "net/url" "time" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" + "github.com/gorilla/mux" tb "gopkg.in/tucnak/telebot.v2" "net/http" + + "github.com/LightningTipBot/LightningTipBot/internal/i18n" ) const ( - invoiceReceivedMessage = "⚡️ You received %d sat." +// invoiceReceivedMessage = "⚡️ You received %d sat." ) type WebhookServer struct { @@ -73,7 +76,7 @@ func (w WebhookServer) receive(writer http.ResponseWriter, request *http.Request return } log.Infoln(fmt.Sprintf("[WebHook] User %s (%d) received invoice of %d sat.", user.Telegram.Username, user.Telegram.ID, depositEvent.Amount/1000)) - _, err = w.bot.Send(user.Telegram, fmt.Sprintf(invoiceReceivedMessage, depositEvent.Amount/1000)) + _, err = w.bot.Send(user.Telegram, fmt.Sprintf(i18n.Translate(user.Telegram.LanguageCode, "invoiceReceivedMessage"), depositEvent.Amount/1000)) if err != nil { log.Errorln(err) } diff --git a/pay.go b/pay.go index 3774fe70..283543e8 100644 --- a/pay.go +++ b/pay.go @@ -8,6 +8,7 @@ import ( log "github.com/sirupsen/logrus" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" decodepay "github.com/fiatjaf/ln-decodepay" @@ -256,10 +257,10 @@ func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { if err != nil { errmsg := fmt.Sprintf("[/pay] Could not pay invoice of %s: %s", userStr, err) if len(err.Error()) == 0 { - err = fmt.Errorf(bot.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) + err = fmt.Errorf(i18n.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) } // bot.trySendMessage(c.Sender, fmt.Sprintf(invoicePaymentFailedMessage, err)) - bot.tryEditMessage(c.Message, fmt.Sprintf(bot.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), MarkdownEscape(err.Error())), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), MarkdownEscape(err.Error())), &tb.ReplyMarkup{}) log.Errorln(errmsg) return } @@ -268,13 +269,13 @@ func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { if c.Message.Private() { // if the command was invoked in private chat - bot.tryEditMessage(c.Message, bot.Translate(payData.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) } else { // if the command was invoked in group chat - bot.trySendMessage(c.Sender, bot.Translate(payData.LanguageCode, "invoicePaidMessage")) - bot.tryEditMessage(c.Message, fmt.Sprintf(bot.Translate(payData.LanguageCode, "invoicePublicPaidMessage"), userStr), &tb.ReplyMarkup{}) + bot.trySendMessage(c.Sender, i18n.Translate(payData.LanguageCode, "invoicePaidMessage")) + bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePublicPaidMessage"), userStr), &tb.ReplyMarkup{}) } - log.Printf("[pay] User %s paid invoice %d (%d sat)", userStr, payData.ID, payData.Amount) + log.Printf("[pay] User %s paid invoice %s (%d sat)", userStr, payData.ID, payData.Amount) return } @@ -293,7 +294,7 @@ func (bot TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { if payData.From.Telegram.ID != c.Sender.ID { return } - bot.tryEditMessage(c.Message, bot.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) payData.InTransaction = false bot.InactivatePay(payData) } diff --git a/send.go b/send.go index d1245db0..10e80db6 100644 --- a/send.go +++ b/send.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" @@ -322,7 +323,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", bot.Translate(sendData.LanguageCode, "sendErrorMessage"), err), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, fmt.Sprintf("%s %s", i18n.Translate(sendData.LanguageCode, "sendErrorMessage"), err), &tb.ReplyMarkup{}) return } @@ -331,15 +332,15 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { sendData.InTransaction = false // notify to user - bot.trySendMessage(to.Telegram, fmt.Sprintf(bot.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) + bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) // bot.trySendMessage(from.Telegram, fmt.Sprintf(Translate(ctx, "sendSentMessage"), amount, toUserStrMd)) if c.Message.Private() { // if the command was invoked in private chat - bot.tryEditMessage(c.Message, fmt.Sprintf(bot.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd), &tb.ReplyMarkup{}) } else { // if the command was invoked in group chat - bot.trySendMessage(c.Sender, fmt.Sprintf(bot.Translate(from.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) - bot.tryEditMessage(c.Message, fmt.Sprintf(bot.Translate(sendData.LanguageCode, "sendPublicSentMessage"), amount, fromUserStrMd, toUserStrMd), &tb.ReplyMarkup{}) + bot.trySendMessage(c.Sender, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) + bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendPublicSentMessage"), amount, fromUserStrMd, toUserStrMd), &tb.ReplyMarkup{}) } // send memo if it was present if len(sendMemo) > 0 { @@ -364,7 +365,7 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { return } // remove buttons from confirmation message - bot.tryEditMessage(c.Message, bot.Translate(sendData.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, i18n.Translate(sendData.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) sendData.InTransaction = false bot.InactivateSend(sendData) } diff --git a/tip.go b/tip.go index e703b3f3..90e97333 100644 --- a/tip.go +++ b/tip.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" ) @@ -120,7 +121,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { log.Infof("[tip] Transaction sent from %s to %s (%d sat).", fromUserStr, toUserStr, amount) // notify users - _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(bot.Translate(from.Telegram.LanguageCode, "tipSentMessage"), amount, toUserStrMd)) + _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "tipSentMessage"), amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[/tip] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -131,7 +132,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if !messageHasTip { bot.tryForwardMessage(to.Telegram, m.ReplyTo, tb.Silent) } - bot.trySendMessage(to.Telegram, fmt.Sprintf(bot.Translate(to.Telegram.LanguageCode, "tipReceivedMessage"), fromUserStrMd, amount)) + bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "tipReceivedMessage"), fromUserStrMd, amount)) if len(tipMemo) > 0 { bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", MarkdownEscape(tipMemo))) diff --git a/translate.go b/translate.go index fe1a4db3..5e615e74 100644 --- a/translate.go +++ b/translate.go @@ -22,11 +22,3 @@ func TranslateUser(ctx context.Context, MessgeID string) string { } return str } - -func (bot *TipBot) Translate(languageCode string, MessgeID string) string { - str, err := i18n.NewLocalizer(bot.bundle, languageCode).Localize(&i18n.LocalizeConfig{MessageID: MessgeID}) - if err != nil { - log.Warnf("Error translating message %s: %s", MessgeID, err) - } - return str -} diff --git a/translations/de.toml b/translations/de.toml index 30633398..792b9633 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -170,6 +170,7 @@ sendHelpText = """📖 Ups, das hat nicht geklappt. %s # INVOICE +invoiceReceivedMessage = """⚡️ Du hast %d sat erhalten.""" invoiceEnterAmountMessage = """Hast du einen Betrag eingegeben?""" invoiceValidAmountMessage = """Hast du einen gültigen Betrag eingegeben?""" invoiceHelpText = """📖 Ups, das hat nicht geklappt. %s diff --git a/translations/en.toml b/translations/en.toml index ed0be8cf..b10063b1 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -170,6 +170,7 @@ sendHelpText = """📖 Oops, that didn't work. %s # INVOICE +invoiceReceivedMessage = """⚡️ You received %d sat.""" invoiceEnterAmountMessage = """Did you enter an amount?""" invoiceValidAmountMessage = """Did you enter a valid amount?""" invoiceHelpText = """📖 Oops, that didn't work. %s diff --git a/translations/es.toml b/translations/es.toml index 8f0b6192..f2222845 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -170,6 +170,7 @@ sendHelpText = """📖 Oops, eso no funcionó. %s # INVOICE +invoiceReceivedMessage = """⚡️ Recibiste %d sat""" invoiceEnterAmountMessage = """¿Ingresaste un monto?""" invoiceValidAmountMessage = """¿Ingresaste un monto válido?""" invoiceHelpText = """📖 Oops, eso no funcionó. %s diff --git a/translations/it.toml b/translations/it.toml index 0d88ed0b..7c144dd4 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -170,6 +170,7 @@ sendHelpText = """📖 Ops, non ha funzionato. %s # INVOICE +invoiceReceivedMessage = """⚡️ Hai ricevuto %d sat.""" invoiceEnterAmountMessage = """Hai inserito un ammontare?""" invoiceValidAmountMessage = """Hai inserito un ammontare valido?""" invoiceHelpText = """📖 Ops, non ha funzionato. %s diff --git a/translations/nl.toml b/translations/nl.toml index 4fc8d75d..47f94435 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -170,6 +170,7 @@ sendHelpText = """📖 Oeps, dat werkte niet. %s # INVOICE +invoiceReceivedMessage = """⚡️ Je hebt %d sat ontvangen .""" invoiceEnterAmountMessage = """Heb je een bedrag ingevoerd?""" invoiceValidAmountMessage = """Heeft u een geldig bedrag ingevoerd?""" invoiceHelpText = """📖 Oeps, dat werkte niet. %s From 903cc3c2e291f684b70636e69ac017306ed51f66 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 7 Oct 2021 00:21:21 +0300 Subject: [PATCH 014/541] Lnurlp comments (#89) * lnurl comments for RECEIVING * fix import cycle * fix import * rename WebhookServer to Server * rename LNURLInvoice to Invoice * add str package. MarkdownEscape lnurl.Invoice comment * add str package * check for comment length * lnurl pay with comment * lnurl pay with comment * check for wallet existence * repackage main to internal Co-authored-by: lngohumble --- config.go => internal/config.go | 2 +- internal/lnbits/types.go | 16 --- internal/lnbits/webhook.go | 84 ------------ internal/lnbits/webhook/webhook.go | 123 ++++++++++++++++++ internal/lnurl/lnurl.go | 65 ++++++++- internal/lnurl/server.go | 35 ++--- internal/str/strings.go | 27 ++++ amounts.go => internal/telegram/amounts.go | 2 +- balance.go => internal/telegram/balance.go | 4 +- bot.go => internal/telegram/bot.go | 36 +++-- database.go => internal/telegram/database.go | 23 ++-- donate.go => internal/telegram/donate.go | 13 +- handler.go => internal/telegram/handler.go | 16 +-- help.go => internal/telegram/help.go | 10 +- helpers.go => internal/telegram/helpers.go | 2 +- .../telegram/inline_faucet.go | 40 +++--- .../telegram/inline_query.go | 12 +- .../telegram/inline_receive.go | 31 +++-- .../telegram/inline_send.go | 30 ++--- .../telegram/interceptor.go | 4 +- invoice.go => internal/telegram/invoice.go | 9 +- link.go => internal/telegram/link.go | 9 +- lnurl.go => internal/telegram/lnurl.go | 60 +++++++-- message.go => internal/telegram/message.go | 2 +- pay.go => internal/telegram/pay.go | 29 +++-- photo.go => internal/telegram/photo.go | 6 +- send.go => internal/telegram/send.go | 33 ++--- start.go => internal/telegram/start.go | 11 +- telegram.go => internal/telegram/telegram.go | 12 +- text.go => internal/telegram/text.go | 4 +- tip.go => internal/telegram/tip.go | 20 +-- tooltip.go => internal/telegram/tooltip.go | 22 ++-- .../telegram/tooltip_test.go | 2 +- .../telegram/transaction.go | 6 +- .../telegram/translate.go | 2 +- users.go => internal/telegram/users.go | 34 +---- main.go | 7 +- 37 files changed, 491 insertions(+), 352 deletions(-) rename config.go => internal/config.go (99%) delete mode 100644 internal/lnbits/webhook.go create mode 100644 internal/lnbits/webhook/webhook.go create mode 100644 internal/str/strings.go rename amounts.go => internal/telegram/amounts.go (98%) rename balance.go => internal/telegram/balance.go (93%) rename bot.go => internal/telegram/bot.go (59%) rename database.go => internal/telegram/database.go (72%) rename donate.go => internal/telegram/donate.go (91%) rename handler.go => internal/telegram/handler.go (95%) rename help.go => internal/telegram/help.go (90%) rename helpers.go => internal/telegram/helpers.go (98%) rename inline_faucet.go => internal/telegram/inline_faucet.go (94%) rename inline_query.go => internal/telegram/inline_query.go (94%) rename inline_receive.go => internal/telegram/inline_receive.go (93%) rename inline_send.go => internal/telegram/inline_send.go (93%) rename interceptor.go => internal/telegram/interceptor.go (98%) rename invoice.go => internal/telegram/invoice.go (92%) rename link.go => internal/telegram/link.go (80%) rename lnurl.go => internal/telegram/lnurl.go (81%) rename message.go => internal/telegram/message.go (98%) rename pay.go => internal/telegram/pay.go (92%) rename photo.go => internal/telegram/photo.go (94%) rename send.go => internal/telegram/send.go (93%) rename start.go => internal/telegram/start.go (91%) rename telegram.go => internal/telegram/telegram.go (76%) rename text.go => internal/telegram/text.go (91%) rename tip.go => internal/telegram/tip.go (85%) rename tooltip.go => internal/telegram/tooltip.go (91%) rename tooltip_test.go => internal/telegram/tooltip_test.go (99%) rename transaction.go => internal/telegram/transaction.go (98%) rename translate.go => internal/telegram/translate.go (97%) rename users.go => internal/telegram/users.go (79%) diff --git a/config.go b/internal/config.go similarity index 99% rename from config.go rename to internal/config.go index 5f2ca0a3..ff814ccb 100644 --- a/config.go +++ b/internal/config.go @@ -1,4 +1,4 @@ -package main +package internal import ( "fmt" diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index b6968a09..53133e84 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -85,19 +85,3 @@ type BitInvoice struct { PaymentHash string `json:"payment_hash"` PaymentRequest string `json:"payment_request"` } -type Webhook struct { - CheckingID string `json:"checking_id"` - Pending int `json:"pending"` - Amount int `json:"amount"` - Fee int `json:"fee"` - Memo string `json:"memo"` - Time int `json:"time"` - Bolt11 string `json:"bolt11"` - Preimage string `json:"preimage"` - PaymentHash string `json:"payment_hash"` - Extra struct { - } `json:"extra"` - WalletID string `json:"wallet_id"` - Webhook string `json:"webhook"` - WebhookStatus interface{} `json:"webhook_status"` -} diff --git a/internal/lnbits/webhook.go b/internal/lnbits/webhook.go deleted file mode 100644 index 7615fbee..00000000 --- a/internal/lnbits/webhook.go +++ /dev/null @@ -1,84 +0,0 @@ -package lnbits - -import ( - "encoding/json" - "fmt" - "net/url" - "time" - - log "github.com/sirupsen/logrus" - "gorm.io/gorm" - - "github.com/gorilla/mux" - tb "gopkg.in/tucnak/telebot.v2" - - "net/http" - - "github.com/LightningTipBot/LightningTipBot/internal/i18n" -) - -const ( -// invoiceReceivedMessage = "⚡️ You received %d sat." -) - -type WebhookServer struct { - httpServer *http.Server - bot *tb.Bot - c *Client - database *gorm.DB -} - -func NewWebhookServer(addr *url.URL, bot *tb.Bot, client *Client, database *gorm.DB) *WebhookServer { - srv := &http.Server{ - Addr: addr.Host, - // Good practice: enforce timeouts for servers you create! - WriteTimeout: 15 * time.Second, - ReadTimeout: 15 * time.Second, - } - apiServer := &WebhookServer{ - c: client, - database: database, - bot: bot, - httpServer: srv, - } - apiServer.httpServer.Handler = apiServer.newRouter() - go apiServer.httpServer.ListenAndServe() - log.Infof("[Webhook] Server started at %s", addr) - return apiServer -} - -func (w *WebhookServer) GetUserByWalletId(walletId string) (*User, error) { - user := &User{} - tx := w.database.Where("wallet_id = ?", walletId).First(user) - if tx.Error != nil { - return user, tx.Error - } - return user, nil -} - -func (w *WebhookServer) newRouter() *mux.Router { - router := mux.NewRouter() - router.HandleFunc("/", w.receive).Methods(http.MethodPost) - return router -} - -func (w WebhookServer) receive(writer http.ResponseWriter, request *http.Request) { - depositEvent := Webhook{} - request.Header.Del("content-length") - err := json.NewDecoder(request.Body).Decode(&depositEvent) - if err != nil { - writer.WriteHeader(400) - return - } - user, err := w.GetUserByWalletId(depositEvent.WalletID) - if err != nil { - writer.WriteHeader(400) - return - } - log.Infoln(fmt.Sprintf("[WebHook] User %s (%d) received invoice of %d sat.", user.Telegram.Username, user.Telegram.ID, depositEvent.Amount/1000)) - _, err = w.bot.Send(user.Telegram, fmt.Sprintf(i18n.Translate(user.Telegram.LanguageCode, "invoiceReceivedMessage"), depositEvent.Amount/1000)) - if err != nil { - log.Errorln(err) - } - writer.WriteHeader(200) -} diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go new file mode 100644 index 00000000..1749fa77 --- /dev/null +++ b/internal/lnbits/webhook/webhook.go @@ -0,0 +1,123 @@ +package webhook + +import ( + "encoding/json" + "fmt" + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/str" + "github.com/LightningTipBot/LightningTipBot/internal/telegram" + "time" + + log "github.com/sirupsen/logrus" + "gorm.io/gorm" + + "github.com/LightningTipBot/LightningTipBot/internal/lnurl" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "net/http" + + "github.com/gorilla/mux" + tb "gopkg.in/tucnak/telebot.v2" + + "github.com/LightningTipBot/LightningTipBot/internal/i18n" +) + +const ( +// invoiceReceivedMessage = "⚡️ You received %d sat." +) + +type Server struct { + httpServer *http.Server + bot *tb.Bot + c *lnbits.Client + database *gorm.DB + buntdb *storage.DB +} + +type Webhook struct { + CheckingID string `json:"checking_id"` + Pending int `json:"pending"` + Amount int `json:"amount"` + Fee int `json:"fee"` + Memo string `json:"memo"` + Time int `json:"time"` + Bolt11 string `json:"bolt11"` + Preimage string `json:"preimage"` + PaymentHash string `json:"payment_hash"` + Extra struct { + } `json:"extra"` + WalletID string `json:"wallet_id"` + Webhook string `json:"webhook"` + WebhookStatus interface{} `json:"webhook_status"` +} + +func NewServer(bot *telegram.TipBot) *Server { + srv := &http.Server{ + Addr: internal.Configuration.Lnbits.WebhookServerUrl.Host, + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + apiServer := &Server{ + c: bot.Client, + database: bot.Database, + bot: bot.Telegram, + httpServer: srv, + buntdb: bot.Bunt, + } + apiServer.httpServer.Handler = apiServer.newRouter() + go apiServer.httpServer.ListenAndServe() + log.Infof("[Webhook] Server started at %s", internal.Configuration.Lnbits.WebhookServerUrl) + return apiServer +} + +func (w *Server) GetUserByWalletId(walletId string) (*lnbits.User, error) { + user := &lnbits.User{} + tx := w.database.Where("wallet_id = ?", walletId).First(user) + if tx.Error != nil { + return user, tx.Error + } + return user, nil +} + +func (w *Server) newRouter() *mux.Router { + router := mux.NewRouter() + router.HandleFunc("/", w.receive).Methods(http.MethodPost) + return router +} + +func (w Server) receive(writer http.ResponseWriter, request *http.Request) { + depositEvent := Webhook{} + // need to delete the header otherwise the Decode will fail + request.Header.Del("content-length") + err := json.NewDecoder(request.Body).Decode(&depositEvent) + if err != nil { + writer.WriteHeader(400) + return + } + user, err := w.GetUserByWalletId(depositEvent.WalletID) + if err != nil { + writer.WriteHeader(400) + return + } + log.Infoln(fmt.Sprintf("[WebHook] User %s (%d) received invoice of %d sat.", user.Telegram.Username, user.Telegram.ID, depositEvent.Amount/1000)) + _, err = w.bot.Send(user.Telegram, fmt.Sprintf(i18n.Translate(user.Telegram.LanguageCode, "invoiceReceivedMessage"), depositEvent.Amount/1000)) + if err != nil { + log.Errorln(err) + } + + // if this invoice is saved in bunt.db, we load it and display the comment from an LNURL invoice + tx := &lnurl.Invoice{PaymentHash: depositEvent.PaymentHash} + err = w.buntdb.Get(tx) + if err != nil { + log.Errorln(err) + } else { + if len(tx.Comment) > 0 { + _, err = w.bot.Send(user.Telegram, fmt.Sprintf(`✉️ %s`, str.MarkdownEscape(tx.Comment))) + if err != nil { + log.Errorln(err) + } + } + } + writer.WriteHeader(200) +} diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 7318e4f4..dbcf0b14 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -9,13 +9,30 @@ import ( "net/url" "strconv" "strings" + "time" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/fiatjaf/go-lnurl" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" ) +type Invoice struct { + PaymentRequest string `json:"payment_request"` + PaymentHash string `json:"payment_hash"` + Amount int64 `json:"amount"` + Comment string `json:"comment"` + ToUser *lnbits.User `json:"to_user"` + CreatedAt time.Time `json:"created_at"` + Paid bool `json:"paid"` + PaidAt time.Time `json:"paid_at"` +} + +func (msg Invoice) Key() string { + return fmt.Sprintf("payment-hash:%s", msg.PaymentHash) +} + func (w Server) handleLnUrl(writer http.ResponseWriter, request *http.Request) { var err error var response interface{} @@ -33,7 +50,12 @@ func (w Server) handleLnUrl(writer http.ResponseWriter, request *http.Request) { NotFoundHandler(writer, fmt.Errorf("[serveLNURLpSecond] Couldn't cast amount to int %v", parseError)) return } - response, err = w.serveLNURLpSecond(username, int64(amount)) + comment := request.FormValue("comment") + if len(comment) > CommentAllowed { + NotFoundHandler(writer, fmt.Errorf("[serveLNURLpSecond] Comment is too long")) + return + } + response, err = w.serveLNURLpSecond(username, int64(amount), comment) } // check if error was returned from first or second handlers if err != nil { @@ -77,12 +99,13 @@ func (w Server) serveLNURLpFirst(username string) (*lnurl.LNURLPayResponse1, err MinSendable: minSendable, MaxSendable: MaxSendable, EncodedMetadata: string(jsonMeta), + CommentAllowed: CommentAllowed, }, nil } // serveLNURLpSecond serves the second LNURL response with the payment request with the correct description hash -func (w Server) serveLNURLpSecond(username string, amount int64) (*lnurl.LNURLPayResponse2, error) { +func (w Server) serveLNURLpSecond(username string, amount int64, comment string) (*lnurl.LNURLPayResponse2, error) { log.Infof("[LNURL] Serving invoice for user %s", username) if amount < minSendable || amount > MaxSendable { // amount is not ok @@ -92,16 +115,33 @@ func (w Server) serveLNURLpSecond(username string, amount int64) (*lnurl.LNURLPa Reason: fmt.Sprintf("Amount out of bounds (min: %d mSat, max: %d mSat).", minSendable, MaxSendable)}, }, fmt.Errorf("amount out of bounds") } - // amount is ok now check for the user + // check comment length + if len(comment) > CommentAllowed { + return &lnurl.LNURLPayResponse2{ + LNURLResponse: lnurl.LNURLResponse{ + Status: statusError, + Reason: fmt.Sprintf("Comment too long (max: %d characters).", CommentAllowed)}, + }, fmt.Errorf("comment too long") + } + + // now check for the user user := &lnbits.User{} tx := w.database.Where("telegram_username = ?", strings.ToLower(username)).First(user) if tx.Error != nil { - return nil, fmt.Errorf("[GetUser] Couldn't fetch user info from database: %v", tx.Error) + return &lnurl.LNURLPayResponse2{ + LNURLResponse: lnurl.LNURLResponse{ + Status: statusError, + Reason: fmt.Sprintf("Invalid user.")}, + }, fmt.Errorf("[GetUser] Couldn't fetch user info from database: %v", tx.Error) } - if user.Wallet == nil || user.Initialized == false { - return nil, fmt.Errorf("[serveLNURLpSecond] invalid user data") + if user.Wallet == nil { + return &lnurl.LNURLPayResponse2{ + LNURLResponse: lnurl.LNURLResponse{ + Status: statusError, + Reason: fmt.Sprintf("Invalid user.")}, + }, fmt.Errorf("[serveLNURLpSecond] user %s not found", username) } - + // user is ok now create invoice // set wallet lnbits client var resp *lnurl.LNURLPayResponse2 @@ -128,6 +168,17 @@ func (w Server) serveLNURLpSecond(username string, amount int64) (*lnurl.LNURLPa } return resp, err } + // save invoice struct for later use + runtime.IgnoreError(w.buntdb.Set( + Invoice{ + ToUser: user, + Amount: amount, + Comment: comment, + PaymentRequest: invoice.PaymentRequest, + PaymentHash: invoice.PaymentHash, + CreatedAt: time.Now(), + })) + return &lnurl.LNURLPayResponse2{ LNURLResponse: lnurl.LNURLResponse{Status: statusOk}, PR: invoice.PaymentRequest, diff --git a/internal/lnurl/server.go b/internal/lnurl/server.go index 76f78069..0bb0259d 100644 --- a/internal/lnurl/server.go +++ b/internal/lnurl/server.go @@ -2,54 +2,59 @@ package lnurl import ( "encoding/json" + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/telegram" "net/http" "net/url" "time" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" "gorm.io/gorm" ) type Server struct { httpServer *http.Server - bot *tb.Bot + bot *telegram.TipBot c *lnbits.Client database *gorm.DB callbackHostname *url.URL + buntdb *storage.DB WebhookServer string } const ( - statusError = "ERROR" - statusOk = "OK" - payRequestTag = "payRequest" - lnurlEndpoint = ".well-known/lnurlp" - minSendable = 1000 // mSat - MaxSendable = 1000000000 + statusError = "ERROR" + statusOk = "OK" + payRequestTag = "payRequest" + lnurlEndpoint = ".well-known/lnurlp" + minSendable = 1000 // mSat + MaxSendable = 1_000_000_000 + CommentAllowed = 256 ) -func NewServer(addr, callbackHostname *url.URL, webhookServer string, bot *tb.Bot, client *lnbits.Client, database *gorm.DB) *Server { +func NewServer(bot *telegram.TipBot) *Server { srv := &http.Server{ - Addr: addr.Host, + Addr: internal.Configuration.Bot.LNURLServerUrl.Host, // Good practice: enforce timeouts for servers you create! WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } apiServer := &Server{ - c: client, - database: database, + c: bot.Client, + database: bot.Database, bot: bot, httpServer: srv, - callbackHostname: callbackHostname, - WebhookServer: webhookServer, + callbackHostname: internal.Configuration.Bot.LNURLHostUrl, + WebhookServer: internal.Configuration.Lnbits.WebhookServer, + buntdb: bot.Bunt, } apiServer.httpServer.Handler = apiServer.newRouter() go apiServer.httpServer.ListenAndServe() - log.Infof("[LNURL] Server started at %s", addr.Host) + log.Infof("[LNURL] Server started at %s", internal.Configuration.Bot.LNURLServerUrl.Host) return apiServer } diff --git a/internal/str/strings.go b/internal/str/strings.go new file mode 100644 index 00000000..07c8fde8 --- /dev/null +++ b/internal/str/strings.go @@ -0,0 +1,27 @@ +package str + +import ( + "fmt" + "strings" +) + +var markdownV2Escapes = []string{"_", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"} +var markdownEscapes = []string{"_", "*", "`", "["} + +func MarkdownV2Escape(s string) string { + for _, esc := range markdownV2Escapes { + if strings.Contains(s, esc) { + s = strings.Replace(s, esc, fmt.Sprintf("\\%s", esc), -1) + } + } + return s +} + +func MarkdownEscape(s string) string { + for _, esc := range markdownEscapes { + if strings.Contains(s, esc) { + s = strings.Replace(s, esc, fmt.Sprintf("\\%s", esc), -1) + } + } + return s +} diff --git a/amounts.go b/internal/telegram/amounts.go similarity index 98% rename from amounts.go rename to internal/telegram/amounts.go index 34e81852..33faa03c 100644 --- a/amounts.go +++ b/internal/telegram/amounts.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "errors" diff --git a/balance.go b/internal/telegram/balance.go similarity index 93% rename from balance.go rename to internal/telegram/balance.go index 22fbdb2d..851411fc 100644 --- a/balance.go +++ b/internal/telegram/balance.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" @@ -15,7 +15,7 @@ func (bot TipBot) balanceHandler(ctx context.Context, m *tb.Message) { // reply only in private message if m.Chat.Type != tb.ChatPrivate { // delete message - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) } // first check whether the user is initialized user := LoadUser(ctx) diff --git a/bot.go b/internal/telegram/bot.go similarity index 59% rename from bot.go rename to internal/telegram/bot.go index 7b4b7f95..c43520be 100644 --- a/bot.go +++ b/internal/telegram/bot.go @@ -1,12 +1,12 @@ -package main +package telegram import ( "fmt" + "github.com/LightningTipBot/LightningTipBot/internal" "sync" "time" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/storage" log "github.com/sirupsen/logrus" "gopkg.in/tucnak/telebot.v2" @@ -15,11 +15,11 @@ import ( ) type TipBot struct { - database *gorm.DB - bunt *storage.DB + Database *gorm.DB + Bunt *storage.DB logger *gorm.DB - telegram *telebot.Bot - client *lnbits.Client + Telegram *telebot.Bot + Client *lnbits.Client } var ( @@ -31,16 +31,18 @@ var ( func NewBot() TipBot { db, txLogger := migration() return TipBot{ - database: db, + Database: db, + Client: lnbits.NewClient(internal.Configuration.Lnbits.AdminKey, internal.Configuration.Lnbits.Url), logger: txLogger, - bunt: storage.NewBunt(Configuration.Database.BuntDbPath), + Bunt: storage.NewBunt(internal.Configuration.Database.BuntDbPath), + Telegram: newTelegramBot(), } } -// newTelegramBot will create a new telegram bot. +// newTelegramBot will create a new Telegram bot. func newTelegramBot() *tb.Bot { tgb, err := tb.NewBot(tb.Settings{ - Token: Configuration.Telegram.ApiKey, + Token: internal.Configuration.Telegram.ApiKey, Poller: &tb.LongPoller{Timeout: 60 * time.Second}, ParseMode: tb.ModeMarkdown, }) @@ -54,7 +56,7 @@ func newTelegramBot() *tb.Bot { // todo -- may want to derive user wallets from this specific bot wallet (master wallet), since lnbits usermanager extension is able to do that. func (bot TipBot) initBotWallet() error { botWalletInitialisation.Do(func() { - _, err := bot.initWallet(bot.telegram.Me) + _, err := bot.initWallet(bot.Telegram.Me) if err != nil { log.Errorln(fmt.Sprintf("[initBotWallet] Could not initialize bot wallet: %s", err.Error())) return @@ -63,20 +65,14 @@ func (bot TipBot) initBotWallet() error { return nil } -// Start will initialize the telegram bot and lnbits. +// Start will initialize the Telegram bot and lnbits. func (bot TipBot) Start() { - // set up lnbits api - bot.client = lnbits.NewClient(Configuration.Lnbits.AdminKey, Configuration.Lnbits.Url) - // set up telebot - bot.telegram = newTelegramBot() - log.Infof("[Telegram] Authorized on account @%s", bot.telegram.Me.Username) + log.Infof("[Telegram] Authorized on account @%s", bot.Telegram.Me.Username) // initialize the bot wallet err := bot.initBotWallet() if err != nil { log.Errorf("Could not initialize bot wallet: %s", err.Error()) } bot.registerTelegramHandlers() - lnbits.NewWebhookServer(Configuration.Lnbits.WebhookServerUrl, bot.telegram, bot.client, bot.database) - lnurl.NewServer(Configuration.Bot.LNURLServerUrl, Configuration.Bot.LNURLHostUrl, Configuration.Lnbits.WebhookServer, bot.telegram, bot.client, bot.database) - bot.telegram.Start() + bot.Telegram.Start() } diff --git a/database.go b/internal/telegram/database.go similarity index 72% rename from database.go rename to internal/telegram/database.go index be393534..21f719b9 100644 --- a/database.go +++ b/internal/telegram/database.go @@ -1,7 +1,8 @@ -package main +package telegram import ( "fmt" + "github.com/LightningTipBot/LightningTipBot/internal" "reflect" "strconv" "strings" @@ -15,12 +16,12 @@ import ( ) func migration() (db *gorm.DB, txLogger *gorm.DB) { - txLogger, err := gorm.Open(sqlite.Open(Configuration.Database.TransactionsPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) + txLogger, err := gorm.Open(sqlite.Open(internal.Configuration.Database.TransactionsPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) if err != nil { panic("Initialize orm failed.") } - orm, err := gorm.Open(sqlite.Open(Configuration.Database.DbPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) + orm, err := gorm.Open(sqlite.Open(internal.Configuration.Database.DbPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) if err != nil { panic("Initialize orm failed.") } @@ -38,7 +39,7 @@ func migration() (db *gorm.DB, txLogger *gorm.DB) { func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.User, error) { toUserDb := &lnbits.User{} - tx := bot.database.Where("telegram_username = ?", strings.ToLower(toUserStrWithoutAt)).First(toUserDb) + tx := bot.Database.Where("telegram_username = ?", strings.ToLower(toUserStrWithoutAt)).First(toUserDb) if tx.Error != nil || toUserDb.Wallet == nil { err := tx.Error if toUserDb.Wallet == nil { @@ -49,7 +50,7 @@ func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.U return toUserDb, nil } -// GetLnbitsUser will not update the user in database. +// GetLnbitsUser will not update the user in Database. // this is required, because fetching lnbits.User from a incomplete tb.User // will update the incomplete (partial) user in storage. // this function will accept users like this: @@ -57,9 +58,9 @@ func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.U // without updating the user in storage. func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { user := &lnbits.User{Name: strconv.Itoa(u.ID)} - tx := bot.database.First(user) + tx := bot.Database.First(user) if tx.Error != nil { - errmsg := fmt.Sprintf("[GetUser] Couldn't fetch %s from database: %s", GetUserStr(u), tx.Error.Error()) + errmsg := fmt.Sprintf("[GetUser] Couldn't fetch %s from Database: %s", GetUserStr(u), tx.Error.Error()) log.Warnln(errmsg) user.Telegram = u return user, tx.Error @@ -67,7 +68,7 @@ func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { return user, nil } -// GetUser from telegram user. Update the user if user information changed. +// GetUser from Telegram user. Update the user if user information changed. func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { user, err := GetLnbitsUser(u, bot) if err != nil { @@ -76,7 +77,7 @@ func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { go func() { userCopy := bot.copyLowercaseUser(u) if !reflect.DeepEqual(userCopy, user.Telegram) { - // update possibly changed user details in database + // update possibly changed user details in Database user.Telegram = userCopy err = UpdateUserRecord(user, bot) if err != nil { @@ -89,9 +90,9 @@ func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { func UpdateUserRecord(user *lnbits.User, bot TipBot) error { user.Telegram = bot.copyLowercaseUser(user.Telegram) - tx := bot.database.Save(user) + tx := bot.Database.Save(user) if tx.Error != nil { - errmsg := fmt.Sprintf("[UpdateUserRecord] Error: Couldn't update %s's info in database.", GetUserStr(user.Telegram)) + errmsg := fmt.Sprintf("[UpdateUserRecord] Error: Couldn't update %s's info in Database.", GetUserStr(user.Telegram)) log.Errorln(errmsg) return tx.Error } diff --git a/donate.go b/internal/telegram/donate.go similarity index 91% rename from donate.go rename to internal/telegram/donate.go index d0498e21..9f035d9f 100644 --- a/donate.go +++ b/internal/telegram/donate.go @@ -1,8 +1,9 @@ -package main +package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/str" "io" "io/ioutil" "net/http" @@ -50,7 +51,7 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { // command is valid msg := bot.trySendMessage(m.Sender, Translate(ctx, "donationProgressMessage")) // get invoice - resp, err := http.Get(fmt.Sprintf(donationEndpoint, amount, GetUserStr(m.Sender), GetUserStr(bot.telegram.Me))) + resp, err := http.Get(fmt.Sprintf(donationEndpoint, amount, GetUserStr(m.Sender), GetUserStr(bot.Telegram.Me))) if err != nil { log.Errorln(err) bot.tryEditMessage(msg, Translate(ctx, "donationErrorMessage")) @@ -66,7 +67,7 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { // send donation invoice user := LoadUser(ctx) // bot.trySendMessage(user.Telegram, string(body)) - _, err = user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: string(body)}, bot.client) + _, err = user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: string(body)}, bot.Client) if err != nil { userStr := GetUserStr(m.Sender) errmsg := fmt.Sprintf("[/donate] Donation failed for user %s: %s", userStr, err) @@ -116,13 +117,13 @@ func (bot TipBot) parseCmdDonHandler(ctx context.Context, m *tb.Message) error { arg := "" if strings.HasPrefix(strings.ToLower(m.Text), "/send") { arg, _ = getArgumentFromCommand(m.Text, 2) - if arg != "@"+bot.telegram.Me.Username { + if arg != "@"+bot.Telegram.Me.Username { return fmt.Errorf("err") } } if strings.HasPrefix(strings.ToLower(m.Text), "/tip") { arg = GetUserStr(m.ReplyTo.Sender) - if arg != "@"+bot.telegram.Me.Username { + if arg != "@"+bot.Telegram.Me.Username { return fmt.Errorf("err") } } @@ -142,7 +143,7 @@ func (bot TipBot) parseCmdDonHandler(ctx context.Context, m *tb.Message) error { } donationInterceptMessage := sb.String() - bot.trySendMessage(m.Sender, MarkdownEscape(donationInterceptMessage)) + bot.trySendMessage(m.Sender, str.MarkdownEscape(donationInterceptMessage)) m.Text = fmt.Sprintf("/donate %d", amount) bot.donationHandler(ctx, m) // returning nil here will abort the parent handler (/pay or /tip) diff --git a/handler.go b/internal/telegram/handler.go similarity index 95% rename from handler.go rename to internal/telegram/handler.go index 5e347f35..e53ffc10 100644 --- a/handler.go +++ b/internal/telegram/handler.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" @@ -15,7 +15,7 @@ type Handler struct { Interceptor *Interceptor } -// registerTelegramHandlers will register all telegram handlers. +// registerTelegramHandlers will register all Telegram handlers. func (bot TipBot) registerTelegramHandlers() { telegramHandlerRegistration.Do(func() { // Set up handlers @@ -54,27 +54,27 @@ func (bot TipBot) registerHandlerWithInterceptor(h Handler) { } } -// handle accepts an endpoint and handler for telegram handler registration. +// handle accepts an endpoint and handler for Telegram handler registration. // function will automatically register string handlers as uppercase and first letter uppercase. func (bot TipBot) handle(endpoint interface{}, handler interface{}) { // register the endpoint - bot.telegram.Handle(endpoint, handler) + bot.Telegram.Handle(endpoint, handler) switch endpoint.(type) { case string: // check if this is a string endpoint sEndpoint := endpoint.(string) if strings.HasPrefix(sEndpoint, "/") { // Uppercase endpoint registration, because starting with slash - bot.telegram.Handle(strings.ToUpper(sEndpoint), handler) + bot.Telegram.Handle(strings.ToUpper(sEndpoint), handler) if len(sEndpoint) > 2 { // Also register endpoint with first letter uppercase - bot.telegram.Handle(fmt.Sprintf("/%s%s", strings.ToUpper(string(sEndpoint[1])), sEndpoint[2:]), handler) + bot.Telegram.Handle(fmt.Sprintf("/%s%s", strings.ToUpper(string(sEndpoint[1])), sEndpoint[2:]), handler) } } } } -// register registers a handler, so that telegram can handle the endpoint correctly. +// register registers a handler, so that Telegram can handle the endpoint correctly. func (bot TipBot) register(h Handler) { if h.Interceptor != nil { bot.registerHandlerWithInterceptor(h) @@ -85,7 +85,7 @@ func (bot TipBot) register(h Handler) { } } -// getHandler returns a list of all handlers, that need to be registered with telegram +// getHandler returns a list of all handlers, that need to be registered with Telegram func (bot TipBot) getHandler() []Handler { return []Handler{ { diff --git a/help.go b/internal/telegram/help.go similarity index 90% rename from help.go rename to internal/telegram/help.go index 3b02246c..1477c6f9 100644 --- a/help.go +++ b/internal/telegram/help.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" @@ -30,7 +30,7 @@ func (bot TipBot) helpHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) if !m.Private() { // delete message - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) } bot.trySendMessage(m.Sender, bot.makeHelpMessage(ctx, m), tb.NoPreview) return @@ -41,7 +41,7 @@ func (bot TipBot) basicsHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) if !m.Private() { // delete message - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) } bot.trySendMessage(m.Sender, Translate(ctx, "basicsMessage"), tb.NoPreview) return @@ -72,7 +72,7 @@ func (bot TipBot) makeAdvancedHelpMessage(ctx context.Context, m *tb.Message) st } // this is so stupid: - return fmt.Sprintf(Translate(ctx, "advancedMessage"), dynamicHelpMessage, GetUserStr(bot.telegram.Me), GetUserStr(bot.telegram.Me), GetUserStr(bot.telegram.Me)) + return fmt.Sprintf(Translate(ctx, "advancedMessage"), dynamicHelpMessage, GetUserStr(bot.Telegram.Me), GetUserStr(bot.Telegram.Me), GetUserStr(bot.Telegram.Me)) } func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) { @@ -80,7 +80,7 @@ func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) if !m.Private() { // delete message - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) } bot.trySendMessage(m.Sender, bot.makeAdvancedHelpMessage(ctx, m), tb.NoPreview) return diff --git a/helpers.go b/internal/telegram/helpers.go similarity index 98% rename from helpers.go rename to internal/telegram/helpers.go index 9211b835..710ace61 100644 --- a/helpers.go +++ b/internal/telegram/helpers.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "math" diff --git a/inline_faucet.go b/internal/telegram/inline_faucet.go similarity index 94% rename from inline_faucet.go rename to internal/telegram/inline_faucet.go index 9d2fc350..fd9ee5d6 100644 --- a/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" @@ -56,7 +56,7 @@ func (msg InlineFaucet) Key() string { func (bot *TipBot) LockFaucet(tx *InlineFaucet) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = true - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -66,7 +66,7 @@ func (bot *TipBot) LockFaucet(tx *InlineFaucet) error { func (bot *TipBot) ReleaseFaucet(tx *InlineFaucet) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = false - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -75,7 +75,7 @@ func (bot *TipBot) ReleaseFaucet(tx *InlineFaucet) error { func (bot *TipBot) inactivateFaucet(tx *InlineFaucet) error { tx.Active = false - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -86,7 +86,7 @@ func (bot *TipBot) inactivateFaucet(tx *InlineFaucet) error { func (bot *TipBot) getInlineFaucet(c *tb.Callback) (*InlineFaucet, error) { inlineFaucet := NewInlineFaucet() inlineFaucet.ID = c.Data - err := bot.bunt.Get(inlineFaucet) + err := bot.Bunt.Get(inlineFaucet) // to avoid race conditions, we block the call if there is // already an active transaction by loop until InTransaction is false @@ -99,7 +99,7 @@ func (bot *TipBot) getInlineFaucet(c *tb.Callback) (*InlineFaucet, error) { default: log.Warnf("[getInlineFaucet] %s in transaction", inlineFaucet.ID) time.Sleep(time.Duration(500) * time.Millisecond) - err = bot.bunt.Get(inlineFaucet) + err = bot.Bunt.Get(inlineFaucet) } } if err != nil { @@ -194,7 +194,7 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { inlineFaucet.Memo = memo inlineFaucet.RemainingAmount = inlineFaucet.Amount inlineFaucet.LanguageCode = ctx.Value("publicLanguageCode").(string) - runtime.IgnoreError(bot.bunt.Set(inlineFaucet)) + runtime.IgnoreError(bot.Bunt.Set(inlineFaucet)) } @@ -203,27 +203,27 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { var err error inlineFaucet.Amount, err = decodeAmountFromCommand(q.Text) if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return } if inlineFaucet.Amount < 1 { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return } peruserStr, err := getArgumentFromCommand(q.Text, 2) if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return } inlineFaucet.PerUserAmount, err = getAmount(peruserStr) if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return } // peruser amount must be >1 and a divisor of amount if inlineFaucet.PerUserAmount < 1 || inlineFaucet.Amount%inlineFaucet.PerUserAmount != 0 { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineFaucetInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineFaucetInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return } inlineFaucet.NTotal = inlineFaucet.Amount / inlineFaucet.PerUserAmount @@ -238,7 +238,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { // check if fromUser has balance if balance < inlineFaucet.Amount { log.Errorf("Balance of user %s too low", fromUserStr) - bot.inlineQueryReplyWithError(q, fmt.Sprintf(TranslateUser(ctx, "inlineSendBalanceLowMessage"), balance), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, fmt.Sprintf(TranslateUser(ctx, "inlineSendBalanceLowMessage"), balance), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return } @@ -286,10 +286,10 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { inlineFaucet.RemainingAmount = inlineFaucet.Amount inlineFaucet.Memo = memo inlineFaucet.LanguageCode = ctx.Value("publicLanguageCode").(string) - runtime.IgnoreError(bot.bunt.Set(inlineFaucet)) + runtime.IgnoreError(bot.Bunt.Set(inlineFaucet)) } - err = bot.telegram.Answer(q, &tb.QueryResponse{ + err = bot.Telegram.Answer(q, &tb.QueryResponse{ Results: results, CacheTime: 1, }) @@ -372,8 +372,8 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.To = append(inlineFaucet.To, to.Telegram) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount - _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) - _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) + _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) + _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[faucet] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -387,7 +387,7 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.Message = inlineFaucet.Message + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetAppendMemo"), memo) } if inlineFaucet.UserNeedsWallet { - inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) + inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } // register new inline buttons @@ -410,7 +410,7 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback // faucet is depleted inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetEndedMessage"), inlineFaucet.Amount, inlineFaucet.NTaken) if inlineFaucet.UserNeedsWallet { - inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) + inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } bot.tryEditMessage(c.Message, inlineFaucet.Message) inlineFaucet.Active = false @@ -429,7 +429,7 @@ func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback // set the inlineFaucet inactive inlineFaucet.Active = false inlineFaucet.InTransaction = false - runtime.IgnoreError(bot.bunt.Set(inlineFaucet)) + runtime.IgnoreError(bot.Bunt.Set(inlineFaucet)) } return } diff --git a/inline_query.go b/internal/telegram/inline_query.go similarity index 94% rename from inline_query.go rename to internal/telegram/inline_query.go index 7b0d24a0..94cf5092 100644 --- a/inline_query.go +++ b/internal/telegram/inline_query.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" @@ -23,17 +23,17 @@ func (bot TipBot) inlineQueryInstructions(ctx context.Context, q *tb.Query) { { url: queryImage, title: TranslateUser(ctx, "inlineQuerySendTitle"), - description: fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.telegram.Me.Username), + description: fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username), }, { url: queryImage, title: TranslateUser(ctx, "inlineQueryReceiveTitle"), - description: fmt.Sprintf(TranslateUser(ctx, "inlineQueryReceiveDescription"), bot.telegram.Me.Username), + description: fmt.Sprintf(TranslateUser(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username), }, { url: queryImage, title: TranslateUser(ctx, "inlineQueryFaucetTitle"), - description: fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username), + description: fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username), }, } results := make(tb.Results, len(instructions)) // []tb.Result @@ -51,7 +51,7 @@ func (bot TipBot) inlineQueryInstructions(ctx context.Context, q *tb.Query) { results[i].SetResultID(strconv.Itoa(i)) } - err := bot.telegram.Answer(q, &tb.QueryResponse{ + err := bot.Telegram.Answer(q, &tb.QueryResponse{ Results: results, CacheTime: 5, // a minute IsPersonal: true, @@ -76,7 +76,7 @@ func (bot TipBot) inlineQueryReplyWithError(q *tb.Query, message string, help st id := fmt.Sprintf("inl-error-%d-%s", q.From.ID, RandStringRunes(5)) result.SetResultID(id) results[0] = result - err := bot.telegram.Answer(q, &tb.QueryResponse{ + err := bot.Telegram.Answer(q, &tb.QueryResponse{ Results: results, CacheTime: 1, // 60 == 1 minute, todo: make higher than 1 s in production diff --git a/inline_receive.go b/internal/telegram/inline_receive.go similarity index 93% rename from inline_receive.go rename to internal/telegram/inline_receive.go index 81bd00ae..5cdaae77 100644 --- a/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" @@ -48,7 +48,7 @@ func (msg InlineReceive) Key() string { func (bot *TipBot) LockReceive(tx *InlineReceive) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = true - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -58,7 +58,7 @@ func (bot *TipBot) LockReceive(tx *InlineReceive) error { func (bot *TipBot) ReleaseReceive(tx *InlineReceive) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = false - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -67,7 +67,7 @@ func (bot *TipBot) ReleaseReceive(tx *InlineReceive) error { func (bot *TipBot) inactivateReceive(tx *InlineReceive) error { tx.Active = false - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -78,7 +78,7 @@ func (bot *TipBot) inactivateReceive(tx *InlineReceive) error { func (bot *TipBot) getInlineReceive(c *tb.Callback) (*InlineReceive, error) { inlineReceive := NewInlineReceive() inlineReceive.ID = c.Data - err := bot.bunt.Get(inlineReceive) + err := bot.Bunt.Get(inlineReceive) // to avoid race conditions, we block the call if there is // already an active transaction by loop until InTransaction is false ticker := time.NewTicker(time.Second * 10) @@ -90,7 +90,7 @@ func (bot *TipBot) getInlineReceive(c *tb.Callback) (*InlineReceive, error) { default: log.Warnf("[getInlineReceive] %s in transaction", inlineReceive.ID) time.Sleep(time.Duration(500) * time.Millisecond) - err = bot.bunt.Get(inlineReceive) + err = bot.Bunt.Get(inlineReceive) } } if err != nil { @@ -106,11 +106,11 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { var err error inlineReceive.Amount, err = decodeAmountFromCommand(q.Text) if err != nil { - bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineQueryReceiveTitle"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineQueryReceiveTitle"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) return } if inlineReceive.Amount < 1 { - bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) return } @@ -164,10 +164,10 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { // add result to persistent struct inlineReceive.Message = inlineMessage inlineReceive.LanguageCode = ctx.Value("publicLanguageCode").(string) - runtime.IgnoreError(bot.bunt.Set(inlineReceive)) + runtime.IgnoreError(bot.Bunt.Set(inlineReceive)) } - err = bot.telegram.Answer(q, &tb.QueryResponse{ + err = bot.Telegram.Answer(q, &tb.QueryResponse{ Results: results, CacheTime: 1, // 60 == 1 minute, todo: make higher than 1 s in production @@ -201,6 +201,9 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac // user `from` is the one who is SENDING // user `to` is the one who is RECEIVING from := LoadUser(ctx) + if from.Wallet == nil { + return + } to := inlineReceive.To toUserStrMd := GetUserStrMd(to.Telegram) fromUserStrMd := GetUserStrMd(from.Telegram) @@ -249,13 +252,13 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac } if !to.Initialized { - inlineReceive.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) + inlineReceive.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } bot.tryEditMessage(c.Message, inlineReceive.Message, &tb.ReplyMarkup{}) // notify users - _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, inlineReceive.Amount)) - _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), inlineReceive.Amount, toUserStrMd)) + _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, inlineReceive.Amount)) + _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), inlineReceive.Amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[acceptInlineReceiveHandler] Error: Receive message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -274,7 +277,7 @@ func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callbac // set the inlineReceive inactive inlineReceive.Active = false inlineReceive.InTransaction = false - runtime.IgnoreError(bot.bunt.Set(inlineReceive)) + runtime.IgnoreError(bot.Bunt.Set(inlineReceive)) } return } diff --git a/inline_send.go b/internal/telegram/inline_send.go similarity index 93% rename from inline_send.go rename to internal/telegram/inline_send.go index 8b39638d..9247af31 100644 --- a/inline_send.go +++ b/internal/telegram/inline_send.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" @@ -48,7 +48,7 @@ func (msg InlineSend) Key() string { func (bot *TipBot) LockInlineSend(tx *InlineSend) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = true - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -58,7 +58,7 @@ func (bot *TipBot) LockInlineSend(tx *InlineSend) error { func (bot *TipBot) ReleaseInlineSend(tx *InlineSend) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = false - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -67,7 +67,7 @@ func (bot *TipBot) ReleaseInlineSend(tx *InlineSend) error { func (bot *TipBot) InactivateInlineSend(tx *InlineSend) error { tx.Active = false - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -78,7 +78,7 @@ func (bot *TipBot) getInlineSend(c *tb.Callback) (*InlineSend, error) { inlineSend := NewInlineSend() inlineSend.ID = c.Data - err := bot.bunt.Get(inlineSend) + err := bot.Bunt.Get(inlineSend) // to avoid race conditions, we block the call if there is // already an active transaction by loop until InTransaction is false @@ -91,7 +91,7 @@ func (bot *TipBot) getInlineSend(c *tb.Callback) (*InlineSend, error) { default: log.Warnf("[getInlineSend] %s in transaction", inlineSend.ID) time.Sleep(time.Duration(500) * time.Millisecond) - err = bot.bunt.Get(inlineSend) + err = bot.Bunt.Get(inlineSend) } } if err != nil { @@ -107,11 +107,11 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { var err error inlineSend.Amount, err = decodeAmountFromCommand(q.Text) if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQuerySendTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQuerySendTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) return } if inlineSend.Amount < 1 { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQuerySendDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) return } fromUser := LoadUser(ctx) @@ -125,7 +125,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { // check if fromUser has balance if balance < inlineSend.Amount { log.Errorf("Balance of user %s too low", fromUserStr) - bot.inlineQueryReplyWithError(q, fmt.Sprintf(TranslateUser(ctx, "inlineSendBalanceLowMessage"), balance), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, fmt.Sprintf(TranslateUser(ctx, "inlineSendBalanceLowMessage"), balance), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) return } @@ -176,10 +176,10 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { inlineSend.From = fromUser inlineSend.LanguageCode = ctx.Value("publicLanguageCode").(string) // add result to persistent struct - runtime.IgnoreError(bot.bunt.Set(inlineSend)) + runtime.IgnoreError(bot.Bunt.Set(inlineSend)) } - err = bot.telegram.Answer(q, &tb.QueryResponse{ + err = bot.Telegram.Answer(q, &tb.QueryResponse{ Results: results, CacheTime: 1, // 60 == 1 minute, todo: make higher than 1 s in production @@ -260,13 +260,13 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) } if !to.Initialized { - inlineSend.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineSend.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.telegram.Me)) + inlineSend.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineSend.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } bot.tryEditMessage(c.Message, inlineSend.Message, &tb.ReplyMarkup{}) // notify users - _, err = bot.telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) - _, err = bot.telegram.Send(fromUser.Telegram, fmt.Sprintf(i18n.Translate(fromUser.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) + _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) + _, err = bot.Telegram.Send(fromUser.Telegram, fmt.Sprintf(i18n.Translate(fromUser.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[sendInline] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -285,7 +285,7 @@ func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) // set the inlineSend inactive inlineSend.Active = false inlineSend.InTransaction = false - runtime.IgnoreError(bot.bunt.Set(inlineSend)) + runtime.IgnoreError(bot.Bunt.Set(inlineSend)) } return } diff --git a/interceptor.go b/internal/telegram/interceptor.go similarity index 98% rename from interceptor.go rename to internal/telegram/interceptor.go index 94b390f6..cbc56a89 100644 --- a/interceptor.go +++ b/internal/telegram/interceptor.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" @@ -51,7 +51,7 @@ func (bot TipBot) loadUserInterceptor(ctx context.Context, i interface{}) (conte return ctx, nil } -// loadReplyToInterceptor Loading the telegram user with message intercept +// loadReplyToInterceptor Loading the Telegram user with message intercept func (bot TipBot) loadReplyToInterceptor(ctx context.Context, i interface{}) (context.Context, error) { switch i.(type) { case *tb.Message: diff --git a/invoice.go b/internal/telegram/invoice.go similarity index 92% rename from invoice.go rename to internal/telegram/invoice.go index 26d259d1..6718312d 100644 --- a/invoice.go +++ b/internal/telegram/invoice.go @@ -1,9 +1,10 @@ -package main +package telegram import ( "bytes" "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal" "strings" log "github.com/sirupsen/logrus" @@ -26,7 +27,7 @@ func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) if m.Chat.Type != tb.ChatPrivate { // delete message - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) return } if len(strings.Split(m.Text, " ")) < 2 { @@ -69,8 +70,8 @@ func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { Out: false, Amount: int64(amount), Memo: memo, - Webhook: Configuration.Lnbits.WebhookServer}, - bot.client) + Webhook: internal.Configuration.Lnbits.WebhookServer}, + bot.Client) if err != nil { errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) log.Errorln(errmsg) diff --git a/link.go b/internal/telegram/link.go similarity index 80% rename from link.go rename to internal/telegram/link.go index 033780e2..7b4a6f51 100644 --- a/link.go +++ b/internal/telegram/link.go @@ -1,9 +1,10 @@ -package main +package telegram import ( "bytes" "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" @@ -11,7 +12,7 @@ import ( ) func (bot TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { - if Configuration.Lnbits.LnbitsPublicUrl == "" { + if internal.Configuration.Lnbits.LnbitsPublicUrl == "" { bot.trySendMessage(m.Sender, Translate(ctx, "couldNotLinkMessage")) return } @@ -20,13 +21,13 @@ func (bot TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { // reply only in private message if m.Chat.Type != tb.ChatPrivate { // delete message - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) } // first check whether the user is initialized fromUser := LoadUser(ctx) bot.trySendMessage(m.Sender, Translate(ctx, "walletConnectMessage")) - lndhubUrl := fmt.Sprintf("lndhub://admin:%s@%slndhub/ext/", fromUser.Wallet.Adminkey, Configuration.Lnbits.LnbitsPublicUrl) + lndhubUrl := fmt.Sprintf("lndhub://admin:%s@%slndhub/ext/", fromUser.Wallet.Adminkey, internal.Configuration.Lnbits.LnbitsPublicUrl) // create qr code qr, err := qrcode.Encode(lndhubUrl, qrcode.Medium, 256) diff --git a/lnurl.go b/internal/telegram/lnurl.go similarity index 81% rename from lnurl.go rename to internal/telegram/lnurl.go index d6976851..071d2379 100644 --- a/lnurl.go +++ b/internal/telegram/lnurl.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "bytes" @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal" "io/ioutil" "net/http" "net/url" @@ -74,12 +75,31 @@ func (bot TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { bot.tryDeleteMessage(msg) // Let the user enter an amount and return - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlEnterAmountMessage"), payParams.MinSendable/1000, payParams.MaxSendable/1000), tb.ForceReply) + bot.trySendMessage(m.Sender, + fmt.Sprintf(Translate(ctx, "lnurlEnterAmountMessage"), payParams.MinSendable/1000, payParams.MaxSendable/1000), + tb.ForceReply) } else { // amount is already present in the command + // amount not in allowed range from LNURL + // if int64(amount) > (payParams.MaxSendable/1000) || int64(amount) < (payParams.MinSendable/1000) { + // err = fmt.Errorf("amount not in range") + // log.Errorln(err) + // bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), payParams.MinSendable/1000, payParams.MaxSendable/1000)) + // ResetUserState(user, bot) + // return + // } // set also amount in the state of the user - // todo: this is repeated code, could be shorter payParams.Amount = amount + + // check if comment is presentin lnrul-p + memo := GetMemoFromCommand(m.Text, 3) + // shorten comment to allowed length + if len(memo) > int(payParams.CommentAllowed) { + memo = memo[:payParams.CommentAllowed] + } + // save it + payParams.Comment = memo + paramsJson, err := json.Marshal(payParams) if err != nil { log.Errorln(err) @@ -95,7 +115,7 @@ func (bot TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { func (bot *TipBot) UserGetLightningAddress(user *tb.User) (string, error) { if len(user.Username) > 0 { - return fmt.Sprintf("%s@%s", strings.ToLower(user.Username), strings.ToLower(Configuration.Bot.LNURLHostUrl.Hostname())), nil + return fmt.Sprintf("%s@%s", strings.ToLower(user.Username), strings.ToLower(internal.Configuration.Bot.LNURLHostUrl.Hostname())), nil } else { return "", fmt.Errorf("user has no username") } @@ -106,7 +126,7 @@ func UserGetLNURL(user *tb.User) (string, error) { if len(name) == 0 { return "", fmt.Errorf("user has no username.") } - callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", Configuration.Bot.LNURLHostName, name) + callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, name) log.Debugf("[lnurlReceiveHandler] %s's LNURL: %s", GetUserStr(user), callback) lnurlEncode, err := lnurl.LNURLEncode(callback) @@ -122,7 +142,7 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { if err != nil { errmsg := fmt.Sprintf("[lnurlReceiveHandler] Failed to get LNURL: %s", err) log.Errorln(errmsg) - bot.telegram.Send(m.Sender, Translate(ctx, "lnurlNoUsernameMessage")) + bot.Telegram.Send(m.Sender, Translate(ctx, "lnurlNoUsernameMessage")) } // create qr code qr, err := qrcode.Encode(lnurlEncode, qrcode.Medium, 256) @@ -137,6 +157,7 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lnurlEncode)}) } +// lnurlEnterAmountHandler is invoked if the user didn't deliver an amount for the lnurl payment func (bot TipBot) lnurlEnterAmountHandler(ctx context.Context, m *tb.Message) { user := LoadUser(ctx) if user.Wallet == nil { @@ -182,7 +203,8 @@ func (bot TipBot) lnurlEnterAmountHandler(ctx context.Context, m *tb.Message) { // LnurlStateResponse saves the state of the user for an LNURL payment type LnurlStateResponse struct { lnurl.LNURLPayResponse1 - Amount int `json:"amount"` + Amount int `json:"amount"` + Comment string `json:"comment"` } // lnurlPayHandler is invoked when the user has delivered an amount and is ready to pay @@ -218,7 +240,13 @@ func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { return } qs := callbackUrl.Query() + // add amount to query string qs.Set("amount", strconv.Itoa(stateResponse.Amount*1000)) + // add comment to query string + if len(stateResponse.Comment) > 0 { + qs.Set("comment", stateResponse.Comment) + } + callbackUrl.RawQuery = qs.Encode() res, err := client.Get(callbackUrl.String()) @@ -242,7 +270,7 @@ func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not receive invoice (wrong address?).")) return } - bot.telegram.Delete(msg) + bot.Telegram.Delete(msg) c.Text = fmt.Sprintf("/pay %s", response2.PR) bot.payHandler(ctx, c) } @@ -250,8 +278,8 @@ func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { func getHttpClient() (*http.Client, error) { client := http.Client{} - if Configuration.Bot.HttpProxy != "" { - proxyUrl, err := url.Parse(Configuration.Bot.HttpProxy) + if internal.Configuration.Bot.HttpProxy != "" { + proxyUrl, err := url.Parse(internal.Configuration.Bot.HttpProxy) if err != nil { log.Errorln(err) return nil, err @@ -365,8 +393,20 @@ func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, ad } if amount > 0 { + // only when amount is given, we will also add a comment to the command + // we do this because if the amount is not given, we will have to ask for it later + // in the lnurl handler and we don't want to add another step where we ask for a comment + // the command to pay to lnurl with comment is /lnurl + // check if comment is presentin lnrul-p + memo := GetMemoFromCommand(m.Text, 3) m.Text = fmt.Sprintf("/lnurl %d %s", amount, lnurl) + // shorten comment to allowed length + if len(memo) > 0 { + m.Text = m.Text + " " + memo + } } else { + // no amount was given so we will just send the lnurl + // this will invoke the "enter amount" dialog in the lnurl handler m.Text = fmt.Sprintf("/lnurl %s", lnurl) } bot.lnurlHandler(ctx, m) diff --git a/message.go b/internal/telegram/message.go similarity index 98% rename from message.go rename to internal/telegram/message.go index e27395aa..7cca211b 100644 --- a/message.go +++ b/internal/telegram/message.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "strconv" diff --git a/pay.go b/internal/telegram/pay.go similarity index 92% rename from pay.go rename to internal/telegram/pay.go index 283543e8..ef3d4727 100644 --- a/pay.go +++ b/internal/telegram/pay.go @@ -1,8 +1,9 @@ -package main +package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/str" "strings" "time" @@ -58,7 +59,7 @@ func (msg PayData) Key() string { func (bot *TipBot) LockPay(tx *PayData) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = true - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -68,7 +69,7 @@ func (bot *TipBot) LockPay(tx *PayData) error { func (bot *TipBot) ReleasePay(tx *PayData) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = false - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -77,7 +78,7 @@ func (bot *TipBot) ReleasePay(tx *PayData) error { func (bot *TipBot) InactivatePay(tx *PayData) error { tx.Active = false - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -88,7 +89,7 @@ func (bot *TipBot) getPay(c *tb.Callback) (*PayData, error) { payData := NewPay() payData.ID = c.Data - err := bot.bunt.Get(payData) + err := bot.Bunt.Get(payData) // to avoid race conditions, we block the call if there is // already an active transaction by loop until InTransaction is false @@ -101,7 +102,7 @@ func (bot *TipBot) getPay(c *tb.Callback) (*PayData, error) { default: log.Infoln("[pay] in transaction") time.Sleep(time.Duration(500) * time.Millisecond) - err = bot.bunt.Get(payData) + err = bot.Bunt.Get(payData) } } if err != nil { @@ -121,14 +122,14 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { return } if len(strings.Split(m.Text, " ")) < 2 { - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, "")) return } userStr := GetUserStr(m.Sender) paymentRequest, err := getArgumentFromCommand(m.Text, 1) if err != nil { - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) errmsg := fmt.Sprintf("[/pay] Error: Could not getArgumentFromCommand: %s", err) log.Errorln(errmsg) @@ -158,13 +159,13 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { // check user balance first balance, err := bot.GetUserBalance(user) if err != nil { - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) errmsg := fmt.Sprintf("[/pay] Error: Could not get user balance: %s", err) log.Errorln(errmsg) return } if amount > balance { - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "insufficientFundsMessage"), balance, amount)) return } @@ -175,7 +176,7 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { confirmText := fmt.Sprintf(Translate(ctx, "confirmPayInvoiceMessage"), amount) if len(bolt11.Description) > 0 { - confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmPayAppendMemo"), MarkdownEscape(bolt11.Description)) + confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmPayAppendMemo"), str.MarkdownEscape(bolt11.Description)) } log.Printf("[/pay] User: %s, amount: %d sat.", userStr, amount) @@ -194,7 +195,7 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { LanguageCode: ctx.Value("publicLanguageCode").(string), } // add result to persistent struct - runtime.IgnoreError(bot.bunt.Set(payData)) + runtime.IgnoreError(bot.Bunt.Set(payData)) SetUserState(user, bot, lnbits.UserStateConfirmPayment, paymentRequest) @@ -253,14 +254,14 @@ func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { userStr := GetUserStr(c.Sender) // pay invoice - invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoiceString}, bot.client) + invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoiceString}, bot.Client) if err != nil { errmsg := fmt.Sprintf("[/pay] Could not pay invoice of %s: %s", userStr, err) if len(err.Error()) == 0 { err = fmt.Errorf(i18n.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) } // bot.trySendMessage(c.Sender, fmt.Sprintf(invoicePaymentFailedMessage, err)) - bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), MarkdownEscape(err.Error())), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), str.MarkdownEscape(err.Error())), &tb.ReplyMarkup{}) log.Errorln(errmsg) return } diff --git a/photo.go b/internal/telegram/photo.go similarity index 94% rename from photo.go rename to internal/telegram/photo.go index 67cb6298..779e3a25 100644 --- a/photo.go +++ b/internal/telegram/photo.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" @@ -42,8 +42,8 @@ func (bot TipBot) photoHandler(ctx context.Context, m *tb.Message) { return } - // get file reader closer from telegram api - reader, err := bot.telegram.GetFile(m.Photo.MediaFile()) + // get file reader closer from Telegram api + reader, err := bot.Telegram.GetFile(m.Photo.MediaFile()) if err != nil { log.Errorf("[photoHandler] getfile error: %v\n", err) return diff --git a/send.go b/internal/telegram/send.go similarity index 93% rename from send.go rename to internal/telegram/send.go index 10e80db6..ac8c74db 100644 --- a/send.go +++ b/internal/telegram/send.go @@ -1,9 +1,10 @@ -package main +package telegram import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/str" "strings" "time" @@ -32,7 +33,7 @@ func helpSendUsage(ctx context.Context, errormsg string) string { func (bot *TipBot) SendCheckSyntax(ctx context.Context, m *tb.Message) (bool, string) { arguments := strings.Split(m.Text, " ") if len(arguments) < 2 { - return false, fmt.Sprintf(Translate(ctx, "sendSyntaxErrorMessage"), GetUserStrMd(bot.telegram.Me)) + return false, fmt.Sprintf(Translate(ctx, "sendSyntaxErrorMessage"), GetUserStrMd(bot.Telegram.Me)) } return true, "" } @@ -65,7 +66,7 @@ func (msg SendData) Key() string { func (bot *TipBot) LockSend(tx *SendData) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = true - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -75,7 +76,7 @@ func (bot *TipBot) LockSend(tx *SendData) error { func (bot *TipBot) ReleaseSend(tx *SendData) error { // immediatelly set intransaction to block duplicate calls tx.InTransaction = false - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -84,7 +85,7 @@ func (bot *TipBot) ReleaseSend(tx *SendData) error { func (bot *TipBot) InactivateSend(tx *SendData) error { tx.Active = false - err := bot.bunt.Set(tx) + err := bot.Bunt.Set(tx) if err != nil { return err } @@ -95,7 +96,7 @@ func (bot *TipBot) getSend(c *tb.Callback) (*SendData, error) { sendData := NewSend() sendData.ID = c.Data - err := bot.bunt.Get(sendData) + err := bot.Bunt.Get(sendData) // to avoid race conditions, we block the call if there is // already an active transaction by loop until InTransaction is false @@ -108,7 +109,7 @@ func (bot *TipBot) getSend(c *tb.Callback) (*SendData, error) { default: log.Infoln("[send] in transaction") time.Sleep(time.Duration(500) * time.Millisecond) - err = bot.bunt.Get(sendData) + err = bot.Bunt.Get(sendData) } } if err != nil { @@ -140,7 +141,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { if ok, errstr := bot.SendCheckSyntax(ctx, m); !ok { bot.trySendMessage(m.Sender, helpSendUsage(ctx, errstr)) - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) return } @@ -176,7 +177,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { errmsg := fmt.Sprintf("[/send] Error: Send amount not valid.") log.Errorln(errmsg) // immediately delete if the amount is bullshit - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) bot.trySendMessage(m.Sender, helpSendUsage(ctx, Translate(ctx, "sendValidAmountMessage"))) return } @@ -209,15 +210,15 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { toUserDb, err := GetUserByTelegramUsername(toUserStrWithoutAt, *bot) if err != nil { - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), toUserStrMention)) return } // entire text of the inline object - confirmText := fmt.Sprintf(Translate(ctx, "confirmSendMessage"), MarkdownEscape(toUserStrMention), amount) + confirmText := fmt.Sprintf(Translate(ctx, "confirmSendMessage"), str.MarkdownEscape(toUserStrMention), amount) if len(sendMemo) > 0 { - confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmSendAppendMemo"), MarkdownEscape(sendMemo)) + confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmSendAppendMemo"), str.MarkdownEscape(sendMemo)) } // object that holds all information about the send payment id := fmt.Sprintf("send-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) @@ -234,16 +235,16 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { LanguageCode: ctx.Value("publicLanguageCode").(string), } // save persistent struct - runtime.IgnoreError(bot.bunt.Set(sendData)) + runtime.IgnoreError(bot.Bunt.Set(sendData)) sendDataJson, err := json.Marshal(sendData) if err != nil { - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) log.Printf("[/send] Error: %s\n", err.Error()) bot.trySendMessage(m.Sender, fmt.Sprint(Translate(ctx, "errorTryLaterMessage"))) return } - // save the send data to the database + // save the send data to the Database // log.Debug(sendData) SetUserState(user, *bot, lnbits.UserStateConfirmSend, string(sendDataJson)) sendButton := sendConfirmationMenu.Data(Translate(ctx, "sendButtonMessage"), "confirm_send") @@ -344,7 +345,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { } // send memo if it was present if len(sendMemo) > 0 { - bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", MarkdownEscape(sendMemo))) + bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", str.MarkdownEscape(sendMemo))) } return diff --git a/start.go b/internal/telegram/start.go similarity index 91% rename from start.go rename to internal/telegram/start.go index e9fc611a..efa98ef9 100644 --- a/start.go +++ b/internal/telegram/start.go @@ -1,9 +1,10 @@ -package main +package telegram import ( "context" "errors" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal" "strconv" log "github.com/sirupsen/logrus" @@ -21,7 +22,7 @@ func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) { // WILL RESULT IN AN ENDLESS LOOP OTHERWISE // bot.helpHandler(m) log.Printf("[/start] User: %s (%d)\n", m.Sender.Username, m.Sender.ID) - walletCreationMsg, err := bot.telegram.Send(m.Sender, Translate(ctx, "startSettingWalletMessage")) + walletCreationMsg, err := bot.Telegram.Send(m.Sender, Translate(ctx, "startSettingWalletMessage")) user, err := bot.initWallet(m.Sender) if err != nil { log.Errorln(fmt.Sprintf("[startHandler] Error with initWallet: %s", err.Error())) @@ -76,9 +77,9 @@ func (bot TipBot) initWallet(tguser *tb.User) (*lnbits.User, error) { func (bot TipBot) createWallet(user *lnbits.User) error { UserStr := GetUserStr(user.Telegram) - u, err := bot.client.CreateUserWithInitialWallet(strconv.Itoa(user.Telegram.ID), + u, err := bot.Client.CreateUserWithInitialWallet(strconv.Itoa(user.Telegram.ID), fmt.Sprintf("%d (%s)", user.Telegram.ID, UserStr), - Configuration.Lnbits.AdminId, + internal.Configuration.Lnbits.AdminId, UserStr) if err != nil { errormsg := fmt.Sprintf("[createWallet] Create wallet error: %s", err) @@ -88,7 +89,7 @@ func (bot TipBot) createWallet(user *lnbits.User) error { user.Wallet = &lnbits.Wallet{} user.ID = u.ID user.Name = u.Name - wallet, err := bot.client.Wallets(*user) + wallet, err := bot.Client.Wallets(*user) if err != nil { errormsg := fmt.Sprintf("[createWallet] Get wallet error: %s", err) log.Errorln(errormsg) diff --git a/telegram.go b/internal/telegram/telegram.go similarity index 76% rename from telegram.go rename to internal/telegram/telegram.go index abad6dac..c213ced9 100644 --- a/telegram.go +++ b/internal/telegram/telegram.go @@ -1,4 +1,4 @@ -package main +package telegram import ( log "github.com/sirupsen/logrus" @@ -6,14 +6,14 @@ import ( ) func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options ...interface{}) (msg *tb.Message) { - msg, err := bot.telegram.Forward(to, what, options...) + msg, err := bot.Telegram.Forward(to, what, options...) if err != nil { log.Errorln(err.Error()) } return } func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...interface{}) (msg *tb.Message) { - msg, err := bot.telegram.Send(to, what, options...) + msg, err := bot.Telegram.Send(to, what, options...) if err != nil { log.Errorln(err.Error()) } @@ -21,7 +21,7 @@ func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...i } func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...interface{}) (msg *tb.Message) { - msg, err := bot.telegram.Reply(to, what, options...) + msg, err := bot.Telegram.Reply(to, what, options...) if err != nil { log.Errorln(err.Error()) } @@ -29,7 +29,7 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i } func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message) { - msg, err := bot.telegram.Edit(to, what, options...) + msg, err := bot.Telegram.Edit(to, what, options...) if err != nil { log.Errorln(err.Error()) } @@ -37,7 +37,7 @@ func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...in } func (bot TipBot) tryDeleteMessage(msg tb.Editable) { - err := bot.telegram.Delete(msg) + err := bot.Telegram.Delete(msg) if err != nil { log.Errorln(err.Error()) } diff --git a/text.go b/internal/telegram/text.go similarity index 91% rename from text.go rename to internal/telegram/text.go index 34166f0a..bae6c497 100644 --- a/text.go +++ b/internal/telegram/text.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" @@ -14,7 +14,7 @@ func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { return } - // check if user is in database, if not, initialize wallet + // check if user is in Database, if not, initialize wallet user := LoadUser(ctx) if user.Wallet == nil || !user.Initialized { bot.startHandler(ctx, m) diff --git a/tip.go b/internal/telegram/tip.go similarity index 85% rename from tip.go rename to internal/telegram/tip.go index 90e97333..70544ffb 100644 --- a/tip.go +++ b/internal/telegram/tip.go @@ -1,8 +1,10 @@ -package main +package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/str" "strings" "time" @@ -29,7 +31,7 @@ func TipCheckSyntax(ctx context.Context, m *tb.Message) (bool, string) { func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { // delete the tip message after a few seconds, this is default behaviour - defer NewMessage(m, WithDuration(time.Second*time.Duration(Configuration.Telegram.MessageDisposeDuration), bot.telegram)) + defer NewMessage(m, WithDuration(time.Second*time.Duration(internal.Configuration.Telegram.MessageDisposeDuration), bot.Telegram)) // check and print all commands bot.anyTextHandler(ctx, m) user := LoadUser(ctx) @@ -39,7 +41,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { // only if message is a reply if !m.IsReply() { - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) bot.trySendMessage(m.Sender, helpTipUsage(ctx, Translate(ctx, "tipDidYouReplyMessage"))) bot.trySendMessage(m.Sender, Translate(ctx, "tipInviteGroupMessage")) return @@ -47,7 +49,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if ok, err := TipCheckSyntax(ctx, m); !ok { bot.trySendMessage(m.Sender, helpTipUsage(ctx, err)) - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) return } @@ -56,7 +58,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if err != nil || amount < 1 { errmsg := fmt.Sprintf("[/tip] Error: Tip amount not valid.") // immediately delete if the amount is bullshit - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) bot.trySendMessage(m.Sender, helpTipUsage(ctx, Translate(ctx, "tipValidAmountMessage"))) log.Errorln(errmsg) return @@ -72,7 +74,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { to := LoadReplyToUser(ctx) if from.Telegram.ID == to.Telegram.ID { - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) bot.trySendMessage(m.Sender, Translate(ctx, "tipYourselfMessage")) return } @@ -108,7 +110,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { t.Memo = transactionMemo success, err := t.Send() if !success { - NewMessage(m, WithDuration(0, bot.telegram)) + NewMessage(m, WithDuration(0, bot.Telegram)) bot.trySendMessage(m.Sender, Translate(ctx, "tipErrorMessage")) errMsg := fmt.Sprintf("[/tip] Transaction failed: %s", err) log.Errorln(errMsg) @@ -121,7 +123,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { log.Infof("[tip] Transaction sent from %s to %s (%d sat).", fromUserStr, toUserStr, amount) // notify users - _, err = bot.telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "tipSentMessage"), amount, toUserStrMd)) + _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "tipSentMessage"), amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[/tip] Error: Send message to %s: %s", toUserStr, err) log.Errorln(errmsg) @@ -135,7 +137,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "tipReceivedMessage"), fromUserStrMd, amount)) if len(tipMemo) > 0 { - bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", MarkdownEscape(tipMemo))) + bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", str.MarkdownEscape(tipMemo))) } return } diff --git a/tooltip.go b/internal/telegram/tooltip.go similarity index 91% rename from tooltip.go rename to internal/telegram/tooltip.go index 75e572c7..184d998e 100644 --- a/tooltip.go +++ b/internal/telegram/tooltip.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "encoding/json" @@ -78,7 +78,7 @@ func (ttt TipTooltip) getUpdatedTipTooltipMessage(botUserName string, notInitial return tipToolTipMessage } -// getTippersString joins all tippers username or telegram id's as mentions (@username or [inline mention of a user](tg://user?id=123456789)) +// getTippersString joins all tippers username or Telegram id's as mentions (@username or [inline mention of a user](tg://user?id=123456789)) func getTippersString(tippers []*tb.User) string { var tippersStr string for _, uniqueUser := range tippers { @@ -102,7 +102,7 @@ func getTippersString(tippers []*tb.User) string { // tipTooltipExists checks if this tip is already known func tipTooltipExists(replyToId int, bot *TipBot) (bool, *TipTooltip) { message := NewTipTooltip(&tb.Message{ReplyTo: &tb.Message{ID: replyToId}}) - err := bot.bunt.Get(message) + err := bot.Bunt.Get(message) if err != nil { return false, message } @@ -128,9 +128,9 @@ func tipTooltipHandler(m *tb.Message, bot *TipBot, amount int, initializedWallet tipmsg = fmt.Sprintf(tooltipSingleTipMessage, tipmsg, userStr) if !initializedWallet { - tipmsg = tipmsg + fmt.Sprintf("\n%s", fmt.Sprintf(tooltipChatWithBotMessage, GetUserStrMd(bot.telegram.Me))) + tipmsg = tipmsg + fmt.Sprintf("\n%s", fmt.Sprintf(tooltipChatWithBotMessage, GetUserStrMd(bot.Telegram.Me))) } - msg, err := bot.telegram.Reply(m.ReplyTo, tipmsg, tb.Silent) + msg, err := bot.Telegram.Reply(m.ReplyTo, tipmsg, tb.Silent) if err != nil { log.Errorf("[tipTooltipHandler Reply] %s", err.Error()) // todo: in case of error we should do something better than just return 0 @@ -138,13 +138,13 @@ func tipTooltipHandler(m *tb.Message, bot *TipBot, amount int, initializedWallet } message := NewTipTooltip(msg, TipAmount(amount), Tips(1)) message.Tippers = appendUinqueUsersToSlice(message.Tippers, m.Sender) - runtime.IgnoreError(bot.bunt.Set(message)) + runtime.IgnoreError(bot.Bunt.Set(message)) } // first call will return false, every following call will return true return hasTip } -// updateToolTip updates existing tip tool tip in telegram +// updateToolTip updates existing tip tool tip in Telegram func (ttt *TipTooltip) updateTooltip(bot *TipBot, user *tb.User, amount int, notInitializedWallet bool) error { ttt.TipAmount += amount ttt.Ntips += 1 @@ -154,12 +154,12 @@ func (ttt *TipTooltip) updateTooltip(bot *TipBot, user *tb.User, amount int, not if err != nil { return err } - return bot.bunt.Set(ttt) + return bot.Bunt.Set(ttt) } // tipTooltipInitializedHandler is called when the user initializes the wallet func tipTooltipInitializedHandler(user *tb.User, bot TipBot) { - runtime.IgnoreError(bot.bunt.View(func(tx *buntdb.Tx) error { + runtime.IgnoreError(bot.Bunt.View(func(tx *buntdb.Tx) error { err := tx.Ascend(storage.MessageOrderedByReplyToFrom, func(key, value string) bool { replyToUserId := gjson.Get(value, storage.MessageOrderedByReplyToFrom) if replyToUserId.String() == strconv.Itoa(user.ID) { @@ -187,8 +187,8 @@ func (ttt TipTooltip) Key() string { // editTooltip updates the tooltip message with the new tip amount and tippers and edits it func (ttt *TipTooltip) editTooltip(bot *TipBot, notInitializedWallet bool) error { - tipToolTip := ttt.getUpdatedTipTooltipMessage(GetUserStrMd(bot.telegram.Me), notInitializedWallet) - m, err := bot.telegram.Edit(ttt.Message.Message, tipToolTip) + tipToolTip := ttt.getUpdatedTipTooltipMessage(GetUserStrMd(bot.Telegram.Me), notInitializedWallet) + m, err := bot.Telegram.Edit(ttt.Message.Message, tipToolTip) if err != nil { return err } diff --git a/tooltip_test.go b/internal/telegram/tooltip_test.go similarity index 99% rename from tooltip_test.go rename to internal/telegram/tooltip_test.go index 7fe89929..548ebf10 100644 --- a/tooltip_test.go +++ b/internal/telegram/tooltip_test.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "testing" diff --git a/transaction.go b/internal/telegram/transaction.go similarity index 98% rename from transaction.go rename to internal/telegram/transaction.go index c74e9993..5ffd06ea 100644 --- a/transaction.go +++ b/internal/telegram/transaction.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "fmt" @@ -126,14 +126,14 @@ func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits Amount: int64(amount), Out: false, Memo: memo}, - bot.client) + bot.Client) if err != nil { errmsg := fmt.Sprintf("[SendTransaction] Error: Could not create invoice for user %s", toUserStr) log.Errorln(errmsg) return false, err } // pay invoice - _, err = from.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoice.PaymentRequest}, bot.client) + _, err = from.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoice.PaymentRequest}, bot.Client) if err != nil { errmsg := fmt.Sprintf("[SendTransaction] Error: Payment from %s to %s of %d sat failed", fromUserStr, toUserStr, amount) log.Errorln(errmsg) diff --git a/translate.go b/internal/telegram/translate.go similarity index 97% rename from translate.go rename to internal/telegram/translate.go index 5e615e74..e1cc26cb 100644 --- a/translate.go +++ b/internal/telegram/translate.go @@ -1,4 +1,4 @@ -package main +package telegram import ( "context" diff --git a/users.go b/internal/telegram/users.go similarity index 79% rename from users.go rename to internal/telegram/users.go index b7696b02..2062984a 100644 --- a/users.go +++ b/internal/telegram/users.go @@ -1,8 +1,9 @@ -package main +package telegram import ( "errors" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/str" "strings" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -15,33 +16,12 @@ import ( func SetUserState(user *lnbits.User, bot TipBot, stateKey lnbits.UserStateKey, stateData string) { user.StateKey = stateKey user.StateData = stateData - bot.database.Table("users").Where("name = ?", user.Name).Update("state_key", user.StateKey).Update("state_data", user.StateData) + bot.Database.Table("users").Where("name = ?", user.Name).Update("state_key", user.StateKey).Update("state_data", user.StateData) } func ResetUserState(user *lnbits.User, bot TipBot) { user.ResetState() - bot.database.Table("users").Where("name = ?", user.Name).Update("state_key", 0).Update("state_data", "") -} - -var markdownV2Escapes = []string{"_", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"} -var markdownEscapes = []string{"_", "*", "`", "["} - -func MarkdownV2Escape(s string) string { - for _, esc := range markdownV2Escapes { - if strings.Contains(s, esc) { - s = strings.Replace(s, esc, fmt.Sprintf("\\%s", esc), -1) - } - } - return s -} - -func MarkdownEscape(s string) string { - for _, esc := range markdownEscapes { - if strings.Contains(s, esc) { - s = strings.Replace(s, esc, fmt.Sprintf("\\%s", esc), -1) - } - } - return s + bot.Database.Table("users").Where("name = ?", user.Name).Update("state_key", 0).Update("state_data", "") } func GetUserStr(user *tb.User) string { @@ -66,7 +46,7 @@ func GetUserStrMd(user *tb.User) string { return userStr } else { // escape only if user has a username - return MarkdownEscape(userStr) + return str.MarkdownEscape(userStr) } } @@ -81,7 +61,7 @@ func appendUinqueUsersToSlice(slice []*tb.User, i *tb.User) []*tb.User { func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int, err error) { - wallet, err := bot.client.Info(*user.Wallet) + wallet, err := bot.Client.Info(*user.Wallet) if err != nil { errmsg := fmt.Sprintf("[GetUserBalance] Error: Couldn't fetch user %s's info from LNbits: %s", GetUserStr(user.Telegram), err) log.Errorln(errmsg) @@ -116,7 +96,7 @@ func (bot *TipBot) CreateWalletForTelegramUser(tbUser *tb.User) (*lnbits.User, e log.Errorln(errmsg) return user, err } - tx := bot.database.Save(user) + tx := bot.Database.Save(user) if tx.Error != nil { return nil, tx.Error } diff --git a/main.go b/main.go index fbc67ea1..275918e1 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,9 @@ package main import ( + "github.com/LightningTipBot/LightningTipBot/internal/lnbits/webhook" + "github.com/LightningTipBot/LightningTipBot/internal/lnurl" + "github.com/LightningTipBot/LightningTipBot/internal/telegram" log "github.com/sirupsen/logrus" "runtime/debug" ) @@ -18,7 +21,9 @@ func main() { // set logger setLogger() defer withRecovery() - bot := NewBot() + bot := telegram.NewBot() + webhook.NewServer(&bot) + lnurl.NewServer(&bot) bot.Start() } From 808a5a3b72b773e16680298e0b65edbf8edf1209 Mon Sep 17 00:00:00 2001 From: LightningTipBot Date: Thu, 7 Oct 2021 22:53:07 +0300 Subject: [PATCH 015/541] price watcher --- internal/price/price.go | 104 ++++++++++++++++++++++++++++++++++++++++ main.go | 5 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 internal/price/price.go diff --git a/internal/price/price.go b/internal/price/price.go new file mode 100644 index 00000000..6ae17889 --- /dev/null +++ b/internal/price/price.go @@ -0,0 +1,104 @@ +package price + +import ( + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" +) + +type PriceWatcher struct { + client *http.Client + UpdateInterval time.Duration + Currencies []string + Exchanges map[string]func(string) (float64, error) + Price map[string]float64 +} + +func NewPriceWatcher() *PriceWatcher { + pricewatcher := &PriceWatcher{ + client: &http.Client{ + Timeout: time.Second * time.Duration(5), + }, + Currencies: []string{"USD", "EUR"}, + Price: make(map[string]float64, 0), + Exchanges: make(map[string]func(string) (float64, error), 0), + UpdateInterval: time.Second * time.Duration(30), + } + pricewatcher.Exchanges["coinbase"] = pricewatcher.GetCoinbasePrice + pricewatcher.Exchanges["bitfinex"] = pricewatcher.GetBitfinexPrice + log.Infof("[PriceWatcher] Watcher started") + return pricewatcher +} + +func (p *PriceWatcher) Start() { + go p.Watch() +} + +func (p *PriceWatcher) Watch() error { + for { + time.Sleep(p.UpdateInterval) + for _, currency := range p.Currencies { + avg_price := 0.0 + n_responses := 0 + for exchange, getPrice := range p.Exchanges { + fprice, err := getPrice(currency) + if err != nil { + log.Error(err) + // if one exchanges is down, use the next + continue + } + n_responses++ + avg_price += fprice + log.Debugf("[PriceWatcher] %s %s price: %f", exchange, currency, fprice) + } + p.Price[currency] = avg_price / float64(n_responses) + log.Debugf("[PriceWatcher] Average %s price: %f", currency, p.Price[currency]) + } + } +} + +func (p *PriceWatcher) GetCoinbasePrice(currency string) (float64, error) { + coinbaseEndpoint, err := url.Parse(fmt.Sprintf("https://api.coinbase.com/v2/prices/spot?currency=%s", currency)) + response, err := p.client.Get(coinbaseEndpoint.String()) + if err, ok := err.(net.Error); ok && err.Timeout() { + return 0, err + } + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + price := gjson.Get(string(bodyBytes), "data.amount") + fprice, err := strconv.ParseFloat(strings.TrimSpace(price.String()), 64) + if err != nil { + log.Fatal(err) + } + return fprice, nil +} + +func (p *PriceWatcher) GetBitfinexPrice(currency string) (float64, error) { + var bitfinexCurrencyToPair = map[string]string{"USD": "btcusd", "EUR": "btceur"} + pair := bitfinexCurrencyToPair[currency] + bitfinexEndpoint, err := url.Parse(fmt.Sprintf("https://api.bitfinex.com/v1/pubticker/%s", pair)) + response, err := p.client.Get(bitfinexEndpoint.String()) + if err, ok := err.(net.Error); ok && err.Timeout() { + return 0, err + } + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Fatal(err) + } + price := gjson.Get(string(bodyBytes), "last_price") + fprice, err := strconv.ParseFloat(strings.TrimSpace(price.String()), 64) + if err != nil { + log.Fatal(err) + } + return fprice, nil +} diff --git a/main.go b/main.go index 275918e1..685d3733 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,13 @@ package main import ( + "runtime/debug" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits/webhook" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" + "github.com/LightningTipBot/LightningTipBot/internal/price" "github.com/LightningTipBot/LightningTipBot/internal/telegram" log "github.com/sirupsen/logrus" - "runtime/debug" ) // setLogger will initialize the log format @@ -24,6 +26,7 @@ func main() { bot := telegram.NewBot() webhook.NewServer(&bot) lnurl.NewServer(&bot) + price.NewPriceWatcher().Start() bot.Start() } From 6a5f09fa1827cb989643c08eb4a078205e31e87d Mon Sep 17 00:00:00 2001 From: lngohumble Date: Thu, 7 Oct 2021 22:08:23 +0200 Subject: [PATCH 016/541] Revert "price watcher" This reverts commit 808a5a3b --- internal/price/price.go | 104 ---------------------------------------- main.go | 5 +- 2 files changed, 1 insertion(+), 108 deletions(-) delete mode 100644 internal/price/price.go diff --git a/internal/price/price.go b/internal/price/price.go deleted file mode 100644 index 6ae17889..00000000 --- a/internal/price/price.go +++ /dev/null @@ -1,104 +0,0 @@ -package price - -import ( - "fmt" - "io/ioutil" - "net" - "net/http" - "net/url" - "strconv" - "strings" - "time" - - log "github.com/sirupsen/logrus" - "github.com/tidwall/gjson" -) - -type PriceWatcher struct { - client *http.Client - UpdateInterval time.Duration - Currencies []string - Exchanges map[string]func(string) (float64, error) - Price map[string]float64 -} - -func NewPriceWatcher() *PriceWatcher { - pricewatcher := &PriceWatcher{ - client: &http.Client{ - Timeout: time.Second * time.Duration(5), - }, - Currencies: []string{"USD", "EUR"}, - Price: make(map[string]float64, 0), - Exchanges: make(map[string]func(string) (float64, error), 0), - UpdateInterval: time.Second * time.Duration(30), - } - pricewatcher.Exchanges["coinbase"] = pricewatcher.GetCoinbasePrice - pricewatcher.Exchanges["bitfinex"] = pricewatcher.GetBitfinexPrice - log.Infof("[PriceWatcher] Watcher started") - return pricewatcher -} - -func (p *PriceWatcher) Start() { - go p.Watch() -} - -func (p *PriceWatcher) Watch() error { - for { - time.Sleep(p.UpdateInterval) - for _, currency := range p.Currencies { - avg_price := 0.0 - n_responses := 0 - for exchange, getPrice := range p.Exchanges { - fprice, err := getPrice(currency) - if err != nil { - log.Error(err) - // if one exchanges is down, use the next - continue - } - n_responses++ - avg_price += fprice - log.Debugf("[PriceWatcher] %s %s price: %f", exchange, currency, fprice) - } - p.Price[currency] = avg_price / float64(n_responses) - log.Debugf("[PriceWatcher] Average %s price: %f", currency, p.Price[currency]) - } - } -} - -func (p *PriceWatcher) GetCoinbasePrice(currency string) (float64, error) { - coinbaseEndpoint, err := url.Parse(fmt.Sprintf("https://api.coinbase.com/v2/prices/spot?currency=%s", currency)) - response, err := p.client.Get(coinbaseEndpoint.String()) - if err, ok := err.(net.Error); ok && err.Timeout() { - return 0, err - } - bodyBytes, err := ioutil.ReadAll(response.Body) - if err != nil { - log.Fatal(err) - } - price := gjson.Get(string(bodyBytes), "data.amount") - fprice, err := strconv.ParseFloat(strings.TrimSpace(price.String()), 64) - if err != nil { - log.Fatal(err) - } - return fprice, nil -} - -func (p *PriceWatcher) GetBitfinexPrice(currency string) (float64, error) { - var bitfinexCurrencyToPair = map[string]string{"USD": "btcusd", "EUR": "btceur"} - pair := bitfinexCurrencyToPair[currency] - bitfinexEndpoint, err := url.Parse(fmt.Sprintf("https://api.bitfinex.com/v1/pubticker/%s", pair)) - response, err := p.client.Get(bitfinexEndpoint.String()) - if err, ok := err.(net.Error); ok && err.Timeout() { - return 0, err - } - bodyBytes, err := ioutil.ReadAll(response.Body) - if err != nil { - log.Fatal(err) - } - price := gjson.Get(string(bodyBytes), "last_price") - fprice, err := strconv.ParseFloat(strings.TrimSpace(price.String()), 64) - if err != nil { - log.Fatal(err) - } - return fprice, nil -} diff --git a/main.go b/main.go index 685d3733..275918e1 100644 --- a/main.go +++ b/main.go @@ -1,13 +1,11 @@ package main import ( - "runtime/debug" - "github.com/LightningTipBot/LightningTipBot/internal/lnbits/webhook" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" - "github.com/LightningTipBot/LightningTipBot/internal/price" "github.com/LightningTipBot/LightningTipBot/internal/telegram" log "github.com/sirupsen/logrus" + "runtime/debug" ) // setLogger will initialize the log format @@ -26,7 +24,6 @@ func main() { bot := telegram.NewBot() webhook.NewServer(&bot) lnurl.NewServer(&bot) - price.NewPriceWatcher().Start() bot.Start() } From 94eff98f603694bb0266e25d21672485c4c4b275 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Thu, 7 Oct 2021 23:46:52 +0200 Subject: [PATCH 017/541] bunt transaction generalization (#73) * add generalized bunt database transaction * use bunt tx for inline faucet and send * use bunt tx for inline receive * use bunt tx for inline send * use bunt tx for pay * fix typo * readd context * add generalized bunt database transaction * use bunt tx for inline faucet and send * use bunt tx for inline receive * use bunt tx for inline send * use bunt tx for pay * fix typo * readd context * some fixes * tigher error checking * add CreatedAt and UpdatedAt flags * check error in GetTransaction * add bunt transaction package and rename stuff * use transaction.Base as function receiver for transaction functions * add transaction.Base.Set function * fix tiptooltip bunt index * refactor * auto update updated at * refactor * remove balance from error * fix updated at * faucet errors * add generalized bunt database transaction * use bunt tx for inline faucet and send * use bunt tx for inline receive * use bunt tx for inline send * use bunt tx for pay * fix typo * readd context * some fixes * tigher error checking * add generalized bunt database transaction * use bunt tx for inline faucet and send * use bunt tx for inline send * add CreatedAt and UpdatedAt flags * check error in GetTransaction * add bunt transaction package and rename stuff * use transaction.Base as function receiver for transaction functions * add transaction.Base.Set function * fix tiptooltip bunt index * refactor * auto update updated at * fix updated at * faucet errors * refactor * remove balance from error * rebase fixes * fix imports * fix de string * fix receive and send * faucet language * remove pay constructor * release faucet * remove redundant inTransaction * remove constructor * remove constructor * transaction get pointer receiver * send race condition * inactivate in db Co-authored-by: LightningTipBot --- internal/errors/errors.go | 32 ++ internal/lnbits/types.go | 4 + internal/storage/bunt.go | 14 - internal/storage/transaction/transaction.go | 98 ++++++ internal/telegram/bot.go | 3 +- internal/telegram/database.go | 19 + internal/telegram/handler.go | 9 +- internal/telegram/inline_faucet.go | 368 ++++++++++---------- internal/telegram/inline_receive.go | 175 +++------- internal/telegram/inline_send.go | 183 +++------- internal/telegram/pay.go | 138 ++------ internal/telegram/send.go | 102 +----- internal/telegram/start.go | 2 + internal/telegram/tooltip.go | 7 +- internal/telegram/users.go | 2 +- translations/de.toml | 2 +- translations/en.toml | 2 +- translations/es.toml | 2 +- translations/nl.toml | 2 +- 19 files changed, 509 insertions(+), 655 deletions(-) create mode 100644 internal/errors/errors.go create mode 100644 internal/storage/transaction/transaction.go diff --git a/internal/errors/errors.go b/internal/errors/errors.go new file mode 100644 index 00000000..72a21e59 --- /dev/null +++ b/internal/errors/errors.go @@ -0,0 +1,32 @@ +package errors + +import "encoding/json" + +type TipBotErrorType int + +const ( + DecodeAmountError TipBotErrorType = 1000 + iota + DecodePerUserAmountError + InvalidAmountError + InvalidAmountPerUserError + GetBalanceError + BalanceToLowError +) + +func New(code TipBotErrorType, err error) TipBotError { + return TipBotError{Err: err, Message: err.Error(), Code: code} +} + +type TipBotError struct { + Message string `json:"message"` + Err error + Code TipBotErrorType `json:"code"` +} + +func (e TipBotError) Error() string { + j, err := json.Marshal(&e) + if err != nil { + return e.Message + } + return string(j) +} diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 53133e84..ea792add 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -1,6 +1,8 @@ package lnbits import ( + "time" + "github.com/imroc/req" tb "gopkg.in/tucnak/telebot.v2" ) @@ -20,6 +22,8 @@ type User struct { Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"` StateKey UserStateKey `json:"stateKey"` StateData string `json:"stateData"` + CreatedAt time.Time `json:"created"` + UpdatedAt time.Time `json:"updated"` } const ( diff --git a/internal/storage/bunt.go b/internal/storage/bunt.go index c2f9f580..c85ec70b 100644 --- a/internal/storage/bunt.go +++ b/internal/storage/bunt.go @@ -16,24 +16,11 @@ type DB struct { *buntdb.DB } -const ( - MessageOrderedByReplyToFrom = "message.reply_to_message.from.id" - MessageOrderedByReplyTo = "message.reply_to_message.id" -) - func NewBunt(filePath string) *DB { db, err := buntdb.Open(filePath) if err != nil { log.Fatal(err) } - err = db.CreateIndex(MessageOrderedByReplyToFrom, "*", buntdb.IndexJSON(MessageOrderedByReplyToFrom)) - if err != nil { - panic(err) - } - err = db.CreateIndex(MessageOrderedByReplyTo, "*", buntdb.IndexJSON(MessageOrderedByReplyTo)) - if err != nil { - panic(err) - } return &DB{db} } @@ -110,5 +97,4 @@ func (db *DB) Delete(index string, object Storable) error { } return nil }) - } diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go new file mode 100644 index 00000000..3bcb9892 --- /dev/null +++ b/internal/storage/transaction/transaction.go @@ -0,0 +1,98 @@ +package transaction + +import ( + "fmt" + "time" + + "github.com/LightningTipBot/LightningTipBot/internal/storage" +) + +type Base struct { + ID string `json:"id"` + Active bool `json:"active"` + InTransaction bool `json:"intransaction"` + CreatedAt time.Time `json:"created"` + UpdatedAt time.Time `json:"updated"` +} + +type Option func(b *Base) + +func ID(id string) Option { + return func(btx *Base) { + btx.ID = id + } +} + +func New(opts ...Option) *Base { + btx := &Base{ + Active: true, + InTransaction: false, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + for _, opt := range opts { + opt(btx) + } + return btx +} + +func (tx Base) Key() string { + return tx.ID +} +func (tx *Base) Lock(s storage.Storable, db *storage.DB) error { + // immediatelly set intransaction to block duplicate calls + tx.InTransaction = true + err := tx.Set(s, db) + if err != nil { + return err + } + return nil +} + +func (tx *Base) Release(s storage.Storable, db *storage.DB) error { + // immediatelly set intransaction to block duplicate calls + tx.InTransaction = false + err := tx.Set(s, db) + if err != nil { + return err + } + return nil +} + +func (tx *Base) Inactivate(s storage.Storable, db *storage.DB) error { + tx.Active = false + err := tx.Set(s, db) + if err != nil { + return err + } + return nil +} + +func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error) { + err := db.Get(s) + if err != nil { + return s, err + } + // to avoid race conditions, we block the call if there is + // already an active transaction by loop until InTransaction is false + ticker := time.NewTicker(time.Second * 10) + for tx.InTransaction { + select { + case <-ticker.C: + return nil, fmt.Errorf("transaction timeout") + default: + time.Sleep(time.Duration(500) * time.Millisecond) + err = db.Get(s) + } + } + if err != nil { + return nil, fmt.Errorf("could not get transaction") + } + + return s, nil +} + +func (tx *Base) Set(s storage.Storable, db *storage.DB) error { + tx.UpdatedAt = time.Now() + return db.Set(s) +} diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index c43520be..4eeb0353 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -29,12 +29,13 @@ var ( // NewBot migrates data and creates a new bot func NewBot() TipBot { + // create sqlite databases db, txLogger := migration() return TipBot{ Database: db, Client: lnbits.NewClient(internal.Configuration.Lnbits.AdminKey, internal.Configuration.Lnbits.Url), logger: txLogger, - Bunt: storage.NewBunt(internal.Configuration.Database.BuntDbPath), + Bunt: createBunt(), Telegram: newTelegramBot(), } } diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 21f719b9..1e929fa5 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -3,9 +3,12 @@ package telegram import ( "fmt" "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/tidwall/buntdb" "reflect" "strconv" "strings" + "time" log "github.com/sirupsen/logrus" @@ -15,6 +18,21 @@ import ( "gorm.io/gorm" ) +const ( + MessageOrderedByReplyToFrom = "message.reply_to_message.from.id" + TipTooltipKeyPattern = "tip-tool-tip:*" +) + +func createBunt() *storage.DB { + // create bunt database + bunt := storage.NewBunt(internal.Configuration.Database.BuntDbPath) + // create bunt database index for ascending (searching) TipTooltips + err := bunt.CreateIndex(MessageOrderedByReplyToFrom, TipTooltipKeyPattern, buntdb.IndexJSON(MessageOrderedByReplyToFrom)) + if err != nil { + panic(err) + } + return bunt +} func migration() (db *gorm.DB, txLogger *gorm.DB) { txLogger, err := gorm.Open(sqlite.Open(internal.Configuration.Database.TransactionsPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) if err != nil { @@ -90,6 +108,7 @@ func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { func UpdateUserRecord(user *lnbits.User, bot TipBot) error { user.Telegram = bot.copyLowercaseUser(user.Telegram) + user.UpdatedAt = time.Now() tx := bot.Database.Save(user) if tx.Error != nil { errmsg := fmt.Sprintf("[UpdateUserRecord] Error: Couldn't update %s's info in Database.", GetUserStr(user.Telegram)) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index e53ffc10..3fc8929d 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -235,8 +235,11 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{tb.OnQuery}, Handler: bot.anyQueryHandler, Interceptor: &Interceptor{ - Type: QueryInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor, bot.localizerInterceptor}}, + Type: QueryInterceptor, + Before: []intercept.Func{ + bot.requireUserInterceptor, + bot.localizerInterceptor, + }}, }, { Endpoints: []interface{}{tb.OnChosenInlineResult}, @@ -300,7 +303,7 @@ func (bot TipBot) getHandler() []Handler { }, { Endpoints: []interface{}{&btnAcceptInlineFaucet}, - Handler: bot.accpetInlineFaucetHandler, + Handler: bot.acceptInlineFaucetHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{bot.loadUserInterceptor}}, diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index fd9ee5d6..dd992022 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "strings" - "time" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/i18n" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" @@ -21,6 +23,7 @@ var ( ) type InlineFaucet struct { + *transaction.Base Message string `json:"inline_faucet_message"` Amount int `json:"inline_faucet_amount"` RemainingAmount int `json:"inline_faucet_remainingamount"` @@ -28,87 +31,12 @@ type InlineFaucet struct { From *lnbits.User `json:"inline_faucet_from"` To []*tb.User `json:"inline_faucet_to"` Memo string `json:"inline_faucet_memo"` - ID string `json:"inline_faucet_id"` - Active bool `json:"inline_faucet_active"` NTotal int `json:"inline_faucet_ntotal"` NTaken int `json:"inline_faucet_ntaken"` UserNeedsWallet bool `json:"inline_faucet_userneedswallet"` - InTransaction bool `json:"inline_faucet_intransaction"` LanguageCode string `json:"languagecode"` } -func NewInlineFaucet() *InlineFaucet { - inlineFaucet := &InlineFaucet{ - Message: "", - NTaken: 0, - UserNeedsWallet: false, - InTransaction: false, - Active: true, - } - return inlineFaucet - -} - -func (msg InlineFaucet) Key() string { - return msg.ID -} - -func (bot *TipBot) LockFaucet(tx *InlineFaucet) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = true - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) ReleaseFaucet(tx *InlineFaucet) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = false - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) inactivateFaucet(tx *InlineFaucet) error { - tx.Active = false - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -// tipTooltipExists checks if this tip is already known -func (bot *TipBot) getInlineFaucet(c *tb.Callback) (*InlineFaucet, error) { - inlineFaucet := NewInlineFaucet() - inlineFaucet.ID = c.Data - err := bot.Bunt.Get(inlineFaucet) - - // to avoid race conditions, we block the call if there is - // already an active transaction by loop until InTransaction is false - ticker := time.NewTicker(time.Second * 10) - - for inlineFaucet.InTransaction { - select { - case <-ticker.C: - return nil, fmt.Errorf("faucet %s timeout", inlineFaucet.ID) - default: - log.Warnf("[getInlineFaucet] %s in transaction", inlineFaucet.ID) - time.Sleep(time.Duration(500) * time.Millisecond) - err = bot.Bunt.Get(inlineFaucet) - } - } - if err != nil { - return nil, fmt.Errorf("could not get inline faucet %s: %s", inlineFaucet.ID, err) - } - return inlineFaucet, nil - -} - func (bot TipBot) mapFaucetLanguage(ctx context.Context, command string) context.Context { if len(strings.Split(command, " ")) > 1 { c := strings.Split(command, " ")[0][1:] // cut the / @@ -117,116 +45,183 @@ func (bot TipBot) mapFaucetLanguage(ctx context.Context, command string) context return ctx } -func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { - bot.anyTextHandler(ctx, m) - if m.Private() { - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetHelpFaucetInGroup"))) - return - } - ctx = bot.mapFaucetLanguage(ctx, m.Text) - inlineFaucet := NewInlineFaucet() - var err error - inlineFaucet.Amount, err = decodeAmountFromCommand(m.Text) +func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User) (*InlineFaucet, error) { + amount, err := decodeAmountFromCommand(text) if err != nil { - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetInvalidAmountMessage"))) - bot.tryDeleteMessage(m) - return + return nil, errors.New(errors.DecodeAmountError, err) } - peruserStr, err := getArgumentFromCommand(m.Text, 2) + peruserStr, err := getArgumentFromCommand(text, 2) if err != nil { - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), "")) - bot.tryDeleteMessage(m) - return + return nil, errors.New(errors.DecodePerUserAmountError, err) } - inlineFaucet.PerUserAmount, err = getAmount(peruserStr) + perUserAmount, err := getAmount(peruserStr) if err != nil { - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetInvalidAmountMessage"))) - bot.tryDeleteMessage(m) - return + return nil, errors.New(errors.InvalidAmountError, err) } - // peruser amount must be >1 and a divisor of amount - if inlineFaucet.PerUserAmount < 1 || inlineFaucet.Amount%inlineFaucet.PerUserAmount != 0 { - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetInvalidPeruserAmountMessage"))) - bot.tryDeleteMessage(m) - return + if perUserAmount < 1 || amount%perUserAmount != 0 { + return nil, errors.New(errors.InvalidAmountPerUserError, fmt.Errorf("invalid amount per user")) } - inlineFaucet.NTotal = inlineFaucet.Amount / inlineFaucet.PerUserAmount + nTotal := amount / perUserAmount fromUser := LoadUser(ctx) - fromUserStr := GetUserStr(m.Sender) + fromUserStr := GetUserStr(sender) balance, err := bot.GetUserBalance(fromUser) if err != nil { - errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) - log.Errorln(errmsg) - bot.tryDeleteMessage(m) - return + return nil, errors.New(errors.GetBalanceError, err) } // check if fromUser has balance - if balance < inlineFaucet.Amount { - log.Errorf("[faucet] Balance of user %s too low", fromUserStr) - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineSendBalanceLowMessage"), balance)) - bot.tryDeleteMessage(m) - return + if balance < amount { + return nil, errors.New(errors.BalanceToLowError, fmt.Errorf("[faucet] Balance of user %s too low: %v", fromUserStr, err)) } - // // check for memo in command - memo := GetMemoFromCommand(m.Text, 3) + memo := GetMemoFromCommand(text, 3) - inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, inlineFaucet.Amount, inlineFaucet.Amount, 0, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.Amount, inlineFaucet.Amount)) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), perUserAmount, amount, amount, 0, nTotal, MakeProgressbar(amount, amount)) if len(memo) > 0 { inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineFaucetAppendMemo"), memo) } + id := fmt.Sprintf("inl-faucet-%d-%d-%s", sender.ID, amount, RandStringRunes(5)) + + return &InlineFaucet{ + Base: transaction.New(transaction.ID(id)), + Message: inlineMessage, + Amount: amount, + From: fromUser, + Memo: memo, + PerUserAmount: perUserAmount, + NTotal: nTotal, + NTaken: 0, + RemainingAmount: amount, + UserNeedsWallet: false, + LanguageCode: ctx.Value("publicLanguageCode").(string), + }, nil + +} +func (bot TipBot) makeFaucet(ctx context.Context, m *tb.Message, query bool) (*InlineFaucet, error) { + faucet, err := bot.createFaucet(ctx, m.Text, m.Sender) + if err != nil { + switch err.(errors.TipBotError).Code { + case errors.DecodeAmountError: + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetInvalidAmountMessage"))) + bot.tryDeleteMessage(m) + return nil, err + case errors.DecodePerUserAmountError: + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), "")) + bot.tryDeleteMessage(m) + return nil, err + case errors.InvalidAmountError: + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetInvalidAmountMessage"))) + bot.tryDeleteMessage(m) + return nil, err + case errors.InvalidAmountPerUserError: + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetInvalidPeruserAmountMessage"))) + bot.tryDeleteMessage(m) + return nil, err + case errors.GetBalanceError: + // log.Errorln(err.Error()) + bot.tryDeleteMessage(m) + return nil, err + case errors.BalanceToLowError: + // log.Errorf(err.Error()) + bot.trySendMessage(m.Sender, Translate(ctx, "inlineSendBalanceLowMessage")) + bot.tryDeleteMessage(m) + return nil, err + } + } + return faucet, err +} + +func (bot TipBot) makeQueryFaucet(ctx context.Context, q *tb.Query, query bool) (*InlineFaucet, error) { + faucet, err := bot.createFaucet(ctx, q.Text, &q.From) + if err != nil { + switch err.(errors.TipBotError).Code { + case errors.DecodeAmountError: + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + return nil, err + case errors.DecodePerUserAmountError: + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + return nil, err + case errors.InvalidAmountError: + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + return nil, err + case errors.InvalidAmountPerUserError: + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineFaucetInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + return nil, err + case errors.GetBalanceError: + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + return nil, err + case errors.BalanceToLowError: + log.Errorf(err.Error()) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + return nil, err + } + } + return faucet, err +} - inlineFaucet.ID = fmt.Sprintf("inl-faucet-%d-%d-%s", m.Sender.ID, inlineFaucet.Amount, RandStringRunes(5)) +func (bot TipBot) makeFaucetKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { + // inlineFaucetMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} acceptInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "collectButtonMessage"), "confirm_faucet_inline") cancelInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_faucet_inline") - acceptInlineFaucetButton.Data = inlineFaucet.ID - cancelInlineFaucetButton.Data = inlineFaucet.ID - + acceptInlineFaucetButton.Data = id + cancelInlineFaucetButton.Data = id inlineFaucetMenu.Inline( inlineFaucetMenu.Row( acceptInlineFaucetButton, cancelInlineFaucetButton), ) - bot.trySendMessage(m.Chat, inlineMessage, inlineFaucetMenu) - log.Infof("[faucet] %s created faucet %s: %d sat (%d per user)", fromUserStr, inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) - inlineFaucet.Message = inlineMessage - inlineFaucet.From = fromUser - inlineFaucet.Memo = memo - inlineFaucet.RemainingAmount = inlineFaucet.Amount - inlineFaucet.LanguageCode = ctx.Value("publicLanguageCode").(string) - runtime.IgnoreError(bot.Bunt.Set(inlineFaucet)) + return inlineFaucetMenu +} +func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { + bot.anyTextHandler(ctx, m) + if m.Private() { + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetHelpFaucetInGroup"))) + return + } + ctx = bot.mapFaucetLanguage(ctx, m.Text) + inlineFaucet, err := bot.makeFaucet(ctx, m, false) + if err != nil { + log.Errorf("[faucet] %s", err) + return + } + fromUserStr := GetUserStr(m.Sender) + bot.trySendMessage(m.Chat, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) + log.Infof("[faucet] %s created faucet %s: %d sat (%d per user)", fromUserStr, inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) + runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) } func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { - inlineFaucet := NewInlineFaucet() - var err error - inlineFaucet.Amount, err = decodeAmountFromCommand(q.Text) + inlineFaucet, err := bot.makeQueryFaucet(ctx, q, false) if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + // log.Errorf("[faucet] %s", err) return } - if inlineFaucet.Amount < 1 { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + /**amount, err := decodeAmountFromCommand(q.Text) + if err != nil { + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) + return + } + if amount < 1 { + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } peruserStr, err := getArgumentFromCommand(q.Text, 2) if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } - inlineFaucet.PerUserAmount, err = getAmount(peruserStr) + perUserAmount, err := getAmount(peruserStr) if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } // peruser amount must be >1 and a divisor of amount - if inlineFaucet.PerUserAmount < 1 || inlineFaucet.Amount%inlineFaucet.PerUserAmount != 0 { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineFaucetInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + if perUserAmount < 1 || amount%perUserAmount != 0 { + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineFaucetInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } - inlineFaucet.NTotal = inlineFaucet.Amount / inlineFaucet.PerUserAmount + nTotal := amount / perUserAmount fromUser := LoadUser(ctx) fromUserStr := GetUserStr(&q.From) balance, err := bot.GetUserBalance(fromUser) @@ -236,79 +231,81 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { return } // check if fromUser has balance - if balance < inlineFaucet.Amount { + if balance < amount { log.Errorf("Balance of user %s too low", fromUserStr) - bot.inlineQueryReplyWithError(q, fmt.Sprintf(TranslateUser(ctx, "inlineSendBalanceLowMessage"), balance), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) return } // check for memo in command memo := GetMemoFromCommand(q.Text, 3) - + **/ + // faucet, err := bot.createFaucet(ctx, q.Text, &q.From) + // if err != nil { + // log.Errorf(err.Error()) + // return + // } urls := []string{ queryImage, } results := make(tb.Results, len(urls)) // []tb.Result for i, url := range urls { - inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, inlineFaucet.Amount, inlineFaucet.Amount, 0, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.Amount, inlineFaucet.Amount)) - if len(memo) > 0 { - inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineFaucetAppendMemo"), memo) - } + // inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), faucet.PerUserAmount, faucet.Amount, faucet.Amount, 0, faucet.NTotal, MakeProgressbar(faucet.Amount, faucet.Amount)) + // if len(faucet.Memo) > 0 { + // inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineFaucetAppendMemo"), faucet.Memo) + // } result := &tb.ArticleResult{ // URL: url, - Text: inlineMessage, + Text: inlineFaucet.Message, Title: fmt.Sprintf(TranslateUser(ctx, "inlineResultFaucetTitle"), inlineFaucet.Amount), Description: TranslateUser(ctx, "inlineResultFaucetDescription"), // required for photos ThumbURL: url, } - id := fmt.Sprintf("inl-faucet-%d-%d-%s", q.From.ID, inlineFaucet.Amount, RandStringRunes(5)) - acceptInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "collectButtonMessage"), "confirm_faucet_inline") - cancelInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_faucet_inline") - acceptInlineFaucetButton.Data = id - cancelInlineFaucetButton.Data = id - - inlineFaucetMenu.Inline( - inlineFaucetMenu.Row( - acceptInlineFaucetButton, - cancelInlineFaucetButton), - ) - result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: inlineFaucetMenu.InlineKeyboard} + // id := fmt.Sprintf("inl-faucet-%d-%d-%s", q.From.ID, faucet.Amount, RandStringRunes(5)) + result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: bot.makeFaucetKeyboard(ctx, inlineFaucet.ID).InlineKeyboard} results[i] = result - // needed to set a unique string ID for each result - results[i].SetResultID(id) + results[i].SetResultID(inlineFaucet.ID) // create persistend inline send struct - inlineFaucet.Message = inlineMessage - inlineFaucet.ID = id - inlineFaucet.From = fromUser - inlineFaucet.RemainingAmount = inlineFaucet.Amount - inlineFaucet.Memo = memo - inlineFaucet.LanguageCode = ctx.Value("publicLanguageCode").(string) - runtime.IgnoreError(bot.Bunt.Set(inlineFaucet)) + // inlineFaucet := InlineFaucet{ + // Base: transaction.New(transaction.ID(id)), + // Message: inlineMessage, + // From: faucet.From, + // Memo: faucet.Memo, + // NTaken: 0, + // Amount: faucet.Amount, + // PerUserAmount: faucet.PerUserAmount, + // RemainingAmount: faucet.Amount, + // UserNeedsWallet: false, + // LanguageCode: ctx.Value("publicLanguageCode").(string), + // } + + runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) + log.Infof("[faucet] %s created inline faucet %s: %d sat (%d per user)", GetUserStr(inlineFaucet.From.Telegram), inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) } err = bot.Telegram.Answer(q, &tb.QueryResponse{ Results: results, CacheTime: 1, }) - log.Infof("[faucet] %s created inline faucet %s: %d sat (%d per user)", fromUserStr, inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) if err != nil { log.Errorln(err) } } -func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) - - inlineFaucet, err := bot.getInlineFaucet(c) + tx := &InlineFaucet{Base: transaction.New(transaction.ID(c.Data))} + fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[faucet] %s", err) return } + inlineFaucet := fn.(*InlineFaucet) from := inlineFaucet.From - err = bot.LockFaucet(inlineFaucet) + err = inlineFaucet.Lock(inlineFaucet, bot.Bunt) if err != nil { log.Errorf("[faucet] LockFaucet %s error: %s", inlineFaucet.ID, err) return @@ -318,7 +315,7 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback return } // release faucet no matter what - defer bot.ReleaseFaucet(inlineFaucet) + defer inlineFaucet.Release(inlineFaucet, bot.Bunt) if from.Telegram.ID == to.Telegram.ID { bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) @@ -389,22 +386,9 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback if inlineFaucet.UserNeedsWallet { inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } - - // register new inline buttons - inlineFaucetMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} - acceptInlineFaucetButton := inlineFaucetMenu.Data(i18n.Translate(inlineFaucet.LanguageCode, "collectButtonMessage"), "confirm_faucet_inline") - cancelInlineFaucetButton := inlineFaucetMenu.Data(i18n.Translate(inlineFaucet.LanguageCode, "cancelButtonMessage"), "cancel_faucet_inline") - acceptInlineFaucetButton.Data = inlineFaucet.ID - cancelInlineFaucetButton.Data = inlineFaucet.ID - - inlineFaucetMenu.Inline( - inlineFaucetMenu.Row( - acceptInlineFaucetButton, - cancelInlineFaucetButton), - ) // update message log.Infoln(inlineFaucet.Message) - bot.tryEditMessage(c.Message, inlineFaucet.Message, inlineFaucetMenu) + bot.tryEditMessage(c.Message, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) } if inlineFaucet.RemainingAmount < inlineFaucet.PerUserAmount { // faucet is depleted @@ -419,17 +403,19 @@ func (bot *TipBot) accpetInlineFaucetHandler(ctx context.Context, c *tb.Callback } func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { - inlineFaucet, err := bot.getInlineFaucet(c) + tx := &InlineFaucet{Base: transaction.New(transaction.ID(c.Data))} + fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineSendHandler] %s", err) return } + inlineFaucet := fn.(*InlineFaucet) if c.Sender.ID == inlineFaucet.From.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false inlineFaucet.InTransaction = false - runtime.IgnoreError(bot.Bunt.Set(inlineFaucet)) + runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) } return } diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 5cdaae77..cfece2bb 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -3,10 +3,10 @@ package telegram import ( "context" "fmt" - "time" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" @@ -20,151 +20,76 @@ var ( ) type InlineReceive struct { - Message string `json:"inline_receive_message"` - Amount int `json:"inline_receive_amount"` - From *lnbits.User `json:"inline_receive_from"` - To *lnbits.User `json:"inline_receive_to"` - Memo string `json:"inline_receive_memo"` - ID string `json:"inline_receive_id"` - Active bool `json:"inline_receive_active"` - InTransaction bool `json:"inline_receive_intransaction"` - LanguageCode string `json:"languagecode"` + *transaction.Base + Message string `json:"inline_receive_message"` + Amount int `json:"inline_receive_amount"` + From *lnbits.User `json:"inline_receive_from"` + To *lnbits.User `json:"inline_receive_to"` + Memo string + LanguageCode string `json:"languagecode"` } -func NewInlineReceive() *InlineReceive { - inlineReceive := &InlineReceive{ - Message: "", - Active: true, - InTransaction: false, - } - return inlineReceive - -} - -func (msg InlineReceive) Key() string { - return msg.ID -} - -func (bot *TipBot) LockReceive(tx *InlineReceive) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = true - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) ReleaseReceive(tx *InlineReceive) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = false - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) inactivateReceive(tx *InlineReceive) error { - tx.Active = false - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -// tipTooltipExists checks if this tip is already known -func (bot *TipBot) getInlineReceive(c *tb.Callback) (*InlineReceive, error) { - inlineReceive := NewInlineReceive() - inlineReceive.ID = c.Data - err := bot.Bunt.Get(inlineReceive) - // to avoid race conditions, we block the call if there is - // already an active transaction by loop until InTransaction is false - ticker := time.NewTicker(time.Second * 10) - - for inlineReceive.InTransaction { - select { - case <-ticker.C: - return nil, fmt.Errorf("inline receive %s timeout", inlineReceive.ID) - default: - log.Warnf("[getInlineReceive] %s in transaction", inlineReceive.ID) - time.Sleep(time.Duration(500) * time.Millisecond) - err = bot.Bunt.Get(inlineReceive) - } - } - if err != nil { - return nil, fmt.Errorf("could not get inline receive %s", inlineReceive.ID) - } - return inlineReceive, nil - +func (bot TipBot) makeReceiveKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { + acceptInlineReceiveButton := inlineReceiveMenu.Data(Translate(ctx, "payReceiveButtonMessage"), "confirm_receive_inline") + cancelInlineReceiveButton := inlineReceiveMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_receive_inline") + acceptInlineReceiveButton.Data = id + cancelInlineReceiveButton.Data = id + inlineReceiveMenu.Inline( + inlineReceiveMenu.Row( + acceptInlineReceiveButton, + cancelInlineReceiveButton), + ) + return inlineReceiveMenu } func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { from := LoadUser(ctx) - inlineReceive := NewInlineReceive() - var err error - inlineReceive.Amount, err = decodeAmountFromCommand(q.Text) + amount, err := decodeAmountFromCommand(q.Text) if err != nil { bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineQueryReceiveTitle"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) return } - if inlineReceive.Amount < 1 { + if amount < 1 { bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) return } - fromUserStr := GetUserStr(&q.From) - // check for memo in command - inlineReceive.Memo = GetMemoFromCommand(q.Text, 2) - + memo := GetMemoFromCommand(q.Text, 2) urls := []string{ queryImage, } results := make(tb.Results, len(urls)) // []tb.Result for i, url := range urls { - - inlineMessage := fmt.Sprintf(Translate(ctx, "inlineReceiveMessage"), fromUserStr, inlineReceive.Amount) - - if len(inlineReceive.Memo) > 0 { - inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineReceiveAppendMemo"), inlineReceive.Memo) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineReceiveMessage"), fromUserStr, amount) + if len(memo) > 0 { + inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineReceiveAppendMemo"), amount) } - result := &tb.ArticleResult{ // URL: url, Text: inlineMessage, - Title: fmt.Sprintf(TranslateUser(ctx, "inlineResultReceiveTitle"), inlineReceive.Amount), - Description: fmt.Sprintf(TranslateUser(ctx, "inlineResultReceiveDescription"), inlineReceive.Amount), + Title: fmt.Sprintf(TranslateUser(ctx, "inlineResultReceiveTitle"), amount), + Description: fmt.Sprintf(TranslateUser(ctx, "inlineResultReceiveDescription"), amount), // required for photos ThumbURL: url, } - id := fmt.Sprintf("inl-receive-%d-%d-%s", q.From.ID, inlineReceive.Amount, RandStringRunes(5)) - acceptInlineReceiveButton := inlineReceiveMenu.Data(Translate(ctx, "payReceiveButtonMessage"), "confirm_receive_inline") - cancelInlineReceiveButton := inlineReceiveMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_receive_inline") - acceptInlineReceiveButton.Data = id - cancelInlineReceiveButton.Data = id - - inlineReceiveMenu.Inline( - inlineReceiveMenu.Row( - acceptInlineReceiveButton, - cancelInlineReceiveButton), - ) - result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: inlineReceiveMenu.InlineKeyboard} - + id := fmt.Sprintf("inl-receive-%d-%d-%s", q.From.ID, amount, RandStringRunes(5)) + result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: bot.makeReceiveKeyboard(ctx, id).InlineKeyboard} results[i] = result - // needed to set a unique string ID for each result results[i].SetResultID(id) - // create persistend inline send struct - // add data to persistent object - inlineReceive.ID = id - inlineReceive.To = from // The user who wants to receive - // add result to persistent struct - inlineReceive.Message = inlineMessage - inlineReceive.LanguageCode = ctx.Value("publicLanguageCode").(string) - runtime.IgnoreError(bot.Bunt.Set(inlineReceive)) + inlineReceive := InlineReceive{ + Base: transaction.New(transaction.ID(id)), + Message: inlineMessage, + To: from, + Memo: memo, + Amount: amount, + From: from, + + LanguageCode: ctx.Value("publicLanguageCode").(string), + } + runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) } err = bot.Telegram.Answer(q, &tb.QueryResponse{ @@ -172,20 +97,21 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { CacheTime: 1, // 60 == 1 minute, todo: make higher than 1 s in production }) - if err != nil { log.Errorln(err) } } func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) { - inlineReceive, err := bot.getInlineReceive(c) + tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} + rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { log.Errorf("[getInlineReceive] %s", err) return } - err = bot.LockReceive(inlineReceive) + inlineReceive := rn.(*InlineReceive) + err = inlineReceive.Lock(inlineReceive, bot.Bunt) if err != nil { log.Errorf("[acceptInlineReceiveHandler] %s", err) return @@ -196,7 +122,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac return } - defer bot.ReleaseReceive(inlineReceive) + defer inlineReceive.Release(inlineReceive, bot.Bunt) // user `from` is the one who is SENDING // user `to` is the one who is RECEIVING @@ -224,12 +150,12 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac // check if fromUser has balance if balance < inlineReceive.Amount { log.Errorf("[acceptInlineReceiveHandler] balance of user %s too low", fromUserStr) - bot.trySendMessage(from.Telegram, fmt.Sprintf(Translate(ctx, "inlineSendBalanceLowMessage"), balance)) + bot.trySendMessage(from.Telegram, Translate(ctx, "inlineSendBalanceLowMessage")) return } // set inactive to avoid double-sends - bot.inactivateReceive(inlineReceive) + inlineReceive.Inactivate(inlineReceive, bot.Bunt) // todo: user new get username function to get userStrings transactionMemo := fmt.Sprintf("InlineReceive from %s to %s (%d sat).", fromUserStr, toUserStr, inlineReceive.Amount) @@ -267,17 +193,20 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac } func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { - inlineReceive, err := bot.getInlineReceive(c) + tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} + rn, err := tx.Get(tx, bot.Bunt) + // immediatelly set intransaction to block duplicate calls if err != nil { log.Errorf("[cancelInlineReceiveHandler] %s", err) return } + inlineReceive := rn.(*InlineReceive) if c.Sender.ID == inlineReceive.To.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineReceive inactive inlineReceive.Active = false inlineReceive.InTransaction = false - runtime.IgnoreError(bot.Bunt.Set(inlineReceive)) + runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) } return } diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 9247af31..08dbab72 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -3,10 +3,10 @@ package telegram import ( "context" "fmt" - "time" - "github.com/LightningTipBot/LightningTipBot/internal/i18n" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" @@ -20,97 +20,38 @@ var ( ) type InlineSend struct { - Message string `json:"inline_send_message"` - Amount int `json:"inline_send_amount"` - From *lnbits.User `json:"inline_send_from"` - To *tb.User `json:"inline_send_to"` - Memo string `json:"inline_send_memo"` - ID string `json:"inline_send_id"` - Active bool `json:"inline_send_active"` - InTransaction bool `json:"inline_send_intransaction"` - LanguageCode string `json:"languagecode"` -} - -func NewInlineSend() *InlineSend { - inlineSend := &InlineSend{ - Message: "", - Active: true, - InTransaction: false, - } - return inlineSend - -} - -func (msg InlineSend) Key() string { - return msg.ID -} - -func (bot *TipBot) LockInlineSend(tx *InlineSend) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = true - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) ReleaseInlineSend(tx *InlineSend) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = false - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil + *transaction.Base + Message string `json:"inline_send_message"` + Amount int `json:"inline_send_amount"` + From *lnbits.User `json:"inline_send_from"` + To *tb.User `json:"inline_send_to"` + Memo string `json:"inline_send_memo"` + LanguageCode string `json:"languagecode"` } -func (bot *TipBot) InactivateInlineSend(tx *InlineSend) error { - tx.Active = false - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) getInlineSend(c *tb.Callback) (*InlineSend, error) { - inlineSend := NewInlineSend() - inlineSend.ID = c.Data - - err := bot.Bunt.Get(inlineSend) - - // to avoid race conditions, we block the call if there is - // already an active transaction by loop until InTransaction is false - ticker := time.NewTicker(time.Second * 10) - - for inlineSend.InTransaction { - select { - case <-ticker.C: - return nil, fmt.Errorf("inline send %s timeout", inlineSend.ID) - default: - log.Warnf("[getInlineSend] %s in transaction", inlineSend.ID) - time.Sleep(time.Duration(500) * time.Millisecond) - err = bot.Bunt.Get(inlineSend) - } - } - if err != nil { - return nil, fmt.Errorf("could not get inline send %s", inlineSend.ID) - } - - return inlineSend, nil - +func (bot TipBot) makeSendKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { + acceptInlineSendButton := inlineSendMenu.Data(Translate(ctx, "receiveButtonMessage"), "confirm_send_inline") + cancelInlineSendButton := inlineSendMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_send_inline") + acceptInlineSendButton.Data = id + cancelInlineSendButton.Data = id + + inlineSendMenu.Inline( + inlineSendMenu.Row( + acceptInlineSendButton, + cancelInlineSendButton), + ) + return inlineSendMenu } func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { - inlineSend := NewInlineSend() - var err error - inlineSend.Amount, err = decodeAmountFromCommand(q.Text) + // inlineSend := NewInlineSend() + // var err error + amount, err := decodeAmountFromCommand(q.Text) if err != nil { bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQuerySendTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) return } - if inlineSend.Amount < 1 { + if amount < 1 { bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) return } @@ -123,60 +64,48 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { return } // check if fromUser has balance - if balance < inlineSend.Amount { + if balance < amount { log.Errorf("Balance of user %s too low", fromUserStr) - bot.inlineQueryReplyWithError(q, fmt.Sprintf(TranslateUser(ctx, "inlineSendBalanceLowMessage"), balance), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) return } - // check for memo in command - inlineSend.Memo = GetMemoFromCommand(q.Text, 2) - + memo := GetMemoFromCommand(q.Text, 2) urls := []string{ queryImage, } results := make(tb.Results, len(urls)) // []tb.Result for i, url := range urls { - - inlineMessage := fmt.Sprintf(Translate(ctx, "inlineSendMessage"), fromUserStr, inlineSend.Amount) - - if len(inlineSend.Memo) > 0 { - inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineSendAppendMemo"), inlineSend.Memo) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineSendMessage"), fromUserStr, amount) + if len(memo) > 0 { + inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineSendAppendMemo"), memo) } - result := &tb.ArticleResult{ // URL: url, Text: inlineMessage, - Title: fmt.Sprintf(TranslateUser(ctx, "inlineResultSendTitle"), inlineSend.Amount), - Description: fmt.Sprintf(TranslateUser(ctx, "inlineResultSendDescription"), inlineSend.Amount), + Title: fmt.Sprintf(TranslateUser(ctx, "inlineResultSendTitle"), amount), + Description: fmt.Sprintf(TranslateUser(ctx, "inlineResultSendDescription"), amount), // required for photos ThumbURL: url, } - id := fmt.Sprintf("inl-send-%d-%d-%s", q.From.ID, inlineSend.Amount, RandStringRunes(5)) - acceptInlineSendButton := inlineSendMenu.Data(Translate(ctx, "receiveButtonMessage"), "confirm_send_inline") - cancelInlineSendButton := inlineSendMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_send_inline") - acceptInlineSendButton.Data = id - cancelInlineSendButton.Data = id - - inlineSendMenu.Inline( - inlineSendMenu.Row( - acceptInlineSendButton, - cancelInlineSendButton), - ) - result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: inlineSendMenu.InlineKeyboard} - + id := fmt.Sprintf("inl-send-%d-%d-%s", q.From.ID, amount, RandStringRunes(5)) + result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: bot.makeSendKeyboard(ctx, id).InlineKeyboard} results[i] = result - // needed to set a unique string ID for each result results[i].SetResultID(id) // add data to persistent object - inlineSend.Message = inlineMessage - inlineSend.ID = id - inlineSend.From = fromUser - inlineSend.LanguageCode = ctx.Value("publicLanguageCode").(string) + inlineSend := InlineSend{ + Base: transaction.New(transaction.ID(id)), + Message: inlineMessage, + From: fromUser, + Memo: memo, + Amount: amount, + LanguageCode: ctx.Value("publicLanguageCode").(string), + } + // add result to persistent struct - runtime.IgnoreError(bot.Bunt.Set(inlineSend)) + runtime.IgnoreError(inlineSend.Set(inlineSend, bot.Bunt)) } err = bot.Telegram.Answer(q, &tb.QueryResponse{ @@ -191,15 +120,18 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) - - inlineSend, err := bot.getInlineSend(c) + tx := &InlineSend{Base: transaction.New(transaction.ID(c.Data))} + sn, err := tx.Get(tx, bot.Bunt) + // immediatelly set intransaction to block duplicate calls if err != nil { log.Errorf("[acceptInlineSendHandler] %s", err) return } + inlineSend := sn.(*InlineSend) + fromUser := inlineSend.From // immediatelly set intransaction to block duplicate calls - err = bot.LockInlineSend(inlineSend) + err = inlineSend.Lock(inlineSend, bot.Bunt) if err != nil { log.Errorf("[getInlineSend] %s", err) return @@ -209,7 +141,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) return } - defer bot.ReleaseInlineSend(inlineSend) + defer inlineSend.Release(inlineSend, bot.Bunt) amount := inlineSend.Amount @@ -237,7 +169,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) } } // set inactive to avoid double-sends - bot.InactivateInlineSend(inlineSend) + inlineSend.Inactivate(inlineSend, bot.Bunt) // todo: user new get username function to get userStrings transactionMemo := fmt.Sprintf("InlineSend from %s to %s (%d sat).", fromUserStr, toUserStr, amount) @@ -258,11 +190,9 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) if len(memo) > 0 { inlineSend.Message = inlineSend.Message + fmt.Sprintf(i18n.Translate(inlineSend.LanguageCode, "inlineSendAppendMemo"), memo) } - if !to.Initialized { inlineSend.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineSend.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } - bot.tryEditMessage(c.Message, inlineSend.Message, &tb.ReplyMarkup{}) // notify users _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) @@ -275,17 +205,20 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) } func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) { - inlineSend, err := bot.getInlineSend(c) + tx := &InlineSend{Base: transaction.New(transaction.ID(c.Data))} + sn, err := tx.Get(tx, bot.Bunt) + // immediatelly set intransaction to block duplicate calls if err != nil { log.Errorf("[cancelInlineSendHandler] %s", err) return } + inlineSend := sn.(*InlineSend) if c.Sender.ID == inlineSend.From.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineSend inactive inlineSend.Active = false inlineSend.InTransaction = false - runtime.IgnoreError(bot.Bunt.Set(inlineSend)) + runtime.IgnoreError(inlineSend.Set(inlineSend, bot.Bunt)) } return } diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index ef3d4727..d6d68ed9 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -3,16 +3,15 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/str" "strings" - "time" - - log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + "github.com/LightningTipBot/LightningTipBot/internal/str" decodepay "github.com/fiatjaf/ln-decodepay" + log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" ) @@ -31,86 +30,15 @@ func helpPayInvoiceUsage(ctx context.Context, errormsg string) string { } type PayData struct { - From *lnbits.User `json:"from"` - ID string `json:"id"` - Invoice string `json:"invoice"` - Hash string `json:"hash"` - Proof string `json:"proof"` - Memo string `json:"memo"` - Message string `json:"message"` - Amount int64 `json:"amount"` - InTransaction bool `json:"intransaction"` - Active bool `json:"active"` - LanguageCode string `json:"languagecode"` -} - -func NewPay() *PayData { - payData := &PayData{ - Active: true, - InTransaction: false, - } - return payData -} - -func (msg PayData) Key() string { - return msg.ID -} - -func (bot *TipBot) LockPay(tx *PayData) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = true - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) ReleasePay(tx *PayData) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = false - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) InactivatePay(tx *PayData) error { - tx.Active = false - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) getPay(c *tb.Callback) (*PayData, error) { - payData := NewPay() - payData.ID = c.Data - - err := bot.Bunt.Get(payData) - - // to avoid race conditions, we block the call if there is - // already an active transaction by loop until InTransaction is false - ticker := time.NewTicker(time.Second * 10) - - for payData.InTransaction { - select { - case <-ticker.C: - return nil, fmt.Errorf("pay timeout") - default: - log.Infoln("[pay] in transaction") - time.Sleep(time.Duration(500) * time.Millisecond) - err = bot.Bunt.Get(payData) - } - } - if err != nil { - return nil, fmt.Errorf("could not get payData") - } - - return payData, nil - + *transaction.Base + From *lnbits.User `json:"from"` + Invoice string `json:"invoice"` + Hash string `json:"hash"` + Proof string `json:"proof"` + Memo string `json:"memo"` + Message string `json:"message"` + Amount int64 `json:"amount"` + LanguageCode string `json:"languagecode"` } // payHandler invoked on "/pay lnbc..." command @@ -184,18 +112,16 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { // object that holds all information about the send payment id := fmt.Sprintf("pay-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) payData := PayData{ - From: user, - Invoice: paymentRequest, - Active: true, - InTransaction: false, - ID: id, - Amount: int64(amount), - Memo: bolt11.Description, - Message: confirmText, - LanguageCode: ctx.Value("publicLanguageCode").(string), + From: user, + Invoice: paymentRequest, + Base: transaction.New(transaction.ID(id)), + Amount: int64(amount), + Memo: bolt11.Description, + Message: confirmText, + LanguageCode: ctx.Value("publicLanguageCode").(string), } // add result to persistent struct - runtime.IgnoreError(bot.Bunt.Set(payData)) + runtime.IgnoreError(payData.Set(payData, bot.Bunt)) SetUserState(user, bot, lnbits.UserStateConfirmPayment, paymentRequest) @@ -215,28 +141,32 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { // confirmPayHandler when user clicked pay on payment confirmation func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { - payData, err := bot.getPay(c) + tx := &PayData{Base: transaction.New(transaction.ID(c.Data))} + sn, err := tx.Get(tx, bot.Bunt) + // immediatelly set intransaction to block duplicate calls if err != nil { - log.Errorf("[acceptSendHandler] %s", err) + log.Errorf("[confirmPayHandler] %s", err) return } + payData := sn.(*PayData) + // onnly the correct user can press if payData.From.Telegram.ID != c.Sender.ID { return } // immediatelly set intransaction to block duplicate calls - err = bot.LockPay(payData) + err = payData.Lock(payData, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) bot.tryDeleteMessage(c.Message) return } if !payData.Active { - log.Errorf("[acceptSendHandler] send not active anymore") + log.Errorf("[confirmPayHandler] send not active anymore") bot.tryDeleteMessage(c.Message) return } - defer bot.ReleasePay(payData) + defer payData.Release(payData, bot.Bunt) // remove buttons from confirmation message // bot.tryEditMessage(c.Message, MarkdownEscape(payData.Message), &tb.ReplyMarkup{}) @@ -284,18 +214,20 @@ func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { func (bot TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { // reset state immediately user := LoadUser(ctx) - ResetUserState(user, bot) - payData, err := bot.getPay(c) + tx := &PayData{Base: transaction.New(transaction.ID(c.Data))} + sn, err := tx.Get(tx, bot.Bunt) + // immediatelly set intransaction to block duplicate calls if err != nil { - log.Errorf("[acceptSendHandler] %s", err) + log.Errorf("[cancelPaymentHandler] %s", err) return } + payData := sn.(*PayData) // onnly the correct user can press if payData.From.Telegram.ID != c.Sender.ID { return } bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) payData.InTransaction = false - bot.InactivatePay(payData) + payData.Inactivate(payData, bot.Bunt) } diff --git a/internal/telegram/send.go b/internal/telegram/send.go index ac8c74db..82977c8b 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -4,13 +4,13 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/str" "strings" - "time" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" @@ -39,87 +39,16 @@ func (bot *TipBot) SendCheckSyntax(ctx context.Context, m *tb.Message) (bool, st } type SendData struct { - ID string `json:"id"` + *transaction.Base From *lnbits.User `json:"from"` ToTelegramId int `json:"to_telegram_id"` ToTelegramUser string `json:"to_telegram_user"` Memo string `json:"memo"` Message string `json:"message"` Amount int64 `json:"amount"` - InTransaction bool `json:"intransaction"` - Active bool `json:"active"` LanguageCode string `json:"languagecode"` } -func NewSend() *SendData { - sendData := &SendData{ - Active: true, - InTransaction: false, - } - return sendData -} - -func (msg SendData) Key() string { - return msg.ID -} - -func (bot *TipBot) LockSend(tx *SendData) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = true - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) ReleaseSend(tx *SendData) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = false - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) InactivateSend(tx *SendData) error { - tx.Active = false - err := bot.Bunt.Set(tx) - if err != nil { - return err - } - return nil -} - -func (bot *TipBot) getSend(c *tb.Callback) (*SendData, error) { - sendData := NewSend() - sendData.ID = c.Data - - err := bot.Bunt.Get(sendData) - - // to avoid race conditions, we block the call if there is - // already an active transaction by loop until InTransaction is false - ticker := time.NewTicker(time.Second * 10) - - for sendData.InTransaction { - select { - case <-ticker.C: - return nil, fmt.Errorf("send timeout") - default: - log.Infoln("[send] in transaction") - time.Sleep(time.Duration(500) * time.Millisecond) - err = bot.Bunt.Get(sendData) - } - } - if err != nil { - return nil, fmt.Errorf("could not get sendData") - } - - return sendData, nil - -} - // sendHandler invoked on "/send 123 @user" command func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) @@ -224,9 +153,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { id := fmt.Sprintf("send-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) sendData := SendData{ From: user, - Active: true, - InTransaction: false, - ID: id, + Base: transaction.New(transaction.ID(id)), Amount: int64(amount), ToTelegramId: toUserDb.Telegram.ID, ToTelegramUser: toUserStrWithoutAt, @@ -235,7 +162,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { LanguageCode: ctx.Value("publicLanguageCode").(string), } // save persistent struct - runtime.IgnoreError(bot.Bunt.Set(sendData)) + runtime.IgnoreError(sendData.Set(sendData, bot.Bunt)) sendDataJson, err := json.Marshal(sendData) if err != nil { @@ -266,17 +193,19 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // sendHandler invoked when user clicked send on payment confirmation func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { - sendData, err := bot.getSend(c) + tx := &SendData{Base: transaction.New(transaction.ID(c.Data))} + sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) return } + sendData := sn.(*SendData) // onnly the correct user can press if sendData.From.Telegram.ID != c.Sender.ID { return } // immediatelly set intransaction to block duplicate calls - err = bot.LockSend(sendData) + err = sendData.Lock(sendData, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) bot.tryDeleteMessage(c.Message) @@ -284,10 +213,10 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { } if !sendData.Active { log.Errorf("[acceptSendHandler] send not active anymore") - bot.tryDeleteMessage(c.Message) + // bot.tryDeleteMessage(c.Message) return } - defer bot.ReleaseSend(sendData) + defer sendData.Release(sendData, bot.Bunt) // // remove buttons from confirmation message // bot.tryEditMessage(c.Message, MarkdownEscape(sendData.Message), &tb.ReplyMarkup{}) @@ -327,11 +256,10 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { bot.tryEditMessage(c.Message, fmt.Sprintf("%s %s", i18n.Translate(sendData.LanguageCode, "sendErrorMessage"), err), &tb.ReplyMarkup{}) return } + sendData.Inactivate(sendData, bot.Bunt) log.Infof("[send] Transaction sent from %s to %s (%d sat).", fromUserStr, toUserStr, amount) - sendData.InTransaction = false - // notify to user bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) // bot.trySendMessage(from.Telegram, fmt.Sprintf(Translate(ctx, "sendSentMessage"), amount, toUserStrMd)) @@ -356,11 +284,13 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { // reset state immediately user := LoadUser(ctx) ResetUserState(user, *bot) - sendData, err := bot.getSend(c) + tx := &SendData{Base: transaction.New(transaction.ID(c.Data))} + sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) return } + sendData := sn.(*SendData) // onnly the correct user can press if sendData.From.Telegram.ID != c.Sender.ID { return @@ -368,5 +298,5 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { // remove buttons from confirmation message bot.tryEditMessage(c.Message, i18n.Translate(sendData.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) sendData.InTransaction = false - bot.InactivateSend(sendData) + sendData.Inactivate(sendData, bot.Bunt) } diff --git a/internal/telegram/start.go b/internal/telegram/start.go index efa98ef9..fbc880f5 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/LightningTipBot/LightningTipBot/internal" "strconv" + "time" log "github.com/sirupsen/logrus" @@ -97,6 +98,7 @@ func (bot TipBot) createWallet(user *lnbits.User) error { } user.Wallet = &wallet[0] user.Initialized = false + user.CreatedAt = time.Now() err = UpdateUserRecord(user, bot) if err != nil { errormsg := fmt.Sprintf("[createWallet] Update user record error: %s", err) diff --git a/internal/telegram/tooltip.go b/internal/telegram/tooltip.go index 184d998e..b7697703 100644 --- a/internal/telegram/tooltip.go +++ b/internal/telegram/tooltip.go @@ -8,7 +8,6 @@ import ( "time" "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/tidwall/buntdb" "github.com/tidwall/gjson" @@ -160,8 +159,8 @@ func (ttt *TipTooltip) updateTooltip(bot *TipBot, user *tb.User, amount int, not // tipTooltipInitializedHandler is called when the user initializes the wallet func tipTooltipInitializedHandler(user *tb.User, bot TipBot) { runtime.IgnoreError(bot.Bunt.View(func(tx *buntdb.Tx) error { - err := tx.Ascend(storage.MessageOrderedByReplyToFrom, func(key, value string) bool { - replyToUserId := gjson.Get(value, storage.MessageOrderedByReplyToFrom) + err := tx.Ascend(MessageOrderedByReplyToFrom, func(key, value string) bool { + replyToUserId := gjson.Get(value, MessageOrderedByReplyToFrom) if replyToUserId.String() == strconv.Itoa(user.ID) { log.Debugln("loading persistent tip tool tip messages") ttt := &TipTooltip{} @@ -182,7 +181,7 @@ func tipTooltipInitializedHandler(user *tb.User, bot TipBot) { } func (ttt TipTooltip) Key() string { - return strconv.Itoa(ttt.Message.Message.ReplyTo.ID) + return fmt.Sprintf("tip-tool-tip:%s", strconv.Itoa(ttt.Message.Message.ReplyTo.ID)) } // editTooltip updates the tooltip message with the new tip amount and tippers and edits it diff --git a/internal/telegram/users.go b/internal/telegram/users.go index 2062984a..51abbc2e 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -106,7 +106,7 @@ func (bot *TipBot) CreateWalletForTelegramUser(tbUser *tb.User) (*lnbits.User, e func (bot *TipBot) UserExists(user *tb.User) (*lnbits.User, bool) { lnbitUser, err := GetUser(user, *bot) - if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { + if err != nil || errors.Is(err, gorm.ErrRecordNotFound) { return nil, false } return lnbitUser, true diff --git a/translations/de.toml b/translations/de.toml index 792b9633..3fd313ba 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -279,7 +279,7 @@ inlineSendCreateWalletMessage = """Chatte mit %s 👈 um dein Wallet zu verwalte sendYourselfMessage = """📖 Du kannst dich nicht selbst bezahlen.""" inlineSendFailedMessage = """🚫 Senden fehlgeschlagen.""" inlineSendInvalidAmountMessage = """🚫 Betrag muss größer als 0 sein.""" -inlineSendBalanceLowMessage = """🚫 Dein Guthaben ist zu niedrig (%d sat).""" +inlineSendBalanceLowMessage = """🚫 Dein Guthaben reicht nicht aus.""" # INLINE RECEIVE diff --git a/translations/en.toml b/translations/en.toml index b10063b1..76da9cb4 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -279,7 +279,7 @@ inlineSendCreateWalletMessage = """Chat with %s 👈 to manage your wallet.""" sendYourselfMessage = """📖 You can't pay to yourself.""" inlineSendFailedMessage = """🚫 Send failed.""" inlineSendInvalidAmountMessage = """🚫 Amount must be larger than 0.""" -inlineSendBalanceLowMessage = """🚫 Your balance is too low (%d sat).""" +inlineSendBalanceLowMessage = """🚫 Your balance is too low.""" # INLINE RECEIVE diff --git a/translations/es.toml b/translations/es.toml index f2222845..f81fbd10 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -279,7 +279,7 @@ inlineSendCreateWalletMessage = """Chatea con %s 👈 para gestionar tu cartera sendYourselfMessage = """📖 No puedes pagarte a ti mismo.""" inlineSendFailedMessage = """🚫 Envío fallido.""" inlineSendInvalidAmountMessage = """🚫 La cantidad debe ser mayor que 0.""" -inlineSendBalanceLowMessage = """🚫 Tu saldo es demasiado bajo (%d sat).""" +inlineSendBalanceLowMessage = """🚫 Tu saldo es demasiado bajo.""" # INLINE RECEIVE diff --git a/translations/nl.toml b/translations/nl.toml index 47f94435..a13ff3be 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -279,7 +279,7 @@ inlineSendCreateWalletMessage = """Chat met %s 👈 om uw wallet te beheren.""" sendYourselfMessage = """📖 Je kunt niet aan jezelf betalen.""" inlineSendFailedMessage = """🚫 verzenden mislukt.""" inlineSendInvalidAmountMessage = """🚫 Bedrag moet groter zijn dan 0.""" -inlineSendBalanceLowMessage = """🚫 Uw saldo is te laag (%d zat).""" +inlineSendBalanceLowMessage = """🚫 Uw saldo is te laag.""" # INLINE RECEIVE From 8c7c22b0d820a337710ed83636bcfe7659a72de2 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 8 Oct 2021 09:35:46 +0300 Subject: [PATCH 018/541] price checker (#90) * price checker * more currencies * debug messages --- internal/price/price.go | 114 +++++++++++++++++++++++++++++++++++ internal/telegram/amounts.go | 17 ++++++ main.go | 5 +- 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 internal/price/price.go diff --git a/internal/price/price.go b/internal/price/price.go new file mode 100644 index 00000000..4a7eaaec --- /dev/null +++ b/internal/price/price.go @@ -0,0 +1,114 @@ +package price + +import ( + "fmt" + "io/ioutil" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" +) + +type PriceWatcher struct { + client *http.Client + UpdateInterval time.Duration + Currencies map[string]string + Exchanges map[string]func(string) (float64, error) +} + +var ( + Price map[string]float64 + P *PriceWatcher +) + +func NewPriceWatcher() *PriceWatcher { + pricewatcher := &PriceWatcher{ + client: &http.Client{ + Timeout: time.Second * time.Duration(5), + }, + Currencies: map[string]string{"EUR": "€", "GBP": "£", "JPY": "¥", "BRL": "R$", "USD": "$", "RUB": "₽", "TRY": "₺"}, + Exchanges: make(map[string]func(string) (float64, error), 0), + UpdateInterval: time.Second * time.Duration(30), + } + pricewatcher.Exchanges["coinbase"] = pricewatcher.GetCoinbasePrice + pricewatcher.Exchanges["bitfinex"] = pricewatcher.GetBitfinexPrice + Price = make(map[string]float64, 0) + log.Infof("[PriceWatcher] Watcher started") + P = pricewatcher + return pricewatcher +} + +func (p *PriceWatcher) Start() { + go p.Watch() +} + +func (p *PriceWatcher) Watch() error { + for { + for currency, _ := range p.Currencies { + avg_price := 0.0 + n_responses := 0 + for exchange, getPrice := range p.Exchanges { + fprice, err := getPrice(currency) + if err != nil { + // log.Debug(err) + // if one exchanges is down, use the next + continue + } + n_responses++ + avg_price += fprice + log.Debugf("[PriceWatcher] %s %s price: %f", exchange, currency, fprice) + time.Sleep(time.Second * time.Duration(2)) + } + Price[currency] = avg_price / float64(n_responses) + log.Debugf("[PriceWatcher] Average %s price: %f", currency, Price[currency]) + } + time.Sleep(p.UpdateInterval) + } +} + +func (p *PriceWatcher) GetCoinbasePrice(currency string) (float64, error) { + coinbaseEndpoint, err := url.Parse(fmt.Sprintf("https://api.coinbase.com/v2/prices/spot?currency=%s", currency)) + response, err := p.client.Get(coinbaseEndpoint.String()) + if err, ok := err.(net.Error); ok && err.Timeout() { + return 0, err + } + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Debug(err) + return 0, err + } + price := gjson.Get(string(bodyBytes), "data.amount") + fprice, err := strconv.ParseFloat(strings.TrimSpace(price.String()), 64) + if err != nil { + log.Debug(err) + return 0, err + } + return fprice, nil +} + +func (p *PriceWatcher) GetBitfinexPrice(currency string) (float64, error) { + var bitfinexCurrencyToPair = map[string]string{"USD": "btcusd", "EUR": "btceur", "GBP": "btcusd", "JPY": "btcjpy", "BRL": "btcbrl", "RUB": "btcrub", "TRY": "btctry"} + pair := bitfinexCurrencyToPair[currency] + bitfinexEndpoint, err := url.Parse(fmt.Sprintf("https://api.bitfinex.com/v1/pubticker/%s", pair)) + response, err := p.client.Get(bitfinexEndpoint.String()) + if err, ok := err.(net.Error); ok && err.Timeout() { + return 0, err + } + bodyBytes, err := ioutil.ReadAll(response.Body) + if err != nil { + log.Debug(err) + return 0, err + } + price := gjson.Get(string(bodyBytes), "last_price") + fprice, err := strconv.ParseFloat(strings.TrimSpace(price.String()), 64) + if err != nil { + log.Debug(err) + return 0, err + } + return fprice, nil +} diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 33faa03c..9eba12a7 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -4,6 +4,9 @@ import ( "errors" "strconv" "strings" + + "github.com/LightningTipBot/LightningTipBot/internal/price" + log "github.com/sirupsen/logrus" ) func getArgumentFromCommand(input string, which int) (output string, err error) { @@ -35,6 +38,20 @@ func getAmount(input string) (amount int, err error) { return amount, err } + // convert fiat currencies to satoshis + for currency, symbol := range price.P.Currencies { + if strings.Contains(input, symbol) { + fmount, err := strconv.ParseFloat(strings.Replace(input, symbol, "", 1), 64) + if err != nil { + log.Errorln(err) + return 0, err + } + amount = int(fmount / price.Price[currency] * float64(100_000_000)) + return amount, nil + } + } + + // use plain integer as satoshis amount, err = strconv.Atoi(input) if err != nil { return 0, err diff --git a/main.go b/main.go index 275918e1..685d3733 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,13 @@ package main import ( + "runtime/debug" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits/webhook" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" + "github.com/LightningTipBot/LightningTipBot/internal/price" "github.com/LightningTipBot/LightningTipBot/internal/telegram" log "github.com/sirupsen/logrus" - "runtime/debug" ) // setLogger will initialize the log format @@ -24,6 +26,7 @@ func main() { bot := telegram.NewBot() webhook.NewServer(&bot) lnurl.NewServer(&bot) + price.NewPriceWatcher().Start() bot.Start() } From a880d7b0448ca12bd4594b92b9924f582b9f550a Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Fri, 8 Oct 2021 08:42:19 +0200 Subject: [PATCH 019/541] Contains -> HasPrefix || HasSuffix (#91) Co-authored-by: LightningTipBot --- internal/telegram/amounts.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 9eba12a7..0f04ecea 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -29,7 +29,7 @@ func decodeAmountFromCommand(input string) (amount int, err error) { func getAmount(input string) (amount int, err error) { // convert something like 1.2k into 1200 - if strings.HasSuffix(input, "k") { + if strings.HasSuffix(strings.ToLower(input), "k") { fmount, err := strconv.ParseFloat(strings.TrimSpace(input[:len(input)-1]), 64) if err != nil { return 0, err @@ -40,7 +40,7 @@ func getAmount(input string) (amount int, err error) { // convert fiat currencies to satoshis for currency, symbol := range price.P.Currencies { - if strings.Contains(input, symbol) { + if strings.HasPrefix(input, symbol) || strings.HasSuffix(input, symbol) { fmount, err := strconv.ParseFloat(strings.Replace(input, symbol, "", 1), 64) if err != nil { log.Errorln(err) From 61bc3710c05bc7e9a294f2272674c42b9736e8d6 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 8 Oct 2021 10:45:29 +0300 Subject: [PATCH 020/541] Fiat price hotfix2 (#92) * INR added * also accept currency string --- internal/price/price.go | 15 +++++++++++++-- internal/telegram/amounts.go | 9 +++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/internal/price/price.go b/internal/price/price.go index 4a7eaaec..b146be8d 100644 --- a/internal/price/price.go +++ b/internal/price/price.go @@ -31,7 +31,18 @@ func NewPriceWatcher() *PriceWatcher { client: &http.Client{ Timeout: time.Second * time.Duration(5), }, - Currencies: map[string]string{"EUR": "€", "GBP": "£", "JPY": "¥", "BRL": "R$", "USD": "$", "RUB": "₽", "TRY": "₺"}, + // attention: $ must come after other $-denominated currencies like R$ + Currencies: map[string]string{ + "EUR": "€", + "GBP": "£", + "JPY": "¥", + "BRL": "R$", + "MXN": "MX$", + "USD": "$", + "RUB": "₽", + "TRY": "₺", + "INR": "₹", + }, Exchanges: make(map[string]func(string) (float64, error), 0), UpdateInterval: time.Second * time.Duration(30), } @@ -92,7 +103,7 @@ func (p *PriceWatcher) GetCoinbasePrice(currency string) (float64, error) { } func (p *PriceWatcher) GetBitfinexPrice(currency string) (float64, error) { - var bitfinexCurrencyToPair = map[string]string{"USD": "btcusd", "EUR": "btceur", "GBP": "btcusd", "JPY": "btcjpy", "BRL": "btcbrl", "RUB": "btcrub", "TRY": "btctry"} + var bitfinexCurrencyToPair = map[string]string{"USD": "btcusd", "EUR": "btceur", "GBP": "btcusd", "JPY": "btcjpy"} pair := bitfinexCurrencyToPair[currency] bitfinexEndpoint, err := url.Parse(fmt.Sprintf("https://api.bitfinex.com/v1/pubticker/%s", pair)) response, err := p.client.Get(bitfinexEndpoint.String()) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 0f04ecea..3a30147d 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -40,8 +40,13 @@ func getAmount(input string) (amount int, err error) { // convert fiat currencies to satoshis for currency, symbol := range price.P.Currencies { - if strings.HasPrefix(input, symbol) || strings.HasSuffix(input, symbol) { - fmount, err := strconv.ParseFloat(strings.Replace(input, symbol, "", 1), 64) + if strings.HasPrefix(input, symbol) || strings.HasSuffix(input, symbol) || // for 1$ and $1 + strings.HasPrefix(strings.ToLower(input), strings.ToLower(currency)) || // for USD1 + strings.HasSuffix(strings.ToLower(input), strings.ToLower(currency)) { // for 1USD + numeric_string := "" + numeric_string = strings.Replace(input, symbol, "", 1) // for symbol like $ + numeric_string = strings.Replace(strings.ToLower(numeric_string), strings.ToLower(currency), "", 1) // for 1USD + fmount, err := strconv.ParseFloat(numeric_string, 64) if err != nil { log.Errorln(err) return 0, err From 7c77d268f65c7322321219c2870923537a6ce3d2 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 8 Oct 2021 10:51:43 +0300 Subject: [PATCH 021/541] Fiat price hotfix2 (#93) * INR added * also accept currency string * dividion by 0 --- internal/telegram/amounts.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 3a30147d..0db7feac 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -51,6 +51,9 @@ func getAmount(input string) (amount int, err error) { log.Errorln(err) return 0, err } + if price.Price[currency] == 0 { + return 0, errors.New("price is zero") + } amount = int(fmount / price.Price[currency] * float64(100_000_000)) return amount, nil } From 08c7ff51130cea344f855c7e7785d72bbbdc1274 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 8 Oct 2021 10:53:33 +0300 Subject: [PATCH 022/541] Fiat price hotfix2 (#94) * INR added * also accept currency string * dividion by 0 * better check --- internal/telegram/amounts.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 0db7feac..e30728e2 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -51,7 +51,7 @@ func getAmount(input string) (amount int, err error) { log.Errorln(err) return 0, err } - if price.Price[currency] == 0 { + if !(price.Price[currency] > 0) { return 0, errors.New("price is zero") } amount = int(fmount / price.Price[currency] * float64(100_000_000)) From 887751cb0fc7df32699de2ab7b0b52f8bd284378 Mon Sep 17 00:00:00 2001 From: Nicolas Bustamante <92287725+nbstme@users.noreply.github.com> Date: Sun, 10 Oct 2021 23:07:44 -0700 Subject: [PATCH 023/541] small typo (#96) --- translations/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/translations/README.md b/translations/README.md index 765f84a9..5e50afdb 100644 --- a/translations/README.md +++ b/translations/README.md @@ -6,7 +6,7 @@ Thank you for helping to translate this bot into many different languages. If yo * Duplicate `en.toml` to your localization and edit string by string. * Do not translate commands in the text! I.e. `/balance` stays `/balance`. * Pay attention to every single `"""` and `%s` or `%d`. -* Your end resiult should have exactly the same number of lines as `en.toml`. +* Your end result should have exactly the same number of lines as `en.toml`. * Start sentences with `C`apital letters, end them with a full stop`.` ## General @@ -45,4 +45,4 @@ Thank you for helping to translate this bot into many different languages. If yo ## GitHub infos * Please submit translations as a GitHub pull-request. This way, you can easily work with others and review each other. To submit a pull-request, you need a Github account. Then, fork the entire project (using the button in the upper-right corner). * Then, create a new branch for your translation. Do this using the Github UI or via the terminal inside the project repository: `git checkout -b translation_es` for example. Then, create the appropriate language file and put it in the translations folder. Then, add it to the branch with by navigating to the translations folder `git add es.toml` and `git commit -m 'add spanish'`. Finally, push the branch to your fork `git push --set-upstream origin translation_es`. When done, open a pull-request in the *original github repo* and select your forked branch. -* Good luck :) \ No newline at end of file +* Good luck :) From 693f12470c68fc68d4438c7456f06c9a2c11775c Mon Sep 17 00:00:00 2001 From: Bas Simons Date: Mon, 11 Oct 2021 08:08:37 +0200 Subject: [PATCH 024/541] Better dutch translations (#95) * Update nl.toml better dutch * Update nl.toml --- translations/nl.toml | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/translations/nl.toml b/translations/nl.toml index a13ff3be..ee7089c0 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -51,10 +51,10 @@ revealButtonMessage = """Laat zijn""" showButtonMessage = """Toon""" hideButtonMessage = """Verbergen""" joinButtonMessage = """Deelnemen""" -optionsButtonMessage = """Ppties""" +optionsButtonMessage = """Opties""" settingsButtonMessage = """Instellingen""" saveButtonMessage = """Save""" -deleteButtonMessage = """Serwijderen""" +deleteButtonMessage = """Verwijderen""" infoButtonMessage = """Info""" # HELP @@ -68,10 +68,10 @@ _Deze bot brengt geen kosten in rekening maar kost Satoshis om te werken. Als je %s ⚙️ *opdrachten* -*/tip* 🏅 Antwoord op een bericht aan tip: `/tip []` +*/tip* 🏅 Antwoord op een bericht aan tip: `/tip []` */balance* 👑 Controleer uw saldo: `/balance` -*/send* 💸 Stuur geld naar een gebruiker: `/send @user or user@ln.tips []` -*/invoice* ⚡️ Ontvang met Lightning: `/invoice []` +*/send* 💸 Stuur geld naar een gebruiker: `/send @user of user@ln.tips []` +*/invoice* ⚡️ Ontvang met Lightning: `/invoice []` */pay* ⚡️ Betaal met Lightning: `/pay ` */donate* ❤️ Doneer aan het project: `/donate 1000` */advanced* 🤖 Geavanceerde functies. @@ -81,7 +81,7 @@ infoHelpMessage = """ℹ️ *Info*""" infoYourLightningAddress = """Uw Lightning adres is `%s`""" basicsMessage = """🧡 *Bitcoin* -_Bitcoin is de munteenheid van het internet. Het is zonder toestemming en gedecentraliseerd en heeft geen meesters en geen controlerende autoriteit. Bitcoin is gezond geld dat sneller, veiliger en inclusiever is dan het oude financiële systeem. +_Bitcoin is de munteenheid van het internet. Het functioneert zonder toestemming, is gedecentraliseerd en heeft geen meesters of een controlerende autoriteit. Bitcoin is gezond geld dat sneller, veiliger en inclusiever is dan het oude financiële systeem. 🧮 *Economie* _De kleinste eenheid van Bitcoin zijn Satoshis (sat) en 100.000.000 sat = 1 Bitcoin. Er zullen ooit maar 21 miljoen Bitcoin zijn. De fiatvalutawaarde van Bitcoin kan dagelijks veranderen. Echter, als u leeft op een Bitcoin-standaard zal 1 sat altijd gelijk zijn aan 1 sat._ @@ -146,14 +146,14 @@ tipErrorMessage = """🚫 Tip mislukt.""" tipUndefinedErrorMsg = """probeer het later nog eens""" tipHelpText = """📖 Oeps, dat werkte niet. %s -*Usage:* `/tip []` +*Usage:* `/tip []` *Example:* `/tip 1000 Dank meme!`""" # SEND sendValidAmountMessage = """Heb je een geldig bedrag ingevoerd?""" sendUserHasNoWalletMessage = """🚫 Gebruiker %s heeft nog geen portemonnee aangemaakt.""" -sendSentMessage = """💸 %d zat verstuurd naar %s.""" +sendSentMessage = """💸 %d sat verstuurd naar %s.""" sendPublicSentMessage = """💸 %d sat verstuurd van %s naar %s.""" sendReceivedMessage = """🏅 %s stuurde u %d sat.""" sendErrorMessage = """🚫 Verzenden mislukt.""" @@ -164,7 +164,7 @@ errorTryLaterMessage = """🚫 Fout. Probeer het later nog eens.""" sendSyntaxErrorMessage = """Heb je een bedrag en een ontvanger ingevoerd? U kunt het /send commando gebruiken om ofwel naar Telegram gebruikers te sturen zoals %s of naar een Lightning adres zoals LightningTipBot@ln.tips.""" sendHelpText = """📖 Oeps, dat werkte niet. %s -*Usage:* `/send []` +*Usage:* `/send []` *Example:* `/send 1000 @LightningTipBot Ik hou gewoon van de bot ❤️` *Example:* `/send 1234 LightningTipBot@ln.tips`""" @@ -175,7 +175,7 @@ invoiceEnterAmountMessage = """Heb je een bedrag ingevoerd?""" invoiceValidAmountMessage = """Heeft u een geldig bedrag ingevoerd?""" invoiceHelpText = """📖 Oeps, dat werkte niet. %s -*Usage:* `/invoice []` +*Usage:* `/invoice []` *Example:* `/invoice 1000 Dank je!`""" # PAY @@ -218,11 +218,11 @@ photoQrRecognizedMessage = """✅ QR code: # LNURL lnurlReceiveInfoText = """👇 U kunt deze statische LNURL gebruiken om betalingen te ontvangen.""" -lnurlResolvingUrlMessage = """🧮 Oplossen adres...""" +lnurlResolvingUrlMessage = """🧮 Opzoeken adres...""" lnurlGettingUserMessage = """🧮 Voorbereiding van betaling...""" lnurlPaymentFailed = """🚫 Betaling mislukt: %s""" lnurlInvalidAmountMessage = """🚫 Ongeldig bedrag.""" -lnurlInvalidAmountRangeMessage = """🚫 Bedrag moet liggen tussen %d en %d zat.""" +lnurlInvalidAmountRangeMessage = """🚫 Bedrag moet liggen tussen %d en %d sat.""" lnurlNoUsernameMessage = """🚫 U moet een Telegram gebruikersnaam instellen om betalingen te ontvangen via LNURL.""" lnurlEnterAmountMessage = """⌨️ Voer een bedrag in tussen %d en %d sat.""" lnurlHelpText = """📖 Oeps, dat werkte niet. %s @@ -243,7 +243,7 @@ couldNotLinkMessage = """🚫 Uw wallet kon niet gelinkt worden. Probeer het lat # FAUCET inlineQueryFaucetTitle = """🚰 Maak een kraan.""" -inlineQueryFaucetDescription = """Gebruik: @%s tapkrann """ +inlineQueryFaucetDescription = """Gebruik: @%s faucet """ inlineResultFaucetTitle = """🚰 Maak een %d sat kraan.""" inlineResultFaucetDescription = """👉 Klik hier om een kraan in deze chat te maken..""" @@ -251,19 +251,19 @@ inlineFaucetMessage = """Druk op ✅ om %d sat te verzamelen 🚰 Remaining: %d/%d sat (given to %d/%d users) %s""" -inlineFaucetEndedMessage = """🚰 kraan leeg 🍺🏅 %d zat gegeven aan %d gebruikers.""" +inlineFaucetEndedMessage = """🚰 kraan leeg 🍺🏅 %d sat gegeven aan %d gebruikers.""" inlineFaucetAppendMemo = """\n✉️ %s""" inlineFaucetCreateWalletMessage = """Chat met %s 👈 om uw wallet te beheren.""" inlineFaucetCancelledMessage = """🚫 kraan geannuleerd.""" inlineFaucetInvalidPeruserAmountMessage = """🚫 Per gebruiker bedrag niet deelbaar van capaciteit.""" inlineFaucetInvalidAmountMessage = """🚫 Ongeldig bedrag.""" -inlineFaucetSentMessage = """🚰 %d zat gestuurd naar %s.""" +inlineFaucetSentMessage = """🚰 %d sat gestuurd naar %s.""" inlineFaucetReceivedMessage = """🚰 %s stuurde je %d sat.""" inlineFaucetHelpFaucetInGroup = """Maak een kraan in een groep met de bot erin of gebruik 👉 inline commando (/advanced voor meer).""" inlineFaucetHelpText = """📖 Oeps, dat werkte niet. %s -*Usage:* `/kraan ` -*Example:* `/kraan 210 21`""" +*Usage:* `/faucet ` +*Example:* `/faucet 210 21`""" # INLINE VERZENDEN @@ -284,8 +284,8 @@ inlineSendBalanceLowMessage = """🚫 Uw saldo is te laag.""" # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Vraag een betaling in een chat.""" -inlineQueryReceiveDescription = """Gebruik: @%s ontvangt []""" -inlineResultReceiveTitle = """🏅 Ontvang %d zat.""" +inlineQueryReceiveDescription = """Gebruik: @%s receive []""" +inlineResultReceiveTitle = """🏅 Ontvang %d sat.""" inlineResultReceiveDescription = """👉 Klik om een betaling van %d sat aan te vragen.""" inlineReceiveMessage = """Druk op 💸 om te betalen aan %s.\n💸 Bedrag: %d sat""" From 88fdb4de9b4fcc801baab15ee1032b268aaca77f Mon Sep 17 00:00:00 2001 From: LightningTipBot Date: Mon, 11 Oct 2021 18:20:25 +0300 Subject: [PATCH 025/541] small fix --- internal/telegram/inline_receive.go | 2 +- translations/es.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index cfece2bb..efdb2313 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -63,7 +63,7 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { for i, url := range urls { inlineMessage := fmt.Sprintf(Translate(ctx, "inlineReceiveMessage"), fromUserStr, amount) if len(memo) > 0 { - inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineReceiveAppendMemo"), amount) + inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineReceiveAppendMemo"), memo) } result := &tb.ArticleResult{ // URL: url, diff --git a/translations/es.toml b/translations/es.toml index f81fbd10..1db4ad4d 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -170,7 +170,7 @@ sendHelpText = """📖 Oops, eso no funcionó. %s # INVOICE -invoiceReceivedMessage = """⚡️ Recibiste %d sat""" +invoiceReceivedMessage = """⚡️ Recibiste %d sat.""" invoiceEnterAmountMessage = """¿Ingresaste un monto?""" invoiceValidAmountMessage = """¿Ingresaste un monto válido?""" invoiceHelpText = """📖 Oops, eso no funcionó. %s From c7212131faea922bba417cc50a13d45e7f1383ed Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 13 Oct 2021 08:43:37 +0300 Subject: [PATCH 026/541] Fix fr.toml (#99) * Create fr.toml French translation * Update with correct line breaks * typo change * fix mistakes, enable fr.toml Co-authored-by: Nicolas Bustamante <92287725+nbstme@users.noreply.github.com> --- internal/i18n/localize.go | 1 + translations/en.toml | 2 +- translations/es.toml | 2 +- translations/fr.toml | 297 ++++++++++++++++++++++++++++++++++++++ translations/it.toml | 2 +- translations/nl.toml | 2 +- 6 files changed, 302 insertions(+), 4 deletions(-) create mode 100644 translations/fr.toml diff --git a/internal/i18n/localize.go b/internal/i18n/localize.go index 4b8ef68e..830bbfec 100644 --- a/internal/i18n/localize.go +++ b/internal/i18n/localize.go @@ -21,6 +21,7 @@ func RegisterLanguages() *i18n.Bundle { bundle.LoadMessageFile("translations/it.toml") bundle.LoadMessageFile("translations/es.toml") bundle.LoadMessageFile("translations/nl.toml") + bundle.LoadMessageFile("translations/fr.toml") return bundle } func Translate(languageCode string, MessgeID string) string { diff --git a/translations/en.toml b/translations/en.toml index 76da9cb4..4b1e1cb9 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -143,7 +143,7 @@ tipYourselfMessage = """📖 You can't tip yourself.""" tipSentMessage = """💸 %d sat sent to %s.""" tipReceivedMessage = """🏅 %s has tipped you %d sat.""" tipErrorMessage = """🚫 Tip failed.""" -tipUndefinedErrorMsg = """please try again later""" +tipUndefinedErrorMsg = """please try again later.""" tipHelpText = """📖 Oops, that didn't work. %s *Usage:* `/tip []` diff --git a/translations/es.toml b/translations/es.toml index 1db4ad4d..542f5189 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -143,7 +143,7 @@ tipYourselfMessage = """📖 No te puedes dar tips a ti mismo.""" tipSentMessage = """💸 %d sat enviado a %s.""" tipReceivedMessage = """🏅 %s te ha dado un tip de %d sat.""" tipErrorMessage = """🚫 El tip ha fallado.""" -tipUndefinedErrorMsg = """por favor, inténtalo más tarde""" +tipUndefinedErrorMsg = """por favor, inténtalo más tarde.""" tipHelpText = """📖 Oops, eso no funcionó. %s *Uso:* `/tip []` diff --git a/translations/fr.toml b/translations/fr.toml new file mode 100644 index 00000000..61a80922 --- /dev/null +++ b/translations/fr.toml @@ -0,0 +1,297 @@ +# COMMANDS + +helpCommandStr = """Aide""" +basicsCommandStr = """basics""" +tipCommandStr = """tip""" +balanceCommandStr = """Solde""" +sendCommandStr = """Envoyer""" +invoiceCommandStr = """invoice""" +payCommandStr = """Payer""" +donateCommandStr = """Donation""" +advancedCommandStr = """Avancée""" +transactionsCommandStr = """Transactions""" +logCommandStr = """log""" +listCommandStr = """list""" + +linkCommandStr = """link""" +lnurlCommandStr = """lnurl""" +faucetCommandStr = """faucet""" + +tipjarCommandStr = """tipjar""" +receiveCommandStr = """Recevoir""" +hideCommandStr = """masquer""" +volcanoCommandStr = """volcano""" +showCommandStr = """montrer""" +optionsCommandStr = """options""" +settingsCommandStr = """réglages""" +saveCommandStr = """sauvegarder""" +deleteCommandStr = """supprimer""" +infoCommandStr = """info""" + +# NOTIFICATIONS + +cantDoThatMessage = """Vous ne pouvez pas faire ça.""" +cantClickMessage = """Ce bouton n'est pas cliquable.""" +balanceTooLowMessage = """Votre solde est trop bas.""" + +# BUTTONS + +sendButtonMessage = """✅ Envoyer""" +payButtonMessage = """✅ Payer""" +payReceiveButtonMessage = """💸 Payé""" +receiveButtonMessage = """✅ Reçu""" +cancelButtonMessage = """🚫 Annuler""" +collectButtonMessage = """✅ Collect""" +nextButtonMessage = """Suivant""" +backButtonMessage = """Précédent""" +acceptButtonMessage = """Accepter""" +denyButtonMessage = """Refuser""" +tipButtonMessage = """Tip""" +revealButtonMessage = """Révéler""" +showButtonMessage = """Afficher""" +hideButtonMessage = """Masquer""" +joinButtonMessage = """Joindre""" +optionsButtonMessage = """Options""" +settingsButtonMessage = """Réglages""" +saveButtonMessage = """Sauvegarder""" +deleteButtonMessage = """Supprimer""" +infoButtonMessage = """Info""" + +# AIDE + +helpMessage = """⚡️ *Wallet* +_Ce bot est un Bitcoin Lightning wallet qui permet d'envoyer des pourboires sur Telegram. Pour envoyer des pourboires, ajoutez le bot à un groupe. L'unité de base des pourboires est les Satoshis (sat). 100,000,000 sat = 1 Bitcoin. Tapez 📚 /basics pour plus d'info._ + +❤️ *Donation* +_Ce bot ne vous facture pas de frais mais nous coûte des Satoshis à opérerer. Si vous aimez ce bot, n'hésitez pas à faire un don pour soutenir le développement. Pour donner, utilisez_ `/donate 1000` + +%s + +⚙️ *Commandes* +*/tip* 🏅 Répondre à un message pour envoyer un pourboire : `/tip []` +*/balance* 👑 Afficher votre solde: `/balance` +*/send* 💸 Envoyer un pourboire à un utilisateur: `/send @user ou user@ln.tips []` +*/invoice* ⚡️ Recevoir avec Lightning : `/invoice []` +*/pay* ⚡️ Payer avec Lightning : `/pay ` +*/donate* ❤️ Faire une donation au projet : `/donate 1000` +*/advanced* 🤖 Fonctionnalités avancées. +*/help* 📖 Aide.""" + +infoHelpMessage = """ℹ️ *Info*""" +infoYourLightningAddress = """Votre adresse Lightning est `%s`""" + +basicsMessage = """🧡 *Bitcoin* +_Bitcoin est la monnaie d'internet. Aucune authorité ne contrôle ce réseau décentralisé et accessible à tous. Bitcoin est une monnaie saine qui est rapide, fiable et plus inclusive que le système financier traditionnel._ + +🧮 *Économie* +_La plus petite unité du Bitcoin est un Satoshis (sat). 100,000,000 sat = 1 Bitcoin. Il n'existe que 21 millions de Bitcoin. La valeur du Bitcoin fluctue tous les jours. Néanmoins, si vous vivez avec le standard du Bitcoin, 1 sat sera toujours égal à 1 sat._ + +⚡️ *Le Lightning Network* +_Le Lightning Network est un protocole de paiement qui permet d'envoyer et de recevoir rapidement du Bitcoin, à moindre coût et sans grande consommation d'énergie. Ce réseau rend accessible Bitcoin à des milliards de personnes._ + +📲 *Lightning Wallets* +_Vos fonds sur ce bot peuvent être envoyés à n'importe quel Lightning Wallet. Nous recommandons de télécharger sur votre téléphone les wallets suivant_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (non-custodial), ou_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(facile)_. + +📄 *Logiciel libre* +_Ce bot est gratuit et_ [libre de droits](https://github.com/LightningTipBot/LightningTipBot)_. Vous pouvez éxecuter le bot sur votre ordinateur et l'utiliser dans votre communauté._ + +✈️ *Telegram* +_Ajoutez ce bot à un groupe Telegram pour envoyer des pourboires en répondant à des messages. Si vous faites de ce bot un adminstrateur d'un groupe, il va réorganiser les commandes pour plus de lisibilité._ + +🏛 *Conditions d'utilisation* +_Ce bot n'assure pas la garde de vos fonds. Nous agissons toujours dans votre intérêt et nous sommes consciens de devoir faire des compromis en raison de l'absence de KYC. Les fonds de votre portefeuille sont aujourd'hui considérés comme une donation. Ne nous donnez pas tout votre argent. Ce bot est une version beta, faites attention s'il vous plaît. À utiliser à vos risques et périls._ + +❤️ *Donation* +_Ce bot ne vous facture pas de frais mais coûte des Satoshis à opérer. Si vous aimez ce bot, n'hésitez pas à faire un don pour soutenir le développement. Pour donner, utilisez_ `/donate 1000`""" + +helpNoUsernameMessage = """👋 Définissez un nom d'utilisateur sur Telegram afin d'utiliser ce bot.""" + +advancedMessage = """%s + +👉 *Inline commands* +*send* 💸 Envoyer des sats : `%s send []` +*receive* 🏅 Demander un paiement : `%s receive []` +*faucet* 🚰 Créer un faucet: `%s faucet ` + +📖 Vous pouvez utiliser ces commandes dans tous les chats et même dans les conversations privées. Attendez une seconde après avoir tapé une commandé puis *click* sur le résultat, n'appuyez pas sur entrée. + +⚙️ *Commandes avancées* +*/link* 🔗 Lier votre wallet à [BlueWallet](https://bluewallet.io/) ou [Zeus](https://zeusln.app/) +*/lnurl* ⚡️ Lnurl recevoir ou payer: `/lnurl` ou `/lnurl ` +*/faucet* 🚰 Créer un faucet `/faucet `""" + +# START + +startSettingWalletMessage = """🧮 Création de votre wallet...""" +startWalletCreatedMessage = """🧮 Wallet créé.""" +startWalletReadyMessage = """✅ *Votre wallet est prêt.*""" +startWalletErrorMessage = """🚫 Erreur lors de la création de votre wallet. Essayez de nouveau.""" +startNoUsernameMessage = """☝️ Vous n'avez pas encore de nom d'utilisateur sur Telegram. Vous n'en n'avez pas besoin pour utiliser ce bot mais l'expérience utilisateur est meilleure avec un nom d'utilisateur. Créez un nom puis tapez /balance pour actualiser le bot.""" + +# BALANCE + +balanceMessage = """👑 *Votre solde:* %d sat""" +balanceErrorMessage = """🚫 Impossible de récupérer votre solde. Essayez à nouveau.""" + +# TIP + +tipDidYouReplyMessage = """Avez-vous répondu à un message pour envoyer un pourboire ? Pour répondre à un message, faites un clic droit puis cliquez sur "répondre". Si vous voulez envoyer des fonds directement à un utilisateur, utilisez la commande /send .""" +tipInviteGroupMessage = """ℹ️ Vous pouvez ajouter ce bot à n'importe quel groupe.""" +tipEnterAmountMessage = """Avez-vous choisi un montant ?""" +tipValidAmountMessage = """Avez-vous choisi un montant correct ?""" +tipYourselfMessage = """📖 Vous ne pouvez pas vous envoyer de pourboire.""" +tipSentMessage = """💸 %d sat envoyé à %s.""" +tipReceivedMessage = """🏅 %s vous a envoyé un pourboire de %d sat.""" +tipErrorMessage = """🚫 Erreur lors de l'envoi.""" +tipUndefinedErrorMsg = """réessayez s'il vous plaît.""" +tipHelpText = """📖 Oops, cela n'a pas fonctionné. %s + +*Usage:* `/tip []` +*Exemple:* `/tip 1000 Dank meme!`""" + +# SEND + +sendValidAmountMessage = """Avez-vous choisi un montant correct ?""" +sendUserHasNoWalletMessage = """🚫 L'utilisateur %s n'a pas encore crée de wallet.""" +sendSentMessage = """💸 %d sat envoyé à %s.""" +sendPublicSentMessage = """💸 %d sat envoyé de %s à %s.""" +sendReceivedMessage = """🏅 %s vous a envoyé %d sat.""" +sendErrorMessage = """🚫 Erreur lors de l'envoi.""" +confirmSendMessage = """Voulez-vous envoyer à %s?\n\n💸 Montant : %d sat""" +confirmSendAppendMemo = """\n✉️ %s""" +sendCancelledMessage = """🚫 Envoi annulé.""" +errorTryLaterMessage = """🚫 Erreur. Merci de réésayer.""" +sendSyntaxErrorMessage = """Avez-vous ajouté un montant et un destinataire ? Utilisez la commande /send pour envoyer à un utilisateur Telegram comme %s ou bien à une adresse Lightning comme LightningTipBot@ln.tips.""" +sendHelpText = """📖 Oops, cela n'a pas fonctionné. %s + +*Usage:* `/send []` +*Exemple:* `/send 1000 @LightningTipBot J'adore le bot ! ❤️` +*Exemple:* `/send 1234 LightningTipBot@ln.tips`""" + +# INVOICE + +invoiceReceivedMessage = """⚡️ Vous avez reçu %d sat.""" +invoiceEnterAmountMessage = """Avez-vous choisi un montant ?""" +invoiceValidAmountMessage = """Avez-vous choisi un montant correct ?""" +invoiceHelpText = """📖 Oops, cela n'a pas fonctionné. %s + +*Usage:* `/invoice []` +*Exemple:* `/invoice 1000 Merci beaucoup !`""" + +# PAY + +paymentCancelledMessage = """🚫 Paiement annulé.""" +invoicePaidMessage = """⚡️ Paiement envoyé.""" +invoicePublicPaidMessage = """⚡️ Paiement envoyé par %s.""" +invalidInvoiceHelpMessage = """Avez-vous ajouté un Lightning invoice valide ? Essayez /send pour envoyer à un utilisateur Telegram ou à une adresse Lightning.""" +invoiceNoAmountMessage = """🚫 Vous ne pouvez pas payer une facture sans montant.""" +insufficientFundsMessage = """🚫 Fonds insuffisants. Vous avez %d sat et vous avez besoin d'un minimum de %d sat.""" +feeReserveMessage = """⚠️ Envoyer tous vos fonds peut ne pas fonctionner en raison de la nécessité de payer des frais de réseau. Si cela échoue, essayez d'envoyer un peu moins.""" +invoicePaymentFailedMessage = """🚫 Le paiement a échoué : %s""" +invoiceUndefinedErrorMessage = """Impossible de payer la facture.""" +confirmPayInvoiceMessage = """Voulez-vous envoyer ce paiement ?\n\n💸 Montant: %d sat""" +confirmPayAppendMemo = """\n✉️ %s""" +payHelpText = """📖 Oops, cela n'a pas fonctionné. %s + +*Usage:* `/pay ` +*Exemple:* `/pay lnbc20n1psscehd...`""" + +# DONATE + +donationSuccess = """🙏 Merci pour votre donation !.""" +donationErrorMessage = """🚫 Oh non, le paiement de la donation a échoué.""" +donationProgressMessage = """🧮 Préparation de votre donation...""" +donationFailedMessage = """🚫 Echec de la donation: %s""" +donateEnterAmountMessage = """Avez-vous choisi un montant ?""" +donateValidAmountMessage = """Avez-vous choisi un montant correct ?""" +donateHelpText = """📖 Oops, cela n'a pas fonctionné. %s + +*Usage:* `/donate ` +*Exemple:* `/donate 1000`""" + +# PHOTO + +photoQrNotRecognizedMessage = """🚫 Impossible de reconnaître une facture Lightning ou un LNRUL. Essayez de centrer le QR code ou de zoomer.""" +photoQrRecognizedMessage = """✅ QR code: +`%s`""" + +# LNURL + +lnurlReceiveInfoText = """👇 Vous ne pouvez pas utiliser cet LNURL pour recevoir des paiements.""" +lnurlResolvingUrlMessage = """🧮 Recherche de l'adresse...""" +lnurlGettingUserMessage = """🧮 Préparation du paiement...""" +lnurlPaymentFailed = """🚫 Echec du paiement : %s""" +lnurlInvalidAmountMessage = """🚫 Montant incorrect.""" +lnurlInvalidAmountRangeMessage = """🚫 Le montant doit être entre %d et %d sat.""" +lnurlNoUsernameMessage = """🚫 Vous devez avoir un nom d'utilisateur Telegram pour recevoir un paiement via LNURL.""" +lnurlEnterAmountMessage = """⌨️ Choisissez un montant entre %d et %d sat.""" +lnurlHelpText = """📖 Oops, cela n'a pas fonctionné. %s + +*Usage:* `/lnurl [montant] ` +*Exemple:* `/lnurl LNURL1DP68GUR...`""" + +# LINK + +walletConnectMessage = """🔗 *Lier votre wallet* + +⚠️ Ne partagez jamais cet URL ou ce QR code au risque de donner accès à vos fonds à un tiers. + +- *BlueWallet:* Appuyez sur *New wallet*, *Import wallet*, *Scan or import a file*, puis scanner le QR code. +- *Zeus:* Copier l'URL, appuyez sur *Add a new node*, *Import* (l'URL), *Save Node Config*.""" +couldNotLinkMessage = """🚫 Impossible de lier votre wallet. Merci de réésayer.""" + +# FAUCET + +inlineQueryFaucetTitle = """🚰 Création d'un faucet.""" +inlineQueryFaucetDescription = """Usage: @%s faucet """ +inlineResultFaucetTitle = """🚰 Création d'un %d sat faucet.""" +inlineResultFaucetDescription = """👉 Appuyez ici pour créer un faucet dans ce chat.""" + +inlineFaucetMessage = """Appuyez sur ✅ pour collecter %d sat de ce faucet. + +🚰 Restant: %d/%d sat (envoyé à %d/%d utilisateurs) +%s""" +inlineFaucetEndedMessage = """🚰 Faucet vide 🍺\n\n🏅 %d sat envoyé à %d utilisateurs.""" +inlineFaucetAppendMemo = """\n✉️ %s""" +inlineFaucetCreateWalletMessage = """Chat avec %s 👈 pour gérer votre wallet.""" +inlineFaucetCancelledMessage = """🚫 Faucet annulé.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 Le montant de l'utilisateur n'est pas suffisament divisdable.""" +inlineFaucetInvalidAmountMessage = """🚫 Montant incorrect.""" +inlineFaucetSentMessage = """🚰 %d sat envoyé à %s.""" +inlineFaucetReceivedMessage = """🚰 %s vous a envoyé %d sat.""" +inlineFaucetHelpFaucetInGroup = """Créez un faucet dans un groupe avec le bot ou utilisez 👉 les commandes (/advanced pour plus).""" +inlineFaucetHelpText = """📖 Oops, cela n'a pas fonctionné. %s + +*Usage:* `/faucet ` +*Exemple:* `/faucet 210 21`""" + +# INLINE SEND + +inlineQuerySendTitle = """💸 Envoyez des paiements dans un chat.""" +inlineQuerySendDescription = """Usage: @%s send []""" +inlineResultSendTitle = """💸 Envoyé %d sat.""" +inlineResultSendDescription = """👉 Clique pour envoyer %d sat sur ce chat.""" + +inlineSendMessage = """Appuyez sur ✅ pour recevoir le paiement de %s.\n\n💸 Montant: %d sat""" +inlineSendAppendMemo = """\n✉️ %s""" +inlineSendUpdateMessageAccept = """💸 %d sat sent from %s to %s.""" +inlineSendCreateWalletMessage = """Chat avec %s 👈 pour gérer votre wallet.""" +sendYourselfMessage = """📖 Vous ne pouvez pas vous envoyer de pourboires.""" +inlineSendFailedMessage = """🚫 Echec de l'envoi.""" +inlineSendInvalidAmountMessage = """🚫 Le montant doit être supérieure à 0.""" +inlineSendBalanceLowMessage = """🚫 Votre solde est trop bas.""" + +# INLINE RECEIVE + +inlineQueryReceiveTitle = """🏅 Demander un paiement dans un chat.""" +inlineQueryReceiveDescription = """Usage: @%s receive []""" +inlineResultReceiveTitle = """🏅 Recevoir %d sat.""" +inlineResultReceiveDescription = """👉 Cliquez ici pour recevoir un paiement de %d sat.""" + +inlineReceiveMessage = """Appuyez sur 💸 pour payer %s.\n\n💸 Montant: %d sat""" +inlineReceiveAppendMemo = """\n✉️ %s""" +inlineReceiveUpdateMessageAccept = """💸 %d sat envoyé de %s à %s.""" +inlineReceiveCreateWalletMessage = """Chat avec %s 👈 pour gérer votre wallet.""" +inlineReceiveYourselfMessage = """📖 Vous ne pouvez pas vous envoyer de pourboires.""" +inlineReceiveFailedMessage = """🚫 La réception a échoué.""" +inlineReceiveCancelledMessage = """🚫 Réception annulée.""" diff --git a/translations/it.toml b/translations/it.toml index 7c144dd4..2fdedb52 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -143,7 +143,7 @@ tipYourselfMessage = """📖 Non puoi inviare una mancia a te stesso.""" tipSentMessage = """💸 %d sat inviati a %s.""" tipReceivedMessage = """🏅 %s ti ha inviato una mancia di %d sat.""" tipErrorMessage = """🚫 Invio mancia non riuscito.""" -tipUndefinedErrorMsg = """Per favore riprova più tardi""" +tipUndefinedErrorMsg = """Per favore riprova più tardi.""" tipHelpText = """📖 Ops, non funziona. %s *Usage:* `/tip []` diff --git a/translations/nl.toml b/translations/nl.toml index ee7089c0..ee14bd70 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -143,7 +143,7 @@ tipYourselfMessage = """📖 Je kunt jezelf geen tip geven.""" tipSentMessage = """💸 %d sat gestuurd naar %s.""" tipReceivedMessage = """🏅 %s heeft je getipt %d sat.""" tipErrorMessage = """🚫 Tip mislukt.""" -tipUndefinedErrorMsg = """probeer het later nog eens""" +tipUndefinedErrorMsg = """probeer het later nog eens.""" tipHelpText = """📖 Oeps, dat werkte niet. %s *Usage:* `/tip []` From 3a2c5b2c511f0bba6cca5cd13cafb1b91fcd658b Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 13 Oct 2021 09:01:06 +0300 Subject: [PATCH 027/541] Portuguese (#100) * For mango-selling grannies in Brazil * fix * rename to pt-br.toml Co-authored-by: Koty <89604768+kotyauditore@users.noreply.github.com> --- internal/i18n/localize.go | 1 + translations/pt-br.toml | 297 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 translations/pt-br.toml diff --git a/internal/i18n/localize.go b/internal/i18n/localize.go index 830bbfec..332ac619 100644 --- a/internal/i18n/localize.go +++ b/internal/i18n/localize.go @@ -22,6 +22,7 @@ func RegisterLanguages() *i18n.Bundle { bundle.LoadMessageFile("translations/es.toml") bundle.LoadMessageFile("translations/nl.toml") bundle.LoadMessageFile("translations/fr.toml") + bundle.LoadMessageFile("translations/pt-br.toml") return bundle } func Translate(languageCode string, MessgeID string) string { diff --git a/translations/pt-br.toml b/translations/pt-br.toml new file mode 100644 index 00000000..6b531899 --- /dev/null +++ b/translations/pt-br.toml @@ -0,0 +1,297 @@ +# COMMANDS + +helpCommandStr = """ajuda""" +basicsCommandStr = """conceitos""" +tipCommandStr = """gorjeta""" +balanceCommandStr = """saldo""" +sendCommandStr = """enviar""" +invoiceCommandStr = """fatura""" +payCommandStr = """pagar""" +donateCommandStr = """doar""" +advancedCommandStr = """avançado""" +transactionsCommandStr = """transações""" +logCommandStr = """log""" +listCommandStr = """lista""" + +linkCommandStr = """enlace""" +lnurlCommandStr = """lnurl""" +faucetCommandStr = """torneira""" + +tipjarCommandStr = """cofrinho""" +receiveCommandStr = """receber""" +hideCommandStr = """ocultar""" +volcanoCommandStr = """vulcão""" +showCommandStr = """mostrar""" +optionsCommandStr = """opções""" +settingsCommandStr = """configurações""" +saveCommandStr = """salvar""" +deleteCommandStr = """apagar""" +infoCommandStr = """info""" + +# NOTIFICATIONS + +cantDoThatMessage = """Não se pode fazer isso.""" +cantClickMessage = """Não se pode clicar neste botão.""" +balanceTooLowMessage = """Seu saldo é muito baixo.""" + +# BUTTONS + +sendButtonMessage = """✅ Enviar""" +payButtonMessage = """✅ Pagar""" +payReceiveButtonMessage = """💸 Pagar""" +receiveButtonMessage = """✅ Receber""" +cancelButtonMessage = """🚫 Cancelar""" +collectButtonMessage = """✅ Cobrar""" +nextButtonMessage = """Seguinte""" +backButtonMessage = """Voltar""" +acceptButtonMessage = """Aceitar""" +denyButtonMessage = """Negar""" +tipButtonMessage = """Gorjeta""" +revealButtonMessage = """Revelar""" +showButtonMessage = """Mostrar""" +hideButtonMessage = """Ocultar""" +joinButtonMessage = """Unir""" +optionsButtonMessage = """Opções""" +settingsButtonMessage = """Configurações""" +saveButtonMessage = """Salvar""" +deleteButtonMessage = """Apagar""" +infoButtonMessage = """Info""" + +# HELP + +helpMessage = """⚡️ *Carteira* +_Este bot é uma carteira Bitcoin Lightning que pode enviar gorjetas via Telegram. Para dar gorjetas, adicione o bot a um bate-papo em grupo. A unidade básica das gorjetas é Satoshis (sat). 100.000.000 sat = 1 Bitcoin. Digite 📚 /basics para saber mais._ + +❤️ *Doar* +_Este bot não cobra nenhuma comissão, mas custa Satoshis para funcionar. Se você gosta do bot, por favor, considere apoiar este projeto com uma doação. Para doar, utilize_ `/donate 1000`. + +%s + +⚙️ *Comandos* +*/tip* 🏅 Responder uma mensagem para dar uma gorjeta: `/tip []` +*/balance* 👑 Confira seu saldo: `/balance`. +*/send* 💸 Enviar dinheiro para um usuário: `/send @user ou user@ln.tips []` +*/invoice* ⚡️ Receber com Lightning: `/invoice []` +*/pay* ⚡️ Pagar com Lightning: `/pay ` +*/donate* ❤️ Doar ao projeto: `/donate 1000` +*/advanced* 🤖 Funções avançadas. +*/help* 📖 Ler esta ajuda.""" + +infoHelpMessage = """ℹ️ *Info*""" +infoYourLightningAddress = """Seu Lightning Address _(Endereço Lightning)_ é `%s`.""" + +basicsMessage = """🧡 *Bitcoin* +_Bitcoin é a moeda da Internet. É uma moeda sem permissão, descentralizada, sem proprietários e sem autoridade de controle. Bitcoin é dinheiro sólido, mais rápido, mais seguro e mais inclusivo do que o sistema financeiro fiat._ + +🧮 *Economia* +_A menor unidade de Bitcoin é Satoshis (sat) e 100.000.000 sat = 1 Bitcoin. Haverá apenas 21 milhões de Bitcoin. O valor do Bitcoin em moeda fiduciária pode mudar diariamente. No entanto, se você vive no padrão Bitcoin, 1 sat será sempre 1 sat._ + +⚡️ *A Rede Lightning (Relâmpago)* +_A rede Lightning é um protocolo de pagamento que permite que os pagamentos Bitcoin sejam feitos de forma rápida e econômica, com consumo mínimo de energia. É o que faz o Bitcoin escalar para bilhões de pessoas em todo o mundo._ + +📲 *Carteiras Lightning (Relâmpago)* +_Seu dinheiro neste bot pode ser enviado para qualquer outra carteira da Lightning e vice versa. As carteiras Lightning recomendadas para seu telefone são_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (não-custodial), ou_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(fácil)_. + +📄 *Código aberto* +_Este bot é gratuito e de_ [Código aberto](https://github.com/LightningTipBot/LightningTipBot)_. Você pode executá-lo no seu próprio computador e utilizá-lo na sua própria comunidade._ + +✈️ *Telegram* +_Adicione este bot ao seu bate-papo de grupo de Telegram para enviar dinheiro usando o comando /tip. Se você fizer do bot o administrador do grupo, ele também limpará os comandos para manter o bate-papo arrumado._ + +🏛 *Termos* +_Nós não somos custódios do seu dinheiro. Atuaremos no seu melhor interesse, mas também estamos conscientes de que a situação sem KYC é complicada até encontrarmos uma solução ótima. Qualquer quantia que você colocar na sua carteira será considerada uma doação. Não ponha todo o seu dinheiro. Favor observar que este bot está em desenvolvimento beta. Use-o pela sua própria conta e risco._ + +❤️ *Doar* +_Este bot não cobra nenhuma comissão, mas custa satoshis para funcionar. Se você gosta do bot, por favor, considere apoiar este projeto com uma doação. Para doar, utilize_ `/donate 1000`.""" + +helpNoUsernameMessage = """👋 Por favor, digite um nome de usuário do Telegram.""" + +advancedMessage = """%s + +👉 *Comandos Inline* +*send* 💸 Enviar sats ao bate-papo: `%s send []` +*receive* 🏅 Solicite um pagamento: `%s receive []`. +*faucet* 🚰 Criar uma torneira: `%s faucet `. + +📖 Você pode usar comandos _inline_ em todas as conversas, mesmo em conversas privadas. Espere um segundo após inserir um comando _inline_ e *clique* no resultado, não pressione enter. + +⚙️ *Comandos avançados* +*/link* 🔗 Vincule sua carteira a [ BlueWallet ](https://bluewallet.io/) ou [ Zeus ](https://zeusln.app/) +*/lnurl* ⚡️ Receber ou pagar com lnurl: `/lnurl` o `/lnurl ` +*/faucet* 🚰 Criar uma torneira: `%s faucet `""" + +# START + +startSettingWalletMessage = """🧮 Preparando sua carteira...""" +startWalletCreatedMessage = """🧮 Carteira criada.""" +startWalletReadyMessage = """✅ *Sua carteira está pronta.*""" +startWalletErrorMessage = """🚫 Erro ao iniciar sua carteira. Por favor, tente novamente mais tarde.""" +startNoUsernameMessage = """☝️ Parece que você ainda não tem um @nomedeusuário no Telegram. Tudo bem, você não precisa de um para usar este bot. Porém, para fazer melhor uso de sua carteira, defina um nome de usuário nas suas configurações de Telegram. Depois, entre /balance para que o bot possa atualizar seu registro de você.""" + +# BALANCE + +balanceMessage = """👑 *Seu saldo:* %d sat""" +balanceErrorMessage = """🚫 Seu saldo não pôde ser recuperado. Por favor, tente novamente mais tarde.""" + +# TIP + +tipDidYouReplyMessage = """Você já respondeu em uma mensagem para dar gorjeta? Para responder qualquer mensagem, clique com o botão direito do mouse -> Responder no seu computador ou deslize a mensagem no seu telefone. Se você quiser enviar diretamente para outro usuário, use o comando /send.""" +tipInviteGroupMessage = """ℹ️ A propósito, você pode convidar este bot a qualquer grupo para começar a dar gorjetas lá.""" +tipEnterAmountMessage = """Você inseriu uma quantia?""" +tipValidAmountMessage = """Você inseriu uma quantia válida?""" +tipYourselfMessage = """📖 Você não pode se dar gorjetas.""" +tipSentMessage = """💸 %d sat enviado a %s.""" +tipReceivedMessage = """🏅 %s lhe deu uma gorjeta de %d sat.""" +tipErrorMessage = """🚫 A gorjeta falhou.""" +tipUndefinedErrorMsg = """Por favor, tente mais tarde""" +tipHelpText = """📖 Opa, isso não funcionou. %s + +*Uso:* `/tip []` +*Exemplo:* `/tip 1000 Meme ruim!`""" + +# SEND + +sendValidAmountMessage = """Você inseriu uma quantia válida?""" +sendUserHasNoWalletMessage = """🚫 O usuário %s ainda não criou uma carteira.""" +sendSentMessage = """💸 %d sat enviado a %s.""" +sendPublicSentMessage = """💸 %d sat enviado(s) de %s a %s.""" +sendReceivedMessage = """🏅 %s lhe enviou %d sat.""" +sendErrorMessage = """🚫 Envio sem sucesso.""" +confirmSendMessage = """Deseja pagar %s?\n\n💸 Quantia: %d sat""" +confirmSendAppendMemo = """\n✉️ %s""" +sendCancelledMessage = """🚫 Envio cancelado.""" +errorTryLaterMessage = """🚫 Erro. Por favor, tente mais tarde.""" +sendSyntaxErrorMessage = """Você já inseriu uma quantia e um destinatário? Você pode usar o comando /send para enviar aos usuários de Telegram como %s ou para um Endereço Lightning como LightningTipBot@ln.tips.""" +sendHelpText = """📖 Opa, isso não funcionou. %s + +*Uso:* `/send []` +*Exemplo:* `/send 1000 @LightningTipBot Gosto do bot ❤️`. +*Exemplo:* `/send 1234 LightningTipBot@ln.tips`""" + +# INVOICE + +invoiceReceivedMessage = """⚡️ Recebeu %d sat.""" +invoiceEnterAmountMessage = """Você inseriu uma quantia?""" +invoiceValidAmountMessage = """Você inseriu uma quantia válida?""" +invoiceHelpText = """📖 Opa, isso não funcionou. %s + +*Uso:* `/invoice []` +*Exemplo:* `/invoice 1000 Obrigado!`""" + +# PAY + +paymentCancelledMessage = """🚫 Pagamento cancelado.""" +invoicePaidMessage = """⚡️ Pagamento enviado.""" +invoicePublicPaidMessage = """⚡️ Pagamento enviado por %s.""" +invalidInvoiceHelpMessage = """Você já inseriu uma fatura Lightning válida? Tente /send se você quiser enviar a um usuário de Telegram ou a um endereço de Lightning.""" +invoiceNoAmountMessage = """🚫 As contas não podem ser pagas sem um valor.""" +insufficientFundsMessage = """🚫 Dinheiro insuficiente. Tem %d sat mas precisa pelo menos %d sat.""" +feeReserveMessage = """⚠️ O envio de todo o saldo pode falhar devido às taxas de rede. Se falhar, tente enviar um pouco menos.""" +invoicePaymentFailedMessage = """🚫 Falha no pagamento: %s""" +invoiceUndefinedErrorMessage = """A fatura não pôde ser paga.""" +confirmPayInvoiceMessage = """Você quer enviar este pagamento?\n\n💸 Quantia: %d sat""" +confirmPayAppendMemo = """\n✉️ %s""" +payHelpText = """📖 Opa, isso não funcionou. %s + +*Uso:* `/pay ` +*Exemplo:* `/pay lnbc20n1psscehd...`""" + +# DONATE + +donationSuccess = """🙏 Obrigado pela sua doação.""" +donationErrorMessage = """🚫 Ó, não. Doação sem sucesso.""" +donationProgressMessage = """🧮 Preparando sua doação...""" +donationFailedMessage = """🚫 Doação sem sucesso: %s""" +donateEnterAmountMessage = """Você inseriu uma quantia?""" +donateValidAmountMessage = """Você inseriu uma quantia válida?""" +donateHelpText = """📖 Opa, isso não funcionou. %s + +*Uso:* `/donate ` +*Exemplo:* `/donate 1000`""" + +# PHOTO + +photoQrNotRecognizedMessage = """🚫 Uma fatura de Lightning ou LNURL não pôde ser reconhecida. Tente centralizar o código QR, cortando ou aumentando a foto.""" +photoQrRecognizedMessage = """✅ Código QR: +`%s`""" + +# LNURL + +lnurlReceiveInfoText = """👇 Você pode usar este LNURL estático para receber pagamentos.""" +lnurlResolvingUrlMessage = """🧮 Solucionando o endereço...""" +lnurlGettingUserMessage = """🧮 Preparando o pagamento...""" +lnurlPaymentFailed = """🚫 Falha no pagamento: %s""" +lnurlInvalidAmountMessage = """🚫 Quantia inválida.""" +lnurlInvalidAmountRangeMessage = """🚫 A quantia deve estar entre %d e %d sat.""" +lnurlNoUsernameMessage = """🚫 Você precisa configurar um nome de usuário de Telegram para receber pagamentos através do LNURL.""" +lnurlEnterAmountMessage = """⌨️ Insira uma quantia entre %d e %d sat.""" +lnurlHelpText = """📖 Opa, isso não funcionou. %s + +*Uso:* `/lnurl [quantidade] ` +*Exemplo:* `/lnurl LNURL1DP68GUR...`""" + +# LINK + +walletConnectMessage = """🔗 *Vincule sua carteira* + +⚠️ Nunca compartilhe a URL ou o código QR com ninguém ou eles poderão acessar seus fundos.. + +- *BlueWallet:* Toque em *Nova carteira*, *Importar carteira*, *Escanear ou importar um arquivo*, e escanear o código QR. +- *Zeus:* Copie a URL abaixo, clique *Adicionar um novo nodo*, *Importar* (a URL), *Salvar configuração do nodo*.""" +couldNotLinkMessage = """🚫 Sua carteira não poderia ser ligada. Por favor, tente novamente mais tarde.""" + +# FAUCET + +inlineQueryFaucetTitle = """🚰 Crie uma torneira.""" +inlineQueryFaucetDescription = """Uso: @%s faucet """ +inlineResultFaucetTitle = """🚰 Criar uma torneira %d sat.""" +inlineResultFaucetDescription = """👉 Aperte aqui para criar uma torneira neste chat.""" + +inlineFaucetMessage = """Aperte ✅ para coletar %d sat desta torneira. + +🚰 Restante: %d/%d sat (para %d/%d usuários) +%s""" +inlineFaucetEndedMessage = """🚰 Torneira vazia 🍺\n\n🏅 %d sat para %d usuários.""" +inlineFaucetAppendMemo = """\n✉️ %s""" +inlineFaucetCreateWalletMessage = """Bate-papo com %s 👈 para administrar sua carteira.""" +inlineFaucetCancelledMessage = """🚫 Torneira cancelada.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 A quantia por usuário não é divisória da capacidade.""" +inlineFaucetInvalidAmountMessage = """🚫 Quantia inválida.""" +inlineFaucetSentMessage = """🚰 %d sat enviado(s) a %s.""" +inlineFaucetReceivedMessage = """🚰 %s lhe enviou %d sat.""" +inlineFaucetHelpFaucetInGroup = """Criar uma torneira em grupo com o bot dentro ou usar o 👉 comando_inline_ (/advanced para mais).""" +inlineFaucetHelpText = """📖 Opa, isso não funcionou. %s + +*Uso:* `/faucet ` +*Exemplo:* `/faucet 210 21`""" + +# INLINE SEND + +inlineQuerySendTitle = """💸 Enviar pagamento para um bate-papo.""" +inlineQuerySendDescription = """Uso: @%s send []""" +inlineResultSendTitle = """💸 Enviar %d sat.""" +inlineResultSendDescription = """👉 Clique para enviar %d sat neste bate-papo.""" + +inlineSendMessage = """Clique ✅ para receber o pagamento de %s.\n\n💸 Quantidade: %d sat""" +inlineSendAppendMemo = """\n✉️ %s""" +inlineSendUpdateMessageAccept = """💸 %d sat enviado de %s a %s.""" +inlineSendCreateWalletMessage = """Bate-papo com %s 👈 para administrar sua carteira.""" +sendYourselfMessage = """📖 Você não pode pagar a si mesmo.""" +inlineSendFailedMessage = """🚫 Envio sem sucesso.""" +inlineSendInvalidAmountMessage = """🚫 A quantia deve ser maior do que 0.""" +inlineSendBalanceLowMessage = """🚫 Seu saldo é muito baixo.""" + +# INLINE RECEIVE + +inlineQueryReceiveTitle = """🏅 Solicite um pagamento em um bate-papo.""" +inlineQueryReceiveDescription = """Uso: @%s receive []""" +inlineResultReceiveTitle = """🏅 Receber %d sat.""" +inlineResultReceiveDescription = """👉 Clique para solicitar um pagamento de %d sat.""" + +inlineReceiveMessage = """Clique 💸 para pagar a %s.\n\n💸 Quantia: %d sat""" +inlineReceiveAppendMemo = """\n✉️ %s""" +inlineReceiveUpdateMessageAccept = """💸 %d sat enviado de %s a %s.""" +inlineReceiveCreateWalletMessage = """Bate-papo com %s 👈 para administrar sua carteira.""" +inlineReceiveYourselfMessage = """📖 Você não pode pagar a si mesmo.""" +inlineReceiveFailedMessage = """🚫 O recebimento falhou.""" +inlineReceiveCancelledMessage = """🚫 Recepção cancelada.""" From 22d4ab284cc2028ef0c2fb848ccc8dcb22b9bf52 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 13 Oct 2021 17:13:28 +0300 Subject: [PATCH 028/541] turkish translations (#101) * turkish translations * fix * fix * fix * add tr.toml --- internal/i18n/localize.go | 1 + internal/price/price.go | 28 ++-- translations/de.toml | 16 +- translations/tr.toml | 297 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 324 insertions(+), 18 deletions(-) create mode 100644 translations/tr.toml diff --git a/internal/i18n/localize.go b/internal/i18n/localize.go index 332ac619..b2a40913 100644 --- a/internal/i18n/localize.go +++ b/internal/i18n/localize.go @@ -23,6 +23,7 @@ func RegisterLanguages() *i18n.Bundle { bundle.LoadMessageFile("translations/nl.toml") bundle.LoadMessageFile("translations/fr.toml") bundle.LoadMessageFile("translations/pt-br.toml") + bundle.LoadMessageFile("translations/tr.toml") return bundle } func Translate(languageCode string, MessgeID string) string { diff --git a/internal/price/price.go b/internal/price/price.go index b146be8d..6da4f2d9 100644 --- a/internal/price/price.go +++ b/internal/price/price.go @@ -94,12 +94,16 @@ func (p *PriceWatcher) GetCoinbasePrice(currency string) (float64, error) { return 0, err } price := gjson.Get(string(bodyBytes), "data.amount") - fprice, err := strconv.ParseFloat(strings.TrimSpace(price.String()), 64) - if err != nil { - log.Debug(err) - return 0, err + if len(price.String()) > 0 { + fprice, err := strconv.ParseFloat(strings.TrimSpace(price.String()), 64) + if err != nil { + log.Debug(err) + return 0, err + } + return fprice, nil + } else { + return 0, fmt.Errorf("no price") } - return fprice, nil } func (p *PriceWatcher) GetBitfinexPrice(currency string) (float64, error) { @@ -116,10 +120,14 @@ func (p *PriceWatcher) GetBitfinexPrice(currency string) (float64, error) { return 0, err } price := gjson.Get(string(bodyBytes), "last_price") - fprice, err := strconv.ParseFloat(strings.TrimSpace(price.String()), 64) - if err != nil { - log.Debug(err) - return 0, err + if len(price.String()) > 0 { + fprice, err := strconv.ParseFloat(strings.TrimSpace(price.String()), 64) + if err != nil { + log.Debug(err) + return 0, err + } + return fprice, nil + } else { + return 0, fmt.Errorf("no price") } - return fprice, nil } diff --git a/translations/de.toml b/translations/de.toml index 3fd313ba..abf03828 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -81,7 +81,7 @@ infoHelpMessage = """ℹ️ *Info*""" infoYourLightningAddress = """Deine Lightning Adresse ist `%s`""" basicsMessage = """🧡 *Bitcoin* -_Bitcoin ist die Währung des Internets. Es ist offen für jeden, dezentral und es gibt niemanden, der Bitcoin kontrolliert. Bitcoin ist hartes Geld, was schneller, sicherer, und fairer ist, als das klassische Finanzsystem._ +_Bitcoin ist die Währung des Internets. Bitcoin ist offen für jeden, ist dezentral, und es gibt niemanden, der Bitcoin kontrolliert. Bitcoin ist hartes Geld, welches schneller, sicherer, und fairer ist, als das klassische Finanzsystem._ 🧮 *Ökonomie* _Die kleinste Einheit von Bitcoin sind Satoshis (sat) und 100,000,000 sat = 1 Bitcoin. Es wird niemals mehr als 21 Millionen Bitcoin geben. Der Fiatgeld-Wert von Bitcoin kann sich täglich ändern. Wenn du jedoch auf einem Bitcoin-Standard lebst, wird 1 sat für immer 1 sat Wert sein._ @@ -90,7 +90,7 @@ _Die kleinste Einheit von Bitcoin sind Satoshis (sat) und 100,000,000 sat = 1 Bi _Das Lightning Netzwerk ist ein Bezahlprotokoll, das schnelle und günstige Bitcoin Zahlungen erlaubt, die fast keine Energie kosten. Dadurch skaliert Bitcoin auf Milliarden von Menschen weltweit._ 📲 *Lightning Wallets* -_Dein Geld auf diesem Bot kannst du an jedes andere Lightning Wallet der Welt versenden. Empfholene Lightning Wallets für dein Handy sind _ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (non-custodial), or_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(einfach)_. +_Dein Geld auf diesem Bot kannst du an jedes andere Lightning Wallet der Welt versenden. Empfohlene Lightning Wallets für dein Handy sind _ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (eigene Verwahrung), oder_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(einfach)_. 📄 *Open Source* _Dieser Bot ist kostenlose_ [Open Source](https://github.com/LightningTipBot/LightningTipBot) _Software. Du kannst ihn auf deinem eigenen Rechner laufen lassen und für deine eigene Community betreiben._ @@ -99,12 +99,12 @@ _Dieser Bot ist kostenlose_ [Open Source](https://github.com/LightningTipBot/Lig _Füge den Bot in deine Telegram Gruppenchats hinzu, um Nachrichten ein /tip zu senden. Wenn der Bot Admin der Gruppe ist, wird er auch den Chat aufräumen, indem er manche Befehle nach Ausführung löscht._ 🏛 *Bedingungen* -_Wir sind keine Verwalter deines Geldes. Wir werden in deinem besten Interesse handeln, aber sind uns auch dessen bewusst, dass die Situation ohne KYC etwas kompliziert ist. Solange wir keine andere Lösung gefunden haben, werden wir alle Beträge auf diesem Bot als Spenden betrachten. Gebe uns nicht dein ganzes Geld. Sei dir dessen bewusst, dass dieser Bot immer noch in der Betaphase ist. Benutzung auf eigene Gefahr._ +_Wir sind keine Verwahrer deines Geldes. Wir werden in deinem besten Interesse handeln, aber sind uns auch dessen bewusst, dass die Situation ohne KYC etwas kompliziert ist. Solange wir keine andere Lösung gefunden haben, werden wir alle Beträge auf diesem Bot als Spenden betrachten. Gebe uns nicht dein ganzes Geld. Sei dir dessen bewusst, dass dieser Bot immer noch in der Betaphase ist. Benutzung auf eigene Gefahr._ ❤️ *Spenden* _Dieser Bot erhebt keine Gebühren, kostet aber Satoshis um betrieben zu werden. Wenn du den Bot magst, kannst das Projekt mit eine Spende unterstützen. Um zu spenden, gebe ein: _ `/donate 1000`""" -helpNoUsernameMessage = """👋 Bitte setze einen Telegram Benutzernamen.""" +helpNoUsernameMessage = """👋 Wähle einen Benutzernamen in den Telegram Einstellungen.""" advancedMessage = """%s @@ -123,7 +123,7 @@ advancedMessage = """%s # START startSettingWalletMessage = """🧮 Richte dein Wallet ein...""" -startWalletCreatedMessage = """🧮 Wallet erstellt.""" +startWalletCreatedMessage = """🧮 Wallet erzeugt.""" startWalletReadyMessage = """✅ *Dein Wallet ist bereit.*""" startWalletErrorMessage = """🚫 Fehler beim einrichten deines Wallets. Bitte versuche es später noch einmal.""" startNoUsernameMessage = """☝️ Es sieht so aus, als hättest du noch keinen Telegram @usernamen. Das ist ok, du brauchst keinen, um diesen Bot zu verwenden. Um jedoch alle Funktionen verwenden zu können, solltest du dir einen Usernamen in den Telegram Einstellungen setzen. Gebe danach ein mal /balance ein, damit der Bot seine Informationen über dich aktualisieren kann.""" @@ -175,7 +175,7 @@ invoiceEnterAmountMessage = """Hast du einen Betrag eingegeben?""" invoiceValidAmountMessage = """Hast du einen gültigen Betrag eingegeben?""" invoiceHelpText = """📖 Ups, das hat nicht geklappt. %s -*Befehl:* `/invoice []` +*Befehl:* `/invoice []` *Beispiel:* `/invoice 1000 Thank you!`""" # PAY @@ -237,7 +237,7 @@ walletConnectMessage = """🔗 *Verbinde dein Wallet* ⚠️ Teile diese URL und den QR code mit niemandem! Jeder, der darauf Zugriff hat, hat auch Zugriff auf dein Konto. - *BlueWallet:* Drücke *New wallet*, *Import wallet*, *Scan or import a file*, und scanne den QR Code. -- *Zeus:* Kiere die URL unten, drücke *Add a new node*, *Import* (die URL), *Save Node Config*.""" +- *Zeus:* Kopiere die URL unten, drücke *Add a new node*, *Import* (die URL), *Save Node Config*.""" couldNotLinkMessage = """🚫 Konnte dein Wallet nicht verbinden. Bitte versuche es später noch einmal.""" # FAUCET @@ -259,7 +259,7 @@ inlineFaucetInvalidPeruserAmountMessage = """🚫 Die Pro-User Menge muss ein na inlineFaucetInvalidAmountMessage = """🚫 Ungültige Menge.""" inlineFaucetSentMessage = """🚰 %d sat an %s gesendet.""" inlineFaucetReceivedMessage = """🚰 %s hat dir %d sat gesendet.""" -inlineFaucetHelpFaucetInGroup = """Erzuge einen Zapfhahn in einer Gruppe, wo der Bot eingeladen ist oder benutze einen 👉 Inline Bommand (/advanced für mehr).""" +inlineFaucetHelpFaucetInGroup = """Erzeuge einen Zapfhahn in einer Gruppe, wo der Bot eingeladen ist oder benutze einen 👉 Inline Befehl (/advanced für mehr).""" inlineFaucetHelpText = """📖 Ups, das hat nicht geklappt. %s *Befehl:* `/faucet ` diff --git a/translations/tr.toml b/translations/tr.toml new file mode 100644 index 00000000..9da41302 --- /dev/null +++ b/translations/tr.toml @@ -0,0 +1,297 @@ +# COMMANDS + +helpCommandStr = """yardım""" +basicsCommandStr = """temelbilgi""" +tipCommandStr = """bahşiş""" +balanceCommandStr = """kredi""" +sendCommandStr = """gönder""" +invoiceCommandStr = """fatura""" +payCommandStr = """öde""" +donateCommandStr = """bağış""" +advancedCommandStr = """gelişmiş""" +transactionsCommandStr = """işlemler""" +logCommandStr = """kayıt""" +listCommandStr = """liste""" + +linkCommandStr = """bağlan""" +lnurlCommandStr = """lnurl""" +faucetCommandStr = """fıçı""" + +tipjarCommandStr = """bağışkutusu""" +receiveCommandStr = """iste""" +hideCommandStr = """sakla""" +volcanoCommandStr = """volkan""" +showCommandStr = """göster""" +optionsCommandStr = """seçenekler""" +settingsCommandStr = """ayarlar""" +saveCommandStr = """kaydet""" +deleteCommandStr = """sil""" +infoCommandStr = """info""" + +# NOTIFICATIONS + +cantDoThatMessage = """Bunu yapamazsın.""" +cantClickMessage = """Bu butona basamazsın.""" +balanceTooLowMessage = """Kredin yetersiz.""" + +# BUTTONS + +sendButtonMessage = """✅ Gönder""" +payButtonMessage = """✅ Öde""" +payReceiveButtonMessage = """💸 Öde""" +receiveButtonMessage = """✅ İste""" +cancelButtonMessage = """🚫 İptal""" +collectButtonMessage = """✅ Topla""" +nextButtonMessage = """İleri""" +backButtonMessage = """Geri""" +acceptButtonMessage = """Onayla""" +denyButtonMessage = """Reddet""" +tipButtonMessage = """Bahşiş""" +revealButtonMessage = """Göster""" +showButtonMessage = """Göster""" +hideButtonMessage = """Sakla""" +joinButtonMessage = """Katıl""" +optionsButtonMessage = """Seçenekler""" +settingsButtonMessage = """Ayarlar""" +saveButtonMessage = """Kaydet""" +deleteButtonMessage = """Sil""" +infoButtonMessage = """Info""" + +# HELP + +helpMessage = """⚡️ *Cüzdan* +_Bitcoin Lightning Cüzdan botu ile Telegram üzerinden bahşiş gönderebilirsin. Bahşiş göndermek için botu bir grup sohbetine ekle. Temel bahşiş birimi Satoshi’dir (sat). 100,000,000 sat = 1 Bitcoin. Daha fazla bilgi için 📚 /basics gir._ + +❤️ *Bağış yap* +_Bu bot ücretsizdir, ancak çalıştırılması için Satoshi gerekir. Botu beğendiysen bağış yaparak projeye destek olabilirsin. Bağış yapmak için şunu gir: _ `/donate 1000` + +%s + +⚙️ *Komutlar* +*/tip* 🏅 Bağış yapmak için mesajı cevapla: `/tip []` +*/balance* 👑 Kredini sorgula: `/balance` +*/send* 💸 Bir kullanıcıya gönder: `/send @user veya user@ln.tips []` +*/invoice* ⚡️ Lightning ile iste: `/invoice []` +*/pay* ⚡️ Lightning ile öde: `/pay ` +*/donate* ❤️ Projeye bağış yap: `/donate 1000` +*/advanced* 🤖 Gelişmiş fonksiyonlar. +*/help* 📖 Yardım.""" + +infoHelpMessage = """ℹ️ *Info*""" +infoYourLightningAddress = """Lightning adresin `%s`""" + +basicsMessage = """🧡 *Bitcoin* +_Bitcoin internetin para birimidir. Herkese açık, merkezi olmayan, kontrol edeni olmayan, klasik finansal sistemden daha hızlı, daha güvenli ve daha adil olan sağlam bir paradır._ + +🧮 *Ekonomi* +_En küçük Bitcoin birimi Satoshi’dir (sat). 100.000.000 sat = 1 Bitcoin. Asla 21 milyondan fazla Bitcoin üretilmeyecek. Bitcoin'in itibari para değeri günlük olarak değişebilir. Ancak Bitcoin Standardı kullanıyorsan, 1 sat sonsuza kadar 1 sat değerinde olacaktır._ + +⚡️ *Lightning Network* +_Lightning Network neredeyse hiç enerji gerektirmeyen hızlı ve ucuz Bitcoin ödemelerine izin veren bir protokolüdür. Böylece Bitcoin dünya çapında milyarlarca insana ulaşır._ + +📲 *Lightning Cüzdanlar* +_Bu bottaki paranı dünyadaki herhangi bir Lightning cüzdanına gönderebilirsin. Cep telefonunun için önerilen Lightning cüzdanları şunlardır:_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (emanet edilmeyen), veya_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(kolay)_. + +📄 *Açık Kaynak* +_Bu bot ücretsiz ve_ [açık kaynak](https://github.com/LightningTipBot/LightningTipBot) _yazılım. Kendi bilgisayarında çalıştırabilir ve topluluğun için kullanabilirsin._ + +✈️ *Telegram* +_/tip göndermek için botu Telegram grubunuza ekle. Bot grubun yöneticisiyse, yürütmeden sonra bazı komutları silerek sohbeti de temizleyecektir._ + +🏛 *Kullanım Şartları* +_Biz senin paranın emanetçisi değiliz. Senin faydanı gözetiyoruz. Ancak KYC'siz durumun biraz karmaşık olduğunun da farkındayız. Başka bir çözüm bulana kadar bu bottaki tüm tutarları bağış olarak kabul ediyoruz. Tüm paranı bize verme. Bu botun hala beta aşamasında olduğunu unutma. Kullanımın senin sorumluluğundadır._ + +❤️ *Bağış yap* +_Bu bot ücretsizdir, ancak çalıştırılması için Satoshi gerekir. Botu beğendiysen bağış yaparak projeye destek olabilirsin. Bağış yapmak için şunu gir:_ `/donate 1000`""" + +helpNoUsernameMessage = """👋 Ayarlarda bir Telegram kullanıcı adı seç.""" + +advancedMessage = """%s + +👉 *Inline komutlar* +*send* 💸 Bir sohbete sat gönder: `%s send []` +*receive* 🏅 Ödeme iste: `%s receive []` +*faucet* 🚰 Bir fıçı oluştur: `%s faucet ` + +📖 İnline komutları her sohbette ve hatta özel mesajlarda kullanabilirsin. Komutu yazdıktan sonra bir saniye bekle ve Enter yazmak yerine sonuca *tıkla*. + +⚙️ *Gelişmiş komutlar* +*/link* 🔗 Cüzdanını bağla: [BlueWallet](https://bluewallet.io/) veya [Zeus](https://zeusln.app/) +*/lnurl* ⚡️ Lnurl iste veya gönder: `/lnurl` veya `/lnurl ` +*/faucet* 🚰 Bir fıçı oluştur: `/faucet `""" + +# START + +startSettingWalletMessage = """🧮 Cüzdanın hazırlanıyor…""" +startWalletCreatedMessage = """🧮 Cüzdanın hazır.""" +startWalletReadyMessage = """✅ *Cüzdanın hazır.*""" +startWalletErrorMessage = """🚫 Cüzdanın hazırlanırken bir hata oluştu. Lütfen daha sonra tekrar dene.""" +startNoUsernameMessage = """☝️ Henüz bir Telegram kullanıcı @adın yok gibi görünüyor. Sorun değil, bu botu kullanmak için kullanıcı adına ihtiyacın yok. Ancak tüm fonksiyonları kullanabilmek için Telegram ayarlarında bir kullanıcı adı belirlemelisin. Ardından botun seninle ilgili bilgilerini güncelleyebilmesi için bir kez /balance gir.""" + +# BALANCE + +balanceMessage = """👑 *Kredin:* %d sat""" +balanceErrorMessage = """🚫 Şu an kredini okuyamıyorum. Lütfen daha sonra tekrar dene.""" + +# TIP + +tipDidYouReplyMessage = """Birine bahşiş göndermek için bir mesajı yanıtladın mı? Yanıtlamak için bilgisayarında mesaja sağ tıklayarak Yanıtla. Telefonda ise mesajı kaydır. Ödemeyi doğrudan başka bir kullanıcıya göndermek istiyorsan /send komutunu kullan.""" +tipInviteGroupMessage = """ℹ️ Bu arada, bu botu herhangi bir grup sohbetine davet edebilir ve orada bahşiş dağıtabilirsin.""" +tipEnterAmountMessage = """Miktar girdin mi?""" +tipValidAmountMessage = """Geçerli bir miktar girdin mi?""" +tipYourselfMessage = """📖 Kendi kendine bahşiş gönderemezsin.""" +tipSentMessage = """💸 %d sat %s a gönderildi.""" +tipReceivedMessage = """🏅 %s sana %d sat bahşiş gönderdi.""" +tipErrorMessage = """🚫 Bahşiş gönderilemedi.""" +tipUndefinedErrorMsg = """lütfen daha sonra tekrar dene.""" +tipHelpText = """📖 Hoppalaa… olmadı. %s + +*Komut:* `/tip []` +*Örnek:* `/tip 1000 Çok iyi yaa!`""" + +# SEND + +sendValidAmountMessage = """Geçerli bir miktar girdin mi?""" +sendUserHasNoWalletMessage = """🚫 %s henüz bir cüzdan oluşturmadı.""" +sendSentMessage = """💸 %d sat %s a gönderildi.""" +sendPublicSentMessage = """💸 %d sat %s dan %s a gönderildi.""" +sendReceivedMessage = """🏅 %s sana %d sat gönderdi.""" +sendErrorMessage = """🚫 Gönderim başarılı olmadı.""" +confirmSendMessage = """%s a ödeme göndermek istiyor musun?\n\n💸 Miktar: %d sat""" +confirmSendAppendMemo = """\n✉️ %s""" +sendCancelledMessage = """🚫 Gönderim iptal edildi.""" +errorTryLaterMessage = """🚫 Hata. Lütfen daha sonra tekrar dene.""" +sendSyntaxErrorMessage = """Geçerli bir miktar ve kullanıcı girdin mi? /send komutu ile bir Telegram kullanıcısına, örnek %s, veya bir Lightning adrese, örnek LightningTipBot@ln.tips, gönderilebilir.""" +sendHelpText = """📖 Hoppalaa… olmadı. %s + +*Komut:* `/send []` +*Örnek:* `/send 1000 @LightningTipBot Bot harika kanka! ❤️` +*Örnek:* `/send 1234 LightningTipBot@ln.tips`""" + +# INVOICE + +invoiceReceivedMessage = """⚡️ %d sat sana geldi.""" +invoiceEnterAmountMessage = """Miktar girdin mi?""" +invoiceValidAmountMessage = """Geçerli bir miktar girdin mi?""" +invoiceHelpText = """📖 Hoppalaa… olmadı. %s + +*Komut:* `/invoice []` +*Örnek:* `/invoice 1000 Şimdiden teşekkürler!`""" + +# PAY + +paymentCancelledMessage = """🚫 Ödeme iptal edildi.""" +invoicePaidMessage = """⚡️ Ödeme gönderildi.""" +invoicePublicPaidMessage = """⚡️ %s ödeme gönderdi.""" +invalidInvoiceHelpMessage = """Geçerli bir Lightning Invoice girdin mi? Telegram kullanıcısına veya Lightning adrese ödeme yapmak için /send komutunu kullan.""" +invoiceNoAmountMessage = """🚫 Miktar belirtilmemiş Invoice ödenemez.""" +insufficientFundsMessage = """🚫 Kredin yetersiz. Şu an cüzdanında %d sat var. Ancak en az %d sat gerekiyor.""" +feeReserveMessage = """⚠️ Cüzdanındaki tüm miktarı göndermek istiyorsun. Ancak Lightning ücreti ödenemeyeceği için gönderim başarısız olabilir. Bu durumda daha az bir miktar dene.""" +invoicePaymentFailedMessage = """🚫 Ödeme başarısız: %s""" +invoiceUndefinedErrorMessage = """Invoice ödenemedi.""" +confirmPayInvoiceMessage = """Bu ödemeyi göndermek istiyor musun?\n\n💸 Miktar: %d sat""" +confirmPayAppendMemo = """\n✉️ %s""" +payHelpText = """📖 Hoppalaa… olmadı. %s + +*Komut:* `/pay ` +*Örnek:* `/pay lnbc20n1psscehd...`""" + +# DONATE + +donationSuccess = """🙏 Bağış için teşekkürler.""" +donationErrorMessage = """🚫 Bağış yapılamadı.""" +donationProgressMessage = """🧮 Bağışın hazırlanıyor…""" +donationFailedMessage = """🚫 Bağış yapılamadı: %s""" +donateEnterAmountMessage = """Miktar girdin mi?""" +donateValidAmountMessage = """Geçerli bir miktar girdin mi?""" +donateHelpText = """📖 Hoppalaa… olmadı. %s + +*Komut:* `/donate ` +*Örnek:* `/donate 1000`""" + +# PHOTO + +photoQrNotRecognizedMessage = """🚫 Lightning Invoice veya LNURL tanımlanamadı. QR Kodunu ortalayarak, boyutlandırarak veya büyüterek tekrar dene.""" +photoQrRecognizedMessage = """✅ QR Code: +`%s`""" + +# LNURL + +lnurlReceiveInfoText = """👇 Bu LNURL kodunu ödeme almak için kullanabilirsin.""" +lnurlResolvingUrlMessage = """🧮 Adres çözümleniyor…""" +lnurlGettingUserMessage = """🧮 Ödeme hazırlanıyor...""" +lnurlPaymentFailed = """🚫 Ödeme başarısız: %s""" +lnurlInvalidAmountMessage = """🚫 Geçersiz miktar.""" +lnurlInvalidAmountRangeMessage = """🚫 Miktar %d ve %d sat arasında olmalı.""" +lnurlNoUsernameMessage = """🚫 LNURL ödemesi almak için bir Telegram kullanıcı ismi seçmelisin.""" +lnurlEnterAmountMessage = """⌨️ %d ve %d sat arasında bir miktar gir.""" +lnurlHelpText = """📖 Hoppalaa… olmadı. %s + +*Komut:* `/lnurl [miktar] ` +*Örnek:* `/lnurl LNURL1DP68GUR...`""" + +# LINK + +walletConnectMessage = """🔗 *Cüzdanını bağla* + +⚠️ Bu URL yi veya QR kodunu kimseyle paylaşma. Bu bilgiye ulaşan biri hesabına da ulaşabilir. + +- *BlueWallet:* *New wallet*, *Import wallet*, *Scan or import a file* tıkla ve QR kodunu tarat. +- *Zeus:* Aşağıdaki URL’yi kopyala ve *Add a new node*, *Import* (URL’yi), *Save Node Config* tıkla.""" +couldNotLinkMessage = """🚫 Cüzdanın bağlanamadı. Lütfen daha sonra tekrar dene.""" + +# FAUCET + +inlineQueryFaucetTitle = """🚰 Bir Fıçı oluştur.""" +inlineQueryFaucetDescription = """Komut: @%s faucet """ +inlineResultFaucetTitle = """🚰 Bir %d sat Fıçı oluştur.""" +inlineResultFaucetDescription = """👉 Fıçı’yı bu sohbete göndermek için tıkla.""" + +inlineFaucetMessage = """✅ butonuna basarak Fıçıdan %d sat çek. + +🚰 Kalan: %d/%d sat (%d/%d çekildi) +%s""" +inlineFaucetEndedMessage = """🚰 Fıçı boşaldı 🍺\n\n🏅 %d sat %d kullanıcıya dağıtıldı.""" +inlineFaucetAppendMemo = """\n✉️ %s""" +inlineFaucetCreateWalletMessage = """%s 👈 ile cüzdanı kullanmak için sohbete başla.""" +inlineFaucetCancelledMessage = """🚫 Fıçı iptal edildi.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 Toplam miktar bölü kullanıcı başına miktar tam sayı olmalı. Bu sayı küsuratlı olmaz.""" +inlineFaucetInvalidAmountMessage = """🚫 Geçersiz miktar.""" +inlineFaucetSentMessage = """🚰 %d sat %s a gönderildi.""" +inlineFaucetReceivedMessage = """🚰 %s sana %d sat gönderdi.""" +inlineFaucetHelpFaucetInGroup = """Fıçıyı İçinde bot olan bir grupta oluştur veya 👉 Inline komutunu kullan (/advanced daha fazla bilgi).""" +inlineFaucetHelpText = """📖 Hoppalaa… olmadı. %s + +*Komut:* `/faucet ` +*Örnek:* `/faucet 210 21`""" + +# INLINE SEND + +inlineQuerySendTitle = """💸 Sohbete ödeme gönder.""" +inlineQuerySendDescription = """Komut: @%s send []""" +inlineResultSendTitle = """💸 %d sat gönder.""" +inlineResultSendDescription = """👉 Sohbete %d sat göndermek için tıkla.""" + +inlineSendMessage = """✅ Butona basarak %s dan ödeme al.\n\n💸 Miktar: %d sat""" +inlineSendAppendMemo = """\n✉️ %s""" +inlineSendUpdateMessageAccept = """💸 %d sat %s dan %s ye gönderildi.""" +inlineSendCreateWalletMessage = """%s 👈 ile cüzdanı kullanmak için sohbete başla.""" +sendYourselfMessage = """📖 Kendi kendine ödeme yapamazsın.""" +inlineSendFailedMessage = """🚫 Gönderim başarısız.""" +inlineSendInvalidAmountMessage = """🚫 Miktar 0 dan büyük olmalı.""" +inlineSendBalanceLowMessage = """🚫 Kredin yetersiz.""" + +# INLINE RECEIVE + +inlineQueryReceiveTitle = """🏅 Sohbetten ödeme iste.""" +inlineQueryReceiveDescription = """Komut: @%s receive []""" +inlineResultReceiveTitle = """🏅 %d sat iste.""" +inlineResultReceiveDescription = """👉%d sat istemek için tıkla.""" + +inlineReceiveMessage = """💸 butonuna basarak %s a öde.\n\n💸 Miktar: %d sat""" +inlineReceiveAppendMemo = """\n✉️ %s""" +inlineReceiveUpdateMessageAccept = """💸 %d sat %s dan %s ye gönderildi.""" +inlineReceiveCreateWalletMessage = """%s 👈 ile cüzdanı kullanmak için sohbete başla.""" +inlineReceiveYourselfMessage = """📖 Kendi kendine ödeme yapamazsın.""" +inlineReceiveFailedMessage = """🚫 İstek başarısız.""" +inlineReceiveCancelledMessage = """🚫 İstek iptal edildi.""" From 236097a46151f68b3f8a4985480a4b3b51555116 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 17 Oct 2021 16:13:41 +0200 Subject: [PATCH 029/541] inline send and receive to specific user (#103) --- internal/telegram/handler.go | 3 +- internal/telegram/inline_receive.go | 79 ++++++++++++++++++++++------- internal/telegram/inline_send.go | 72 +++++++++++++++++++++----- translations/de.toml | 4 +- translations/en.toml | 4 +- translations/es.toml | 4 +- translations/fr.toml | 4 +- translations/it.toml | 4 +- translations/nl.toml | 4 +- translations/pt-br.toml | 4 +- translations/tr.toml | 4 +- 11 files changed, 137 insertions(+), 49 deletions(-) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 3fc8929d..ed20c2a0 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -6,6 +6,7 @@ import ( "strings" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" ) @@ -20,7 +21,7 @@ func (bot TipBot) registerTelegramHandlers() { telegramHandlerRegistration.Do(func() { // Set up handlers for _, h := range bot.getHandler() { - fmt.Println("registering", h.Endpoints) + log.Debugln("registering", h.Endpoints) bot.register(h) } diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index efdb2313..1287937d 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "strings" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -21,12 +22,13 @@ var ( type InlineReceive struct { *transaction.Base - Message string `json:"inline_receive_message"` - Amount int `json:"inline_receive_amount"` - From *lnbits.User `json:"inline_receive_from"` - To *lnbits.User `json:"inline_receive_to"` - Memo string - LanguageCode string `json:"languagecode"` + Message string `json:"inline_receive_message"` + Amount int `json:"inline_receive_amount"` + From *lnbits.User `json:"inline_receive_from"` + To *lnbits.User `json:"inline_receive_to"` + From_SpecificUser bool `json:"from_specific_user"` + Memo string `json:"inline_receive_memo"` + LanguageCode string `json:"languagecode"` } func (bot TipBot) makeReceiveKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { @@ -43,7 +45,7 @@ func (bot TipBot) makeReceiveKeyboard(ctx context.Context, id string) *tb.ReplyM } func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { - from := LoadUser(ctx) + to := LoadUser(ctx) amount, err := decodeAmountFromCommand(q.Text) if err != nil { bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineQueryReceiveTitle"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) @@ -53,15 +55,45 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) return } - fromUserStr := GetUserStr(&q.From) + toUserStr := GetUserStr(&q.From) + + // check whether the 3rd argument is a username + // command is "@LightningTipBot receive 123 @from_user This is the memo" + memo_argn := 2 // argument index at which the memo starts, will be 3 if there is a from_username in command + fromUserDb := &lnbits.User{} + from_SpecificUser := false + if len(strings.Split(q.Text, " ")) > 2 { + from_username := strings.Split(q.Text, " ")[2] + if strings.HasPrefix(from_username, "@") { + fromUserDb, err = GetUserByTelegramUsername(from_username[1:], bot) // must be without the @ + if err != nil { + //NewMessage(m, WithDuration(0, bot.Telegram)) + //bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), toUserStrMention)) + bot.inlineQueryReplyWithError(q, + fmt.Sprintf(TranslateUser(ctx, "sendUserHasNoWalletMessage"), from_username), + fmt.Sprintf(TranslateUser(ctx, "inlineQueryReceiveDescription"), + bot.Telegram.Me.Username)) + return + } + memo_argn = 3 // assume that memo starts after the from_username + from_SpecificUser = true + } + } + // check for memo in command - memo := GetMemoFromCommand(q.Text, 2) + memo := GetMemoFromCommand(q.Text, memo_argn) urls := []string{ queryImage, } results := make(tb.Results, len(urls)) // []tb.Result for i, url := range urls { - inlineMessage := fmt.Sprintf(Translate(ctx, "inlineReceiveMessage"), fromUserStr, amount) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineReceiveMessage"), toUserStr, amount) + + // modify message if payment is to specific user + if from_SpecificUser { + inlineMessage = fmt.Sprintf("@%s: %s", fromUserDb.Telegram.Username, inlineMessage) + } + if len(memo) > 0 { inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineReceiveAppendMemo"), memo) } @@ -80,14 +112,14 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { results[i].SetResultID(id) // create persistend inline send struct inlineReceive := InlineReceive{ - Base: transaction.New(transaction.ID(id)), - Message: inlineMessage, - To: from, - Memo: memo, - Amount: amount, - From: from, - - LanguageCode: ctx.Value("publicLanguageCode").(string), + Base: transaction.New(transaction.ID(id)), + Message: inlineMessage, + To: to, + Memo: memo, + Amount: amount, + From: fromUserDb, + From_SpecificUser: from_SpecificUser, + LanguageCode: ctx.Value("publicLanguageCode").(string), } runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) } @@ -130,6 +162,17 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac if from.Wallet == nil { return } + // check if this payment is requested from a specific user + if inlineReceive.From_SpecificUser { + if inlineReceive.From.Telegram.ID != from.Telegram.ID { + // log.Infof("User %d is not User %d", inlineReceive.From.Telegram.ID, from.Telegram.ID) + return + } + } else { + // otherwise, we just set it to the user who has clicked + inlineReceive.From = from + } + to := inlineReceive.To toUserStrMd := GetUserStrMd(to.Telegram) fromUserStrMd := GetUserStrMd(from.Telegram) diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 08dbab72..9ccb3f98 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -3,6 +3,8 @@ package telegram import ( "context" "fmt" + "strings" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -21,12 +23,13 @@ var ( type InlineSend struct { *transaction.Base - Message string `json:"inline_send_message"` - Amount int `json:"inline_send_amount"` - From *lnbits.User `json:"inline_send_from"` - To *tb.User `json:"inline_send_to"` - Memo string `json:"inline_send_memo"` - LanguageCode string `json:"languagecode"` + Message string `json:"inline_send_message"` + Amount int `json:"inline_send_amount"` + From *lnbits.User `json:"inline_send_from"` + To *lnbits.User `json:"inline_send_to"` + To_SpecificUser bool `json:"to_specific_user"` + Memo string `json:"inline_send_memo"` + LanguageCode string `json:"languagecode"` } func (bot TipBot) makeSendKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { @@ -69,14 +72,44 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) return } + + // check whether the 3rd argument is a username + // command is "@LightningTipBot send 123 @to_user This is the memo" + memo_argn := 2 // argument index at which the memo starts, will be 3 if there is a to_username in command + toUserDb := &lnbits.User{} + to_SpecificUser := false + if len(strings.Split(q.Text, " ")) > 2 { + to_username := strings.Split(q.Text, " ")[2] + if strings.HasPrefix(to_username, "@") { + toUserDb, err = GetUserByTelegramUsername(to_username[1:], bot) // must be without the @ + if err != nil { + //NewMessage(m, WithDuration(0, bot.Telegram)) + //bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), toUserStrMention)) + bot.inlineQueryReplyWithError(q, + fmt.Sprintf(TranslateUser(ctx, "sendUserHasNoWalletMessage"), to_username), + fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), + bot.Telegram.Me.Username)) + return + } + memo_argn = 3 // assume that memo starts after the to_username + to_SpecificUser = true + } + } + // check for memo in command - memo := GetMemoFromCommand(q.Text, 2) + memo := GetMemoFromCommand(q.Text, memo_argn) urls := []string{ queryImage, } results := make(tb.Results, len(urls)) // []tb.Result for i, url := range urls { inlineMessage := fmt.Sprintf(Translate(ctx, "inlineSendMessage"), fromUserStr, amount) + + // modify message if payment is to specific user + if to_SpecificUser { + inlineMessage = fmt.Sprintf("@%s: %s", toUserDb.Telegram.Username, inlineMessage) + } + if len(memo) > 0 { inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineSendAppendMemo"), memo) } @@ -96,12 +129,14 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { // add data to persistent object inlineSend := InlineSend{ - Base: transaction.New(transaction.ID(id)), - Message: inlineMessage, - From: fromUser, - Memo: memo, - Amount: amount, - LanguageCode: ctx.Value("publicLanguageCode").(string), + Base: transaction.New(transaction.ID(id)), + Message: inlineMessage, + From: fromUser, + To: toUserDb, + To_SpecificUser: to_SpecificUser, + Memo: memo, + Amount: amount, + LanguageCode: ctx.Value("publicLanguageCode").(string), } // add result to persistent struct @@ -145,7 +180,16 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) amount := inlineSend.Amount - inlineSend.To = to.Telegram + // check if this payment goes to a specific user + if inlineSend.To_SpecificUser { + if inlineSend.To.Telegram.ID != to.Telegram.ID { + // log.Infof("User %d is not User %d", inlineSend.To.Telegram.ID, to.Telegram.ID) + return + } + } else { + // otherwise, we just set it to the user who has clicked + inlineSend.To = to + } if fromUser.Telegram.ID == to.Telegram.ID { bot.trySendMessage(fromUser.Telegram, Translate(ctx, "sendYourselfMessage")) diff --git a/translations/de.toml b/translations/de.toml index abf03828..c4b35ee5 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -268,7 +268,7 @@ inlineFaucetHelpText = """📖 Ups, das hat nicht geklappt. %s # INLINE SEND inlineQuerySendTitle = """💸 Sende Zahlungen in einem Chat.""" -inlineQuerySendDescription = """Befehl: @%s send []""" +inlineQuerySendDescription = """Befehl: @%s send [] []""" inlineResultSendTitle = """💸 Sende %d sat.""" inlineResultSendDescription = """👉 Klicke hier um %d sat in diesen Chat zu senden.""" @@ -284,7 +284,7 @@ inlineSendBalanceLowMessage = """🚫 Dein Guthaben reicht nicht aus.""" # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Empfange eine Zahlung in einem Chat.""" -inlineQueryReceiveDescription = """Befehl: @%s receive []""" +inlineQueryReceiveDescription = """Befehl: @%s receive [] []""" inlineResultReceiveTitle = """🏅 Empfange %d sat.""" inlineResultReceiveDescription = """👉 Klicke hier um eine Zahlung von %d sat zu empfangen.""" diff --git a/translations/en.toml b/translations/en.toml index 4b1e1cb9..725a1a41 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -268,7 +268,7 @@ inlineFaucetHelpText = """📖 Oops, that didn't work. %s # INLINE SEND inlineQuerySendTitle = """💸 Send payment to a chat.""" -inlineQuerySendDescription = """Usage: @%s send []""" +inlineQuerySendDescription = """Usage: @%s send [] []""" inlineResultSendTitle = """💸 Send %d sat.""" inlineResultSendDescription = """👉 Click to send %d sat to this chat.""" @@ -284,7 +284,7 @@ inlineSendBalanceLowMessage = """🚫 Your balance is too low.""" # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Request a payment in a chat.""" -inlineQueryReceiveDescription = """Usage: @%s receive []""" +inlineQueryReceiveDescription = """Usage: @%s receive [] []""" inlineResultReceiveTitle = """🏅 Receive %d sat.""" inlineResultReceiveDescription = """👉 Click to request a payment of %d sat.""" diff --git a/translations/es.toml b/translations/es.toml index 542f5189..ec940ad0 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -268,7 +268,7 @@ inlineFaucetHelpText = """📖 Oops, eso no funcionó. %s # INLINE SEND inlineQuerySendTitle = """💸 Enviar el pago a un chat.""" -inlineQuerySendDescription = """Uso: @%s send []""" +inlineQuerySendDescription = """Uso: @%s send [] []""" inlineResultSendTitle = """💸 Enviar %d sat.""" inlineResultSendDescription = """👉 Haga clic para enviar %d sat a este chat.""" @@ -284,7 +284,7 @@ inlineSendBalanceLowMessage = """🚫 Tu saldo es demasiado bajo.""" # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Solicita un pago en un chat.""" -inlineQueryReceiveDescription = """Uso: @%s receive []""" +inlineQueryReceiveDescription = """Uso: @%s receive [] []""" inlineResultReceiveTitle = """🏅 Recibir %d sat.""" inlineResultReceiveDescription = """👉 Haga clic para solicitar un pago de %d sat.""" diff --git a/translations/fr.toml b/translations/fr.toml index 61a80922..3da66846 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -268,7 +268,7 @@ inlineFaucetHelpText = """📖 Oops, cela n'a pas fonctionné # INLINE SEND inlineQuerySendTitle = """💸 Envoyez des paiements dans un chat.""" -inlineQuerySendDescription = """Usage: @%s send []""" +inlineQuerySendDescription = """Usage: @%s send [] []""" inlineResultSendTitle = """💸 Envoyé %d sat.""" inlineResultSendDescription = """👉 Clique pour envoyer %d sat sur ce chat.""" @@ -284,7 +284,7 @@ inlineSendBalanceLowMessage = """🚫 Votre solde est trop bas.""" # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Demander un paiement dans un chat.""" -inlineQueryReceiveDescription = """Usage: @%s receive []""" +inlineQueryReceiveDescription = """Usage: @%s receive [] []""" inlineResultReceiveTitle = """🏅 Recevoir %d sat.""" inlineResultReceiveDescription = """👉 Cliquez ici pour recevoir un paiement de %d sat.""" diff --git a/translations/it.toml b/translations/it.toml index 2fdedb52..3fb4fa64 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -268,7 +268,7 @@ inlineFaucetHelpText = """📖 Ops, non ha funzionato. %s # INLINE SEND inlineQuerySendTitle = """💸 Invia pagamento in una chat.""" -inlineQuerySendDescription = """Sintassi: @%s send []""" +inlineQuerySendDescription = """Sintassi: @%s send [] []""" inlineResultSendTitle = """💸 Invio %d sat.""" inlineResultSendDescription = """👉 Clicca per inviare %d sat in questa chat.""" @@ -284,7 +284,7 @@ inlineSendBalanceLowMessage = """🚫 Il tuo saldo è insufficiente (%d sat). # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Richiedi un pagamento in una chat.""" -inlineQueryReceiveDescription = """Sintassi: @%s receive []""" +inlineQueryReceiveDescription = """Sintassi: @%s receive [] []""" inlineResultReceiveTitle = """🏅 Ricevi %d sat.""" inlineResultReceiveDescription = """👉 Clicca per richiedere un pagamento di %d sat.""" diff --git a/translations/nl.toml b/translations/nl.toml index ee14bd70..1ca6fcfb 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -268,7 +268,7 @@ inlineFaucetHelpText = """📖 Oeps, dat werkte niet. %s # INLINE VERZENDEN inlineQuerySendTitle = """💸 Stuur betaling naar een chat.""" -inlineQuerySendDescription = """Gebruik: @%s send []""" +inlineQuerySendDescription = """Gebruik: @%s send [] []""" inlineResultSendTitle = """💸 Stuur %d sat.""" inlineResultSendDescription = """👉 Klik om %d sat naar deze chat te sturen.""" @@ -284,7 +284,7 @@ inlineSendBalanceLowMessage = """🚫 Uw saldo is te laag.""" # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Vraag een betaling in een chat.""" -inlineQueryReceiveDescription = """Gebruik: @%s receive []""" +inlineQueryReceiveDescription = """Gebruik: @%s receive [] []""" inlineResultReceiveTitle = """🏅 Ontvang %d sat.""" inlineResultReceiveDescription = """👉 Klik om een betaling van %d sat aan te vragen.""" diff --git a/translations/pt-br.toml b/translations/pt-br.toml index 6b531899..95710f8b 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -268,7 +268,7 @@ inlineFaucetHelpText = """📖 Opa, isso não funcionou. %s # INLINE SEND inlineQuerySendTitle = """💸 Enviar pagamento para um bate-papo.""" -inlineQuerySendDescription = """Uso: @%s send []""" +inlineQuerySendDescription = """Uso: @%s send [] []""" inlineResultSendTitle = """💸 Enviar %d sat.""" inlineResultSendDescription = """👉 Clique para enviar %d sat neste bate-papo.""" @@ -284,7 +284,7 @@ inlineSendBalanceLowMessage = """🚫 Seu saldo é muito baixo.""" # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Solicite um pagamento em um bate-papo.""" -inlineQueryReceiveDescription = """Uso: @%s receive []""" +inlineQueryReceiveDescription = """Uso: @%s receive [] []""" inlineResultReceiveTitle = """🏅 Receber %d sat.""" inlineResultReceiveDescription = """👉 Clique para solicitar um pagamento de %d sat.""" diff --git a/translations/tr.toml b/translations/tr.toml index 9da41302..5a8adf0b 100644 --- a/translations/tr.toml +++ b/translations/tr.toml @@ -268,7 +268,7 @@ inlineFaucetHelpText = """📖 Hoppalaa… olmadı. %s # INLINE SEND inlineQuerySendTitle = """💸 Sohbete ödeme gönder.""" -inlineQuerySendDescription = """Komut: @%s send []""" +inlineQuerySendDescription = """Komut: @%s send [] []""" inlineResultSendTitle = """💸 %d sat gönder.""" inlineResultSendDescription = """👉 Sohbete %d sat göndermek için tıkla.""" @@ -284,7 +284,7 @@ inlineSendBalanceLowMessage = """🚫 Kredin yetersiz.""" # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Sohbetten ödeme iste.""" -inlineQueryReceiveDescription = """Komut: @%s receive []""" +inlineQueryReceiveDescription = """Komut: @%s receive [] []""" inlineResultReceiveTitle = """🏅 %d sat iste.""" inlineResultReceiveDescription = """👉%d sat istemek için tıkla.""" From 10c9121cc72a32d651fb878c92bd5d0b2bb6f80f Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 18 Oct 2021 10:30:07 +0200 Subject: [PATCH 030/541] lnurl for user id (#104) * lnurl for user id * adjust help --- internal/lnurl/lnurl.go | 12 +++++++++++- internal/telegram/help.go | 29 +++++++++++------------------ internal/telegram/lnurl.go | 9 ++++++--- translations/de.toml | 6 +++--- translations/en.toml | 8 ++++---- translations/es.toml | 6 +++--- translations/fr.toml | 6 +++--- translations/it.toml | 6 +++--- translations/nl.toml | 6 +++--- translations/pt-br.toml | 6 +++--- translations/tr.toml | 8 ++++---- 11 files changed, 54 insertions(+), 48 deletions(-) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index dbcf0b14..2fd840fd 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -126,7 +126,17 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) // now check for the user user := &lnbits.User{} - tx := w.database.Where("telegram_username = ?", strings.ToLower(username)).First(user) + // check if "username" is actually the user ID + id, err := strconv.Atoi(username) + tx := w.database + if err == nil { + // asume it's a user ID + tx = w.database.Where("telegram_id = ?", fmt.Sprint(id)).First(user) + } else { + // assume it's a string @username + tx = w.database.Where("telegram_username = ?", strings.ToLower(username)).First(user) + } + if tx.Error != nil { return &lnurl.LNURLPayResponse2{ LNURLResponse: lnurl.LNURLResponse{ diff --git a/internal/telegram/help.go b/internal/telegram/help.go index 1477c6f9..be7b5f4b 100644 --- a/internal/telegram/help.go +++ b/internal/telegram/help.go @@ -49,28 +49,21 @@ func (bot TipBot) basicsHandler(ctx context.Context, m *tb.Message) { func (bot TipBot) makeAdvancedHelpMessage(ctx context.Context, m *tb.Message) string { - dynamicHelpMessage := "" + dynamicHelpMessage := "ℹ️ *Info*\n" // user has no username set if len(m.Sender.Username) == 0 { // return fmt.Sprintf(helpMessage, fmt.Sprintf("%s\n\n", helpNoUsernameMessage)) - dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("%s", Translate(ctx, "helpNoUsernameMessage")) - } else { - dynamicHelpMessage = "ℹ️ *Info*\n" - lnaddr, err := bot.UserGetLightningAddress(m.Sender) - if err != nil { - dynamicHelpMessage = "" - } else { - dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("Your Lightning Address:\n`%s`\n", lnaddr) - } - - lnurl, err := UserGetLNURL(m.Sender) - if err != nil { - dynamicHelpMessage = "" - } else { - dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("Your LNURL:\n`%s`", lnurl) - } - + dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("%s", Translate(ctx, "helpNoUsernameMessage")) + "\n" } + lnaddr, err := bot.UserGetLightningAddress(m.Sender) + if err == nil { + dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("Lightning Address: `%s`\n", lnaddr) + } + lnurl, err := UserGetLNURL(m.Sender) + if err == nil { + dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("LNURL: `%s`", lnurl) + } + // this is so stupid: return fmt.Sprintf(Translate(ctx, "advancedMessage"), dynamicHelpMessage, GetUserStr(bot.Telegram.Me), GetUserStr(bot.Telegram.Me), GetUserStr(bot.Telegram.Me)) } diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 071d2379..328591db 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -6,13 +6,14 @@ import ( "encoding/json" "errors" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal" "io/ioutil" "net/http" "net/url" "strconv" "strings" + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" @@ -117,14 +118,16 @@ func (bot *TipBot) UserGetLightningAddress(user *tb.User) (string, error) { if len(user.Username) > 0 { return fmt.Sprintf("%s@%s", strings.ToLower(user.Username), strings.ToLower(internal.Configuration.Bot.LNURLHostUrl.Hostname())), nil } else { - return "", fmt.Errorf("user has no username") + return fmt.Sprintf("%s@%s", fmt.Sprint(user.ID), strings.ToLower(internal.Configuration.Bot.LNURLHostUrl.Hostname())), nil + // return "", fmt.Errorf("user has no username") } } func UserGetLNURL(user *tb.User) (string, error) { name := strings.ToLower(strings.ToLower(user.Username)) if len(name) == 0 { - return "", fmt.Errorf("user has no username.") + name = fmt.Sprint(user.ID) + // return "", fmt.Errorf("user has no username.") } callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, name) log.Debugf("[lnurlReceiveHandler] %s's LNURL: %s", GetUserStr(user), callback) diff --git a/translations/de.toml b/translations/de.toml index c4b35ee5..1aa1faaf 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -109,9 +109,9 @@ helpNoUsernameMessage = """👋 Wähle einen Benutzernamen in den Telegram Einst advancedMessage = """%s 👉 *Inline Befehle* -*send* 💸 Sende sats an einen Chat: `%s send []` -*receive* 🏅 Bitte um Zahlung: `%s receive []` -*faucet* 🚰 Erzeuge einen Zapfhahn: `%s faucet ` +*send* 💸 Sende sats an einen Chat: `%s send [] []` +*receive* 🏅 Bitte um Zahlung: `%s receive [] []` +*faucet* 🚰 Erzeuge einen Zapfhahn: `%s faucet []` 📖 Du kannst Inline Befehle in jedem Chat verwenden, sogar in privaten Nachrichten. Warte eine Sekunde, nachdem du den Befehl eingegeben hast und *klicke* auf das Ergebnis, statt Enter einzugeben. diff --git a/translations/en.toml b/translations/en.toml index 725a1a41..78897c5e 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -104,14 +104,14 @@ _We are not custodian of your funds. We will act in your best interest but we're ❤️ *Donate* _This bot charges no fees but costs satoshis to operate. If you like the bot, please consider supporting this project with a donation. To donate, use_ `/donate 1000`""" -helpNoUsernameMessage = """👋 Please set a Telegram username.""" +helpNoUsernameMessage = """👋 Please, set a Telegram username.""" advancedMessage = """%s 👉 *Inline commands* -*send* 💸 Send sats to chat: `%s send []` -*receive* 🏅 Request a payment: `%s receive []` -*faucet* 🚰 Create a faucet: `%s faucet ` +*send* 💸 Send sats to chat: `%s send [] []` +*receive* 🏅 Request a payment: `%s receive [] []` +*faucet* 🚰 Create a faucet: `%s faucet []` 📖 You can use inline commands in every chat, even in private conversations. Wait a second after entering an inline command and *click* the result, don't press enter. diff --git a/translations/es.toml b/translations/es.toml index ec940ad0..6f8962fc 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -109,9 +109,9 @@ helpNoUsernameMessage = """👋 Por favor, pon un nombre de usuario de Telegram. advancedMessage = """%s 👉 *Comandos Inline* -*send* 💸 Enviar sats al chat: `%s send []` -*receive* 🏅 Solicita un pago: `%s receive []`. -*faucet* 🚰 Crear un grifo: `%s faucet `. +*send* 💸 Enviar sats al chat: `%s send [] []` +*receive* 🏅 Solicita un pago: `%s receive [] []`. +*faucet* 🚰 Crear un grifo: `%s faucet []`. 📖 Puedes usar comandos _inline_ en todos los chats, incluso en las conversaciones privadas. Espera un segundo después de introducir un comando _inline_ y *haz clic* en el resultado, no pulses enter. diff --git a/translations/fr.toml b/translations/fr.toml index 3da66846..d080fd60 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -109,9 +109,9 @@ helpNoUsernameMessage = """👋 Définissez un nom d'utilisateur sur Telegram af advancedMessage = """%s 👉 *Inline commands* -*send* 💸 Envoyer des sats : `%s send []` -*receive* 🏅 Demander un paiement : `%s receive []` -*faucet* 🚰 Créer un faucet: `%s faucet ` +*send* 💸 Envoyer des sats : `%s send [] []` +*receive* 🏅 Demander un paiement : `%s receive [] []` +*faucet* 🚰 Créer un faucet: `%s faucet []` 📖 Vous pouvez utiliser ces commandes dans tous les chats et même dans les conversations privées. Attendez une seconde après avoir tapé une commandé puis *click* sur le résultat, n'appuyez pas sur entrée. diff --git a/translations/it.toml b/translations/it.toml index 3fb4fa64..913241e2 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -109,9 +109,9 @@ helpNoUsernameMessage = """👋 Per favore imposta un nome utente Telegram.""" advancedMessage = """%s 👉 *Comandi in linea* -*send* 💸 Invia alcuni sat a una chat: `%s send []` -*receive* 🏅 Richiedi un pagamento: `%s receive []` -*faucet* 🚰 Eroga fondi ai partecipanti della chat: `%s faucet ` +*send* 💸 Invia alcuni sat a una chat: `%s send [] []` +*receive* 🏅 Richiedi un pagamento: `%s receive [] []` +*faucet* 🚰 Eroga fondi ai partecipanti della chat: `%s faucet []` 📖 Puoi usare i comandi in linea in ogni chat, anche nelle conversazioni private. Attendi un secondo dopo aver inviato un comando in linea e *clicca* sull'azione desiderata, non premere invio. diff --git a/translations/nl.toml b/translations/nl.toml index 1ca6fcfb..9c87d686 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -109,9 +109,9 @@ helpNoUsernameMessage = """👋 Stel alstublieft een Telegram gebruikersnaam in. advancedMessage = """%s 👉 *Inline commands* -*send* 💸 Stuur sats naar chat: `%s send []` -*receive* 🏅 Verzoek om betaling: `%s receive []` -*faucet* 🚰 Maak een kraan: `%s faucet ` +*send* 💸 Stuur sats naar chat: `%s send [] []` +*receive* 🏅 Verzoek om betaling: `%s receive [] []` +*faucet* 🚰 Maak een kraan: `%s faucet []` 📖 Je kunt inline commando's in elke chat gebruiken, zelfs in privé gesprekken. Wacht een seconde na het invoeren van een inline commando en *klik* op het resultaat, druk niet op enter. diff --git a/translations/pt-br.toml b/translations/pt-br.toml index 95710f8b..410fec00 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -109,9 +109,9 @@ helpNoUsernameMessage = """👋 Por favor, digite um nome de usuário do Telegra advancedMessage = """%s 👉 *Comandos Inline* -*send* 💸 Enviar sats ao bate-papo: `%s send []` -*receive* 🏅 Solicite um pagamento: `%s receive []`. -*faucet* 🚰 Criar uma torneira: `%s faucet `. +*send* 💸 Enviar sats ao bate-papo: `%s send [] []` +*receive* 🏅 Solicite um pagamento: `%s receive [] []`. +*faucet* 🚰 Criar uma torneira: `%s faucet []`. 📖 Você pode usar comandos _inline_ em todas as conversas, mesmo em conversas privadas. Espere um segundo após inserir um comando _inline_ e *clique* no resultado, não pressione enter. diff --git a/translations/tr.toml b/translations/tr.toml index 5a8adf0b..03f09648 100644 --- a/translations/tr.toml +++ b/translations/tr.toml @@ -109,10 +109,10 @@ helpNoUsernameMessage = """👋 Ayarlarda bir Telegram kullanıcı adı seç.""" advancedMessage = """%s 👉 *Inline komutlar* -*send* 💸 Bir sohbete sat gönder: `%s send []` -*receive* 🏅 Ödeme iste: `%s receive []` -*faucet* 🚰 Bir fıçı oluştur: `%s faucet ` - +*send* 💸 Bir sohbete sat gönder: `%s send [] []` +*receive* 🏅 Ödeme iste: `%s receive [] []` +*faucet* 🚰 Bir fıçı oluştur: `%s faucet []` + 📖 İnline komutları her sohbette ve hatta özel mesajlarda kullanabilirsin. Komutu yazdıktan sonra bir saniye bekle ve Enter yazmak yerine sonuca *tıkla*. ⚙️ *Gelişmiş komutlar* From bb002531229b66558caf7fce090aa1ad73202632 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 18 Oct 2021 20:46:59 +0200 Subject: [PATCH 031/541] Translation Indonesian (#106) * Translation to Indonesian language First time using github but I think I did everything correctly though. Truly sorry if I've made a noob mistake Trying to make the language as friendly yet polite and as formal as possible Thanx and keep up the great work * inline commands in english * readd en.toml * enable translation Co-authored-by: hmalau <92701132+hmalau@users.noreply.github.com> --- internal/i18n/localize.go | 1 + translations/id.toml | 297 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 translations/id.toml diff --git a/internal/i18n/localize.go b/internal/i18n/localize.go index b2a40913..a70c25b5 100644 --- a/internal/i18n/localize.go +++ b/internal/i18n/localize.go @@ -24,6 +24,7 @@ func RegisterLanguages() *i18n.Bundle { bundle.LoadMessageFile("translations/fr.toml") bundle.LoadMessageFile("translations/pt-br.toml") bundle.LoadMessageFile("translations/tr.toml") + bundle.LoadMessageFile("translations/id.toml") return bundle } func Translate(languageCode string, MessgeID string) string { diff --git a/translations/id.toml b/translations/id.toml new file mode 100644 index 00000000..57cc1c3b --- /dev/null +++ b/translations/id.toml @@ -0,0 +1,297 @@ +# COMMANDS + +helpCommandStr = """bantuan""" +basicsCommandStr = """dasar""" +tipCommandStr = """tip""" +balanceCommandStr = """saldo""" +sendCommandStr = """kirim""" +invoiceCommandStr = """invoice""" +payCommandStr = """bayar""" +donateCommandStr = """donasi""" +advancedCommandStr = """lanjutan""" +transactionsCommandStr = """transaksi""" +logCommandStr = """log""" +listCommandStr = """daftar""" + +linkCommandStr = """link""" +lnurlCommandStr = """lnurl""" +faucetCommandStr = """keran""" + +tipjarCommandStr = """tipjar""" +receiveCommandStr = """terima""" +hideCommandStr = """sembunyikan""" +volcanoCommandStr = """volcano""" +showCommandStr = """munculkan""" +optionsCommandStr = """pilihan""" +settingsCommandStr = """pengaturan""" +saveCommandStr = """simpan""" +deleteCommandStr = """hapus""" +infoCommandStr = """info""" + +# NOTIFICATIONS + +cantDoThatMessage = """Kamu tidak bisa melakukan itu.""" +cantClickMessage = """Kamu tidak dapat menekan tombol itu.""" +balanceTooLowMessage = """Saldo mu terlalu rendah.""" + +# BUTTONS + +sendButtonMessage = """✅ Kirim""" +payButtonMessage = """✅ Bayar""" +payReceiveButtonMessage = """💸 Bayar""" +receiveButtonMessage = """✅ Terima""" +cancelButtonMessage = """🚫 Batal""" +collectButtonMessage = """✅ Ambil""" +nextButtonMessage = """Lanjut""" +backButtonMessage = """Kembali""" +acceptButtonMessage = """Setuju""" +denyButtonMessage = """Tolak""" +tipButtonMessage = """Tip""" +revealButtonMessage = """Perlihatkan""" +showButtonMessage = """Munculkan""" +hideButtonMessage = """Sembunyikan""" +joinButtonMessage = """Bergabung""" +optionsButtonMessage = """Pilihan""" +settingsButtonMessage = """Pengaturan""" +saveButtonMessage = """Simpan""" +deleteButtonMessage = """Hapus""" +infoButtonMessage = """Info""" + +# HELP + +helpMessage = """⚡️ *Dompet* +_Bot ini adalah sebuah dompet Bitcoin Lightning yang dapat mengirimkan tip melalui Telegram. Untuk memberi tip, tambahkan bot ke chat grup. Unit dasar dari tip adalah Satoshi (sat). 100,000,000 sat = 1 Bitcoin. Ketik 📚 /basics untuk informasi lebih banyak._ + +❤️ *Donasi* +_Bot ini tidak memungut biaya tapi memerlukan Satoshi untuk bekerja. Kalau kamu menyukai bot ini, tolong pertimbangkan mendukung proyek ini dengan donasi. Untuk melakukan donasi, pakai_ `/donate 1000` + +%s + +⚙️ *Commands* +*/tip* 🏅 Membalas sebuah pesan untuk memberi tip: `/tip []` +*/balance* 👑 Memeriksa saldo mu: `/balance` +*/send* 💸 Mengirim dana ke seorang pengguna: `/send @user or user@ln.tips []` +*/invoice* ⚡️ Menerima menggunakan Lightning: `/invoice []` +*/pay* ⚡️ Membayar dengan Lightning: `/pay ` +*/donate* ❤️ Memberi donasi ke proyek: `/donate 1000` +*/advanced* 🤖 Fitur lanjutan. +*/help* 📖 Membaca daftar bantuan.""" + +infoHelpMessage = """ℹ️ *Info*""" +infoYourLightningAddress = """Alamat Lightning mu adalah `%s`""" + +basicsMessage = """🧡 *Bitcoin* +_Bitcoin adalah mata uang dari internet. Tidak memerlukan ijin dan terdesentralisasi dan tidak memiliki penguasa dan tidak ada otoritas yang mengatur. Bitcoin adalah uang yang tangguh yang lebih cepat, lebih aman, dan lebih inklusif dibanding sistem keuangan tradisional._ + +🧮 *Ekonomi* +_Unit terkecil dari Bitcoin adalah Satoshi (sat) dan 100,000,000 sat = 1 Bitcoin. Hanya akan pernah ada 21 Juta Bitcoin. Nilai Bitcoin dalam mata uang fiat dapat berubah tiap hari. Namun, jika kamu hidup dalam sebuah standar Bitcoin maka 1 sat akan selalu sama dengan 1 sat._ + +⚡️ *The Lightning Network* +_Lightning Network adalah sebuah protokol pembayaran yang memungkinkan pembayaran Bitcoin dengan cepat dan murah yang memerlukan hampir tidak ada energi. Ini lah yang membesarkan skala Bitcoin ke milyaran orang diseluruh dunia._ + +📲 *Lightning Wallets* +_Dana mu di bot ini dapat dikirim ke dompet Lightning manapun lainnya dan kebalikannya. Rekomendasi dompet Lightning untuk handphone mu adalah_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (non-custodial), or_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(easy)_. + +📄 *Open Source* +_Bot ini gratis dan merupakan perangkat lunak_ [open source](https://github.com/LightningTipBot/LightningTipBot) _. Kamu dapat menjalankannya di komputer mu sendiri dan menggunakannya di komunitas mu._ + +✈️ *Telegram* +_Tambahkan bot ini ke percakapan grup Telegram mu untuk /tip postingan. Jika kamu membuat bot ini sebagai admin maka akan membersihkan perintah untuk agar percakapan tetap rapi._ + +🏛 *Persyaratan* +_Kami tidak menyimpan dana mu. Kami akan bertindak yang terbaik untuk kamu namun kami juga sadar bahwa situasi tanpa KYC agak sulit sampai kami bisa memikirkan sesuatu. Berapa pun jumlah yang kamu masukkan ke dompet mu akan dianggap sebagai donasi. Jangan kirim ke kami semua uang mu. Tolong sadar bahwa bot ini masih dalam perkembangan beta. Pergunakan lah dengan resiko mu sendiri._ + +❤️ *Donasi* +_Bot ini tidak memungut biaya tapi memerlukan Satoshi untuk bekerja. Kalau kamu menyukai bot ini, tolong pertimbangkan mendukung proyek ini dengan donasi. Untuk melakukan donasi, pakai_ `/donate 1000`""" + +helpNoUsernameMessage = """👋 Silakan mengatur nama pengguna untuk Telegram.""" + +advancedMessage = """%s + +👉 *Sebaris perintah* +*send* 💸 Kirim sats ke percakapan: `%s send []` +*receive* 🏅 Permintaan pembayaran: `%s receive []` +*faucet* 🚰 Membuat sebuah keran: `%s faucet ` + +📖 Kamu dapat menggunakan sebaris perintah di tiap percakapan, bahkan di percakapan privat. Tunggu sejenak setelah memasukkan sebaris perintah lalu *pencet* hasilnya, jangan tekan enter. + +⚙️ *Perintah lanjutan* +*/link* 🔗 Menghubungkan dompet mu ke [BlueWallet](https://bluewallet.io/) atau [Zeus](https://zeusln.app/) +*/lnurl* ⚡️ Lnurl menerima atau membayar: `/lnurl` atau `/lnurl ` +*/faucet* 🚰 Membuat sebuah keran `/faucet `""" + +# START + +startSettingWalletMessage = """🧮 Sedang menyiapkan dompet mu...""" +startWalletCreatedMessage = """🧮 Dompet sudah dibuat.""" +startWalletReadyMessage = """✅ *Dompet mu sudah siap.*""" +startWalletErrorMessage = """🚫 Ada kesalahan dalam pembuatan dompet mu. Silakan coba lagi nanti.""" +startNoUsernameMessage = """☝️ Kelihatannya kamu belum memiliki sebuah @username Telegram. Tidak apa-apa, kamu tidak memerlukan nya untuk menggunakan bot ini. Namun, agar dompet mu bisa lebih berguna, atur nama pengguna di pengaturan Telegram. Lalu, ketik /balance agar bot dapat memperbaharui catatan nya mengenai kamu.""" + +# BALANCE + +balanceMessage = """👑 *Saldo mu:* %d sat""" +balanceErrorMessage = """🚫 Tidak dapat memeriksa saldo mu. Tolong coba lagi nanti.""" + +# TIP + +tipDidYouReplyMessage = """Apakah kamu tadi membalas pesan untuk memberikan tip? Untuk membalas pesan mana pun, klik-kanan -> Ketik balasan pada komputer mu atau geser pesan yang ada di handphone mu. Jika kamu mau mengirimkan secara langsung, gunakan perintah /send .""" +tipInviteGroupMessage = """ℹ️ Ngomong-ngomong, kamu bisa mengundang bot ini ke grup mana saja untuk mulai memberikan tip disana.""" +tipEnterAmountMessage = """Apakah kamu sudah memasukkan jumlah?""" +tipValidAmountMessage = """Apakah kamu memasukkan jumlah yang benar?""" +tipYourselfMessage = """📖 Kamu tidak dapat memberi tip ke diri sendiri.""" +tipSentMessage = """💸 %d sat terkirim ke %s.""" +tipReceivedMessage = """🏅 %s telah memberi tip sebanyak %d sat.""" +tipErrorMessage = """🚫 Pengiriman tip gagal.""" +tipUndefinedErrorMsg = """Silakan coba lagi nanti.""" +tipHelpText = """📖 Waduh, itu tidak berhasil. %s + +*Usage:* `/tip []` +*Example:* `/tip 1000 Makasih!`""" + +# SEND + +sendValidAmountMessage = """Apakah kamu memasukkan jumlah yang benar?""" +sendUserHasNoWalletMessage = """🚫 Pengguna %s belum membuat dompet.""" +sendSentMessage = """💸 %d sat terkirim ke %s.""" +sendPublicSentMessage = """💸 %d sat terkirim dari %s ke %s.""" +sendReceivedMessage = """🏅 %s mengirimkan kamu %d sat.""" +sendErrorMessage = """🚫 Pengiriman gagal.""" +confirmSendMessage = """Apakah kamu ingin membayar ke %s?\n\n💸 Jumlah: %d sat""" +confirmSendAppendMemo = """\n✉️ %s""" +sendCancelledMessage = """🚫 Pengiriman dibatalkan.""" +errorTryLaterMessage = """🚫 Error. Silakan coba lagi nanti.""" +sendSyntaxErrorMessage = """Apakah kamu sudah memasukkan jumlah dan penerima? Kamu dapat menggunakan perintah /send untuk ke pengguna Telegram seperti %s atau ke alamat Lightning seperti LightningTipBot@ln.tips.""" +sendHelpText = """📖 Waduh, itu tidak berhasil. %s + +*Usage:* `/send []` +*Example:* `/send 1000 @LightningTipBot Gampang botnya dipakai ❤️` +*Example:* `/send 1234 LightningTipBot@ln.tips`""" + +# INVOICE + +invoiceReceivedMessage = """⚡️ Kamu menerima %d sat.""" +invoiceEnterAmountMessage = """Apakah kamu sudah memasukkan jumlah?""" +invoiceValidAmountMessage = """Apakah kamu memasukkan jumlah yang benar?""" +invoiceHelpText = """📖 Waduh, itu tidak berhasil. %s + +*Usage:* `/invoice []` +*Example:* `/invoice 1000 Makasih!`""" + +# PAY + +paymentCancelledMessage = """🚫 Pembayaran dibatalkan.""" +invoicePaidMessage = """⚡️ Pembayaran terkirim.""" +invoicePublicPaidMessage = """⚡️ Pembayaran dikirim oleh %s.""" +invalidInvoiceHelpMessage = """Apakah kamu memasukan invoice Lightning yang benar? Coba /send jika kamu mau mengirimkan ke pengguna Telegram atau alamat Lightning.""" +invoiceNoAmountMessage = """🚫 Tidak dapat membayar invoice tanpa jumlah.""" +insufficientFundsMessage = """🚫 Kekurangan dana. Kamu memiliki %d sat namun kamu memerlukan setidaknya %d sat.""" +feeReserveMessage = """⚠️ Mengirimkan seluruh saldo bisa gagal karena adanya biaya jaringan. Jika gagal, coba jumlah kirimnya dikurangi sedikit.""" +invoicePaymentFailedMessage = """🚫 Pembayaran gagal: %s""" +invoiceUndefinedErrorMessage = """Tidak dapat membayar invoice.""" +confirmPayInvoiceMessage = """Apakah kamu mau mengirimkan pembayaran ini?\n\n💸 Jumlah: %d sat""" +confirmPayAppendMemo = """\n✉️ %s""" +payHelpText = """📖 Waduh, itu tidak berhasil. %s + +*Usage:* `/pay ` +*Example:* `/pay lnbc20n1psscehd...`""" + +# DONATE + +donationSuccess = """🙏 Terima kasih untuk donasi mu.""" +donationErrorMessage = """🚫 Waduh. Donasi gagal.""" +donationProgressMessage = """🧮 Menyiapkan donasi mu...""" +donationFailedMessage = """🚫 Donasi gagal: %s""" +donateEnterAmountMessage = """Apakah kamu sudah memasukkan jumlah?""" +donateValidAmountMessage = """Apakah kamu memasukkan jumlah yang benar?""" +donateHelpText = """📖 Waduh, itu tidak berhasil. %s + +*Usage:* `/donate ` +*Example:* `/donate 1000`""" + +# PHOTO + +photoQrNotRecognizedMessage = """🚫 Tidak dapat mengenali sebuah invoice Lightning invoice atau sebuah LNURL. Coba arahkan ke tengah QR code, potong foto kodenya, atau zoom in.""" +photoQrRecognizedMessage = """✅ QR code: +`%s`""" + +# LNURL + +lnurlReceiveInfoText = """👇 Kamu dapat menggunakan LNURL statis ini untuk menerima pembayaran.""" +lnurlResolvingUrlMessage = """🧮 Menyelesaikan alamat...""" +lnurlGettingUserMessage = """🧮 Mempersiapkan pembayaran...""" +lnurlPaymentFailed = """🚫 Pembayaran gagal: %s""" +lnurlInvalidAmountMessage = """🚫 Jumlah tidak benar.""" +lnurlInvalidAmountRangeMessage = """🚫 Jumlah harus diantara %d dan %d sat.""" +lnurlNoUsernameMessage = """🚫 Kamu harus mengatur nama pengguna Telegram untuk menerima pembayaran melalui LNURL.""" +lnurlEnterAmountMessage = """⌨️ Masukkan jumlah diantara %d dan %d sat.""" +lnurlHelpText = """📖 Waduh, itu tidak berhasil. %s + +*Usage:* `/lnurl [jumlah] ` +*Example:* `/lnurl LNURL1DP68GUR...`""" + +# LINK + +walletConnectMessage = """🔗 *Hubungkan dompet mu* + +⚠️ Jangan pernah membagikan URL atau kode QR nya dengan siapa pun karena mereka akan bisa mengakses dana mu. + +- *BlueWallet:* Tekan *New wallet*, *Import wallet*, *Scan atau import a file*, lalu scan kode QR nya. +- *Zeus:* Salin URL dibawah, tekan *Add a new node*, *Import* (URL nya), *Save Node Config*.""" +couldNotLinkMessage = """🚫 Tidak dapat menghubungkan dompet mu. Silakan coba lagi nanti.""" + +# FAUCET + +inlineQueryFaucetTitle = """🚰 Membuat sebuah keran.""" +inlineQueryFaucetDescription = """Penggunaan: @%s faucet """ +inlineResultFaucetTitle = """🚰 Buat sebuah keran %d sat.""" +inlineResultFaucetDescription = """👉 Pencet disini untuk membuat sebuah keran di percakapan ini.""" + +inlineFaucetMessage = """Tekan ✅ untuk mengambil %d sat dari keran ini. + +🚰 Tersisa: %d/%d sat (diberikan kepada %d/%d pengguna) +%s""" +inlineFaucetEndedMessage = """🚰 Keran kosong 🍺\n\n🏅 %d sat diberikan kepada %d pengguna.""" +inlineFaucetAppendMemo = """\n✉️ %s""" +inlineFaucetCreateWalletMessage = """Bercakap dengan %s 👈 untuk mengelola dompet mu.""" +inlineFaucetCancelledMessage = """🚫 Keran dibatalkan.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 Jumlah untuk tiap pengguna tidak terbagi dari kapasitas.""" +inlineFaucetInvalidAmountMessage = """🚫 Jumlah tidak benar.""" +inlineFaucetSentMessage = """🚰 %d sat terkirim ke %s.""" +inlineFaucetReceivedMessage = """🚰 %s mengirimkan kamu %d sat.""" +inlineFaucetHelpFaucetInGroup = """Buat sebuah keran dalam sebuah grup dengan bot nya didalam atau gunakan 👉 perintah sebaris (/advanced untuk lebih lagi).""" +inlineFaucetHelpText = """📖 Waduh, itu tidak berhasil. %s + +*Usage:* `/faucet ` +*Example:* `/faucet 210 21`""" + +# INLINE SEND + +inlineQuerySendTitle = """💸 Kirimkan pembayaran ke sebuah percakapan.""" +inlineQuerySendDescription = """Penggunaan: @%s send [] []""" +inlineResultSendTitle = """💸 Kirim %d sat.""" +inlineResultSendDescription = """👉 Pencet untuk mengirim %d sat ke percakapan ini.""" + +inlineSendMessage = """Tekan ✅ untuk menerima pembayaran dari %s.\n\n💸 Jumlah: %d sat""" +inlineSendAppendMemo = """\n✉️ %s""" +inlineSendUpdateMessageAccept = """💸 %d sat terkirim dari %s ke %s.""" +inlineSendCreateWalletMessage = """Bercakap dengan %s 👈 untuk mengelola dompet mu.""" +sendYourselfMessage = """📖 Kamu tidak dapat membayar diri mu sendiri.""" +inlineSendFailedMessage = """🚫 Pengiriman tidak berhasil.""" +inlineSendInvalidAmountMessage = """🚫 Jumlah harus lebih besar dari 0.""" +inlineSendBalanceLowMessage = """🚫 Saldo mu terlalu rendah.""" + +# INLINE RECEIVE + +inlineQueryReceiveTitle = """🏅 Meminta pembayaran dalam sebuah percakapan.""" +inlineQueryReceiveDescription = """Penggunaan: @%s receive [] []""" +inlineResultReceiveTitle = """🏅 Menerima %d sat.""" +inlineResultReceiveDescription = """👉 Pencet untuk permintaan pembayaran sebesar %d sat.""" + +inlineReceiveMessage = """Tekan 💸 untuk membayar ke %s.\n\n💸 Jumlah: %d sat""" +inlineReceiveAppendMemo = """\n✉️ %s""" +inlineReceiveUpdateMessageAccept = """💸 %d sat terkirim oleh %s ke %s.""" +inlineReceiveCreateWalletMessage = """Bercakap dengan %s 👈 untuk mengelola dompet mu.""" +inlineReceiveYourselfMessage = """📖 Kamu tidak dapat membayar diri mu sendiri.""" +inlineReceiveFailedMessage = """🚫 Penerimaan gagal.""" +inlineReceiveCancelledMessage = """🚫 Penerimaan dibatalkan.""" From fd9ad39b0c2ac1bcdff2285e64971963d80ab88d Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 24 Oct 2021 20:31:52 +0200 Subject: [PATCH 032/541] anonymous user ID for lnurl (#107) * anonymous user ID for lnurl * anonymous lnurl * migrate at every start, not good * anon id at user creation * migrations file * fix database migration task * remove comment * anon label Co-authored-by: lngohumble --- internal/database/migrations.go | 26 ++++++++++++++++++++ internal/lnbits/types.go | 1 + internal/lnurl/lnurl.go | 2 +- internal/str/strings.go | 13 ++++++++++ internal/telegram/bot.go | 4 ++-- internal/telegram/database.go | 42 +++++++++++++++++++++++++-------- internal/telegram/help.go | 12 ++++++---- internal/telegram/lnurl.go | 34 ++++++++++++++++---------- internal/telegram/start.go | 5 +++- internal/telegram/users.go | 9 +++---- 10 files changed, 112 insertions(+), 36 deletions(-) create mode 100644 internal/database/migrations.go diff --git a/internal/database/migrations.go b/internal/database/migrations.go new file mode 100644 index 00000000..38536808 --- /dev/null +++ b/internal/database/migrations.go @@ -0,0 +1,26 @@ +package database + +import ( + "fmt" + + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/str" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +func MigrateAnonIdHash(db *gorm.DB) error { + users := []lnbits.User{} + _ = db.Find(&users) + for _, u := range users { + log.Info(u.ID, str.Int32Hash(u.ID)) + u.AnonID = fmt.Sprint(str.Int32Hash(u.ID)) + tx := db.Save(u) + if tx.Error != nil { + errmsg := fmt.Sprintf("[MigrateAnonIdHash] Error: Couldn't migrate user %s (%d)", u.Telegram.Username, u.Telegram.ID) + log.Errorln(errmsg) + return tx.Error + } + } + return nil +} diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index ea792add..ab671412 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -24,6 +24,7 @@ type User struct { StateData string `json:"stateData"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` + AnonID string `jsin:"anonid"` } const ( diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 2fd840fd..129974f5 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -131,7 +131,7 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) tx := w.database if err == nil { // asume it's a user ID - tx = w.database.Where("telegram_id = ?", fmt.Sprint(id)).First(user) + tx = w.database.Where("anon_id = ?", fmt.Sprint(id)).First(user) } else { // assume it's a string @username tx = w.database.Where("telegram_username = ?", strings.ToLower(username)).First(user) diff --git a/internal/str/strings.go b/internal/str/strings.go index 07c8fde8..c1e5d4b1 100644 --- a/internal/str/strings.go +++ b/internal/str/strings.go @@ -2,6 +2,7 @@ package str import ( "fmt" + "hash/fnv" "strings" ) @@ -25,3 +26,15 @@ func MarkdownEscape(s string) string { } return s } + +func Int32Hash(s string) uint32 { + h := fnv.New32a() + h.Write([]byte(s)) + return h.Sum32() +} + +func Int64Hash(s string) uint64 { + h := fnv.New64a() + h.Write([]byte(s)) + return h.Sum64() +} diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 4eeb0353..d201e8d3 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -2,10 +2,10 @@ package telegram import ( "fmt" - "github.com/LightningTipBot/LightningTipBot/internal" "sync" "time" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/storage" log "github.com/sirupsen/logrus" @@ -30,7 +30,7 @@ var ( // NewBot migrates data and creates a new bot func NewBot() TipBot { // create sqlite databases - db, txLogger := migration() + db, txLogger := AutoMigration() return TipBot{ Database: db, Client: lnbits.NewClient(internal.Configuration.Lnbits.AdminKey, internal.Configuration.Lnbits.Url), diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 1e929fa5..2a504a2b 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -2,14 +2,16 @@ package telegram import ( "fmt" - "github.com/LightningTipBot/LightningTipBot/internal" - "github.com/LightningTipBot/LightningTipBot/internal/storage" - "github.com/tidwall/buntdb" "reflect" "strconv" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/database" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/tidwall/buntdb" + log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -33,21 +35,41 @@ func createBunt() *storage.DB { } return bunt } -func migration() (db *gorm.DB, txLogger *gorm.DB) { - txLogger, err := gorm.Open(sqlite.Open(internal.Configuration.Database.TransactionsPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) - if err != nil { - panic("Initialize orm failed.") + +func ColumnMigrationTasks(db *gorm.DB) error { + var err error + if !db.Migrator().HasColumn(&lnbits.User{}, "anon_id") { + // first we need to auto migrate the user. This will create anon_id column + err = db.AutoMigrate(&lnbits.User{}) + if err != nil { + panic(err) + } + log.Info("Running ano_id database migrations ...") + // run the migration on anon_id + err = database.MigrateAnonIdHash(db) } + // todo -- add more database field migrations here in the future + return err +} +func AutoMigration() (db *gorm.DB, txLogger *gorm.DB) { orm, err := gorm.Open(sqlite.Open(internal.Configuration.Database.DbPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) if err != nil { panic("Initialize orm failed.") } - + err = ColumnMigrationTasks(orm) + if err != nil { + panic(err) + } err = orm.AutoMigrate(&lnbits.User{}) if err != nil { panic(err) } + + txLogger, err = gorm.Open(sqlite.Open(internal.Configuration.Database.TransactionsPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) + if err != nil { + panic("Initialize orm failed.") + } err = txLogger.AutoMigrate(&Transaction{}) if err != nil { panic(err) @@ -93,7 +115,7 @@ func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { return user, err } go func() { - userCopy := bot.copyLowercaseUser(u) + userCopy := bot.CopyLowercaseUser(u) if !reflect.DeepEqual(userCopy, user.Telegram) { // update possibly changed user details in Database user.Telegram = userCopy @@ -107,7 +129,7 @@ func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { } func UpdateUserRecord(user *lnbits.User, bot TipBot) error { - user.Telegram = bot.copyLowercaseUser(user.Telegram) + user.Telegram = bot.CopyLowercaseUser(user.Telegram) user.UpdatedAt = time.Now() tx := bot.Database.Save(user) if tx.Error != nil { diff --git a/internal/telegram/help.go b/internal/telegram/help.go index be7b5f4b..91f49582 100644 --- a/internal/telegram/help.go +++ b/internal/telegram/help.go @@ -8,13 +8,14 @@ import ( ) func (bot TipBot) makeHelpMessage(ctx context.Context, m *tb.Message) string { + fromUser := LoadUser(ctx) dynamicHelpMessage := "" // user has no username set if len(m.Sender.Username) == 0 { // return fmt.Sprintf(helpMessage, fmt.Sprintf("%s\n\n", helpNoUsernameMessage)) dynamicHelpMessage = dynamicHelpMessage + "\n" + Translate(ctx, "helpNoUsernameMessage") } - lnaddr, _ := bot.UserGetLightningAddress(m.Sender) + lnaddr, _ := bot.UserGetLightningAddress(fromUser) if len(lnaddr) > 0 { dynamicHelpMessage = dynamicHelpMessage + "\n" + fmt.Sprintf(Translate(ctx, "infoYourLightningAddress"), lnaddr) } @@ -48,18 +49,19 @@ func (bot TipBot) basicsHandler(ctx context.Context, m *tb.Message) { } func (bot TipBot) makeAdvancedHelpMessage(ctx context.Context, m *tb.Message) string { - + fromUser := LoadUser(ctx) dynamicHelpMessage := "ℹ️ *Info*\n" // user has no username set if len(m.Sender.Username) == 0 { // return fmt.Sprintf(helpMessage, fmt.Sprintf("%s\n\n", helpNoUsernameMessage)) dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("%s", Translate(ctx, "helpNoUsernameMessage")) + "\n" } - lnaddr, err := bot.UserGetLightningAddress(m.Sender) + // we print the anonymous ln address in the advanced help + lnaddr, err := bot.UserGetAnonLightningAddress(fromUser) if err == nil { - dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("Lightning Address: `%s`\n", lnaddr) + dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("Anonymous lightning address: `%s`\n", lnaddr) } - lnurl, err := UserGetLNURL(m.Sender) + lnurl, err := UserGetLNURL(fromUser) if err == nil { dynamicHelpMessage = dynamicHelpMessage + fmt.Sprintf("LNURL: `%s`", lnurl) } diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 328591db..612bc5d6 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -114,23 +114,30 @@ func (bot TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { } } -func (bot *TipBot) UserGetLightningAddress(user *tb.User) (string, error) { - if len(user.Username) > 0 { - return fmt.Sprintf("%s@%s", strings.ToLower(user.Username), strings.ToLower(internal.Configuration.Bot.LNURLHostUrl.Hostname())), nil +func (bot *TipBot) UserGetLightningAddress(user *lnbits.User) (string, error) { + if len(user.Telegram.Username) > 0 { + return fmt.Sprintf("%s@%s", strings.ToLower(user.Telegram.Username), strings.ToLower(internal.Configuration.Bot.LNURLHostUrl.Hostname())), nil } else { - return fmt.Sprintf("%s@%s", fmt.Sprint(user.ID), strings.ToLower(internal.Configuration.Bot.LNURLHostUrl.Hostname())), nil - // return "", fmt.Errorf("user has no username") + lnaddr, err := bot.UserGetAnonLightningAddress(user) + return lnaddr, err } } -func UserGetLNURL(user *tb.User) (string, error) { - name := strings.ToLower(strings.ToLower(user.Username)) - if len(name) == 0 { - name = fmt.Sprint(user.ID) - // return "", fmt.Errorf("user has no username.") - } +func (bot *TipBot) UserGetAnonLightningAddress(user *lnbits.User) (string, error) { + return fmt.Sprintf("%s@%s", fmt.Sprint(user.AnonID), strings.ToLower(internal.Configuration.Bot.LNURLHostUrl.Hostname())), nil +} + +func UserGetLNURL(user *lnbits.User) (string, error) { + // before: we used the username for the LNURL + // name := strings.ToLower(strings.ToLower(user.Telegram.Username)) + // if len(name) == 0 { + // name = fmt.Sprint(user.AnonID) + // // return "", fmt.Errorf("user has no username.") + // } + // now: use only the anon ID as LNURL + name := fmt.Sprint(user.AnonID) callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, name) - log.Debugf("[lnurlReceiveHandler] %s's LNURL: %s", GetUserStr(user), callback) + log.Debugf("[lnurlReceiveHandler] %s's LNURL: %s", GetUserStr(user.Telegram), callback) lnurlEncode, err := lnurl.LNURLEncode(callback) if err != nil { @@ -141,7 +148,8 @@ func UserGetLNURL(user *tb.User) (string, error) { // lnurlReceiveHandler outputs the LNURL of the user func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { - lnurlEncode, err := UserGetLNURL(m.Sender) + fromUser := LoadUser(ctx) + lnurlEncode, err := UserGetLNURL(fromUser) if err != nil { errmsg := fmt.Sprintf("[lnurlReceiveHandler] Failed to get LNURL: %s", err) log.Errorln(errmsg) diff --git a/internal/telegram/start.go b/internal/telegram/start.go index fbc880f5..ed5df463 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -4,13 +4,15 @@ import ( "context" "errors" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal" "strconv" "time" + "github.com/LightningTipBot/LightningTipBot/internal" + log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/str" tb "gopkg.in/tucnak/telebot.v2" "gorm.io/gorm" ) @@ -97,6 +99,7 @@ func (bot TipBot) createWallet(user *lnbits.User) error { return err } user.Wallet = &wallet[0] + user.AnonID = fmt.Sprint(str.Int32Hash(user.ID)) user.Initialized = false user.CreatedAt = time.Now() err = UpdateUserRecord(user, bot) diff --git a/internal/telegram/users.go b/internal/telegram/users.go index 51abbc2e..53e25c98 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -3,9 +3,10 @@ package telegram import ( "errors" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/str" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/str" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" @@ -78,15 +79,15 @@ func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int, err error) { return } -// copyLowercaseUser will create a coy user and cast username to lowercase. -func (bot *TipBot) copyLowercaseUser(u *tb.User) *tb.User { +// CopyLowercaseUser will create a coy user and cast username to lowercase. +func (bot *TipBot) CopyLowercaseUser(u *tb.User) *tb.User { userCopy := *u userCopy.Username = strings.ToLower(u.Username) return &userCopy } func (bot *TipBot) CreateWalletForTelegramUser(tbUser *tb.User) (*lnbits.User, error) { - userCopy := bot.copyLowercaseUser(tbUser) + userCopy := bot.CopyLowercaseUser(tbUser) user := &lnbits.User{Telegram: userCopy} userStr := GetUserStr(tbUser) log.Printf("[CreateWalletForTelegramUser] Creating wallet for user %s ... ", userStr) From 8da9e0c38c50fb37e35b0f09329607166110a782 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 24 Oct 2021 20:52:24 +0200 Subject: [PATCH 033/541] atoi is 32 bit only, change to ParseInt (#109) --- internal/lnurl/lnurl.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 129974f5..62d2c404 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -127,11 +127,10 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) // now check for the user user := &lnbits.User{} // check if "username" is actually the user ID - id, err := strconv.Atoi(username) tx := w.database - if err == nil { + if _, err := strconv.ParseInt(username, 10, 64); err == nil { // asume it's a user ID - tx = w.database.Where("anon_id = ?", fmt.Sprint(id)).First(user) + tx = w.database.Where("anon_id = ?", username).First(user) } else { // assume it's a string @username tx = w.database.Where("telegram_username = ?", strings.ToLower(username)).First(user) From 97b6e8f043cf65811a3328756c7817560d9a9461 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 26 Oct 2021 19:37:04 +0200 Subject: [PATCH 034/541] LNURL errors verbose (#110) * internal/telegram/lnurl.go * update gjson --- go.mod | 6 ++---- go.sum | 25 ++++++++++--------------- internal/telegram/lnurl.go | 6 +++++- 3 files changed, 17 insertions(+), 20 deletions(-) diff --git a/go.mod b/go.mod index 00cbca72..422f3d07 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,8 @@ require ( github.com/nicksnyder/go-i18n/v2 v2.1.2 github.com/sirupsen/logrus v1.2.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e - github.com/tidwall/btree v0.6.1 // indirect - github.com/tidwall/buntdb v1.2.6 - github.com/tidwall/gjson v1.8.1 - github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/buntdb v1.2.7 + github.com/tidwall/gjson v1.10.2 golang.org/x/text v0.3.5 gopkg.in/tucnak/telebot.v2 v2.3.5 gorm.io/driver/sqlite v1.1.4 diff --git a/go.sum b/go.sum index 4b68dded..baf1a12b 100644 --- a/go.sum +++ b/go.sum @@ -160,7 +160,6 @@ github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KV github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nicksnyder/go-i18n v1.10.1 h1:isfg77E/aCD7+0lD/D00ebR2MV5vgeQ276WYyDaCRQc= github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c= github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= @@ -198,32 +197,28 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/tidwall/btree v0.6.0 h1:JLYAFGV+1gjyFi3iQbO/fupBin+Ooh7dxqVV0twJ1Bo= -github.com/tidwall/btree v0.6.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4= +github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= +github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= github.com/tidwall/btree v0.6.1 h1:75VVgBeviiDO+3g4U+7+BaNBNhNINxB0ULPT3fs9pMY= github.com/tidwall/btree v0.6.1/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4= -github.com/tidwall/buntdb v1.2.6 h1:eS0QSmzHfCKjxxYGh8eH6wnK5VLsJ7UjyyIr29JmnEg= -github.com/tidwall/buntdb v1.2.6/go.mod h1:zpXqlA5D2772I4cTqV3ifr2AZihDgi8FV7xAQu6edfc= +github.com/tidwall/buntdb v1.2.7 h1:SIyObKAymzLyGhDeIhVk2Yc1/EwfCC75Uyu77CHlVoA= +github.com/tidwall/buntdb v1.2.7/go.mod h1:b6KvZM27x/8JLI5hgRhRu60pa3q0Tz9c50TyD46OHUM= github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls= github.com/tidwall/gjson v1.6.1 h1:LRbvNuNuvAiISWg6gxLEFuCe72UKy5hDqhxW/8183ws= github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0= -github.com/tidwall/gjson v1.8.0 h1:Qt+orfosKn0rbNTZqHYDqBrmm3UDA4KRkv70fDzG+PQ= -github.com/tidwall/gjson v1.8.0/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= -github.com/tidwall/gjson v1.8.1 h1:8j5EE9Hrh3l9Od1OIEDAb7IpezNA20UdRngNAj5N0WU= -github.com/tidwall/gjson v1.8.1/go.mod h1:5/xDoumyyDNerp2U36lyolv46b3uF/9Bu6OfyQ9GImk= -github.com/tidwall/grect v0.1.2 h1:wKVeQVZhjaFCKTTlpkDe3Ex4ko3cMGW3MRKawRe8uQ4= -github.com/tidwall/grect v0.1.2/go.mod h1:v+n4ewstPGduVJebcp5Eh2WXBJBumNzyhK8GZt4gHNw= +github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo= +github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/grect v0.1.3 h1:z9YwQAMUxVSBde3b7Sl8Da37rffgNfZ6Fq6h9t6KdXE= +github.com/tidwall/grect v0.1.3/go.mod h1:8GMjwh3gPZVpLBI/jDz9uslCe0dpxRpWDdtN0lWAS/E= github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= github.com/tidwall/lotsa v1.0.2/go.mod h1:X6NiU+4yHA3fE3Puvpnn1XMDrFZrE9JO2/w+UMuqgR8= github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc= github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E= -github.com/tidwall/match v1.0.3 h1:FQUVvBImDutD8wJLN6c5eMzWtjgONK9MwIBCOrUJKeE= -github.com/tidwall/match v1.0.3/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.0.2 h1:Z7S3cePv9Jwm1KwS0513MRaoUe3S01WPbLNV40pwWZU= github.com/tidwall/pretty v1.0.2/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= -github.com/tidwall/pretty v1.1.0 h1:K3hMW5epkdAVwibsQEfR/7Zj0Qgt4DxtNumTq/VloO8= -github.com/tidwall/pretty v1.1.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 612bc5d6..a1ff8cfc 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -278,7 +278,11 @@ func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { json.Unmarshal(body, &response2) if len(response2.PR) < 1 { - bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not receive invoice (wrong address?).")) + error_reason := "Could not receive invoice." + if len(response2.Reason) > 0 { + error_reason = response2.Reason + } + bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), error_reason)) return } bot.Telegram.Delete(msg) From c90cfd9d8a1916000fab05e1c93b65a7b7d75f0c Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 27 Oct 2021 11:55:59 +0200 Subject: [PATCH 035/541] tipjar (#114) * tipjar * tipjar * fix pointers and wallet keys * better comment --- internal/lnbits/lnbits.go | 31 ++- internal/telegram/balance.go | 2 +- internal/telegram/handler.go | 35 ++- internal/telegram/help.go | 6 +- internal/telegram/helpers.go | 8 + internal/telegram/inline_faucet.go | 96 ++------ internal/telegram/inline_query.go | 8 + internal/telegram/inline_receive.go | 2 +- internal/telegram/inline_send.go | 2 +- internal/telegram/inline_tipjar.go | 330 ++++++++++++++++++++++++++++ internal/telegram/interceptor.go | 6 +- internal/telegram/invoice.go | 5 +- internal/telegram/link.go | 3 +- internal/telegram/lnurl.go | 6 +- internal/telegram/message.go | 14 +- internal/telegram/pay.go | 16 +- internal/telegram/send.go | 18 +- internal/telegram/telegram.go | 10 +- internal/telegram/tip.go | 21 +- internal/telegram/transaction.go | 8 +- internal/telegram/users.go | 6 +- translations/en.toml | 28 +++ 22 files changed, 502 insertions(+), 159 deletions(-) create mode 100644 internal/telegram/inline_tipjar.go diff --git a/internal/lnbits/lnbits.go b/internal/lnbits/lnbits.go index 27e106dd..bd8fe2d3 100644 --- a/internal/lnbits/lnbits.go +++ b/internal/lnbits/lnbits.go @@ -8,6 +8,10 @@ import ( func NewClient(key, url string) *Client { return &Client{ url: url, + // info: this header holds the ADMIN key for the entire API + // it can be used to create wallets for example + // if you want to check the balance of a user, use w.Inkey + // if you want to make a payment, use w.Adminkey header: req.Header{ "Content-Type": "application/json", "Accept": "application/json", @@ -79,8 +83,13 @@ func (c *Client) CreateWallet(userId, walletName, adminId string) (wal Wallet, e // Invoice creates an invoice associated with this wallet. func (w Wallet) Invoice(params InvoiceParams, c *Client) (lntx BitInvoice, err error) { - c.header["X-Api-Key"] = w.Adminkey - resp, err := req.Post(c.url+"/api/v1/payments", c.header, req.BodyJSON(¶ms)) + // custom header with invoice key + invoiceHeader := req.Header{ + "Content-Type": "application/json", + "Accept": "application/json", + "X-Api-Key": w.Inkey, + } + resp, err := req.Post(c.url+"/api/v1/payments", invoiceHeader, req.BodyJSON(¶ms)) if err != nil { return } @@ -98,8 +107,13 @@ func (w Wallet) Invoice(params InvoiceParams, c *Client) (lntx BitInvoice, err e // Info returns wallet information func (c Client) Info(w Wallet) (wtx Wallet, err error) { - c.header["X-Api-Key"] = w.Adminkey - resp, err := req.Get(c.url+"/api/v1/wallet", c.header, nil) + // custom header with invoice key + invoiceHeader := req.Header{ + "Content-Type": "application/json", + "Accept": "application/json", + "X-Api-Key": w.Inkey, + } + resp, err := req.Get(c.url+"/api/v1/wallet", invoiceHeader, nil) if err != nil { return } @@ -135,8 +149,13 @@ func (c Client) Wallets(w User) (wtx []Wallet, err error) { // Pay pays a given invoice with funds from the wallet. func (w Wallet) Pay(params PaymentParams, c *Client) (wtx BitInvoice, err error) { - c.header["X-Api-Key"] = w.Adminkey - resp, err := req.Post(c.url+"/api/v1/payments", c.header, req.BodyJSON(¶ms)) + // custom header with admin key + adminHeader := req.Header{ + "Content-Type": "application/json", + "Accept": "application/json", + "X-Api-Key": w.Adminkey, + } + resp, err := req.Post(c.url+"/api/v1/payments", adminHeader, req.BodyJSON(¶ms)) if err != nil { return } diff --git a/internal/telegram/balance.go b/internal/telegram/balance.go index 851411fc..36185c77 100644 --- a/internal/telegram/balance.go +++ b/internal/telegram/balance.go @@ -15,7 +15,7 @@ func (bot TipBot) balanceHandler(ctx context.Context, m *tb.Message) { // reply only in private message if m.Chat.Type != tb.ChatPrivate { // delete message - NewMessage(m, WithDuration(0, bot.Telegram)) + bot.tryDeleteMessage(m) } // first check whether the user is initialized user := LoadUser(ctx) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index ed20c2a0..0a7ad866 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -94,13 +94,6 @@ func (bot TipBot) getHandler() []Handler { Handler: bot.startHandler, Interceptor: &Interceptor{Type: MessageInterceptor}, }, - { - Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan", "/grifo"}, - Handler: bot.faucetHandler, - Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, - }, { Endpoints: []interface{}{"/tip"}, Handler: bot.tipHandler, @@ -153,6 +146,20 @@ func (bot TipBot) getHandler() []Handler { bot.loadReplyToInterceptor, }}, }, + { + Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan", "/grifo"}, + Handler: bot.faucetHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.requireUserInterceptor}}, + }, + { + Endpoints: []interface{}{"/tipjar", "/spendendose"}, + Handler: bot.tipjarHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{bot.requireUserInterceptor}}, + }, { Endpoints: []interface{}{"/help"}, Handler: bot.helpHandler, @@ -316,5 +323,19 @@ func (bot TipBot) getHandler() []Handler { Type: CallbackInterceptor, Before: []intercept.Func{bot.loadUserInterceptor}}, }, + { + Endpoints: []interface{}{&btnAcceptInlineTipjar}, + Handler: bot.acceptInlineTipjarHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{&btnCancelInlineTipjar}, + Handler: bot.cancelInlineTipjarHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, } } diff --git a/internal/telegram/help.go b/internal/telegram/help.go index 91f49582..5e854138 100644 --- a/internal/telegram/help.go +++ b/internal/telegram/help.go @@ -31,7 +31,7 @@ func (bot TipBot) helpHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) if !m.Private() { // delete message - NewMessage(m, WithDuration(0, bot.Telegram)) + bot.tryDeleteMessage(m) } bot.trySendMessage(m.Sender, bot.makeHelpMessage(ctx, m), tb.NoPreview) return @@ -42,7 +42,7 @@ func (bot TipBot) basicsHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) if !m.Private() { // delete message - NewMessage(m, WithDuration(0, bot.Telegram)) + bot.tryDeleteMessage(m) } bot.trySendMessage(m.Sender, Translate(ctx, "basicsMessage"), tb.NoPreview) return @@ -75,7 +75,7 @@ func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) if !m.Private() { // delete message - NewMessage(m, WithDuration(0, bot.Telegram)) + bot.tryDeleteMessage(m) } bot.trySendMessage(m.Sender, bot.makeAdvancedHelpMessage(ctx, m), tb.NoPreview) return diff --git a/internal/telegram/helpers.go b/internal/telegram/helpers.go index 710ace61..4d1bb378 100644 --- a/internal/telegram/helpers.go +++ b/internal/telegram/helpers.go @@ -41,3 +41,11 @@ func MakeProgressbar(current int, total int) string { progressbar += strings.Repeat("⬜️", MAX_BARS-int(progress)) return progressbar } + +func MakeTipjarbar(current int, total int) string { + MAX_BARS := 16 + progress := math.Round((float64(current) / float64(total)) * float64(MAX_BARS)) + progressbar := strings.Repeat("🍯", int(progress)) + progressbar += strings.Repeat("⬜️", MAX_BARS-int(progress)) + return progressbar +} diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index dd992022..ed80b2c7 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -24,17 +24,17 @@ var ( type InlineFaucet struct { *transaction.Base - Message string `json:"inline_faucet_message"` - Amount int `json:"inline_faucet_amount"` - RemainingAmount int `json:"inline_faucet_remainingamount"` - PerUserAmount int `json:"inline_faucet_peruseramount"` - From *lnbits.User `json:"inline_faucet_from"` - To []*tb.User `json:"inline_faucet_to"` - Memo string `json:"inline_faucet_memo"` - NTotal int `json:"inline_faucet_ntotal"` - NTaken int `json:"inline_faucet_ntaken"` - UserNeedsWallet bool `json:"inline_faucet_userneedswallet"` - LanguageCode string `json:"languagecode"` + Message string `json:"inline_faucet_message"` + Amount int `json:"inline_faucet_amount"` + RemainingAmount int `json:"inline_faucet_remainingamount"` + PerUserAmount int `json:"inline_faucet_peruseramount"` + From *lnbits.User `json:"inline_faucet_from"` + To []*lnbits.User `json:"inline_faucet_to"` + Memo string `json:"inline_faucet_memo"` + NTotal int `json:"inline_faucet_ntotal"` + NTaken int `json:"inline_faucet_ntaken"` + UserNeedsWallet bool `json:"inline_faucet_userneedswallet"` + LanguageCode string `json:"languagecode"` } func (bot TipBot) mapFaucetLanguage(ctx context.Context, command string) context.Context { @@ -196,64 +196,11 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { // log.Errorf("[faucet] %s", err) return } - /**amount, err := decodeAmountFromCommand(q.Text) - if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) - return - } - if amount < 1 { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) - return - } - - peruserStr, err := getArgumentFromCommand(q.Text, 2) - if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) - return - } - perUserAmount, err := getAmount(peruserStr) - if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) - return - } - // peruser amount must be >1 and a divisor of amount - if perUserAmount < 1 || amount%perUserAmount != 0 { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineFaucetInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) - return - } - nTotal := amount / perUserAmount - fromUser := LoadUser(ctx) - fromUserStr := GetUserStr(&q.From) - balance, err := bot.GetUserBalance(fromUser) - if err != nil { - errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) - log.Errorln(errmsg) - return - } - // check if fromUser has balance - if balance < amount { - log.Errorf("Balance of user %s too low", fromUserStr) - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.telegram.Me.Username)) - return - } - - // check for memo in command - memo := GetMemoFromCommand(q.Text, 3) - **/ - // faucet, err := bot.createFaucet(ctx, q.Text, &q.From) - // if err != nil { - // log.Errorf(err.Error()) - // return - // } urls := []string{ queryImage, } results := make(tb.Results, len(urls)) // []tb.Result for i, url := range urls { - // inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), faucet.PerUserAmount, faucet.Amount, faucet.Amount, 0, faucet.NTotal, MakeProgressbar(faucet.Amount, faucet.Amount)) - // if len(faucet.Memo) > 0 { - // inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineFaucetAppendMemo"), faucet.Memo) - // } result := &tb.ArticleResult{ // URL: url, Text: inlineFaucet.Message, @@ -262,26 +209,11 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { // required for photos ThumbURL: url, } - // id := fmt.Sprintf("inl-faucet-%d-%d-%s", q.From.ID, faucet.Amount, RandStringRunes(5)) result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: bot.makeFaucetKeyboard(ctx, inlineFaucet.ID).InlineKeyboard} results[i] = result // needed to set a unique string ID for each result results[i].SetResultID(inlineFaucet.ID) - // create persistend inline send struct - // inlineFaucet := InlineFaucet{ - // Base: transaction.New(transaction.ID(id)), - // Message: inlineMessage, - // From: faucet.From, - // Memo: faucet.Memo, - // NTaken: 0, - // Amount: faucet.Amount, - // PerUserAmount: faucet.PerUserAmount, - // RemainingAmount: faucet.Amount, - // UserNeedsWallet: false, - // LanguageCode: ctx.Value("publicLanguageCode").(string), - // } - runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) log.Infof("[faucet] %s created inline faucet %s: %d sat (%d per user)", GetUserStr(inlineFaucet.From.Telegram), inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) } @@ -323,9 +255,9 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } // check if to user has already taken from the faucet for _, a := range inlineFaucet.To { - if a.ID == to.Telegram.ID { + if a.Telegram.ID == to.Telegram.ID { // to user is already in To slice, has taken from facuet - log.Infof("[faucet] %s already took from faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) + // log.Infof("[faucet] %s already took from faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) return } } @@ -366,7 +298,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback log.Infof("[faucet] faucet %s: %d sat from %s to %s ", inlineFaucet.ID, inlineFaucet.PerUserAmount, fromUserStr, toUserStr) inlineFaucet.NTaken += 1 - inlineFaucet.To = append(inlineFaucet.To, to.Telegram) + inlineFaucet.To = append(inlineFaucet.To, to) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index 94cf5092..0719280b 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -35,6 +35,11 @@ func (bot TipBot) inlineQueryInstructions(ctx context.Context, q *tb.Query) { title: TranslateUser(ctx, "inlineQueryFaucetTitle"), description: fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username), }, + { + url: queryImage, + title: TranslateUser(ctx, "inlineQueryTipjarTitle"), + description: fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username), + }, } results := make(tb.Results, len(instructions)) // []tb.Result for i, instruction := range instructions { @@ -130,6 +135,9 @@ func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) { } bot.handleInlineFaucetQuery(ctx, q) } + if strings.HasPrefix(q.Text, "tipjar") { + bot.handleInlineTipjarQuery(ctx, q) + } if strings.HasPrefix(q.Text, "receive") || strings.HasPrefix(q.Text, "get") || strings.HasPrefix(q.Text, "payme") || strings.HasPrefix(q.Text, "request") { bot.handleInlineReceiveQuery(ctx, q) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 1287937d..30a83721 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -67,7 +67,7 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { if strings.HasPrefix(from_username, "@") { fromUserDb, err = GetUserByTelegramUsername(from_username[1:], bot) // must be without the @ if err != nil { - //NewMessage(m, WithDuration(0, bot.Telegram)) + //bot.tryDeleteMessage(m) //bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), toUserStrMention)) bot.inlineQueryReplyWithError(q, fmt.Sprintf(TranslateUser(ctx, "sendUserHasNoWalletMessage"), from_username), diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 9ccb3f98..fd81c1b8 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -83,7 +83,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { if strings.HasPrefix(to_username, "@") { toUserDb, err = GetUserByTelegramUsername(to_username[1:], bot) // must be without the @ if err != nil { - //NewMessage(m, WithDuration(0, bot.Telegram)) + //bot.tryDeleteMessage(m) //bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), toUserStrMention)) bot.inlineQueryReplyWithError(q, fmt.Sprintf(TranslateUser(ctx, "sendUserHasNoWalletMessage"), to_username), diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go new file mode 100644 index 00000000..a0504c34 --- /dev/null +++ b/internal/telegram/inline_tipjar.go @@ -0,0 +1,330 @@ +package telegram + +import ( + "context" + "fmt" + "strings" + + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + log "github.com/sirupsen/logrus" + tb "gopkg.in/tucnak/telebot.v2" +) + +var ( + inlineTipjarMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: false} + btnCancelInlineTipjar = inlineTipjarMenu.Data("🚫", "cancel_tipjar_inline") + btnAcceptInlineTipjar = inlineTipjarMenu.Data("💸 Pay", "confirm_tipjar_inline") +) + +type InlineTipjar struct { + *transaction.Base + Message string `json:"inline_tipjar_message"` + Amount int `json:"inline_tipjar_amount"` + GivenAmount int `json:"inline_tipjar_givenamount"` + PerUserAmount int `json:"inline_tipjar_peruseramount"` + To *lnbits.User `json:"inline_tipjar_to"` + From []*lnbits.User `json:"inline_tipjar_from"` + Memo string `json:"inline_tipjar_memo"` + NTotal int `json:"inline_tipjar_ntotal"` + NGiven int `json:"inline_tipjar_ngiven"` + LanguageCode string `json:"languagecode"` +} + +func (bot TipBot) mapTipjarLanguage(ctx context.Context, command string) context.Context { + if len(strings.Split(command, " ")) > 1 { + c := strings.Split(command, " ")[0][1:] // cut the / + ctx = bot.commandTranslationMap(ctx, c) + } + return ctx +} + +func (bot TipBot) createTipjar(ctx context.Context, text string, sender *tb.User) (*InlineTipjar, error) { + amount, err := decodeAmountFromCommand(text) + if err != nil { + return nil, errors.New(errors.DecodeAmountError, err) + } + peruserStr, err := getArgumentFromCommand(text, 2) + if err != nil { + return nil, errors.New(errors.DecodePerUserAmountError, err) + } + perUserAmount, err := getAmount(peruserStr) + if err != nil { + return nil, errors.New(errors.InvalidAmountError, err) + } + if perUserAmount < 1 || amount%perUserAmount != 0 { + return nil, errors.New(errors.InvalidAmountPerUserError, fmt.Errorf("invalid amount per user")) + } + nTotal := amount / perUserAmount + toUser := LoadUser(ctx) + // toUserStr := GetUserStr(sender) + // // check for memo in command + memo := GetMemoFromCommand(text, 3) + + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineTipjarMessage"), perUserAmount, 0, amount, 0, MakeTipjarbar(0, amount)) + if len(memo) > 0 { + inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineTipjarAppendMemo"), memo) + } + id := fmt.Sprintf("inl-tipjar-%d-%d-%s", sender.ID, amount, RandStringRunes(5)) + + return &InlineTipjar{ + Base: transaction.New(transaction.ID(id)), + Message: inlineMessage, + Amount: amount, + To: toUser, + Memo: memo, + PerUserAmount: perUserAmount, + NTotal: nTotal, + NGiven: 0, + GivenAmount: 0, + LanguageCode: ctx.Value("publicLanguageCode").(string), + }, nil + +} +func (bot TipBot) makeTipjar(ctx context.Context, m *tb.Message, query bool) (*InlineTipjar, error) { + tipjar, err := bot.createTipjar(ctx, m.Text, m.Sender) + if err != nil { + switch err.(errors.TipBotError).Code { + case errors.DecodeAmountError: + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineTipjarHelpText"), Translate(ctx, "inlineTipjarInvalidAmountMessage"))) + bot.tryDeleteMessage(m) + return nil, err + case errors.DecodePerUserAmountError: + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineTipjarHelpText"), "")) + bot.tryDeleteMessage(m) + return nil, err + case errors.InvalidAmountError: + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineTipjarHelpText"), Translate(ctx, "inlineTipjarInvalidAmountMessage"))) + bot.tryDeleteMessage(m) + return nil, err + case errors.InvalidAmountPerUserError: + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineTipjarHelpText"), Translate(ctx, "inlineTipjarInvalidPeruserAmountMessage"))) + bot.tryDeleteMessage(m) + return nil, err + case errors.GetBalanceError: + // log.Errorln(err.Error()) + bot.tryDeleteMessage(m) + return nil, err + case errors.BalanceToLowError: + // log.Errorf(err.Error()) + bot.trySendMessage(m.Sender, Translate(ctx, "inlineSendBalanceLowMessage")) + bot.tryDeleteMessage(m) + return nil, err + } + } + return tipjar, err +} + +func (bot TipBot) makeQueryTipjar(ctx context.Context, q *tb.Query, query bool) (*InlineTipjar, error) { + tipjar, err := bot.createTipjar(ctx, q.Text, &q.From) + if err != nil { + switch err.(errors.TipBotError).Code { + case errors.DecodeAmountError: + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryTipjarTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + return nil, err + case errors.DecodePerUserAmountError: + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryTipjarTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + return nil, err + case errors.InvalidAmountError: + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + return nil, err + case errors.InvalidAmountPerUserError: + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineTipjarInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + return nil, err + case errors.GetBalanceError: + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryTipjarTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + return nil, err + case errors.BalanceToLowError: + log.Errorf(err.Error()) + bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + return nil, err + } + } + return tipjar, err +} + +func (bot TipBot) makeTipjarKeyboard(ctx context.Context, inlineTipjar *InlineTipjar) *tb.ReplyMarkup { + // inlineTipjarMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} + // slice of buttons + buttons := make([]tb.Btn, 0) + acceptInlineTipjarButton := inlineTipjarMenu.Data(Translate(ctx, "payReceiveButtonMessage"), "confirm_tipjar_inline", inlineTipjar.ID) + buttons = append(buttons, acceptInlineTipjarButton) + cancelInlineTipjarButton := inlineTipjarMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_tipjar_inline", inlineTipjar.ID) + buttons = append(buttons, cancelInlineTipjarButton) + + inlineTipjarMenu.Inline( + inlineTipjarMenu.Row(buttons...)) + return inlineTipjarMenu +} + +func (bot TipBot) tipjarHandler(ctx context.Context, m *tb.Message) { + bot.anyTextHandler(ctx, m) + if m.Private() { + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineTipjarHelpText"), Translate(ctx, "inlineTipjarHelpTipjarInGroup"))) + return + } + ctx = bot.mapTipjarLanguage(ctx, m.Text) + inlineTipjar, err := bot.makeTipjar(ctx, m, false) + if err != nil { + log.Errorf("[tipjar] %s", err) + return + } + toUserStr := GetUserStr(m.Sender) + bot.trySendMessage(m.Chat, inlineTipjar.Message, bot.makeTipjarKeyboard(ctx, inlineTipjar)) + log.Infof("[tipjar] %s created tipjar %s: %d sat (%d per user)", toUserStr, inlineTipjar.ID, inlineTipjar.Amount, inlineTipjar.PerUserAmount) + runtime.IgnoreError(inlineTipjar.Set(inlineTipjar, bot.Bunt)) +} + +func (bot TipBot) handleInlineTipjarQuery(ctx context.Context, q *tb.Query) { + inlineTipjar, err := bot.makeQueryTipjar(ctx, q, false) + if err != nil { + // log.Errorf("[tipjar] %s", err) + return + } + urls := []string{ + queryImage, + } + results := make(tb.Results, len(urls)) // []tb.Result + for i, url := range urls { + result := &tb.ArticleResult{ + // URL: url, + Text: inlineTipjar.Message, + Title: fmt.Sprintf(TranslateUser(ctx, "inlineResultTipjarTitle"), inlineTipjar.Amount), + Description: TranslateUser(ctx, "inlineResultTipjarDescription"), + // required for photos + ThumbURL: url, + } + result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: bot.makeTipjarKeyboard(ctx, inlineTipjar).InlineKeyboard} + results[i] = result + // needed to set a unique string ID for each result + results[i].SetResultID(inlineTipjar.ID) + + runtime.IgnoreError(inlineTipjar.Set(inlineTipjar, bot.Bunt)) + log.Infof("[tipjar] %s created inline tipjar %s: %d sat (%d per user)", GetUserStr(inlineTipjar.To.Telegram), inlineTipjar.ID, inlineTipjar.Amount, inlineTipjar.PerUserAmount) + } + + err = bot.Telegram.Answer(q, &tb.QueryResponse{ + Results: results, + CacheTime: 1, + IsPersonal: true, + }) + if err != nil { + log.Errorln(err) + } +} + +func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback) { + from := LoadUser(ctx) + if from.Wallet == nil { + return + } + tx := &InlineTipjar{Base: transaction.New(transaction.ID(c.Data))} + fn, err := tx.Get(tx, bot.Bunt) + if err != nil { + log.Errorf("[tipjar] %s", err) + return + } + inlineTipjar := fn.(*InlineTipjar) + to := inlineTipjar.To + err = inlineTipjar.Lock(inlineTipjar, bot.Bunt) + if err != nil { + log.Errorf("[tipjar] LockTipjar %s error: %s", inlineTipjar.ID, err) + return + } + if !inlineTipjar.Active { + log.Errorf(fmt.Sprintf("[tipjar] tipjar %s inactive.", inlineTipjar.ID)) + return + } + // release tipjar no matter what + defer inlineTipjar.Release(inlineTipjar, bot.Bunt) + + if from.Telegram.ID == to.Telegram.ID { + bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) + return + } + // // check if to user has already given to the tipjar + for _, a := range inlineTipjar.From { + if a.Telegram.ID == to.Telegram.ID { + // to user is already in To slice, has taken from facuet + // log.Infof("[tipjar] %s already gave to tipjar %s", GetUserStr(to.Telegram), inlineTipjar.ID) + return + } + } + if inlineTipjar.GivenAmount < inlineTipjar.Amount { + toUserStrMd := GetUserStrMd(to.Telegram) + fromUserStrMd := GetUserStrMd(from.Telegram) + toUserStr := GetUserStr(to.Telegram) + fromUserStr := GetUserStr(from.Telegram) + + // todo: user new get username function to get userStrings + transactionMemo := fmt.Sprintf("Tipjar from %s to %s (%d sat).", fromUserStr, toUserStr, inlineTipjar.PerUserAmount) + t := NewTransaction(bot, from, to, inlineTipjar.PerUserAmount, TransactionType("tipjar")) + t.Memo = transactionMemo + + success, err := t.Send() + if !success { + bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) + errMsg := fmt.Sprintf("[tipjar] Transaction failed: %s", err) + log.Errorln(errMsg) + return + } + + log.Infof("[tipjar] tipjar %s: %d sat from %s to %s ", inlineTipjar.ID, inlineTipjar.PerUserAmount, fromUserStr, toUserStr) + inlineTipjar.NGiven += 1 + inlineTipjar.From = append(inlineTipjar.From, from) + inlineTipjar.GivenAmount = inlineTipjar.GivenAmount + inlineTipjar.PerUserAmount + + _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineTipjarReceivedMessage"), fromUserStrMd, inlineTipjar.PerUserAmount)) + _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineTipjarSentMessage"), inlineTipjar.PerUserAmount, toUserStrMd)) + if err != nil { + errmsg := fmt.Errorf("[tipjar] Error: Send message to %s: %s", toUserStr, err) + log.Errorln(errmsg) + return + } + + // build tipjar message + inlineTipjar.Message = fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarMessage"), inlineTipjar.PerUserAmount, inlineTipjar.GivenAmount, inlineTipjar.Amount, inlineTipjar.NGiven, MakeTipjarbar(inlineTipjar.GivenAmount, inlineTipjar.Amount)) + memo := inlineTipjar.Memo + if len(memo) > 0 { + inlineTipjar.Message = inlineTipjar.Message + fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarAppendMemo"), memo) + } + // if inlineTipjar.UserNeedsWallet { + // inlineTipjar.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) + // } + // update message + log.Infoln(inlineTipjar.Message) + bot.tryEditMessage(c.Message, inlineTipjar.Message, bot.makeTipjarKeyboard(ctx, inlineTipjar)) + } + if inlineTipjar.GivenAmount >= inlineTipjar.Amount { + // tipjar is full + inlineTipjar.Message = fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarEndedMessage"), inlineTipjar.Amount, inlineTipjar.NGiven) + // if inlineTipjar.UserNeedsWallet { + // inlineTipjar.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) + // } + bot.tryEditMessage(c.Message, inlineTipjar.Message) + inlineTipjar.Active = false + } + +} + +func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) { + tx := &InlineTipjar{Base: transaction.New(transaction.ID(c.Data))} + fn, err := tx.Get(tx, bot.Bunt) + if err != nil { + log.Errorf("[cancelInlineSendHandler] %s", err) + return + } + inlineTipjar := fn.(*InlineTipjar) + if c.Sender.ID == inlineTipjar.To.Telegram.ID { + bot.tryEditMessage(c.Message, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) + // set the inlineTipjar inactive + inlineTipjar.Active = false + inlineTipjar.InTransaction = false + runtime.IgnoreError(inlineTipjar.Set(inlineTipjar, bot.Bunt)) + } + return +} diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index cbc56a89..d7289720 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -122,7 +122,11 @@ func (bot TipBot) logMessageInterceptor(ctx context.Context, i interface{}) (con case *tb.Message: m := i.(*tb.Message) if m.Text != "" { - log.Infof("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, m.Text) + log_string := fmt.Sprintf("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, m.Text) + if m.IsReply() { + log_string = fmt.Sprintf("%s -> %s", log_string, GetUserStr(m.ReplyTo.Sender)) + } + log.Infof(log_string) } else if m.Photo != nil { log.Infof("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, photoTag) } diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 6718312d..26cfc30b 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -4,9 +4,10 @@ import ( "bytes" "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal" "strings" + "github.com/LightningTipBot/LightningTipBot/internal" + log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -27,7 +28,7 @@ func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { bot.anyTextHandler(ctx, m) if m.Chat.Type != tb.ChatPrivate { // delete message - NewMessage(m, WithDuration(0, bot.Telegram)) + bot.tryDeleteMessage(m) return } if len(strings.Split(m.Text, " ")) < 2 { diff --git a/internal/telegram/link.go b/internal/telegram/link.go index 7b4a6f51..4ce7d91f 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal" log "github.com/sirupsen/logrus" @@ -21,7 +22,7 @@ func (bot TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { // reply only in private message if m.Chat.Type != tb.ChatPrivate { // delete message - NewMessage(m, WithDuration(0, bot.Telegram)) + bot.tryDeleteMessage(m) } // first check whether the user is initialized fromUser := LoadUser(ctx) diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index a1ff8cfc..b1450376 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -23,7 +23,7 @@ import ( ) // lnurlHandler is invoked on /lnurl command -func (bot TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { // commands: // /lnurl // /lnurl @@ -169,7 +169,7 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { } // lnurlEnterAmountHandler is invoked if the user didn't deliver an amount for the lnurl payment -func (bot TipBot) lnurlEnterAmountHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) lnurlEnterAmountHandler(ctx context.Context, m *tb.Message) { user := LoadUser(ctx) if user.Wallet == nil { return @@ -219,7 +219,7 @@ type LnurlStateResponse struct { } // lnurlPayHandler is invoked when the user has delivered an amount and is ready to pay -func (bot TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { +func (bot *TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { msg := bot.trySendMessage(c.Sender, Translate(ctx, "lnurlGettingUserMessage")) user := LoadUser(ctx) diff --git a/internal/telegram/message.go b/internal/telegram/message.go index 7cca211b..cb4304df 100644 --- a/internal/telegram/message.go +++ b/internal/telegram/message.go @@ -4,8 +4,6 @@ import ( "strconv" "time" - log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" ) @@ -16,10 +14,10 @@ type Message struct { type MessageOption func(m *Message) -func WithDuration(duration time.Duration, bot *tb.Bot) MessageOption { +func WithDuration(duration time.Duration, tipBot *TipBot) MessageOption { return func(m *Message) { m.duration = duration - go m.dispose(bot) + go m.dispose(tipBot) } } @@ -37,17 +35,13 @@ func (msg Message) Key() string { return strconv.Itoa(msg.Message.ID) } -func (msg Message) dispose(telegram *tb.Bot) { +func (msg Message) dispose(tipBot *TipBot) { // do not delete messages from private chat if msg.Message.Private() { return } go func() { time.Sleep(msg.duration) - err := telegram.Delete(msg.Message) - if err != nil { - log.Println(err.Error()) - return - } + tipBot.tryDeleteMessage(msg.Message) }() } diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index d6d68ed9..dbdc4663 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -42,7 +42,7 @@ type PayData struct { } // payHandler invoked on "/pay lnbc..." command -func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { // check and print all commands bot.anyTextHandler(ctx, m) user := LoadUser(ctx) @@ -50,14 +50,14 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { return } if len(strings.Split(m.Text, " ")) < 2 { - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, "")) return } userStr := GetUserStr(m.Sender) paymentRequest, err := getArgumentFromCommand(m.Text, 1) if err != nil { - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) errmsg := fmt.Sprintf("[/pay] Error: Could not getArgumentFromCommand: %s", err) log.Errorln(errmsg) @@ -80,20 +80,20 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { if amount <= 0 { bot.trySendMessage(m.Sender, Translate(ctx, "invoiceNoAmountMessage")) errmsg := fmt.Sprint("[/pay] Error: invoice without amount") - log.Errorln(errmsg) + log.Warnln(errmsg) return } // check user balance first balance, err := bot.GetUserBalance(user) if err != nil { - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) errmsg := fmt.Sprintf("[/pay] Error: Could not get user balance: %s", err) log.Errorln(errmsg) return } if amount > balance { - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "insufficientFundsMessage"), balance, amount)) return } @@ -140,7 +140,7 @@ func (bot TipBot) payHandler(ctx context.Context, m *tb.Message) { } // confirmPayHandler when user clicked pay on payment confirmation -func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { tx := &PayData{Base: transaction.New(transaction.ID(c.Data))} sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls @@ -211,7 +211,7 @@ func (bot TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { } // cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { // reset state immediately user := LoadUser(ctx) ResetUserState(user, bot) diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 82977c8b..9657e3fe 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -58,7 +58,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { } // reset state immediately - ResetUserState(user, *bot) + ResetUserState(user, bot) // check and print all commands @@ -70,7 +70,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { if ok, errstr := bot.SendCheckSyntax(ctx, m); !ok { bot.trySendMessage(m.Sender, helpSendUsage(ctx, errstr)) - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) return } @@ -104,9 +104,9 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // ASSUME INTERNAL SEND TO TELEGRAM USER if err != nil || amount < 1 { errmsg := fmt.Sprintf("[/send] Error: Send amount not valid.") - log.Errorln(errmsg) + log.Warnln(errmsg) // immediately delete if the amount is bullshit - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, helpSendUsage(ctx, Translate(ctx, "sendValidAmountMessage"))) return } @@ -139,7 +139,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { toUserDb, err := GetUserByTelegramUsername(toUserStrWithoutAt, *bot) if err != nil { - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), toUserStrMention)) return } @@ -166,14 +166,14 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { sendDataJson, err := json.Marshal(sendData) if err != nil { - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) log.Printf("[/send] Error: %s\n", err.Error()) bot.trySendMessage(m.Sender, fmt.Sprint(Translate(ctx, "errorTryLaterMessage"))) return } // save the send data to the Database // log.Debug(sendData) - SetUserState(user, *bot, lnbits.UserStateConfirmSend, string(sendDataJson)) + SetUserState(user, bot, lnbits.UserStateConfirmSend, string(sendDataJson)) sendButton := sendConfirmationMenu.Data(Translate(ctx, "sendButtonMessage"), "confirm_send") cancelButton := sendConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_send") sendButton.Data = id @@ -224,7 +224,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { // decode callback data // log.Debug("[send] Callback: %s", c.Data) from := LoadUser(ctx) - ResetUserState(from, *bot) // we don't need to check the statekey anymore like we did earlier + ResetUserState(from, bot) // we don't need to check the statekey anymore like we did earlier // information about the send toId := sendData.ToTelegramId @@ -283,7 +283,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { // reset state immediately user := LoadUser(ctx) - ResetUserState(user, *bot) + ResetUserState(user, bot) tx := &SendData{Base: transaction.New(transaction.ID(c.Data))} sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index c213ced9..b87458c4 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -8,14 +8,14 @@ import ( func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options ...interface{}) (msg *tb.Message) { msg, err := bot.Telegram.Forward(to, what, options...) if err != nil { - log.Errorln(err.Error()) + log.Warnln(err.Error()) } return } func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...interface{}) (msg *tb.Message) { msg, err := bot.Telegram.Send(to, what, options...) if err != nil { - log.Errorln(err.Error()) + log.Warnln(err.Error()) } return } @@ -23,7 +23,7 @@ func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...i func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...interface{}) (msg *tb.Message) { msg, err := bot.Telegram.Reply(to, what, options...) if err != nil { - log.Errorln(err.Error()) + log.Warnln(err.Error()) } return } @@ -31,7 +31,7 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message) { msg, err := bot.Telegram.Edit(to, what, options...) if err != nil { - log.Errorln(err.Error()) + log.Warnln(err.Error()) } return } @@ -39,6 +39,6 @@ func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...in func (bot TipBot) tryDeleteMessage(msg tb.Editable) { err := bot.Telegram.Delete(msg) if err != nil { - log.Errorln(err.Error()) + log.Warnln(err.Error()) } } diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index 70544ffb..808a8576 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -3,11 +3,12 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal" - "github.com/LightningTipBot/LightningTipBot/internal/str" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/str" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" @@ -31,7 +32,7 @@ func TipCheckSyntax(ctx context.Context, m *tb.Message) (bool, string) { func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { // delete the tip message after a few seconds, this is default behaviour - defer NewMessage(m, WithDuration(time.Second*time.Duration(internal.Configuration.Telegram.MessageDisposeDuration), bot.Telegram)) + defer NewMessage(m, WithDuration(time.Second*time.Duration(internal.Configuration.Telegram.MessageDisposeDuration), bot)) // check and print all commands bot.anyTextHandler(ctx, m) user := LoadUser(ctx) @@ -41,7 +42,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { // only if message is a reply if !m.IsReply() { - NewMessage(m, WithDuration(0, bot.Telegram)) + bot.tryDeleteMessage(m) bot.trySendMessage(m.Sender, helpTipUsage(ctx, Translate(ctx, "tipDidYouReplyMessage"))) bot.trySendMessage(m.Sender, Translate(ctx, "tipInviteGroupMessage")) return @@ -49,7 +50,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if ok, err := TipCheckSyntax(ctx, m); !ok { bot.trySendMessage(m.Sender, helpTipUsage(ctx, err)) - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) return } @@ -58,9 +59,9 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if err != nil || amount < 1 { errmsg := fmt.Sprintf("[/tip] Error: Tip amount not valid.") // immediately delete if the amount is bullshit - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, helpTipUsage(ctx, Translate(ctx, "tipValidAmountMessage"))) - log.Errorln(errmsg) + log.Warnln(errmsg) return } @@ -74,7 +75,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { to := LoadReplyToUser(ctx) if from.Telegram.ID == to.Telegram.ID { - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, Translate(ctx, "tipYourselfMessage")) return } @@ -110,10 +111,10 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { t.Memo = transactionMemo success, err := t.Send() if !success { - NewMessage(m, WithDuration(0, bot.Telegram)) + NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, Translate(ctx, "tipErrorMessage")) errMsg := fmt.Sprintf("[/tip] Transaction failed: %s", err) - log.Errorln(errMsg) + log.Warnln(errMsg) return } diff --git a/internal/telegram/transaction.go b/internal/telegram/transaction.go index 5ffd06ea..4b8c6753 100644 --- a/internal/telegram/transaction.go +++ b/internal/telegram/transaction.go @@ -10,10 +10,6 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -const ( - balanceTooLowMessage = "Your balance is too low." -) - type Transaction struct { ID uint `gorm:"primarykey"` Time time.Time `json:"time"` @@ -112,8 +108,8 @@ func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits } // check if fromUser has balance if balance < amount { - errmsg := fmt.Sprintf(balanceTooLowMessage) - log.Errorf("Balance of user %s too low", fromUserStr) + errmsg := fmt.Sprintf("balance too low.") + log.Warnf("Balance of user %s too low", fromUserStr) return false, fmt.Errorf(errmsg) } diff --git a/internal/telegram/users.go b/internal/telegram/users.go index 53e25c98..6ba03997 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -14,13 +14,13 @@ import ( "gorm.io/gorm" ) -func SetUserState(user *lnbits.User, bot TipBot, stateKey lnbits.UserStateKey, stateData string) { +func SetUserState(user *lnbits.User, bot *TipBot, stateKey lnbits.UserStateKey, stateData string) { user.StateKey = stateKey user.StateData = stateData bot.Database.Table("users").Where("name = ?", user.Name).Update("state_key", user.StateKey).Update("state_data", user.StateData) } -func ResetUserState(user *lnbits.User, bot TipBot) { +func ResetUserState(user *lnbits.User, bot *TipBot) { user.ResetState() bot.Database.Table("users").Where("name = ?", user.Name).Update("state_key", 0).Update("state_data", "") } @@ -64,7 +64,7 @@ func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int, err error) { wallet, err := bot.Client.Info(*user.Wallet) if err != nil { - errmsg := fmt.Sprintf("[GetUserBalance] Error: Couldn't fetch user %s's info from LNbits: %s", GetUserStr(user.Telegram), err) + errmsg := fmt.Sprintf("[GetUserBalance] Error: Couldn't fetch user %s's info from LNbits: %s", GetUserStr(user.Telegram), err.Error()) log.Errorln(errmsg) return } diff --git a/translations/en.toml b/translations/en.toml index 78897c5e..3fcb5041 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -57,6 +57,10 @@ saveButtonMessage = """Save""" deleteButtonMessage = """Delete""" infoButtonMessage = """Info""" +cancelButtonEmoji = """🚫""" +payButtonEmoji = """💸""" + + # HELP helpMessage = """⚡️ *Wallet* @@ -295,3 +299,27 @@ inlineReceiveCreateWalletMessage = """Chat with %s 👈 to manage your wallet."" inlineReceiveYourselfMessage = """📖 You can't pay to yourself.""" inlineReceiveFailedMessage = """🚫 Receive failed.""" inlineReceiveCancelledMessage = """🚫 Receive cancelled.""" + +# TIPJAR + +inlineQueryTipjarTitle = """🍯 Create a tipjar.""" +inlineQueryTipjarDescription = """Usage: @%s tipjar """ +inlineResultTipjarTitle = """🍯 Create a %d sat tipjar.""" +inlineResultTipjarDescription = """👉 Click here to create a tipjar in this chat.""" + +inlineTipjarMessage = """Press 💸 to *pay %d sat* to this tipjar. + +🙏 Given: *%d*/%d sat (by %d users) +%s""" +inlineTipjarEndedMessage = """🍯 Tipjar full ⭐️\n\n🏅 %d sat collected with %d donations.""" +inlineTipjarAppendMemo = """\n✉️ %s""" +inlineTipjarCancelledMessage = """🚫 Tipjar cancelled.""" +inlineTipjarInvalidPeruserAmountMessage = """🚫 Peruser amount not divisor of capacity.""" +inlineTipjarInvalidAmountMessage = """🚫 Invalid amount.""" +inlineTipjarSentMessage = """🍯 %d sat sent to %s.""" +inlineTipjarReceivedMessage = """🍯 %s sent you %d sat.""" +inlineTipjarHelpTipjarInGroup = """Create a tipjar in a group with the bot inside or use 👉 inline command (/advanced for more).""" +inlineTipjarHelpText = """📖 Oops, that didn't work. %s + +*Usage:* `/tipjar ` +*Example:* `/tipjar 210 21`""" \ No newline at end of file From f0385128db42b462fa326e29c7583f11701dde57 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 27 Oct 2021 12:04:32 +0200 Subject: [PATCH 036/541] add username to tipjar (#115) --- internal/telegram/inline_tipjar.go | 27 ++++++++++++++++++++++++--- translations/en.toml | 4 ++-- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index a0504c34..ef40a024 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -65,7 +65,15 @@ func (bot TipBot) createTipjar(ctx context.Context, text string, sender *tb.User // // check for memo in command memo := GetMemoFromCommand(text, 3) - inlineMessage := fmt.Sprintf(Translate(ctx, "inlineTipjarMessage"), perUserAmount, 0, amount, 0, MakeTipjarbar(0, amount)) + inlineMessage := fmt.Sprintf( + Translate(ctx, "inlineTipjarMessage"), + perUserAmount, + GetUserStr(toUser.Telegram), + 0, + amount, + 0, + MakeTipjarbar(0, amount), + ) if len(memo) > 0 { inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineTipjarAppendMemo"), memo) } @@ -287,7 +295,15 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback } // build tipjar message - inlineTipjar.Message = fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarMessage"), inlineTipjar.PerUserAmount, inlineTipjar.GivenAmount, inlineTipjar.Amount, inlineTipjar.NGiven, MakeTipjarbar(inlineTipjar.GivenAmount, inlineTipjar.Amount)) + inlineTipjar.Message = fmt.Sprintf( + i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarMessage"), + inlineTipjar.PerUserAmount, + GetUserStr(inlineTipjar.To.Telegram), + inlineTipjar.GivenAmount, + inlineTipjar.Amount, + inlineTipjar.NGiven, + MakeTipjarbar(inlineTipjar.GivenAmount, inlineTipjar.Amount), + ) memo := inlineTipjar.Memo if len(memo) > 0 { inlineTipjar.Message = inlineTipjar.Message + fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarAppendMemo"), memo) @@ -301,7 +317,12 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback } if inlineTipjar.GivenAmount >= inlineTipjar.Amount { // tipjar is full - inlineTipjar.Message = fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarEndedMessage"), inlineTipjar.Amount, inlineTipjar.NGiven) + inlineTipjar.Message = fmt.Sprintf( + i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarEndedMessage"), + GetUserStr(inlineTipjar.To.Telegram), + inlineTipjar.Amount, + inlineTipjar.NGiven, + ) // if inlineTipjar.UserNeedsWallet { // inlineTipjar.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) // } diff --git a/translations/en.toml b/translations/en.toml index 3fcb5041..b0215923 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -307,11 +307,11 @@ inlineQueryTipjarDescription = """Usage: @%s tipjar """ inlineResultTipjarTitle = """🍯 Create a %d sat tipjar.""" inlineResultTipjarDescription = """👉 Click here to create a tipjar in this chat.""" -inlineTipjarMessage = """Press 💸 to *pay %d sat* to this tipjar. +inlineTipjarMessage = """Press 💸 to *pay %d sat* to this tipjar by %s. 🙏 Given: *%d*/%d sat (by %d users) %s""" -inlineTipjarEndedMessage = """🍯 Tipjar full ⭐️\n\n🏅 %d sat collected with %d donations.""" +inlineTipjarEndedMessage = """🍯 %s's tipjar is full ⭐️\n\n🏅 %d sat given by %d users.""" inlineTipjarAppendMemo = """\n✉️ %s""" inlineTipjarCancelledMessage = """🚫 Tipjar cancelled.""" inlineTipjarInvalidPeruserAmountMessage = """🚫 Peruser amount not divisor of capacity.""" From 51c410c045cc844e3863a390bc1e9b499829a7f5 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 27 Oct 2021 15:23:01 +0200 Subject: [PATCH 037/541] Tipjar hotfix2: translations and help (#116) * translations and help * shorten it --- botfather-setcommands.txt | 1 + internal/telegram/help.go | 6 +++++- internal/telegram/inline_query.go | 8 ++++++-- translations/de.toml | 34 +++++++++++++++++++++++++++---- translations/en.toml | 11 +++++----- translations/es.toml | 8 +++++--- translations/fr.toml | 8 +++++--- translations/id.toml | 8 +++++--- translations/it.toml | 30 ++++++++++++++------------- translations/nl.toml | 8 +++++--- translations/pt-br.toml | 8 +++++--- translations/tr.toml | 6 ++++-- 12 files changed, 93 insertions(+), 43 deletions(-) diff --git a/botfather-setcommands.txt b/botfather-setcommands.txt index b0b4a6b8..1a50f82b 100644 --- a/botfather-setcommands.txt +++ b/botfather-setcommands.txt @@ -13,4 +13,5 @@ invoice - Receive with Lightning: /invoice 1000 pay - Pay with Lightning: /pay lnbc10n1ps... donate - Donate: /donate 1000 faucet - Create a faucet: /faucet 2100 21 +tipjar - Create a tipjar: /tipjar 100 10 advanced - Advanced help \ No newline at end of file diff --git a/internal/telegram/help.go b/internal/telegram/help.go index 5e854138..bb9be99a 100644 --- a/internal/telegram/help.go +++ b/internal/telegram/help.go @@ -67,7 +67,11 @@ func (bot TipBot) makeAdvancedHelpMessage(ctx context.Context, m *tb.Message) st } // this is so stupid: - return fmt.Sprintf(Translate(ctx, "advancedMessage"), dynamicHelpMessage, GetUserStr(bot.Telegram.Me), GetUserStr(bot.Telegram.Me), GetUserStr(bot.Telegram.Me)) + return fmt.Sprintf( + Translate(ctx, "advancedMessage"), + dynamicHelpMessage, + GetUserStr(bot.Telegram.Me), + ) } func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) { diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index 0719280b..0582fc8d 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -101,7 +101,7 @@ func (bot TipBot) commandTranslationMap(ctx context.Context, command string) con // case "faucet": // ctx = context.WithValue(ctx, "publicLanguageCode", "en") // ctx = context.WithValue(ctx, "publicLocalizer", i18n.NewLocalizer(i18n.Bundle, "en")) - case "zapfhahn": + case "zapfhahn", "spendendose": ctx = context.WithValue(ctx, "publicLanguageCode", "de") ctx = context.WithValue(ctx, "publicLocalizer", i18n2.NewLocalizer(i18n.Bundle, "de")) case "kraan": @@ -135,7 +135,11 @@ func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) { } bot.handleInlineFaucetQuery(ctx, q) } - if strings.HasPrefix(q.Text, "tipjar") { + if strings.HasPrefix(q.Text, "tipjar") || strings.HasPrefix(q.Text, "spendendose") { + if len(strings.Split(q.Text, " ")) > 1 { + c := strings.Split(q.Text, " ")[0] + ctx = bot.commandTranslationMap(ctx, c) + } bot.handleInlineTipjarQuery(ctx, q) } diff --git a/translations/de.toml b/translations/de.toml index 1aa1faaf..d91d8ceb 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -110,15 +110,17 @@ advancedMessage = """%s 👉 *Inline Befehle* *send* 💸 Sende sats an einen Chat: `%s send [] []` -*receive* 🏅 Bitte um Zahlung: `%s receive [] []` -*faucet* 🚰 Erzeuge einen Zapfhahn: `%s faucet []` +*receive* 🏅 Bitte um Zahlung: `... receive [] []` +*faucet* 🚰 Erzeuge einen Zapfhahn: `... faucet []` +*tipjar* 🍯 Erzeuge eine Spendendose: `... tipjar []` 📖 Du kannst Inline Befehle in jedem Chat verwenden, sogar in privaten Nachrichten. Warte eine Sekunde, nachdem du den Befehl eingegeben hast und *klicke* auf das Ergebnis, statt Enter einzugeben. ⚙️ *Fortgeschrittene Befehle* */link* 🔗 Verbinde dein Wallet mit [BlueWallet](https://bluewallet.io/) oder [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl empfangen oder senden: `/lnurl` or `/lnurl ` -*/faucet* 🚰 Erzeuge einen Zapfhahn `/faucet `""" +*/faucet* 🚰 Erzeuge einen Zapfhahn: `/faucet ` +*/tipjar* 🍯 Erzeuge eine Spendendose: `/tipjar `""" # START @@ -284,7 +286,7 @@ inlineSendBalanceLowMessage = """🚫 Dein Guthaben reicht nicht aus.""" # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Empfange eine Zahlung in einem Chat.""" -inlineQueryReceiveDescription = """Befehl: @%s receive [] []""" +inlineQueryReceiveDescription = """Befehl: @%s receive [] []""" inlineResultReceiveTitle = """🏅 Empfange %d sat.""" inlineResultReceiveDescription = """👉 Klicke hier um eine Zahlung von %d sat zu empfangen.""" @@ -295,3 +297,27 @@ inlineReceiveCreateWalletMessage = """Chatte mit %s 👈 um dein Wallet zu verwa inlineReceiveYourselfMessage = """📖 Du kannst dich nicht selbst bezahlen.""" inlineReceiveFailedMessage = """🚫 Empfangen fehlgeschlagen.""" inlineReceiveCancelledMessage = """🚫 Empfangen abgebrochen.""" + +# TIPJAR + +inlineQueryTipjarTitle = """🍯 Erzeuge eine Spendendose.""" +inlineQueryTipjarDescription = """Befehl: @%s tipjar """ +inlineResultTipjarTitle = """🍯 Erzeuge eine Spendendose für %d sat.""" +inlineResultTipjarDescription = """👉 Klicke hier um eine Spendendose zu erzeugen.""" + +inlineTipjarMessage = """Drücke 💸 up *%d sat* in die Spendendose von %s zu *zahlen*. + +🙏 Gespendet: *%d*/%d sat (von %d Benutzern) +%s""" +inlineTipjarEndedMessage = """🍯 %s's Spendendose is voll ⭐️\n\n🏅 %d sat gespendet von %d Nutzern.""" +inlineTipjarAppendMemo = """\n✉️ %s""" +inlineTipjarCancelledMessage = """🚫 Spendendose abgebrochen.""" +inlineTipjarInvalidPeruserAmountMessage = """🚫 Die Pro-User Menge muss ein natürlicher Teiler der Gesamtmenge sein.""" +inlineTipjarInvalidAmountMessage = """🚫 Ungültige Menge.""" +inlineTipjarSentMessage = """🍯 %d sat an %s gesendet.""" +inlineTipjarReceivedMessage = """🍯 %s hat dir %d sat gesendet.""" +inlineTipjarHelpTipjarInGroup = """Erzeuge eine Spendendose in einer Gruppe, wo der Bot eingeladen ist oder benutze einen 👉 Inline Befehl (/advanced für mehr).""" +inlineTipjarHelpText = """📖 Ups, das hat nicht geklappt. %s + +*Befehl:* `/tipjar ` +*Beispiel:* `/tipjar 210 21`""" diff --git a/translations/en.toml b/translations/en.toml index b0215923..fb2f693e 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -60,7 +60,6 @@ infoButtonMessage = """Info""" cancelButtonEmoji = """🚫""" payButtonEmoji = """💸""" - # HELP helpMessage = """⚡️ *Wallet* @@ -114,15 +113,17 @@ advancedMessage = """%s 👉 *Inline commands* *send* 💸 Send sats to chat: `%s send [] []` -*receive* 🏅 Request a payment: `%s receive [] []` -*faucet* 🚰 Create a faucet: `%s faucet []` +*receive* 🏅 Request a payment: `... receive [] []` +*faucet* 🚰 Create a faucet: `... faucet []` +*tipjar* 🍯 Create a tipjar: `... tipjar []` 📖 You can use inline commands in every chat, even in private conversations. Wait a second after entering an inline command and *click* the result, don't press enter. ⚙️ *Advanced commands* */link* 🔗 Link your wallet to [BlueWallet](https://bluewallet.io/) or [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl receive or pay: `/lnurl` or `/lnurl ` -*/faucet* 🚰 Create a faucet `/faucet `""" +*/faucet* 🚰 Create a faucet: `/faucet ` +*/tipjar* 🍯 Create a tipjar: `/tipjar `""" # START @@ -322,4 +323,4 @@ inlineTipjarHelpTipjarInGroup = """Create a tipjar in a group with the inlineTipjarHelpText = """📖 Oops, that didn't work. %s *Usage:* `/tipjar ` -*Example:* `/tipjar 210 21`""" \ No newline at end of file +*Example:* `/tipjar 210 21`""" diff --git a/translations/es.toml b/translations/es.toml index 6f8962fc..b70b4a0f 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -110,15 +110,17 @@ advancedMessage = """%s 👉 *Comandos Inline* *send* 💸 Enviar sats al chat: `%s send [] []` -*receive* 🏅 Solicita un pago: `%s receive [] []`. -*faucet* 🚰 Crear un grifo: `%s faucet []`. +*receive* 🏅 Solicita un pago: `... receive [] []` +*faucet* 🚰 Crear un grifo: `... faucet []` +*tipjar* 🍯 Crear un tipjar: `... tipjar []` 📖 Puedes usar comandos _inline_ en todos los chats, incluso en las conversaciones privadas. Espera un segundo después de introducir un comando _inline_ y *haz clic* en el resultado, no pulses enter. ⚙️ *Comandos avanzados* */link* 🔗 Enlaza tu monedero a [ BlueWallet ](https://bluewallet.io/) o [ Zeus ](https://zeusln.app/) */lnurl* ⚡️ Lnurl recibir o pagar: `/lnurl` o `/lnurl ` -*/faucet* 🚰 Crear un grifo: `%s faucet `""" +*/faucet* 🚰 Crear un grifo: `/faucet ` +*/tipjar* 🍯 Crear un tipjar: `/tipjar `""" # START diff --git a/translations/fr.toml b/translations/fr.toml index d080fd60..18e5465c 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -110,15 +110,17 @@ advancedMessage = """%s 👉 *Inline commands* *send* 💸 Envoyer des sats : `%s send [] []` -*receive* 🏅 Demander un paiement : `%s receive [] []` -*faucet* 🚰 Créer un faucet: `%s faucet []` +*receive* 🏅 Demander un paiement : `... receive [] []` +*faucet* 🚰 Créer un faucet: `... faucet []` +*tipjar* 🍯 Créer un tipjar: `... tipjar []` 📖 Vous pouvez utiliser ces commandes dans tous les chats et même dans les conversations privées. Attendez une seconde après avoir tapé une commandé puis *click* sur le résultat, n'appuyez pas sur entrée. ⚙️ *Commandes avancées* */link* 🔗 Lier votre wallet à [BlueWallet](https://bluewallet.io/) ou [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl recevoir ou payer: `/lnurl` ou `/lnurl ` -*/faucet* 🚰 Créer un faucet `/faucet `""" +*/faucet* 🚰 Créer un faucet: `/faucet ` +*/tipjar* 🍯 Créer un tipjar: `/tipjar `""" # START diff --git a/translations/id.toml b/translations/id.toml index 57cc1c3b..d0d96b07 100644 --- a/translations/id.toml +++ b/translations/id.toml @@ -110,15 +110,17 @@ advancedMessage = """%s 👉 *Sebaris perintah* *send* 💸 Kirim sats ke percakapan: `%s send []` -*receive* 🏅 Permintaan pembayaran: `%s receive []` -*faucet* 🚰 Membuat sebuah keran: `%s faucet ` +*receive* 🏅 Permintaan pembayaran: `... receive []` +*faucet* 🚰 Membuat sebuah keran: `... faucet ` +*tipjar* 🍯 Create a tipjar: `... tipjar []` 📖 Kamu dapat menggunakan sebaris perintah di tiap percakapan, bahkan di percakapan privat. Tunggu sejenak setelah memasukkan sebaris perintah lalu *pencet* hasilnya, jangan tekan enter. ⚙️ *Perintah lanjutan* */link* 🔗 Menghubungkan dompet mu ke [BlueWallet](https://bluewallet.io/) atau [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl menerima atau membayar: `/lnurl` atau `/lnurl ` -*/faucet* 🚰 Membuat sebuah keran `/faucet `""" +*/faucet* 🚰 Membuat sebuah keran `/faucet ` +*/tipjar* 🍯 Create a tipjar: `/tipjar `""" # START diff --git a/translations/it.toml b/translations/it.toml index 913241e2..c39e7aa6 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -68,10 +68,10 @@ _Questo bot non applica commissioni agli utenti, ma costa alcuni Satoshi gestirl %s ⚙️ *Comandi* -*/tip* 🏅 Rispondi così a un messaggio per inviare una mancia: `/tip []` +*/tip* 🏅 Rispondi così a un messaggio per inviare una mancia: `/tip []` */balance* 👑 Verifica il tuo saldo residuo: `/balance` -*/send* 💸 Invia fondi a un utente: `/send @utente o utente@ln.tips []` -*/invoice* ⚡️ Ricevi attraverso Lightning: `/invoice []` +*/send* 💸 Invia fondi a un utente: `/send @utente o utente@ln.tips []` +*/invoice* ⚡️ Ricevi attraverso Lightning: `/invoice []` */pay* ⚡️ Paga attraverso Lightning: `/pay ` */donate* ❤️ Dona per supportare questo progetto: `/donate 1000` */advanced* 🤖 Funzioni avanzate. @@ -109,16 +109,18 @@ helpNoUsernameMessage = """👋 Per favore imposta un nome utente Telegram.""" advancedMessage = """%s 👉 *Comandi in linea* -*send* 💸 Invia alcuni sat a una chat: `%s send [] []` -*receive* 🏅 Richiedi un pagamento: `%s receive [] []` -*faucet* 🚰 Eroga fondi ai partecipanti della chat: `%s faucet []` +*send* 💸 Invia alcuni sat a una chat: `%s send [] []` +*receive* 🏅 Richiedi un pagamento: `... receive [] []` +*faucet* 🚰 Crea una distribuzione: `... faucet []` +*tipjar* 🍯 Crea un tipjar: `... tipjar []` 📖 Puoi usare i comandi in linea in ogni chat, anche nelle conversazioni private. Attendi un secondo dopo aver inviato un comando in linea e *clicca* sull'azione desiderata, non premere invio. ⚙️ *Comandi avanzati* */link* 🔗 Crea un collegamento al tuo wallet [BlueWallet](https://bluewallet.io/) o [Zeus](https://zeusln.app/) */lnurl* ⚡️ Ricevi o paga un Lnurl: `/lnurl` or `/lnurl ` -*/faucet* 🚰 Eroga fondi ai partecipanti della chat `/faucet `""" +*/faucet* 🚰 Crea una distribuzione: `/faucet ` +*/tipjar* 🍯 Crea un tipjar: `/tipjar `""" # START @@ -146,7 +148,7 @@ tipErrorMessage = """🚫 Invio mancia non riuscito.""" tipUndefinedErrorMsg = """Per favore riprova più tardi.""" tipHelpText = """📖 Ops, non funziona. %s -*Usage:* `/tip []` +*Usage:* `/tip []` *Example:* `/tip 1000 meme fantastico!`""" # SEND @@ -164,7 +166,7 @@ errorTryLaterMessage = """🚫 Errore. Per favore riprova più tardi.""" sendSyntaxErrorMessage = """Hai specificato un ammontare e un destinatario? Puoi usare il comando /send per inviare sia a utenti Telegram come %s sia a un indirizzo Lightning del tipo LightningTipBot@ln.tips.""" sendHelpText = """📖 Ops, non ha funzionato. %s -*Sintassi:* `/send []` +*Sintassi:* `/send []` *Esempio:* `/send 1000 @LightningTipBot Amo questo bot ❤️` *Esempio:* `/send 1234 LightningTipBot@ln.tips`""" @@ -175,7 +177,7 @@ invoiceEnterAmountMessage = """Hai inserito un ammontare?""" invoiceValidAmountMessage = """Hai inserito un ammontare valido?""" invoiceHelpText = """📖 Ops, non ha funzionato. %s -*Sintassi:* `/invoice []` +*Sintassi:* `/invoice []` *Esempio:* `/invoice 1000 Grazie!`""" # PAY @@ -243,7 +245,7 @@ couldNotLinkMessage = """🚫 Non sono riuscito a collegare il tuo wallet. Per f # FAUCET inlineQueryFaucetTitle = """🚰 Crea una distribuzione di fondi.""" -inlineQueryFaucetDescription = """Sintassi: @%s faucet """ +inlineQueryFaucetDescription = """Sintassi: @%s faucet """ inlineResultFaucetTitle = """🚰 Crea una distribuzione per un totale di %d sat.""" inlineResultFaucetDescription = """👉 Clicca qui per creare una distribuzione di fondi in questa chat.""" @@ -262,13 +264,13 @@ inlineFaucetReceivedMessage = """🚰 %s ti ha inviato %d sat.""" inlineFaucetHelpFaucetInGroup = """Crea una distribuzione in un gruppo in cui sia presente il bot oppure usa il 👉 comando in linea (/advanced per ulteriori funzionalità).""" inlineFaucetHelpText = """📖 Ops, non ha funzionato. %s -*Sintassi:* `/faucet ` +*Sintassi:* `/faucet ` *Esempio:* `/faucet 210 21`""" # INLINE SEND inlineQuerySendTitle = """💸 Invia pagamento in una chat.""" -inlineQuerySendDescription = """Sintassi: @%s send [] []""" +inlineQuerySendDescription = """Sintassi: @%s send [] []""" inlineResultSendTitle = """💸 Invio %d sat.""" inlineResultSendDescription = """👉 Clicca per inviare %d sat in questa chat.""" @@ -284,7 +286,7 @@ inlineSendBalanceLowMessage = """🚫 Il tuo saldo è insufficiente (%d sat). # INLINE RECEIVE inlineQueryReceiveTitle = """🏅 Richiedi un pagamento in una chat.""" -inlineQueryReceiveDescription = """Sintassi: @%s receive [] []""" +inlineQueryReceiveDescription = """Sintassi: @%s receive [] []""" inlineResultReceiveTitle = """🏅 Ricevi %d sat.""" inlineResultReceiveDescription = """👉 Clicca per richiedere un pagamento di %d sat.""" diff --git a/translations/nl.toml b/translations/nl.toml index 9c87d686..d995ac90 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -110,15 +110,17 @@ advancedMessage = """%s 👉 *Inline commands* *send* 💸 Stuur sats naar chat: `%s send [] []` -*receive* 🏅 Verzoek om betaling: `%s receive [] []` -*faucet* 🚰 Maak een kraan: `%s faucet []` +*receive* 🏅 Verzoek om betaling: `... receive [] []` +*faucet* 🚰 Maak een kraan: `... faucet []` +*tipjar* 🍯 Maak een tipjar: `... tipjar []` 📖 Je kunt inline commando's in elke chat gebruiken, zelfs in privé gesprekken. Wacht een seconde na het invoeren van een inline commando en *klik* op het resultaat, druk niet op enter. ⚙️ *Geavanceerde opdrachten* */link* 🔗 Koppel uw wallet aan [BlueWallet](https://bluewallet.io/) of [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl ontvangen of betalen: `/lnurl` of `/lnurl ` -*/faucet* 🚰 Maak een kraan `/faucet `""" +*/faucet* 🚰 Maak een kraan: `/faucet ` +*/tipjar* 🍯 Maak een tipjar: `/tipjar `""" # START diff --git a/translations/pt-br.toml b/translations/pt-br.toml index 410fec00..8bac0c8e 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -110,15 +110,17 @@ advancedMessage = """%s 👉 *Comandos Inline* *send* 💸 Enviar sats ao bate-papo: `%s send [] []` -*receive* 🏅 Solicite um pagamento: `%s receive [] []`. -*faucet* 🚰 Criar uma torneira: `%s faucet []`. +*receive* 🏅 Solicite um pagamento: `... receive [] []` +*faucet* 🚰 Criar uma torneira: `... faucet []` +*tipjar* 🍯 Criar uma tipjar: `... tipjar []` 📖 Você pode usar comandos _inline_ em todas as conversas, mesmo em conversas privadas. Espere um segundo após inserir um comando _inline_ e *clique* no resultado, não pressione enter. ⚙️ *Comandos avançados* */link* 🔗 Vincule sua carteira a [ BlueWallet ](https://bluewallet.io/) ou [ Zeus ](https://zeusln.app/) */lnurl* ⚡️ Receber ou pagar com lnurl: `/lnurl` o `/lnurl ` -*/faucet* 🚰 Criar uma torneira: `%s faucet `""" +*/faucet* 🚰 Criar uma torneira: `/faucet ` +*/tipjar* 🍯 Criar uma tipjar: `/tipjar `""" # START diff --git a/translations/tr.toml b/translations/tr.toml index 03f09648..adbc4d86 100644 --- a/translations/tr.toml +++ b/translations/tr.toml @@ -112,13 +112,15 @@ advancedMessage = """%s *send* 💸 Bir sohbete sat gönder: `%s send [] []` *receive* 🏅 Ödeme iste: `%s receive [] []` *faucet* 🚰 Bir fıçı oluştur: `%s faucet []` - +*tipjar* 🍯 Bir tipjar oluştur: `... tipjar []` + 📖 İnline komutları her sohbette ve hatta özel mesajlarda kullanabilirsin. Komutu yazdıktan sonra bir saniye bekle ve Enter yazmak yerine sonuca *tıkla*. ⚙️ *Gelişmiş komutlar* */link* 🔗 Cüzdanını bağla: [BlueWallet](https://bluewallet.io/) veya [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl iste veya gönder: `/lnurl` veya `/lnurl ` -*/faucet* 🚰 Bir fıçı oluştur: `/faucet `""" +*/faucet* 🚰 Bir fıçı oluştur: `/faucet ` +*/tipjar* 🍯 Bir tipjar oluştur: `/tipjar `""" # START From e4833bd9c60f4e32396cd4aecc7b830ed38b024f Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 29 Oct 2021 22:04:53 +0200 Subject: [PATCH 038/541] User cache (#117) * adding users cache in memory * adding users cache in memory * adding eko/gocache and removing CopyLowercaseUser * using UpdateUserRecord * updateCachedUser in user states * remove redundant updatePersistentUser * refactor * remove cachedUser * remove user chan * errors for failed invoice creation * errors for failed invoice and failed pay * add GetUserBalanceCached inline commands * translation * remove redundant set user state reset state * update cache ttl Co-authored-by: lngohumble --- go.mod | 4 +- go.sum | 441 ++++++++++++++++++++++++++++ internal/lnurl/lnurl.go | 3 +- internal/telegram/bot.go | 9 + internal/telegram/database.go | 56 +++- internal/telegram/inline_faucet.go | 2 +- internal/telegram/inline_receive.go | 2 +- internal/telegram/inline_send.go | 2 +- internal/telegram/invoice.go | 4 + internal/telegram/pay.go | 4 +- internal/telegram/users.go | 46 +-- 11 files changed, 530 insertions(+), 43 deletions(-) diff --git a/go.mod b/go.mod index 422f3d07..c322e98e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/BurntSushi/toml v0.3.1 + github.com/eko/gocache v1.2.0 github.com/fiatjaf/go-lnurl v1.4.0 github.com/fiatjaf/ln-decodepay v1.1.0 github.com/gorilla/mux v1.8.0 @@ -11,7 +12,8 @@ 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/sirupsen/logrus v1.2.0 + github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/sirupsen/logrus v1.6.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tidwall/buntdb v1.2.7 github.com/tidwall/gjson v1.10.2 diff --git a/go.sum b/go.sum index baf1a12b..6b52cb9d 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,49 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= git.schwanenlied.me/yawning/bsaes.git v0.0.0-20180720073208-c0276d75487e/go.mod h1:BWqTsj8PgcPriQJGl7el20J/7TuT1d/hSyFDXMEpoEo= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bdzq+51GR4/0DIhaICZEOm+OHvXGwwB2trKZ8B4Y6eQ= github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc= +github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2 h1:pami0oPhVosjOu/qRHepRmdjD6hGILF7DBr+qQZeP10= +github.com/XiaoMi/pegasus-go-client v0.0.0-20210427083443-f3b6b08bc4c2/go.mod h1:jNIx5ykW1MroBuaTja9+VpglmaJOUzezumfhLlER3oY= github.com/Yawning/aez v0.0.0-20180114000226-4dad034d9db2/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/allegro/bigcache/v2 v2.2.5 h1:mRc8r6GQjuJsmSKQNPsR5jQVXc8IJ1xsW5YXUYMLfqI= +github.com/allegro/bigcache/v2 v2.2.5/go.mod h1:FppZsIO+IZk7gCuj5FiIDHGygD9xvWQcqg1uIPMb6tY= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0= +github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/btcsuite/btcd v0.0.0-20190629003639-c26ffa870fd8/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.0.0-20190824003749-130ea5bddde3/go.mod h1:3J08xEfcugPacsc34/LKRU2yO7YmuT8yt28J8k2+rrI= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= @@ -54,50 +84,173 @@ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.0 h1:c8LkOFQTzuO0WBM/ae5HdGQuZPfPxp7lqBRwQRm4fSc= +github.com/cenkalti/backoff/v4 v4.1.0/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coocood/freecache v1.1.1 h1:uukNF7QKCZEdZ9gAV7WQzvh0SbjwdMF6m3x3rxEkaPc= +github.com/coocood/freecache v1.1.1/go.mod h1:OKrEjkGVoxZhyWAJoeFi5BMLUJm2Tit0kpGkIr7NGYY= github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgraph-io/ristretto v0.0.3 h1:jh22xisGBjrEVnRZ1DVTpBVQm0Xndu8sMl0CWDzSIBI= +github.com/dgraph-io/ristretto v0.0.3/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/eko/gocache v1.2.0 h1:SCtTs65qMXjhdtu62yHPCQuzdMkQjP+fQmkNrVutkRw= +github.com/eko/gocache v1.2.0/go.mod h1:6u8/2bnr+nOf87mRXWS710rqNNZUECF4CGsPNnsoJ78= +github.com/elazarl/goproxy v0.0.0-20170405201442-c4fc26588b6e/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fiatjaf/go-lnurl v1.4.0 h1:hVFEEJD2A9D6ojEcqLyD54CM2ZJ9Tzs2jNKw/GNq52A= github.com/fiatjaf/go-lnurl v1.4.0/go.mod h1:BqA8WXAOzntF7Z3EkVO7DfP4y5rhWUmJ/Bu9KBke+rs= github.com/fiatjaf/ln-decodepay v1.1.0 h1:HigjqNH+ApiO6gm7RV23jXNFuvwq+zgsWl4BJAfPWwE= github.com/fiatjaf/ln-decodepay v1.1.0/go.mod h1:2qdTT95b8Z4dfuxiZxXuJ1M7bQ9CrLieEA1DKC50q6s= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.2.2/go.mod h1:Qh/WofXFeiAFII1aEBu529AtJo6Zg2VHscnEsbBnJ20= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94= +github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= +github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= +github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= +github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-redis/redis/v8 v8.8.2 h1:O/NcHqobw7SEptA0yA6up6spZVFtwE06SXM8rgLtsP8= +github.com/go-redis/redis/v8 v8.8.2/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqWMnCV1iP5Y= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gnostic v0.0.0-20170729233727-0c5108395e2d/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.8.6/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/imroc/req v0.3.0 h1:3EioagmlSG+z+KySToa+Ylo3pTFZs+jh3Brl7ngU12U= github.com/imroc/req v0.3.0/go.mod h1:F+NZ+2EFSo6EFXdeIbpfE9hcC233id70kf0byW97Caw= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= @@ -110,8 +263,18 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.2 h1:eVKgfIdy9b6zbWBMgFpfDPoAMifwSZagU9HmEU6zgiI= github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/jrick/logrotate v1.0.0 h1:lQ1bL/n9mBNeIXoTUoYRlK4dHuNJVofX9oWqBtPnSzI= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= github.com/juju/errors v0.0.0-20190806202954-0232dcc7464d/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= @@ -120,12 +283,17 @@ github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0t github.com/juju/utils v0.0.0-20180820210520-bf9cc5bdd62d/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec h1:n1NeQ3SgUHyISrjFFoO5dR748Is8dBL9qpaTNfphQrs= github.com/kkdai/bstream v0.0.0-20181106074824-b3251f7901ec/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -147,56 +315,176 @@ github.com/lightningnetwork/lnd/queue v1.0.3 h1:5ufYVE7lh9GJnL1wOoeO3bZ3aAHWNnkN github.com/lightningnetwork/lnd/queue v1.0.3/go.mod h1:YTkTVZCxz8tAYreH27EO3s8572ODumWrNdYW2E/YKxg= github.com/lightningnetwork/lnd/ticker v1.0.0 h1:S1b60TEGoTtCe2A0yeB+ecoj/kkS4qpwh6l+AkQEZwU= github.com/lightningnetwork/lnd/ticker v1.0.0/go.mod h1:iaLXJiVgI1sPANIF2qYYUJXjoksPNvGNYowB8aRbpX0= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY= github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/makiuchi-d/gozxing v0.0.2 h1:TGSCQRXd9QL1ze1G1JE9sZBMEr6/HLx7m5ADlLUgq7E= github.com/makiuchi-d/gozxing v0.0.2/go.mod h1:Tt5nF+kNliU+5MDxqPpsFrtsWNdABQho/xdCZZVKCQc= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= github.com/mattn/go-sqlite3 v1.14.5/go.mod h1:WVKg1VTActs4Qso6iwGbiFih2UIHo0ENGwNd0Lj+XmI= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8 h1:PRMAcldsl4mXKJeRNB/KVNz6TlbS6hk2Rs42PqgU3Ws= github.com/miekg/dns v0.0.0-20171125082028-79bfde677fa8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c= github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= +github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= +github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +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/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= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pegasus-kv/thrift v0.13.0 h1:4ESwaNoHImfbHa9RUGJiJZ4hrxorihZHk5aarYwY8d4= +github.com/pegasus-kv/thrift v0.13.0/go.mod h1:Gl9NT/WHG6ABm6NsrbfE8LiJN0sAyneCrvB4qN4NPqQ= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.18.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= +github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/tidwall/assert v0.1.0 h1:aWcKyRBUAdLoVebxo95N7+YZVTFF/ASTr7BN4sLP6XI= github.com/tidwall/assert v0.1.0/go.mod h1:QLYtGyeqse53vuELQheYl9dngGCJQ+mTtlxcktb+Kj8= github.com/tidwall/btree v0.6.1 h1:75VVgBeviiDO+3g4U+7+BaNBNhNINxB0ULPT3fs9pMY= @@ -225,86 +513,228 @@ github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= github.com/urfave/cli v1.18.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.mongodb.org/mongo-driver v1.0.3/go.mod h1:u7ryQJ+DOzQmeO7zB6MHyr8jkEQvC8vH7qLUO4lqsUM= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/otel v0.19.0 h1:Lenfy7QHRXPZVsw/12CWpxX6d/JkrX8wrx2vO8G80Ng= +go.opentelemetry.io/otel v0.19.0/go.mod h1:j9bF567N9EfomkSidSfmMwIwIBuP37AMAIzVW85OxSg= +go.opentelemetry.io/otel/metric v0.19.0 h1:dtZ1Ju44gkJkYvo+3qGqVXmf88tc+a42edOywypengg= +go.opentelemetry.io/otel/metric v0.19.0/go.mod h1:8f9fglJPRnXuskQmKpnad31lcLJ2VmNNqIsx/uIwBSc= +go.opentelemetry.io/otel/oteltest v0.19.0 h1:YVfA0ByROYqTwOxqHVZYZExzEpfZor+MU1rU+ip2v9Q= +go.opentelemetry.io/otel/oteltest v0.19.0/go.mod h1:tI4yxwh8U21v7JD6R3BcA/2+RBoTKFexE/PJ/nSO7IA= +go.opentelemetry.io/otel/trace v0.19.0 h1:1ucYlenXIDA1OlHVLDZKX0ObXV5RLaq06DtUKz5e5zc= +go.opentelemetry.io/otel/trace v0.19.0/go.mod h1:4IXiNextNOpPnRlI4ryK69mn5iC84bjBWZQA5DXz/qg= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37 h1:cg5LA/zNPRzIXIWSCxQW10Rvpy94aQh3LT/ShoCpkHw= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191105084925-a882066a44e0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a h1:DcqTD9SDLc+1P/r1EmRBwnVsrOwW+kk2vWf9n+1sGhs= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191010194322-b09406accb47/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= +golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030221726-6c7e314b6563/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190201180003-4b09977fb922/go.mod h1:L3J43x8/uS+qIUoksaLKe6OS3nUKxOKuIFz1sl2/jx4= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 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.3.5 h1:TdMJTlG8kvepsvZdy/gPeYEBdwKdwFFjH1AQTua9BOU= gopkg.in/tucnak/telebot.v2 v2.3.5/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= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= @@ -312,3 +742,14 @@ gorm.io/gorm v1.21.12 h1:3fQM0Eiz7jcJEhPggHEpoYnsGZqynMzverL77DV40RM= gorm.io/gorm v1.21.12/go.mod h1:F+OptMscr0P2F2qU97WT1WimdH9GaQPoDW7AYd5i2Y0= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +k8s.io/apimachinery v0.0.0-20191123233150-4c4803ed55e3 h1:FErmbNIJruD5GT2oVEjtPn5Ar5+rcWJsC8/PPUkR0s4= +k8s.io/apimachinery v0.0.0-20191123233150-4c4803ed55e3/go.mod h1:b9qmWdKlLuU9EBh+06BtLcSf/Mu89rWL33naRxs1uZg= +k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= +k8s.io/kube-openapi v0.0.0-20191107075043-30be4d16710a/go.mod h1:1TqjTSzOxsLGIKfj0lK8EeCP7K1iUG65v09OM0/WG5E= +sigs.k8s.io/structured-merge-diff v0.0.0-20190525122527-15d366b2352e/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 62d2c404..aed953d2 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -8,7 +8,6 @@ import ( "net/http" "net/url" "strconv" - "strings" "time" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -133,7 +132,7 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) tx = w.database.Where("anon_id = ?", username).First(user) } else { // assume it's a string @username - tx = w.database.Where("telegram_username = ?", strings.ToLower(username)).First(user) + tx = w.database.Where("telegram_username = ? COLLATE NOCASE", username).First(user) } if tx.Error != nil { diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index d201e8d3..81c9bb9b 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -2,12 +2,14 @@ package telegram import ( "fmt" + "github.com/eko/gocache/store" "sync" "time" "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/storage" + gocache "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" "gopkg.in/tucnak/telebot.v2" tb "gopkg.in/tucnak/telebot.v2" @@ -20,6 +22,10 @@ type TipBot struct { logger *gorm.DB Telegram *telebot.Bot Client *lnbits.Client + Cache +} +type Cache struct { + *store.GoCacheStore } var ( @@ -29,6 +35,8 @@ var ( // NewBot migrates data and creates a new bot func NewBot() TipBot { + gocacheClient := gocache.New(5*time.Minute, 10*time.Minute) + gocacheStore := store.NewGoCache(gocacheClient, nil) // create sqlite databases db, txLogger := AutoMigration() return TipBot{ @@ -37,6 +45,7 @@ func NewBot() TipBot { logger: txLogger, Bunt: createBunt(), Telegram: newTelegramBot(), + Cache: Cache{GoCacheStore: gocacheStore}, } } diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 2a504a2b..02aeab6b 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -2,9 +2,9 @@ package telegram import ( "fmt" + "github.com/eko/gocache/store" "reflect" "strconv" - "strings" "time" "github.com/LightningTipBot/LightningTipBot/internal" @@ -79,7 +79,7 @@ func AutoMigration() (db *gorm.DB, txLogger *gorm.DB) { func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.User, error) { toUserDb := &lnbits.User{} - tx := bot.Database.Where("telegram_username = ?", strings.ToLower(toUserStrWithoutAt)).First(toUserDb) + tx := bot.Database.Where("telegram_username = ? COLLATE NOCASE", toUserStrWithoutAt).First(toUserDb) if tx.Error != nil || toUserDb.Wallet == nil { err := tx.Error if toUserDb.Wallet == nil { @@ -89,6 +89,14 @@ func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.U } return toUserDb, nil } +func getCachedUser(u *tb.User, bot TipBot) (*lnbits.User, error) { + user := &lnbits.User{Name: strconv.Itoa(u.ID)} + if us, err := bot.Cache.Get(user.Name); err == nil { + return us.(*lnbits.User), nil + } + user.Telegram = u + return user, gorm.ErrRecordNotFound +} // GetLnbitsUser will not update the user in Database. // this is required, because fetching lnbits.User from a incomplete tb.User @@ -105,31 +113,44 @@ func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { user.Telegram = u return user, tx.Error } + // todo -- unblock this ! return user, nil } // GetUser from Telegram user. Update the user if user information changed. func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { - user, err := GetLnbitsUser(u, bot) - if err != nil { - return user, err + var user *lnbits.User + var err error + if user, err = getCachedUser(u, bot); err != nil { + user, err = GetLnbitsUser(u, bot) + if err != nil { + return user, err + } + updateCachedUser(user, bot) } - go func() { - userCopy := bot.CopyLowercaseUser(u) - if !reflect.DeepEqual(userCopy, user.Telegram) { - // update possibly changed user details in Database - user.Telegram = userCopy - err = UpdateUserRecord(user, bot) - if err != nil { - log.Warnln(fmt.Sprintf("[UpdateUserRecord] %s", err.Error())) - } + if telegramUserChanged(u, user.Telegram) { + // update possibly changed user details in Database + user.Telegram = u + err = UpdateUserRecord(user, bot) + if err != nil { + log.Warnln(fmt.Sprintf("[UpdateUserRecord] %s", err.Error())) } - }() + } return user, err } +func updateCachedUser(apiUser *lnbits.User, bot TipBot) { + bot.Cache.Set(apiUser.Name, apiUser, &store.Options{Expiration: 1 * time.Minute}) +} + +func telegramUserChanged(apiUser, stateUser *tb.User) bool { + if reflect.DeepEqual(apiUser, stateUser) { + return false + } + return true +} + func UpdateUserRecord(user *lnbits.User, bot TipBot) error { - user.Telegram = bot.CopyLowercaseUser(user.Telegram) user.UpdatedAt = time.Now() tx := bot.Database.Save(user) if tx.Error != nil { @@ -138,5 +159,8 @@ func UpdateUserRecord(user *lnbits.User, bot TipBot) error { return tx.Error } log.Debugf("[UpdateUserRecord] Records of user %s updated.", GetUserStr(user.Telegram)) + if bot.Cache.GoCacheStore != nil { + updateCachedUser(user, bot) + } return nil } diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index ed80b2c7..254f9279 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -64,7 +64,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User nTotal := amount / perUserAmount fromUser := LoadUser(ctx) fromUserStr := GetUserStr(sender) - balance, err := bot.GetUserBalance(fromUser) + balance, err := bot.GetUserBalanceCached(fromUser) if err != nil { return nil, errors.New(errors.GetBalanceError, err) } diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 30a83721..25fb4c4e 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -184,7 +184,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac return } // balance check of the user - balance, err := bot.GetUserBalance(from) + balance, err := bot.GetUserBalanceCached(from) if err != nil { errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) log.Errorln(errmsg) diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index fd81c1b8..b9fe234e 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -60,7 +60,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { } fromUser := LoadUser(ctx) fromUserStr := GetUserStr(&q.From) - balance, err := bot.GetUserBalance(fromUser) + balance, err := bot.GetUserBalanceCached(fromUser) if err != nil { errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) log.Errorln(errmsg) diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 26cfc30b..285960eb 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -64,6 +64,7 @@ func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { memo = memo + tag } + creatingMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlGettingUserMessage")) log.Infof("[/invoice] Creating invoice for %s of %d sat.", userStr, amount) // generate invoice invoice, err := user.Wallet.Invoice( @@ -75,6 +76,7 @@ func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { bot.Client) if err != nil { errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) + bot.tryEditMessage(creatingMsg, Translate(ctx, "errorTryLaterMessage")) log.Errorln(errmsg) return } @@ -83,10 +85,12 @@ func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { qr, err := qrcode.Encode(invoice.PaymentRequest, qrcode.Medium, 256) if err != nil { errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err) + bot.tryEditMessage(creatingMsg, Translate(ctx, "errorTryLaterMessage")) log.Errorln(errmsg) return } + bot.tryDeleteMessage(creatingMsg) // send the invoice data to user bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) log.Printf("[/invoice] Incvoice created. User: %s, amount: %d sat.", userStr, amount) diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index dbdc4663..e67eaf55 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -159,10 +159,12 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { if err != nil { log.Errorf("[acceptSendHandler] %s", err) bot.tryDeleteMessage(c.Message) + bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) return } if !payData.Active { log.Errorf("[confirmPayHandler] send not active anymore") + bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) bot.tryDeleteMessage(c.Message) return } @@ -219,7 +221,7 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { - log.Errorf("[cancelPaymentHandler] %s", err) + log.Errorf("[cancelPaymentHandler] %s", err.Error()) return } payData := sn.(*PayData) diff --git a/internal/telegram/users.go b/internal/telegram/users.go index 6ba03997..e9ee41bb 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -3,26 +3,25 @@ package telegram import ( "errors" "fmt" - "strings" - - "github.com/LightningTipBot/LightningTipBot/internal/str" - "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/str" + "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" "gorm.io/gorm" + "time" ) func SetUserState(user *lnbits.User, bot *TipBot, stateKey lnbits.UserStateKey, stateData string) { user.StateKey = stateKey user.StateData = stateData - bot.Database.Table("users").Where("name = ?", user.Name).Update("state_key", user.StateKey).Update("state_data", user.StateData) + UpdateUserRecord(user, *bot) + } func ResetUserState(user *lnbits.User, bot *TipBot) { user.ResetState() - bot.Database.Table("users").Where("name = ?", user.Name).Update("state_key", 0).Update("state_data", "") + UpdateUserRecord(user, *bot) } func GetUserStr(user *tb.User) string { @@ -60,8 +59,16 @@ func appendUinqueUsersToSlice(slice []*tb.User, i *tb.User) []*tb.User { return append(slice, i) } -func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int, err error) { +func (bot *TipBot) GetUserBalanceCached(user *lnbits.User) (amount int, err error) { + u, err := bot.Cache.Get(fmt.Sprintf("%s_balance", user.Name)) + if err != nil { + return bot.GetUserBalance(user) + } + cachedBalance := u.(int) + return cachedBalance, nil +} +func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int, err error) { wallet, err := bot.Client.Info(*user.Wallet) if err != nil { errmsg := fmt.Sprintf("[GetUserBalance] Error: Couldn't fetch user %s's info from LNbits: %s", GetUserStr(user.Telegram), err.Error()) @@ -76,19 +83,18 @@ func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int, err error) { // msat to sat amount = int(wallet.Balance) / 1000 log.Infof("[GetUserBalance] %s's balance: %d sat\n", GetUserStr(user.Telegram), amount) - return -} -// CopyLowercaseUser will create a coy user and cast username to lowercase. -func (bot *TipBot) CopyLowercaseUser(u *tb.User) *tb.User { - userCopy := *u - userCopy.Username = strings.ToLower(u.Username) - return &userCopy + // update user balance in cache + bot.Cache.Set( + fmt.Sprintf("%s_balance", user.Name), + amount, + &store.Options{Expiration: 30 * time.Second}, + ) + return } func (bot *TipBot) CreateWalletForTelegramUser(tbUser *tb.User) (*lnbits.User, error) { - userCopy := bot.CopyLowercaseUser(tbUser) - user := &lnbits.User{Telegram: userCopy} + user := &lnbits.User{Telegram: tbUser} userStr := GetUserStr(tbUser) log.Printf("[CreateWalletForTelegramUser] Creating wallet for user %s ... ", userStr) err := bot.createWallet(user) @@ -97,9 +103,9 @@ func (bot *TipBot) CreateWalletForTelegramUser(tbUser *tb.User) (*lnbits.User, e log.Errorln(errmsg) return user, err } - tx := bot.Database.Save(user) - if tx.Error != nil { - return nil, tx.Error + err = UpdateUserRecord(user, *bot) + if err != nil { + return nil, err } log.Printf("[CreateWalletForTelegramUser] Wallet created for user %s. ", userStr) return user, nil From 11edce049ebdff68e1af43a6f424dcfe0ef923ce Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 30 Oct 2021 21:26:01 +0200 Subject: [PATCH 039/541] General amount (#118) * generalized amount started * add new file * uncomment old pay * longer comments * lnurl pay with generalized amount input * generalized amount for /invoice * amount input for all commands * remove logging * delete old comments --- internal/lnbits/types.go | 2 + internal/lnurl/server.go | 7 +- internal/telegram/amounts.go | 140 ++++++++++- internal/telegram/donate.go | 27 +- internal/telegram/invoice.go | 26 +- internal/telegram/lnurl-pay.go | 226 +++++++++++++++++ internal/telegram/lnurl.go | 435 +++++++++++++-------------------- internal/telegram/pay.go | 75 +++--- internal/telegram/send.go | 9 +- internal/telegram/text.go | 4 +- translations/de.toml | 2 +- translations/en.toml | 3 +- translations/es.toml | 2 +- translations/fr.toml | 2 +- translations/id.toml | 2 +- translations/it.toml | 2 +- translations/nl.toml | 2 +- translations/pt-br.toml | 2 +- translations/tr.toml | 2 +- 19 files changed, 629 insertions(+), 341 deletions(-) create mode 100644 internal/telegram/lnurl-pay.go diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index ab671412..c0398ca5 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -32,6 +32,8 @@ const ( UserStateConfirmSend UserStateLNURLEnterAmount UserStateConfirmLNURLPay + UserEnterAmount + UserHasEnteredAmount ) type UserStateKey int diff --git a/internal/lnurl/server.go b/internal/lnurl/server.go index 0bb0259d..0de73de8 100644 --- a/internal/lnurl/server.go +++ b/internal/lnurl/server.go @@ -2,12 +2,13 @@ package lnurl import ( "encoding/json" - "github.com/LightningTipBot/LightningTipBot/internal" - "github.com/LightningTipBot/LightningTipBot/internal/telegram" "net/http" "net/url" "time" + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/telegram" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/gorilla/mux" @@ -32,7 +33,7 @@ const ( lnurlEndpoint = ".well-known/lnurlp" minSendable = 1000 // mSat MaxSendable = 1_000_000_000 - CommentAllowed = 256 + CommentAllowed = 500 ) func NewServer(bot *telegram.TipBot) *Server { diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index e30728e2..e917a164 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -1,12 +1,19 @@ package telegram import ( + "context" + "encoding/json" "errors" + "fmt" "strconv" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/price" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" log "github.com/sirupsen/logrus" + tb "gopkg.in/tucnak/telebot.v2" ) func getArgumentFromCommand(input string, which int) (output string, err error) { @@ -64,10 +71,135 @@ func getAmount(input string) (amount int, err error) { if err != nil { return 0, err } - if amount < 1 { - errmsg := "error: Amount must be greater than 0" - // log.Errorln(errmsg) - return 0, errors.New(errmsg) + if amount <= 0 { + return 0, errors.New("amount must be greater than 0") } return amount, err } + +type EnterAmountStateData struct { + ID string `json:"ID"` // holds the ID of the tx object in bunt db + Type string `json:"Type"` // holds type of the tx in bunt db (needed for type checking) + Amount int64 `json:"Amount"` // holds the amount entered by the user mSat + AmountMin int64 `json:"AmountMin"` // holds the minimum amount that needs to be entered mSat + AmountMax int64 `json:"AmountMax"` // holds the maximum amount that needs to be entered mSat + OiringalCommand string `json:"OiringalCommand"` // hold the originally entered command for evtl later use +} + +func (bot *TipBot) askForAmount(ctx context.Context, id string, eventType string, amountMin int64, amountMax int64, originalCommand string) (enterAmountStateData *EnterAmountStateData, err error) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + enterAmountStateData = &EnterAmountStateData{ + ID: id, + Type: eventType, + AmountMin: amountMin, + AmountMax: amountMax, + OiringalCommand: originalCommand, + } + // set LNURLPayResponse1 in the state of the user + stateDataJson, err := json.Marshal(enterAmountStateData) + if err != nil { + log.Errorln(err) + return + } + SetUserState(user, bot, lnbits.UserEnterAmount, string(stateDataJson)) + askAmountText := Translate(ctx, "lnurlEnterAmountMessage") + if amountMin > 0 && amountMax >= amountMin { + askAmountText = fmt.Sprintf(Translate(ctx, "lnurlEnterAmountRangeMessage"), enterAmountStateData.AmountMin/1000, enterAmountStateData.AmountMax/1000) + } + // Let the user enter an amount and return + bot.trySendMessage(user.Telegram, askAmountText, tb.ForceReply) + return +} + +// enterAmountHandler is invoked in anyTextHandler when the user needs to enter an amount +// the amount is then stored as an entry in the user's stateKey in the user database +// any other handler that relies on this, needs to load the resulting amount from the database +func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + + if !(user.StateKey == lnbits.UserEnterAmount) { + ResetUserState(user, bot) + return // errors.New("user state does not match"), 0 + } + + var EnterAmountStateData EnterAmountStateData + err := json.Unmarshal([]byte(user.StateData), &EnterAmountStateData) + if err != nil { + log.Errorf("[enterAmountHandler] %s", err.Error()) + ResetUserState(user, bot) + return + } + + amount, err := getAmount(m.Text) + if err != nil { + log.Warnf("[enterAmountHandler] %s", err.Error()) + bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) + ResetUserState(user, bot) + return //err, 0 + } + // amount not in allowed range from LNURL + if EnterAmountStateData.AmountMin > 0 && EnterAmountStateData.AmountMax >= EnterAmountStateData.AmountMin && // this line checks whether min_max is set at all + (amount > int(EnterAmountStateData.AmountMax/1000) || amount < int(EnterAmountStateData.AmountMin/1000)) { // this line then checks whether the amount is in the range + err = fmt.Errorf("amount not in range") + log.Warnf("[enterAmountHandler] %s", err.Error()) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), EnterAmountStateData.AmountMin/1000, EnterAmountStateData.AmountMax/1000)) + ResetUserState(user, bot) + return + } + + // find out which type the object in bunt has waiting for an amount + // we stored this in the EnterAmountStateData before + switch EnterAmountStateData.Type { + case "LnurlPayState": + tx := &LnurlPayState{Base: transaction.New(transaction.ID(EnterAmountStateData.ID))} + sn, err := tx.Get(tx, bot.Bunt) + if err != nil { + return + } + LnurlPayState := sn.(*LnurlPayState) + LnurlPayState.Amount = amount * 1000 // mSat + // add result to persistent struct + runtime.IgnoreError(LnurlPayState.Set(LnurlPayState, bot.Bunt)) + + EnterAmountStateData.Amount = int64(amount) * 1000 // mSat + StateDataJson, err := json.Marshal(EnterAmountStateData) + if err != nil { + log.Errorln(err) + return + } + SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(StateDataJson)) + bot.lnurlPayHandlerSend(ctx, m) + return + case "CreateInvoiceState": + m.Text = fmt.Sprintf("/invoice %d", amount) + SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") + bot.invoiceHandler(ctx, m) + return + case "CreateDonationState": + m.Text = fmt.Sprintf("/donate %d", amount) + SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") + bot.donationHandler(ctx, m) + return + case "CreateSendState": + splits := strings.SplitAfterN(EnterAmountStateData.OiringalCommand, " ", 2) + if len(splits) > 1 { + m.Text = fmt.Sprintf("/send %d %s", amount, splits[1]) + SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") + bot.sendHandler(ctx, m) + return + } + return + default: + ResetUserState(user, bot) + return + } + // // reset database entry + // ResetUserState(user, bot) + // return +} diff --git a/internal/telegram/donate.go b/internal/telegram/donate.go index 9f035d9f..2592f29c 100644 --- a/internal/telegram/donate.go +++ b/internal/telegram/donate.go @@ -3,12 +3,13 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/str" "io" "io/ioutil" "net/http" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/str" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" @@ -34,24 +35,22 @@ func helpDonateUsage(ctx context.Context, errormsg string) string { func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { // check and print all commands bot.anyTextHandler(ctx, m) - - if len(strings.Split(m.Text, " ")) < 2 { - bot.trySendMessage(m.Sender, helpDonateUsage(ctx, Translate(ctx, "donateEnterAmountMessage"))) + user := LoadUser(ctx) + if user.Wallet == nil { return } + // if no amount is in the command, ask for it amount, err := decodeAmountFromCommand(m.Text) - if err != nil { - return - } - if amount < 1 { - bot.trySendMessage(m.Sender, helpDonateUsage(ctx, Translate(ctx, "donateValidAmountMessage"))) + if (err != nil || amount < 1) && m.Chat.Type == tb.ChatPrivate { + // // no amount was entered, set user state and ask for amount + bot.askForAmount(ctx, "", "CreateDonationState", 0, 0, m.Text) return } // command is valid - msg := bot.trySendMessage(m.Sender, Translate(ctx, "donationProgressMessage")) + msg := bot.trySendMessage(user.Telegram, Translate(ctx, "donationProgressMessage")) // get invoice - resp, err := http.Get(fmt.Sprintf(donationEndpoint, amount, GetUserStr(m.Sender), GetUserStr(bot.Telegram.Me))) + resp, err := http.Get(fmt.Sprintf(donationEndpoint, amount, GetUserStr(user.Telegram), GetUserStr(bot.Telegram.Me))) if err != nil { log.Errorln(err) bot.tryEditMessage(msg, Translate(ctx, "donationErrorMessage")) @@ -65,14 +64,14 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { } // send donation invoice - user := LoadUser(ctx) + // user := LoadUser(ctx) // bot.trySendMessage(user.Telegram, string(body)) _, err = user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: string(body)}, bot.Client) if err != nil { - userStr := GetUserStr(m.Sender) + userStr := GetUserStr(user.Telegram) errmsg := fmt.Sprintf("[/donate] Donation failed for user %s: %s", userStr, err) log.Errorln(errmsg) - bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "donationFailedMessage"), err)) + bot.tryEditMessage(msg, Translate(ctx, "donationErrorMessage")) return } bot.tryEditMessage(msg, Translate(ctx, "donationSuccess")) diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 285960eb..4621e46b 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -26,29 +26,21 @@ func helpInvoiceUsage(ctx context.Context, errormsg string) string { func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { // check and print all commands bot.anyTextHandler(ctx, m) - if m.Chat.Type != tb.ChatPrivate { - // delete message - bot.tryDeleteMessage(m) - return - } - if len(strings.Split(m.Text, " ")) < 2 { - bot.trySendMessage(m.Sender, helpInvoiceUsage(ctx, Translate(ctx, "invoiceEnterAmountMessage"))) - return - } - user := LoadUser(ctx) if user.Wallet == nil { return } - - userStr := GetUserStr(m.Sender) - amount, err := decodeAmountFromCommand(m.Text) - if err != nil { + userStr := GetUserStr(user.Telegram) + if m.Chat.Type != tb.ChatPrivate { + // delete message + bot.tryDeleteMessage(m) return } - if amount > 0 { - } else { - bot.trySendMessage(m.Sender, helpInvoiceUsage(ctx, Translate(ctx, "invoiceValidAmountMessage"))) + // if no amount is in the command, ask for it + amount, err := decodeAmountFromCommand(m.Text) + if (err != nil || amount < 1) && m.Chat.Type == tb.ChatPrivate { + // // no amount was entered, set user state and ask fo""r amount + bot.askForAmount(ctx, "", "CreateInvoiceState", 0, 0, m.Text) return } diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go new file mode 100644 index 00000000..ea1a65f7 --- /dev/null +++ b/internal/telegram/lnurl-pay.go @@ -0,0 +1,226 @@ +package telegram + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/url" + "strconv" + "strings" + + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + lnurl "github.com/fiatjaf/go-lnurl" + log "github.com/sirupsen/logrus" + tb "gopkg.in/tucnak/telebot.v2" +) + +// LnurlPayState saves the state of the user for an LNURL payment +type LnurlPayState struct { + *transaction.Base + From *lnbits.User `json:"from"` + LNURLPayResponse1 lnurl.LNURLPayResponse1 `json:"LNURLPayResponse1"` + LNURLPayResponse2 lnurl.LNURLPayResponse2 `json:"LNURLPayResponse2"` + Amount int `json:"amount"` + Comment string `json:"comment"` + LanguageCode string `json:"languagecode"` +} + +// lnurlPayHandler1 is invoked when the first lnurl response was a lnurlpay response +// at this point, the user hans't necessarily entered an amount yet +func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams LnurlPayState) { + user := LoadUser(ctx) + if user.Wallet == nil { + return + } + // object that holds all information about the send payment + id := fmt.Sprintf("lnurlp-%d-%s", m.Sender.ID, RandStringRunes(5)) + lnurlPayState := LnurlPayState{ + Base: transaction.New(transaction.ID(id)), + LNURLPayResponse1: payParams.LNURLPayResponse1, + LanguageCode: ctx.Value("publicLanguageCode").(string), + } + // add result to persistent struct + runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) + + // if no amount is in the command, ask for it + amount, err := decodeAmountFromCommand(m.Text) + if err != nil || amount < 1 { + // // no amount was entered, set user state and ask for amount + bot.askForAmount(ctx, id, "LnurlPayState", lnurlPayState.LNURLPayResponse1.MinSendable, lnurlPayState.LNURLPayResponse1.MaxSendable, m.Text) + return + } + + // amount is already present in the command, i.e., /lnurl + // amount not in allowed range from LNURL + if int64(amount) > (lnurlPayState.LNURLPayResponse1.MaxSendable/1000) || int64(amount) < (lnurlPayState.LNURLPayResponse1.MinSendable/1000) { + err = fmt.Errorf("amount not in range") + log.Warnf("[lnurlPayHandler] Error: %s", err.Error()) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), lnurlPayState.LNURLPayResponse1.MinSendable/1000, lnurlPayState.LNURLPayResponse1.MaxSendable/1000)) + ResetUserState(user, bot) + return + } + // set also amount in the state of the user + lnurlPayState.Amount = amount * 1000 // save as mSat + + // check if comment is presentin lnrul-p + memo := GetMemoFromCommand(m.Text, 3) + // shorten comment to allowed length + if len(memo) > int(lnurlPayState.LNURLPayResponse1.CommentAllowed) { + memo = memo[:lnurlPayState.LNURLPayResponse1.CommentAllowed] + } + if len(memo) > 0 { + lnurlPayState.Comment = memo + } + + // add result to persistent struct + runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) + + // not necessary to save this in the state data, but till doing it + paramsJson, err := json.Marshal(lnurlPayState) + if err != nil { + log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) + // bot.trySendMessage(m.Sender, err.Error()) + return + } + SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(paramsJson)) + // directly go to confirm + bot.lnurlPayHandlerSend(ctx, m) + return +} + +// lnurlPayHandler is invoked when the user has delivered an amount and is ready to pay +func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + if user.Wallet == nil { + return + } + statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlGettingUserMessage")) + + // assert that user has entered an amount + if user.StateKey != lnbits.UserHasEnteredAmount { + log.Errorln("[lnurlPayHandler] state keys don't match") + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return + } + + // read the enter amount state from user.StateData + var enterAmountData EnterAmountStateData + err := json.Unmarshal([]byte(user.StateData), &enterAmountData) + if err != nil { + log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return + } + + // use the enter amount state of the user to load the LNURL payment state + tx := &LnurlPayState{Base: transaction.New(transaction.ID(enterAmountData.ID))} + fn, err := tx.Get(tx, bot.Bunt) + if err != nil { + log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return + } + lnurlPayState := fn.(*LnurlPayState) + + // LnurlPayState loaded + + client, err := bot.GetHttpClient() + if err != nil { + log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) + // bot.trySendMessage(c.Sender, err.Error()) + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return + } + callbackUrl, err := url.Parse(lnurlPayState.LNURLPayResponse1.Callback) + if err != nil { + log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) + // bot.trySendMessage(c.Sender, err.Error()) + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return + } + qs := callbackUrl.Query() + // add amount to query string + qs.Set("amount", strconv.Itoa(lnurlPayState.Amount)) // msat + // add comment to query string + if len(lnurlPayState.Comment) > 0 { + qs.Set("comment", lnurlPayState.Comment) + } + + callbackUrl.RawQuery = qs.Encode() + + res, err := client.Get(callbackUrl.String()) + if err != nil { + log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) + // bot.trySendMessage(c.Sender, err.Error()) + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) + // bot.trySendMessage(c.Sender, err.Error()) + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return + } + + var response2 lnurl.LNURLPayResponse2 + json.Unmarshal(body, &response2) + if response2.Status == "ERROR" || len(response2.PR) < 1 { + error_reason := "Could not receive invoice." + if len(response2.Reason) > 0 { + error_reason = response2.Reason + } + log.Errorf("[lnurlPayHandler] Error in LNURLPayResponse2: %s", error_reason) + bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), error_reason)) + return + } + + lnurlPayState.LNURLPayResponse2 = response2 + // add result to persistent struct + runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) + bot.Telegram.Delete(statusMsg) + m.Text = fmt.Sprintf("/pay %s", response2.PR) + bot.payHandler(ctx, m) +} + +func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, address string, amount int) error { + split := strings.Split(address, "@") + if len(split) != 2 { + return fmt.Errorf("lightning address format wrong") + } + host := strings.ToLower(split[1]) + name := strings.ToLower(split[0]) + + // convert address scheme into LNURL Bech32 format + callback := fmt.Sprintf("https://%s/.well-known/lnurlp/%s", host, name) + + log.Infof("[sendToLightningAddress] %s: callback: %s", GetUserStr(m.Sender), callback) + + lnurl, err := lnurl.LNURLEncode(callback) + if err != nil { + return err + } + + if amount > 0 { + // only when amount is given, we will also add a comment to the command + // we do this because if the amount is not given, we will have to ask for it later + // in the lnurl handler and we don't want to add another step where we ask for a comment + // the command to pay to lnurl with comment is /lnurl + // check if comment is presentin lnrul-p + memo := GetMemoFromCommand(m.Text, 3) + m.Text = fmt.Sprintf("/lnurl %d %s", amount, lnurl) + // shorten comment to allowed length + if len(memo) > 0 { + m.Text = m.Text + " " + memo + } + } else { + // no amount was given so we will just send the lnurl + // this will invoke the "enter amount" dialog in the lnurl handler + m.Text = fmt.Sprintf("/lnurl %s", lnurl) + } + bot.lnurlHandler(ctx, m) + return nil +} diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index b1450376..c7f1a47c 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -3,13 +3,11 @@ package telegram import ( "bytes" "context" - "encoding/json" "errors" "fmt" "io/ioutil" "net/http" "net/url" - "strconv" "strings" "github.com/LightningTipBot/LightningTipBot/internal" @@ -22,13 +20,35 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) +func (bot *TipBot) GetHttpClient() (*http.Client, error) { + client := http.Client{} + if internal.Configuration.Bot.HttpProxy != "" { + proxyUrl, err := url.Parse(internal.Configuration.Bot.HttpProxy) + if err != nil { + log.Errorln(err) + return nil, err + } + client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)} + } + return &client, nil +} +func (bot TipBot) cancelLnUrlHandler(c *tb.Callback) { +} + // lnurlHandler is invoked on /lnurl command func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { // commands: // /lnurl // /lnurl // or /lnurl + if m.Chat.Type != tb.ChatPrivate { + return + } log.Infof("[lnurlHandler] %s", m.Text) + user := LoadUser(ctx) + if user.Wallet == nil { + return + } // if only /lnurl is entered, show the lnurl of the user if m.Text == "/lnurl" { @@ -38,80 +58,27 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { // assume payment // HandleLNURL by fiatjaf/go-lnurl - msg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlResolvingUrlMessage")) - _, params, err := HandleLNURL(m.Text) + statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlResolvingUrlMessage")) + _, params, err := bot.HandleLNURL(m.Text) if err != nil { - bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not resolve LNURL.")) + bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not resolve LNURL.")) log.Errorln(err) return } - var payParams LnurlStateResponse switch params.(type) { case lnurl.LNURLPayResponse1: - payParams = LnurlStateResponse{LNURLPayResponse1: params.(lnurl.LNURLPayResponse1)} - log.Infof("[lnurlHandler] %s", payParams.Callback) + payParams := LnurlPayState{LNURLPayResponse1: params.(lnurl.LNURLPayResponse1)} + log.Infof("[lnurlHandler] %s", payParams.LNURLPayResponse1.Callback) + bot.tryDeleteMessage(statusMsg) + bot.lnurlPayHandler(ctx, m, payParams) + return default: err := fmt.Errorf("invalid LNURL type.") log.Errorln(err) - bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) + bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) // bot.trySendMessage(m.Sender, err.Error()) return } - user := LoadUser(ctx) - if user.Wallet == nil { - return - } - - // if no amount is in the command, ask for it - amount, err := decodeAmountFromCommand(m.Text) - if err != nil || amount < 1 { - // set LNURLPayResponse1 in the state of the user - paramsJson, err := json.Marshal(payParams) - if err != nil { - log.Errorln(err) - return - } - - SetUserState(user, bot, lnbits.UserStateLNURLEnterAmount, string(paramsJson)) - - bot.tryDeleteMessage(msg) - // Let the user enter an amount and return - bot.trySendMessage(m.Sender, - fmt.Sprintf(Translate(ctx, "lnurlEnterAmountMessage"), payParams.MinSendable/1000, payParams.MaxSendable/1000), - tb.ForceReply) - } else { - // amount is already present in the command - // amount not in allowed range from LNURL - // if int64(amount) > (payParams.MaxSendable/1000) || int64(amount) < (payParams.MinSendable/1000) { - // err = fmt.Errorf("amount not in range") - // log.Errorln(err) - // bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), payParams.MinSendable/1000, payParams.MaxSendable/1000)) - // ResetUserState(user, bot) - // return - // } - // set also amount in the state of the user - payParams.Amount = amount - - // check if comment is presentin lnrul-p - memo := GetMemoFromCommand(m.Text, 3) - // shorten comment to allowed length - if len(memo) > int(payParams.CommentAllowed) { - memo = memo[:payParams.CommentAllowed] - } - // save it - payParams.Comment = memo - - paramsJson, err := json.Marshal(payParams) - if err != nil { - log.Errorln(err) - // bot.trySendMessage(m.Sender, err.Error()) - return - } - SetUserState(user, bot, lnbits.UserStateConfirmLNURLPay, string(paramsJson)) - bot.tryDeleteMessage(msg) - // directly go to confirm - bot.lnurlPayHandler(ctx, m) - } } func (bot *TipBot) UserGetLightningAddress(user *lnbits.User) (string, error) { @@ -151,14 +118,14 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { fromUser := LoadUser(ctx) lnurlEncode, err := UserGetLNURL(fromUser) if err != nil { - errmsg := fmt.Sprintf("[lnurlReceiveHandler] Failed to get LNURL: %s", err) + errmsg := fmt.Sprintf("[userLnurlHandler] Failed to get LNURL: %s", err) log.Errorln(errmsg) bot.Telegram.Send(m.Sender, Translate(ctx, "lnurlNoUsernameMessage")) } // create qr code qr, err := qrcode.Encode(lnurlEncode, qrcode.Medium, 256) if err != nil { - errmsg := fmt.Sprintf("[lnurlReceiveHandler] Failed to create QR code for LNURL: %s", err) + errmsg := fmt.Sprintf("[userLnurlHandler] Failed to create QR code for LNURL: %s", err) log.Errorln(errmsg) return } @@ -168,147 +135,131 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lnurlEncode)}) } -// lnurlEnterAmountHandler is invoked if the user didn't deliver an amount for the lnurl payment -func (bot *TipBot) lnurlEnterAmountHandler(ctx context.Context, m *tb.Message) { - user := LoadUser(ctx) - if user.Wallet == nil { - return - } - - if user.StateKey == lnbits.UserStateLNURLEnterAmount { - a, err := strconv.Atoi(m.Text) - if err != nil { - log.Errorln(err) - bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) - ResetUserState(user, bot) - return - } - amount := int64(a) - var stateResponse LnurlStateResponse - err = json.Unmarshal([]byte(user.StateData), &stateResponse) - if err != nil { - log.Errorln(err) - ResetUserState(user, bot) - return - } - // amount not in allowed range from LNURL - if amount > (stateResponse.MaxSendable/1000) || amount < (stateResponse.MinSendable/1000) { - err = fmt.Errorf("amount not in range") - log.Errorln(err) - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), stateResponse.MinSendable/1000, stateResponse.MaxSendable/1000)) - ResetUserState(user, bot) - return - } - stateResponse.Amount = a - state, err := json.Marshal(stateResponse) - if err != nil { - log.Errorln(err) - ResetUserState(user, bot) - return - } - SetUserState(user, bot, lnbits.UserStateConfirmLNURLPay, string(state)) - bot.lnurlPayHandler(ctx, m) - } -} - -// LnurlStateResponse saves the state of the user for an LNURL payment -type LnurlStateResponse struct { - lnurl.LNURLPayResponse1 - Amount int `json:"amount"` - Comment string `json:"comment"` -} - -// lnurlPayHandler is invoked when the user has delivered an amount and is ready to pay -func (bot *TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { - msg := bot.trySendMessage(c.Sender, Translate(ctx, "lnurlGettingUserMessage")) - - user := LoadUser(ctx) - if user.Wallet == nil { - return - } - - if user.StateKey == lnbits.UserStateConfirmLNURLPay { - client, err := getHttpClient() - if err != nil { - log.Errorln(err) - // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) - return - } - var stateResponse LnurlStateResponse - err = json.Unmarshal([]byte(user.StateData), &stateResponse) - if err != nil { - log.Errorln(err) - // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) - return - } - callbackUrl, err := url.Parse(stateResponse.Callback) - if err != nil { - log.Errorln(err) - // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) - return - } - qs := callbackUrl.Query() - // add amount to query string - qs.Set("amount", strconv.Itoa(stateResponse.Amount*1000)) - // add comment to query string - if len(stateResponse.Comment) > 0 { - qs.Set("comment", stateResponse.Comment) - } - - callbackUrl.RawQuery = qs.Encode() - - res, err := client.Get(callbackUrl.String()) - if err != nil { - log.Errorln(err) - // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) - return - } - var response2 lnurl.LNURLPayResponse2 - body, err := ioutil.ReadAll(res.Body) - if err != nil { - log.Errorln(err) - // bot.trySendMessage(c.Sender, err.Error()) - bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) - return - } - json.Unmarshal(body, &response2) - - if len(response2.PR) < 1 { - error_reason := "Could not receive invoice." - if len(response2.Reason) > 0 { - error_reason = response2.Reason - } - bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), error_reason)) - return - } - bot.Telegram.Delete(msg) - c.Text = fmt.Sprintf("/pay %s", response2.PR) - bot.payHandler(ctx, c) - } -} - -func getHttpClient() (*http.Client, error) { - client := http.Client{} - if internal.Configuration.Bot.HttpProxy != "" { - proxyUrl, err := url.Parse(internal.Configuration.Bot.HttpProxy) - if err != nil { - log.Errorln(err) - return nil, err - } - client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)} - } - return &client, nil -} -func (bot TipBot) cancelLnUrlHandler(c *tb.Callback) { - -} +// // lnurlEnterAmountHandler is invoked if the user didn't deliver an amount for the lnurl payment +// func (bot *TipBot) lnurlEnterAmountHandler(ctx context.Context, m *tb.Message) { +// user := LoadUser(ctx) +// if user.Wallet == nil { +// return +// } + +// if user.StateKey == lnbits.UserStateLNURLEnterAmount || user.StateKey == lnbits.UserEnterAmount { +// a, err := strconv.Atoi(m.Text) +// if err != nil { +// log.Errorln(err) +// bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) +// ResetUserState(user, bot) +// return +// } +// amount := int64(a) +// var stateResponse LnurlPayState +// err = json.Unmarshal([]byte(user.StateData), &stateResponse) +// if err != nil { +// log.Errorln(err) +// ResetUserState(user, bot) +// return +// } +// // amount not in allowed range from LNURL +// if amount > (stateResponse.LNURLPayResponse1.MaxSendable/1000) || amount < (stateResponse.LNURLPayResponse1.MinSendable/1000) { +// err = fmt.Errorf("amount not in range") +// log.Errorln(err) +// bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), stateResponse.LNURLPayResponse1.MinSendable/1000, stateResponse.LNURLPayResponse1.MaxSendable/1000)) +// ResetUserState(user, bot) +// return +// } +// stateResponse.Amount = a +// state, err := json.Marshal(stateResponse) +// if err != nil { +// log.Errorln(err) +// ResetUserState(user, bot) +// return +// } +// SetUserState(user, bot, lnbits.UserStateConfirmLNURLPay, string(state)) +// bot.lnurlPayHandler(ctx, m) +// } +// } + +// // LnurlStateResponse saves the state of the user for an LNURL payment +// type LnurlStateResponse struct { +// lnurl.LNURLPayResponse1 +// Amount int `json:"amount"` +// Comment string `json:"comment"` +// } + +// // lnurlPayHandler is invoked when the user has delivered an amount and is ready to pay +// func (bot *TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { +// msg := bot.trySendMessage(c.Sender, Translate(ctx, "lnurlGettingUserMessage")) + +// user := LoadUser(ctx) +// if user.Wallet == nil { +// return +// } + +// if user.StateKey == lnbits.UserStateConfirmLNURLPay { +// client, err := getHttpClient() +// if err != nil { +// log.Errorln(err) +// // bot.trySendMessage(c.Sender, err.Error()) +// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) +// return +// } +// var stateResponse LnurlStateResponse +// err = json.Unmarshal([]byte(user.StateData), &stateResponse) +// if err != nil { +// log.Errorln(err) +// // bot.trySendMessage(c.Sender, err.Error()) +// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) +// return +// } +// callbackUrl, err := url.Parse(stateResponse.Callback) +// if err != nil { +// log.Errorln(err) +// // bot.trySendMessage(c.Sender, err.Error()) +// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) +// return +// } +// qs := callbackUrl.Query() +// // add amount to query string +// qs.Set("amount", strconv.Itoa(stateResponse.Amount*1000)) +// // add comment to query string +// if len(stateResponse.Comment) > 0 { +// qs.Set("comment", stateResponse.Comment) +// } + +// callbackUrl.RawQuery = qs.Encode() + +// res, err := client.Get(callbackUrl.String()) +// if err != nil { +// log.Errorln(err) +// // bot.trySendMessage(c.Sender, err.Error()) +// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) +// return +// } +// var response2 lnurl.LNURLPayResponse2 +// body, err := ioutil.ReadAll(res.Body) +// if err != nil { +// log.Errorln(err) +// // bot.trySendMessage(c.Sender, err.Error()) +// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) +// return +// } +// json.Unmarshal(body, &response2) + +// if len(response2.PR) < 1 { +// error_reason := "Could not receive invoice." +// if len(response2.Reason) > 0 { +// error_reason = response2.Reason +// } +// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), error_reason)) +// return +// } +// bot.Telegram.Delete(msg) +// c.Text = fmt.Sprintf("/pay %s", response2.PR) +// bot.payHandler(ctx, c) +// } +// } // from https://github.com/fiatjaf/go-lnurl -func HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error) { +func (bot *TipBot) HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error) { var err error var rawurl string @@ -339,22 +290,21 @@ func HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error) { return rawurl, nil, err } - query := parsed.Query() + // query := parsed.Query() - switch query.Get("tag") { - case "login": - value, err := lnurl.HandleAuth(rawurl, parsed, query) - return rawurl, value, err - case "withdrawRequest": - if value, ok := lnurl.HandleFastWithdraw(query); ok { - return rawurl, value, nil - } - } - client, err := getHttpClient() + // switch query.Get("tag") { + // case "login": + // value, err := lnurl.HandleAuth(rawurl, parsed, query) + // return rawurl, value, err + // case "withdrawRequest": + // if value, ok := lnurl.HandleFastWithdraw(query); ok { + // return rawurl, value, nil + // } + // } + client, err := bot.GetHttpClient() if err != nil { return "", nil, err } - resp, err := client.Get(rawurl) if err != nil { return rawurl, nil, err @@ -375,55 +325,16 @@ func HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error) { } switch j.Get("tag").String() { - case "withdrawRequest": - value, err := lnurl.HandleWithdraw(j) - return rawurl, value, err + // case "withdrawRequest": + // value, err := lnurl.HandleWithdraw(j) + // return rawurl, value, err case "payRequest": value, err := lnurl.HandlePay(j) return rawurl, value, err - case "channelRequest": - value, err := lnurl.HandleChannel(j) - return rawurl, value, err + // case "channelRequest": + // value, err := lnurl.HandleChannel(j) + // return rawurl, value, err default: return rawurl, nil, errors.New("unknown response tag " + j.String()) } } - -func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, address string, amount int) error { - split := strings.Split(address, "@") - if len(split) != 2 { - return fmt.Errorf("lightning address format wrong") - } - host := strings.ToLower(split[1]) - name := strings.ToLower(split[0]) - - // convert address scheme into LNURL Bech32 format - callback := fmt.Sprintf("https://%s/.well-known/lnurlp/%s", host, name) - - log.Infof("[sendToLightningAddress] %s: callback: %s", GetUserStr(m.Sender), callback) - - lnurl, err := lnurl.LNURLEncode(callback) - if err != nil { - return err - } - - if amount > 0 { - // only when amount is given, we will also add a comment to the command - // we do this because if the amount is not given, we will have to ask for it later - // in the lnurl handler and we don't want to add another step where we ask for a comment - // the command to pay to lnurl with comment is /lnurl - // check if comment is presentin lnrul-p - memo := GetMemoFromCommand(m.Text, 3) - m.Text = fmt.Sprintf("/lnurl %d %s", amount, lnurl) - // shorten comment to allowed length - if len(memo) > 0 { - m.Text = m.Text + " " + memo - } - } else { - // no amount was given so we will just send the lnurl - // this will invoke the "enter amount" dialog in the lnurl handler - m.Text = fmt.Sprintf("/lnurl %s", lnurl) - } - bot.lnurlHandler(ctx, m) - return nil -} diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index e67eaf55..48e6b08d 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -31,14 +31,15 @@ func helpPayInvoiceUsage(ctx context.Context, errormsg string) string { type PayData struct { *transaction.Base - From *lnbits.User `json:"from"` - Invoice string `json:"invoice"` - Hash string `json:"hash"` - Proof string `json:"proof"` - Memo string `json:"memo"` - Message string `json:"message"` - Amount int64 `json:"amount"` - LanguageCode string `json:"languagecode"` + From *lnbits.User `json:"from"` + Invoice string `json:"invoice"` + Hash string `json:"hash"` + Proof string `json:"proof"` + Memo string `json:"memo"` + Message string `json:"message"` + Amount int64 `json:"amount"` + LanguageCode string `json:"languagecode"` + TelegramMessage *tb.Message `json:"telegrammessage"` } // payHandler invoked on "/pay lnbc..." command @@ -84,19 +85,23 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { return } + statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlGettingUserMessage")) // check user balance first balance, err := bot.GetUserBalance(user) if err != nil { NewMessage(m, WithDuration(0, bot)) errmsg := fmt.Sprintf("[/pay] Error: Could not get user balance: %s", err) log.Errorln(errmsg) + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } + if amount > balance { NewMessage(m, WithDuration(0, bot)) - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "insufficientFundsMessage"), balance, amount)) + bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "insufficientFundsMessage"), balance, amount)) return } + bot.tryDeleteMessage(statusMsg) // send warning that the invoice might fail due to missing fee reserve if float64(amount) > float64(balance)*0.99 { bot.trySendMessage(m.Sender, Translate(ctx, "feeReserveMessage")) @@ -111,19 +116,6 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { // object that holds all information about the send payment id := fmt.Sprintf("pay-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) - payData := PayData{ - From: user, - Invoice: paymentRequest, - Base: transaction.New(transaction.ID(id)), - Amount: int64(amount), - Memo: bolt11.Description, - Message: confirmText, - LanguageCode: ctx.Value("publicLanguageCode").(string), - } - // add result to persistent struct - runtime.IgnoreError(payData.Set(payData, bot.Bunt)) - - SetUserState(user, bot, lnbits.UserStateConfirmPayment, paymentRequest) // // // create inline buttons payButton := paymentConfirmationMenu.Data(Translate(ctx, "payButtonMessage"), "confirm_pay") @@ -136,7 +128,21 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { payButton, cancelButton), ) - bot.trySendMessage(m.Chat, confirmText, paymentConfirmationMenu) + payMessage := bot.trySendMessage(m.Chat, confirmText, paymentConfirmationMenu) + payData := PayData{ + Base: transaction.New(transaction.ID(id)), + From: user, + Invoice: paymentRequest, + Amount: int64(amount), + Memo: bolt11.Description, + Message: confirmText, + LanguageCode: ctx.Value("publicLanguageCode").(string), + TelegramMessage: payMessage, + } + // add result to persistent struct + runtime.IgnoreError(payData.Set(payData, bot.Bunt)) + + SetUserState(user, bot, lnbits.UserStateConfirmPayment, paymentRequest) } // confirmPayHandler when user clicked pay on payment confirmation @@ -185,15 +191,28 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { ResetUserState(user, bot) userStr := GetUserStr(c.Sender) + + // update button text + bot.tryEditMessage( + c.Message, + payData.Message, + &tb.ReplyMarkup{ + InlineKeyboard: [][]tb.InlineButton{ + {tb.InlineButton{Text: i18n.Translate(payData.LanguageCode, "lnurlGettingUserMessage")}}, + }, + }, + ) // pay invoice invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoiceString}, bot.Client) if err != nil { errmsg := fmt.Sprintf("[/pay] Could not pay invoice of %s: %s", userStr, err) - if len(err.Error()) == 0 { - err = fmt.Errorf(i18n.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) - } - // bot.trySendMessage(c.Sender, fmt.Sprintf(invoicePaymentFailedMessage, err)) - bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), str.MarkdownEscape(err.Error())), &tb.ReplyMarkup{}) + err = fmt.Errorf(i18n.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) + bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), err.Error()), &tb.ReplyMarkup{}) + // verbose error message, turned off for now + // if len(err.Error()) == 0 { + // err = fmt.Errorf(i18n.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) + // } + // bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), str.MarkdownEscape(err.Error())), &tb.ReplyMarkup{}) log.Errorln(errmsg) return } diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 9657e3fe..671a885d 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -63,7 +63,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // check and print all commands // If the send is a reply, then trigger /tip handler - if m.IsReply() { + if m.IsReply() && m.Chat.Type != tb.ChatPrivate { bot.tipHandler(ctx, m) return } @@ -100,6 +100,11 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // todo: this error might have been overwritten by the functions above // we should only check for a valid amount here, instead of error and amount + amount, err = decodeAmountFromCommand(m.Text) + if (err != nil || amount < 1) && m.Chat.Type == tb.ChatPrivate { + bot.askForAmount(ctx, "", "CreateSendState", 0, 0, m.Text) + return + } // ASSUME INTERNAL SEND TO TELEGRAM USER if err != nil || amount < 1 { @@ -118,7 +123,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { toUserStrMention := "" toUserStrWithoutAt := "" - // check for user in command, accepts user mention or plan username without @ + // check for user in command, accepts user mention or plain username without @ if len(m.Entities) > 1 && m.Entities[1].Type == "mention" { toUserStrMention = m.Text[m.Entities[1].Offset : m.Entities[1].Offset+m.Entities[1].Length] toUserStrWithoutAt = strings.TrimPrefix(toUserStrMention, "@") diff --git a/internal/telegram/text.go b/internal/telegram/text.go index bae6c497..6a4958bd 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -36,8 +36,8 @@ func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { // could be a LNURL // var lnurlregex = regexp.MustCompile(`.*?((lnurl)([0-9]{1,}[a-z0-9]+){1})`) - if user.StateKey == lnbits.UserStateLNURLEnterAmount { - bot.lnurlEnterAmountHandler(ctx, m) + if user.StateKey == lnbits.UserStateLNURLEnterAmount || user.StateKey == lnbits.UserEnterAmount { + bot.enterAmountHandler(ctx, m) } } diff --git a/translations/de.toml b/translations/de.toml index d91d8ceb..92255e0c 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -226,7 +226,7 @@ lnurlPaymentFailed = """🚫 Zahlung fehlgeschlagen: %s""" lnurlInvalidAmountMessage = """🚫 Ungültiger Betrag.""" lnurlInvalidAmountRangeMessage = """🚫 Betrag muss zwischen %d und %d sat liegen.""" lnurlNoUsernameMessage = """🚫 Du musst einen Telegram Username anlegen, um per LNURL Zahlungen zu empfangen.""" -lnurlEnterAmountMessage = """⌨️ Gebe einen Betrag zwischen %d und %d sat ein.""" +lnurlEnterAmountRangeMessage = """⌨️ Gebe einen Betrag zwischen %d und %d sat ein.""" lnurlHelpText = """📖 Ups, das hat nicht geklappt. %s *Befehl:* `/lnurl [betrag] ` diff --git a/translations/en.toml b/translations/en.toml index fb2f693e..e8a83dba 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -229,7 +229,8 @@ lnurlPaymentFailed = """🚫 Payment failed: %s""" lnurlInvalidAmountMessage = """🚫 Invalid amount.""" lnurlInvalidAmountRangeMessage = """🚫 Amount must be between %d and %d sat.""" lnurlNoUsernameMessage = """🚫 You need to set a Telegram username to receive payments via LNURL.""" -lnurlEnterAmountMessage = """⌨️ Enter an amount between %d and %d sat.""" +lnurlEnterAmountRangeMessage = """⌨️ Enter an amount between %d and %d sat.""" +lnurlEnterAmountMessage = """⌨️ Enter an amount.""" lnurlHelpText = """📖 Oops, that didn't work. %s *Usage:* `/lnurl [amount] ` diff --git a/translations/es.toml b/translations/es.toml index b70b4a0f..6f8f23f3 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -226,7 +226,7 @@ lnurlPaymentFailed = """🚫 Pago fallido: %s""" lnurlInvalidAmountMessage = """🚫 Monto inválido.""" lnurlInvalidAmountRangeMessage = """🚫 El monto debe estar entre %d y %d sat.""" lnurlNoUsernameMessage = """🚫 Tienes que establecer un nombre de usuario de Telegram para recibir pagos a través de LNURL.""" -lnurlEnterAmountMessage = """⌨️ Introduce un monto entre %d y %d sat.""" +lnurlEnterAmountRangeMessage = """⌨️ Introduce un monto entre %d y %d sat.""" lnurlHelpText = """📖 Oops, eso no funcionó. %s *Uso:* `/lnurl [monto] ` diff --git a/translations/fr.toml b/translations/fr.toml index 18e5465c..0eda81f9 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -226,7 +226,7 @@ lnurlPaymentFailed = """🚫 Echec du paiement : %s""" lnurlInvalidAmountMessage = """🚫 Montant incorrect.""" lnurlInvalidAmountRangeMessage = """🚫 Le montant doit être entre %d et %d sat.""" lnurlNoUsernameMessage = """🚫 Vous devez avoir un nom d'utilisateur Telegram pour recevoir un paiement via LNURL.""" -lnurlEnterAmountMessage = """⌨️ Choisissez un montant entre %d et %d sat.""" +lnurlEnterAmountRangeMessage = """⌨️ Choisissez un montant entre %d et %d sat.""" lnurlHelpText = """📖 Oops, cela n'a pas fonctionné. %s *Usage:* `/lnurl [montant] ` diff --git a/translations/id.toml b/translations/id.toml index d0d96b07..34a49a16 100644 --- a/translations/id.toml +++ b/translations/id.toml @@ -226,7 +226,7 @@ lnurlPaymentFailed = """🚫 Pembayaran gagal: %s""" lnurlInvalidAmountMessage = """🚫 Jumlah tidak benar.""" lnurlInvalidAmountRangeMessage = """🚫 Jumlah harus diantara %d dan %d sat.""" lnurlNoUsernameMessage = """🚫 Kamu harus mengatur nama pengguna Telegram untuk menerima pembayaran melalui LNURL.""" -lnurlEnterAmountMessage = """⌨️ Masukkan jumlah diantara %d dan %d sat.""" +lnurlEnterAmountRangeMessage = """⌨️ Masukkan jumlah diantara %d dan %d sat.""" lnurlHelpText = """📖 Waduh, itu tidak berhasil. %s *Usage:* `/lnurl [jumlah] ` diff --git a/translations/it.toml b/translations/it.toml index c39e7aa6..e768cf98 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -226,7 +226,7 @@ lnurlPaymentFailed = """🚫 Pagamento non riuscito: %s""" lnurlInvalidAmountMessage = """🚫 Ammontare non valido.""" lnurlInvalidAmountRangeMessage = """🚫 L'ammontare deve essere compreso tra %d e %d sat.""" lnurlNoUsernameMessage = """🚫 Devi impostare un nome utente Telegram per ricevere pagamenti tramite un LNURL.""" -lnurlEnterAmountMessage = """⌨️ Imposta un ammontare tra %d e %d sat.""" +lnurlEnterAmountRangeMessage = """⌨️ Imposta un ammontare tra %d e %d sat.""" lnurlHelpText = """📖 Ops, non ha funzionato. %s *Sintassi:* `/lnurl [ammontare] ` diff --git a/translations/nl.toml b/translations/nl.toml index d995ac90..aa6596b4 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -226,7 +226,7 @@ lnurlPaymentFailed = """🚫 Betaling mislukt: %s""" lnurlInvalidAmountMessage = """🚫 Ongeldig bedrag.""" lnurlInvalidAmountRangeMessage = """🚫 Bedrag moet liggen tussen %d en %d sat.""" lnurlNoUsernameMessage = """🚫 U moet een Telegram gebruikersnaam instellen om betalingen te ontvangen via LNURL.""" -lnurlEnterAmountMessage = """⌨️ Voer een bedrag in tussen %d en %d sat.""" +lnurlEnterAmountRangeMessage = """⌨️ Voer een bedrag in tussen %d en %d sat.""" lnurlHelpText = """📖 Oeps, dat werkte niet. %s *Usage:* `/lnurl [bedrag] ` diff --git a/translations/pt-br.toml b/translations/pt-br.toml index 8bac0c8e..e15e19b6 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -226,7 +226,7 @@ lnurlPaymentFailed = """🚫 Falha no pagamento: %s""" lnurlInvalidAmountMessage = """🚫 Quantia inválida.""" lnurlInvalidAmountRangeMessage = """🚫 A quantia deve estar entre %d e %d sat.""" lnurlNoUsernameMessage = """🚫 Você precisa configurar um nome de usuário de Telegram para receber pagamentos através do LNURL.""" -lnurlEnterAmountMessage = """⌨️ Insira uma quantia entre %d e %d sat.""" +lnurlEnterAmountRangeMessage = """⌨️ Insira uma quantia entre %d e %d sat.""" lnurlHelpText = """📖 Opa, isso não funcionou. %s *Uso:* `/lnurl [quantidade] ` diff --git a/translations/tr.toml b/translations/tr.toml index adbc4d86..98e68ef7 100644 --- a/translations/tr.toml +++ b/translations/tr.toml @@ -226,7 +226,7 @@ lnurlPaymentFailed = """🚫 Ödeme başarısız: %s""" lnurlInvalidAmountMessage = """🚫 Geçersiz miktar.""" lnurlInvalidAmountRangeMessage = """🚫 Miktar %d ve %d sat arasında olmalı.""" lnurlNoUsernameMessage = """🚫 LNURL ödemesi almak için bir Telegram kullanıcı ismi seçmelisin.""" -lnurlEnterAmountMessage = """⌨️ %d ve %d sat arasında bir miktar gir.""" +lnurlEnterAmountRangeMessage = """⌨️ %d ve %d sat arasında bir miktar gir.""" lnurlHelpText = """📖 Hoppalaa… olmadı. %s *Komut:* `/lnurl [miktar] ` From d8a98ad433099d1f16e6d54a9df65429f7aea381 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 1 Nov 2021 19:03:24 +0100 Subject: [PATCH 040/541] fix (#120) --- internal/price/price.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/price/price.go b/internal/price/price.go index 6da4f2d9..39f4a166 100644 --- a/internal/price/price.go +++ b/internal/price/price.go @@ -3,7 +3,6 @@ package price import ( "fmt" "io/ioutil" - "net" "net/http" "net/url" "strconv" @@ -85,7 +84,7 @@ func (p *PriceWatcher) Watch() error { func (p *PriceWatcher) GetCoinbasePrice(currency string) (float64, error) { coinbaseEndpoint, err := url.Parse(fmt.Sprintf("https://api.coinbase.com/v2/prices/spot?currency=%s", currency)) response, err := p.client.Get(coinbaseEndpoint.String()) - if err, ok := err.(net.Error); ok && err.Timeout() { + if err != nil { return 0, err } bodyBytes, err := ioutil.ReadAll(response.Body) @@ -111,7 +110,7 @@ func (p *PriceWatcher) GetBitfinexPrice(currency string) (float64, error) { pair := bitfinexCurrencyToPair[currency] bitfinexEndpoint, err := url.Parse(fmt.Sprintf("https://api.bitfinex.com/v1/pubticker/%s", pair)) response, err := p.client.Get(bitfinexEndpoint.String()) - if err, ok := err.(net.Error); ok && err.Timeout() { + if err != nil { return 0, err } bodyBytes, err := ioutil.ReadAll(response.Body) From 5725a0c9c01581ce34dc476223792d95e5421a66 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 3 Nov 2021 20:08:56 +0100 Subject: [PATCH 041/541] do not return (#123) * do not return * warn not error --- internal/telegram/inline_faucet.go | 3 +-- internal/telegram/inline_receive.go | 3 +-- internal/telegram/inline_send.go | 3 +-- internal/telegram/inline_tipjar.go | 3 +-- internal/telegram/tip.go | 3 +-- 5 files changed, 5 insertions(+), 10 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 254f9279..a3e094f2 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -305,8 +305,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[faucet] Error: Send message to %s: %s", toUserStr, err) - log.Errorln(errmsg) - return + log.Warnln(errmsg) } // build faucet message diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 25fb4c4e..95aaf2a3 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -230,8 +230,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), inlineReceive.Amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[acceptInlineReceiveHandler] Error: Receive message to %s: %s", toUserStr, err) - log.Errorln(errmsg) - return + log.Warnln(errmsg) } } diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index b9fe234e..5844a814 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -243,8 +243,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) _, err = bot.Telegram.Send(fromUser.Telegram, fmt.Sprintf(i18n.Translate(fromUser.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[sendInline] Error: Send message to %s: %s", toUserStr, err) - log.Errorln(errmsg) - return + log.Warnln(errmsg) } } diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index ef40a024..402cd9a2 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -290,8 +290,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineTipjarSentMessage"), inlineTipjar.PerUserAmount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[tipjar] Error: Send message to %s: %s", toUserStr, err) - log.Errorln(errmsg) - return + log.Warnln(errmsg) } // build tipjar message diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index 808a8576..61606e0b 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -127,8 +127,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "tipSentMessage"), amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[/tip] Error: Send message to %s: %s", toUserStr, err) - log.Errorln(errmsg) - return + log.Warnln(errmsg) } // forward tipped message to user once From 4a03724540b844fe8685295c60d3aaaca046a00f Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 9 Nov 2021 17:54:35 +0100 Subject: [PATCH 042/541] fix lnurl parsing and comment saving (#125) --- internal/telegram/lnurl-pay.go | 42 ++++++---- internal/telegram/lnurl.go | 142 ++++----------------------------- 2 files changed, 44 insertions(+), 140 deletions(-) diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index ea1a65f7..d5a8bacb 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -42,12 +42,33 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams LNURLPayResponse1: payParams.LNURLPayResponse1, LanguageCode: ctx.Value("publicLanguageCode").(string), } - // add result to persistent struct + + // first we check whether an amount is present in the command + amount, amount_err := decodeAmountFromCommand(m.Text) + + // we need to figure out whether the memo starts at position 2 or 3 + // so either /lnurl [memo] or /lnurl [memo] + memoStartsAt := 2 + if amount_err == nil { + // amount was present + memoStartsAt = 3 + } + // check if memo is presentin lnrul-p + memo := GetMemoFromCommand(m.Text, memoStartsAt) + // shorten memo to allowed length + if len(memo) > int(lnurlPayState.LNURLPayResponse1.CommentAllowed) { + memo = memo[:lnurlPayState.LNURLPayResponse1.CommentAllowed] + } + if len(memo) > 0 { + lnurlPayState.Comment = memo + } + + // add result to persistent struct, with memo runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) + // now we actualy check whether the amount is already set because we can ask for it if not // if no amount is in the command, ask for it - amount, err := decodeAmountFromCommand(m.Text) - if err != nil || amount < 1 { + if amount_err != nil || amount < 1 { // // no amount was entered, set user state and ask for amount bot.askForAmount(ctx, id, "LnurlPayState", lnurlPayState.LNURLPayResponse1.MinSendable, lnurlPayState.LNURLPayResponse1.MaxSendable, m.Text) return @@ -55,8 +76,9 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams // amount is already present in the command, i.e., /lnurl // amount not in allowed range from LNURL - if int64(amount) > (lnurlPayState.LNURLPayResponse1.MaxSendable/1000) || int64(amount) < (lnurlPayState.LNURLPayResponse1.MinSendable/1000) { - err = fmt.Errorf("amount not in range") + if int64(amount) > (lnurlPayState.LNURLPayResponse1.MaxSendable/1000) || int64(amount) < (lnurlPayState.LNURLPayResponse1.MinSendable/1000) && + (lnurlPayState.LNURLPayResponse1.MaxSendable != 0 && lnurlPayState.LNURLPayResponse1.MinSendable != 0) { // only if max and min are set + err := fmt.Errorf("amount not in range") log.Warnf("[lnurlPayHandler] Error: %s", err.Error()) bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), lnurlPayState.LNURLPayResponse1.MinSendable/1000, lnurlPayState.LNURLPayResponse1.MaxSendable/1000)) ResetUserState(user, bot) @@ -65,16 +87,6 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams // set also amount in the state of the user lnurlPayState.Amount = amount * 1000 // save as mSat - // check if comment is presentin lnrul-p - memo := GetMemoFromCommand(m.Text, 3) - // shorten comment to allowed length - if len(memo) > int(lnurlPayState.LNURLPayResponse1.CommentAllowed) { - memo = memo[:lnurlPayState.LNURLPayResponse1.CommentAllowed] - } - if len(memo) > 0 { - lnurlPayState.Comment = memo - } - // add result to persistent struct runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index c7f1a47c..74ee2003 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -55,11 +55,26 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { bot.lnurlReceiveHandler(ctx, m) return } + statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlResolvingUrlMessage")) + + var lnurlSplit string + split := strings.Split(m.Text, " ") + if _, err := decodeAmountFromCommand(m.Text); err == nil { + // command is /lnurl 123 [memo] + if len(split) > 2 { + lnurlSplit = split[2] + } + } else if len(split) > 1 { + lnurlSplit = split[1] + } else { + bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not resolve LNURL.")) + log.Warnln("[/lnurl] Could not parse command.") + return + } // assume payment // HandleLNURL by fiatjaf/go-lnurl - statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlResolvingUrlMessage")) - _, params, err := bot.HandleLNURL(m.Text) + _, params, err := bot.HandleLNURL(lnurlSplit) if err != nil { bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not resolve LNURL.")) log.Errorln(err) @@ -135,129 +150,6 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lnurlEncode)}) } -// // lnurlEnterAmountHandler is invoked if the user didn't deliver an amount for the lnurl payment -// func (bot *TipBot) lnurlEnterAmountHandler(ctx context.Context, m *tb.Message) { -// user := LoadUser(ctx) -// if user.Wallet == nil { -// return -// } - -// if user.StateKey == lnbits.UserStateLNURLEnterAmount || user.StateKey == lnbits.UserEnterAmount { -// a, err := strconv.Atoi(m.Text) -// if err != nil { -// log.Errorln(err) -// bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) -// ResetUserState(user, bot) -// return -// } -// amount := int64(a) -// var stateResponse LnurlPayState -// err = json.Unmarshal([]byte(user.StateData), &stateResponse) -// if err != nil { -// log.Errorln(err) -// ResetUserState(user, bot) -// return -// } -// // amount not in allowed range from LNURL -// if amount > (stateResponse.LNURLPayResponse1.MaxSendable/1000) || amount < (stateResponse.LNURLPayResponse1.MinSendable/1000) { -// err = fmt.Errorf("amount not in range") -// log.Errorln(err) -// bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), stateResponse.LNURLPayResponse1.MinSendable/1000, stateResponse.LNURLPayResponse1.MaxSendable/1000)) -// ResetUserState(user, bot) -// return -// } -// stateResponse.Amount = a -// state, err := json.Marshal(stateResponse) -// if err != nil { -// log.Errorln(err) -// ResetUserState(user, bot) -// return -// } -// SetUserState(user, bot, lnbits.UserStateConfirmLNURLPay, string(state)) -// bot.lnurlPayHandler(ctx, m) -// } -// } - -// // LnurlStateResponse saves the state of the user for an LNURL payment -// type LnurlStateResponse struct { -// lnurl.LNURLPayResponse1 -// Amount int `json:"amount"` -// Comment string `json:"comment"` -// } - -// // lnurlPayHandler is invoked when the user has delivered an amount and is ready to pay -// func (bot *TipBot) lnurlPayHandler(ctx context.Context, c *tb.Message) { -// msg := bot.trySendMessage(c.Sender, Translate(ctx, "lnurlGettingUserMessage")) - -// user := LoadUser(ctx) -// if user.Wallet == nil { -// return -// } - -// if user.StateKey == lnbits.UserStateConfirmLNURLPay { -// client, err := getHttpClient() -// if err != nil { -// log.Errorln(err) -// // bot.trySendMessage(c.Sender, err.Error()) -// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) -// return -// } -// var stateResponse LnurlStateResponse -// err = json.Unmarshal([]byte(user.StateData), &stateResponse) -// if err != nil { -// log.Errorln(err) -// // bot.trySendMessage(c.Sender, err.Error()) -// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) -// return -// } -// callbackUrl, err := url.Parse(stateResponse.Callback) -// if err != nil { -// log.Errorln(err) -// // bot.trySendMessage(c.Sender, err.Error()) -// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) -// return -// } -// qs := callbackUrl.Query() -// // add amount to query string -// qs.Set("amount", strconv.Itoa(stateResponse.Amount*1000)) -// // add comment to query string -// if len(stateResponse.Comment) > 0 { -// qs.Set("comment", stateResponse.Comment) -// } - -// callbackUrl.RawQuery = qs.Encode() - -// res, err := client.Get(callbackUrl.String()) -// if err != nil { -// log.Errorln(err) -// // bot.trySendMessage(c.Sender, err.Error()) -// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) -// return -// } -// var response2 lnurl.LNURLPayResponse2 -// body, err := ioutil.ReadAll(res.Body) -// if err != nil { -// log.Errorln(err) -// // bot.trySendMessage(c.Sender, err.Error()) -// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) -// return -// } -// json.Unmarshal(body, &response2) - -// if len(response2.PR) < 1 { -// error_reason := "Could not receive invoice." -// if len(response2.Reason) > 0 { -// error_reason = response2.Reason -// } -// bot.tryEditMessage(msg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), error_reason)) -// return -// } -// bot.Telegram.Delete(msg) -// c.Text = fmt.Sprintf("/pay %s", response2.PR) -// bot.payHandler(ctx, c) -// } -// } - // from https://github.com/fiatjaf/go-lnurl func (bot *TipBot) HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error) { var err error From fb73c1fe08b3321cc5e567654b44cab57c8cb011 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 11 Nov 2021 11:56:53 +0100 Subject: [PATCH 043/541] markdown escape usernames (#126) --- internal/telegram/inline_tipjar.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 402cd9a2..4386c3e3 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -68,7 +68,7 @@ func (bot TipBot) createTipjar(ctx context.Context, text string, sender *tb.User inlineMessage := fmt.Sprintf( Translate(ctx, "inlineTipjarMessage"), perUserAmount, - GetUserStr(toUser.Telegram), + GetUserStrMd(toUser.Telegram), 0, amount, 0, @@ -297,7 +297,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback inlineTipjar.Message = fmt.Sprintf( i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarMessage"), inlineTipjar.PerUserAmount, - GetUserStr(inlineTipjar.To.Telegram), + GetUserStrMd(inlineTipjar.To.Telegram), inlineTipjar.GivenAmount, inlineTipjar.Amount, inlineTipjar.NGiven, @@ -318,7 +318,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback // tipjar is full inlineTipjar.Message = fmt.Sprintf( i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarEndedMessage"), - GetUserStr(inlineTipjar.To.Telegram), + GetUserStrMd(inlineTipjar.To.Telegram), inlineTipjar.Amount, inlineTipjar.NGiven, ) From 5eaf380e91556475a07b506d07cdd77b45fd60f5 Mon Sep 17 00:00:00 2001 From: Koty <89604768+kotyauditore@users.noreply.github.com> Date: Mon, 15 Nov 2021 00:53:48 +0400 Subject: [PATCH 044/541] Tipjar translations into Spanish and Portuguese (#122) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * For mango-selling grannies in Brazil * Update pt-br.toml #TIPJAR pt-br translation * Update es.toml :) #TIPJAR es ahora alcancía * Update es.toml minor fix * Update pt-br.toml Minor fixes to pt-br.toml file * remove pt.toml --- translations/es.toml | 28 ++++++++++++++++++++++++++-- translations/pt-br.toml | 24 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/translations/es.toml b/translations/es.toml index 6f8f23f3..d9deda31 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -17,7 +17,7 @@ linkCommandStr = """enlace""" lnurlCommandStr = """lnurl""" faucetCommandStr = """grifo""" -tipjarCommandStr = """bote""" +tipjarCommandStr = """alcancía""" receiveCommandStr = """recibir""" hideCommandStr = """ocultar""" volcanoCommandStr = """volcán""" @@ -124,7 +124,7 @@ advancedMessage = """%s # START -startSettingWalletMessage = """🧮 Configurar tu monedero...""" +startSettingWalletMessage = """🧮 Configurando tu monedero...""" startWalletCreatedMessage = """🧮 Monedero creado.""" startWalletReadyMessage = """✅ *Tu monedero está listo.*""" startWalletErrorMessage = """🚫 Error al iniciar tu monedero. Vuelve a intentarlo más tarde.""" @@ -297,3 +297,27 @@ inlineReceiveCreateWalletMessage = """Chatea con %s 👈 para gestionar tu carte inlineReceiveYourselfMessage = """📖 No puedes pagarte a ti mismo.""" inlineReceiveFailedMessage = """🚫 Recepción fallida.""" inlineReceiveCancelledMessage = """🚫 Recepción cancelada.""" + +# TIPJAR + +inlineQueryTipjarTitle = """🍯 Crear una alcancía.""" +inlineQueryTipjarDescription = """Uso: @%s alcancía """ +inlineResultTipjarTitle = """🍯 Crear una alcancía de %d sat.""" +inlineResultTipjarDescription = """👉 Haz clic aquí para crear una alcancía en este chat.""" + +inlineTipjarMessage = """Pulsa 💸 para *pagar %d sat* a esa alcancía de %s. + +🙏 Dados: *%d*/%d sat (por %d usuarios) +%s""" +inlineTipjarEndedMessage = """🍯 La alcancía de %s está llena ⭐️\n\n🏅 %d sat dados por %d usuários.""" +inlineTipjarAppendMemo = """\n✉️ %s""" +inlineTipjarCancelledMessage = """🚫 Alcancía cancelada.""" +inlineTipjarInvalidPeruserAmountMessage = """🚫 El monto por usuario no es divisor de la capacidad.""" +inlineTipjarInvalidAmountMessage = """🚫 Monto inválido.""" +inlineTipjarSentMessage = """🍯 %d sat enviado(s) a %s.""" +inlineTipjarReceivedMessage = """🍯 %s te envió %d sat.""" +inlineTipjarHelpTipjarInGroup = """Crea una alcancía en un grupo con el bot dentro o utiliza el 👉 comando _inline_ (/advanced para más).""" +inlineTipjarHelpText = """📖 Oops, eso no funcionó. %s + +*Uso:* `/tipjar ` +*Ejemplo:* `/tipjar 210 21`""" diff --git a/translations/pt-br.toml b/translations/pt-br.toml index e15e19b6..4933dfac 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -297,3 +297,27 @@ inlineReceiveCreateWalletMessage = """Bate-papo com %s 👈 para administrar sua inlineReceiveYourselfMessage = """📖 Você não pode pagar a si mesmo.""" inlineReceiveFailedMessage = """🚫 O recebimento falhou.""" inlineReceiveCancelledMessage = """🚫 Recepção cancelada.""" + +# TIPJAR + +inlineQueryTipjarTitle = """🍯 Criar um cofrinho.""" +inlineQueryTipjarDescription = """Uso: @%s cofrinho """ +inlineResultTipjarTitle = """🍯 Criar um cofrinho de %d sat.""" +inlineResultTipjarDescription = """👉 Clique aqui para criar um cofrinho neste chat.""" + +inlineTipjarMessage = """Pressione 💸 para *pagar %d sat* a esse cofrinho de %s. + +🙏 Doados: *%d*/%d sat (por %d usuários) +%s""" +inlineTipjarEndedMessage = """🍯 O cofrinho de %s está cheio ⭐️\n\n🏅 %d sat doados por %d usuários.""" +inlineTipjarAppendMemo = """\n✉️ %s""" +inlineTipjarCancelledMessage = """🚫 Cofrinho cancelado.""" +inlineTipjarInvalidPeruserAmountMessage = """🚫 Quantidade por usuário não divisor da capacidade.""" +inlineTipjarInvalidAmountMessage = """🚫 Quantia inválida.""" +inlineTipjarSentMessage = """🍯 %d sat enviado(s) a %s.""" +inlineTipjarReceivedMessage = """🍯 %s lhe enviou %d sat.""" +inlineTipjarHelpTipjarInGroup = """Criar uma cofrinho em grupo com o bot dentro ou usar o 👉 comando_inline_ (/advanced para mais).""" +inlineTipjarHelpText = """📖 Opa, isso não funcionou. %s + +*Uso:* `/tipjar ` +*Exemplo:* `/tipjar 210 21`""" From 051174bd1c7f212e4dfcf8de6b845b2111144a66 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 14 Nov 2021 21:57:54 +0100 Subject: [PATCH 045/541] using anyChosenInlineHandler (#128) * using anyChosenInlineHandler * error logging --- internal/telegram/inline_faucet.go | 4 +++- internal/telegram/inline_query.go | 18 +++++++++++++++++- internal/telegram/inline_receive.go | 4 +++- internal/telegram/inline_send.go | 4 +++- internal/telegram/inline_tipjar.go | 4 +++- 5 files changed, 29 insertions(+), 5 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index a3e094f2..1cba4ff1 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -3,7 +3,9 @@ package telegram import ( "context" "fmt" + "github.com/eko/gocache/store" "strings" + "time" "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/i18n" @@ -214,7 +216,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { // needed to set a unique string ID for each result results[i].SetResultID(inlineFaucet.ID) - runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) + bot.Cache.Set(inlineFaucet.ID, inlineFaucet, &store.Options{Expiration: 5 * time.Minute}) log.Infof("[faucet] %s created inline faucet %s: %d sat (%d per user)", GetUserStr(inlineFaucet.From.Telegram), inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) } diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index 0582fc8d..ac3f5e5c 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -3,6 +3,9 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "reflect" "strconv" "strings" @@ -92,7 +95,20 @@ func (bot TipBot) inlineQueryReplyWithError(q *tb.Query, message string, help st } func (bot TipBot) anyChosenInlineHandler(q *tb.ChosenInlineResult) { - fmt.Printf(q.Query) + // load inline object from cache + inlineObject, err := bot.Cache.Get(q.ResultID) + // check error + if err != nil { + log.Errorf("[anyChosenInlineHandler] could not find inline object in cache. %v", err) + return + } + switch inlineObject.(type) { + case storage.Storable: + // persist inline object in bunt + runtime.IgnoreError(bot.Bunt.Set(inlineObject.(storage.Storable))) + default: + log.Errorf("[anyChosenInlineHandler] invalid inline object type: %s, query: %s", reflect.TypeOf(inlineObject).String(), q.Query) + } } func (bot TipBot) commandTranslationMap(ctx context.Context, command string) context.Context { diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 95aaf2a3..5b50294a 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -3,7 +3,9 @@ package telegram import ( "context" "fmt" + "github.com/eko/gocache/store" "strings" + "time" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -121,7 +123,7 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { From_SpecificUser: from_SpecificUser, LanguageCode: ctx.Value("publicLanguageCode").(string), } - runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) + bot.Cache.Set(inlineReceive.ID, inlineReceive, &store.Options{Expiration: 5 * time.Minute}) } err = bot.Telegram.Answer(q, &tb.QueryResponse{ diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 5844a814..7eb3f15c 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -3,7 +3,9 @@ package telegram import ( "context" "fmt" + "github.com/eko/gocache/store" "strings" + "time" "github.com/LightningTipBot/LightningTipBot/internal/i18n" @@ -140,7 +142,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { } // add result to persistent struct - runtime.IgnoreError(inlineSend.Set(inlineSend, bot.Bunt)) + bot.Cache.Set(inlineSend.ID, inlineSend, &store.Options{Expiration: 5 * time.Minute}) } err = bot.Telegram.Answer(q, &tb.QueryResponse{ diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 4386c3e3..d8219058 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -3,7 +3,9 @@ package telegram import ( "context" "fmt" + "github.com/eko/gocache/store" "strings" + "time" "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/i18n" @@ -211,7 +213,7 @@ func (bot TipBot) handleInlineTipjarQuery(ctx context.Context, q *tb.Query) { // needed to set a unique string ID for each result results[i].SetResultID(inlineTipjar.ID) - runtime.IgnoreError(inlineTipjar.Set(inlineTipjar, bot.Bunt)) + bot.Cache.Set(inlineTipjar.ID, inlineTipjar, &store.Options{Expiration: 5 * time.Minute}) log.Infof("[tipjar] %s created inline tipjar %s: %d sat (%d per user)", GetUserStr(inlineTipjar.To.Telegram), inlineTipjar.ID, inlineTipjar.Amount, inlineTipjar.PerUserAmount) } From dd572472662c705070b810d9dbe98dfff12e9eeb Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 14 Nov 2021 22:03:14 +0100 Subject: [PATCH 046/541] increase lnbits pay timeout (#127) --- internal/lnbits/lnbits.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/lnbits/lnbits.go b/internal/lnbits/lnbits.go index bd8fe2d3..8c4edde2 100644 --- a/internal/lnbits/lnbits.go +++ b/internal/lnbits/lnbits.go @@ -2,6 +2,7 @@ package lnbits import ( "github.com/imroc/req" + "time" ) // NewClient returns a new lnbits api client. Pass your API key and url here. @@ -155,7 +156,9 @@ func (w Wallet) Pay(params PaymentParams, c *Client) (wtx BitInvoice, err error) "Accept": "application/json", "X-Api-Key": w.Adminkey, } - resp, err := req.Post(c.url+"/api/v1/payments", adminHeader, req.BodyJSON(¶ms)) + r := req.New() + r.SetTimeout(time.Hour * 24) + resp, err := r.Post(c.url+"/api/v1/payments", adminHeader, req.BodyJSON(¶ms)) if err != nil { return } From 0653498e0fb9115b191a8751a6bacad8e0d44ef6 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 14 Nov 2021 22:25:47 +0100 Subject: [PATCH 047/541] Lnurl withdraw (#129) * go-lnurl --> v1.8.4 * clean up comments * delete HandleLNURL() * lnurl-withdrawal * actual file * remove comments * remove comments * refactor translation * translations * add type check and * translations Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> Co-authored-by: lngohumble --- go.mod | 2 +- go.sum | 2 + internal/lnurl/lnurl.go | 28 +-- internal/telegram/amounts.go | 22 +- internal/telegram/handler.go | 14 ++ internal/telegram/lnurl-pay.go | 76 +++---- internal/telegram/lnurl-withdraw.go | 330 ++++++++++++++++++++++++++++ internal/telegram/lnurl.go | 97 +------- translations/de.toml | 14 +- translations/en.toml | 15 +- translations/es.toml | 13 +- translations/fr.toml | 5 +- translations/id.toml | 4 +- translations/it.toml | 5 +- translations/nl.toml | 5 +- translations/pt-br.toml | 13 +- 16 files changed, 491 insertions(+), 154 deletions(-) create mode 100644 internal/telegram/lnurl-withdraw.go diff --git a/go.mod b/go.mod index c322e98e..25b2acdb 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.15 require ( github.com/BurntSushi/toml v0.3.1 github.com/eko/gocache v1.2.0 - github.com/fiatjaf/go-lnurl v1.4.0 + github.com/fiatjaf/go-lnurl v1.8.3 github.com/fiatjaf/ln-decodepay v1.1.0 github.com/gorilla/mux v1.8.0 github.com/imroc/req v0.3.0 diff --git a/go.sum b/go.sum index 6b52cb9d..a7922cb7 100644 --- a/go.sum +++ b/go.sum @@ -137,6 +137,8 @@ github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fiatjaf/go-lnurl v1.4.0 h1:hVFEEJD2A9D6ojEcqLyD54CM2ZJ9Tzs2jNKw/GNq52A= github.com/fiatjaf/go-lnurl v1.4.0/go.mod h1:BqA8WXAOzntF7Z3EkVO7DfP4y5rhWUmJ/Bu9KBke+rs= +github.com/fiatjaf/go-lnurl v1.8.3 h1:ONQUQsXIZKkzrzax2SMUr5W0PreluhO4tct1JM0V/MA= +github.com/fiatjaf/go-lnurl v1.8.3/go.mod h1:0PPdV2GFnJK97ztksEIwrcEUVTothAKJsmzqPphAQhs= github.com/fiatjaf/ln-decodepay v1.1.0 h1:HigjqNH+ApiO6gm7RV23jXNFuvwq+zgsWl4BJAfPWwE= github.com/fiatjaf/ln-decodepay v1.1.0/go.mod h1:2qdTT95b8Z4dfuxiZxXuJ1M7bQ9CrLieEA1DKC50q6s= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index aed953d2..d3575cdc 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -78,7 +78,7 @@ func (w Server) handleLnUrl(writer http.ResponseWriter, request *http.Request) { // serveLNURLpFirst serves the first part of the LNURLp protocol with the endpoint // to call and the metadata that matches the description hash of the second response -func (w Server) serveLNURLpFirst(username string) (*lnurl.LNURLPayResponse1, error) { +func (w Server) serveLNURLpFirst(username string) (*lnurl.LNURLPayParams, error) { log.Infof("[LNURL] Serving endpoint for user %s", username) callbackURL, err := url.Parse(fmt.Sprintf("%s/%s/%s", w.callbackHostname.String(), lnurlEndpoint, username)) if err != nil { @@ -90,11 +90,10 @@ func (w Server) serveLNURLpFirst(username string) (*lnurl.LNURLPayResponse1, err return nil, err } - return &lnurl.LNURLPayResponse1{ + return &lnurl.LNURLPayParams{ LNURLResponse: lnurl.LNURLResponse{Status: statusOk}, Tag: payRequestTag, Callback: callbackURL.String(), - CallbackURL: callbackURL, // probably no need to set this here MinSendable: minSendable, MaxSendable: MaxSendable, EncodedMetadata: string(jsonMeta), @@ -104,11 +103,11 @@ func (w Server) serveLNURLpFirst(username string) (*lnurl.LNURLPayResponse1, err } // serveLNURLpSecond serves the second LNURL response with the payment request with the correct description hash -func (w Server) serveLNURLpSecond(username string, amount int64, comment string) (*lnurl.LNURLPayResponse2, error) { +func (w Server) serveLNURLpSecond(username string, amount int64, comment string) (*lnurl.LNURLPayValues, error) { log.Infof("[LNURL] Serving invoice for user %s", username) if amount < minSendable || amount > MaxSendable { // amount is not ok - return &lnurl.LNURLPayResponse2{ + return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ Status: statusError, Reason: fmt.Sprintf("Amount out of bounds (min: %d mSat, max: %d mSat).", minSendable, MaxSendable)}, @@ -116,7 +115,7 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) } // check comment length if len(comment) > CommentAllowed { - return &lnurl.LNURLPayResponse2{ + return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ Status: statusError, Reason: fmt.Sprintf("Comment too long (max: %d characters).", CommentAllowed)}, @@ -136,14 +135,14 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) } if tx.Error != nil { - return &lnurl.LNURLPayResponse2{ + return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ Status: statusError, Reason: fmt.Sprintf("Invalid user.")}, }, fmt.Errorf("[GetUser] Couldn't fetch user info from database: %v", tx.Error) } if user.Wallet == nil { - return &lnurl.LNURLPayResponse2{ + return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ Status: statusError, Reason: fmt.Sprintf("Invalid user.")}, @@ -152,7 +151,7 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) // user is ok now create invoice // set wallet lnbits client - var resp *lnurl.LNURLPayResponse2 + var resp *lnurl.LNURLPayValues // the same description_hash needs to be built in the second request metadata := w.metaData(username) @@ -169,7 +168,7 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) w.c) if err != nil { err = fmt.Errorf("[serveLNURLpSecond] Couldn't create invoice: %v", err) - resp = &lnurl.LNURLPayResponse2{ + resp = &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ Status: statusError, Reason: "Couldn't create invoice."}, @@ -187,10 +186,10 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) CreatedAt: time.Now(), })) - return &lnurl.LNURLPayResponse2{ + return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{Status: statusOk}, PR: invoice.PaymentRequest, - Routes: make([][]lnurl.RouteInfo, 0), + Routes: make([]struct{}, 0), SuccessAction: &lnurl.SuccessAction{Message: "Payment received!", Tag: "message"}, }, nil @@ -211,6 +210,7 @@ func (w Server) descriptionHash(metadata lnurl.Metadata) (string, error) { // and is used again in the second response to verify the description hash func (w Server) metaData(username string) lnurl.Metadata { return lnurl.Metadata{ - {"text/identifier", fmt.Sprintf("%s@%s", username, w.callbackHostname.Hostname())}, - {"text/plain", fmt.Sprintf("Pay to %s@%s", username, w.callbackHostname.Hostname())}} + Description: fmt.Sprintf("Pay to %s@%s", username, w.callbackHostname.Hostname()), + LightningAddress: fmt.Sprintf("%s@%s", username, w.callbackHostname.Hostname()), + } } diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index e917a164..6728b0ba 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -98,7 +98,7 @@ func (bot *TipBot) askForAmount(ctx context.Context, id string, eventType string AmountMax: amountMax, OiringalCommand: originalCommand, } - // set LNURLPayResponse1 in the state of the user + // set LNURLPayParams in the state of the user stateDataJson, err := json.Marshal(enterAmountStateData) if err != nil { log.Errorln(err) @@ -176,6 +176,26 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(StateDataJson)) bot.lnurlPayHandlerSend(ctx, m) return + case "LnurlWithdrawState": + tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(EnterAmountStateData.ID))} + sn, err := tx.Get(tx, bot.Bunt) + if err != nil { + return + } + LnurlWithdrawState := sn.(*LnurlWithdrawState) + LnurlWithdrawState.Amount = amount * 1000 // mSat + // add result to persistent struct + runtime.IgnoreError(LnurlWithdrawState.Set(LnurlWithdrawState, bot.Bunt)) + + EnterAmountStateData.Amount = int64(amount) * 1000 // mSat + StateDataJson, err := json.Marshal(EnterAmountStateData) + if err != nil { + log.Errorln(err) + return + } + SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(StateDataJson)) + bot.lnurlWithdrawHandlerWithdraw(ctx, m) + return case "CreateInvoiceState": m.Text = fmt.Sprintf("/invoice %d", amount) SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 0a7ad866..e0a5efd3 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -337,5 +337,19 @@ func (bot TipBot) getHandler() []Handler { Type: CallbackInterceptor, Before: []intercept.Func{bot.loadUserInterceptor}}, }, + { + Endpoints: []interface{}{&btnWithdraw}, + Handler: bot.confirmWithdrawHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, + { + Endpoints: []interface{}{&btnCancelWithdraw}, + Handler: bot.cancelWithdrawHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{bot.loadUserInterceptor}}, + }, } } diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index d5a8bacb..4af0af96 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -20,12 +20,12 @@ import ( // LnurlPayState saves the state of the user for an LNURL payment type LnurlPayState struct { *transaction.Base - From *lnbits.User `json:"from"` - LNURLPayResponse1 lnurl.LNURLPayResponse1 `json:"LNURLPayResponse1"` - LNURLPayResponse2 lnurl.LNURLPayResponse2 `json:"LNURLPayResponse2"` - Amount int `json:"amount"` - Comment string `json:"comment"` - LanguageCode string `json:"languagecode"` + From *lnbits.User `json:"from"` + LNURLPayParams lnurl.LNURLPayParams `json:"LNURLPayParams"` + LNURLPayValues lnurl.LNURLPayValues `json:"LNURLPayValues"` + Amount int `json:"amount"` + Comment string `json:"comment"` + LanguageCode string `json:"languagecode"` } // lnurlPayHandler1 is invoked when the first lnurl response was a lnurlpay response @@ -38,9 +38,10 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams // object that holds all information about the send payment id := fmt.Sprintf("lnurlp-%d-%s", m.Sender.ID, RandStringRunes(5)) lnurlPayState := LnurlPayState{ - Base: transaction.New(transaction.ID(id)), - LNURLPayResponse1: payParams.LNURLPayResponse1, - LanguageCode: ctx.Value("publicLanguageCode").(string), + Base: transaction.New(transaction.ID(id)), + From: user, + LNURLPayParams: payParams.LNURLPayParams, + LanguageCode: ctx.Value("publicLanguageCode").(string), } // first we check whether an amount is present in the command @@ -53,34 +54,24 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams // amount was present memoStartsAt = 3 } - // check if memo is presentin lnrul-p + // check if memo is present in command memo := GetMemoFromCommand(m.Text, memoStartsAt) // shorten memo to allowed length - if len(memo) > int(lnurlPayState.LNURLPayResponse1.CommentAllowed) { - memo = memo[:lnurlPayState.LNURLPayResponse1.CommentAllowed] + if len(memo) > int(lnurlPayState.LNURLPayParams.CommentAllowed) { + memo = memo[:lnurlPayState.LNURLPayParams.CommentAllowed] } if len(memo) > 0 { lnurlPayState.Comment = memo } - // add result to persistent struct, with memo - runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) - - // now we actualy check whether the amount is already set because we can ask for it if not - // if no amount is in the command, ask for it - if amount_err != nil || amount < 1 { - // // no amount was entered, set user state and ask for amount - bot.askForAmount(ctx, id, "LnurlPayState", lnurlPayState.LNURLPayResponse1.MinSendable, lnurlPayState.LNURLPayResponse1.MaxSendable, m.Text) - return - } - // amount is already present in the command, i.e., /lnurl // amount not in allowed range from LNURL - if int64(amount) > (lnurlPayState.LNURLPayResponse1.MaxSendable/1000) || int64(amount) < (lnurlPayState.LNURLPayResponse1.MinSendable/1000) && - (lnurlPayState.LNURLPayResponse1.MaxSendable != 0 && lnurlPayState.LNURLPayResponse1.MinSendable != 0) { // only if max and min are set + if amount_err == nil && + (int64(amount) > (lnurlPayState.LNURLPayParams.MaxSendable/1000) || int64(amount) < (lnurlPayState.LNURLPayParams.MinSendable/1000)) && + (lnurlPayState.LNURLPayParams.MaxSendable != 0 && lnurlPayState.LNURLPayParams.MinSendable != 0) { // only if max and min are set err := fmt.Errorf("amount not in range") log.Warnf("[lnurlPayHandler] Error: %s", err.Error()) - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), lnurlPayState.LNURLPayResponse1.MinSendable/1000, lnurlPayState.LNURLPayResponse1.MaxSendable/1000)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), lnurlPayState.LNURLPayParams.MinSendable/1000, lnurlPayState.LNURLPayParams.MaxSendable/1000)) ResetUserState(user, bot) return } @@ -90,7 +81,14 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams // add result to persistent struct runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) - // not necessary to save this in the state data, but till doing it + // now we actualy check whether the amount was in the command and if not, ask for it + if amount_err != nil || amount < 1 { + // // no amount was entered, set user state and ask for amount + bot.askForAmount(ctx, id, "LnurlPayState", lnurlPayState.LNURLPayParams.MinSendable, lnurlPayState.LNURLPayParams.MaxSendable, m.Text) + return + } + + // We need to save the pay state in the user state so we can load the payment in the next handler paramsJson, err := json.Marshal(lnurlPayState) if err != nil { log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) @@ -103,7 +101,7 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams return } -// lnurlPayHandler is invoked when the user has delivered an amount and is ready to pay +// lnurlPayHandlerSend is invoked when the user has delivered an amount and is ready to pay func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { user := LoadUser(ctx) if user.Wallet == nil { @@ -113,7 +111,7 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { // assert that user has entered an amount if user.StateKey != lnbits.UserHasEnteredAmount { - log.Errorln("[lnurlPayHandler] state keys don't match") + log.Errorln("[lnurlPayHandlerSend] state keys don't match") bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } @@ -122,7 +120,7 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { var enterAmountData EnterAmountStateData err := json.Unmarshal([]byte(user.StateData), &enterAmountData) if err != nil { - log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) + log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } @@ -131,7 +129,7 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { tx := &LnurlPayState{Base: transaction.New(transaction.ID(enterAmountData.ID))} fn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) + log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } @@ -141,15 +139,13 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { client, err := bot.GetHttpClient() if err != nil { - log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) - // bot.trySendMessage(c.Sender, err.Error()) + log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } - callbackUrl, err := url.Parse(lnurlPayState.LNURLPayResponse1.Callback) + callbackUrl, err := url.Parse(lnurlPayState.LNURLPayParams.Callback) if err != nil { - log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) - // bot.trySendMessage(c.Sender, err.Error()) + log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } @@ -166,31 +162,29 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { res, err := client.Get(callbackUrl.String()) if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) - // bot.trySendMessage(c.Sender, err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } body, err := ioutil.ReadAll(res.Body) if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) - // bot.trySendMessage(c.Sender, err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } - var response2 lnurl.LNURLPayResponse2 + var response2 lnurl.LNURLPayValues json.Unmarshal(body, &response2) if response2.Status == "ERROR" || len(response2.PR) < 1 { error_reason := "Could not receive invoice." if len(response2.Reason) > 0 { error_reason = response2.Reason } - log.Errorf("[lnurlPayHandler] Error in LNURLPayResponse2: %s", error_reason) + log.Errorf("[lnurlPayHandler] Error in LNURLPayValues: %s", error_reason) bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), error_reason)) return } - lnurlPayState.LNURLPayResponse2 = response2 + lnurlPayState.LNURLPayValues = response2 // add result to persistent struct runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) bot.Telegram.Delete(statusMsg) diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go new file mode 100644 index 00000000..2990d19b --- /dev/null +++ b/internal/telegram/lnurl-withdraw.go @@ -0,0 +1,330 @@ +package telegram + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net/url" + + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + "github.com/LightningTipBot/LightningTipBot/internal/str" + lnurl "github.com/fiatjaf/go-lnurl" + log "github.com/sirupsen/logrus" + tb "gopkg.in/tucnak/telebot.v2" +) + +var ( + withdrawConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + btnCancelWithdraw = paymentConfirmationMenu.Data("🚫 Cancel", "cancel_withdraw") + btnWithdraw = paymentConfirmationMenu.Data("✅ Withdraw", "confirm_withdraw") +) + +// LnurlWithdrawState saves the state of the user for an LNURL payment +type LnurlWithdrawState struct { + *transaction.Base + From *lnbits.User `json:"from"` + LNURLWithdrawResponse lnurl.LNURLWithdrawResponse `json:"LNURLWithdrawResponse"` + LNURResponse lnurl.LNURLResponse `json:"LNURLResponse"` + Amount int `json:"amount"` + Comment string `json:"comment"` + LanguageCode string `json:"languagecode"` + Success bool `json:"success"` + Invoice lnbits.BitInvoice `json:"invoice"` + Message string `json:"message"` +} + +// lnurlWithdrawHandler is invoked when the first lnurl response was a lnurl-withdraw response +// at this point, the user hans't necessarily entered an amount yet +func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, withdrawParams LnurlWithdrawState) { + user := LoadUser(ctx) + if user.Wallet == nil { + return + } + // object that holds all information about the send payment + id := fmt.Sprintf("lnurlw-%d-%s", m.Sender.ID, RandStringRunes(5)) + LnurlWithdrawState := LnurlWithdrawState{ + Base: transaction.New(transaction.ID(id)), + From: user, + LNURLWithdrawResponse: withdrawParams.LNURLWithdrawResponse, + LanguageCode: ctx.Value("publicLanguageCode").(string), + } + + // first we check whether an amount is present in the command + amount, amount_err := decodeAmountFromCommand(m.Text) + + // amount is already present in the command, i.e., /lnurl + // amount not in allowed range from LNURL + if amount_err == nil && + (int64(amount) > (LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable/1000) || int64(amount) < (LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable/1000)) && + (LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable != 0 && LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable != 0) { // only if max and min are set + err := fmt.Errorf("amount not in range") + log.Warnf("[lnurlWithdrawHandler] Error: %s", err.Error()) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable/1000, LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable/1000)) + ResetUserState(user, bot) + return + } + // set also amount in the state of the user + LnurlWithdrawState.Amount = amount * 1000 // save as mSat + + // add result to persistent struct + runtime.IgnoreError(LnurlWithdrawState.Set(LnurlWithdrawState, bot.Bunt)) + + // now we actualy check whether the amount was in the command and if not, ask for it + if amount_err != nil || amount < 1 { + // // no amount was entered, set user state and ask for amount + bot.askForAmount(ctx, id, "LnurlWithdrawState", LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable, LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable, m.Text) + return + } + + // We need to save the pay state in the user state so we can load the payment in the next handler + paramsJson, err := json.Marshal(LnurlWithdrawState) + if err != nil { + log.Errorf("[lnurlWithdrawHandler] Error: %s", err.Error()) + // bot.trySendMessage(m.Sender, err.Error()) + return + } + SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(paramsJson)) + // directly go to confirm + bot.lnurlWithdrawHandlerWithdraw(ctx, m) + return +} + +// lnurlWithdrawHandlerWithdraw is invoked when the user has delivered an amount and is ready to pay +func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + if user.Wallet == nil { + return + } + statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlPreparingWithdraw")) + + // assert that user has entered an amount + if user.StateKey != lnbits.UserHasEnteredAmount { + log.Errorln("[lnurlWithdrawHandlerWithdraw] state keys don't match") + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return + } + + // read the enter amount state from user.StateData + var enterAmountData EnterAmountStateData + err := json.Unmarshal([]byte(user.StateData), &enterAmountData) + if err != nil { + log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return + } + + // use the enter amount state of the user to load the LNURL payment state + tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(enterAmountData.ID))} + fn, err := tx.Get(tx, bot.Bunt) + if err != nil { + log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return + } + var lnurlWithdrawState *LnurlWithdrawState + switch fn.(type) { + case *LnurlWithdrawState: + lnurlWithdrawState = fn.(*LnurlWithdrawState) + default: + log.Errorf("[lnurlWithdrawHandlerWithdraw] invalid type") + } + + confirmText := fmt.Sprintf(Translate(ctx, "confirmLnurlWithdrawMessage"), lnurlWithdrawState.Amount/1000) + if len(lnurlWithdrawState.LNURLWithdrawResponse.DefaultDescription) > 0 { + confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmPayAppendMemo"), str.MarkdownEscape(lnurlWithdrawState.LNURLWithdrawResponse.DefaultDescription)) + } + lnurlWithdrawState.Message = confirmText + + // create inline buttons + withdrawButton := paymentConfirmationMenu.Data(Translate(ctx, "withdrawButtonMessage"), "confirm_withdraw") + btnCancelWithdraw := paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_withdraw") + withdrawButton.Data = lnurlWithdrawState.ID + btnCancelWithdraw.Data = lnurlWithdrawState.ID + + withdrawConfirmationMenu.Inline( + withdrawConfirmationMenu.Row( + withdrawButton, + btnCancelWithdraw), + ) + + bot.tryEditMessage(statusMsg, confirmText, withdrawConfirmationMenu) + + // // add response to persistent struct + // lnurlWithdrawState.LNURResponse = response2 + runtime.IgnoreError(lnurlWithdrawState.Set(lnurlWithdrawState, bot.Bunt)) +} + +// confirmPayHandler when user clicked pay on payment confirmation +func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { + tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(c.Data))} + fn, err := tx.Get(tx, bot.Bunt) + if err != nil { + log.Errorf("[confirmWithdrawHandler] Error: %s", err.Error()) + return + } + var lnurlWithdrawState *LnurlWithdrawState + switch fn.(type) { + case *LnurlWithdrawState: + lnurlWithdrawState = fn.(*LnurlWithdrawState) + default: + log.Errorf("[confirmWithdrawHandler] invalid type") + } + // onnly the correct user can press + if lnurlWithdrawState.From.Telegram.ID != c.Sender.ID { + return + } + // immediatelly set intransaction to block duplicate calls + err = lnurlWithdrawState.Lock(lnurlWithdrawState, bot.Bunt) + if err != nil { + log.Errorf("[confirmWithdrawHandler] %s", err) + bot.tryDeleteMessage(c.Message) + bot.tryEditMessage(c.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) + return + } + if !lnurlWithdrawState.Active { + log.Errorf("[confirmPayHandler] send not active anymore") + bot.tryEditMessage(c.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) + bot.tryDeleteMessage(c.Message) + return + } + defer lnurlWithdrawState.Release(lnurlWithdrawState, bot.Bunt) + + user := LoadUser(ctx) + if user.Wallet == nil { + bot.tryDeleteMessage(c.Message) + return + } + + // reset state immediately + ResetUserState(user, bot) + + // update button text + bot.tryEditMessage( + c.Message, + lnurlWithdrawState.Message, + &tb.ReplyMarkup{ + InlineKeyboard: [][]tb.InlineButton{ + {tb.InlineButton{Text: i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlPreparingWithdraw")}}, + }, + }, + ) + + // lnurlWithdrawState loaded + client, err := bot.GetHttpClient() + if err != nil { + log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) + bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + return + } + callbackUrl, err := url.Parse(lnurlWithdrawState.LNURLWithdrawResponse.Callback) + if err != nil { + log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) + bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + return + } + + // generate an invoice and add the pr to the request + // generate invoice + invoice, err := user.Wallet.Invoice( + lnbits.InvoiceParams{ + Out: false, + Amount: int64(lnurlWithdrawState.Amount) / 1000, + Memo: "Withdraw", + Webhook: internal.Configuration.Lnbits.WebhookServer}, + bot.Client) + if err != nil { + errmsg := fmt.Sprintf("[lnurlWithdrawHandlerWithdraw] Could not create an invoice: %s", err) + log.Errorln(errmsg) + return + } + lnurlWithdrawState.Invoice = invoice + + qs := callbackUrl.Query() + // add amount to query string + qs.Set("pr", invoice.PaymentRequest) + qs.Set("k1", lnurlWithdrawState.LNURLWithdrawResponse.K1) + + callbackUrl.RawQuery = qs.Encode() + + res, err := client.Get(callbackUrl.String()) + if err != nil || res.StatusCode >= 300 { + log.Errorf("[lnurlWithdrawHandlerWithdraw] Failed.") + bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + return + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) + bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + return + } + + // parse the response + var response2 lnurl.LNURLResponse + json.Unmarshal(body, &response2) + if response2.Status == "OK" { + // bot.trySendMessage(c.Sender, Translate(ctx, "lnurlWithdrawSuccess")) + // update button text + bot.tryEditMessage( + c.Message, + lnurlWithdrawState.Message, + &tb.ReplyMarkup{ + InlineKeyboard: [][]tb.InlineButton{ + {tb.InlineButton{Text: i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawSuccess")}}, + }, + }, + ) + + } else { + log.Errorf("[lnurlWithdrawHandlerWithdraw] LNURLWithdraw failed.") + // bot.trySendMessage(c.Sender, Translate(ctx, "lnurlWithdrawFailed")) + // update button text + bot.tryEditMessage( + c.Message, + lnurlWithdrawState.Message, + &tb.ReplyMarkup{ + InlineKeyboard: [][]tb.InlineButton{ + {tb.InlineButton{Text: i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawFailed")}}, + }, + }, + ) + return + } + + // add response to persistent struct + lnurlWithdrawState.LNURResponse = response2 + runtime.IgnoreError(lnurlWithdrawState.Set(lnurlWithdrawState, bot.Bunt)) + +} + +// cancelPaymentHandler invoked when user clicked cancel on payment confirmation +func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { + // reset state immediately + user := LoadUser(ctx) + ResetUserState(user, bot) + tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(c.Data))} + fn, err := tx.Get(tx, bot.Bunt) + if err != nil { + log.Errorf("[cancelWithdrawHandler] Error: %s", err.Error()) + return + } + var lnurlWithdrawState *LnurlWithdrawState + switch fn.(type) { + case *LnurlWithdrawState: + lnurlWithdrawState = fn.(*LnurlWithdrawState) + default: + log.Errorf("[cancelWithdrawHandler] invalid type") + } + // onnly the correct user can press + if lnurlWithdrawState.From.Telegram.ID != c.Sender.ID { + return + } + bot.tryEditMessage(c.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawCancelled"), &tb.ReplyMarkup{}) + lnurlWithdrawState.InTransaction = false + lnurlWithdrawState.Inactivate(lnurlWithdrawState, bot.Bunt) +} diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 74ee2003..3d4bea19 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -3,9 +3,7 @@ package telegram import ( "bytes" "context" - "errors" "fmt" - "io/ioutil" "net/http" "net/url" "strings" @@ -16,7 +14,6 @@ import ( lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" - "github.com/tidwall/gjson" tb "gopkg.in/tucnak/telebot.v2" ) @@ -74,19 +71,24 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { // assume payment // HandleLNURL by fiatjaf/go-lnurl - _, params, err := bot.HandleLNURL(lnurlSplit) + _, params, err := lnurl.HandleLNURL(lnurlSplit) if err != nil { bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not resolve LNURL.")) log.Errorln(err) return } switch params.(type) { - case lnurl.LNURLPayResponse1: - payParams := LnurlPayState{LNURLPayResponse1: params.(lnurl.LNURLPayResponse1)} - log.Infof("[lnurlHandler] %s", payParams.LNURLPayResponse1.Callback) + case lnurl.LNURLPayParams: + payParams := LnurlPayState{LNURLPayParams: params.(lnurl.LNURLPayParams)} + log.Infof("[lnurlHandler] %s", payParams.LNURLPayParams.Callback) bot.tryDeleteMessage(statusMsg) bot.lnurlPayHandler(ctx, m, payParams) return + case lnurl.LNURLWithdrawResponse: + withdrawParams := LnurlWithdrawState{LNURLWithdrawResponse: params.(lnurl.LNURLWithdrawResponse)} + log.Infof("[lnurlHandler] %s", withdrawParams.LNURLWithdrawResponse.Callback) + bot.tryDeleteMessage(statusMsg) + bot.lnurlWithdrawHandler(ctx, m, withdrawParams) default: err := fmt.Errorf("invalid LNURL type.") log.Errorln(err) @@ -149,84 +151,3 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { // send the lnurl data to user bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lnurlEncode)}) } - -// from https://github.com/fiatjaf/go-lnurl -func (bot *TipBot) HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error) { - var err error - var rawurl string - - if name, domain, ok := lnurl.ParseInternetIdentifier(rawlnurl); ok { - isOnion := strings.Index(domain, ".onion") == len(domain)-6 - rawurl = domain + "/.well-known/lnurlp/" + name - if isOnion { - rawurl = "http://" + rawurl - } else { - rawurl = "https://" + rawurl - } - } else if strings.HasPrefix(rawlnurl, "http") { - rawurl = rawlnurl - } else { - foundUrl, ok := lnurl.FindLNURLInText(rawlnurl) - if !ok { - return "", nil, - errors.New("invalid bech32-encoded lnurl: " + rawlnurl) - } - rawurl, err = lnurl.LNURLDecode(foundUrl) - if err != nil { - return "", nil, err - } - } - - parsed, err := url.Parse(rawurl) - if err != nil { - return rawurl, nil, err - } - - // query := parsed.Query() - - // switch query.Get("tag") { - // case "login": - // value, err := lnurl.HandleAuth(rawurl, parsed, query) - // return rawurl, value, err - // case "withdrawRequest": - // if value, ok := lnurl.HandleFastWithdraw(query); ok { - // return rawurl, value, nil - // } - // } - client, err := bot.GetHttpClient() - if err != nil { - return "", nil, err - } - resp, err := client.Get(rawurl) - if err != nil { - return rawurl, nil, err - } - - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - return rawurl, nil, err - } - - j := gjson.ParseBytes(b) - if j.Get("status").String() == "ERROR" { - return rawurl, nil, lnurl.LNURLErrorResponse{ - URL: parsed, - Reason: j.Get("reason").String(), - Status: "ERROR", - } - } - - switch j.Get("tag").String() { - // case "withdrawRequest": - // value, err := lnurl.HandleWithdraw(j) - // return rawurl, value, err - case "payRequest": - value, err := lnurl.HandlePay(j) - return rawurl, value, err - // case "channelRequest": - // value, err := lnurl.HandleChannel(j) - // return rawurl, value, err - default: - return rawurl, nil, errors.New("unknown response tag " + j.String()) - } -} diff --git a/translations/de.toml b/translations/de.toml index 92255e0c..0f389a0f 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -40,6 +40,7 @@ sendButtonMessage = """✅ Senden""" payButtonMessage = """✅ Bezahlen""" payReceiveButtonMessage = """💸 Bezahlen""" receiveButtonMessage = """✅ Empfangen""" +withdrawButtonMessage = """✅ Abheben""" cancelButtonMessage = """🚫 Abbrechen""" collectButtonMessage = """✅ Einsammeln""" nextButtonMessage = """Vor""" @@ -122,6 +123,10 @@ advancedMessage = """%s */faucet* 🚰 Erzeuge einen Zapfhahn: `/faucet ` */tipjar* 🍯 Erzeuge eine Spendendose: `/tipjar `""" +# GENERIC +lnurlEnterAmountRangeMessage = """⌨️ Gebe einen Betrag zwischen %d und %d sat ein.""" +lnurlEnterAmountMessage = """⌨️ Gebe einen Betrag ein.""" + # START startSettingWalletMessage = """🧮 Richte dein Wallet ein...""" @@ -226,12 +231,19 @@ lnurlPaymentFailed = """🚫 Zahlung fehlgeschlagen: %s""" lnurlInvalidAmountMessage = """🚫 Ungültiger Betrag.""" lnurlInvalidAmountRangeMessage = """🚫 Betrag muss zwischen %d und %d sat liegen.""" lnurlNoUsernameMessage = """🚫 Du musst einen Telegram Username anlegen, um per LNURL Zahlungen zu empfangen.""" -lnurlEnterAmountRangeMessage = """⌨️ Gebe einen Betrag zwischen %d und %d sat ein.""" lnurlHelpText = """📖 Ups, das hat nicht geklappt. %s *Befehl:* `/lnurl [betrag] ` *Beispiel:* `/lnurl LNURL1DP68GUR...`""" +# LNURL WITHDRAW + +confirmLnurlWithdrawMessage = """Möchtest du diese Abhebung tätigen?\n\n💸 Betrag: %d sat""" +lnurlPreparingWithdraw = """🧮 Bereite Abhebung vor...""" +lnurlWithdrawFailed = """🚫 Abhebung gescheitert.""" +lnurlWithdrawCancelled = """🚫 Abhebung abgebrochen.""" +lnurlWithdrawSuccess = """✅ Abhebung angefordert.""" + # LINK walletConnectMessage = """🔗 *Verbinde dein Wallet* diff --git a/translations/en.toml b/translations/en.toml index e8a83dba..e645e249 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -40,6 +40,7 @@ sendButtonMessage = """✅ Send""" payButtonMessage = """✅ Pay""" payReceiveButtonMessage = """💸 Pay""" receiveButtonMessage = """✅ Receive""" +withdrawButtonMessage = """✅ Withdraw""" cancelButtonMessage = """🚫 Cancel""" collectButtonMessage = """✅ Collect""" nextButtonMessage = """Next""" @@ -125,6 +126,10 @@ advancedMessage = """%s */faucet* 🚰 Create a faucet: `/faucet ` */tipjar* 🍯 Create a tipjar: `/tipjar `""" +# GENERIC +lnurlEnterAmountRangeMessage = """⌨️ Enter an amount between %d and %d sat.""" +lnurlEnterAmountMessage = """⌨️ Enter an amount.""" + # START startSettingWalletMessage = """🧮 Setting up your wallet...""" @@ -229,13 +234,19 @@ lnurlPaymentFailed = """🚫 Payment failed: %s""" lnurlInvalidAmountMessage = """🚫 Invalid amount.""" lnurlInvalidAmountRangeMessage = """🚫 Amount must be between %d and %d sat.""" lnurlNoUsernameMessage = """🚫 You need to set a Telegram username to receive payments via LNURL.""" -lnurlEnterAmountRangeMessage = """⌨️ Enter an amount between %d and %d sat.""" -lnurlEnterAmountMessage = """⌨️ Enter an amount.""" lnurlHelpText = """📖 Oops, that didn't work. %s *Usage:* `/lnurl [amount] ` *Example:* `/lnurl LNURL1DP68GUR...`""" +# LNURL WITHDRAW + +confirmLnurlWithdrawMessage = """Do you want to make this withdrawal?\n\n💸 Amount: %d sat""" +lnurlPreparingWithdraw = """🧮 Preparing withdrawal...""" +lnurlWithdrawFailed = """🚫 Withdrawal failed.""" +lnurlWithdrawCancelled = """🚫 Withdrawal cancelled.""" +lnurlWithdrawSuccess = """✅ Withdrawal requested.""" + # LINK walletConnectMessage = """🔗 *Link your wallet* diff --git a/translations/es.toml b/translations/es.toml index d9deda31..4418ca73 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -122,6 +122,10 @@ advancedMessage = """%s */faucet* 🚰 Crear un grifo: `/faucet ` */tipjar* 🍯 Crear un tipjar: `/tipjar `""" +# GENERIC +lnurlEnterAmountRangeMessage = """⌨️ Introduce un monto entre %d y %d sat.""" +lnurlEnterAmountMessage = """⌨️ Introduce un monto.""" + # START startSettingWalletMessage = """🧮 Configurando tu monedero...""" @@ -226,12 +230,19 @@ lnurlPaymentFailed = """🚫 Pago fallido: %s""" lnurlInvalidAmountMessage = """🚫 Monto inválido.""" lnurlInvalidAmountRangeMessage = """🚫 El monto debe estar entre %d y %d sat.""" lnurlNoUsernameMessage = """🚫 Tienes que establecer un nombre de usuario de Telegram para recibir pagos a través de LNURL.""" -lnurlEnterAmountRangeMessage = """⌨️ Introduce un monto entre %d y %d sat.""" lnurlHelpText = """📖 Oops, eso no funcionó. %s *Uso:* `/lnurl [monto] ` *Ejemplo:* `/lnurl LNURL1DP68GUR...`""" +# LNURL WITHDRAW + +confirmLnurlWithdrawMessage = """¿Desea realizar este retiro?\n💸 Monto: %d sat""" +lnurlPreparingWithdraw = """🧮 Preparando retiro...""" +lnurlWithdrawFailed = """🚫 Retiro fallido.""" +lnurlWithdrawCancelled = """🚫 Retiro cancelado.""" +lnurlWithdrawSuccess = """✅ Retiro solicitado.""" + # LINK walletConnectMessage = """🔗 *Enlaza tu monedero* diff --git a/translations/fr.toml b/translations/fr.toml index 0eda81f9..6daf58b5 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -122,6 +122,10 @@ advancedMessage = """%s */faucet* 🚰 Créer un faucet: `/faucet ` */tipjar* 🍯 Créer un tipjar: `/tipjar `""" +# GENERIC +lnurlEnterAmountRangeMessage = """⌨️ Choisissez un montant entre %d et %d sat.""" +lnurlEnterAmountMessage = """⌨️ Choisissez un montant.""" + # START startSettingWalletMessage = """🧮 Création de votre wallet...""" @@ -226,7 +230,6 @@ lnurlPaymentFailed = """🚫 Echec du paiement : %s""" lnurlInvalidAmountMessage = """🚫 Montant incorrect.""" lnurlInvalidAmountRangeMessage = """🚫 Le montant doit être entre %d et %d sat.""" lnurlNoUsernameMessage = """🚫 Vous devez avoir un nom d'utilisateur Telegram pour recevoir un paiement via LNURL.""" -lnurlEnterAmountRangeMessage = """⌨️ Choisissez un montant entre %d et %d sat.""" lnurlHelpText = """📖 Oops, cela n'a pas fonctionné. %s *Usage:* `/lnurl [montant] ` diff --git a/translations/id.toml b/translations/id.toml index 34a49a16..00401366 100644 --- a/translations/id.toml +++ b/translations/id.toml @@ -122,6 +122,9 @@ advancedMessage = """%s */faucet* 🚰 Membuat sebuah keran `/faucet ` */tipjar* 🍯 Create a tipjar: `/tipjar `""" +# GENERIC +lnurlEnterAmountRangeMessage = """⌨️ Masukkan jumlah diantara %d dan %d sat.""" + # START startSettingWalletMessage = """🧮 Sedang menyiapkan dompet mu...""" @@ -226,7 +229,6 @@ lnurlPaymentFailed = """🚫 Pembayaran gagal: %s""" lnurlInvalidAmountMessage = """🚫 Jumlah tidak benar.""" lnurlInvalidAmountRangeMessage = """🚫 Jumlah harus diantara %d dan %d sat.""" lnurlNoUsernameMessage = """🚫 Kamu harus mengatur nama pengguna Telegram untuk menerima pembayaran melalui LNURL.""" -lnurlEnterAmountRangeMessage = """⌨️ Masukkan jumlah diantara %d dan %d sat.""" lnurlHelpText = """📖 Waduh, itu tidak berhasil. %s *Usage:* `/lnurl [jumlah] ` diff --git a/translations/it.toml b/translations/it.toml index e768cf98..8a54b320 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -122,6 +122,10 @@ advancedMessage = """%s */faucet* 🚰 Crea una distribuzione: `/faucet ` */tipjar* 🍯 Crea un tipjar: `/tipjar `""" +# GENERIC +lnurlEnterAmountRangeMessage = """⌨️ Imposta un ammontare tra %d e %d sat.""" +lnurlEnterAmountMessage = """⌨️ Imposta un ammontare.""" + # START startSettingWalletMessage = """🧮 Sto creando il tuo wallet...""" @@ -226,7 +230,6 @@ lnurlPaymentFailed = """🚫 Pagamento non riuscito: %s""" lnurlInvalidAmountMessage = """🚫 Ammontare non valido.""" lnurlInvalidAmountRangeMessage = """🚫 L'ammontare deve essere compreso tra %d e %d sat.""" lnurlNoUsernameMessage = """🚫 Devi impostare un nome utente Telegram per ricevere pagamenti tramite un LNURL.""" -lnurlEnterAmountRangeMessage = """⌨️ Imposta un ammontare tra %d e %d sat.""" lnurlHelpText = """📖 Ops, non ha funzionato. %s *Sintassi:* `/lnurl [ammontare] ` diff --git a/translations/nl.toml b/translations/nl.toml index aa6596b4..b5d9967f 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -122,6 +122,10 @@ advancedMessage = """%s */faucet* 🚰 Maak een kraan: `/faucet ` */tipjar* 🍯 Maak een tipjar: `/tipjar `""" +# GENERIC +lnurlEnterAmountRangeMessage = """⌨️ Voer een bedrag in tussen %d en %d sat.""" +lnurlEnterAmountMessage = """⌨️ Voer een bedrag.""" + # START startSettingWalletMessage = """🧮 Uw wallet instellen...""" @@ -226,7 +230,6 @@ lnurlPaymentFailed = """🚫 Betaling mislukt: %s""" lnurlInvalidAmountMessage = """🚫 Ongeldig bedrag.""" lnurlInvalidAmountRangeMessage = """🚫 Bedrag moet liggen tussen %d en %d sat.""" lnurlNoUsernameMessage = """🚫 U moet een Telegram gebruikersnaam instellen om betalingen te ontvangen via LNURL.""" -lnurlEnterAmountRangeMessage = """⌨️ Voer een bedrag in tussen %d en %d sat.""" lnurlHelpText = """📖 Oeps, dat werkte niet. %s *Usage:* `/lnurl [bedrag] ` diff --git a/translations/pt-br.toml b/translations/pt-br.toml index 4933dfac..54db2c23 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -122,6 +122,10 @@ advancedMessage = """%s */faucet* 🚰 Criar uma torneira: `/faucet ` */tipjar* 🍯 Criar uma tipjar: `/tipjar `""" +# GENERIC +lnurlEnterAmountRangeMessage = """⌨️ Insira uma quantia entre %d e %d sat.""" +lnurlEnterAmountMessage = """⌨️ Insira uma quantia.""" + # START startSettingWalletMessage = """🧮 Preparando sua carteira...""" @@ -226,12 +230,19 @@ lnurlPaymentFailed = """🚫 Falha no pagamento: %s""" lnurlInvalidAmountMessage = """🚫 Quantia inválida.""" lnurlInvalidAmountRangeMessage = """🚫 A quantia deve estar entre %d e %d sat.""" lnurlNoUsernameMessage = """🚫 Você precisa configurar um nome de usuário de Telegram para receber pagamentos através do LNURL.""" -lnurlEnterAmountRangeMessage = """⌨️ Insira uma quantia entre %d e %d sat.""" lnurlHelpText = """📖 Opa, isso não funcionou. %s *Uso:* `/lnurl [quantidade] ` *Exemplo:* `/lnurl LNURL1DP68GUR...`""" +# LNURL WITHDRAW + +confirmLnurlWithdrawMessage = """Você quer fazer esse saque?\n💸 Quantia: %d sat""" +lnurlPreparingWithdraw = """🧮 Preparando o saque...""" +lnurlWithdrawFailed = """🚫 Saque falhou.""" +lnurlWithdrawCancelled = """🚫 Saque cancelado.""" +lnurlWithdrawSuccess = """✅ Saque solicitado.""" + # LINK walletConnectMessage = """🔗 *Vincule sua carteira* From 9290f55fa4eda1cd3a3b965c0c510894180586a7 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 15 Nov 2021 00:02:10 +0100 Subject: [PATCH 048/541] fix server (#130) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/lnurl/lnurl.go | 12 ++++-------- internal/telegram/lnurl.go | 4 ++-- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index d3575cdc..c2859380 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -41,17 +41,17 @@ func (w Server) handleLnUrl(writer http.ResponseWriter, request *http.Request) { } else { stringAmount := request.FormValue("amount") if stringAmount == "" { - NotFoundHandler(writer, fmt.Errorf("[serveLNURLpSecond] Form value 'amount' is not set")) + NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Form value 'amount' is not set")) return } amount, parseError := strconv.Atoi(stringAmount) if parseError != nil { - NotFoundHandler(writer, fmt.Errorf("[serveLNURLpSecond] Couldn't cast amount to int %v", parseError)) + NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't cast amount to int %v", parseError)) return } comment := request.FormValue("comment") if len(comment) > CommentAllowed { - NotFoundHandler(writer, fmt.Errorf("[serveLNURLpSecond] Comment is too long")) + NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Comment is too long")) return } response, err = w.serveLNURLpSecond(username, int64(amount), comment) @@ -85,10 +85,6 @@ func (w Server) serveLNURLpFirst(username string) (*lnurl.LNURLPayParams, error) return nil, err } metadata := w.metaData(username) - jsonMeta, err := json.Marshal(metadata) - if err != nil { - return nil, err - } return &lnurl.LNURLPayParams{ LNURLResponse: lnurl.LNURLResponse{Status: statusOk}, @@ -96,7 +92,7 @@ func (w Server) serveLNURLpFirst(username string) (*lnurl.LNURLPayParams, error) Callback: callbackURL.String(), MinSendable: minSendable, MaxSendable: MaxSendable, - EncodedMetadata: string(jsonMeta), + EncodedMetadata: metadata.Encode(), CommentAllowed: CommentAllowed, }, nil diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 3d4bea19..56b0114d 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -80,13 +80,13 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { switch params.(type) { case lnurl.LNURLPayParams: payParams := LnurlPayState{LNURLPayParams: params.(lnurl.LNURLPayParams)} - log.Infof("[lnurlHandler] %s", payParams.LNURLPayParams.Callback) + log.Infof("[LNURL-p] %s", payParams.LNURLPayParams.Callback) bot.tryDeleteMessage(statusMsg) bot.lnurlPayHandler(ctx, m, payParams) return case lnurl.LNURLWithdrawResponse: withdrawParams := LnurlWithdrawState{LNURLWithdrawResponse: params.(lnurl.LNURLWithdrawResponse)} - log.Infof("[lnurlHandler] %s", withdrawParams.LNURLWithdrawResponse.Callback) + log.Infof("[LNURL-w] %s", withdrawParams.LNURLWithdrawResponse.Callback) bot.tryDeleteMessage(statusMsg) bot.lnurlWithdrawHandler(ctx, m, withdrawParams) default: From 8c4adb4dd4fc2610a60236c71e7134a32087d783 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 15 Nov 2021 01:03:09 +0100 Subject: [PATCH 049/541] Fix lnurlpserver (#131) * fix server * go-lnurl v1.8.4 Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 25b2acdb..d3e334a1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.15 require ( github.com/BurntSushi/toml v0.3.1 github.com/eko/gocache v1.2.0 - github.com/fiatjaf/go-lnurl v1.8.3 + github.com/fiatjaf/go-lnurl v1.8.4 github.com/fiatjaf/ln-decodepay v1.1.0 github.com/gorilla/mux v1.8.0 github.com/imroc/req v0.3.0 diff --git a/go.sum b/go.sum index a7922cb7..77407854 100644 --- a/go.sum +++ b/go.sum @@ -139,6 +139,8 @@ github.com/fiatjaf/go-lnurl v1.4.0 h1:hVFEEJD2A9D6ojEcqLyD54CM2ZJ9Tzs2jNKw/GNq52 github.com/fiatjaf/go-lnurl v1.4.0/go.mod h1:BqA8WXAOzntF7Z3EkVO7DfP4y5rhWUmJ/Bu9KBke+rs= github.com/fiatjaf/go-lnurl v1.8.3 h1:ONQUQsXIZKkzrzax2SMUr5W0PreluhO4tct1JM0V/MA= github.com/fiatjaf/go-lnurl v1.8.3/go.mod h1:0PPdV2GFnJK97ztksEIwrcEUVTothAKJsmzqPphAQhs= +github.com/fiatjaf/go-lnurl v1.8.4 h1:uVH8833lVgvNqW3S0qr0I8VsIId5mVw0lE0KnEjgh/o= +github.com/fiatjaf/go-lnurl v1.8.4/go.mod h1:0PPdV2GFnJK97ztksEIwrcEUVTothAKJsmzqPphAQhs= github.com/fiatjaf/ln-decodepay v1.1.0 h1:HigjqNH+ApiO6gm7RV23jXNFuvwq+zgsWl4BJAfPWwE= github.com/fiatjaf/ln-decodepay v1.1.0/go.mod h1:2qdTT95b8Z4dfuxiZxXuJ1M7bQ9CrLieEA1DKC50q6s= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= From 88bbd4d2cf2c1ffc8527ee1939cf33810e2a4253 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 15 Nov 2021 01:12:52 +0100 Subject: [PATCH 050/541] Fix lnurlpserver (#132) * fix server * go-lnurl v1.8.4 * encode Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/lnurl/lnurl.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index c2859380..582cd393 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -3,7 +3,6 @@ package lnurl import ( "crypto/sha256" "encoding/hex" - "encoding/json" "fmt" "net/http" "net/url" @@ -193,11 +192,7 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) // descriptionHash is the SHA256 hash of the metadata func (w Server) descriptionHash(metadata lnurl.Metadata) (string, error) { - jsonMeta, err := json.Marshal(metadata) - if err != nil { - return "", err - } - hash := sha256.Sum256([]byte(string(jsonMeta))) + hash := sha256.Sum256([]byte(metadata.Encode())) hashString := hex.EncodeToString(hash[:]) return hashString, nil } From e1d858752f91d819ca964422c8a3843f035a4dd7 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Mon, 15 Nov 2021 10:18:57 +0100 Subject: [PATCH 051/541] emojis for logging (#133) * emojis for logging * emojis for logging --- internal/lnbits/webhook/webhook.go | 8 +++++--- internal/telegram/inline_faucet.go | 5 +++-- internal/telegram/inline_receive.go | 5 +++-- internal/telegram/inline_send.go | 5 +++-- internal/telegram/inline_tipjar.go | 5 +++-- internal/telegram/pay.go | 4 ++-- internal/telegram/send.go | 2 +- internal/telegram/start.go | 2 +- internal/telegram/tip.go | 2 +- 9 files changed, 22 insertions(+), 16 deletions(-) diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index 1749fa77..c3808007 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -3,18 +3,20 @@ package webhook import ( "encoding/json" "fmt" + "time" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/LightningTipBot/LightningTipBot/internal/telegram" - "time" log "github.com/sirupsen/logrus" "gorm.io/gorm" + "net/http" + "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/storage" - "net/http" "github.com/gorilla/mux" tb "gopkg.in/tucnak/telebot.v2" @@ -100,7 +102,7 @@ func (w Server) receive(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(400) return } - log.Infoln(fmt.Sprintf("[WebHook] User %s (%d) received invoice of %d sat.", user.Telegram.Username, user.Telegram.ID, depositEvent.Amount/1000)) + log.Infoln(fmt.Sprintf("[⚡️ WebHook] User %s (%d) received invoice of %d sat.", user.Telegram.Username, user.Telegram.ID, depositEvent.Amount/1000)) _, err = w.bot.Send(user.Telegram, fmt.Sprintf(i18n.Translate(user.Telegram.LanguageCode, "invoiceReceivedMessage"), depositEvent.Amount/1000)) if err != nil { log.Errorln(err) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 1cba4ff1..591f1caf 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -3,10 +3,11 @@ package telegram import ( "context" "fmt" - "github.com/eko/gocache/store" "strings" "time" + "github.com/eko/gocache/store" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/i18n" @@ -298,7 +299,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback return } - log.Infof("[faucet] faucet %s: %d sat from %s to %s ", inlineFaucet.ID, inlineFaucet.PerUserAmount, fromUserStr, toUserStr) + log.Infof("[💸 faucet] Faucet %s from %s to %s (%d sat).", inlineFaucet.ID, fromUserStr, toUserStr, inlineFaucet.PerUserAmount) inlineFaucet.NTaken += 1 inlineFaucet.To = append(inlineFaucet.To, to) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 5b50294a..fae66631 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -3,10 +3,11 @@ package telegram import ( "context" "fmt" - "github.com/eko/gocache/store" "strings" "time" + "github.com/eko/gocache/store" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" @@ -214,7 +215,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac return } - log.Infof("[acceptInlineReceiveHandler] %d sat from %s to %s", inlineReceive.Amount, fromUserStr, toUserStr) + log.Infof("[💸 inlineReceive] Send from %s to %s (%d sat).", fromUserStr, toUserStr, inlineReceive.Amount) inlineReceive.Message = fmt.Sprintf("%s", fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendUpdateMessageAccept"), inlineReceive.Amount, fromUserStrMd, toUserStrMd)) memo := inlineReceive.Memo diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 7eb3f15c..873af57b 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -3,10 +3,11 @@ package telegram import ( "context" "fmt" - "github.com/eko/gocache/store" "strings" "time" + "github.com/eko/gocache/store" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -229,7 +230,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) return } - log.Infof("[sendInline] %d sat from %s to %s", amount, fromUserStr, toUserStr) + log.Infof("[💸 sendInline] Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount) inlineSend.Message = fmt.Sprintf("%s", fmt.Sprintf(i18n.Translate(inlineSend.LanguageCode, "inlineSendUpdateMessageAccept"), amount, fromUserStrMd, toUserStrMd)) memo := inlineSend.Memo diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index d8219058..096bca3a 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -3,10 +3,11 @@ package telegram import ( "context" "fmt" - "github.com/eko/gocache/store" "strings" "time" + "github.com/eko/gocache/store" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -283,7 +284,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback return } - log.Infof("[tipjar] tipjar %s: %d sat from %s to %s ", inlineTipjar.ID, inlineTipjar.PerUserAmount, fromUserStr, toUserStr) + log.Infof("[💸 tipjar] Tipjar %s from %s to %s (%d sat).", inlineTipjar.ID, fromUserStr, toUserStr, inlineTipjar.PerUserAmount) inlineTipjar.NGiven += 1 inlineTipjar.From = append(inlineTipjar.From, from) inlineTipjar.GivenAmount = inlineTipjar.GivenAmount + inlineTipjar.PerUserAmount diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 48e6b08d..e8132624 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -112,7 +112,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmPayAppendMemo"), str.MarkdownEscape(bolt11.Description)) } - log.Printf("[/pay] User: %s, amount: %d sat.", userStr, amount) + log.Infof("[/pay] Invoice entered. User: %s, amount: %d sat.", userStr, amount) // object that holds all information about the send payment id := fmt.Sprintf("pay-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) @@ -227,7 +227,7 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { bot.trySendMessage(c.Sender, i18n.Translate(payData.LanguageCode, "invoicePaidMessage")) bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePublicPaidMessage"), userStr), &tb.ReplyMarkup{}) } - log.Printf("[pay] User %s paid invoice %s (%d sat)", userStr, payData.ID, payData.Amount) + log.Printf("[⚡️ pay] User %s paid invoice %s (%d sat)", userStr, payData.ID, payData.Amount) return } diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 671a885d..53b87eca 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -263,7 +263,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { } sendData.Inactivate(sendData, bot.Bunt) - log.Infof("[send] Transaction sent from %s to %s (%d sat).", fromUserStr, toUserStr, amount) + log.Infof("[💸 send] Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount) // notify to user bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) diff --git a/internal/telegram/start.go b/internal/telegram/start.go index ed5df463..0cb33dae 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -24,7 +24,7 @@ func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) { // ATTENTION: DO NOT CALL ANY HANDLER BEFORE THE WALLET IS CREATED // WILL RESULT IN AN ENDLESS LOOP OTHERWISE // bot.helpHandler(m) - log.Printf("[/start] User: %s (%d)\n", m.Sender.Username, m.Sender.ID) + log.Printf("[⭐️ /start] New user: %s (%d)\n", GetUserStr(m.Sender), m.Sender.ID) walletCreationMsg, err := bot.Telegram.Send(m.Sender, Translate(ctx, "startSettingWalletMessage")) user, err := bot.initWallet(m.Sender) if err != nil { diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index 61606e0b..45772446 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -121,7 +121,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { // update tooltip if necessary messageHasTip := tipTooltipHandler(m, bot, amount, to.Initialized) - log.Infof("[tip] Transaction sent from %s to %s (%d sat).", fromUserStr, toUserStr, amount) + log.Infof("[💸 tip] Tip from %s to %s (%d sat).", fromUserStr, toUserStr, amount) // notify users _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "tipSentMessage"), amount, toUserStrMd)) From 69b0dd5a4aba4ffab6c9da49d3e2757bba6da0aa Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 15 Nov 2021 11:42:35 +0100 Subject: [PATCH 052/541] Lnurl errors localized1 (#134) * verbose errors * add translations Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/lnurl.go | 12 ++++++------ translations/de.toml | 1 + translations/en.toml | 1 + translations/es.toml | 3 ++- translations/fr.toml | 1 + translations/id.toml | 10 ++++++++++ translations/it.toml | 1 + translations/nl.toml | 9 +++++++++ translations/pt-br.toml | 1 + 9 files changed, 32 insertions(+), 7 deletions(-) diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 56b0114d..7516b474 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -64,7 +64,7 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { } else if len(split) > 1 { lnurlSplit = split[1] } else { - bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not resolve LNURL.")) + bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), "Could not parse command.")) log.Warnln("[/lnurl] Could not parse command.") return } @@ -73,8 +73,8 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { // HandleLNURL by fiatjaf/go-lnurl _, params, err := lnurl.HandleLNURL(lnurlSplit) if err != nil { - bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), "could not resolve LNURL.")) - log.Errorln(err) + bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), err.Error())) + log.Warnf("[HandleLNURL] Error: %s", err.Error()) return } switch params.(type) { @@ -90,9 +90,9 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { bot.tryDeleteMessage(statusMsg) bot.lnurlWithdrawHandler(ctx, m, withdrawParams) default: - err := fmt.Errorf("invalid LNURL type.") - log.Errorln(err) - bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), err)) + err := fmt.Errorf("Invalid LNURL type.") + log.Warnln(err) + bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), err.Error())) // bot.trySendMessage(m.Sender, err.Error()) return } diff --git a/translations/de.toml b/translations/de.toml index 0f389a0f..6669cf13 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -126,6 +126,7 @@ advancedMessage = """%s # GENERIC lnurlEnterAmountRangeMessage = """⌨️ Gebe einen Betrag zwischen %d und %d sat ein.""" lnurlEnterAmountMessage = """⌨️ Gebe einen Betrag ein.""" +errorReasonMessage = """🚫 Fehler: %s""" # START diff --git a/translations/en.toml b/translations/en.toml index e645e249..8ec60860 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -129,6 +129,7 @@ advancedMessage = """%s # GENERIC lnurlEnterAmountRangeMessage = """⌨️ Enter an amount between %d and %d sat.""" lnurlEnterAmountMessage = """⌨️ Enter an amount.""" +errorReasonMessage = """🚫 Error: %s""" # START diff --git a/translations/es.toml b/translations/es.toml index 4418ca73..cdba24a3 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -125,6 +125,7 @@ advancedMessage = """%s # GENERIC lnurlEnterAmountRangeMessage = """⌨️ Introduce un monto entre %d y %d sat.""" lnurlEnterAmountMessage = """⌨️ Introduce un monto.""" +errorReasonMessage = """🚫 Error: %s""" # START @@ -237,7 +238,7 @@ lnurlHelpText = """📖 Oops, eso no funcionó. %s # LNURL WITHDRAW -confirmLnurlWithdrawMessage = """¿Desea realizar este retiro?\n💸 Monto: %d sat""" +confirmLnurlWithdrawMessage = """¿Desea realizar este retiro?\n\n💸 Monto: %d sat""" lnurlPreparingWithdraw = """🧮 Preparando retiro...""" lnurlWithdrawFailed = """🚫 Retiro fallido.""" lnurlWithdrawCancelled = """🚫 Retiro cancelado.""" diff --git a/translations/fr.toml b/translations/fr.toml index 6daf58b5..199e5606 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -125,6 +125,7 @@ advancedMessage = """%s # GENERIC lnurlEnterAmountRangeMessage = """⌨️ Choisissez un montant entre %d et %d sat.""" lnurlEnterAmountMessage = """⌨️ Choisissez un montant.""" +errorReasonMessage = """🚫 Erreur: %s""" # START diff --git a/translations/id.toml b/translations/id.toml index 00401366..5c3f0a03 100644 --- a/translations/id.toml +++ b/translations/id.toml @@ -124,6 +124,8 @@ advancedMessage = """%s # GENERIC lnurlEnterAmountRangeMessage = """⌨️ Masukkan jumlah diantara %d dan %d sat.""" +lnurlEnterAmountMessage = """⌨️ Masukkan jumlah.""" +errorReasonMessage = """🚫 Error: %s""" # START @@ -234,6 +236,14 @@ lnurlHelpText = """📖 Waduh, itu tidak berhasil. %s *Usage:* `/lnurl [jumlah] ` *Example:* `/lnurl LNURL1DP68GUR...`""" +# LNURL PENARIKAN + +confirmLnurlWithdrawMessage = """Apakah anda ingin melakukan penarikan ini?\n\n💸 Jumlah: %d sat""" +lnurlPreparingWithdraw = """🧮 Mempersiapkan penarikan...""" +lnurlWithdrawFailed = """🚫 Penarikan gagal.""" +lnurlWithdrawCancelled = """🚫 Penarikan dibatalkan.""" +lnurlWithdrawSuccess = """✅ Penarikan diajukan.""" + # LINK walletConnectMessage = """🔗 *Hubungkan dompet mu* diff --git a/translations/it.toml b/translations/it.toml index 8a54b320..7c5bc205 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -125,6 +125,7 @@ advancedMessage = """%s # GENERIC lnurlEnterAmountRangeMessage = """⌨️ Imposta un ammontare tra %d e %d sat.""" lnurlEnterAmountMessage = """⌨️ Imposta un ammontare.""" +errorReasonMessage = """🚫 Errore: %s""" # START diff --git a/translations/nl.toml b/translations/nl.toml index b5d9967f..cd78119a 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -125,6 +125,7 @@ advancedMessage = """%s # GENERIC lnurlEnterAmountRangeMessage = """⌨️ Voer een bedrag in tussen %d en %d sat.""" lnurlEnterAmountMessage = """⌨️ Voer een bedrag.""" +errorReasonMessage = """🚫 Fout: %s""" # START @@ -235,6 +236,14 @@ lnurlHelpText = """📖 Oeps, dat werkte niet. %s *Usage:* `/lnurl [bedrag] ` *Example:* `/lnurl LNURL1DP68GUR...`""" +# LNURL WITHDRAW + +confirmLnurlWithdrawMessage = """Wilt u deze opname maken?💸 Bedrag: %d sat""" +lnurlPreparingWithdraw = """🧮 Opname aan het voorbereiden...""" +lnurlWithdrawFailed = """🚫 Opname mislukt.""" +lnurlWithdrawCancelled = """🚫 Opname geannuleerd.""" +lnurlWithdrawSuccess = """✅ Opname aangevraagd.""" + # LINK walletConnectMessage = """🔗 *Koppel uw wallet* diff --git a/translations/pt-br.toml b/translations/pt-br.toml index 54db2c23..c34efc17 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -125,6 +125,7 @@ advancedMessage = """%s # GENERIC lnurlEnterAmountRangeMessage = """⌨️ Insira uma quantia entre %d e %d sat.""" lnurlEnterAmountMessage = """⌨️ Insira uma quantia.""" +errorReasonMessage = """🚫 Erro: %s""" # START From 1f25cc6ded435a3c1e833bfcaf400b14beb24347 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 15 Nov 2021 16:41:34 +0100 Subject: [PATCH 053/541] Add russian translation (#136) * Add Russian translation * turn on ru.toml * load gently Co-authored-by: Filiph Protsenko Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/i18n/localize.go | 1 + internal/lnbits/webhook/webhook.go | 2 +- translations/ru.toml | 339 +++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 translations/ru.toml diff --git a/internal/i18n/localize.go b/internal/i18n/localize.go index a70c25b5..a8c88f76 100644 --- a/internal/i18n/localize.go +++ b/internal/i18n/localize.go @@ -25,6 +25,7 @@ func RegisterLanguages() *i18n.Bundle { bundle.LoadMessageFile("translations/pt-br.toml") bundle.LoadMessageFile("translations/tr.toml") bundle.LoadMessageFile("translations/id.toml") + bundle.LoadMessageFile("translations/ru.toml") return bundle } func Translate(languageCode string, MessgeID string) string { diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index c3808007..4d268150 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -102,7 +102,7 @@ func (w Server) receive(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(400) return } - log.Infoln(fmt.Sprintf("[⚡️ WebHook] User %s (%d) received invoice of %d sat.", user.Telegram.Username, user.Telegram.ID, depositEvent.Amount/1000)) + log.Infoln(fmt.Sprintf("[⚡️ WebHook] User %s (%d) received invoice of %d sat.", telegram.GetUserStr(user.Telegram), user.Telegram.ID, depositEvent.Amount/1000)) _, err = w.bot.Send(user.Telegram, fmt.Sprintf(i18n.Translate(user.Telegram.LanguageCode, "invoiceReceivedMessage"), depositEvent.Amount/1000)) if err != nil { log.Errorln(err) diff --git a/translations/ru.toml b/translations/ru.toml new file mode 100644 index 00000000..da0d1c1f --- /dev/null +++ b/translations/ru.toml @@ -0,0 +1,339 @@ +# COMMANDS + +helpCommandStr = """справка""" +basicsCommandStr = """основы""" +tipCommandStr = """tip""" +balanceCommandStr = """баланс""" +sendCommandStr = """отправить""" +invoiceCommandStr = """инвойс""" +payCommandStr = """оплатить""" +donateCommandStr = """пожертвовать""" +advancedCommandStr = """детально""" +transactionsCommandStr = """транзакции""" +logCommandStr = """лог""" +listCommandStr = """список""" + +linkCommandStr = """link""" +lnurlCommandStr = """lnurl""" +faucetCommandStr = """faucet""" + +tipjarCommandStr = """tipjar""" +receiveCommandStr = """получить""" +hideCommandStr = """спрятать""" +volcanoCommandStr = """volcano""" +showCommandStr = """показать""" +optionsCommandStr = """опции""" +settingsCommandStr = """настройки""" +saveCommandStr = """сохранить""" +deleteCommandStr = """удалить""" +infoCommandStr = """инфо""" + +# NOTIFICATIONS + +cantDoThatMessage = """Вы не можете это сделать.""" +cantClickMessage = """Вы не можете нажать на эту кнопку.""" +balanceTooLowMessage = """Недостаточно средств.""" + +# BUTTONS + +sendButtonMessage = """✅ Отправить""" +payButtonMessage = """✅ Оплатить""" +payReceiveButtonMessage = """💸 Оплатить""" +receiveButtonMessage = """✅ Получить""" +withdrawButtonMessage = """✅ Вывести""" +cancelButtonMessage = """🚫 Отмена""" +collectButtonMessage = """✅ Забрать""" +nextButtonMessage = """Вперед""" +backButtonMessage = """Назад""" +acceptButtonMessage = """Принять""" +denyButtonMessage = """Запретить""" +tipButtonMessage = """Tip""" +revealButtonMessage = """Развернуть""" +showButtonMessage = """Показать""" +hideButtonMessage = """Спрятать""" +joinButtonMessage = """Присоединиться""" +optionsButtonMessage = """Опции""" +settingsButtonMessage = """Настройки""" +saveButtonMessage = """Сохранить""" +deleteButtonMessage = """Удалить""" +infoButtonMessage = """Инфо""" + +cancelButtonEmoji = """🚫""" +payButtonEmoji = """💸""" + +# HELP + +helpMessage = """⚡️ *Wallet* +_Этот бот представляет собой кошелек Bitcoin Lightning, который может отправлять Tip (небольшие сумму в Сатоши) через Telegram. Чтобы отправить Tip, добавьте бота в групповой чат. Основной единицей чаевых являются сатоши (sat). 100 000 000 сат = 1 биткоин. Введите 📚 /basics для получения дополнительной информации._ + +❤️ *Donate* +_Этот бот не взимает комиссии, но его работа обходится в сатоши. Если вам нравится бот, пожалуйста, поддержите этот проект пожертвованием. Чтобы сделать пожертвование, воспользуйтесь командой_ `/donate 1000` + +%s + +⚙️ *Команды* +*/tip* 🏅 Ответьте на сообщение чтобы отправить Tip (небольшую сумму в Сатоши): `/tip []` +*/balance* 👑 Проверить баланс: `/balance` +*/send* 💸 Отправить ссредства пользователю: `/send <количество> @ или @ln.tips []` +*/invoice* ⚡️ Получить через Lightning: `/invoice <количество> []` +*/pay* ⚡️ Оплатить через Lightning: `/pay <инвойс>` +*/donate* ❤️ Отправить пожертвование проекту: `/donate 1000` +*/advanced* 🤖 Продвинутые возможности. +*/help* 📖 Прочитайте справку.""" + +infoHelpMessage = """ℹ️ *Info*""" +infoYourLightningAddress = """Ваш адрес в системе Lightning - `%s`""" + +basicsMessage = """🧡 *Биткоин* +_Биткоин - это валюта интернета. Она не имеет ограничений и децентрализована, у нее нет хозяев и контролирующих органов. Биткойн - это разумные деньги, которые быстрее, надежнее и всеохватнее, чем традиционная финансовая система._ + +🧮 *Экономика* +_Наименьшей единицей Биткоина являются "Сатоши" (sat), а 100 000 000 sat = 1 Биткойн. В мире существует только 21 миллион биткоинов. Стоимость Биткойна выражаемая в фиатной валюте может меняться ежедневно. Однако, если вы живете по стандарту Биткойна, то 1 sat всегда будет равен 1 sat._ + +⚡️ *The Lightning Network* +_The Lightning Network - это платежный протокол, который обеспечивает быстрые и дешевые платежи Биткоинами, практически не требующие энергии. Именно благодаря ему Биткоин доступен миллиардам людей по всему миру._ + +📲 *Lightning-кошельки* +_Ваши средства на этом боте могут быть отправлены на любой другой Lightning-кошелек и наоборот. Рекомендуемые Lightning-кошельки для вашего телефона:_ [Phoenix](https://phoenix.acinq.co/)_,_ [Breez](https://breez.technology/)_,_ [Muun](https://muun.com/)_ (не требующие обслуживания), или_ [Wallet of Satoshi](https://www.walletofsatoshi.com/) _(простой)_. + +📄 *Open Source* +_Этот бот является бесплатным и_ [с открытым исходным кодом](https://github.com/LightningTipBot/LightningTipBot) _. Вы можете запустить его на своем компьютере и использовать в своем сообществе._ + +✈️ *Telegram* +_Добавьте этого бота в чат вашей группы Telegram для /tip сообщений. Если вы сделаете бота администратором группы, он также будет очищать команды для поддержания чистоты в чате._ + +🏛 *Условия* +_Мы не являемся хранителями ваших средств. Мы будем действовать в ваших интересах, но мы также понимаем, что ситуация без поддержки стандарта KYC - немного сложная, до тех пор, пока мы не придумаем что-нибудь. Любая сумма, которую вы загрузите на свой кошелек, будет считаться пожертвованием. Не отдавайте нам все свои деньги. Имейте в виду, что этот бот находится в стадии бета-разработки. Используйте на свой страх и риск._ + +❤️ *Пожертвования* +_Этот бот не взимает комиссии, но его работа обходится в сатоши. Если вам нравится бот, пожалуйста, поддержите этот проект пожертвованием. Чтобы сделать пожертвование, воспользуйтесь командой_ `/donate 1000`""" + +helpNoUsernameMessage = """👋 Пожалуйста, установите имя пользователя в Telegram.""" + +advancedMessage = """%s + +👉 *Команды* +*send* 💸 Отправить Сатоши в чате: `%s send <количество> [] []` +*receive* 🏅 Запросить оплату: `... receive <количество> [] []` +*faucet* 🚰 Создать криптораздачу: `... faucet <ёмкость> <на_пользователя> []` +*tipjar* 🍯 Создать копилку: `... tipjar <ёмкость> <на_пользователя> []` + +📖 Вы можете использовать команды в любом чате, даже в личных беседах. Подождите секунду после ввода команды и *щелкните* результат, не нажимайте Enter.. + +⚙️ *Продвинутые команды* +*/link* 🔗 Link your wallet to [BlueWallet](https://bluewallet.io/) or [Zeus](https://zeusln.app/) +*/lnurl* Получить или оплатить через ⚡️Lnurl: `/lnurl` or `/lnurl ` +*/faucet* 🚰 Создать криптораздачу: `/faucet <ёмкость> <на_пользователя>` +*/tipjar* 🍯 Создать копилку: `/tipjar <ёмкость> <на_пользователя>`""" + +# GENERIC +lnurlEnterAmountRangeMessage = """⌨️ Введите количество между %d и %d sat.""" +lnurlEnterAmountMessage = """⌨️ Введите количество.""" +errorReasonMessage = """🚫 Ошибка: %s""" + +# START + +startSettingWalletMessage = """🧮 Настраиваю кошелёк для вас...""" +startWalletCreatedMessage = """🧮 Кошелёк создан.""" +startWalletReadyMessage = """✅ *Кошелёк готов к использованию.*""" +startWalletErrorMessage = """🚫 Ошибка инициализации кошелька. Попробуйте позже.""" +startNoUsernameMessage = """☝️ Похоже, что у вас еще нет @имени_пользователя в Telegram. Это нормально, оно не обязательно для использования этого бота. Однако, чтобы лучше использовать свой кошелек, установите имя пользователя в настройках Telegram. Затем введите /balance, чтобы бот мог обновить свои данные о вас.""" + +# BALANCE + +balanceMessage = """👑 *Ваш баланс:* %d sat""" +balanceErrorMessage = """🚫 Не удалось получить ваш баланс. Пожалуйста, повторите попытку позже.""" + +# TIP + +tipDidYouReplyMessage = """Вы ответили на сообщение чтобы отправить Сатоши? Чтобы ответить на любое сообщение, нажмите правой кнопкой мыши -> Ответить на компьютере или проведите пальцем по сообщению на телефоне. Если вы хотите отправить Сатоши непосредственно другому пользователю, используйте команду /send.""" +tipInviteGroupMessage = """ℹ️ Кстати, вы можете пригласить этого бота в любую группу, чтобы можно было отправлять чаевые там.""" +tipEnterAmountMessage = """Ввели ли вы сумму?""" +tipValidAmountMessage = """Ввели ли вы правильную сумму?""" +tipYourselfMessage = """📖 Вы не можете отправлять самому себе.""" +tipSentMessage = """💸 %d sat отправлено %s.""" +tipReceivedMessage = """🏅 %s отправил(а) вам %d sat.""" +tipErrorMessage = """🚫 Отправка не удалась.""" +tipUndefinedErrorMsg = """пожалуйста попробуйте позжа.""" +tipHelpText = """📖 Упс, это не сработало. %s + +*Использование:* `/tip <количество> []` +*Пример:* `/tip 1000 Спасибо!`""" + +# SEND + +sendValidAmountMessage = """Ввели ли вы правильную сумму?""" +sendUserHasNoWalletMessage = """🚫 Пользователь %s не создал кошелёк.""" +sendSentMessage = """💸 %d sat отправлено %s.""" +sendPublicSentMessage = """💸 %d sat отправил(а) %s для %s.""" +sendReceivedMessage = """🏅 %s отправил(а) вам %d sat.""" +sendErrorMessage = """🚫 Неуспешная отправка.""" +confirmSendMessage = """Вы хотите отправить Сатоши %s?\n\n💸 Amount: %d sat""" +confirmSendAppendMemo = """\n✉️ %s""" +sendCancelledMessage = """🚫 Отправка отменена.""" +errorTryLaterMessage = """🚫 Ошибка. Пожалуйста попробуйте позже.""" +sendSyntaxErrorMessage = """Вы ввели сумму и получателя? Вы можете использовать команду /send для отправки либо пользователям Telegram, например %s, либо на адрес Lightning, например LightningTipBot@ln.tips.""" +sendHelpText = """📖 Упс, это не сработало. %s + +*Использование:* `/send <количество> <пользователь> []` +*Пример:* `/send 1000 @LightningTipBot I just like the bot ❤️` +*Пример:* `/send 1234 LightningTipBot@ln.tips`""" + +# INVOICE + +invoiceReceivedMessage = """⚡️ Вы получили %d sat.""" +invoiceEnterAmountMessage = """Ввели ли вы сумму?""" +invoiceValidAmountMessage = """Ввели ли вы правильную сумму?""" +invoiceHelpText = """📖 Упс, это не сработало. %s + +*Использование:* `/invoice <количество> []` +*Пример:* `/invoice 1000 Спасибо!`""" + +# PAY + +paymentCancelledMessage = """🚫 Платёж отменён.""" +invoicePaidMessage = """⚡️ Платёж отправлен.""" +invoicePublicPaidMessage = """⚡️ %s отправил(а) платёж.""" +invalidInvoiceHelpMessage = """Вы ввели существующий счет в Lightning? Попробуйте /send, если вы хотите отправить пользователю Telegram или по адресу Lightning.""" +invoiceNoAmountMessage = """🚫 Невозможно оплатить инвойс без указания суммы.""" +insufficientFundsMessage = """🚫 Недостаточно средств. У вас есть %d sat, но вам нужно не менее %d sat.""" +feeReserveMessage = """⚠️ Отправка всего вашего баланса может провалиться из-за сетевых сборов. Если это не удается, попробуйте отправить немного меньше.""" +invoicePaymentFailedMessage = """🚫 Платёж не прошёл: %s""" +invoiceUndefinedErrorMessage = """Невозможно оплатить счёт.""" +confirmPayInvoiceMessage = """Хотите ли вы отправить этот платеж?\n\n💸 Amount: %d sat""" +confirmPayAppendMemo = """\n✉️ %s""" +payHelpText = """📖 Упс, это не сработало. %s + +*Использование:* `/pay <инвойс>` +*Пример:* `/pay lnbc20n1psscehd...`""" + +# DONATE + +donationSuccess = """🙏 Спасибо за ваше пожертвование.""" +donationErrorMessage = """🚫 О нет! Пожертвование не удалось.""" +donationProgressMessage = """🧮 Подготовка вашего пожертвования...""" +donationFailedMessage = """🚫 Пожертвование не удалось: %s""" +donateEnterAmountMessage = """Ввели ли вы сумму?""" +donateValidAmountMessage = """Ввели ли вы правильную сумму?""" +donateHelpText = """📖 Упс, это не сработало. %s + +*Использование:* `/donate <количество>` +*Пример:* `/donate 1000`""" + +# PHOTO + +photoQrNotRecognizedMessage = """🚫 Не удалось распознать счет Lightning или LNURL. Попробуйте отцентрировать QR-код, обрезать фотографию или увеличить масштаб.""" +photoQrRecognizedMessage = """✅ QR код: +`%s`""" + +# LNURL + +lnurlReceiveInfoText = """👇 Вы можете использовать этот статический LNURL для приема платежей.""" +lnurlResolvingUrlMessage = """🧮 Уточнение адреса...""" +lnurlGettingUserMessage = """🧮 Подготовка платежа...""" +lnurlPaymentFailed = """🚫 Платеж не прошел: %s""" +lnurlInvalidAmountMessage = """🚫 Неверная сумма.""" +lnurlInvalidAmountRangeMessage = """🚫 Сумма должна быть от %d до %d sat.""" +lnurlNoUsernameMessage = """🚫 Вам нужно установить имя пользователя Telegram для получения платежей через LNURL.""" +lnurlHelpText = """📖 Упс, это не сработало. %s + +*Использование:* `/lnurl [количество] ` +*Пример:* `/lnurl LNURL1DP68GUR...`""" + +# LNURL WITHDRAW + +confirmLnurlWithdrawMessage = """Вы хотите вывести средства?\n\n💸 Сумма: %d сб""" +lnurlPreparingWithdraw = """🧮 Подготовка к выводу средств...""" +lnurlWithdrawFailed = """🚫 Вывод средств не удался.""" +lnurlWithdrawCancelled = """🚫 Вывод средств отменен.""" +lnurlWithdrawSuccess = """✅ Вывод средств запрошен.""" + +# LINK + +walletConnectMessage = """🔗 *Подключение кошелька* + +⚠️ Никогда и никому не сообщайте URL или QR-код, иначе они смогут получить доступ к вашим средствам. + +- *BlueWallet:* Нажмите *New wallet*, *Import wallet*, *Scan or import a file*, и отсканируйте QR-код. +- *Zeus:* Скопируйте URL ниже, нажмите *Add a new node*, *Import* (the URL), *Save Node Config*.""" +couldNotLinkMessage = """🚫 Не удалось связать ваш кошелек. Пожалуйста, повторите попытку позже.""" + +# FAUCET + +inlineQueryFaucetTitle = """🚰 Создать криптораздачу.""" +inlineQueryFaucetDescription = """Использование: @%s faucet """ +inlineResultFaucetTitle = """🚰 Создать %d sat криптораздачу.""" +inlineResultFaucetDescription = """👉 Нажмите здесь, чтобы создать криптораздачу в этом чате.""" + +inlineFaucetMessage = """Нажмите ✅, чтобы забрать %d sat из этой криптораздачи. + +🚰 Осталось: %d/%d sat (отдано %d/%d пользователям) +%s""" +inlineFaucetEndedMessage = """🚰 Криптораздача завершена 🍺\n\n🏅 %d sat отдано %d пользователям.""" +inlineFaucetAppendMemo = """\n✉️ %s""" +inlineFaucetCreateWalletMessage = """Напишите %s 👈 чтобы управлять своим кошельком.""" +inlineFaucetCancelledMessage = """🚫 Криптораздача отменена.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 Количество пользователей не является делителем емкости.""" +inlineFaucetInvalidAmountMessage = """🚫 Неверное количество.""" +inlineFaucetSentMessage = """🚰 %d sat отправлено %s.""" +inlineFaucetReceivedMessage = """🚰 %s отправил вам %d sat.""" +inlineFaucetHelpFaucetInGroup = """Создайте криптораздачу в группе с ботом внутри или используйте 👉 команду (/advanced для справки).""" +inlineFaucetHelpText = """📖 Упс, это не сработало. %s + +*Использование:* `/faucet <ёмкость> <на_пользователя>` +*Пример:* `/faucet 210 21`""" + +# INLINE SEND + +inlineQuerySendTitle = """💸 Отправьте платеж в чат.""" +inlineQuerySendDescription = """Использование: @%s send <количество> [<пользователь>] []""" +inlineResultSendTitle = """💸 Отправить %d sat.""" +inlineResultSendDescription = """👉 Нажмите, чтобы отправить %d сат в этот чат.""" + +inlineSendMessage = """Нажмите ✅ для получения платежа от %s.\n\n💸 Сумма: %d sat""" +inlineSendAppendMemo = """\n✉️ %s""" +inlineSendUpdateMessageAccept = """💸 %d sat отправлено с %s на %s.""" +inlineSendCreateWalletMessage = """Напишите %s 👈 чтобы управлять своим кошельком.""" +sendYourselfMessage = """📖 Вы не можете платить себе.""" +inlineSendFailedMessage = """🚫 Отправка не удалась.""" +inlineSendInvalidAmountMessage = """🚫 Сумма должна быть больше 0.""" +inlineSendBalanceLowMessage = """🚫 Ваш баланс слишком низкий.""" + +# INLINE RECEIVE + +inlineQueryReceiveTitle = """🏅 Запросить платёж в чате.""" +inlineQueryReceiveDescription = """Использование: @%s receive <количество> [<пользователь>] []""" +inlineResultReceiveTitle = """🏅 Получить %d sat.""" +inlineResultReceiveDescription = """👉 Нажмите, чтобы запросить выплату в размере %d sat.""" + +inlineReceiveMessage = """Нажмите 💸 чтобы отправить Сатоши %s.\n\n💸 Количество: %d sat""" +inlineReceiveAppendMemo = """\n✉️ %s""" +inlineReceiveUpdateMessageAccept = """💸 %d sat отправлено от %s, получатель - %s.""" +inlineReceiveCreateWalletMessage = """Напишите %s 👈 чтобы управлять своим кошельком.""" +inlineReceiveYourselfMessage = """📖 Вы не можете платить себе.""" +inlineReceiveFailedMessage = """🚫 Получение не удалось.""" +inlineReceiveCancelledMessage = """🚫 Получение отменено.""" + +# TIPJAR + +inlineQueryTipjarTitle = """🍯 Создать копилку.""" +inlineQueryTipjarDescription = """Использование: @%s tipjar <ёмкость> <на_пользователя>""" +inlineResultTipjarTitle = """🍯 Создать копилку на %d sat .""" +inlineResultTipjarDescription = """👉 Нажмите здесь, чтобы создать копилку в этом чате.""" + +inlineTipjarMessage = """Нажмите 💸 чтобы *отправить %d sat* в эту копилку, созданную %s. + +🙏 Накоплено: *%d*/%d sat (от %d пользователей) +%s""" +inlineTipjarEndedMessage = """🍯 %s's копилка заполнена ⭐️\n\n🏅 %d sat отправи(л/ли) %d пользовате(ль/ли).""" +inlineTipjarAppendMemo = """\n✉️ %s""" +inlineTipjarCancelledMessage = """🚫 Копилка отозвана.""" +inlineTipjarInvalidPeruserAmountMessage = """🚫 Количество пользователей не является делителем емкости.""" +inlineTipjarInvalidAmountMessage = """🚫 Отправка не удалась.""" +inlineTipjarSentMessage = """🍯 %d sat отправлено в пользу %s.""" +inlineTipjarReceivedMessage = """🍯 %s отправил вам %d sat.""" +inlineTipjarHelpTipjarInGroup = """Создайте копилку в группе с ботом внутри или используйте 👉 команду (/advanced для справки).""" +inlineTipjarHelpText = """📖 Oops, that didn't work. %s + +*Использование:* `/tipjar <ёмкость> <на_пользователя>` +*Пример:* `/tipjar 210 21`""" From 3f26f0b44f3a977e7b4aa1bd219f1fe0af388459 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 16 Nov 2021 20:40:19 +0100 Subject: [PATCH 054/541] copy handlelnurl (#137) * copy handlelnurl * rephrase error Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/lnurl.go | 108 ++++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 2 deletions(-) diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 7516b474..3444a776 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -3,12 +3,15 @@ package telegram import ( "bytes" "context" + "errors" "fmt" + "io/ioutil" "net/http" "net/url" "strings" "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/tidwall/gjson" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" lnurl "github.com/fiatjaf/go-lnurl" @@ -71,7 +74,7 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { // assume payment // HandleLNURL by fiatjaf/go-lnurl - _, params, err := lnurl.HandleLNURL(lnurlSplit) + _, params, err := bot.HandleLNURL(lnurlSplit) if err != nil { bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), err.Error())) log.Warnf("[HandleLNURL] Error: %s", err.Error()) @@ -90,7 +93,9 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { bot.tryDeleteMessage(statusMsg) bot.lnurlWithdrawHandler(ctx, m, withdrawParams) default: - err := fmt.Errorf("Invalid LNURL type.") + if err == nil { + err = errors.New("Invalid LNURL type.") + } log.Warnln(err) bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), err.Error())) // bot.trySendMessage(m.Sender, err.Error()) @@ -151,3 +156,102 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { // send the lnurl data to user bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lnurlEncode)}) } + +// fiatjaf/go-lnurl 1.8.4 with proxy +func (bot TipBot) HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error) { + var err error + var rawurl string + + if name, domain, ok := lnurl.ParseInternetIdentifier(rawlnurl); ok { + isOnion := strings.Index(domain, ".onion") == len(domain)-6 + rawurl = domain + "/.well-known/lnurlp/" + name + if isOnion { + rawurl = "http://" + rawurl + } else { + rawurl = "https://" + rawurl + } + } else if strings.HasPrefix(rawlnurl, "http") { + rawurl = rawlnurl + } else if strings.HasPrefix(rawlnurl, "lnurlp://") || + strings.HasPrefix(rawlnurl, "lnurlw://") || + strings.HasPrefix(rawlnurl, "lnurla://") || + strings.HasPrefix(rawlnurl, "keyauth://") { + + scheme := "https:" + if strings.Contains(rawurl, ".onion/") || strings.HasSuffix(rawurl, ".onion") { + scheme = "http:" + } + location := strings.SplitN(rawlnurl, ":", 2)[1] + rawurl = scheme + location + } else { + lnurl_str, ok := lnurl.FindLNURLInText(rawlnurl) + if !ok { + return "", nil, + errors.New("invalid bech32-encoded lnurl: " + rawlnurl) + } + rawurl, err = lnurl.LNURLDecode(lnurl_str) + if err != nil { + return "", nil, err + } + } + + parsed, err := url.Parse(rawurl) + if err != nil { + return rawurl, nil, err + } + + query := parsed.Query() + + switch query.Get("tag") { + case "login": + value, err := lnurl.HandleAuth(rawurl, parsed, query) + return rawurl, value, err + case "withdrawRequest": + if value, ok := lnurl.HandleFastWithdraw(query); ok { + return rawurl, value, nil + } + } + + // // original withouth proxy + // resp, err := http.Get(rawurl) + // if err != nil { + // return rawurl, nil, err + // } + + client, err := bot.GetHttpClient() + if err != nil { + return "", nil, err + } + resp, err := client.Get(rawurl) + if err != nil { + return rawurl, nil, err + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return rawurl, nil, err + } + + j := gjson.ParseBytes(b) + if j.Get("status").String() == "ERROR" { + return rawurl, nil, lnurl.LNURLErrorResponse{ + URL: parsed, + Reason: j.Get("reason").String(), + Status: "ERROR", + } + } + + switch j.Get("tag").String() { + case "withdrawRequest": + value, err := lnurl.HandleWithdraw(b) + return rawurl, value, err + case "payRequest": + value, err := lnurl.HandlePay(b) + return rawurl, value, err + // case "channelRequest": + // value, err := lnurl.HandleChannel(b) + // return rawurl, value, err + default: + return rawurl, nil, errors.New("unkown LNURL response.") + } +} From 8f569ed4c3da3b94b53e4e5ccddcd8f74c0b1049 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 17 Nov 2021 19:51:51 +0100 Subject: [PATCH 055/541] send input (#138) * send input * remove message limiter * write invoice into tx db * do not delete tip twice * hide link after 60 sedondw * refactor button text update * remove print * translations enter user * add tr * no error on invalid user input * printing fix Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/lnbits/types.go | 2 + internal/telegram/amounts.go | 4 +- internal/telegram/database.go | 7 ++- internal/telegram/link.go | 13 +++-- internal/telegram/lnurl-withdraw.go | 58 +++++++++------------ internal/telegram/lnurl.go | 5 +- internal/telegram/pay.go | 6 +-- internal/telegram/send.go | 28 ++++++---- internal/telegram/text.go | 81 +++++++++++++++++++++++++++++ internal/telegram/tip.go | 4 +- internal/telegram/transaction.go | 42 ++++++++------- translations/de.toml | 4 +- translations/en.toml | 8 +-- translations/es.toml | 7 +-- translations/fr.toml | 7 +-- translations/id.toml | 7 +-- translations/it.toml | 7 +-- translations/nl.toml | 7 +-- translations/pt-br.toml | 7 +-- translations/ru.toml | 5 +- translations/tr.toml | 7 ++- 21 files changed, 216 insertions(+), 100 deletions(-) diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index c0398ca5..7f19df3e 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -34,6 +34,8 @@ const ( UserStateConfirmLNURLPay UserEnterAmount UserHasEnteredAmount + UserEnterUser + UserHasEnteredUser ) type UserStateKey int diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 6728b0ba..0662cbae 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -105,9 +105,9 @@ func (bot *TipBot) askForAmount(ctx context.Context, id string, eventType string return } SetUserState(user, bot, lnbits.UserEnterAmount, string(stateDataJson)) - askAmountText := Translate(ctx, "lnurlEnterAmountMessage") + askAmountText := Translate(ctx, "enterAmountMessage") if amountMin > 0 && amountMax >= amountMin { - askAmountText = fmt.Sprintf(Translate(ctx, "lnurlEnterAmountRangeMessage"), enterAmountStateData.AmountMin/1000, enterAmountStateData.AmountMax/1000) + askAmountText = fmt.Sprintf(Translate(ctx, "enterAmountRangeMessage"), enterAmountStateData.AmountMin/1000, enterAmountStateData.AmountMax/1000) } // Let the user enter an amount and return bot.trySendMessage(user.Telegram, askAmountText, tb.ForceReply) diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 02aeab6b..f83c568e 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -2,11 +2,12 @@ package telegram import ( "fmt" - "github.com/eko/gocache/store" "reflect" "strconv" "time" + "github.com/eko/gocache/store" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/database" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -79,6 +80,10 @@ func AutoMigration() (db *gorm.DB, txLogger *gorm.DB) { func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.User, error) { toUserDb := &lnbits.User{} + // return error if username is too long + if len(toUserStrWithoutAt) > 100 { + return nil, fmt.Errorf("[GetUserByTelegramUsername] Telegram username is too long: %s..", toUserStrWithoutAt[:100]) + } tx := bot.Database.Where("telegram_username = ? COLLATE NOCASE", toUserStrWithoutAt).First(toUserDb) if tx.Error != nil || toUserDb.Wallet == nil { err := tx.Error diff --git a/internal/telegram/link.go b/internal/telegram/link.go index 4ce7d91f..8e275290 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "time" "github.com/LightningTipBot/LightningTipBot/internal" @@ -12,7 +13,7 @@ import ( tb "gopkg.in/tucnak/telebot.v2" ) -func (bot TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { if internal.Configuration.Lnbits.LnbitsPublicUrl == "" { bot.trySendMessage(m.Sender, Translate(ctx, "couldNotLinkMessage")) return @@ -38,6 +39,12 @@ func (bot TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { return } - // send the invoice data to user - bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lndhubUrl)}) + // send the link to the user + linkmsg := bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lndhubUrl)}) + time.Sleep(time.Second * 60) + bot.tryDeleteMessage(linkmsg) + bot.trySendMessage(m.Sender, Translate(ctx, "linkHiddenMessage")) + // auto delete the message + // NewMessage(linkmsg, WithDuration(time.Second*time.Duration(internal.Configuration.Telegram.MessageDisposeDuration), bot)) + } diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 2990d19b..7c5855cb 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -38,6 +38,18 @@ type LnurlWithdrawState struct { Message string `json:"message"` } +func (bot *TipBot) editSingleButton(ctx context.Context, m *tb.Message, message string, button string) { + bot.tryEditMessage( + m, + message, + &tb.ReplyMarkup{ + InlineKeyboard: [][]tb.InlineButton{ + {tb.InlineButton{Text: button}}, + }, + }, + ) +} + // lnurlWithdrawHandler is invoked when the first lnurl response was a lnurl-withdraw response // at this point, the user hans't necessarily entered an amount yet func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, withdrawParams LnurlWithdrawState) { @@ -68,6 +80,13 @@ func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, with ResetUserState(user, bot) return } + + // if no amount is entered, and if only one amount is possible, we use it + if amount_err != nil && LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable == LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable { + amount = int(LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable / 1000) + amount_err = nil + } + // set also amount in the state of the user LnurlWithdrawState.Amount = amount * 1000 // save as mSat @@ -204,15 +223,7 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { ResetUserState(user, bot) // update button text - bot.tryEditMessage( - c.Message, - lnurlWithdrawState.Message, - &tb.ReplyMarkup{ - InlineKeyboard: [][]tb.InlineButton{ - {tb.InlineButton{Text: i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlPreparingWithdraw")}}, - }, - }, - ) + bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlPreparingWithdraw")) // lnurlWithdrawState loaded client, err := bot.GetHttpClient() @@ -248,19 +259,20 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { // add amount to query string qs.Set("pr", invoice.PaymentRequest) qs.Set("k1", lnurlWithdrawState.LNURLWithdrawResponse.K1) - callbackUrl.RawQuery = qs.Encode() res, err := client.Get(callbackUrl.String()) if err != nil || res.StatusCode >= 300 { log.Errorf("[lnurlWithdrawHandlerWithdraw] Failed.") - bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) return } body, err := ioutil.ReadAll(res.Body) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) - bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) return } @@ -268,31 +280,13 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { var response2 lnurl.LNURLResponse json.Unmarshal(body, &response2) if response2.Status == "OK" { - // bot.trySendMessage(c.Sender, Translate(ctx, "lnurlWithdrawSuccess")) // update button text - bot.tryEditMessage( - c.Message, - lnurlWithdrawState.Message, - &tb.ReplyMarkup{ - InlineKeyboard: [][]tb.InlineButton{ - {tb.InlineButton{Text: i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawSuccess")}}, - }, - }, - ) + bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawSuccess")) } else { log.Errorf("[lnurlWithdrawHandlerWithdraw] LNURLWithdraw failed.") - // bot.trySendMessage(c.Sender, Translate(ctx, "lnurlWithdrawFailed")) // update button text - bot.tryEditMessage( - c.Message, - lnurlWithdrawState.Message, - &tb.ReplyMarkup{ - InlineKeyboard: [][]tb.InlineButton{ - {tb.InlineButton{Text: i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawFailed")}}, - }, - }, - ) + bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawFailed")) return } diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 3444a776..36693d63 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -226,6 +226,9 @@ func (bot TipBot) HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error if err != nil { return rawurl, nil, err } + if resp.StatusCode >= 300 { + return rawurl, nil, errors.New("HTTP error: " + resp.Status) + } b, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -252,6 +255,6 @@ func (bot TipBot) HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error // value, err := lnurl.HandleChannel(b) // return rawurl, value, err default: - return rawurl, nil, errors.New("unkown LNURL response.") + return rawurl, nil, errors.New("Unkown LNURL response.") } } diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index e8132624..1f82fff1 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -85,23 +85,21 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { return } - statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlGettingUserMessage")) // check user balance first balance, err := bot.GetUserBalance(user) if err != nil { NewMessage(m, WithDuration(0, bot)) errmsg := fmt.Sprintf("[/pay] Error: Could not get user balance: %s", err) log.Errorln(errmsg) - bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + bot.trySendMessage(m.Sender, Translate(ctx, "errorTryLaterMessage")) return } if amount > balance { NewMessage(m, WithDuration(0, bot)) - bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "insufficientFundsMessage"), balance, amount)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "insufficientFundsMessage"), balance, amount)) return } - bot.tryDeleteMessage(statusMsg) // send warning that the invoice might fail due to missing fee reserve if float64(amount) > float64(balance)*0.99 { bot.trySendMessage(m.Sender, Translate(ctx, "feeReserveMessage")) diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 53b87eca..e88921bd 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -68,11 +68,11 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { return } - if ok, errstr := bot.SendCheckSyntax(ctx, m); !ok { - bot.trySendMessage(m.Sender, helpSendUsage(ctx, errstr)) - NewMessage(m, WithDuration(0, bot)) - return - } + // if ok, errstr := bot.SendCheckSyntax(ctx, m); !ok { + // bot.trySendMessage(m.Sender, helpSendUsage(ctx, errstr)) + // NewMessage(m, WithDuration(0, bot)) + // return + // } // get send amount, returns 0 if no amount is given amount, err := decodeAmountFromCommand(m.Text) @@ -98,8 +98,14 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { } } - // todo: this error might have been overwritten by the functions above - // we should only check for a valid amount here, instead of error and amount + // is a user given? + arg, err = getArgumentFromCommand(m.Text, 1) + if err != nil && m.Chat.Type == tb.ChatPrivate { + bot.askForUser(ctx, "", "CreateSendState", m.Text) + return + } + + // is an amount given? amount, err = decodeAmountFromCommand(m.Text) if (err != nil || amount < 1) && m.Chat.Type == tb.ChatPrivate { bot.askForAmount(ctx, "", "CreateSendState", 0, 0, m.Text) @@ -133,8 +139,8 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { log.Errorln(err.Error()) return } - toUserStrMention = "@" + toUserStrWithoutAt toUserStrWithoutAt = strings.TrimPrefix(toUserStrWithoutAt, "@") + toUserStrMention = "@" + toUserStrWithoutAt } err = bot.parseCmdDonHandler(ctx, m) @@ -145,7 +151,11 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { toUserDb, err := GetUserByTelegramUsername(toUserStrWithoutAt, *bot) if err != nil { NewMessage(m, WithDuration(0, bot)) - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), toUserStrMention)) + // cut username if it's too long + if len(toUserStrMention) > 100 { + toUserStrMention = toUserStrMention[:100] + } + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), str.MarkdownEscape(toUserStrMention))) return } diff --git a/internal/telegram/text.go b/internal/telegram/text.go index 6a4958bd..9f1d94ac 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -2,10 +2,13 @@ package telegram import ( "context" + "encoding/json" + "fmt" "strings" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" + log "github.com/sirupsen/logrus" tb "gopkg.in/tucnak/telebot.v2" ) @@ -36,8 +39,86 @@ func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { // 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 user.StateKey == lnbits.UserEnterUser { + bot.enterUserHandler(ctx, m) + } + +} + +type EnterUserStateData struct { + ID string `json:"ID"` // holds the ID of the tx object in bunt db + Type string `json:"Type"` // holds type of the tx in bunt db (needed for type checking) + Amount int64 `json:"Amount"` // holds the amount entered by the user mSat + AmountMin int64 `json:"AmountMin"` // holds the minimum amount that needs to be entered mSat + AmountMax int64 `json:"AmountMax"` // holds the maximum amount that needs to be entered mSat + OiringalCommand string `json:"OiringalCommand"` // hold the originally entered command for evtl later use +} + +func (bot *TipBot) askForUser(ctx context.Context, id string, eventType string, originalCommand string) (enterUserStateData *EnterUserStateData, err error) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + enterUserStateData = &EnterUserStateData{ + ID: id, + Type: eventType, + OiringalCommand: originalCommand, + } + // set LNURLPayParams in the state of the user + stateDataJson, err := json.Marshal(enterUserStateData) + if err != nil { + log.Errorln(err) + return + } + SetUserState(user, bot, lnbits.UserEnterUser, string(stateDataJson)) + // Let the user enter a user and return + bot.trySendMessage(user.Telegram, Translate(ctx, "enterUserMessage"), tb.ForceReply) + return +} +// enterAmountHandler is invoked in anyTextHandler when the user needs to enter an amount +// the amount is then stored as an entry in the user's stateKey in the user database +// any other handler that relies on this, needs to load the resulting amount from the database +func (bot *TipBot) enterUserHandler(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + + if !(user.StateKey == lnbits.UserEnterUser) { + ResetUserState(user, bot) + return // errors.New("user state does not match"), 0 + } + if len(m.Text) < 4 || strings.HasPrefix(m.Text, "/") { + ResetUserState(user, bot) + return + } + + var EnterUserStateData EnterUserStateData + err := json.Unmarshal([]byte(user.StateData), &EnterUserStateData) + if err != nil { + log.Errorf("[EnterUserHandler] %s", err.Error()) + ResetUserState(user, bot) + return + } + + userstr := m.Text + + // find out which type the object in bunt has waiting for an amount + // we stored this in the EnterAmountStateData before + switch EnterUserStateData.Type { + case "CreateSendState": + m.Text = fmt.Sprintf("/send %s", userstr) + SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") + bot.sendHandler(ctx, m) + return + default: + ResetUserState(user, bot) + return + } } diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index 45772446..5bab0bdb 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -31,8 +31,6 @@ func TipCheckSyntax(ctx context.Context, m *tb.Message) (bool, string) { } func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { - // delete the tip message after a few seconds, this is default behaviour - defer NewMessage(m, WithDuration(time.Second*time.Duration(internal.Configuration.Telegram.MessageDisposeDuration), bot)) // check and print all commands bot.anyTextHandler(ctx, m) user := LoadUser(ctx) @@ -139,5 +137,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if len(tipMemo) > 0 { bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", str.MarkdownEscape(tipMemo))) } + // delete the tip message after a few seconds, this is default behaviour + NewMessage(m, WithDuration(time.Second*time.Duration(internal.Configuration.Telegram.MessageDisposeDuration), bot)) return } diff --git a/internal/telegram/transaction.go b/internal/telegram/transaction.go index 4b8c6753..c1f5e7fb 100644 --- a/internal/telegram/transaction.go +++ b/internal/telegram/transaction.go @@ -11,25 +11,26 @@ import ( ) type Transaction struct { - ID uint `gorm:"primarykey"` - Time time.Time `json:"time"` - Bot *TipBot `gorm:"-"` - From *lnbits.User `json:"from" gorm:"-"` - To *lnbits.User `json:"to" gorm:"-"` - FromId int `json:"from_id" ` - ToId int `json:"to_id" ` - FromUser string `json:"from_user"` - ToUser string `json:"to_user"` - Type string `json:"type"` - Amount int `json:"amount"` - ChatID int64 `json:"chat_id"` - ChatName string `json:"chat_name"` - Memo string `json:"memo"` - Success bool `json:"success"` - FromWallet string `json:"from_wallet"` - ToWallet string `json:"to_wallet"` - FromLNbitsID string `json:"from_lnbits"` - ToLNbitsID string `json:"to_lnbits"` + ID uint `gorm:"primarykey"` + Time time.Time `json:"time"` + Bot *TipBot `gorm:"-"` + From *lnbits.User `json:"from" gorm:"-"` + To *lnbits.User `json:"to" gorm:"-"` + FromId int `json:"from_id" ` + ToId int `json:"to_id" ` + FromUser string `json:"from_user"` + ToUser string `json:"to_user"` + Type string `json:"type"` + Amount int `json:"amount"` + ChatID int64 `json:"chat_id"` + ChatName string `json:"chat_name"` + Memo string `json:"memo"` + Success bool `json:"success"` + FromWallet string `json:"from_wallet"` + ToWallet string `json:"to_wallet"` + FromLNbitsID string `json:"from_lnbits"` + ToLNbitsID string `json:"to_lnbits"` + Invoice lnbits.BitInvoice `gorm:"embedded;embeddedPrefix:invoice_"` } type TransactionOption func(t *Transaction) @@ -86,7 +87,7 @@ func (t *Transaction) Send() (success bool, err error) { // save transaction to db tx := t.Bot.logger.Save(t) if tx.Error != nil { - errMsg := fmt.Sprintf("Error: Could not log transaction: %s", err) + errMsg := fmt.Sprintf("Error: Could not log transaction: %s", err.Error()) log.Errorln(errMsg) } @@ -128,6 +129,7 @@ func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits log.Errorln(errmsg) return false, err } + t.Invoice = invoice // pay invoice _, err = from.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoice.PaymentRequest}, bot.Client) if err != nil { diff --git a/translations/de.toml b/translations/de.toml index 6669cf13..93d1d7a0 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -124,8 +124,8 @@ advancedMessage = """%s */tipjar* 🍯 Erzeuge eine Spendendose: `/tipjar `""" # GENERIC -lnurlEnterAmountRangeMessage = """⌨️ Gebe einen Betrag zwischen %d und %d sat ein.""" -lnurlEnterAmountMessage = """⌨️ Gebe einen Betrag ein.""" +enterAmountRangeMessage = """⌨️ Gebe einen Betrag zwischen %d und %d sat ein.""" +enterAmountMessage = """⌨️ Gebe einen Betrag ein.""" errorReasonMessage = """🚫 Fehler: %s""" # START diff --git a/translations/en.toml b/translations/en.toml index 8ec60860..bd76728a 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -127,8 +127,9 @@ advancedMessage = """%s */tipjar* 🍯 Create a tipjar: `/tipjar `""" # GENERIC -lnurlEnterAmountRangeMessage = """⌨️ Enter an amount between %d and %d sat.""" -lnurlEnterAmountMessage = """⌨️ Enter an amount.""" +enterAmountRangeMessage = """💯 Enter an amount between %d and %d sat.""" +enterAmountMessage = """💯 Enter an amount.""" +enterUserMessage = """👤 Enter a user.""" errorReasonMessage = """🚫 Error: %s""" # START @@ -256,7 +257,8 @@ walletConnectMessage = """🔗 *Link your wallet* - *BlueWallet:* Press *New wallet*, *Import wallet*, *Scan or import a file*, and scan the QR code. - *Zeus:* Copy the URL below, press *Add a new node*, *Import* (the URL), *Save Node Config*.""" -couldNotLinkMessage = """🚫 Couldn't link your wallet. Please try again later.""" +couldNotLinkMessage = """🚫 Couldn't link your wallet. Please try again later.""" +linkHiddenMessage = """🔍 Link hidden. Enter /link to see it again.""" # FAUCET diff --git a/translations/es.toml b/translations/es.toml index cdba24a3..f7c5586e 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -123,9 +123,10 @@ advancedMessage = """%s */tipjar* 🍯 Crear un tipjar: `/tipjar `""" # GENERIC -lnurlEnterAmountRangeMessage = """⌨️ Introduce un monto entre %d y %d sat.""" -lnurlEnterAmountMessage = """⌨️ Introduce un monto.""" -errorReasonMessage = """🚫 Error: %s""" +enterAmountRangeMessage = """💯 Introduce un monto entre %d y %d sat.""" +enterAmountMessage = """💯 Introduce un monto.""" +enterUserMessage = """👤 Introduce un usuario.""" +errorReasonMessage = """🚫 Error: %s""" # START diff --git a/translations/fr.toml b/translations/fr.toml index 199e5606..05ad2245 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -123,9 +123,10 @@ advancedMessage = """%s */tipjar* 🍯 Créer un tipjar: `/tipjar `""" # GENERIC -lnurlEnterAmountRangeMessage = """⌨️ Choisissez un montant entre %d et %d sat.""" -lnurlEnterAmountMessage = """⌨️ Choisissez un montant.""" -errorReasonMessage = """🚫 Erreur: %s""" +enterAmountRangeMessage = """💯 Choisissez un montant entre %d et %d sat.""" +enterAmountMessage = """💯 Choisissez un montant.""" +enterUserMessage = """👤 Choisissez un utilisateur.""" +errorReasonMessage = """🚫 Erreur: %s""" # START diff --git a/translations/id.toml b/translations/id.toml index 5c3f0a03..dfa88e89 100644 --- a/translations/id.toml +++ b/translations/id.toml @@ -123,9 +123,10 @@ advancedMessage = """%s */tipjar* 🍯 Create a tipjar: `/tipjar `""" # GENERIC -lnurlEnterAmountRangeMessage = """⌨️ Masukkan jumlah diantara %d dan %d sat.""" -lnurlEnterAmountMessage = """⌨️ Masukkan jumlah.""" -errorReasonMessage = """🚫 Error: %s""" +enterAmountRangeMessage = """💯 Masukkan jumlah diantara %d dan %d sat.""" +enterAmountMessage = """💯 Masukkan jumlah.""" +enterUserMessage = """👤 Masukkan user.""" +errorReasonMessage = """🚫 Error: %s""" # START diff --git a/translations/it.toml b/translations/it.toml index 7c5bc205..3f5358c1 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -123,9 +123,10 @@ advancedMessage = """%s */tipjar* 🍯 Crea un tipjar: `/tipjar `""" # GENERIC -lnurlEnterAmountRangeMessage = """⌨️ Imposta un ammontare tra %d e %d sat.""" -lnurlEnterAmountMessage = """⌨️ Imposta un ammontare.""" -errorReasonMessage = """🚫 Errore: %s""" +enterAmountRangeMessage = """💯 Imposta un ammontare tra %d e %d sat.""" +enterAmountMessage = """💯 Imposta un ammontare.""" +enterUserMessage = """👤 Imposta un utente.""" +errorReasonMessage = """🚫 Errore: %s""" # START diff --git a/translations/nl.toml b/translations/nl.toml index cd78119a..34fca649 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -123,9 +123,10 @@ advancedMessage = """%s */tipjar* 🍯 Maak een tipjar: `/tipjar `""" # GENERIC -lnurlEnterAmountRangeMessage = """⌨️ Voer een bedrag in tussen %d en %d sat.""" -lnurlEnterAmountMessage = """⌨️ Voer een bedrag.""" -errorReasonMessage = """🚫 Fout: %s""" +enterAmountRangeMessage = """💯 Voer een bedrag in tussen %d en %d sat.""" +enterAmountMessage = """💯 Voer een bedrag.""" +enterUserMessage = """👤 Voer een gebruiker.""" +errorReasonMessage = """🚫 Fout: %s""" # START diff --git a/translations/pt-br.toml b/translations/pt-br.toml index c34efc17..a4f3c059 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -123,9 +123,10 @@ advancedMessage = """%s */tipjar* 🍯 Criar uma tipjar: `/tipjar `""" # GENERIC -lnurlEnterAmountRangeMessage = """⌨️ Insira uma quantia entre %d e %d sat.""" -lnurlEnterAmountMessage = """⌨️ Insira uma quantia.""" -errorReasonMessage = """🚫 Erro: %s""" +enterAmountRangeMessage = """💯 Insira uma quantia entre %d e %d sat.""" +enterAmountMessage = """💯 Insira uma quantia.""" +enterUserMessage = """👤 Insira um usuário.""" +errorReasonMessage = """🚫 Erro: %s""" # START diff --git a/translations/ru.toml b/translations/ru.toml index da0d1c1f..80d836a1 100644 --- a/translations/ru.toml +++ b/translations/ru.toml @@ -127,8 +127,9 @@ advancedMessage = """%s */tipjar* 🍯 Создать копилку: `/tipjar <ёмкость> <на_пользователя>`""" # GENERIC -lnurlEnterAmountRangeMessage = """⌨️ Введите количество между %d и %d sat.""" -lnurlEnterAmountMessage = """⌨️ Введите количество.""" +enterAmountRangeMessage = """💯 Введите количество между %d и %d sat.""" +enterAmountMessage = """💯 Введите количество.""" +enterUserMessage = """👤 введите пользователя.""" errorReasonMessage = """🚫 Ошибка: %s""" # START diff --git a/translations/tr.toml b/translations/tr.toml index 98e68ef7..1512505f 100644 --- a/translations/tr.toml +++ b/translations/tr.toml @@ -122,6 +122,12 @@ advancedMessage = """%s */faucet* 🚰 Bir fıçı oluştur: `/faucet ` */tipjar* 🍯 Bir tipjar oluştur: `/tipjar `""" +# GENERIC +enterAmountRangeMessage = """💯 %d ve %d sat arasında bir miktar gir.""" +enterAmountMessage = """💯 Bir miktar gir.""" +enterUserMessage = """👤 Bir kullanıcı gir.""" +errorReasonMessage = """🚫 Hata: %s""" + # START startSettingWalletMessage = """🧮 Cüzdanın hazırlanıyor…""" @@ -226,7 +232,6 @@ lnurlPaymentFailed = """🚫 Ödeme başarısız: %s""" lnurlInvalidAmountMessage = """🚫 Geçersiz miktar.""" lnurlInvalidAmountRangeMessage = """🚫 Miktar %d ve %d sat arasında olmalı.""" lnurlNoUsernameMessage = """🚫 LNURL ödemesi almak için bir Telegram kullanıcı ismi seçmelisin.""" -lnurlEnterAmountRangeMessage = """⌨️ %d ve %d sat arasında bir miktar gir.""" lnurlHelpText = """📖 Hoppalaa… olmadı. %s *Komut:* `/lnurl [miktar] ` From 488fab7f9f2b6a6cc2b3dfcecf3ed690f093f17f Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 18 Nov 2021 22:04:29 +0100 Subject: [PATCH 056/541] expressive errors (#139) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/lnurl-withdraw.go | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 7c5855cb..12b60df3 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -151,6 +151,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa lnurlWithdrawState = fn.(*LnurlWithdrawState) default: log.Errorf("[lnurlWithdrawHandlerWithdraw] invalid type") + bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) + return } confirmText := fmt.Sprintf(Translate(ctx, "confirmLnurlWithdrawMessage"), lnurlWithdrawState.Amount/1000) @@ -192,6 +194,7 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { lnurlWithdrawState = fn.(*LnurlWithdrawState) default: log.Errorf("[confirmWithdrawHandler] invalid type") + return } // onnly the correct user can press if lnurlWithdrawState.From.Telegram.ID != c.Sender.ID { @@ -225,17 +228,11 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { // update button text bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlPreparingWithdraw")) - // lnurlWithdrawState loaded - client, err := bot.GetHttpClient() - if err != nil { - log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) - bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) - return - } callbackUrl, err := url.Parse(lnurlWithdrawState.LNURLWithdrawResponse.Callback) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) - bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) return } @@ -251,6 +248,7 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { if err != nil { errmsg := fmt.Sprintf("[lnurlWithdrawHandlerWithdraw] Could not create an invoice: %s", err) log.Errorln(errmsg) + bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) return } lnurlWithdrawState.Invoice = invoice @@ -261,6 +259,14 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { qs.Set("k1", lnurlWithdrawState.LNURLWithdrawResponse.K1) callbackUrl.RawQuery = qs.Encode() + // lnurlWithdrawState loaded + client, err := bot.GetHttpClient() + if err != nil { + log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) + // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) + return + } res, err := client.Get(callbackUrl.String()) if err != nil || res.StatusCode >= 300 { log.Errorf("[lnurlWithdrawHandlerWithdraw] Failed.") From 6e6c9e02a433270e981392b0073c6dd9af4a3f22 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 19 Nov 2021 21:23:39 +0100 Subject: [PATCH 057/541] donate message (#140) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/donate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/donate.go b/internal/telegram/donate.go index 2592f29c..00606db4 100644 --- a/internal/telegram/donate.go +++ b/internal/telegram/donate.go @@ -48,7 +48,7 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { } // command is valid - msg := bot.trySendMessage(user.Telegram, Translate(ctx, "donationProgressMessage")) + msg := bot.trySendMessage(m.Chat, Translate(ctx, "donationProgressMessage")) // get invoice resp, err := http.Get(fmt.Sprintf(donationEndpoint, amount, GetUserStr(user.Telegram), GetUserStr(bot.Telegram.Me))) if err != nil { From 3891c70344f623b5ccf58240a671b75dda1ab50e Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 26 Nov 2021 13:36:02 +0100 Subject: [PATCH 058/541] banned prefix filter (#141) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/link.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/telegram/link.go b/internal/telegram/link.go index 8e275290..c5bb1ad9 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "strings" "time" "github.com/LightningTipBot/LightningTipBot/internal" @@ -29,6 +30,11 @@ func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { fromUser := LoadUser(ctx) bot.trySendMessage(m.Sender, Translate(ctx, "walletConnectMessage")) + // do not respond to banned users + if strings.HasPrefix(fromUser.Wallet.Adminkey, "banned") || strings.HasPrefix(fromUser.Wallet.Adminkey, "_") { + return + } + lndhubUrl := fmt.Sprintf("lndhub://admin:%s@%slndhub/ext/", fromUser.Wallet.Adminkey, internal.Configuration.Lnbits.LnbitsPublicUrl) // create qr code From 4ba892908bdb355be767e577576c505593fa49c5 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 1 Dec 2021 05:32:53 +0100 Subject: [PATCH 059/541] update telebot (#144) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d3e334a1..749d3a23 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/tidwall/buntdb v1.2.7 github.com/tidwall/gjson v1.10.2 golang.org/x/text v0.3.5 - gopkg.in/tucnak/telebot.v2 v2.3.5 + gopkg.in/tucnak/telebot.v2 v2.4.1 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.12 ) diff --git a/go.sum b/go.sum index 77407854..5e614542 100644 --- a/go.sum +++ b/go.sum @@ -728,6 +728,8 @@ gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= gopkg.in/tucnak/telebot.v2 v2.3.5 h1:TdMJTlG8kvepsvZdy/gPeYEBdwKdwFFjH1AQTua9BOU= gopkg.in/tucnak/telebot.v2 v2.3.5/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= +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= From 4a9a77d5f1c66dc9f7ca822bb58c00f04565f1b3 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 1 Dec 2021 06:05:20 +0100 Subject: [PATCH 060/541] Telebot update (#145) * update telebot * test message Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/bot.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 81c9bb9b..6ba95737 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -2,10 +2,11 @@ package telegram import ( "fmt" - "github.com/eko/gocache/store" "sync" "time" + "github.com/eko/gocache/store" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -53,7 +54,7 @@ func NewBot() TipBot { func newTelegramBot() *tb.Bot { tgb, err := tb.NewBot(tb.Settings{ Token: internal.Configuration.Telegram.ApiKey, - Poller: &tb.LongPoller{Timeout: 60 * time.Second}, + Poller: &tb.LongPoller{Timeout: 60 * time.Second, AllowedUpdates: []string{"message"}}, ParseMode: tb.ModeMarkdown, }) if err != nil { From 79112717cf5fa0860a10241b8e121cc9722e179e Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 1 Dec 2021 07:08:46 +0100 Subject: [PATCH 061/541] Telebot update (#146) * update telebot * test message * revert Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> From eeb9d5224efc2629a0831235c147cfbe4da63a16 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 1 Dec 2021 07:11:49 +0100 Subject: [PATCH 062/541] revert (#148) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/bot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 6ba95737..9d223614 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -54,7 +54,7 @@ func NewBot() TipBot { func newTelegramBot() *tb.Bot { tgb, err := tb.NewBot(tb.Settings{ Token: internal.Configuration.Telegram.ApiKey, - Poller: &tb.LongPoller{Timeout: 60 * time.Second, AllowedUpdates: []string{"message"}}, + Poller: &tb.LongPoller{Timeout: 60 * time.Second}, ParseMode: tb.ModeMarkdown, }) if err != nil { From 7c8ba7895d2b2c993524173ab0f7d8c34eaafa92 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 1 Dec 2021 08:52:47 +0100 Subject: [PATCH 063/541] telebot fork user ID int64 (#149) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- go.mod | 4 +++- go.sum | 12 ++++++++---- internal/lnbits/types.go | 2 +- internal/lnbits/webhook/webhook.go | 2 +- internal/telegram/amounts.go | 2 +- internal/telegram/balance.go | 2 +- internal/telegram/bot.go | 4 ++-- internal/telegram/database.go | 6 +++--- internal/telegram/donate.go | 2 +- internal/telegram/handler.go | 2 +- internal/telegram/help.go | 2 +- internal/telegram/inline_faucet.go | 2 +- internal/telegram/inline_query.go | 7 ++++--- internal/telegram/inline_receive.go | 2 +- internal/telegram/inline_send.go | 2 +- internal/telegram/inline_tipjar.go | 2 +- internal/telegram/intercept/callback.go | 2 +- internal/telegram/intercept/message.go | 3 ++- internal/telegram/intercept/query.go | 3 ++- internal/telegram/interceptor.go | 2 +- internal/telegram/invoice.go | 2 +- internal/telegram/link.go | 2 +- internal/telegram/lnurl-pay.go | 2 +- internal/telegram/lnurl-withdraw.go | 2 +- internal/telegram/lnurl.go | 2 +- internal/telegram/message.go | 2 +- internal/telegram/pay.go | 2 +- internal/telegram/photo.go | 2 +- internal/telegram/send.go | 4 ++-- internal/telegram/start.go | 4 ++-- internal/telegram/telegram.go | 2 +- internal/telegram/text.go | 2 +- internal/telegram/tip.go | 2 +- internal/telegram/tooltip.go | 4 ++-- internal/telegram/tooltip_test.go | 2 +- internal/telegram/transaction.go | 6 +++--- internal/telegram/users.go | 5 +++-- 37 files changed, 61 insertions(+), 51 deletions(-) diff --git a/go.mod b/go.mod index 749d3a23..d5a69f78 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,9 @@ require ( github.com/tidwall/buntdb v1.2.7 github.com/tidwall/gjson v1.10.2 golang.org/x/text v0.3.5 - gopkg.in/tucnak/telebot.v2 v2.4.1 + gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.12 ) + +// replace gopkg.in/lightningtipbot/telebot.v2 => ../telebot diff --git a/go.sum b/go.sum index 5e614542..09295538 100644 --- a/go.sum +++ b/go.sum @@ -716,6 +716,14 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/lightningtipbot/telebot.v2 v2.3.5 h1:TdMJTlG8kvepsvZdy/gPeYEBdwKdwFFjH1AQTua9BOU= +gopkg.in/lightningtipbot/telebot.v2 v2.3.5/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= +gopkg.in/lightningtipbot/telebot.v2 v2.4.1 h1:bUOFHtHhuhPekjHGe1Q1BmITvtBLdQI4yjSMC405KcU= +gopkg.in/lightningtipbot/telebot.v2 v2.4.1/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201070229-f317e8453e9f h1:BnYlAEmRJi8YEcSbXO6PlAVwl7WpKJvMqkijHZmO8Gw= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201070229-f317e8453e9f/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955 h1:uhQUyDqilPUh58BWtIeSAupjovs1QQjC6pEoErqD86o= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= @@ -726,10 +734,6 @@ 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.3.5 h1:TdMJTlG8kvepsvZdy/gPeYEBdwKdwFFjH1AQTua9BOU= -gopkg.in/tucnak/telebot.v2 v2.3.5/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= -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= diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 7f19df3e..e446c560 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -4,7 +4,7 @@ import ( "time" "github.com/imroc/req" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type Client struct { diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index 4d268150..c18c4cbb 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -19,7 +19,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/gorilla/mux" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" "github.com/LightningTipBot/LightningTipBot/internal/i18n" ) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 0662cbae..8d54bada 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -13,7 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func getArgumentFromCommand(input string, which int) (output string, err error) { diff --git a/internal/telegram/balance.go b/internal/telegram/balance.go index 36185c77..9850fae6 100644 --- a/internal/telegram/balance.go +++ b/internal/telegram/balance.go @@ -6,7 +6,7 @@ import ( log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot TipBot) balanceHandler(ctx context.Context, m *tb.Message) { diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 9d223614..efff9870 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -12,8 +12,8 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/storage" gocache "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" - "gopkg.in/tucnak/telebot.v2" - tb "gopkg.in/tucnak/telebot.v2" + "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" "gorm.io/gorm" ) diff --git a/internal/telegram/database.go b/internal/telegram/database.go index f83c568e..7736b70f 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -16,7 +16,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" "gorm.io/driver/sqlite" "gorm.io/gorm" ) @@ -95,7 +95,7 @@ func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.U return toUserDb, nil } func getCachedUser(u *tb.User, bot TipBot) (*lnbits.User, error) { - user := &lnbits.User{Name: strconv.Itoa(u.ID)} + user := &lnbits.User{Name: strconv.FormatInt(u.ID, 10)} if us, err := bot.Cache.Get(user.Name); err == nil { return us.(*lnbits.User), nil } @@ -110,7 +110,7 @@ func getCachedUser(u *tb.User, bot TipBot) (*lnbits.User, error) { // &tb.User{ID: toId, Username: username} // without updating the user in storage. func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { - user := &lnbits.User{Name: strconv.Itoa(u.ID)} + user := &lnbits.User{Name: strconv.FormatInt(u.ID, 10)} tx := bot.Database.First(user) if tx.Error != nil { errmsg := fmt.Sprintf("[GetUser] Couldn't fetch %s from Database: %s", GetUserStr(u), tx.Error.Error()) diff --git a/internal/telegram/donate.go b/internal/telegram/donate.go index 00606db4..bee1b80f 100644 --- a/internal/telegram/donate.go +++ b/internal/telegram/donate.go @@ -12,7 +12,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) // PLEASE DO NOT CHANGE THE CODE IN THIS FILE diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index e0a5efd3..3a5fa2ec 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -7,7 +7,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type Handler struct { diff --git a/internal/telegram/help.go b/internal/telegram/help.go index bb9be99a..ee9a554c 100644 --- a/internal/telegram/help.go +++ b/internal/telegram/help.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot TipBot) makeHelpMessage(ctx context.Context, m *tb.Message) string { diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 591f1caf..990e7e07 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -16,7 +16,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index ac3f5e5c..1724d262 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -3,16 +3,17 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/storage" "reflect" "strconv" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) const queryImage = "https://avatars.githubusercontent.com/u/88730856?v=4" diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index fae66631..f9d4919b 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -14,7 +14,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 873af57b..83d0325a 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -15,7 +15,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 096bca3a..0c2a3519 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -15,7 +15,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go index 62413558..9b106643 100644 --- a/internal/telegram/intercept/callback.go +++ b/internal/telegram/intercept/callback.go @@ -3,7 +3,7 @@ package intercept import ( "context" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type CallbackFuncHandler func(ctx context.Context, message *tb.Callback) diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go index 9068c411..7a7327d9 100644 --- a/internal/telegram/intercept/message.go +++ b/internal/telegram/intercept/message.go @@ -2,8 +2,9 @@ package intercept import ( "context" + log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type MessageInterface interface { diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go index 16a50668..52c5e5f5 100644 --- a/internal/telegram/intercept/query.go +++ b/internal/telegram/intercept/query.go @@ -2,8 +2,9 @@ package intercept import ( "context" + log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type QueryFuncHandler func(ctx context.Context, message *tb.Query) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index d7289720..2a1251e3 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -10,7 +10,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type InterceptorType int diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 4621e46b..3d58b2ab 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -12,7 +12,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/skip2/go-qrcode" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func helpInvoiceUsage(ctx context.Context, errormsg string) string { diff --git a/internal/telegram/link.go b/internal/telegram/link.go index c5bb1ad9..cd666f0d 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -11,7 +11,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index 4af0af96..dff01773 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -14,7 +14,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) // LnurlPayState saves the state of the user for an LNURL payment diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 12b60df3..bc6088f6 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -15,7 +15,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 36693d63..73ae41f6 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -17,7 +17,7 @@ import ( lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot *TipBot) GetHttpClient() (*http.Client, error) { diff --git a/internal/telegram/message.go b/internal/telegram/message.go index cb4304df..b1341cef 100644 --- a/internal/telegram/message.go +++ b/internal/telegram/message.go @@ -4,7 +4,7 @@ import ( "strconv" "time" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type Message struct { diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 1f82fff1..ff275be1 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -12,7 +12,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" decodepay "github.com/fiatjaf/ln-decodepay" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/photo.go b/internal/telegram/photo.go index 779e3a25..7c8a9e0a 100644 --- a/internal/telegram/photo.go +++ b/internal/telegram/photo.go @@ -11,7 +11,7 @@ import ( "github.com/makiuchi-d/gozxing" "github.com/makiuchi-d/gozxing/qrcode" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) // TryRecognizeInvoiceFromQrCode will try to read an invoice string from a qr code and invoke the payment handler. diff --git a/internal/telegram/send.go b/internal/telegram/send.go index e88921bd..2b8e0388 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -13,7 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( @@ -41,7 +41,7 @@ func (bot *TipBot) SendCheckSyntax(ctx context.Context, m *tb.Message) (bool, st type SendData struct { *transaction.Base From *lnbits.User `json:"from"` - ToTelegramId int `json:"to_telegram_id"` + ToTelegramId int64 `json:"to_telegram_id"` ToTelegramUser string `json:"to_telegram_user"` Memo string `json:"memo"` Message string `json:"message"` diff --git a/internal/telegram/start.go b/internal/telegram/start.go index 0cb33dae..5c109fa1 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -13,7 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/str" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" "gorm.io/gorm" ) @@ -80,7 +80,7 @@ func (bot TipBot) initWallet(tguser *tb.User) (*lnbits.User, error) { func (bot TipBot) createWallet(user *lnbits.User) error { UserStr := GetUserStr(user.Telegram) - u, err := bot.Client.CreateUserWithInitialWallet(strconv.Itoa(user.Telegram.ID), + u, err := bot.Client.CreateUserWithInitialWallet(strconv.FormatInt(user.Telegram.ID, 10), fmt.Sprintf("%d (%s)", user.Telegram.ID, UserStr), internal.Configuration.Lnbits.AdminId, UserStr) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index b87458c4..368ad016 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -2,7 +2,7 @@ package telegram import ( log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options ...interface{}) (msg *tb.Message) { diff --git a/internal/telegram/text.go b/internal/telegram/text.go index 9f1d94ac..a1d06a08 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -9,7 +9,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index 5bab0bdb..eeec852c 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -11,7 +11,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/i18n" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func helpTipUsage(ctx context.Context, errormsg string) string { diff --git a/internal/telegram/tooltip.go b/internal/telegram/tooltip.go index b7697703..9da54674 100644 --- a/internal/telegram/tooltip.go +++ b/internal/telegram/tooltip.go @@ -13,7 +13,7 @@ import ( log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) const ( @@ -161,7 +161,7 @@ func tipTooltipInitializedHandler(user *tb.User, bot TipBot) { runtime.IgnoreError(bot.Bunt.View(func(tx *buntdb.Tx) error { err := tx.Ascend(MessageOrderedByReplyToFrom, func(key, value string) bool { replyToUserId := gjson.Get(value, MessageOrderedByReplyToFrom) - if replyToUserId.String() == strconv.Itoa(user.ID) { + if replyToUserId.String() == strconv.FormatInt(user.ID, 10) { log.Debugln("loading persistent tip tool tip messages") ttt := &TipTooltip{} err := json.Unmarshal([]byte(value), ttt) diff --git a/internal/telegram/tooltip_test.go b/internal/telegram/tooltip_test.go index 548ebf10..b007a46b 100644 --- a/internal/telegram/tooltip_test.go +++ b/internal/telegram/tooltip_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/transaction.go b/internal/telegram/transaction.go index c1f5e7fb..80e20bb6 100644 --- a/internal/telegram/transaction.go +++ b/internal/telegram/transaction.go @@ -7,7 +7,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type Transaction struct { @@ -16,8 +16,8 @@ type Transaction struct { Bot *TipBot `gorm:"-"` From *lnbits.User `json:"from" gorm:"-"` To *lnbits.User `json:"to" gorm:"-"` - FromId int `json:"from_id" ` - ToId int `json:"to_id" ` + FromId int64 `json:"from_id" ` + ToId int64 `json:"to_id" ` FromUser string `json:"from_user"` ToUser string `json:"to_user"` Type string `json:"type"` diff --git a/internal/telegram/users.go b/internal/telegram/users.go index e9ee41bb..6365012c 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -3,13 +3,14 @@ package telegram import ( "errors" "fmt" + "time" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" "gorm.io/gorm" - "time" ) func SetUserState(user *lnbits.User, bot *TipBot, stateKey lnbits.UserStateKey, stateData string) { From 0abe56dfba0b209b3899a58d8dcf1de3cc324144 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 1 Dec 2021 08:59:51 +0100 Subject: [PATCH 064/541] Revert "telebot fork user ID int64 (#149)" (#150) This reverts commit 7c8ba7895d2b2c993524173ab0f7d8c34eaafa92. --- go.mod | 4 +--- go.sum | 12 ++++-------- internal/lnbits/types.go | 2 +- internal/lnbits/webhook/webhook.go | 2 +- internal/telegram/amounts.go | 2 +- internal/telegram/balance.go | 2 +- internal/telegram/bot.go | 4 ++-- internal/telegram/database.go | 6 +++--- internal/telegram/donate.go | 2 +- internal/telegram/handler.go | 2 +- internal/telegram/help.go | 2 +- internal/telegram/inline_faucet.go | 2 +- internal/telegram/inline_query.go | 7 +++---- internal/telegram/inline_receive.go | 2 +- internal/telegram/inline_send.go | 2 +- internal/telegram/inline_tipjar.go | 2 +- internal/telegram/intercept/callback.go | 2 +- internal/telegram/intercept/message.go | 3 +-- internal/telegram/intercept/query.go | 3 +-- internal/telegram/interceptor.go | 2 +- internal/telegram/invoice.go | 2 +- internal/telegram/link.go | 2 +- internal/telegram/lnurl-pay.go | 2 +- internal/telegram/lnurl-withdraw.go | 2 +- internal/telegram/lnurl.go | 2 +- internal/telegram/message.go | 2 +- internal/telegram/pay.go | 2 +- internal/telegram/photo.go | 2 +- internal/telegram/send.go | 4 ++-- internal/telegram/start.go | 4 ++-- internal/telegram/telegram.go | 2 +- internal/telegram/text.go | 2 +- internal/telegram/tip.go | 2 +- internal/telegram/tooltip.go | 4 ++-- internal/telegram/tooltip_test.go | 2 +- internal/telegram/transaction.go | 6 +++--- internal/telegram/users.go | 5 ++--- 37 files changed, 51 insertions(+), 61 deletions(-) diff --git a/go.mod b/go.mod index d5a69f78..749d3a23 100644 --- a/go.mod +++ b/go.mod @@ -18,9 +18,7 @@ require ( github.com/tidwall/buntdb v1.2.7 github.com/tidwall/gjson v1.10.2 golang.org/x/text v0.3.5 - gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955 + gopkg.in/tucnak/telebot.v2 v2.4.1 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.12 ) - -// replace gopkg.in/lightningtipbot/telebot.v2 => ../telebot diff --git a/go.sum b/go.sum index 09295538..5e614542 100644 --- a/go.sum +++ b/go.sum @@ -716,14 +716,6 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/lightningtipbot/telebot.v2 v2.3.5 h1:TdMJTlG8kvepsvZdy/gPeYEBdwKdwFFjH1AQTua9BOU= -gopkg.in/lightningtipbot/telebot.v2 v2.3.5/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= -gopkg.in/lightningtipbot/telebot.v2 v2.4.1 h1:bUOFHtHhuhPekjHGe1Q1BmITvtBLdQI4yjSMC405KcU= -gopkg.in/lightningtipbot/telebot.v2 v2.4.1/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= -gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201070229-f317e8453e9f h1:BnYlAEmRJi8YEcSbXO6PlAVwl7WpKJvMqkijHZmO8Gw= -gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201070229-f317e8453e9f/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= -gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955 h1:uhQUyDqilPUh58BWtIeSAupjovs1QQjC6pEoErqD86o= -gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= @@ -734,6 +726,10 @@ 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.3.5 h1:TdMJTlG8kvepsvZdy/gPeYEBdwKdwFFjH1AQTua9BOU= +gopkg.in/tucnak/telebot.v2 v2.3.5/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= +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= diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index e446c560..7f19df3e 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -4,7 +4,7 @@ import ( "time" "github.com/imroc/req" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) type Client struct { diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index c18c4cbb..4d268150 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -19,7 +19,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/gorilla/mux" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" "github.com/LightningTipBot/LightningTipBot/internal/i18n" ) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 8d54bada..0662cbae 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -13,7 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) func getArgumentFromCommand(input string, which int) (output string, err error) { diff --git a/internal/telegram/balance.go b/internal/telegram/balance.go index 9850fae6..36185c77 100644 --- a/internal/telegram/balance.go +++ b/internal/telegram/balance.go @@ -6,7 +6,7 @@ import ( log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) func (bot TipBot) balanceHandler(ctx context.Context, m *tb.Message) { diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index efff9870..9d223614 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -12,8 +12,8 @@ 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" + "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" "gorm.io/gorm" ) diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 7736b70f..f83c568e 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -16,7 +16,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" "gorm.io/driver/sqlite" "gorm.io/gorm" ) @@ -95,7 +95,7 @@ func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.U return toUserDb, nil } func getCachedUser(u *tb.User, bot TipBot) (*lnbits.User, error) { - user := &lnbits.User{Name: strconv.FormatInt(u.ID, 10)} + user := &lnbits.User{Name: strconv.Itoa(u.ID)} if us, err := bot.Cache.Get(user.Name); err == nil { return us.(*lnbits.User), nil } @@ -110,7 +110,7 @@ func getCachedUser(u *tb.User, bot TipBot) (*lnbits.User, error) { // &tb.User{ID: toId, Username: username} // without updating the user in storage. func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { - user := &lnbits.User{Name: strconv.FormatInt(u.ID, 10)} + user := &lnbits.User{Name: strconv.Itoa(u.ID)} tx := bot.Database.First(user) if tx.Error != nil { errmsg := fmt.Sprintf("[GetUser] Couldn't fetch %s from Database: %s", GetUserStr(u), tx.Error.Error()) diff --git a/internal/telegram/donate.go b/internal/telegram/donate.go index bee1b80f..00606db4 100644 --- a/internal/telegram/donate.go +++ b/internal/telegram/donate.go @@ -12,7 +12,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) // PLEASE DO NOT CHANGE THE CODE IN THIS FILE diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 3a5fa2ec..e0a5efd3 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -7,7 +7,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) type Handler struct { diff --git a/internal/telegram/help.go b/internal/telegram/help.go index ee9a554c..bb9be99a 100644 --- a/internal/telegram/help.go +++ b/internal/telegram/help.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) func (bot TipBot) makeHelpMessage(ctx context.Context, m *tb.Message) string { diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 990e7e07..591f1caf 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -16,7 +16,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) var ( diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index 1724d262..ac3f5e5c 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -3,17 +3,16 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/storage" "reflect" "strconv" "strings" - "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/storage" - "github.com/LightningTipBot/LightningTipBot/internal/i18n" i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) const queryImage = "https://avatars.githubusercontent.com/u/88730856?v=4" diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index f9d4919b..fae66631 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -14,7 +14,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) var ( diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 83d0325a..873af57b 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -15,7 +15,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) var ( diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 0c2a3519..096bca3a 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -15,7 +15,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) var ( diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go index 9b106643..62413558 100644 --- a/internal/telegram/intercept/callback.go +++ b/internal/telegram/intercept/callback.go @@ -3,7 +3,7 @@ package intercept import ( "context" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) type CallbackFuncHandler func(ctx context.Context, message *tb.Callback) diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go index 7a7327d9..9068c411 100644 --- a/internal/telegram/intercept/message.go +++ b/internal/telegram/intercept/message.go @@ -2,9 +2,8 @@ package intercept import ( "context" - log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) type MessageInterface interface { diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go index 52c5e5f5..16a50668 100644 --- a/internal/telegram/intercept/query.go +++ b/internal/telegram/intercept/query.go @@ -2,9 +2,8 @@ package intercept import ( "context" - log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) type QueryFuncHandler func(ctx context.Context, message *tb.Query) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 2a1251e3..d7289720 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -10,7 +10,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) type InterceptorType int diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 3d58b2ab..4621e46b 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -12,7 +12,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/skip2/go-qrcode" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) func helpInvoiceUsage(ctx context.Context, errormsg string) string { diff --git a/internal/telegram/link.go b/internal/telegram/link.go index cd666f0d..c5bb1ad9 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -11,7 +11,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index dff01773..4af0af96 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -14,7 +14,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) // LnurlPayState saves the state of the user for an LNURL payment diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index bc6088f6..12b60df3 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -15,7 +15,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) var ( diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 73ae41f6..36693d63 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -17,7 +17,7 @@ import ( lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) func (bot *TipBot) GetHttpClient() (*http.Client, error) { diff --git a/internal/telegram/message.go b/internal/telegram/message.go index b1341cef..cb4304df 100644 --- a/internal/telegram/message.go +++ b/internal/telegram/message.go @@ -4,7 +4,7 @@ import ( "strconv" "time" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) type Message struct { diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index ff275be1..1f82fff1 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -12,7 +12,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" decodepay "github.com/fiatjaf/ln-decodepay" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) var ( diff --git a/internal/telegram/photo.go b/internal/telegram/photo.go index 7c8a9e0a..779e3a25 100644 --- a/internal/telegram/photo.go +++ b/internal/telegram/photo.go @@ -11,7 +11,7 @@ import ( "github.com/makiuchi-d/gozxing" "github.com/makiuchi-d/gozxing/qrcode" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) // TryRecognizeInvoiceFromQrCode will try to read an invoice string from a qr code and invoke the payment handler. diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 2b8e0388..e88921bd 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -13,7 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) var ( @@ -41,7 +41,7 @@ func (bot *TipBot) SendCheckSyntax(ctx context.Context, m *tb.Message) (bool, st type SendData struct { *transaction.Base From *lnbits.User `json:"from"` - ToTelegramId int64 `json:"to_telegram_id"` + ToTelegramId int `json:"to_telegram_id"` ToTelegramUser string `json:"to_telegram_user"` Memo string `json:"memo"` Message string `json:"message"` diff --git a/internal/telegram/start.go b/internal/telegram/start.go index 5c109fa1..0cb33dae 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -13,7 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/str" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" "gorm.io/gorm" ) @@ -80,7 +80,7 @@ func (bot TipBot) initWallet(tguser *tb.User) (*lnbits.User, error) { func (bot TipBot) createWallet(user *lnbits.User) error { UserStr := GetUserStr(user.Telegram) - u, err := bot.Client.CreateUserWithInitialWallet(strconv.FormatInt(user.Telegram.ID, 10), + u, err := bot.Client.CreateUserWithInitialWallet(strconv.Itoa(user.Telegram.ID), fmt.Sprintf("%d (%s)", user.Telegram.ID, UserStr), internal.Configuration.Lnbits.AdminId, UserStr) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 368ad016..b87458c4 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -2,7 +2,7 @@ package telegram import ( log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options ...interface{}) (msg *tb.Message) { diff --git a/internal/telegram/text.go b/internal/telegram/text.go index a1d06a08..9f1d94ac 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -9,7 +9,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index eeec852c..5bab0bdb 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -11,7 +11,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/i18n" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) func helpTipUsage(ctx context.Context, errormsg string) string { diff --git a/internal/telegram/tooltip.go b/internal/telegram/tooltip.go index 9da54674..b7697703 100644 --- a/internal/telegram/tooltip.go +++ b/internal/telegram/tooltip.go @@ -13,7 +13,7 @@ import ( log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) const ( @@ -161,7 +161,7 @@ func tipTooltipInitializedHandler(user *tb.User, bot TipBot) { runtime.IgnoreError(bot.Bunt.View(func(tx *buntdb.Tx) error { err := tx.Ascend(MessageOrderedByReplyToFrom, func(key, value string) bool { replyToUserId := gjson.Get(value, MessageOrderedByReplyToFrom) - if replyToUserId.String() == strconv.FormatInt(user.ID, 10) { + if replyToUserId.String() == strconv.Itoa(user.ID) { log.Debugln("loading persistent tip tool tip messages") ttt := &TipTooltip{} err := json.Unmarshal([]byte(value), ttt) diff --git a/internal/telegram/tooltip_test.go b/internal/telegram/tooltip_test.go index b007a46b..548ebf10 100644 --- a/internal/telegram/tooltip_test.go +++ b/internal/telegram/tooltip_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) var ( diff --git a/internal/telegram/transaction.go b/internal/telegram/transaction.go index 80e20bb6..c1f5e7fb 100644 --- a/internal/telegram/transaction.go +++ b/internal/telegram/transaction.go @@ -7,7 +7,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" ) type Transaction struct { @@ -16,8 +16,8 @@ type Transaction struct { Bot *TipBot `gorm:"-"` From *lnbits.User `json:"from" gorm:"-"` To *lnbits.User `json:"to" gorm:"-"` - FromId int64 `json:"from_id" ` - ToId int64 `json:"to_id" ` + FromId int `json:"from_id" ` + ToId int `json:"to_id" ` FromUser string `json:"from_user"` ToUser string `json:"to_user"` Type string `json:"type"` diff --git a/internal/telegram/users.go b/internal/telegram/users.go index 6365012c..e9ee41bb 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -3,14 +3,13 @@ package telegram import ( "errors" "fmt" - "time" - "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/tucnak/telebot.v2" "gorm.io/gorm" + "time" ) func SetUserState(user *lnbits.User, bot *TipBot, stateKey lnbits.UserStateKey, stateData string) { From ba6bf0a3178e535976f7419123ba227b7bf1fe4f Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 1 Dec 2021 11:24:28 +0100 Subject: [PATCH 065/541] telebot fork user ID int64 (#151) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- go.mod | 4 +++- go.sum | 12 ++++++++---- internal/lnbits/types.go | 2 +- internal/lnbits/webhook/webhook.go | 2 +- internal/telegram/amounts.go | 2 +- internal/telegram/balance.go | 2 +- internal/telegram/bot.go | 4 ++-- internal/telegram/database.go | 6 +++--- internal/telegram/donate.go | 2 +- internal/telegram/handler.go | 2 +- internal/telegram/help.go | 2 +- internal/telegram/inline_faucet.go | 2 +- internal/telegram/inline_query.go | 7 ++++--- internal/telegram/inline_receive.go | 2 +- internal/telegram/inline_send.go | 2 +- internal/telegram/inline_tipjar.go | 2 +- internal/telegram/intercept/callback.go | 2 +- internal/telegram/intercept/message.go | 3 ++- internal/telegram/intercept/query.go | 3 ++- internal/telegram/interceptor.go | 2 +- internal/telegram/invoice.go | 2 +- internal/telegram/link.go | 2 +- internal/telegram/lnurl-pay.go | 2 +- internal/telegram/lnurl-withdraw.go | 2 +- internal/telegram/lnurl.go | 2 +- internal/telegram/message.go | 2 +- internal/telegram/pay.go | 2 +- internal/telegram/photo.go | 2 +- internal/telegram/send.go | 4 ++-- internal/telegram/start.go | 4 ++-- internal/telegram/telegram.go | 2 +- internal/telegram/text.go | 2 +- internal/telegram/tip.go | 2 +- internal/telegram/tooltip.go | 4 ++-- internal/telegram/tooltip_test.go | 2 +- internal/telegram/transaction.go | 6 +++--- internal/telegram/users.go | 5 +++-- 37 files changed, 61 insertions(+), 51 deletions(-) diff --git a/go.mod b/go.mod index 749d3a23..d5a69f78 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,9 @@ require ( github.com/tidwall/buntdb v1.2.7 github.com/tidwall/gjson v1.10.2 golang.org/x/text v0.3.5 - gopkg.in/tucnak/telebot.v2 v2.4.1 + gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.12 ) + +// replace gopkg.in/lightningtipbot/telebot.v2 => ../telebot diff --git a/go.sum b/go.sum index 5e614542..09295538 100644 --- a/go.sum +++ b/go.sum @@ -716,6 +716,14 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/lightningtipbot/telebot.v2 v2.3.5 h1:TdMJTlG8kvepsvZdy/gPeYEBdwKdwFFjH1AQTua9BOU= +gopkg.in/lightningtipbot/telebot.v2 v2.3.5/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= +gopkg.in/lightningtipbot/telebot.v2 v2.4.1 h1:bUOFHtHhuhPekjHGe1Q1BmITvtBLdQI4yjSMC405KcU= +gopkg.in/lightningtipbot/telebot.v2 v2.4.1/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201070229-f317e8453e9f h1:BnYlAEmRJi8YEcSbXO6PlAVwl7WpKJvMqkijHZmO8Gw= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201070229-f317e8453e9f/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955 h1:uhQUyDqilPUh58BWtIeSAupjovs1QQjC6pEoErqD86o= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= @@ -726,10 +734,6 @@ 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.3.5 h1:TdMJTlG8kvepsvZdy/gPeYEBdwKdwFFjH1AQTua9BOU= -gopkg.in/tucnak/telebot.v2 v2.3.5/go.mod h1:BgaIIx50PSRS9pG59JH+geT82cfvoJU/IaI5TJdN3v8= -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= diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 7f19df3e..e446c560 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -4,7 +4,7 @@ import ( "time" "github.com/imroc/req" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type Client struct { diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index 4d268150..c18c4cbb 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -19,7 +19,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/gorilla/mux" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" "github.com/LightningTipBot/LightningTipBot/internal/i18n" ) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 0662cbae..8d54bada 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -13,7 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func getArgumentFromCommand(input string, which int) (output string, err error) { diff --git a/internal/telegram/balance.go b/internal/telegram/balance.go index 36185c77..9850fae6 100644 --- a/internal/telegram/balance.go +++ b/internal/telegram/balance.go @@ -6,7 +6,7 @@ import ( log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot TipBot) balanceHandler(ctx context.Context, m *tb.Message) { diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 9d223614..efff9870 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -12,8 +12,8 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/storage" gocache "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" - "gopkg.in/tucnak/telebot.v2" - tb "gopkg.in/tucnak/telebot.v2" + "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" "gorm.io/gorm" ) diff --git a/internal/telegram/database.go b/internal/telegram/database.go index f83c568e..7736b70f 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -16,7 +16,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" "gorm.io/driver/sqlite" "gorm.io/gorm" ) @@ -95,7 +95,7 @@ func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.U return toUserDb, nil } func getCachedUser(u *tb.User, bot TipBot) (*lnbits.User, error) { - user := &lnbits.User{Name: strconv.Itoa(u.ID)} + user := &lnbits.User{Name: strconv.FormatInt(u.ID, 10)} if us, err := bot.Cache.Get(user.Name); err == nil { return us.(*lnbits.User), nil } @@ -110,7 +110,7 @@ func getCachedUser(u *tb.User, bot TipBot) (*lnbits.User, error) { // &tb.User{ID: toId, Username: username} // without updating the user in storage. func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { - user := &lnbits.User{Name: strconv.Itoa(u.ID)} + user := &lnbits.User{Name: strconv.FormatInt(u.ID, 10)} tx := bot.Database.First(user) if tx.Error != nil { errmsg := fmt.Sprintf("[GetUser] Couldn't fetch %s from Database: %s", GetUserStr(u), tx.Error.Error()) diff --git a/internal/telegram/donate.go b/internal/telegram/donate.go index 00606db4..bee1b80f 100644 --- a/internal/telegram/donate.go +++ b/internal/telegram/donate.go @@ -12,7 +12,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) // PLEASE DO NOT CHANGE THE CODE IN THIS FILE diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index e0a5efd3..3a5fa2ec 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -7,7 +7,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type Handler struct { diff --git a/internal/telegram/help.go b/internal/telegram/help.go index bb9be99a..ee9a554c 100644 --- a/internal/telegram/help.go +++ b/internal/telegram/help.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot TipBot) makeHelpMessage(ctx context.Context, m *tb.Message) string { diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 591f1caf..990e7e07 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -16,7 +16,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index ac3f5e5c..1724d262 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -3,16 +3,17 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/storage" "reflect" "strconv" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) const queryImage = "https://avatars.githubusercontent.com/u/88730856?v=4" diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index fae66631..f9d4919b 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -14,7 +14,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 873af57b..83d0325a 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -15,7 +15,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 096bca3a..0c2a3519 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -15,7 +15,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go index 62413558..9b106643 100644 --- a/internal/telegram/intercept/callback.go +++ b/internal/telegram/intercept/callback.go @@ -3,7 +3,7 @@ package intercept import ( "context" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type CallbackFuncHandler func(ctx context.Context, message *tb.Callback) diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go index 9068c411..7a7327d9 100644 --- a/internal/telegram/intercept/message.go +++ b/internal/telegram/intercept/message.go @@ -2,8 +2,9 @@ package intercept import ( "context" + log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type MessageInterface interface { diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go index 16a50668..52c5e5f5 100644 --- a/internal/telegram/intercept/query.go +++ b/internal/telegram/intercept/query.go @@ -2,8 +2,9 @@ package intercept import ( "context" + log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type QueryFuncHandler func(ctx context.Context, message *tb.Query) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index d7289720..2a1251e3 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -10,7 +10,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type InterceptorType int diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 4621e46b..3d58b2ab 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -12,7 +12,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/skip2/go-qrcode" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func helpInvoiceUsage(ctx context.Context, errormsg string) string { diff --git a/internal/telegram/link.go b/internal/telegram/link.go index c5bb1ad9..cd666f0d 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -11,7 +11,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index 4af0af96..dff01773 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -14,7 +14,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) // LnurlPayState saves the state of the user for an LNURL payment diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 12b60df3..bc6088f6 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -15,7 +15,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 36693d63..73ae41f6 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -17,7 +17,7 @@ import ( lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot *TipBot) GetHttpClient() (*http.Client, error) { diff --git a/internal/telegram/message.go b/internal/telegram/message.go index cb4304df..b1341cef 100644 --- a/internal/telegram/message.go +++ b/internal/telegram/message.go @@ -4,7 +4,7 @@ import ( "strconv" "time" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type Message struct { diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 1f82fff1..ff275be1 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -12,7 +12,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" decodepay "github.com/fiatjaf/ln-decodepay" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/photo.go b/internal/telegram/photo.go index 779e3a25..7c8a9e0a 100644 --- a/internal/telegram/photo.go +++ b/internal/telegram/photo.go @@ -11,7 +11,7 @@ import ( "github.com/makiuchi-d/gozxing" "github.com/makiuchi-d/gozxing/qrcode" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) // TryRecognizeInvoiceFromQrCode will try to read an invoice string from a qr code and invoke the payment handler. diff --git a/internal/telegram/send.go b/internal/telegram/send.go index e88921bd..2b8e0388 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -13,7 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( @@ -41,7 +41,7 @@ func (bot *TipBot) SendCheckSyntax(ctx context.Context, m *tb.Message) (bool, st type SendData struct { *transaction.Base From *lnbits.User `json:"from"` - ToTelegramId int `json:"to_telegram_id"` + ToTelegramId int64 `json:"to_telegram_id"` ToTelegramUser string `json:"to_telegram_user"` Memo string `json:"memo"` Message string `json:"message"` diff --git a/internal/telegram/start.go b/internal/telegram/start.go index 0cb33dae..5c109fa1 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -13,7 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/str" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" "gorm.io/gorm" ) @@ -80,7 +80,7 @@ func (bot TipBot) initWallet(tguser *tb.User) (*lnbits.User, error) { func (bot TipBot) createWallet(user *lnbits.User) error { UserStr := GetUserStr(user.Telegram) - u, err := bot.Client.CreateUserWithInitialWallet(strconv.Itoa(user.Telegram.ID), + u, err := bot.Client.CreateUserWithInitialWallet(strconv.FormatInt(user.Telegram.ID, 10), fmt.Sprintf("%d (%s)", user.Telegram.ID, UserStr), internal.Configuration.Lnbits.AdminId, UserStr) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index b87458c4..368ad016 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -2,7 +2,7 @@ package telegram import ( log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options ...interface{}) (msg *tb.Message) { diff --git a/internal/telegram/text.go b/internal/telegram/text.go index 9f1d94ac..a1d06a08 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -9,7 +9,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func (bot TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index 5bab0bdb..eeec852c 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -11,7 +11,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/i18n" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) func helpTipUsage(ctx context.Context, errormsg string) string { diff --git a/internal/telegram/tooltip.go b/internal/telegram/tooltip.go index b7697703..9da54674 100644 --- a/internal/telegram/tooltip.go +++ b/internal/telegram/tooltip.go @@ -13,7 +13,7 @@ import ( log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) const ( @@ -161,7 +161,7 @@ func tipTooltipInitializedHandler(user *tb.User, bot TipBot) { runtime.IgnoreError(bot.Bunt.View(func(tx *buntdb.Tx) error { err := tx.Ascend(MessageOrderedByReplyToFrom, func(key, value string) bool { replyToUserId := gjson.Get(value, MessageOrderedByReplyToFrom) - if replyToUserId.String() == strconv.Itoa(user.ID) { + if replyToUserId.String() == strconv.FormatInt(user.ID, 10) { log.Debugln("loading persistent tip tool tip messages") ttt := &TipTooltip{} err := json.Unmarshal([]byte(value), ttt) diff --git a/internal/telegram/tooltip_test.go b/internal/telegram/tooltip_test.go index 548ebf10..b007a46b 100644 --- a/internal/telegram/tooltip_test.go +++ b/internal/telegram/tooltip_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) var ( diff --git a/internal/telegram/transaction.go b/internal/telegram/transaction.go index c1f5e7fb..80e20bb6 100644 --- a/internal/telegram/transaction.go +++ b/internal/telegram/transaction.go @@ -7,7 +7,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" ) type Transaction struct { @@ -16,8 +16,8 @@ type Transaction struct { Bot *TipBot `gorm:"-"` From *lnbits.User `json:"from" gorm:"-"` To *lnbits.User `json:"to" gorm:"-"` - FromId int `json:"from_id" ` - ToId int `json:"to_id" ` + FromId int64 `json:"from_id" ` + ToId int64 `json:"to_id" ` FromUser string `json:"from_user"` ToUser string `json:"to_user"` Type string `json:"type"` diff --git a/internal/telegram/users.go b/internal/telegram/users.go index e9ee41bb..6365012c 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -3,13 +3,14 @@ package telegram import ( "errors" "fmt" + "time" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" - tb "gopkg.in/tucnak/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v2" "gorm.io/gorm" - "time" ) func SetUserState(user *lnbits.User, bot *TipBot, stateKey lnbits.UserStateKey, stateData string) { From 25d9b8af3bb2a56fc082ef0f7104a6d31c4f9279 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Mon, 6 Dec 2021 20:08:33 +0100 Subject: [PATCH 066/541] Check admin rights (#152) * add rightsToDoAction * comments * comments * typo * check for id * return CanEditMessages --- internal/telegram/telegram.go | 65 ++++++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 368ad016..210b81c6 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -1,8 +1,11 @@ package telegram import ( + "fmt" + "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" + "time" ) func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options ...interface{}) (msg *tb.Message) { @@ -29,16 +32,76 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i } func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message) { - msg, err := bot.Telegram.Edit(to, what, options...) + if !allowedToPerformAction(bot, to, isAdminAndCanEdit) { + return + } + var err error + msg, err = bot.Telegram.Edit(to, what, options...) if err != nil { log.Warnln(err.Error()) } return + } func (bot TipBot) tryDeleteMessage(msg tb.Editable) { + if !allowedToPerformAction(bot, msg, isAdminAndCanDelete) { + return + } err := bot.Telegram.Delete(msg) if err != nil { log.Warnln(err.Error()) } + return + +} + +// allowedToPerformAction will check if bot is allowed to perform an action on the tb.Editable. +// this function will persist the admins list in cache for 5 minutes. +// if no admins list is found for this group, bot will always fetch a fresh list. +func allowedToPerformAction(bot TipBot, editable tb.Editable, action func(members []tb.ChatMember, me *tb.User) bool) bool { + switch editable.(type) { + case *tb.Message: + message := editable.(*tb.Message) + if message.Sender.ID == bot.Telegram.Me.ID { + break + } + chat := message.Chat + if chat.Type == tb.ChatSuperGroup || chat.Type == tb.ChatGroup { + admins, err := bot.Cache.Get(fmt.Sprintf("admins-%d", chat.ID)) + if err != nil { + admins, err = bot.Telegram.AdminsOf(message.Chat) + if err != nil { + log.Warnln(err.Error()) + return false + } + bot.Cache.Set(fmt.Sprintf("admins-%d", chat.ID), admins, &store.Options{Expiration: 5 * time.Minute}) + } + if action(admins.([]tb.ChatMember), bot.Telegram.Me) { + return true + } + return false + } + } + return true +} + +// isAdminAndCanDelete will check if me is in members list and allowed to delete messages +func isAdminAndCanDelete(members []tb.ChatMember, me *tb.User) bool { + for _, admin := range members { + if admin.User.ID == me.ID { + return admin.CanDeleteMessages + } + } + return false +} + +// isAdminAndCanEdit will check if me is in members list and allowed to edit messages +func isAdminAndCanEdit(members []tb.ChatMember, me *tb.User) bool { + for _, admin := range members { + if admin.User.ID == me.ID { + return admin.CanEditMessages + } + } + return false } From aaa8a3cc197c8af800cd93686adf5f34e2da40ca Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Mon, 6 Dec 2021 22:22:02 +0100 Subject: [PATCH 067/541] WIP: rate limiter per chat (#86) * add rate limiter per chat * differentiate between global and chat rate limiting * remove diffs * resolve conflicts * resolve conflict * refactor limiter * rename * rename * logs * remove logs * reset buckets * new rate limiter * fix comments * separate id and global limiter * rename stuff * reset bucket size Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- go.mod | 2 + go.sum | 4 ++ internal/rate/limiter.go | 87 +++++++++++++++++++++++++++++++++++ internal/telegram/bot.go | 4 ++ internal/telegram/telegram.go | 6 +++ 5 files changed, 103 insertions(+) create mode 100644 internal/rate/limiter.go diff --git a/go.mod b/go.mod index d5a69f78..de57f04a 100644 --- a/go.mod +++ b/go.mod @@ -13,11 +13,13 @@ require ( github.com/makiuchi-d/gozxing v0.0.2 github.com/nicksnyder/go-i18n/v2 v2.1.2 github.com/patrickmn/go-cache v2.1.0+incompatible + github.com/sethvargo/go-limiter v0.7.2 github.com/sirupsen/logrus v1.6.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tidwall/buntdb v1.2.7 github.com/tidwall/gjson v1.10.2 golang.org/x/text v0.3.5 + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955 gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.12 diff --git a/go.sum b/go.sum index 09295538..6bf23071 100644 --- a/go.sum +++ b/go.sum @@ -453,6 +453,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sethvargo/go-limiter v0.7.2 h1:FgC4N7RMpV5gMrUdda15FaFTkQ/L4fEqM7seXMs4oO8= +github.com/sethvargo/go-limiter v0.7.2/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiwf72uGu0CXCcU= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -649,7 +651,9 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/rate/limiter.go b/internal/rate/limiter.go new file mode 100644 index 00000000..41847782 --- /dev/null +++ b/internal/rate/limiter.go @@ -0,0 +1,87 @@ +package rate + +import ( + "context" + "golang.org/x/time/rate" + tb "gopkg.in/lightningtipbot/telebot.v2" + "strconv" + "sync" +) + +// Limiter +type Limiter struct { + keys map[string]*rate.Limiter + mu *sync.RWMutex + r rate.Limit + b int +} + +var idLimiter *Limiter +var globalLimiter *rate.Limiter + +// NewLimiter creates both chat and global rate limiters. +func Start() { + idLimiter = newIdRateLimiter(rate.Limit(0.3), 20) + globalLimiter = rate.NewLimiter(rate.Limit(30), 30) +} + +// NewRateLimiter . +func newIdRateLimiter(r rate.Limit, b int) *Limiter { + i := &Limiter{ + keys: make(map[string]*rate.Limiter), + mu: &sync.RWMutex{}, + r: r, + b: b, + } + + return i +} + +func CheckLimit(to interface{}) { + globalLimiter.Wait(context.Background()) + var id string + switch to.(type) { + case *tb.Chat: + id = strconv.FormatInt(to.(*tb.Chat).ID, 10) + case *tb.User: + id = strconv.FormatInt(to.(*tb.User).ID, 10) + case tb.Recipient: + id = to.(tb.Recipient).Recipient() + case *tb.Message: + if to.(*tb.Message).Chat != nil { + id = strconv.FormatInt(to.(*tb.Message).Chat.ID, 10) + } + } + if len(id) > 0 { + idLimiter.GetLimiter(id).Wait(context.Background()) + } +} + +// Add creates a new rate limiter and adds it to the keys map, +// using the key +func (i *Limiter) Add(key string) *rate.Limiter { + i.mu.Lock() + defer i.mu.Unlock() + + limiter := rate.NewLimiter(i.r, i.b) + + i.keys[key] = limiter + + return limiter +} + +// GetLimiter returns the rate limiter for the provided key if it exists. +// Otherwise, calls Add to add key address to the map +func (i *Limiter) GetLimiter(key string) *rate.Limiter { + i.mu.Lock() + limiter, exists := i.keys[key] + + if !exists { + i.mu.Unlock() + return i.Add(key) + } + + i.mu.Unlock() + + return limiter +} diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index efff9870..a5b9876a 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -5,6 +5,8 @@ import ( "sync" "time" + limiter "github.com/LightningTipBot/LightningTipBot/internal/rate" + "github.com/eko/gocache/store" "github.com/LightningTipBot/LightningTipBot/internal" @@ -23,6 +25,7 @@ type TipBot struct { logger *gorm.DB Telegram *telebot.Bot Client *lnbits.Client + limiter map[string]limiter.Limiter Cache } type Cache struct { @@ -40,6 +43,7 @@ func NewBot() TipBot { gocacheStore := store.NewGoCache(gocacheClient, nil) // create sqlite databases db, txLogger := AutoMigration() + limiter.Start() return TipBot{ Database: db, Client: lnbits.NewClient(internal.Configuration.Lnbits.AdminKey, internal.Configuration.Lnbits.Url), diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 210b81c6..6a8ecdf5 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -2,6 +2,7 @@ package telegram import ( "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/rate" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" @@ -9,6 +10,7 @@ import ( ) func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options ...interface{}) (msg *tb.Message) { + rate.CheckLimit(to) msg, err := bot.Telegram.Forward(to, what, options...) if err != nil { log.Warnln(err.Error()) @@ -16,6 +18,7 @@ func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options . return } func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...interface{}) (msg *tb.Message) { + rate.CheckLimit(to) msg, err := bot.Telegram.Send(to, what, options...) if err != nil { log.Warnln(err.Error()) @@ -24,6 +27,7 @@ func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...i } func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...interface{}) (msg *tb.Message) { + rate.CheckLimit(to) msg, err := bot.Telegram.Reply(to, what, options...) if err != nil { log.Warnln(err.Error()) @@ -35,6 +39,7 @@ func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...in if !allowedToPerformAction(bot, to, isAdminAndCanEdit) { return } + rate.CheckLimit(to) var err error msg, err = bot.Telegram.Edit(to, what, options...) if err != nil { @@ -48,6 +53,7 @@ func (bot TipBot) tryDeleteMessage(msg tb.Editable) { if !allowedToPerformAction(bot, msg, isAdminAndCanDelete) { return } + rate.CheckLimit(msg) err := bot.Telegram.Delete(msg) if err != nil { log.Warnln(err.Error()) From 38251600f93af6e9e2a8f998b16cda9fd68696d9 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 6 Dec 2021 22:37:22 +0100 Subject: [PATCH 068/541] tx timeout sleep faster (#153) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/storage/transaction/transaction.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index 3bcb9892..2105aff0 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -75,13 +75,13 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error } // to avoid race conditions, we block the call if there is // already an active transaction by loop until InTransaction is false - ticker := time.NewTicker(time.Second * 10) + ticker := time.NewTicker(time.Second * 3) for tx.InTransaction { select { case <-ticker.C: return nil, fmt.Errorf("transaction timeout") default: - time.Sleep(time.Duration(500) * time.Millisecond) + time.Sleep(time.Duration(200) * time.Millisecond) err = db.Get(s) } } From a0799b1b32b31ef3264d8abf41d7d3586eea5006 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Tue, 7 Dec 2021 10:34:37 +0100 Subject: [PATCH 069/541] isAdminAndCanEdit fix (#154) * Update telegram.go * remove isAdminAndCanEdit on tryEdit * remove isAdminAndCanEdit --- internal/telegram/telegram.go | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 6a8ecdf5..b1a4e7a0 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -36,9 +36,6 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i } func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message) { - if !allowedToPerformAction(bot, to, isAdminAndCanEdit) { - return - } rate.CheckLimit(to) var err error msg, err = bot.Telegram.Edit(to, what, options...) @@ -101,13 +98,3 @@ func isAdminAndCanDelete(members []tb.ChatMember, me *tb.User) bool { } return false } - -// isAdminAndCanEdit will check if me is in members list and allowed to edit messages -func isAdminAndCanEdit(members []tb.ChatMember, me *tb.User) bool { - for _, admin := range members { - if admin.User.ID == me.ID { - return admin.CanEditMessages - } - } - return false -} From 236b095bbe7b054294155d95be0bf18786f284c8 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 7 Dec 2021 20:06:17 +0100 Subject: [PATCH 070/541] new telebot (#155) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- go.mod | 2 +- go.sum | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/go.mod b/go.mod index de57f04a..d5883c19 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/tidwall/gjson v1.10.2 golang.org/x/text v0.3.5 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 - gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955 + gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211207185736-224e4f70c5ad gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.12 ) diff --git a/go.sum b/go.sum index 6bf23071..b94a5aa0 100644 --- a/go.sum +++ b/go.sum @@ -728,6 +728,8 @@ gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201070229-f317e8453e9f h1:BnYl gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201070229-f317e8453e9f/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955 h1:uhQUyDqilPUh58BWtIeSAupjovs1QQjC6pEoErqD86o= gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211207185736-224e4f70c5ad h1:HR3vqf1DS7hEQ0j77J4ctumZgzDIHNXZtag60U9OOig= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211207185736-224e4f70c5ad/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= From f6768b72eaea9a8fe584958b2048b1e52e3d6492 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Tue, 7 Dec 2021 22:55:21 +0100 Subject: [PATCH 071/541] Mutex lock per Telegram User ID (#156) * lets go * dunno * fix after interceptor * getTelegramUserFromInterface * clean up requireUserInterceptor * clean up bs * cleanup * cleanup2 * add comment * fix before default interceptor * async sleep * back to 60 * remove newline * adding defer interceptor * handlers tighter Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/storage/transaction/transaction.go | 4 +- internal/telegram/handler.go | 99 +++++++++++++-------- internal/telegram/inline_query.go | 2 + internal/telegram/intercept/callback.go | 10 ++- internal/telegram/intercept/message.go | 10 ++- internal/telegram/intercept/query.go | 16 +++- internal/telegram/interceptor.go | 72 +++++++++++---- internal/telegram/link.go | 7 +- 8 files changed, 157 insertions(+), 63 deletions(-) diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index 2105aff0..08319157 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -75,13 +75,13 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error } // to avoid race conditions, we block the call if there is // already an active transaction by loop until InTransaction is false - ticker := time.NewTicker(time.Second * 3) + ticker := time.NewTicker(time.Second * 2) for tx.InTransaction { select { case <-ticker.C: return nil, fmt.Errorf("transaction timeout") default: - time.Sleep(time.Duration(200) * time.Millisecond) + time.Sleep(time.Duration(500) * time.Millisecond) err = db.Get(s) } } diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 3a5fa2ec..ab78b66d 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -28,29 +28,43 @@ func (bot TipBot) registerTelegramHandlers() { }) } +func getDefaultBeforeInterceptor(bot TipBot) []intercept.Func { + return []intercept.Func{bot.lockInterceptor, bot.localizerInterceptor} +} +func getDefaultDeferInterceptor(bot TipBot) []intercept.Func { + return []intercept.Func{bot.unlockInterceptor} +} +func getDefaultAfterInterceptor(bot TipBot) []intercept.Func { + return []intercept.Func{} +} + // registerHandlerWithInterceptor will register a handler with all the predefined interceptors, based on the interceptor type func (bot TipBot) registerHandlerWithInterceptor(h Handler) { + h.Interceptor.Before = append(getDefaultBeforeInterceptor(bot), h.Interceptor.Before...) + h.Interceptor.After = append(h.Interceptor.After, getDefaultAfterInterceptor(bot)...) + h.Interceptor.OnDefer = append(h.Interceptor.OnDefer, getDefaultDeferInterceptor(bot)...) + switch h.Interceptor.Type { case MessageInterceptor: - h.Interceptor.Before = append(h.Interceptor.Before, bot.localizerInterceptor) for _, endpoint := range h.Endpoints { bot.handle(endpoint, intercept.HandlerWithMessage(h.Handler.(func(ctx context.Context, query *tb.Message)), intercept.WithBeforeMessage(h.Interceptor.Before...), - intercept.WithAfterMessage(h.Interceptor.After...))) + intercept.WithAfterMessage(h.Interceptor.After...), + intercept.WithDeferMessage(h.Interceptor.OnDefer...))) } case QueryInterceptor: - h.Interceptor.Before = append(h.Interceptor.Before, bot.localizerInterceptor) for _, endpoint := range h.Endpoints { bot.handle(endpoint, intercept.HandlerWithQuery(h.Handler.(func(ctx context.Context, query *tb.Query)), intercept.WithBeforeQuery(h.Interceptor.Before...), - intercept.WithAfterQuery(h.Interceptor.After...))) + intercept.WithAfterQuery(h.Interceptor.After...), + intercept.WithDeferQuery(h.Interceptor.OnDefer...))) } case CallbackInterceptor: - h.Interceptor.Before = append(h.Interceptor.Before, bot.localizerInterceptor) for _, endpoint := range h.Endpoints { bot.handle(endpoint, intercept.HandlerWithCallback(h.Handler.(func(ctx context.Context, callback *tb.Callback)), intercept.WithBeforeCallback(h.Interceptor.Before...), - intercept.WithAfterCallback(h.Interceptor.After...))) + intercept.WithAfterCallback(h.Interceptor.After...), + intercept.WithDeferCallback(h.Interceptor.OnDefer...))) } } } @@ -90,9 +104,14 @@ func (bot TipBot) register(h Handler) { func (bot TipBot) getHandler() []Handler { return []Handler{ { - Endpoints: []interface{}{"/start"}, - Handler: bot.startHandler, - Interceptor: &Interceptor{Type: MessageInterceptor}, + Endpoints: []interface{}{"/start"}, + Handler: bot.startHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.loadUserInterceptor, + }}, }, { Endpoints: []interface{}{"/tip"}, @@ -101,7 +120,7 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.logMessageInterceptor, - bot.loadUserInterceptor, + bot.requireUserInterceptor, bot.loadReplyToInterceptor, }}, }, @@ -112,7 +131,7 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.logMessageInterceptor, - bot.loadUserInterceptor, + bot.requireUserInterceptor, }}, }, { @@ -122,7 +141,7 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.logMessageInterceptor, - bot.loadUserInterceptor, + bot.requireUserInterceptor, }}, }, { @@ -132,7 +151,7 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.logMessageInterceptor, - bot.loadUserInterceptor, + bot.requireUserInterceptor, }}, }, { @@ -142,7 +161,7 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.logMessageInterceptor, - bot.loadUserInterceptor, + bot.requireUserInterceptor, bot.loadReplyToInterceptor, }}, }, @@ -150,15 +169,21 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan", "/grifo"}, Handler: bot.faucetHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.requireUserInterceptor, + }}, }, { Endpoints: []interface{}{"/tipjar", "/spendendose"}, Handler: bot.tipjarHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.logMessageInterceptor, + bot.requireUserInterceptor, + }}, }, { Endpoints: []interface{}{"/help"}, @@ -187,7 +212,7 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.logMessageInterceptor, - bot.loadUserInterceptor, + bot.requireUserInterceptor, }}, }, { @@ -197,7 +222,7 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.logMessageInterceptor, - bot.loadUserInterceptor, + bot.requireUserInterceptor, }}, }, { @@ -207,7 +232,7 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.logMessageInterceptor, - bot.loadUserInterceptor}}, + bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{"/lnurl"}, @@ -216,7 +241,7 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.logMessageInterceptor, - bot.loadUserInterceptor}}, + bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{tb.OnPhoto}, @@ -226,7 +251,7 @@ func (bot TipBot) getHandler() []Handler { Before: []intercept.Func{ bot.requirePrivateChatInterceptor, bot.logMessageInterceptor, - bot.loadUserInterceptor}}, + bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{tb.OnText}, @@ -234,9 +259,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.requirePrivateChatInterceptor, - bot.logMessageInterceptor, // Log message only if private chat - bot.loadUserInterceptor, + bot.requirePrivateChatInterceptor, // Respond to any text only in private chat + bot.logMessageInterceptor, + bot.loadUserInterceptor, // need to use loadUserInterceptor instead of requireUserInterceptor, because user might not be registered yet }}, }, { @@ -258,28 +283,28 @@ func (bot TipBot) getHandler() []Handler { Handler: bot.confirmPayHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{&btnCancelPay}, Handler: bot.cancelPaymentHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{&btnSend}, Handler: bot.confirmSendHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{&btnCancelSend}, Handler: bot.cancelSendHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{&btnAcceptInlineSend}, @@ -293,7 +318,7 @@ func (bot TipBot) getHandler() []Handler { Handler: bot.cancelInlineSendHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{&btnAcceptInlineReceive}, @@ -307,7 +332,7 @@ func (bot TipBot) getHandler() []Handler { Handler: bot.cancelInlineReceiveHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{&btnAcceptInlineFaucet}, @@ -321,35 +346,35 @@ func (bot TipBot) getHandler() []Handler { Handler: bot.cancelInlineFaucetHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{&btnAcceptInlineTipjar}, Handler: bot.acceptInlineTipjarHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{&btnCancelInlineTipjar}, Handler: bot.cancelInlineTipjarHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{&btnWithdraw}, Handler: bot.confirmWithdrawHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, { Endpoints: []interface{}{&btnCancelWithdraw}, Handler: bot.cancelWithdrawHandler, Interceptor: &Interceptor{ Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Before: []intercept.Func{bot.requireUserInterceptor}}, }, } } diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index 1724d262..7326982e 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -95,6 +95,8 @@ func (bot TipBot) inlineQueryReplyWithError(q *tb.Query, message string, help st } } +// anyChosenInlineHandler will load any inline object from cache and store into bunt. +// this is used to decrease bunt db write ops. func (bot TipBot) anyChosenInlineHandler(q *tb.ChosenInlineResult) { // load inline object from cache inlineObject, err := bot.Cache.Get(q.ResultID) diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go index 9b106643..c07e1595 100644 --- a/internal/telegram/intercept/callback.go +++ b/internal/telegram/intercept/callback.go @@ -13,6 +13,7 @@ type handlerCallbackInterceptor struct { handler CallbackFuncHandler before CallbackChain after CallbackChain + onDefer CallbackChain } type CallbackChain []Func type CallbackInterceptOption func(*handlerCallbackInterceptor) @@ -27,6 +28,11 @@ func WithAfterCallback(chain ...Func) CallbackInterceptOption { a.after = chain } } +func WithDeferCallback(chain ...Func) CallbackInterceptOption { + return func(a *handlerCallbackInterceptor) { + a.onDefer = chain + } +} func interceptCallback(ctx context.Context, message *tb.Callback, hm CallbackChain) (context.Context, error) { if ctx == nil { @@ -50,7 +56,9 @@ func HandlerWithCallback(handler CallbackFuncHandler, option ...CallbackIntercep opt(hm) } return func(c *tb.Callback) { - ctx, err := interceptCallback(context.Background(), c, hm.before) + ctx := context.Background() + defer interceptCallback(ctx, c, hm.onDefer) + ctx, err := interceptCallback(ctx, c, hm.before) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go index 7a7327d9..f6bbd4ca 100644 --- a/internal/telegram/intercept/message.go +++ b/internal/telegram/intercept/message.go @@ -16,6 +16,7 @@ type handlerMessageInterceptor struct { handler MessageFuncHandler before MessageChain after MessageChain + onDefer MessageChain } type MessageChain []Func type MessageInterceptOption func(*handlerMessageInterceptor) @@ -30,6 +31,11 @@ func WithAfterMessage(chain ...Func) MessageInterceptOption { a.after = chain } } +func WithDeferMessage(chain ...Func) MessageInterceptOption { + return func(a *handlerMessageInterceptor) { + a.onDefer = chain + } +} func interceptMessage(ctx context.Context, message *tb.Message, hm MessageChain) (context.Context, error) { if ctx == nil { @@ -54,7 +60,9 @@ func HandlerWithMessage(handler MessageFuncHandler, option ...MessageInterceptOp opt(hm) } return func(message *tb.Message) { - ctx, err := interceptMessage(context.Background(), message, hm.before) + ctx := context.Background() + defer interceptMessage(ctx, message, hm.onDefer) + ctx, err := interceptMessage(ctx, message, hm.before) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go index 52c5e5f5..41fe6716 100644 --- a/internal/telegram/intercept/query.go +++ b/internal/telegram/intercept/query.go @@ -13,6 +13,7 @@ type handlerQueryInterceptor struct { handler QueryFuncHandler before QueryChain after QueryChain + onDefer QueryChain } type QueryChain []Func type QueryInterceptOption func(*handlerQueryInterceptor) @@ -27,6 +28,11 @@ func WithAfterQuery(chain ...Func) QueryInterceptOption { a.after = chain } } +func WithDeferQuery(chain ...Func) QueryInterceptOption { + return func(a *handlerQueryInterceptor) { + a.onDefer = chain + } +} func interceptQuery(ctx context.Context, message *tb.Query, hm QueryChain) (context.Context, error) { if ctx == nil { @@ -49,14 +55,16 @@ func HandlerWithQuery(handler QueryFuncHandler, option ...QueryInterceptOption) for _, opt := range option { opt(hm) } - return func(message *tb.Query) { - ctx, err := interceptQuery(context.Background(), message, hm.before) + return func(query *tb.Query) { + ctx := context.Background() + defer interceptQuery(ctx, query, hm.onDefer) + ctx, err := interceptQuery(context.Background(), query, hm.before) if err != nil { log.Traceln(err) return } - hm.handler(ctx, message) - _, err = interceptQuery(ctx, message, hm.after) + hm.handler(ctx, query) + _, err = interceptQuery(ctx, query, hm.after) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 2a1251e3..fb77cc3c 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "sync" "github.com/LightningTipBot/LightningTipBot/internal/i18n" i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" @@ -21,36 +22,75 @@ const ( QueryInterceptor ) +func init() { + handlerUserMutex = make(map[int64]*sync.Mutex) +} + var invalidTypeError = fmt.Errorf("invalid type") type Interceptor struct { - Type InterceptorType - Before []intercept.Func - After []intercept.Func + Type InterceptorType + Before []intercept.Func + After []intercept.Func + OnDefer []intercept.Func +} +type HandlerMutex map[int64]*sync.Mutex + +var handlerUserMutex HandlerMutex + +func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + user := getTelegramUserFromInterface(i) + if user != nil { + handlerUserMutex[user.ID].Unlock() + } + return ctx, nil } +func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + user := getTelegramUserFromInterface(i) + if user != nil { + if handlerUserMutex[user.ID] == nil { + handlerUserMutex[user.ID] = &sync.Mutex{} + } + handlerUserMutex[user.ID].Lock() + return ctx, nil + } + return nil, invalidTypeError +} + +// requireUserInterceptor will return an error if user is not found +// user is here an lnbits.User func (bot TipBot) requireUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - switch i.(type) { - case *tb.Query: - user, err := GetUser(&i.(*tb.Query).From, bot) - return context.WithValue(ctx, "user", user), err - case *tb.Callback: - c := i.(*tb.Callback) - m := *c.Message - m.Sender = c.Sender - user, err := GetUser(i.(*tb.Callback).Sender, bot) - return context.WithValue(ctx, "user", user), err - case *tb.Message: - user, err := GetUser(i.(*tb.Message).Sender, bot) - return context.WithValue(ctx, "user", user), err + var user *lnbits.User + var err error + u := getTelegramUserFromInterface(i) + if u != nil { + user, err = GetUser(u, bot) + if user != nil { + return context.WithValue(ctx, "user", user), err + } } return nil, invalidTypeError } + func (bot TipBot) loadUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { ctx, _ = bot.requireUserInterceptor(ctx, i) return ctx, nil } +// getTelegramUserFromInterface returns the tb user based in interface type +func getTelegramUserFromInterface(i interface{}) (user *tb.User) { + switch i.(type) { + case *tb.Query: + user = &i.(*tb.Query).From + case *tb.Callback: + user = i.(*tb.Callback).Sender + case *tb.Message: + user = i.(*tb.Message).Sender + } + return +} + // loadReplyToInterceptor Loading the Telegram user with message intercept func (bot TipBot) loadReplyToInterceptor(ctx context.Context, i interface{}) (context.Context, error) { switch i.(type) { diff --git a/internal/telegram/link.go b/internal/telegram/link.go index cd666f0d..d7dcd076 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -47,8 +47,11 @@ func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { // send the link to the user linkmsg := bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lndhubUrl)}) - time.Sleep(time.Second * 60) - bot.tryDeleteMessage(linkmsg) + + go func() { + time.Sleep(time.Second * 60) + bot.tryDeleteMessage(linkmsg) + }() bot.trySendMessage(m.Sender, Translate(ctx, "linkHiddenMessage")) // auto delete the message // NewMessage(linkmsg, WithDuration(time.Second*time.Duration(internal.Configuration.Telegram.MessageDisposeDuration), bot)) From 1b922c03a91a38682fa0a57cd0136afef2c744a4 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 8 Dec 2021 09:55:56 +0100 Subject: [PATCH 072/541] mutex interceptor patch to prevent concurrent map read / write (#157) * add handlerMapMutex * cleanup --- internal/telegram/interceptor.go | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index fb77cc3c..f48c5fab 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -23,7 +23,8 @@ const ( ) func init() { - handlerUserMutex = make(map[int64]*sync.Mutex) + handlerMutex = make(map[int64]*sync.Mutex) + handlerMapMutex = &sync.Mutex{} } var invalidTypeError = fmt.Errorf("invalid type") @@ -34,25 +35,34 @@ type Interceptor struct { After []intercept.Func OnDefer []intercept.Func } -type HandlerMutex map[int64]*sync.Mutex -var handlerUserMutex HandlerMutex +// 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 { - handlerUserMutex[user.ID].Unlock() + handlerMapMutex.Lock() + handlerMutex[user.ID].Unlock() + handlerMapMutex.Unlock() } return ctx, nil } +// lockInterceptor invoked as first before interceptor func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { user := getTelegramUserFromInterface(i) if user != nil { - if handlerUserMutex[user.ID] == nil { - handlerUserMutex[user.ID] = &sync.Mutex{} + handlerMapMutex.Lock() + defer handlerMapMutex.Unlock() + if handlerMutex[user.ID] == nil { + handlerMutex[user.ID] = &sync.Mutex{} } - handlerUserMutex[user.ID].Lock() + handlerMutex[user.ID].Lock() return ctx, nil } return nil, invalidTypeError From f50e6ced719b8940f29b9611944cdedbbbfd0fb9 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 8 Dec 2021 22:22:11 +0100 Subject: [PATCH 073/541] test mutex (#158) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/interceptor.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index f48c5fab..3d2e96d9 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -47,7 +47,9 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context user := getTelegramUserFromInterface(i) if user != nil { handlerMapMutex.Lock() - handlerMutex[user.ID].Unlock() + if handlerMutex[user.ID] != nil { + handlerMutex[user.ID].Unlock() + } handlerMapMutex.Unlock() } return ctx, nil @@ -58,10 +60,10 @@ func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.C user := getTelegramUserFromInterface(i) if user != nil { handlerMapMutex.Lock() - defer handlerMapMutex.Unlock() if handlerMutex[user.ID] == nil { handlerMutex[user.ID] = &sync.Mutex{} } + handlerMapMutex.Unlock() handlerMutex[user.ID].Lock() return ctx, nil } From 1ea9268698ed3b52b09102f797eccaf89ac178b2 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 11 Dec 2021 22:09:52 +0100 Subject: [PATCH 074/541] better (#160) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 5 ++--- internal/telegram/pay.go | 4 +++- internal/telegram/send.go | 5 +++++ 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 990e7e07..fb2c4297 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -184,7 +184,7 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { ctx = bot.mapFaucetLanguage(ctx, m.Text) inlineFaucet, err := bot.makeFaucet(ctx, m, false) if err != nil { - log.Errorf("[faucet] %s", err) + log.Warnf("[faucet] %s", err.Error()) return } fromUserStr := GetUserStr(m.Sender) @@ -273,7 +273,6 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // check if user exists and create a wallet if not _, exists := bot.UserExists(to.Telegram) if !exists { - log.Infof("[faucet] User %s has no wallet.", toUserStr) to, err = bot.CreateWalletForTelegramUser(to.Telegram) if err != nil { errmsg := fmt.Errorf("[faucet] Error: Could not create wallet for %s", toUserStr) @@ -295,7 +294,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if !success { bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) errMsg := fmt.Sprintf("[faucet] Transaction failed: %s", err) - log.Errorln(errMsg) + log.Warnln(errMsg) return } diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index ff275be1..a46b6763 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -200,6 +200,8 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { }, }, ) + + log.Infof("[/pay] Attempting %s's invoice %s (%d sat)", userStr, payData.ID, payData.Amount) // pay invoice invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoiceString}, bot.Client) if err != nil { @@ -225,7 +227,7 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { bot.trySendMessage(c.Sender, i18n.Translate(payData.LanguageCode, "invoicePaidMessage")) bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePublicPaidMessage"), userStr), &tb.ReplyMarkup{}) } - log.Printf("[⚡️ pay] User %s paid invoice %s (%d sat)", userStr, payData.ID, payData.Amount) + log.Infof("[⚡️ pay] User %s paid invoice %s (%d sat)", userStr, payData.ID, payData.Amount) return } diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 2b8e0388..ba6ec68c 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -159,6 +159,11 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { return } + if user.ID == toUserDb.ID { + bot.trySendMessage(m.Sender, Translate(ctx, "sendYourselfMessage")) + return + } + // entire text of the inline object confirmText := fmt.Sprintf(Translate(ctx, "confirmSendMessage"), str.MarkdownEscape(toUserStrMention), amount) if len(sendMemo) > 0 { From 0cfb6936195788edc347178c6124bfdbf4e8ed77 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 11 Dec 2021 23:17:51 +0100 Subject: [PATCH 075/541] int64 amounts (#159) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/amounts.go | 12 ++++++------ internal/telegram/helpers.go | 4 ++-- internal/telegram/inline_faucet.go | 8 ++++---- internal/telegram/inline_receive.go | 2 +- internal/telegram/inline_send.go | 2 +- internal/telegram/inline_tipjar.go | 8 ++++---- internal/telegram/lnurl-pay.go | 6 +++--- internal/telegram/lnurl-withdraw.go | 4 ++-- internal/telegram/pay.go | 2 +- internal/telegram/send.go | 2 +- internal/telegram/tooltip.go | 8 ++++---- internal/telegram/tooltip_test.go | 2 +- internal/telegram/transaction.go | 6 +++--- internal/telegram/users.go | 8 ++++---- 14 files changed, 37 insertions(+), 37 deletions(-) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 8d54bada..3091e3f0 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -24,7 +24,7 @@ func getArgumentFromCommand(input string, which int) (output string, err error) return output, nil } -func decodeAmountFromCommand(input string) (amount int, err error) { +func decodeAmountFromCommand(input string) (amount int64, err error) { if len(strings.Split(input, " ")) < 2 { errmsg := "message doesn't contain any amount" // log.Errorln(errmsg) @@ -34,14 +34,14 @@ func decodeAmountFromCommand(input string) (amount int, err error) { return amount, err } -func getAmount(input string) (amount int, err error) { +func getAmount(input string) (amount int64, err error) { // convert something like 1.2k into 1200 if strings.HasSuffix(strings.ToLower(input), "k") { fmount, err := strconv.ParseFloat(strings.TrimSpace(input[:len(input)-1]), 64) if err != nil { return 0, err } - amount = int(fmount * 1000) + amount = int64(fmount * 1000) return amount, err } @@ -61,13 +61,13 @@ func getAmount(input string) (amount int, err error) { if !(price.Price[currency] > 0) { return 0, errors.New("price is zero") } - amount = int(fmount / price.Price[currency] * float64(100_000_000)) + amount = int64(fmount / price.Price[currency] * float64(100_000_000)) return amount, nil } } // use plain integer as satoshis - amount, err = strconv.Atoi(input) + amount, err = strconv.ParseInt(input, 10, 64) if err != nil { return 0, err } @@ -145,7 +145,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { } // amount not in allowed range from LNURL if EnterAmountStateData.AmountMin > 0 && EnterAmountStateData.AmountMax >= EnterAmountStateData.AmountMin && // this line checks whether min_max is set at all - (amount > int(EnterAmountStateData.AmountMax/1000) || amount < int(EnterAmountStateData.AmountMin/1000)) { // this line then checks whether the amount is in the range + (amount > int64(EnterAmountStateData.AmountMax/1000) || amount < int64(EnterAmountStateData.AmountMin/1000)) { // this line then checks whether the amount is in the range err = fmt.Errorf("amount not in range") log.Warnf("[enterAmountHandler] %s", err.Error()) bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), EnterAmountStateData.AmountMin/1000, EnterAmountStateData.AmountMax/1000)) diff --git a/internal/telegram/helpers.go b/internal/telegram/helpers.go index 4d1bb378..74e42828 100644 --- a/internal/telegram/helpers.go +++ b/internal/telegram/helpers.go @@ -34,7 +34,7 @@ func GetMemoFromCommand(command string, fromWord int) string { return memo } -func MakeProgressbar(current int, total int) string { +func MakeProgressbar(current int64, total int64) string { MAX_BARS := 16 progress := math.Round((float64(current) / float64(total)) * float64(MAX_BARS)) progressbar := strings.Repeat("🟩", int(progress)) @@ -42,7 +42,7 @@ func MakeProgressbar(current int, total int) string { return progressbar } -func MakeTipjarbar(current int, total int) string { +func MakeTipjarbar(current int64, total int64) string { MAX_BARS := 16 progress := math.Round((float64(current) / float64(total)) * float64(MAX_BARS)) progressbar := strings.Repeat("🍯", int(progress)) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index fb2c4297..8290b621 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -28,9 +28,9 @@ var ( type InlineFaucet struct { *transaction.Base Message string `json:"inline_faucet_message"` - Amount int `json:"inline_faucet_amount"` - RemainingAmount int `json:"inline_faucet_remainingamount"` - PerUserAmount int `json:"inline_faucet_peruseramount"` + Amount int64 `json:"inline_faucet_amount"` + RemainingAmount int64 `json:"inline_faucet_remainingamount"` + PerUserAmount int64 `json:"inline_faucet_peruseramount"` From *lnbits.User `json:"inline_faucet_from"` To []*lnbits.User `json:"inline_faucet_to"` Memo string `json:"inline_faucet_memo"` @@ -64,7 +64,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User if perUserAmount < 1 || amount%perUserAmount != 0 { return nil, errors.New(errors.InvalidAmountPerUserError, fmt.Errorf("invalid amount per user")) } - nTotal := amount / perUserAmount + nTotal := int(amount / perUserAmount) fromUser := LoadUser(ctx) fromUserStr := GetUserStr(sender) balance, err := bot.GetUserBalanceCached(fromUser) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index f9d4919b..a1e8e6da 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -26,7 +26,7 @@ var ( type InlineReceive struct { *transaction.Base Message string `json:"inline_receive_message"` - Amount int `json:"inline_receive_amount"` + Amount int64 `json:"inline_receive_amount"` From *lnbits.User `json:"inline_receive_from"` To *lnbits.User `json:"inline_receive_to"` From_SpecificUser bool `json:"from_specific_user"` diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 83d0325a..87c7b5dd 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -27,7 +27,7 @@ var ( type InlineSend struct { *transaction.Base Message string `json:"inline_send_message"` - Amount int `json:"inline_send_amount"` + Amount int64 `json:"inline_send_amount"` From *lnbits.User `json:"inline_send_from"` To *lnbits.User `json:"inline_send_to"` To_SpecificUser bool `json:"to_specific_user"` diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 0c2a3519..2fc2ac33 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -27,9 +27,9 @@ var ( type InlineTipjar struct { *transaction.Base Message string `json:"inline_tipjar_message"` - Amount int `json:"inline_tipjar_amount"` - GivenAmount int `json:"inline_tipjar_givenamount"` - PerUserAmount int `json:"inline_tipjar_peruseramount"` + Amount int64 `json:"inline_tipjar_amount"` + GivenAmount int64 `json:"inline_tipjar_givenamount"` + PerUserAmount int64 `json:"inline_tipjar_peruseramount"` To *lnbits.User `json:"inline_tipjar_to"` From []*lnbits.User `json:"inline_tipjar_from"` Memo string `json:"inline_tipjar_memo"` @@ -62,7 +62,7 @@ func (bot TipBot) createTipjar(ctx context.Context, text string, sender *tb.User if perUserAmount < 1 || amount%perUserAmount != 0 { return nil, errors.New(errors.InvalidAmountPerUserError, fmt.Errorf("invalid amount per user")) } - nTotal := amount / perUserAmount + nTotal := int(amount / perUserAmount) toUser := LoadUser(ctx) // toUserStr := GetUserStr(sender) // // check for memo in command diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index dff01773..1a000623 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -23,7 +23,7 @@ type LnurlPayState struct { From *lnbits.User `json:"from"` LNURLPayParams lnurl.LNURLPayParams `json:"LNURLPayParams"` LNURLPayValues lnurl.LNURLPayValues `json:"LNURLPayValues"` - Amount int `json:"amount"` + Amount int64 `json:"amount"` Comment string `json:"comment"` LanguageCode string `json:"languagecode"` } @@ -151,7 +151,7 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { } qs := callbackUrl.Query() // add amount to query string - qs.Set("amount", strconv.Itoa(lnurlPayState.Amount)) // msat + qs.Set("amount", strconv.FormatInt(lnurlPayState.Amount, 10)) // msat // add comment to query string if len(lnurlPayState.Comment) > 0 { qs.Set("comment", lnurlPayState.Comment) @@ -192,7 +192,7 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { bot.payHandler(ctx, m) } -func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, address string, amount int) error { +func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, address string, amount int64) error { split := strings.Split(address, "@") if len(split) != 2 { return fmt.Errorf("lightning address format wrong") diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index bc6088f6..0683e5c0 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -30,7 +30,7 @@ type LnurlWithdrawState struct { From *lnbits.User `json:"from"` LNURLWithdrawResponse lnurl.LNURLWithdrawResponse `json:"LNURLWithdrawResponse"` LNURResponse lnurl.LNURLResponse `json:"LNURLResponse"` - Amount int `json:"amount"` + Amount int64 `json:"amount"` Comment string `json:"comment"` LanguageCode string `json:"languagecode"` Success bool `json:"success"` @@ -83,7 +83,7 @@ func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, with // if no amount is entered, and if only one amount is possible, we use it if amount_err != nil && LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable == LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable { - amount = int(LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable / 1000) + amount = int64(LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable / 1000) amount_err = nil } diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index a46b6763..2d6ee7d7 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -76,7 +76,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { log.Errorln(errmsg) return } - amount := int(bolt11.MSatoshi / 1000) + amount := int64(bolt11.MSatoshi / 1000) if amount <= 0 { bot.trySendMessage(m.Sender, Translate(ctx, "invoiceNoAmountMessage")) diff --git a/internal/telegram/send.go b/internal/telegram/send.go index ba6ec68c..28fb4257 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -265,7 +265,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { fromUserStr := GetUserStr(from.Telegram) transactionMemo := fmt.Sprintf("Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount) - t := NewTransaction(bot, from, to, int(amount), TransactionType("send")) + t := NewTransaction(bot, from, to, amount, TransactionType("send")) t.Memo = transactionMemo success, err := t.Send() diff --git a/internal/telegram/tooltip.go b/internal/telegram/tooltip.go index 9da54674..fa4ae0bd 100644 --- a/internal/telegram/tooltip.go +++ b/internal/telegram/tooltip.go @@ -26,7 +26,7 @@ const ( type TipTooltip struct { Message - TipAmount int `json:"tip_amount"` + TipAmount int64 `json:"tip_amount"` Ntips int `json:"ntips"` LastTip time.Time `json:"last_tip"` Tippers []*tb.User `json:"tippers"` @@ -36,7 +36,7 @@ const maxNamesInTipperMessage = 5 type TipTooltipOption func(m *TipTooltip) -func TipAmount(amount int) TipTooltipOption { +func TipAmount(amount int64) TipTooltipOption { return func(m *TipTooltip) { m.TipAmount = amount } @@ -110,7 +110,7 @@ func tipTooltipExists(replyToId int, bot *TipBot) (bool, *TipTooltip) { } // tipTooltipHandler function to update the tooltip below a tipped message. either updates or creates initial tip tool tip -func tipTooltipHandler(m *tb.Message, bot *TipBot, amount int, initializedWallet bool) (hasTip bool) { +func tipTooltipHandler(m *tb.Message, bot *TipBot, amount int64, initializedWallet bool) (hasTip bool) { // todo: this crashes if the tooltip message (maybe also the original tipped message) was deleted in the mean time!!! need to check for existence! hasTip, ttt := tipTooltipExists(m.ReplyTo.ID, bot) if hasTip { @@ -144,7 +144,7 @@ func tipTooltipHandler(m *tb.Message, bot *TipBot, amount int, initializedWallet } // updateToolTip updates existing tip tool tip in Telegram -func (ttt *TipTooltip) updateTooltip(bot *TipBot, user *tb.User, amount int, notInitializedWallet bool) error { +func (ttt *TipTooltip) updateTooltip(bot *TipBot, user *tb.User, amount int64, notInitializedWallet bool) error { ttt.TipAmount += amount ttt.Ntips += 1 ttt.Tippers = appendUinqueUsersToSlice(ttt.Tippers, user) diff --git a/internal/telegram/tooltip_test.go b/internal/telegram/tooltip_test.go index b007a46b..bd4beb87 100644 --- a/internal/telegram/tooltip_test.go +++ b/internal/telegram/tooltip_test.go @@ -45,7 +45,7 @@ func Test_getTippersString(t *testing.T) { func TestMessage_getTooltipMessage(t *testing.T) { type fields struct { Message Message - TipAmount int + TipAmount int64 Ntips int LastTip time.Time Tippers []*tb.User diff --git a/internal/telegram/transaction.go b/internal/telegram/transaction.go index 80e20bb6..9ce6f6a8 100644 --- a/internal/telegram/transaction.go +++ b/internal/telegram/transaction.go @@ -21,7 +21,7 @@ type Transaction struct { FromUser string `json:"from_user"` ToUser string `json:"to_user"` Type string `json:"type"` - Amount int `json:"amount"` + Amount int64 `json:"amount"` ChatID int64 `json:"chat_id"` ChatName string `json:"chat_name"` Memo string `json:"memo"` @@ -48,7 +48,7 @@ func TransactionType(transactionType string) TransactionOption { } } -func NewTransaction(bot *TipBot, from *lnbits.User, to *lnbits.User, amount int, opts ...TransactionOption) *Transaction { +func NewTransaction(bot *TipBot, from *lnbits.User, to *lnbits.User, amount int64, opts ...TransactionOption) *Transaction { t := &Transaction{ Bot: bot, From: from, @@ -94,7 +94,7 @@ func (t *Transaction) Send() (success bool, err error) { return success, err } -func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits.User, amount int, memo string) (bool, error) { +func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits.User, amount int64, memo string) (bool, error) { fromUserStr := GetUserStr(from.Telegram) toUserStr := GetUserStr(to.Telegram) diff --git a/internal/telegram/users.go b/internal/telegram/users.go index 6365012c..be9ed7d1 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -60,16 +60,16 @@ func appendUinqueUsersToSlice(slice []*tb.User, i *tb.User) []*tb.User { return append(slice, i) } -func (bot *TipBot) GetUserBalanceCached(user *lnbits.User) (amount int, err error) { +func (bot *TipBot) GetUserBalanceCached(user *lnbits.User) (amount int64, err error) { u, err := bot.Cache.Get(fmt.Sprintf("%s_balance", user.Name)) if err != nil { return bot.GetUserBalance(user) } - cachedBalance := u.(int) + cachedBalance := u.(int64) return cachedBalance, nil } -func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int, err error) { +func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int64, err error) { wallet, err := bot.Client.Info(*user.Wallet) if err != nil { errmsg := fmt.Sprintf("[GetUserBalance] Error: Couldn't fetch user %s's info from LNbits: %s", GetUserStr(user.Telegram), err.Error()) @@ -82,7 +82,7 @@ func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int, err error) { return } // msat to sat - amount = int(wallet.Balance) / 1000 + amount = int64(wallet.Balance) / 1000 log.Infof("[GetUserBalance] %s's balance: %d sat\n", GetUserStr(user.Telegram), amount) // update user balance in cache From 5b93d43bc513ad91ea83cba280a402a07f0e3e7d Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 14 Dec 2021 18:06:37 +0100 Subject: [PATCH 076/541] speed up tx lock (#161) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/storage/transaction/transaction.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index 08319157..560d957c 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -75,13 +75,13 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error } // to avoid race conditions, we block the call if there is // already an active transaction by loop until InTransaction is false - ticker := time.NewTicker(time.Second * 2) + ticker := time.NewTicker(time.Millisecond * 200) for tx.InTransaction { select { case <-ticker.C: return nil, fmt.Errorf("transaction timeout") default: - time.Sleep(time.Duration(500) * time.Millisecond) + time.Sleep(time.Duration(100) * time.Millisecond) err = db.Get(s) } } From 6299adf633579cfe853a878aa1f64445b1680baa Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 14 Dec 2021 19:44:16 +0100 Subject: [PATCH 077/541] Faucet per_user minimum 5 sat, Tipjar Button reverse (#162) * tipjar and faucet change Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 2 +- internal/telegram/inline_tipjar.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 8290b621..6e44066b 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -61,7 +61,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User if err != nil { return nil, errors.New(errors.InvalidAmountError, err) } - if perUserAmount < 1 || amount%perUserAmount != 0 { + if perUserAmount < 5 || amount%perUserAmount != 0 { return nil, errors.New(errors.InvalidAmountPerUserError, fmt.Errorf("invalid amount per user")) } nTotal := int(amount / perUserAmount) diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 2fc2ac33..a4a0626f 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -162,10 +162,10 @@ func (bot TipBot) makeTipjarKeyboard(ctx context.Context, inlineTipjar *InlineTi // inlineTipjarMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} // slice of buttons buttons := make([]tb.Btn, 0) - acceptInlineTipjarButton := inlineTipjarMenu.Data(Translate(ctx, "payReceiveButtonMessage"), "confirm_tipjar_inline", inlineTipjar.ID) - buttons = append(buttons, acceptInlineTipjarButton) cancelInlineTipjarButton := inlineTipjarMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_tipjar_inline", inlineTipjar.ID) buttons = append(buttons, cancelInlineTipjarButton) + acceptInlineTipjarButton := inlineTipjarMenu.Data(Translate(ctx, "payReceiveButtonMessage"), "confirm_tipjar_inline", inlineTipjar.ID) + buttons = append(buttons, acceptInlineTipjarButton) inlineTipjarMenu.Inline( inlineTipjarMenu.Row(buttons...)) From 1815bcfaf973bd4879c2aafca96db75682818e86 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 17 Dec 2021 09:51:52 +0000 Subject: [PATCH 078/541] Receive button order (#163) * receive button reorder Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_receive.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index a1e8e6da..01d161a2 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -41,8 +41,9 @@ func (bot TipBot) makeReceiveKeyboard(ctx context.Context, id string) *tb.ReplyM cancelInlineReceiveButton.Data = id inlineReceiveMenu.Inline( inlineReceiveMenu.Row( + cancelInlineReceiveButton, acceptInlineReceiveButton, - cancelInlineReceiveButton), + ), ) return inlineReceiveMenu } From 9e4b612c261bc94ff7add7e77bcf3234f1c530c0 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 17 Dec 2021 20:41:13 +0000 Subject: [PATCH 079/541] Invoice events for inline receive (#164) * speed up tx lock * receive via invoice * update telebot to save message InlineID * remove debug ifs * correct response * comment Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- go.mod | 2 +- go.sum | 2 + internal/lnbits/webhook/webhook.go | 26 ++++-- internal/telegram/bot.go | 1 + internal/telegram/inline_receive.go | 134 +++++++++++++++++++++++----- internal/telegram/invoice.go | 87 ++++++++++++++++-- 6 files changed, 216 insertions(+), 36 deletions(-) diff --git a/go.mod b/go.mod index d5883c19..c6a16487 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/tidwall/gjson v1.10.2 golang.org/x/text v0.3.5 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 - gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211207185736-224e4f70c5ad + gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211217193303-c005cce171ac gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.12 ) diff --git a/go.sum b/go.sum index b94a5aa0..ffd9a2e5 100644 --- a/go.sum +++ b/go.sum @@ -730,6 +730,8 @@ gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955 h1:uhQU gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211201074627-babf9f2cc955/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211207185736-224e4f70c5ad h1:HR3vqf1DS7hEQ0j77J4ctumZgzDIHNXZtag60U9OOig= gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211207185736-224e4f70c5ad/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211217193303-c005cce171ac h1:EcXyrTUaQJYiHtLQstXdW04sRH9HwH9mpaxy9Lkcomc= +gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211217193303-c005cce171ac/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index c18c4cbb..fdea4655 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -103,17 +103,33 @@ func (w Server) receive(writer http.ResponseWriter, request *http.Request) { return } log.Infoln(fmt.Sprintf("[⚡️ WebHook] User %s (%d) received invoice of %d sat.", telegram.GetUserStr(user.Telegram), user.Telegram.ID, depositEvent.Amount/1000)) + + writer.WriteHeader(200) + + // trigger invoice events + txInvoiceEvent := &telegram.InvocieEvent{PaymentHash: depositEvent.PaymentHash} + err = w.buntdb.Get(txInvoiceEvent) + if err != nil { + log.Errorln(err) + } else { + // do something with the event + if c := telegram.InvoiceCallback[txInvoiceEvent.Callback]; c != nil { + c(txInvoiceEvent) + return + } + } + + // else, send a message to the user if there is no callback for this invoice _, err = w.bot.Send(user.Telegram, fmt.Sprintf(i18n.Translate(user.Telegram.LanguageCode, "invoiceReceivedMessage"), depositEvent.Amount/1000)) if err != nil { log.Errorln(err) } - // if this invoice is saved in bunt.db, we load it and display the comment from an LNURL invoice + // legacy (should be replaced with the invoice listener above) + // check if invoice corresponds to an LNURL-p request, we load it and display the comment from an LNURL invoice tx := &lnurl.Invoice{PaymentHash: depositEvent.PaymentHash} err = w.buntdb.Get(tx) - if err != nil { - log.Errorln(err) - } else { + if err == nil { if len(tx.Comment) > 0 { _, err = w.bot.Send(user.Telegram, fmt.Sprintf(`✉️ %s`, str.MarkdownEscape(tx.Comment))) if err != nil { @@ -121,5 +137,5 @@ func (w Server) receive(writer http.ResponseWriter, request *http.Request) { } } } - writer.WriteHeader(200) + } diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index a5b9876a..5ff8dc85 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -89,5 +89,6 @@ func (bot TipBot) Start() { log.Errorf("Could not initialize bot wallet: %s", err.Error()) } bot.registerTelegramHandlers() + initInvoiceEventCallbacks(bot) bot.Telegram.Start() } diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 01d161a2..a953c9a2 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -1,12 +1,14 @@ package telegram import ( + "bytes" "context" "fmt" "strings" "time" "github.com/eko/gocache/store" + "github.com/skip2/go-qrcode" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -25,7 +27,8 @@ var ( type InlineReceive struct { *transaction.Base - Message string `json:"inline_receive_message"` + MessageText string `json:"inline_receive_messagetext"` + Message *tb.Message `json:"inline_receive_message"` Amount int64 `json:"inline_receive_amount"` From *lnbits.User `json:"inline_receive_from"` To *lnbits.User `json:"inline_receive_to"` @@ -117,7 +120,7 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { // create persistend inline send struct inlineReceive := InlineReceive{ Base: transaction.New(transaction.ID(id)), - Message: inlineMessage, + MessageText: inlineMessage, To: to, Memo: memo, Amount: amount, @@ -147,25 +150,14 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac return } inlineReceive := rn.(*InlineReceive) - err = inlineReceive.Lock(inlineReceive, bot.Bunt) - if err != nil { - log.Errorf("[acceptInlineReceiveHandler] %s", err) - return - } - if !inlineReceive.Active { log.Errorf("[acceptInlineReceiveHandler] inline receive not active anymore") return } - defer inlineReceive.Release(inlineReceive, bot.Bunt) - // user `from` is the one who is SENDING // user `to` is the one who is RECEIVING from := LoadUser(ctx) - if from.Wallet == nil { - return - } // check if this payment is requested from a specific user if inlineReceive.From_SpecificUser { if inlineReceive.From.Telegram.ID != from.Telegram.ID { @@ -175,18 +167,57 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac } else { // otherwise, we just set it to the user who has clicked inlineReceive.From = from + } + inlineReceive.Message = c.Message + runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) to := inlineReceive.To - toUserStrMd := GetUserStrMd(to.Telegram) - fromUserStrMd := GetUserStrMd(from.Telegram) - toUserStr := GetUserStr(to.Telegram) - fromUserStr := GetUserStr(from.Telegram) - if from.Telegram.ID == to.Telegram.ID { bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) return } + + if from.Wallet == nil || from.Wallet.Balance < inlineReceive.Amount { + // if user has no wallet, show invoice + bot.tryEditMessage(inlineReceive.Message, inlineReceive.MessageText, &tb.ReplyMarkup{}) + // runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) + bot.inlineReceiveInvoice(ctx, c, inlineReceive) + return + } else { + // else, do an internal transaction + bot.sendInlineReceiveHandler(ctx, c) + return + } +} + +func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) { + tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} + rn, err := tx.Get(tx, bot.Bunt) + // immediatelly set intransaction to block duplicate calls + if err != nil { + log.Errorf("[getInlineReceive] %s", err) + return + } + inlineReceive := rn.(*InlineReceive) + err = inlineReceive.Lock(inlineReceive, bot.Bunt) + if err != nil { + log.Errorf("[acceptInlineReceiveHandler] %s", err) + return + } + + if !inlineReceive.Active { + log.Errorf("[acceptInlineReceiveHandler] inline receive not active anymore") + return + } + + // defer inlineReceive.Release(inlineReceive, bot.Bunt) + + // from := inlineReceive.From + from := LoadUser(ctx) + to := inlineReceive.To + toUserStr := GetUserStr(to.Telegram) + fromUserStr := GetUserStr(from.Telegram) // balance check of the user balance, err := bot.GetUserBalanceCached(from) if err != nil { @@ -217,18 +248,76 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac } log.Infof("[💸 inlineReceive] Send from %s to %s (%d sat).", fromUserStr, toUserStr, inlineReceive.Amount) + inlineReceive.Release(inlineReceive, bot.Bunt) + bot.finishInlineReceiveHandler(ctx, c) +} + +func (bot *TipBot) inlineReceiveInvoice(ctx context.Context, c *tb.Callback, inlineReceive *InlineReceive) { + if !inlineReceive.Active { + log.Errorf("[acceptInlineReceiveHandler] inline receive not active anymore") + return + } + invoice, err := bot.createInvoiceEvent(ctx, inlineReceive.To, inlineReceive.Amount, fmt.Sprintf("Pay to %s", GetUserStr(inlineReceive.To.Telegram)), InvoiceCallbackInlineReceive, inlineReceive.ID) + if err != nil { + errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) + bot.tryEditMessage(inlineReceive.Message, Translate(ctx, "errorTryLaterMessage")) + log.Errorln(errmsg) + return + } + + // create qr code + qr, err := qrcode.Encode(invoice.PaymentRequest, qrcode.Medium, 256) + if err != nil { + errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err) + bot.tryEditMessage(inlineReceive.Message, Translate(ctx, "errorTryLaterMessage")) + log.Errorln(errmsg) + return + } + + // send the invoice data to user + var msg *tb.Message + if inlineReceive.Message.Chat != nil { + msg = bot.trySendMessage(inlineReceive.Message.Chat, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) + } else { + msg = bot.trySendMessage(c.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) + bot.tryEditMessage(inlineReceive.Message, fmt.Sprintf("%s\n\nPay this invoice:\n```%s```", inlineReceive.MessageText, invoice.PaymentRequest)) + } + invoice.InvoiceMessage = msg + runtime.IgnoreError(bot.Bunt.Set(invoice)) + log.Printf("[/invoice] Incvoice created. User: %s, amount: %d sat.", GetUserStr(inlineReceive.To.Telegram), inlineReceive.Amount) + +} +func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvocieEvent) { + bot.tryDeleteMessage(invoiceEvent.InvoiceMessage) + bot.finishInlineReceiveHandler(nil, &tb.Callback{Data: string(invoiceEvent.CallbackData)}) +} + +func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) { + tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} + rn, err := tx.Get(tx, bot.Bunt) + // immediatelly set intransaction to block duplicate calls + if err != nil { + log.Errorf("[getInlineReceive] %s", err) + return + } + inlineReceive := rn.(*InlineReceive) - inlineReceive.Message = fmt.Sprintf("%s", fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendUpdateMessageAccept"), inlineReceive.Amount, fromUserStrMd, toUserStrMd)) + from := inlineReceive.From + to := inlineReceive.To + toUserStrMd := GetUserStrMd(to.Telegram) + fromUserStrMd := GetUserStrMd(from.Telegram) + toUserStr := GetUserStr(to.Telegram) + inlineReceive.MessageText = fmt.Sprintf("%s", fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendUpdateMessageAccept"), inlineReceive.Amount, fromUserStrMd, toUserStrMd)) memo := inlineReceive.Memo if len(memo) > 0 { - inlineReceive.Message = inlineReceive.Message + fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveAppendMemo"), memo) + inlineReceive.MessageText += fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveAppendMemo"), memo) } if !to.Initialized { - inlineReceive.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) + inlineReceive.MessageText += "\n\n" + fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } - bot.tryEditMessage(c.Message, inlineReceive.Message, &tb.ReplyMarkup{}) + bot.tryEditMessage(inlineReceive.Message, inlineReceive.MessageText, &tb.ReplyMarkup{}) // notify users _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, inlineReceive.Amount)) _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), inlineReceive.Amount, toUserStrMd)) @@ -236,6 +325,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac errmsg := fmt.Errorf("[acceptInlineReceiveHandler] Error: Receive message to %s: %s", toUserStr, err) log.Warnln(errmsg) } + // inlineReceive.Release(inlineReceive, bot.Bunt) } func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 3d58b2ab..5f0df1d8 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -10,11 +10,48 @@ import ( log "github.com/sirupsen/logrus" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/skip2/go-qrcode" tb "gopkg.in/lightningtipbot/telebot.v2" ) +type InvoiceEventCallback map[int]func(*InvocieEvent) + +var InvoiceCallback InvoiceEventCallback + +func initInvoiceEventCallbacks(bot TipBot) { + InvoiceCallback = InvoiceEventCallback{ + InvoiceCallbackGeneric: bot.triggerInvoiceEvent, + InvoiceCallbackInlineReceive: bot.inlineReceiveEvent, + } +} + +type InvoiceEventKey int + +const ( + InvoiceCallbackGeneric = iota + 1 + InvoiceCallbackInlineReceive +) + +type InvocieEvent struct { + PaymentHash string `json:"payment_hash"` + PaymentRequest string `json:"payment_request"` + Amount int64 `json:"amount"` + Memo string `json:"memo"` + User *lnbits.User `json:"user"` + Message *tb.Message `json:"message"` + InvoiceMessage *tb.Message `json:"invoice_message"` + LanguageCode string `json:"languagecode"` + Callback int `json:"func"` + CallbackData string `json:"callbackdata"` +} + +func (invoiceEvent InvocieEvent) Key() string { + return fmt.Sprintf("invoice:%s", invoiceEvent.PaymentHash) +} + func helpInvoiceUsage(ctx context.Context, errormsg string) string { if len(errormsg) > 0 { return fmt.Sprintf(Translate(ctx, "invoiceHelpText"), fmt.Sprintf("%s", errormsg)) @@ -23,7 +60,7 @@ func helpInvoiceUsage(ctx context.Context, errormsg string) string { } } -func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { // check and print all commands bot.anyTextHandler(ctx, m) user := LoadUser(ctx) @@ -59,13 +96,14 @@ func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { creatingMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlGettingUserMessage")) log.Infof("[/invoice] Creating invoice for %s of %d sat.", userStr, amount) // generate invoice - invoice, err := user.Wallet.Invoice( - lnbits.InvoiceParams{ - Out: false, - Amount: int64(amount), - Memo: memo, - Webhook: internal.Configuration.Lnbits.WebhookServer}, - bot.Client) + // invoice, err := user.Wallet.Invoice( + // lnbits.InvoiceParams{ + // Out: false, + // Amount: int64(amount), + // Memo: memo, + // Webhook: internal.Configuration.Lnbits.WebhookServer}, + // bot.Client) + invoice, err := bot.createInvoiceEvent(ctx, user, amount, memo, InvoiceCallbackGeneric, "") if err != nil { errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) bot.tryEditMessage(creatingMsg, Translate(ctx, "errorTryLaterMessage")) @@ -88,3 +126,36 @@ func (bot TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { log.Printf("[/invoice] Incvoice created. User: %s, amount: %d sat.", userStr, amount) return } + +func (bot *TipBot) createInvoiceEvent(ctx context.Context, user *lnbits.User, amount int64, memo string, callback int, callbackData string) (InvocieEvent, error) { + invoice, err := user.Wallet.Invoice( + lnbits.InvoiceParams{ + Out: false, + Amount: int64(amount), + Memo: memo, + Webhook: internal.Configuration.Lnbits.WebhookServer}, + bot.Client) + if err != nil { + errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) + log.Errorln(errmsg) + return InvocieEvent{}, err + } + + invoiceEvent := InvocieEvent{ + PaymentHash: invoice.PaymentHash, + PaymentRequest: invoice.PaymentRequest, + Amount: amount, + Memo: memo, + User: user, + Callback: callback, + CallbackData: callbackData, + LanguageCode: ctx.Value("publicLanguageCode").(string), + } + // save invoice struct for later use + runtime.IgnoreError(bot.Bunt.Set(invoiceEvent)) + return invoiceEvent, nil +} + +func (bot *TipBot) triggerInvoiceEvent(invoiceEvent *InvocieEvent) { + bot.trySendMessage(invoiceEvent.User.Telegram, fmt.Sprintf(i18n.Translate(invoiceEvent.User.Telegram.LanguageCode, "invoiceReceivedMessage"), invoiceEvent.Amount)) +} From 0f6c37a40c148f0a11d96e727c89fe73c5f71454 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 17 Dec 2021 20:47:22 +0000 Subject: [PATCH 080/541] Check balance inline receive (#166) * speed up tx lock * check balance Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_receive.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index a953c9a2..25dcbf76 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -178,7 +178,13 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac return } - if from.Wallet == nil || from.Wallet.Balance < inlineReceive.Amount { + balance, err := bot.GetUserBalance(from) + if err != nil { + errmsg := fmt.Sprintf("[inlineReceive] Error: Could not get user balance: %s", err) + log.Warnln(errmsg) + } + + if from.Wallet == nil || balance < inlineReceive.Amount { // if user has no wallet, show invoice bot.tryEditMessage(inlineReceive.Message, inlineReceive.MessageText, &tb.ReplyMarkup{}) // runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) From 307d9cd873391a31a2e0c36f5447b0c8583d59b4 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 18 Dec 2021 18:05:48 +0000 Subject: [PATCH 081/541] Debug tip tooltip missing (#167) * speed up tx lock * debug for tip Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/price/price.go | 6 ++-- internal/telegram/tip.go | 2 +- internal/telegram/tooltip.go | 64 ++++++++++++++++-------------------- main.go | 2 +- 4 files changed, 33 insertions(+), 41 deletions(-) diff --git a/internal/price/price.go b/internal/price/price.go index 39f4a166..04fe5986 100644 --- a/internal/price/price.go +++ b/internal/price/price.go @@ -62,7 +62,7 @@ func (p *PriceWatcher) Watch() error { for currency, _ := range p.Currencies { avg_price := 0.0 n_responses := 0 - for exchange, getPrice := range p.Exchanges { + for _, getPrice := range p.Exchanges { fprice, err := getPrice(currency) if err != nil { // log.Debug(err) @@ -71,11 +71,11 @@ func (p *PriceWatcher) Watch() error { } n_responses++ avg_price += fprice - log.Debugf("[PriceWatcher] %s %s price: %f", exchange, currency, fprice) + // log.Debugf("[PriceWatcher] %s %s price: %f", exchange, currency, fprice) time.Sleep(time.Second * time.Duration(2)) } Price[currency] = avg_price / float64(n_responses) - log.Debugf("[PriceWatcher] Average %s price: %f", currency, Price[currency]) + // log.Debugf("[PriceWatcher] Average %s price: %f", currency, Price[currency]) } time.Sleep(p.UpdateInterval) } diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index eeec852c..ba7ca7a8 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -69,7 +69,6 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { } // TIP COMMAND IS VALID from := LoadUser(ctx) - to := LoadReplyToUser(ctx) if from.Telegram.ID == to.Telegram.ID { @@ -118,6 +117,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { // update tooltip if necessary messageHasTip := tipTooltipHandler(m, bot, amount, to.Initialized) + log.Debugf("[tip] Has tip: %t", messageHasTip) log.Infof("[💸 tip] Tip from %s to %s (%d sat).", fromUserStr, toUserStr, amount) diff --git a/internal/telegram/tooltip.go b/internal/telegram/tooltip.go index fa4ae0bd..758a8f66 100644 --- a/internal/telegram/tooltip.go +++ b/internal/telegram/tooltip.go @@ -32,6 +32,10 @@ type TipTooltip struct { Tippers []*tb.User `json:"tippers"` } +func (ttt TipTooltip) Key() string { + return fmt.Sprintf("tip-tool-tip:%s", strconv.Itoa(ttt.Message.Message.ReplyTo.ID)) +} + const maxNamesInTipperMessage = 5 type TipTooltipOption func(m *TipTooltip) @@ -117,42 +121,40 @@ func tipTooltipHandler(m *tb.Message, bot *TipBot, amount int64, initializedWall // update the tooltip with new tippers err := ttt.updateTooltip(bot, m.Sender, amount, !initializedWallet) if err != nil { - log.Println(err) + log.Errorln(err) // could not update the message (return false to ) return false } } else { - tipmsg := fmt.Sprintf(tooltipTipAmountMessage, amount) - userStr := GetUserStrMd(m.Sender) - tipmsg = fmt.Sprintf(tooltipSingleTipMessage, tipmsg, userStr) - - if !initializedWallet { - tipmsg = tipmsg + fmt.Sprintf("\n%s", fmt.Sprintf(tooltipChatWithBotMessage, GetUserStrMd(bot.Telegram.Me))) - } - msg, err := bot.Telegram.Reply(m.ReplyTo, tipmsg, tb.Silent) - if err != nil { - log.Errorf("[tipTooltipHandler Reply] %s", err.Error()) - // todo: in case of error we should do something better than just return 0 - return false - } - message := NewTipTooltip(msg, TipAmount(amount), Tips(1)) - message.Tippers = appendUinqueUsersToSlice(message.Tippers, m.Sender) - runtime.IgnoreError(bot.Bunt.Set(message)) + newToolTip(m, bot, amount, initializedWallet) } // first call will return false, every following call will return true return hasTip } +func newToolTip(m *tb.Message, bot *TipBot, amount int64, initializedWallet bool) { + tipmsg := fmt.Sprintf(tooltipTipAmountMessage, amount) + userStr := GetUserStrMd(m.Sender) + tipmsg = fmt.Sprintf(tooltipSingleTipMessage, tipmsg, userStr) + + if !initializedWallet { + tipmsg = tipmsg + fmt.Sprintf("\n%s", fmt.Sprintf(tooltipChatWithBotMessage, GetUserStrMd(bot.Telegram.Me))) + } + msg := bot.tryReplyMessage(m.ReplyTo, tipmsg, tb.Silent) + message := NewTipTooltip(msg, TipAmount(amount), Tips(1)) + message.Tippers = appendUinqueUsersToSlice(message.Tippers, m.Sender) + runtime.IgnoreError(bot.Bunt.Set(message)) + log.Debugf("[newToolTip]: New reply message: %d (Bunt: %s)", msg.ID, message.Key()) +} + // updateToolTip updates existing tip tool tip in Telegram func (ttt *TipTooltip) updateTooltip(bot *TipBot, user *tb.User, amount int64, notInitializedWallet bool) error { ttt.TipAmount += amount ttt.Ntips += 1 ttt.Tippers = appendUinqueUsersToSlice(ttt.Tippers, user) ttt.LastTip = time.Now() - err := ttt.editTooltip(bot, notInitializedWallet) - if err != nil { - return err - } + ttt.editTooltip(bot, notInitializedWallet) + log.Debugf("[updateTooltip]: Update tip tooltip (Bunt: %s)", ttt.Key()) return bot.Bunt.Set(ttt) } @@ -162,16 +164,14 @@ func tipTooltipInitializedHandler(user *tb.User, bot TipBot) { err := tx.Ascend(MessageOrderedByReplyToFrom, func(key, value string) bool { replyToUserId := gjson.Get(value, MessageOrderedByReplyToFrom) if replyToUserId.String() == strconv.FormatInt(user.ID, 10) { - log.Debugln("loading persistent tip tool tip messages") + log.Debugln("[tipTooltipInitializedHandler] loading persistent tip tool tip messages") ttt := &TipTooltip{} err := json.Unmarshal([]byte(value), ttt) if err != nil { log.Errorln(err) } - err = ttt.editTooltip(&bot, false) - if err != nil { - log.Errorf("[tipTooltipInitializedHandler] could not edit tooltip: %s", err.Error()) - } + // edit to remove the "chat with bot" message + ttt.editTooltip(&bot, false) } return true @@ -180,17 +180,9 @@ func tipTooltipInitializedHandler(user *tb.User, bot TipBot) { })) } -func (ttt TipTooltip) Key() string { - return fmt.Sprintf("tip-tool-tip:%s", strconv.Itoa(ttt.Message.Message.ReplyTo.ID)) -} - // editTooltip updates the tooltip message with the new tip amount and tippers and edits it -func (ttt *TipTooltip) editTooltip(bot *TipBot, notInitializedWallet bool) error { +func (ttt *TipTooltip) editTooltip(bot *TipBot, notInitializedWallet bool) { tipToolTip := ttt.getUpdatedTipTooltipMessage(GetUserStrMd(bot.Telegram.Me), notInitializedWallet) - m, err := bot.Telegram.Edit(ttt.Message.Message, tipToolTip) - if err != nil { - return err - } + m := bot.tryEditMessage(ttt.Message.Message, tipToolTip) ttt.Message.Message.Text = m.Text - return nil } diff --git a/main.go b/main.go index 685d3733..025c10e0 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.InfoLevel) + log.SetLevel(log.DebugLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true From a4f5b21f1d04c282042b7bae8bea0dda42615faa Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 19 Dec 2021 14:41:01 +0000 Subject: [PATCH 082/541] Invoice events refactoring (#168) * speed up tx lock * clean up * refactor lnurl comment notification * fix function name * transaction: do not check balance; refactor invoice notifications Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/lnbits/webhook/webhook.go | 60 +++++++------------- internal/lnurl/lnurl.go | 53 ++++++++++-------- internal/telegram/bot.go | 2 +- internal/telegram/inline_receive.go | 5 +- internal/telegram/invoice.go | 87 ++++++++++++++++++----------- internal/telegram/tip.go | 2 +- internal/telegram/transaction.go | 43 ++++++-------- 7 files changed, 127 insertions(+), 125 deletions(-) diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index fdea4655..9aa3a875 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -7,7 +7,6 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/LightningTipBot/LightningTipBot/internal/telegram" log "github.com/sirupsen/logrus" @@ -15,7 +14,6 @@ import ( "net/http" - "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/gorilla/mux" @@ -24,10 +22,6 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/i18n" ) -const ( -// invoiceReceivedMessage = "⚡️ You received %d sat." -) - type Server struct { httpServer *http.Server bot *tb.Bot @@ -37,17 +31,16 @@ type Server struct { } type Webhook struct { - CheckingID string `json:"checking_id"` - Pending int `json:"pending"` - Amount int `json:"amount"` - Fee int `json:"fee"` - Memo string `json:"memo"` - Time int `json:"time"` - Bolt11 string `json:"bolt11"` - Preimage string `json:"preimage"` - PaymentHash string `json:"payment_hash"` - Extra struct { - } `json:"extra"` + CheckingID string `json:"checking_id"` + Pending int `json:"pending"` + Amount int `json:"amount"` + Fee int `json:"fee"` + Memo string `json:"memo"` + Time int `json:"time"` + Bolt11 string `json:"bolt11"` + Preimage string `json:"preimage"` + PaymentHash string `json:"payment_hash"` + Extra struct{} `json:"extra"` WalletID string `json:"wallet_id"` Webhook string `json:"webhook"` WebhookStatus interface{} `json:"webhook_status"` @@ -55,8 +48,7 @@ type Webhook struct { func NewServer(bot *telegram.TipBot) *Server { srv := &http.Server{ - Addr: internal.Configuration.Lnbits.WebhookServerUrl.Host, - // Good practice: enforce timeouts for servers you create! + Addr: internal.Configuration.Lnbits.WebhookServerUrl.Host, WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, } @@ -88,26 +80,26 @@ func (w *Server) newRouter() *mux.Router { return router } -func (w Server) receive(writer http.ResponseWriter, request *http.Request) { - depositEvent := Webhook{} +func (w *Server) receive(writer http.ResponseWriter, request *http.Request) { + webhookEvent := Webhook{} // need to delete the header otherwise the Decode will fail request.Header.Del("content-length") - err := json.NewDecoder(request.Body).Decode(&depositEvent) + err := json.NewDecoder(request.Body).Decode(&webhookEvent) if err != nil { writer.WriteHeader(400) return } - user, err := w.GetUserByWalletId(depositEvent.WalletID) + user, err := w.GetUserByWalletId(webhookEvent.WalletID) if err != nil { writer.WriteHeader(400) return } - log.Infoln(fmt.Sprintf("[⚡️ WebHook] User %s (%d) received invoice of %d sat.", telegram.GetUserStr(user.Telegram), user.Telegram.ID, depositEvent.Amount/1000)) + log.Infoln(fmt.Sprintf("[⚡️ WebHook] User %s (%d) received invoice of %d sat.", telegram.GetUserStr(user.Telegram), user.Telegram.ID, webhookEvent.Amount/1000)) writer.WriteHeader(200) // trigger invoice events - txInvoiceEvent := &telegram.InvocieEvent{PaymentHash: depositEvent.PaymentHash} + txInvoiceEvent := &telegram.InvoiceEvent{Invoice: &telegram.Invoice{PaymentHash: webhookEvent.PaymentHash}} err = w.buntdb.Get(txInvoiceEvent) if err != nil { log.Errorln(err) @@ -119,23 +111,9 @@ func (w Server) receive(writer http.ResponseWriter, request *http.Request) { } } - // else, send a message to the user if there is no callback for this invoice - _, err = w.bot.Send(user.Telegram, fmt.Sprintf(i18n.Translate(user.Telegram.LanguageCode, "invoiceReceivedMessage"), depositEvent.Amount/1000)) + // fallback: send a message to the user if there is no callback for this invoice + _, err = w.bot.Send(user.Telegram, fmt.Sprintf(i18n.Translate(user.Telegram.LanguageCode, "invoiceReceivedMessage"), webhookEvent.Amount/1000)) if err != nil { log.Errorln(err) } - - // legacy (should be replaced with the invoice listener above) - // check if invoice corresponds to an LNURL-p request, we load it and display the comment from an LNURL invoice - tx := &lnurl.Invoice{PaymentHash: depositEvent.PaymentHash} - err = w.buntdb.Get(tx) - if err == nil { - if len(tx.Comment) > 0 { - _, err = w.bot.Send(user.Telegram, fmt.Sprintf(`✉️ %s`, str.MarkdownEscape(tx.Comment))) - if err != nil { - log.Errorln(err) - } - } - } - } diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 582cd393..ba939d6c 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -11,24 +11,23 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/telegram" "github.com/fiatjaf/go-lnurl" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" ) -type Invoice struct { - PaymentRequest string `json:"payment_request"` - PaymentHash string `json:"payment_hash"` - Amount int64 `json:"amount"` - Comment string `json:"comment"` - ToUser *lnbits.User `json:"to_user"` - CreatedAt time.Time `json:"created_at"` - Paid bool `json:"paid"` - PaidAt time.Time `json:"paid_at"` +type LNURLInvoice struct { + *telegram.Invoice + Comment string `json:"comment"` + User *lnbits.User `json:"user"` + CreatedAt time.Time `json:"created_at"` + Paid bool `json:"paid"` + PaidAt time.Time `json:"paid_at"` } -func (msg Invoice) Key() string { - return fmt.Sprintf("payment-hash:%s", msg.PaymentHash) +func (lnurlInvoice LNURLInvoice) Key() string { + return fmt.Sprintf("lnurl-p:%s", lnurlInvoice.PaymentHash) } func (w Server) handleLnUrl(writer http.ResponseWriter, request *http.Request) { @@ -98,9 +97,9 @@ func (w Server) serveLNURLpFirst(username string) (*lnurl.LNURLPayParams, error) } // serveLNURLpSecond serves the second LNURL response with the payment request with the correct description hash -func (w Server) serveLNURLpSecond(username string, amount int64, comment string) (*lnurl.LNURLPayValues, error) { +func (w Server) serveLNURLpSecond(username string, amount_msat int64, comment string) (*lnurl.LNURLPayValues, error) { log.Infof("[LNURL] Serving invoice for user %s", username) - if amount < minSendable || amount > MaxSendable { + if amount_msat < minSendable || amount_msat > MaxSendable { // amount is not ok return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ @@ -156,7 +155,7 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) } invoice, err := user.Wallet.Invoice( lnbits.InvoiceParams{ - Amount: amount / 1000, + Amount: amount_msat / 1000, Out: false, DescriptionHash: descriptionHash, Webhook: w.WebhookServer}, @@ -170,15 +169,25 @@ func (w Server) serveLNURLpSecond(username string, amount int64, comment string) } return resp, err } - // save invoice struct for later use + invoiceStruct := &telegram.Invoice{ + PaymentRequest: invoice.PaymentRequest, + PaymentHash: invoice.PaymentHash, + Amount: amount_msat / 1000, + } + // save lnurl invoice struct for later use (will hold the comment or other metdata for a notification when paid) + runtime.IgnoreError(w.buntdb.Set( + LNURLInvoice{ + Invoice: invoiceStruct, + User: user, + Comment: comment, + CreatedAt: time.Now(), + })) + // save the invoice Event that will be loaded when the invoice is paid and trigger the comment display callback runtime.IgnoreError(w.buntdb.Set( - Invoice{ - ToUser: user, - Amount: amount, - Comment: comment, - PaymentRequest: invoice.PaymentRequest, - PaymentHash: invoice.PaymentHash, - CreatedAt: time.Now(), + telegram.InvoiceEvent{ + Invoice: invoiceStruct, + User: user, + Callback: telegram.InvoiceCallbackLNURLPayReceive, })) return &lnurl.LNURLPayValues{ diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 5ff8dc85..cd40c522 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -81,7 +81,7 @@ func (bot TipBot) initBotWallet() error { } // Start will initialize the Telegram bot and lnbits. -func (bot TipBot) Start() { +func (bot *TipBot) Start() { log.Infof("[Telegram] Authorized on account @%s", bot.Telegram.Me.Username) // initialize the bot wallet err := bot.initBotWallet() diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 25dcbf76..b7c78fd5 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -263,7 +263,7 @@ func (bot *TipBot) inlineReceiveInvoice(ctx context.Context, c *tb.Callback, inl log.Errorf("[acceptInlineReceiveHandler] inline receive not active anymore") return } - invoice, err := bot.createInvoiceEvent(ctx, inlineReceive.To, inlineReceive.Amount, fmt.Sprintf("Pay to %s", GetUserStr(inlineReceive.To.Telegram)), InvoiceCallbackInlineReceive, inlineReceive.ID) + invoice, err := bot.createInvoiceWithEvent(ctx, inlineReceive.To, inlineReceive.Amount, fmt.Sprintf("Pay to %s", GetUserStr(inlineReceive.To.Telegram)), InvoiceCallbackInlineReceive, inlineReceive.ID) if err != nil { errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) bot.tryEditMessage(inlineReceive.Message, Translate(ctx, "errorTryLaterMessage")) @@ -293,8 +293,9 @@ func (bot *TipBot) inlineReceiveInvoice(ctx context.Context, c *tb.Callback, inl log.Printf("[/invoice] Incvoice created. User: %s, amount: %d sat.", GetUserStr(inlineReceive.To.Telegram), inlineReceive.Amount) } -func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvocieEvent) { +func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { bot.tryDeleteMessage(invoiceEvent.InvoiceMessage) + bot.notifyInvoiceReceivedEvent(invoiceEvent) bot.finishInlineReceiveHandler(nil, &tb.Callback{Data: string(invoiceEvent.CallbackData)}) } diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 5f0df1d8..3d464b23 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "strings" + "time" "github.com/LightningTipBot/LightningTipBot/internal" @@ -13,18 +14,20 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/skip2/go-qrcode" tb "gopkg.in/lightningtipbot/telebot.v2" ) -type InvoiceEventCallback map[int]func(*InvocieEvent) +type InvoiceEventCallback map[int]func(*InvoiceEvent) var InvoiceCallback InvoiceEventCallback -func initInvoiceEventCallbacks(bot TipBot) { +func initInvoiceEventCallbacks(bot *TipBot) { InvoiceCallback = InvoiceEventCallback{ - InvoiceCallbackGeneric: bot.triggerInvoiceEvent, - InvoiceCallbackInlineReceive: bot.inlineReceiveEvent, + InvoiceCallbackGeneric: bot.notifyInvoiceReceivedEvent, + InvoiceCallbackInlineReceive: bot.inlineReceiveEvent, + InvoiceCallbackLNURLPayReceive: bot.lnurlReceiveEvent, } } @@ -33,13 +36,17 @@ type InvoiceEventKey int const ( InvoiceCallbackGeneric = iota + 1 InvoiceCallbackInlineReceive + InvoiceCallbackLNURLPayReceive ) -type InvocieEvent struct { - PaymentHash string `json:"payment_hash"` - PaymentRequest string `json:"payment_request"` - Amount int64 `json:"amount"` - Memo string `json:"memo"` +type Invoice struct { + PaymentHash string `json:"payment_hash"` + PaymentRequest string `json:"payment_request"` + Amount int64 `json:"amount"` + Memo string `json:"memo"` +} +type InvoiceEvent struct { + *Invoice User *lnbits.User `json:"user"` Message *tb.Message `json:"message"` InvoiceMessage *tb.Message `json:"invoice_message"` @@ -48,7 +55,7 @@ type InvocieEvent struct { CallbackData string `json:"callbackdata"` } -func (invoiceEvent InvocieEvent) Key() string { +func (invoiceEvent InvoiceEvent) Key() string { return fmt.Sprintf("invoice:%s", invoiceEvent.PaymentHash) } @@ -95,15 +102,7 @@ func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { creatingMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlGettingUserMessage")) log.Infof("[/invoice] Creating invoice for %s of %d sat.", userStr, amount) - // generate invoice - // invoice, err := user.Wallet.Invoice( - // lnbits.InvoiceParams{ - // Out: false, - // Amount: int64(amount), - // Memo: memo, - // Webhook: internal.Configuration.Lnbits.WebhookServer}, - // bot.Client) - invoice, err := bot.createInvoiceEvent(ctx, user, amount, memo, InvoiceCallbackGeneric, "") + invoice, err := bot.createInvoiceWithEvent(ctx, user, amount, memo, InvoiceCallbackGeneric, "") if err != nil { errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) bot.tryEditMessage(creatingMsg, Translate(ctx, "errorTryLaterMessage")) @@ -127,7 +126,7 @@ func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { return } -func (bot *TipBot) createInvoiceEvent(ctx context.Context, user *lnbits.User, amount int64, memo string, callback int, callbackData string) (InvocieEvent, error) { +func (bot *TipBot) createInvoiceWithEvent(ctx context.Context, user *lnbits.User, amount int64, memo string, callback int, callbackData string) (InvoiceEvent, error) { invoice, err := user.Wallet.Invoice( lnbits.InvoiceParams{ Out: false, @@ -138,24 +137,48 @@ func (bot *TipBot) createInvoiceEvent(ctx context.Context, user *lnbits.User, am if err != nil { errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) log.Errorln(errmsg) - return InvocieEvent{}, err + return InvoiceEvent{}, err } - - invoiceEvent := InvocieEvent{ - PaymentHash: invoice.PaymentHash, - PaymentRequest: invoice.PaymentRequest, - Amount: amount, - Memo: memo, - User: user, - Callback: callback, - CallbackData: callbackData, - LanguageCode: ctx.Value("publicLanguageCode").(string), + invoiceEvent := InvoiceEvent{ + Invoice: &Invoice{PaymentHash: invoice.PaymentHash, + PaymentRequest: invoice.PaymentRequest, + Amount: amount, + Memo: memo}, + User: user, + Callback: callback, + CallbackData: callbackData, + LanguageCode: ctx.Value("publicLanguageCode").(string), } // save invoice struct for later use runtime.IgnoreError(bot.Bunt.Set(invoiceEvent)) return invoiceEvent, nil } -func (bot *TipBot) triggerInvoiceEvent(invoiceEvent *InvocieEvent) { +func (bot *TipBot) notifyInvoiceReceivedEvent(invoiceEvent *InvoiceEvent) { bot.trySendMessage(invoiceEvent.User.Telegram, fmt.Sprintf(i18n.Translate(invoiceEvent.User.Telegram.LanguageCode, "invoiceReceivedMessage"), invoiceEvent.Amount)) } + +type LNURLInvoice struct { + *Invoice + Comment string `json:"comment"` + User *lnbits.User `json:"user"` + CreatedAt time.Time `json:"created_at"` + Paid bool `json:"paid"` + PaidAt time.Time `json:"paid_at"` +} + +func (lnurlInvoice LNURLInvoice) Key() string { + return fmt.Sprintf("lnurl-p:%s", lnurlInvoice.PaymentHash) +} + +func (bot *TipBot) lnurlReceiveEvent(invoiceEvent *InvoiceEvent) { + bot.notifyInvoiceReceivedEvent(invoiceEvent) + tx := &LNURLInvoice{Invoice: &Invoice{PaymentHash: invoiceEvent.PaymentHash}} + err := bot.Bunt.Get(tx) + log.Debugf("[lnurl-p] Received invoice for %s of %d sat.", GetUserStr(invoiceEvent.User.Telegram), tx.Amount) + if err == nil { + if len(tx.Comment) > 0 { + bot.trySendMessage(tx.User.Telegram, fmt.Sprintf(`✉️ %s`, str.MarkdownEscape(tx.Comment))) + } + } +} diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index ba7ca7a8..a495533a 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -109,7 +109,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { success, err := t.Send() if !success { NewMessage(m, WithDuration(0, bot)) - bot.trySendMessage(m.Sender, Translate(ctx, "tipErrorMessage")) + bot.trySendMessage(m.Sender, fmt.Sprintf("%s %s", Translate(ctx, "tipErrorMessage"), err)) errMsg := fmt.Sprintf("[/tip] Transaction failed: %s", err) log.Warnln(errMsg) return diff --git a/internal/telegram/transaction.go b/internal/telegram/transaction.go index 9ce6f6a8..c38aa87f 100644 --- a/internal/telegram/transaction.go +++ b/internal/telegram/transaction.go @@ -70,18 +70,9 @@ func NewTransaction(bot *TipBot, from *lnbits.User, to *lnbits.User, amount int6 } func (t *Transaction) Send() (success bool, err error) { - // maybe remove comments, GTP-3 dreamed this up but it's nice: - // if t.From.ID == t.To.ID { - // err = fmt.Errorf("Can not send transaction to yourself.") - // return false, err - // } - - // todo: remove this commend if the backend is back up success, err = t.SendTransaction(t.Bot, t.From, t.To, t.Amount, t.Memo) - // success = true if success { t.Success = success - // TODO: call post-send methods } // save transaction to db @@ -90,7 +81,6 @@ func (t *Transaction) Send() (success bool, err error) { errMsg := fmt.Sprintf("Error: Could not log transaction: %s", err.Error()) log.Errorln(errMsg) } - return success, err } @@ -100,19 +90,20 @@ func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits t.FromWallet = from.Wallet.ID t.FromLNbitsID = from.ID - // check if fromUser has balance - balance, err := bot.GetUserBalance(from) - if err != nil { - errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) - log.Errorln(errmsg) - return false, err - } - // check if fromUser has balance - if balance < amount { - errmsg := fmt.Sprintf("balance too low.") - log.Warnf("Balance of user %s too low", fromUserStr) - return false, fmt.Errorf(errmsg) - } + + // // check if fromUser has balance + // balance, err := bot.GetUserBalance(from) + // if err != nil { + // errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) + // log.Errorln(errmsg) + // return false, err + // } + // // check if fromUser has balance + // if balance < amount { + // errmsg := fmt.Sprintf("balance too low.") + // log.Warnf("Balance of user %s too low", fromUserStr) + // return false, fmt.Errorf(errmsg) + // } t.ToWallet = to.ID t.ToLNbitsID = to.ID @@ -125,7 +116,7 @@ func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits Memo: memo}, bot.Client) if err != nil { - errmsg := fmt.Sprintf("[SendTransaction] Error: Could not create invoice for user %s", toUserStr) + errmsg := fmt.Sprintf("[Send] Error: Could not create invoice for user %s", toUserStr) log.Errorln(errmsg) return false, err } @@ -133,8 +124,8 @@ func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits // pay invoice _, err = from.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoice.PaymentRequest}, bot.Client) if err != nil { - errmsg := fmt.Sprintf("[SendTransaction] Error: Payment from %s to %s of %d sat failed", fromUserStr, toUserStr, amount) - log.Errorln(errmsg) + errmsg := fmt.Sprintf("[Send] Payment failed (%s to %s of %d sat): %s", fromUserStr, toUserStr, amount, err.Error()) + log.Warnf(errmsg) return false, err } return true, err From 15d711a6fd7fc68cd17cd9680eafdbe806fa5e60 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 19 Dec 2021 17:51:49 +0000 Subject: [PATCH 083/541] Tipjar fix usercheck (#169) * check for correct user for tipjar Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/storage/transaction/transaction.go | 4 ++-- internal/telegram/inline_tipjar.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index 560d957c..c1f9725e 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -75,13 +75,13 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error } // to avoid race conditions, we block the call if there is // already an active transaction by loop until InTransaction is false - ticker := time.NewTicker(time.Millisecond * 200) + ticker := time.NewTicker(time.Millisecond * 100) for tx.InTransaction { select { case <-ticker.C: return nil, fmt.Errorf("transaction timeout") default: - time.Sleep(time.Duration(100) * time.Millisecond) + time.Sleep(time.Duration(75) * time.Millisecond) err = db.Get(s) } } diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index a4a0626f..ffa03738 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -259,7 +259,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback } // // check if to user has already given to the tipjar for _, a := range inlineTipjar.From { - if a.Telegram.ID == to.Telegram.ID { + if a.Telegram.ID == from.Telegram.ID { // to user is already in To slice, has taken from facuet // log.Infof("[tipjar] %s already gave to tipjar %s", GetUserStr(to.Telegram), inlineTipjar.ID) return From 179d40178c66e25e363cda3988344f01c1e579a7 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 21 Dec 2021 16:07:26 +0100 Subject: [PATCH 084/541] Faucet no tx error (#170) * speed up tx lock * remove tx timeout errors Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 2 +- internal/telegram/inline_receive.go | 2 +- internal/telegram/inline_send.go | 2 +- internal/telegram/inline_tipjar.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 6e44066b..aa0b88b8 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -235,7 +235,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback tx := &InlineFaucet{Base: transaction.New(transaction.ID(c.Data))} fn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[faucet] %s", err) + // log.Errorf("[faucet] %s", err) return } inlineFaucet := fn.(*InlineFaucet) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index b7c78fd5..55f3a4b6 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -202,7 +202,7 @@ func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { - log.Errorf("[getInlineReceive] %s", err) + // log.Errorf("[getInlineReceive] %s", err) return } inlineReceive := rn.(*InlineReceive) diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 87c7b5dd..58d1ab63 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -162,7 +162,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { - log.Errorf("[acceptInlineSendHandler] %s", err) + // log.Errorf("[acceptInlineSendHandler] %s", err) return } inlineSend := sn.(*InlineSend) diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index ffa03738..281bb998 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -236,7 +236,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback tx := &InlineTipjar{Base: transaction.New(transaction.ID(c.Data))} fn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[tipjar] %s", err) + // log.Errorf("[tipjar] %s", err) return } inlineTipjar := fn.(*InlineTipjar) From f313b5cdbe444cfa8439dc2114e5347dcb197c61 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 21 Dec 2021 16:16:42 +0100 Subject: [PATCH 085/541] Faucet no tx error (#171) * speed up tx lock * remove tx timeout errors * cacel faucet on error * cancle faucet on error Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index aa0b88b8..efdae2b2 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -295,6 +295,11 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) errMsg := fmt.Sprintf("[faucet] Transaction failed: %s", err) log.Warnln(errMsg) + + // if faucet fails, cancel it: + c.Sender.ID = inlineFaucet.From.Telegram.ID // overwrite the sender of the callback to be the faucet owner + log.Debugf("[faucet] Canceling faucet %s...", inlineFaucet.ID) + bot.cancelInlineFaucetHandler(ctx, c) return } @@ -350,5 +355,6 @@ func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.InTransaction = false runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) } + log.Debugf("[faucet] Faucet %s canceled.", inlineFaucet.ID) return } From 4b620186f173188ff2e2355dce31c7e08720f6ef Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 22 Dec 2021 02:35:00 +0100 Subject: [PATCH 086/541] Lnbits postgres compatible (#172) * speed up tx lock * webhook fix Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/lnbits/webhook/webhook.go | 11 +++++++---- main.go | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index 9aa3a875..27f3bce2 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -32,11 +32,11 @@ type Server struct { type Webhook struct { CheckingID string `json:"checking_id"` - Pending int `json:"pending"` - Amount int `json:"amount"` - Fee int `json:"fee"` + Pending bool `json:"pending"` + Amount int64 `json:"amount"` + Fee int64 `json:"fee"` Memo string `json:"memo"` - Time int `json:"time"` + Time int64 `json:"time"` Bolt11 string `json:"bolt11"` Preimage string `json:"preimage"` PaymentHash string `json:"payment_hash"` @@ -81,16 +81,19 @@ func (w *Server) newRouter() *mux.Router { } func (w *Server) receive(writer http.ResponseWriter, request *http.Request) { + log.Debugln("[Webhook] Received request") webhookEvent := Webhook{} // need to delete the header otherwise the Decode will fail request.Header.Del("content-length") err := json.NewDecoder(request.Body).Decode(&webhookEvent) if err != nil { + log.Errorf("[Webhook] Error decoding request: %s", err) writer.WriteHeader(400) return } user, err := w.GetUserByWalletId(webhookEvent.WalletID) if err != nil { + log.Errorf("[Webhook] Error getting user: %s", err) writer.WriteHeader(400) return } diff --git a/main.go b/main.go index 025c10e0..685d3733 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.DebugLevel) + log.SetLevel(log.InfoLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true From a59a7c226d0b63aad3b4761661c93460021e7630 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 22 Dec 2021 14:40:48 +0100 Subject: [PATCH 087/541] defer intercept after before intercept error check (#173) * defer intercept after before intercept error check. * log trace level Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/intercept/callback.go | 2 +- internal/telegram/intercept/message.go | 3 +-- internal/telegram/intercept/query.go | 2 +- internal/telegram/interceptor.go | 2 ++ main.go | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go index c07e1595..cd2a24b4 100644 --- a/internal/telegram/intercept/callback.go +++ b/internal/telegram/intercept/callback.go @@ -57,8 +57,8 @@ func HandlerWithCallback(handler CallbackFuncHandler, option ...CallbackIntercep } return func(c *tb.Callback) { ctx := context.Background() - defer interceptCallback(ctx, c, hm.onDefer) ctx, err := interceptCallback(ctx, c, hm.before) + defer interceptCallback(ctx, c, hm.onDefer) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go index f6bbd4ca..fd1b5ae9 100644 --- a/internal/telegram/intercept/message.go +++ b/internal/telegram/intercept/message.go @@ -54,15 +54,14 @@ func interceptMessage(ctx context.Context, message *tb.Message, hm MessageChain) } func HandlerWithMessage(handler MessageFuncHandler, option ...MessageInterceptOption) func(message *tb.Message) { - hm := &handlerMessageInterceptor{handler: handler} for _, opt := range option { opt(hm) } return func(message *tb.Message) { ctx := context.Background() - defer interceptMessage(ctx, message, hm.onDefer) ctx, err := interceptMessage(ctx, message, hm.before) + defer interceptMessage(ctx, message, hm.onDefer) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go index 41fe6716..e67e9055 100644 --- a/internal/telegram/intercept/query.go +++ b/internal/telegram/intercept/query.go @@ -57,8 +57,8 @@ func HandlerWithQuery(handler QueryFuncHandler, option ...QueryInterceptOption) } return func(query *tb.Query) { ctx := context.Background() - defer interceptQuery(ctx, query, hm.onDefer) ctx, err := interceptQuery(context.Background(), query, hm.before) + defer interceptQuery(ctx, query, hm.onDefer) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 3d2e96d9..47bfcfb1 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -51,6 +51,7 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context handlerMutex[user.ID].Unlock() } handlerMapMutex.Unlock() + log.Debugf("[mutex] Unlocked user %d", user.ID) } return ctx, nil } @@ -65,6 +66,7 @@ func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.C } handlerMapMutex.Unlock() handlerMutex[user.ID].Lock() + log.Debugf("[mutex] Locked user %d", user.ID) return ctx, nil } return nil, invalidTypeError diff --git a/main.go b/main.go index 685d3733..8d277363 100644 --- a/main.go +++ b/main.go @@ -12,7 +12,7 @@ import ( // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.InfoLevel) + log.SetLevel(log.TraceLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true From 91d38b5bb42ddd21ceec16af264700dd33a4364c Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 22 Dec 2021 17:33:14 +0100 Subject: [PATCH 088/541] Faucet debug logging (#175) * speed up tx lock * add logging Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/storage/transaction/transaction.go | 7 +++++++ internal/telegram/inline_faucet.go | 6 +++--- internal/telegram/inline_tipjar.go | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index c1f9725e..782a776f 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -5,6 +5,7 @@ import ( "time" "github.com/LightningTipBot/LightningTipBot/internal/storage" + log "github.com/sirupsen/logrus" ) type Base struct { @@ -44,8 +45,10 @@ func (tx *Base) Lock(s storage.Storable, db *storage.DB) error { tx.InTransaction = true err := tx.Set(s, db) if err != nil { + log.Debugf("[Lock] %s Error: %s", tx.ID, err.Error()) return err } + log.Debugf("[Lock] %s", tx.ID) return nil } @@ -54,8 +57,10 @@ func (tx *Base) Release(s storage.Storable, db *storage.DB) error { tx.InTransaction = false err := tx.Set(s, db) if err != nil { + log.Debugf("[Release] %s Error: %s", tx.ID, err.Error()) return err } + log.Debugf("[Release] %s", tx.ID) return nil } @@ -63,8 +68,10 @@ func (tx *Base) Inactivate(s storage.Storable, db *storage.DB) error { tx.Active = false err := tx.Set(s, db) if err != nil { + log.Debugf("[Inactivate] %s Error: %s", tx.ID, err.Error()) return err } + log.Debugf("[Inactivate] %s", tx.ID) return nil } diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index efdae2b2..f82b556a 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -196,7 +196,7 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { inlineFaucet, err := bot.makeQueryFaucet(ctx, q, false) if err != nil { - // log.Errorf("[faucet] %s", err) + log.Errorf("[handleInlineFaucetQuery] %s", err) return } urls := []string{ @@ -235,7 +235,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback tx := &InlineFaucet{Base: transaction.New(transaction.ID(c.Data))} fn, err := tx.Get(tx, bot.Bunt) if err != nil { - // log.Errorf("[faucet] %s", err) + log.Debugf("[acceptInlineFaucetHandler] %s", err) return } inlineFaucet := fn.(*InlineFaucet) @@ -344,7 +344,7 @@ func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback tx := &InlineFaucet{Base: transaction.New(transaction.ID(c.Data))} fn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[cancelInlineSendHandler] %s", err) + log.Debugf("[cancelInlineFaucetHandler] %s", err) return } inlineFaucet := fn.(*InlineFaucet) diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 281bb998..91bf0bec 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -338,7 +338,7 @@ func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback tx := &InlineTipjar{Base: transaction.New(transaction.ID(c.Data))} fn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[cancelInlineSendHandler] %s", err) + log.Errorf("[cancelInlineTipjarHandler] %s", err) return } inlineTipjar := fn.(*InlineTipjar) From 8b40253169224079281fcc28bc953e8f22670c97 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 22 Dec 2021 17:34:57 +0100 Subject: [PATCH 089/541] remove default interceptors (#174) * defer intercept after before intercept error check. * log trace level * get rid of default interceptors Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/handler.go | 314 +++++++++++++++++++++++++++++------ 1 file changed, 265 insertions(+), 49 deletions(-) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index ab78b66d..9fdc7e5e 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -40,9 +40,9 @@ func getDefaultAfterInterceptor(bot TipBot) []intercept.Func { // registerHandlerWithInterceptor will register a handler with all the predefined interceptors, based on the interceptor type func (bot TipBot) registerHandlerWithInterceptor(h Handler) { - h.Interceptor.Before = append(getDefaultBeforeInterceptor(bot), h.Interceptor.Before...) - h.Interceptor.After = append(h.Interceptor.After, getDefaultAfterInterceptor(bot)...) - h.Interceptor.OnDefer = append(h.Interceptor.OnDefer, getDefaultDeferInterceptor(bot)...) + //h.Interceptor.Before = append(getDefaultBeforeInterceptor(bot), h.Interceptor.Before...) + //h.Interceptor.After = append(h.Interceptor.After, getDefaultAfterInterceptor(bot)...) + //h.Interceptor.OnDefer = append(h.Interceptor.OnDefer, getDefaultDeferInterceptor(bot)...) switch h.Interceptor.Type { case MessageInterceptor: @@ -109,9 +109,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.loadUserInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/tip"}, @@ -119,10 +125,16 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, bot.loadReplyToInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/pay"}, @@ -130,9 +142,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/invoice"}, @@ -140,9 +158,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/balance"}, @@ -150,9 +174,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/send"}, @@ -160,10 +190,16 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, bot.loadReplyToInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan", "/grifo"}, @@ -171,9 +207,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/tipjar", "/spendendose"}, @@ -181,9 +223,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/help"}, @@ -191,9 +239,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.loadUserInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/basics"}, @@ -201,9 +255,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.loadUserInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/donate"}, @@ -211,9 +271,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/advanced"}, @@ -221,9 +287,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/link"}, @@ -231,8 +303,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, - bot.requireUserInterceptor}}, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{"/lnurl"}, @@ -240,8 +319,15 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, - bot.requireUserInterceptor}}, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{tb.OnPhoto}, @@ -250,8 +336,15 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.requirePrivateChatInterceptor, + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, - bot.requireUserInterceptor}}, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{tb.OnText}, @@ -260,9 +353,15 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.requirePrivateChatInterceptor, // Respond to any text only in private chat + bot.lockInterceptor, + bot.localizerInterceptor, bot.logMessageInterceptor, bot.loadUserInterceptor, // need to use loadUserInterceptor instead of requireUserInterceptor, because user might not be registered yet - }}, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{tb.OnQuery}, @@ -270,9 +369,14 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: QueryInterceptor, Before: []intercept.Func{ - bot.requireUserInterceptor, + bot.lockInterceptor, bot.localizerInterceptor, - }}, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{tb.OnChosenInlineResult}, @@ -282,99 +386,211 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnPay}, Handler: bot.confirmPayHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnCancelPay}, Handler: bot.cancelPaymentHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnSend}, Handler: bot.confirmSendHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnCancelSend}, Handler: bot.cancelSendHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnAcceptInlineSend}, Handler: bot.acceptInlineSendHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.loadUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnCancelInlineSend}, Handler: bot.cancelInlineSendHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnAcceptInlineReceive}, Handler: bot.acceptInlineReceiveHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.loadUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnCancelInlineReceive}, Handler: bot.cancelInlineReceiveHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnAcceptInlineFaucet}, Handler: bot.acceptInlineFaucetHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.loadUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.loadUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnCancelInlineFaucet}, Handler: bot.cancelInlineFaucetHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnAcceptInlineTipjar}, Handler: bot.acceptInlineTipjarHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnCancelInlineTipjar}, Handler: bot.cancelInlineTipjarHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnWithdraw}, Handler: bot.confirmWithdrawHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, { Endpoints: []interface{}{&btnCancelWithdraw}, Handler: bot.cancelWithdrawHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, - Before: []intercept.Func{bot.requireUserInterceptor}}, + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.lockInterceptor, + bot.localizerInterceptor, + bot.requireUserInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, }, } } From f85ec34bccc70d62769206facf2a90eb8a04f259 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 22 Dec 2021 17:48:30 +0100 Subject: [PATCH 090/541] fix lock intercept (#176) --- internal/telegram/handler.go | 62 ++++++++++++------------- internal/telegram/intercept/callback.go | 2 +- internal/telegram/intercept/message.go | 2 +- internal/telegram/intercept/query.go | 2 +- 4 files changed, 34 insertions(+), 34 deletions(-) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 9fdc7e5e..dea88c95 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -109,10 +109,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.loadUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -125,11 +125,11 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, bot.loadReplyToInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -142,10 +142,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -158,10 +158,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -174,10 +174,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -190,11 +190,11 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, bot.loadReplyToInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -207,10 +207,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -223,10 +223,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -239,10 +239,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.loadUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -255,10 +255,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.loadUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -271,10 +271,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -287,10 +287,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -303,10 +303,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -319,10 +319,10 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: MessageInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -336,10 +336,10 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.requirePrivateChatInterceptor, - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -353,10 +353,10 @@ func (bot TipBot) getHandler() []Handler { Type: MessageInterceptor, Before: []intercept.Func{ bot.requirePrivateChatInterceptor, // Respond to any text only in private chat - bot.lockInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.loadUserInterceptor, // need to use loadUserInterceptor instead of requireUserInterceptor, because user might not be registered yet + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -369,9 +369,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: QueryInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -388,9 +388,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -403,9 +403,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -418,9 +418,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -433,9 +433,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -448,9 +448,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.loadUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -463,9 +463,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -478,9 +478,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.loadUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -493,9 +493,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -508,9 +508,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.loadUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -523,9 +523,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -538,9 +538,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -553,9 +553,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -568,9 +568,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, @@ -583,9 +583,9 @@ func (bot TipBot) getHandler() []Handler { Interceptor: &Interceptor{ Type: CallbackInterceptor, Before: []intercept.Func{ - bot.lockInterceptor, bot.localizerInterceptor, bot.requireUserInterceptor, + bot.lockInterceptor, }, OnDefer: []intercept.Func{ bot.unlockInterceptor, diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go index cd2a24b4..d9698be2 100644 --- a/internal/telegram/intercept/callback.go +++ b/internal/telegram/intercept/callback.go @@ -58,11 +58,11 @@ func HandlerWithCallback(handler CallbackFuncHandler, option ...CallbackIntercep return func(c *tb.Callback) { ctx := context.Background() ctx, err := interceptCallback(ctx, c, hm.before) - defer interceptCallback(ctx, c, hm.onDefer) if err != nil { log.Traceln(err) return } + defer interceptCallback(ctx, c, hm.onDefer) hm.handler(ctx, c) _, err = interceptCallback(ctx, c, hm.after) if err != nil { diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go index fd1b5ae9..293c09fd 100644 --- a/internal/telegram/intercept/message.go +++ b/internal/telegram/intercept/message.go @@ -61,11 +61,11 @@ func HandlerWithMessage(handler MessageFuncHandler, option ...MessageInterceptOp return func(message *tb.Message) { ctx := context.Background() ctx, err := interceptMessage(ctx, message, hm.before) - defer interceptMessage(ctx, message, hm.onDefer) if err != nil { log.Traceln(err) return } + defer interceptMessage(ctx, message, hm.onDefer) hm.handler(ctx, message) _, err = interceptMessage(ctx, message, hm.after) if err != nil { diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go index e67e9055..4dfc4264 100644 --- a/internal/telegram/intercept/query.go +++ b/internal/telegram/intercept/query.go @@ -58,11 +58,11 @@ func HandlerWithQuery(handler QueryFuncHandler, option ...QueryInterceptOption) return func(query *tb.Query) { ctx := context.Background() ctx, err := interceptQuery(context.Background(), query, hm.before) - defer interceptQuery(ctx, query, hm.onDefer) if err != nil { log.Traceln(err) return } + defer interceptQuery(ctx, query, hm.onDefer) hm.handler(ctx, query) _, err = interceptQuery(ctx, query, hm.after) if err != nil { From 64f9492af20e27d1e74fa32a30ee58e597a4f1a9 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 22 Dec 2021 18:03:03 +0100 Subject: [PATCH 091/541] update no private chat error message (#177) --- internal/telegram/interceptor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 47bfcfb1..f886fdc7 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -162,7 +162,7 @@ func (bot TipBot) requirePrivateChatInterceptor(ctx context.Context, i interface case *tb.Message: m := i.(*tb.Message) if m.Chat.Type != tb.ChatPrivate { - return nil, fmt.Errorf("no private chat") + return nil, fmt.Errorf("[requirePrivateChatInterceptor] no private chat") } return ctx, nil } From 7e2404eaf5afb4c01aa1139e27edafd51138720a Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 22 Dec 2021 18:54:06 +0100 Subject: [PATCH 092/541] add transaction mutex lock (#178) --- internal/storage/transaction/transaction.go | 36 +++++++++++++++++---- internal/telegram/inline_faucet.go | 2 +- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index 782a776f..b56e0ff0 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -2,6 +2,7 @@ package transaction import ( "fmt" + "sync" "time" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -16,6 +17,14 @@ 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 { @@ -45,7 +54,7 @@ func (tx *Base) Lock(s storage.Storable, db *storage.DB) error { tx.InTransaction = true err := tx.Set(s, db) if err != nil { - log.Debugf("[Lock] %s Error: %s", tx.ID, err.Error()) + log.Debugf("[Bunt Lock] %s Error: %s", tx.ID, err.Error()) return err } log.Debugf("[Lock] %s", tx.ID) @@ -57,10 +66,17 @@ func (tx *Base) Release(s storage.Storable, db *storage.DB) error { tx.InTransaction = false err := tx.Set(s, db) if err != nil { - log.Debugf("[Release] %s Error: %s", tx.ID, err.Error()) + log.Debugf("[Bunt Release] %s Error: %s", tx.ID, err.Error()) return err } - log.Debugf("[Release] %s", tx.ID) + log.Debugf("[Bunt Release] %s", tx.ID) + transactionMapMutex.Lock() + if transactionMutex[tx.ID] != nil { + transactionMutex[tx.ID].Unlock() + log.Tracef("[TX mutex] Release %s", tx.ID) + } + transactionMapMutex.Unlock() + return nil } @@ -68,14 +84,22 @@ func (tx *Base) Inactivate(s storage.Storable, db *storage.DB) error { tx.Active = false err := tx.Set(s, db) if err != nil { - log.Debugf("[Inactivate] %s Error: %s", tx.ID, err.Error()) + log.Debugf("[Bunt Inactivate] %s Error: %s", tx.ID, err.Error()) return err } - log.Debugf("[Inactivate] %s", tx.ID) + log.Debugf("[Bunt Inactivate] %s", tx.ID) return nil } 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() + log.Tracef("[TX mutex] Lock %s", tx.ID) + err := db.Get(s) if err != nil { return s, err @@ -86,7 +110,7 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error for tx.InTransaction { select { case <-ticker.C: - return nil, fmt.Errorf("transaction timeout") + return nil, fmt.Errorf("[Bunt Lock] transaction timeout") default: time.Sleep(time.Duration(75) * time.Millisecond) err = db.Get(s) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index f82b556a..cf2f8c58 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -245,12 +245,12 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback log.Errorf("[faucet] LockFaucet %s error: %s", inlineFaucet.ID, err) return } + defer inlineFaucet.Release(inlineFaucet, bot.Bunt) if !inlineFaucet.Active { log.Errorf(fmt.Sprintf("[faucet] faucet %s inactive.", inlineFaucet.ID)) return } // release faucet no matter what - defer inlineFaucet.Release(inlineFaucet, bot.Bunt) if from.Telegram.ID == to.Telegram.ID { bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) From 72ef490538764887ff241a9ceb9b5b87ad8409ef Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 22 Dec 2021 19:13:21 +0100 Subject: [PATCH 093/541] unlock tx mutex on GET error (#179) --- internal/storage/transaction/transaction.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index b56e0ff0..71fccc68 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -60,6 +60,15 @@ 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 (tx *Base) Release(s storage.Storable, db *storage.DB) error { // immediatelly set intransaction to block duplicate calls @@ -70,13 +79,7 @@ func (tx *Base) Release(s storage.Storable, db *storage.DB) error { return err } log.Debugf("[Bunt Release] %s", tx.ID) - transactionMapMutex.Lock() - if transactionMutex[tx.ID] != nil { - transactionMutex[tx.ID].Unlock() - log.Tracef("[TX mutex] Release %s", tx.ID) - } - transactionMapMutex.Unlock() - + unlock(tx.ID) return nil } @@ -102,6 +105,7 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error err := db.Get(s) if err != nil { + unlock(tx.ID) return s, err } // to avoid race conditions, we block the call if there is @@ -110,6 +114,7 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error for tx.InTransaction { select { case <-ticker.C: + unlock(tx.ID) return nil, fmt.Errorf("[Bunt Lock] transaction timeout") default: time.Sleep(time.Duration(75) * time.Millisecond) @@ -117,6 +122,7 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error } } if err != nil { + unlock(tx.ID) return nil, fmt.Errorf("could not get transaction") } From 6b5e30dbe4abee375c1c25834ac2c1f555a14eeb Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 22 Dec 2021 19:24:51 +0100 Subject: [PATCH 094/541] Fix tx lock (#180) * unlock tx mutex on GET error * log tx id on timeout --- internal/storage/transaction/transaction.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index 71fccc68..54db374f 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -115,7 +115,7 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error select { case <-ticker.C: unlock(tx.ID) - return nil, fmt.Errorf("[Bunt Lock] transaction timeout") + return nil, fmt.Errorf("[Bunt Lock] transaction timeout %s", tx.ID) default: time.Sleep(time.Duration(75) * time.Millisecond) err = db.Get(s) From f8fadb172910bc1a9edb8244fe7daa8c6cebe8ea Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 23 Dec 2021 10:16:13 +0100 Subject: [PATCH 095/541] Onlysats (#181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- go.mod | 1 + go.sum | 4 + internal/config.go | 1 + internal/lnbits/types.go | 7 + internal/runtime/function.go | 76 ++ internal/runtime/mutex.go | 31 + internal/storage/bunt.go | 38 +- internal/storage/transaction/transaction.go | 42 +- internal/telegram/bot.go | 8 +- internal/telegram/buttons.go | 24 + internal/telegram/database.go | 4 +- internal/telegram/files.go | 32 + internal/telegram/handler.go | 415 ++++++ internal/telegram/interceptor.go | 32 +- internal/telegram/photo.go | 8 +- internal/telegram/send.go | 2 +- internal/telegram/shop.go | 1364 +++++++++++++++++++ internal/telegram/shop_helpers.go | 291 ++++ internal/telegram/state.go | 25 + internal/telegram/text.go | 17 +- 20 files changed, 2335 insertions(+), 87 deletions(-) create mode 100644 internal/runtime/function.go create mode 100644 internal/runtime/mutex.go create mode 100644 internal/telegram/buttons.go create mode 100644 internal/telegram/files.go create mode 100644 internal/telegram/shop.go create mode 100644 internal/telegram/shop_helpers.go create mode 100644 internal/telegram/state.go diff --git a/go.mod b/go.mod index c6a16487..2666e8ee 100644 --- a/go.mod +++ b/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 diff --git a/go.sum b/go.sum index ffd9a2e5..c06a9c1a 100644 --- a/go.sum +++ b/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= diff --git a/internal/config.go b/internal/config.go index ff814ccb..9d7b436e 100644 --- a/internal/config.go +++ b/internal/config.go @@ -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"` } diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index e446c560..d9d4e49a 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -36,6 +36,13 @@ const ( UserHasEnteredAmount UserEnterUser UserHasEnteredUser + UserEnterShopTitle + UserStateShopItemSendPhoto + UserStateShopItemSendTitle + UserStateShopItemSendDescription + UserStateShopItemSendPrice + UserStateShopItemSendItemFile + UserEnterShopsDescription ) type UserStateKey int diff --git a/internal/runtime/function.go b/internal/runtime/function.go new file mode 100644 index 00000000..96bbbc8c --- /dev/null +++ b/internal/runtime/function.go @@ -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) + } + } + }() +} diff --git a/internal/runtime/mutex.go b/internal/runtime/mutex.go new file mode 100644 index 00000000..aa972c4b --- /dev/null +++ b/internal/runtime/mutex.go @@ -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() + } +} diff --git a/internal/storage/bunt.go b/internal/storage/bunt.go index c85ec70b..58b7fc0f 100644 --- a/internal/storage/bunt.go +++ b/internal/storage/bunt.go @@ -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 }) } diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index 54db374f..4ba86d32 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -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) +} diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index cd40c522..6dcec788 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -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() } diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go new file mode 100644 index 00000000..7bb392cb --- /dev/null +++ b/internal/telegram/buttons.go @@ -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 +} diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 7736b70f..8bdc98a7 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -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 { diff --git a/internal/telegram/files.go b/internal/telegram/files.go new file mode 100644 index 00000000..ae456569 --- /dev/null +++ b/internal/telegram/files.go @@ -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 + } +} diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index dea88c95..54610915 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -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, + }}, + }, } } diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index f886fdc7..29cb8480 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -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 diff --git a/internal/telegram/photo.go b/internal/telegram/photo.go index 7c8a9e0a..155bcb6d 100644 --- a/internal/telegram/photo.go +++ b/internal/telegram/photo.go @@ -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()) diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 28fb4257..4145087d 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -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) diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go new file mode 100644 index 00000000..b21a072c --- /dev/null +++ b/internal/telegram/shop.go @@ -0,0 +1,1364 @@ +package telegram + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + "github.com/LightningTipBot/LightningTipBot/internal/i18n" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + "github.com/LightningTipBot/LightningTipBot/internal/str" + "github.com/eko/gocache/store" + log "github.com/sirupsen/logrus" + tb "gopkg.in/lightningtipbot/telebot.v2" +) + +type ShopView struct { + ID string + ShopID string + ShopOwner *lnbits.User + Page int + Message *tb.Message + StatusMessages []*tb.Message +} + +type ShopItem struct { + ID string `json:"ID"` // ID of the tx object in bunt db + ShopID string `json:"shopID"` // ID of the shop + Owner *lnbits.User `json:"owner"` // Owner of the item + Type string `json:"Type"` // Type of the tx object in bunt db + FileIDs []string `json:"fileIDs"` // Telegram fileID of the item files + FileTypes []string `json:"fileTypes"` // Telegram file type of the item files + Title string `json:"title"` // Title of the item + Description string `json:"description"` // Description of the item + Price int64 `json:"price"` // price of the item + NSold int `json:"nSold"` // number of times item was sold + TbPhoto *tb.Photo `json:"tbPhoto"` // Telegram photo object + LanguageCode string `json:"languagecode"` + MaxFiles int `json:"maxFiles"` +} + +type Shop struct { + *transaction.Base + ID string `json:"ID"` // holds the ID of the tx object in bunt db + Owner *lnbits.User `json:"owner"` // owner of the shop + Type string `json:"Type"` // type of the shop + Title string `json:"title"` // Title of the item + Description string `json:"description"` // Description of the item + ItemIds []string `json:"ItemsIDs"` // + Items map[string]ShopItem `json:"Items"` // + LanguageCode string `json:"languagecode"` + ShopsID string `json:"shopsID"` + MaxItems int `json:"maxItems"` +} + +type Shops struct { + *transaction.Base + ID string `json:"ID"` // holds the ID of the tx object in bunt db + Owner *lnbits.User `json:"owner"` // owner of the shop + Shops []string `json:"shop"` // + MaxShops int `json:"maxShops"` + Description string `json:"description"` +} + +const ( + MAX_SHOPS = 10 + MAX_ITEMS_PER_SHOP = 20 + MAX_FILES_PER_ITEM = 200 + SHOP_TITLE_MAX_LENGTH = 50 + ITEM_TITLE_MAX_LENGTH = 1500 + SHOPS_DESCRIPTION_MAX_LENGTH = 1500 +) + +func (shop *Shop) getItem(itemId string) (item ShopItem, ok bool) { + item, ok = shop.Items[itemId] + return +} + +var ( + shopKeyboard = &tb.ReplyMarkup{ResizeReplyKeyboard: false} + browseShopButton = shopKeyboard.Data("Browse shops", "shops_browse") + shopNewShopButton = shopKeyboard.Data("New Shop", "shops_newshop") + shopDeleteShopButton = shopKeyboard.Data("Delete Shops", "shops_deleteshop") + shopLinkShopButton = shopKeyboard.Data("Shop links", "shops_linkshop") + shopRenameShopButton = shopKeyboard.Data("Rename shop", "shops_renameshop") + shopResetShopAskButton = shopKeyboard.Data("Delete all shops", "shops_reset_ask") + shopResetShopButton = shopKeyboard.Data("Delete all shops", "shops_reset") + shopDescriptionShopButton = shopKeyboard.Data("Shop description", "shops_description") + shopSettingsButton = shopKeyboard.Data("Settings", "shops_settings") + shopShopsButton = shopKeyboard.Data("Back", "shops_shops") + + shopAddItemButton = shopKeyboard.Data("New item", "shop_additem") + shopNextitemButton = shopKeyboard.Data(">", "shop_nextitem") + shopPrevitemButton = shopKeyboard.Data("<", "shop_previtem") + shopBuyitemButton = shopKeyboard.Data("Buy", "shop_buyitem") + + shopSelectButton = shopKeyboard.Data("SHOP SELECTOR", "select_shop") // shop slectino buttons + shopDeleteSelectButton = shopKeyboard.Data("DELETE SHOP SELECTOR", "delete_shop") // shop slectino buttons + shopLinkSelectButton = shopKeyboard.Data("LINK SHOP SELECTOR", "link_shop") // shop slectino buttons + shopRenameSelectButton = shopKeyboard.Data("RENAME SHOP SELECTOR", "rename_shop") // shop slectino buttons + shopItemPriceButton = shopKeyboard.Data("Price", "shop_itemprice") + shopItemDeleteButton = shopKeyboard.Data("Delete", "shop_itemdelete") + shopItemTitleButton = shopKeyboard.Data("Set title", "shop_itemtitle") + shopItemAddFileButton = shopKeyboard.Data("Add file", "shop_itemaddfile") + shopItemSettingsButton = shopKeyboard.Data("Item settings", "shop_itemsettings") + shopItemSettingsBackButton = shopKeyboard.Data("Back", "shop_itemsettingsback") + + shopItemBuyButton = shopKeyboard.Data("Buy", "shop_itembuy") + shopItemCancelBuyButton = shopKeyboard.Data("Cancel", "shop_itemcancelbuy") +) + +// shopItemPriceHandler is invoked when the user presses the item settings button to set a price +func (bot *TipBot) shopItemPriceHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + if shop.Owner.Telegram.ID != c.Sender.ID { + return + } + item := shop.Items[shop.ItemIds[shopView.Page]] + // sanity check + if item.ID != c.Data { + log.Error("[shopItemPriceHandler] item id mismatch") + return + } + // We need to save the pay state in the user state so we can load the payment in the next handler + SetUserState(user, bot, lnbits.UserStateShopItemSendPrice, item.ID) + bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("💯 Enter a price."), tb.ForceReply) +} + +// enterShopItemPriceHandler is invoked when the user enters a price amount +func (bot *TipBot) enterShopItemPriceHandler(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + if err != nil { + return + } + if shop.Owner.Telegram.ID != m.Sender.ID { + return + } + item := shop.Items[shop.ItemIds[shopView.Page]] + // sanity check + if item.ID != user.StateData { + log.Error("[shopItemPriceHandler] item id mismatch") + return + } + if shop.Owner.Telegram.ID != m.Sender.ID { + return + } + + var amount int64 + if m.Text == "0" { + amount = 0 + } else { + amount, err = getAmount(m.Text) + if err != nil { + log.Warnf("[enterShopItemPriceHandler] %s", err.Error()) + bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) + ResetUserState(user, bot) + return //err, 0 + } + } + + if amount > 200 { + bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("ℹ️ During alpha testing, price can be max 200 sat.")) + amount = 200 + } + item.Price = amount + shop.Items[item.ID] = item + runtime.IgnoreError(shop.Set(shop, bot.ShopBunt)) + bot.tryDeleteMessage(m) + bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("✅ Price set.")) + ResetUserState(user, bot) + // go func() { + // time.Sleep(time.Duration(5) * time.Second) + // bot.shopViewDeleteAllStatusMsgs(ctx, user) + // }() + bot.displayShopItem(ctx, shopView.Message, shop) +} + +// shopItemPriceHandler is invoked when the user presses the item settings button to set a item title +func (bot *TipBot) shopItemTitleHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + if shop.Owner.Telegram.ID != c.Sender.ID { + return + } + item := shop.Items[shop.ItemIds[shopView.Page]] + // sanity check + if item.ID != c.Data { + log.Error("[shopItemTitleHandler] item id mismatch") + return + } + // We need to save the pay state in the user state so we can load the payment in the next handler + SetUserState(user, bot, lnbits.UserStateShopItemSendTitle, item.ID) + bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("⌨️ Enter item title."), tb.ForceReply) +} + +// enterShopItemTitleHandler is invoked when the user enters a title of the item +func (bot *TipBot) enterShopItemTitleHandler(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + if err != nil { + return + } + if shop.Owner.Telegram.ID != m.Sender.ID { + return + } + item := shop.Items[shop.ItemIds[shopView.Page]] + // sanity check + if item.ID != user.StateData { + log.Error("[enterShopItemTitleHandler] item id mismatch") + return + } + if shop.Owner.Telegram.ID != m.Sender.ID { + return + } + if len(m.Text) == 0 { + ResetUserState(user, bot) + bot.sendStatusMessageAndDelete(ctx, m.Sender, "🚫 Action cancelled.") + go func() { + time.Sleep(time.Duration(5) * time.Second) + bot.shopViewDeleteAllStatusMsgs(ctx, user, 1) + }() + return + } + // crop item title + if len(m.Text) > ITEM_TITLE_MAX_LENGTH { + m.Text = m.Text[:ITEM_TITLE_MAX_LENGTH] + } + item.Title = m.Text + item.TbPhoto.Caption = m.Text + shop.Items[item.ID] = item + runtime.IgnoreError(shop.Set(shop, bot.ShopBunt)) + bot.tryDeleteMessage(m) + bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("✅ Title set.")) + ResetUserState(user, bot) + // go func() { + // time.Sleep(time.Duration(5) * time.Second) + // bot.shopViewDeleteAllStatusMsgs(ctx, user) + // }() + bot.displayShopItem(ctx, shopView.Message, shop) +} + +// shopItemSettingsHandler is invoked when the user presses the item settings button +func (bot *TipBot) shopItemSettingsHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + item := shop.Items[shop.ItemIds[shopView.Page]] + // sanity check + if item.ID != c.Data { + log.Error("[shopItemSettingsHandler] item id mismatch") + return + } + if item.TbPhoto != nil { + item.TbPhoto.Caption = bot.getItemTitle(ctx, &item) + } + bot.tryEditMessage(shopView.Message, item.TbPhoto, bot.shopItemSettingsMenu(ctx, shop, &item)) +} + +// shopItemPriceHandler is invoked when the user presses the item settings button to set a item title +func (bot *TipBot) shopItemDeleteHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + if err != nil { + return + } + if shop.Owner.Telegram.ID != c.Sender.ID { + return + } + item := shop.Items[shop.ItemIds[shopView.Page]] + if shop.Owner.Telegram.ID != c.Sender.ID { + return + } + + // delete ItemID of item + for i, itemId := range shop.ItemIds { + if itemId == item.ID { + if len(shop.ItemIds) == 1 { + shop.ItemIds = []string{} + } else { + shop.ItemIds = append(shop.ItemIds[:i], shop.ItemIds[i+1:]...) + } + break + } + } + // delete item itself + delete(shop.Items, item.ID) + runtime.IgnoreError(shop.Set(shop, bot.ShopBunt)) + + ResetUserState(user, bot) + bot.sendStatusMessageAndDelete(ctx, c.Message.Chat, fmt.Sprintf("✅ Item deleted.")) + // go func() { + // time.Sleep(time.Duration(5) * time.Second) + // bot.shopViewDeleteAllStatusMsgs(ctx, user) + // }() + if shopView.Page > 0 { + shopView.Page-- + } + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + bot.displayShopItem(ctx, shopView.Message, shop) +} + +// displayShopItemHandler is invoked when the user presses the back button in the item settings +func (bot *TipBot) displayShopItemHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + // item := shop.Items[shop.ItemIds[shopView.Page]] + // // sanity check + // if item.ID != c.Data { + // log.Error("[shopItemSettingsHandler] item id mismatch") + // return + // } + bot.displayShopItem(ctx, c.Message, shop) +} + +// shopNextItemHandler is invoked when the user presses the next item button +func (bot *TipBot) shopNextItemButtonHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + // shopView, err := bot.Cache.Get(fmt.Sprintf("shopview-%d", user.Telegram.ID)) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + if shopView.Page < len(shop.Items)-1 { + shopView.Page++ + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + shop, err = bot.getShop(ctx, shopView.ShopID) + bot.displayShopItem(ctx, c.Message, shop) + } +} + +// shopPrevItemButtonHandler is invoked when the user presses the previous item button +func (bot *TipBot) shopPrevItemButtonHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + if shopView.Page == 0 { + c.Message.Text = "/shops " + shopView.ShopOwner.Telegram.Username + bot.shopsHandler(ctx, c.Message) + return + } + if shopView.Page > 0 { + shopView.Page-- + } + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + shop, err := bot.getShop(ctx, shopView.ShopID) + bot.displayShopItem(ctx, c.Message, shop) +} + +func (bot *TipBot) getItemTitle(ctx context.Context, item *ShopItem) string { + caption := "" + if len(item.Title) > 0 { + caption = fmt.Sprintf("%s", item.Title) + } + if len(item.FileIDs) > 0 { + if len(caption) > 0 { + caption += " " + } + caption += fmt.Sprintf("(%d Files)", len(item.FileIDs)) + } + if item.Price > 0 { + caption += fmt.Sprintf("\n\n💸 Price: %d sat", item.Price) + } + // item.TbPhoto.Caption = caption + return caption +} + +// displayShopItem renders the current item in the shopView +// requires that the shopview page is already set accordingly +// m is the message that will be edited +func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Shop) *tb.Message { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + log.Errorf("[displayShopItem] %s", err.Error()) + return nil + } + // failsafe: if the page is out of bounds, reset it + if shopView.Page >= len(shop.Items) { + shopView.Page = len(shop.Items) - 1 + } + + if len(shop.Items) == 0 { + no_items_message := "There are no items in this shop yet." + if len(shopView.Message.Text) > 0 { + shopView.Message = bot.tryEditMessage(shopView.Message, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) + } else { + bot.tryDeleteMessage(shopView.Message) + shopView.Message = bot.trySendMessage(shopView.Message.Chat, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) + } + shopView.Page = 0 + return shopView.Message + } + + item := shop.Items[shop.ItemIds[shopView.Page]] + if item.TbPhoto != nil { + item.TbPhoto.Caption = bot.getItemTitle(ctx, &item) + } + + // var msg *tb.Message + if shopView.Message != nil { + if item.TbPhoto != nil { + if shopView.Message.Photo != nil { + // can only edit photo messages with another photo + shopView.Message = bot.tryEditMessage(shopView.Message, item.TbPhoto, bot.shopMenu(ctx, shop, &item)) + } else { + // if editing failes + bot.tryDeleteMessage(shopView.Message) + shopView.Message = bot.trySendMessage(shopView.Message.Chat, item.TbPhoto, bot.shopMenu(ctx, shop, &item)) + } + } else if item.Title != "" { + shopView.Message = bot.tryEditMessage(shopView.Message, item.Title, bot.shopMenu(ctx, shop, &item)) + if shopView.Message == nil { + shopView.Message = bot.trySendMessage(shopView.Message.Chat, item.Title, bot.shopMenu(ctx, shop, &item)) + } + } + } else { + if m != nil && m.Chat != nil { + shopView.Message = bot.trySendMessage(m.Chat, item.TbPhoto, bot.shopMenu(ctx, shop, &item)) + } else { + shopView.Message = bot.trySendMessage(user.Telegram, item.TbPhoto, bot.shopMenu(ctx, shop, &item)) + } + // shopView.Page = 0 + } + // shopView.Message = msg + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + return shopView.Message +} + +// shopHandler is invoked when the user enters /shop +func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) { + if !m.Private() { + return + } + user := LoadUser(ctx) + shopOwner := user + + // when no argument is given, i.e. command is only /shop, load /shops + shop := &Shop{} + if len(strings.Split(m.Text, " ")) < 2 || !strings.HasPrefix(strings.Split(m.Text, " ")[1], "shop-") { + bot.shopsHandler(ctx, m) + return + } else { + // else: get shop by shop ID + shopID := strings.Split(m.Text, " ")[1] + var err error + shop, err = bot.getShop(ctx, shopID) + if err != nil { + log.Errorf("[shopHandler] %s", err) + return + } + } + shopOwner = shop.Owner + shopView := ShopView{ + ID: fmt.Sprintf("shopview-%d", user.Telegram.ID), + ShopID: shop.ID, + Page: 0, + ShopOwner: shopOwner, + } + // bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + shopView.Message = bot.displayShopItem(ctx, m, shop) + // shopMessage := &tb.Message{Chat: m.Chat} + // if len(shop.ItemIds) > 0 { + // // item := shop.Items[shop.ItemIds[shopView.Page]] + // // shopMessage = bot.trySendMessage(m.Chat, item.TbPhoto, bot.shopMenu(ctx, shop, &item)) + // shopMessage = bot.displayShopItem(ctx, m, shop) + // } else { + // shopMessage = bot.trySendMessage(m.Chat, "No items in shop.", bot.shopMenu(ctx, shop, &ShopItem{})) + // } + // shopView.Message = shopMessage + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + return +} + +// shopNewItemHandler is invoked when the user presses the new item button +func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shop, err := bot.getShop(ctx, c.Data) + if err != nil { + log.Errorf("[shopNewItemHandler] %s", err) + return + } + if shop.Owner.Telegram.ID != c.Sender.ID { + return + } + if len(shop.Items) >= shop.MaxItems { + bot.trySendMessage(c.Sender, fmt.Sprintf("🚫 You can only have %d items in this shop. Delete an item to add a new one.", shop.MaxItems)) + return + } + + // We need to save the pay state in the user state so we can load the payment in the next handler + paramsJson, err := json.Marshal(shop) + if err != nil { + log.Errorf("[lnurlWithdrawHandler] Error: %s", err.Error()) + // bot.trySendMessage(m.Sender, err.Error()) + return + } + SetUserState(user, bot, lnbits.UserStateShopItemSendPhoto, string(paramsJson)) + bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("🌄 Send me an image.")) +} + +// addShopItem is a helper function for creating a shop item in the database +func (bot *TipBot) addShopItem(ctx context.Context, shopId string) (*Shop, ShopItem, error) { + shop, err := bot.getShop(ctx, shopId) + if err != nil { + log.Errorf("[addShopItem] %s", err) + return shop, ShopItem{}, err + } + user := LoadUser(ctx) + // onnly the correct user can press + if shop.Owner.Telegram.ID != user.Telegram.ID { + return shop, ShopItem{}, fmt.Errorf("not owner") + } + // err = shop.Lock(shop, bot.ShopBunt) + // defer shop.Release(shop, bot.ShopBunt) + + itemId := fmt.Sprintf("item-%s-%s", shop.ID, RandStringRunes(8)) + item := ShopItem{ + ID: itemId, + ShopID: shop.ID, + Owner: user, + Type: "photo", + LanguageCode: shop.LanguageCode, + MaxFiles: MAX_FILES_PER_ITEM, + } + shop.Items[itemId] = item + shop.ItemIds = append(shop.ItemIds, itemId) + runtime.IgnoreError(shop.Set(shop, bot.ShopBunt)) + return shop, shop.Items[itemId], nil +} + +// addShopItemPhoto is invoked when the users sends a photo as a new item +func (bot *TipBot) addShopItemPhoto(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + + // read item from user.StateData + var state_shop Shop + err := json.Unmarshal([]byte(user.StateData), &state_shop) + if err != nil { + log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) + bot.trySendMessage(m.Sender, Translate(ctx, "errorTryLaterMessage"), Translate(ctx, "errorTryLaterMessage")) + return + } + if state_shop.Owner.Telegram.ID != m.Sender.ID { + return + } + if m.Photo == nil { + bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("🚫 That didn't work. You need to send an image (not a file).")) + ResetUserState(user, bot) + return + } + + shop, item, err := bot.addShopItem(ctx, state_shop.ID) + // err = shop.Lock(shop, bot.ShopBunt) + // defer shop.Release(shop, bot.ShopBunt) + item.TbPhoto = m.Photo + item.Title = m.Caption + shop.Items[item.ID] = item + runtime.IgnoreError(shop.Set(shop, bot.ShopBunt)) + + bot.tryDeleteMessage(m) + bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("✅ Image added.")) + ResetUserState(user, bot) + // go func() { + // time.Sleep(time.Duration(5) * time.Second) + // bot.shopViewDeleteAllStatusMsgs(ctx, user) + // }() + + shopView, err := bot.getUserShopview(ctx, user) + shopView.Page = len(shop.Items) - 1 + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + bot.displayShopItem(ctx, shopView.Message, shop) + + log.Infof("[🛍 shop] %s added an item %s:%s.", GetUserStr(user.Telegram), shop.ID, item.ID) +} + +// ------------------- item files ---------- +// shopItemAddItemHandler is invoked when the user presses the new item button +func (bot *TipBot) shopItemAddItemHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + log.Errorf("[addItemFileHandler] %s", err.Error()) + return + } + + shop, err := bot.getShop(ctx, shopView.ShopID) + if err != nil { + log.Errorf("[shopNewItemHandler] %s", err) + return + } + + itemID := c.Data + + item := shop.Items[itemID] + + if len(item.FileIDs) >= item.MaxFiles { + bot.trySendMessage(c.Sender, fmt.Sprintf("🚫 You can only have %d files in this item.", item.MaxFiles)) + return + } + SetUserState(user, bot, lnbits.UserStateShopItemSendItemFile, c.Data) + bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("💾 Send me one or more files.")) +} + +// addItemFileHandler is invoked when the users sends a new file for the item +func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + log.Errorf("[addItemFileHandler] %s", err.Error()) + return + } + + shop, err := bot.getShop(ctx, shopView.ShopID) + if err != nil { + log.Errorf("[shopNewItemHandler] %s", err) + return + } + + itemID := user.StateData + + item := shop.Items[itemID] + if m.Photo != nil { + item.FileIDs = append(item.FileIDs, m.Photo.FileID) + item.FileTypes = append(item.FileTypes, "photo") + bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("ℹ️ To send more than one photo at a time, send them as files.")) + } else if m.Document != nil { + item.FileIDs = append(item.FileIDs, m.Document.FileID) + item.FileTypes = append(item.FileTypes, "document") + } else if m.Audio != nil { + item.FileIDs = append(item.FileIDs, m.Audio.FileID) + item.FileTypes = append(item.FileTypes, "audio") + } else if m.Video != nil { + item.FileIDs = append(item.FileIDs, m.Video.FileID) + item.FileTypes = append(item.FileTypes, "video") + } else if m.Voice != nil { + item.FileIDs = append(item.FileIDs, m.Voice.FileID) + item.FileTypes = append(item.FileTypes, "voice") + } else if m.VideoNote != nil { + item.FileIDs = append(item.FileIDs, m.VideoNote.FileID) + item.FileTypes = append(item.FileTypes, "videonote") + } else if m.Sticker != nil { + item.FileIDs = append(item.FileIDs, m.Sticker.FileID) + item.FileTypes = append(item.FileTypes, "sticker") + } else { + log.Errorf("[addItemFileHandler] no file found") + return + } + shop.Items[item.ID] = item + + runtime.IgnoreError(shop.Set(shop, bot.ShopBunt)) + bot.tryDeleteMessage(m) + bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("✅ File added.")) + + // ticker := runtime.GetTicker(shop.ID, runtime.WithDuration(5*time.Second)) + // if !ticker.Started { + // ticker.Do(func() { + // bot.shopViewDeleteAllStatusMsgs(ctx, user) + // // removing ticker asap done + // runtime.RemoveTicker(shop.ID) + // }) + // } else { + // ticker.ResetChan <- struct{}{} + // } + + // // start a ticker to check if the user has sent more files + // if t, ok := fileStateResetTicker.Get(shop.ID); ok { + // // state reset ticker found. resetting ticker. + // t.(*runtime.ResettableFunctionTicker).ResetChan <- struct{}{} + // } else { + // // state reset ticker not found. creating new one. + // ticker := runtime.NewResettableFunctionTicker(runtime.WithDuration(time.Second * 5)) + // // storing reset ticker in mem + // fileStateResetTicker.Set(shop.ID, ticker) + // go func() { + // // starting ticker + // ticker.Do(func() { + // // time.Sleep(time.Duration(5) * time.Second) + // bot.shopViewDeleteAllStatusMsgs(ctx, user) + // // removing ticker asap done + // fileStateResetTicker.Remove(shop.ID) + // }) + // }() + // } + + // go func() { + // time.Sleep(time.Duration(5) * time.Second) + // bot.shopViewDeleteAllStatusMsgs(ctx, user) + // }() + bot.displayShopItem(ctx, shopView.Message, shop) + log.Infof("[🛍 shop] %s added a file to shop:item %s:%s.", GetUserStr(user.Telegram), shop.ID, item.ID) +} + +func (bot *TipBot) shopGetItemFilesHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + log.Errorf("[addItemFileHandler] %s", err.Error()) + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + if err != nil { + log.Errorf("[shopNewItemHandler] %s", err) + return + } + itemID := c.Data + item := shop.Items[itemID] + + if item.Price <= 0 { + bot.shopSendItemFilesToUser(ctx, user, itemID) + } else { + if item.TbPhoto != nil { + item.TbPhoto.Caption = bot.getItemTitle(ctx, &item) + } + bot.tryEditMessage(shopView.Message, item.TbPhoto, bot.shopItemConfirmBuyMenu(ctx, shop, &item)) + } + + // // send the cover image + // bot.sendFileByID(ctx, c.Sender, item.TbPhoto.FileID, "photo") + // // and all other files + // for i, fileID := range item.FileIDs { + // bot.sendFileByID(ctx, c.Sender, fileID, item.FileTypes[i]) + // } + // log.Infof("[🛍 shop] %s got %d items from %s's item %s (for %d sat).", GetUserStr(user.Telegram), len(item.FileIDs), GetUserStr(shop.Owner.Telegram), item.ID, item.Price) + +} + +// shopConfirmBuyHandler is invoked when the user has confirmed to pay for an item +func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + log.Errorf("[shopConfirmBuyHandler] %s", err.Error()) + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + if err != nil { + log.Errorf("[shopConfirmBuyHandler] %s", err) + return + } + itemID := c.Data + item := shop.Items[itemID] + if item.Owner.ID != shop.Owner.ID { + log.Errorf("[shopConfirmBuyHandler] Owners do not match.") + return + } + from := user + to := shop.Owner + + // fromUserStr := GetUserStr(from.Telegram) + // fromUserStrMd := GetUserStrMd(from.Telegram) + toUserStr := GetUserStr(to.Telegram) + toUserStrMd := GetUserStrMd(to.Telegram) + amount := item.Price + if amount <= 0 { + log.Errorf("[shopConfirmBuyHandler] item has no price.") + return + } + transactionMemo := fmt.Sprintf("Buy item %s (%d sat).", toUserStr, amount) + t := NewTransaction(bot, from, to, amount, TransactionType("shop")) + t.Memo = transactionMemo + + success, err := t.Send() + if !success || err != nil { + // bot.trySendMessage(c.Sender, sendErrorMessage) + errmsg := fmt.Sprintf("[shop] Error: Transaction failed. %s", err) + log.Errorln(errmsg) + bot.trySendMessage(user.Telegram, i18n.Translate(user.Telegram.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{}) + return + } + // bot.trySendMessage(user.Telegram, fmt.Sprintf("🛍 %d sat sent to %s.", amount, toUserStrMd), &tb.ReplyMarkup{}) + shopItemTitle := "an item" + if len(item.Title) > 0 { + shopItemTitle = fmt.Sprintf("%s", item.Title) + } + bot.trySendMessage(to.Telegram, fmt.Sprintf("🛍 Someone bought `%s` from your shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), str.MarkdownEscape(shop.Title), amount)) + bot.trySendMessage(from.Telegram, fmt.Sprintf("🛍 You bought `%s` from %s's shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), toUserStrMd, str.MarkdownEscape(shop.Title), amount)) + log.Infof("[🛍 shop] %s bought `%s` from %s's shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), toUserStrMd, str.MarkdownEscape(shop.Title), amount) + bot.shopSendItemFilesToUser(ctx, user, itemID) +} + +// shopSendItemFilesToUser is a handler function to send itemID's files to the user +func (bot *TipBot) shopSendItemFilesToUser(ctx context.Context, toUser *lnbits.User, itemID string) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + log.Errorf("[addItemFileHandler] %s", err.Error()) + return + } + shop, err := bot.getShop(ctx, shopView.ShopID) + if err != nil { + log.Errorf("[shopNewItemHandler] %s", err) + return + } + item := shop.Items[itemID] + // send the cover image + bot.sendFileByID(ctx, toUser.Telegram, item.TbPhoto.FileID, "photo") + // and all other files + for i, fileID := range item.FileIDs { + bot.sendFileByID(ctx, toUser.Telegram, fileID, item.FileTypes[i]) + } + log.Infof("[🛍 shop] %s got %d items from %s's item %s (for %d sat).", GetUserStr(user.Telegram), len(item.FileIDs), GetUserStr(shop.Owner.Telegram), item.ID, item.Price) + + // delete old shop and show again below the files + if shopView.Message != nil { + bot.tryDeleteMessage(shopView.Message) + } + shopView.Message = nil + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + bot.displayShopItem(ctx, &tb.Message{}, shop) +} + +func (bot *TipBot) sendFileByID(ctx context.Context, to tb.Recipient, fileId string, fileType string) { + switch fileType { + case "photo": + sendable := &tb.Photo{File: tb.File{FileID: fileId}} + bot.trySendMessage(to, sendable) + case "document": + sendable := &tb.Document{File: tb.File{FileID: fileId}} + bot.trySendMessage(to, sendable) + case "audio": + sendable := &tb.Audio{File: tb.File{FileID: fileId}} + bot.trySendMessage(to, sendable) + case "video": + sendable := &tb.Video{File: tb.File{FileID: fileId}} + bot.trySendMessage(to, sendable) + case "voice": + sendable := &tb.Voice{File: tb.File{FileID: fileId}} + bot.trySendMessage(to, sendable) + case "videonote": + sendable := &tb.VideoNote{File: tb.File{FileID: fileId}} + bot.trySendMessage(to, sendable) + case "sticker": + sendable := &tb.Sticker{File: tb.File{FileID: fileId}} + bot.trySendMessage(to, sendable) + } + return +} + +// -------------- shops handler -------------- +// var ShopsText = "*Welcome to %s shop.*\n%s\nThere are %d shops here.\n%s" +var ShopsText = "" +var ShopsTextWelcome = "*You are browsing %s shop.*" +var ShopsTextShopCount = "*Browse %d shops:*" +var ShopsTextHelp = "⚠️ Shops are still in beta. Expect bugs." +var ShopsNoShopsText = "*There are no shops here yet.*" + +// shopsHandlerCallback is a warpper for shopsHandler for callbacks +func (bot *TipBot) shopsHandlerCallback(ctx context.Context, c *tb.Callback) { + bot.shopsHandler(ctx, c.Message) +} + +// shopsHandler is invoked when the user enters /shops +func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { + if !m.Private() { + return + } + user := LoadUser(ctx) + shopOwner := user + + // if the user in the command, i.e. /shops @user + if len(strings.Split(m.Text, " ")) > 1 && strings.HasPrefix(strings.Split(m.Text, " ")[0], "/shop") { + toUserStrMention := "" + toUserStrWithoutAt := "" + + // check for user in command, accepts user mention or plain username without @ + if len(m.Entities) > 1 && m.Entities[1].Type == "mention" { + toUserStrMention = m.Text[m.Entities[1].Offset : m.Entities[1].Offset+m.Entities[1].Length] + toUserStrWithoutAt = strings.TrimPrefix(toUserStrMention, "@") + } else { + var err error + toUserStrWithoutAt, err = getArgumentFromCommand(m.Text, 1) + if err != nil { + log.Errorln(err.Error()) + return + } + toUserStrWithoutAt = strings.TrimPrefix(toUserStrWithoutAt, "@") + toUserStrMention = "@" + toUserStrWithoutAt + } + + toUserDb, err := GetUserByTelegramUsername(toUserStrWithoutAt, *bot) + if err != nil { + NewMessage(m, WithDuration(0, bot)) + // cut username if it's too long + if len(toUserStrMention) > 100 { + toUserStrMention = toUserStrMention[:100] + } + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), str.MarkdownEscape(toUserStrMention))) + return + } + // overwrite user with the one from db + shopOwner = toUserDb + } else if !strings.HasPrefix(strings.Split(m.Text, " ")[0], "/shop") { + // otherwise, the user is returning to a shops view from a back button callback + shopView, err := bot.getUserShopview(ctx, user) + if err == nil { + shopOwner = shopView.ShopOwner + } + } + + if shopOwner == nil { + log.Error("[shopsHandler] shopOwner is nil") + return + } + shops, err := bot.getUserShops(ctx, shopOwner) + if err != nil && user.Telegram.ID == shopOwner.Telegram.ID { + shops, err = bot.initUserShops(ctx, user) + if err != nil { + log.Errorf("[shopsHandler] %s", err) + return + } + } + + if len(shops.Shops) == 0 && user.Telegram.ID != shopOwner.Telegram.ID { + bot.trySendMessage(m.Chat, fmt.Sprintf("This user has no shops yet.")) + return + } + + // build shop list + shopTitles := "" + for _, shopId := range shops.Shops { + shop, err := bot.getShop(ctx, shopId) + if err != nil { + log.Errorf("[shopsHandler] %s", err) + return + } + shopTitles += fmt.Sprintf("\n· %s (%d items)", str.MarkdownEscape(shop.Title), len(shop.Items)) + + } + + // build shop text + + // shows "your shop" or "@other's shop" + shopOwnerText := "your" + if shopOwner.Telegram.ID != user.Telegram.ID { + shopOwnerText = fmt.Sprintf("%s's", GetUserStrMd(shopOwner.Telegram)) + } + ShopsText = fmt.Sprintf(ShopsTextWelcome, shopOwnerText) + if len(shops.Description) > 0 { + ShopsText += fmt.Sprintf("\n\n%s\n", shops.Description) + } else { + ShopsText += "\n" + } + if len(shops.Shops) > 0 { + ShopsText += fmt.Sprintf("\n%s\n", fmt.Sprintf(ShopsTextShopCount, len(shops.Shops))) + } else { + ShopsText += fmt.Sprintf("\n%s\n", ShopsNoShopsText) + } + + if len(shops.Shops) > 0 { + ShopsText += fmt.Sprintf("%s\n", shopTitles) + } + ShopsText += fmt.Sprintf("\n%s", ShopsTextHelp) + + // fmt.Sprintf(ShopsText, shopOwnerText, len(shops.Shops), shopTitles) + + // if the user used the command /shops, we will send a new message + // if the user clicked a button and has a shopview set, we will edit an old message + shopView, err := bot.getUserShopview(ctx, user) + var shopsMsg *tb.Message + if err == nil && !strings.HasPrefix(strings.Split(m.Text, " ")[0], "/shop") { + // the user is returning to a shops view from a back button callback + if shopView.Message.Photo == nil { + shopsMsg = bot.tryEditMessage(shopView.Message, ShopsText, bot.shopsMainMenu(ctx, shops)) + } + if shopsMsg == nil { + // if editing has failed, we will send a new message + bot.tryDeleteMessage(shopView.Message) + shopsMsg = bot.trySendMessage(m.Chat, ShopsText, bot.shopsMainMenu(ctx, shops)) + + } + } else { + // the user has entered /shops or + // the user has no shopview set, so we will send a new message + if shopView.Message != nil { + // delete any old shop message + bot.tryDeleteMessage(shopView.Message) + } + shopsMsg = bot.trySendMessage(m.Chat, ShopsText, bot.shopsMainMenu(ctx, shops)) + } + shopViewNew := ShopView{ + ID: fmt.Sprintf("shopview-%d", user.Telegram.ID), + Message: shopsMsg, + ShopOwner: shopOwner, + StatusMessages: shopView.StatusMessages, // keep the old status messages + } + bot.Cache.Set(shopViewNew.ID, shopViewNew, &store.Options{Expiration: 24 * time.Hour}) + return +} + +// shopsDeleteShopBrowser is invoked when the user clicks on "delete shops" and makes a list of all shops +func (bot *TipBot) shopsDeleteShopBrowser(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shops, err := bot.getUserShops(ctx, user) + if err != nil { + return + } + var s []*Shop + for _, shopId := range shops.Shops { + shop, _ := bot.getShop(ctx, shopId) + if shop.Owner.Telegram.ID != c.Sender.ID { + return + } + s = append(s, shop) + } + shopShopsButton := shopKeyboard.Data("⬅️ Back", "shops_shops", shops.ID) + + shopResetShopAskButton = shopKeyboard.Data("⚠️ Delete all shops", "shops_reset_ask", shops.ID) + shopKeyboard.Inline(buttonWrapper(append(bot.makseShopSelectionButtons(s, "delete_shop"), shopResetShopAskButton, shopShopsButton), shopKeyboard, 1)...) + bot.tryEditMessage(c.Message, "Which shop do you want to delete?", shopKeyboard) +} + +func (bot *TipBot) shopsAskDeleteAllShopsHandler(ctx context.Context, c *tb.Callback) { + shopResetShopButton := shopKeyboard.Data("⚠️ Delete all shops", "shops_reset", c.Data) + buttons := []tb.Row{ + shopKeyboard.Row(shopResetShopButton), + shopKeyboard.Row(shopShopsButton), + } + shopKeyboard.Inline( + buttons..., + ) + bot.tryEditMessage(c.Message, "Are you sure you want to delete all shops?\nYou will lose all items as well.", shopKeyboard) +} + +// shopsLinkShopBrowser is invoked when the user clicks on "shop links" and makes a list of all shops +func (bot *TipBot) shopsLinkShopBrowser(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shops, err := bot.getUserShops(ctx, user) + if err != nil { + return + } + var s []*Shop + for _, shopId := range shops.Shops { + shop, _ := bot.getShop(ctx, shopId) + if shop.Owner.Telegram.ID != c.Sender.ID { + return + } + s = append(s, shop) + } + shopShopsButton := shopKeyboard.Data("⬅️ Back", "shops_shops", shops.ID) + shopKeyboard.Inline(buttonWrapper(append(bot.makseShopSelectionButtons(s, "link_shop"), shopShopsButton), shopKeyboard, 1)...) + bot.tryEditMessage(c.Message, "Select the shop you want to get the link of.", shopKeyboard) +} + +// shopSelectLink is invoked when the user has chosen a shop to get the link of +func (bot *TipBot) shopSelectLink(ctx context.Context, c *tb.Callback) { + shop, _ := bot.getShop(ctx, c.Data) + if shop.Owner.Telegram.ID != c.Sender.ID { + return + } + bot.trySendMessage(c.Sender, fmt.Sprintf("*%s*: `/shop %s`", shop.Title, shop.ID)) +} + +// shopsLinkShopBrowser is invoked when the user clicks on "shop links" and makes a list of all shops +func (bot *TipBot) shopsRenameShopBrowser(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shops, err := bot.getUserShops(ctx, user) + if err != nil { + return + } + var s []*Shop + for _, shopId := range shops.Shops { + shop, _ := bot.getShop(ctx, shopId) + if shop.Owner.Telegram.ID != c.Sender.ID { + return + } + s = append(s, shop) + } + shopShopsButton := shopKeyboard.Data("⬅️ Back", "shops_shops", shops.ID) + shopKeyboard.Inline(buttonWrapper(append(bot.makseShopSelectionButtons(s, "rename_shop"), shopShopsButton), shopKeyboard, 1)...) + bot.tryEditMessage(c.Message, "Select the shop you want to rename.", shopKeyboard) +} + +// shopSelectLink is invoked when the user has chosen a shop to get the link of +func (bot *TipBot) shopSelectRename(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shop, _ := bot.getShop(ctx, c.Data) + if shop.Owner.Telegram.ID != c.Sender.ID { + return + } + // We need to save the pay state in the user state so we can load the payment in the next handler + SetUserState(user, bot, lnbits.UserEnterShopTitle, shop.ID) + bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("⌨️ Enter the name of your shop."), tb.ForceReply) +} + +// shopsDescriptionHandler is invoked when the user clicks on "description" to set a shop description +func (bot *TipBot) shopsDescriptionHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shops, err := bot.getUserShops(ctx, user) + if err != nil { + log.Errorf("[shopsDescriptionHandler] %s", err) + return + } + SetUserState(user, bot, lnbits.UserEnterShopsDescription, shops.ID) + bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("⌨️ Enter a description."), tb.ForceReply) +} + +// enterShopsDescriptionHandler is invoked when the user enters the shop title +func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + shops, err := bot.getUserShops(ctx, user) + if err != nil { + log.Errorf("[enterShopsDescriptionHandler] %s", err) + return + } + if shops.Owner.Telegram.ID != m.Sender.ID { + return + } + if len(m.Text) == 0 { + ResetUserState(user, bot) + bot.sendStatusMessageAndDelete(ctx, m.Sender, "🚫 Action cancelled.") + go func() { + time.Sleep(time.Duration(5) * time.Second) + bot.shopViewDeleteAllStatusMsgs(ctx, user, 1) + }() + return + } + + // crop shop title + if len(m.Text) > SHOPS_DESCRIPTION_MAX_LENGTH { + m.Text = m.Text[:SHOPS_DESCRIPTION_MAX_LENGTH] + } + shops.Description = m.Text + runtime.IgnoreError(shops.Set(shops, bot.ShopBunt)) + bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("✅ Description set.")) + ResetUserState(user, bot) + // go func() { + // time.Sleep(time.Duration(5) * time.Second) + // bot.shopViewDeleteAllStatusMsgs(ctx, user) + // }() + bot.shopsHandler(ctx, m) + bot.tryDeleteMessage(m) +} + +// shopsResetHandler is invoked when the user clicks button to reset shops completely +func (bot *TipBot) shopsResetHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shops, err := bot.getUserShops(ctx, user) + if err != nil { + log.Errorf("[shopsResetHandler] %s", err) + return + } + if shops.Owner.Telegram.ID != c.Sender.ID { + return + } + runtime.IgnoreError(shops.Delete(shops, bot.ShopBunt)) + bot.sendStatusMessageAndDelete(ctx, c.Sender, fmt.Sprintf("✅ Shops reset.")) + // go func() { + // time.Sleep(time.Duration(5) * time.Second) + // bot.shopViewDeleteAllStatusMsgs(ctx, user) + // }() + bot.shopsHandlerCallback(ctx, c) +} + +// shopSelect is invoked when the user has selected a shop to browse +func (bot *TipBot) shopSelect(ctx context.Context, c *tb.Callback) { + shop, _ := bot.getShop(ctx, c.Data) + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + shopView = ShopView{ + ID: fmt.Sprintf("shopview-%d", c.Sender.ID), + ShopID: shop.ID, + Page: 0, + } + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + } + shopView.Page = 0 + shopView.ShopID = shop.ID + + // var shopMessage *tb.Message + shopMessage := bot.displayShopItem(ctx, c.Message, shop) + // if len(shop.ItemIds) > 0 { + // bot.tryDeleteMessage(c.Message) + // shopMessage = bot.displayShopItem(ctx, c.Message, shop) + // } else { + // shopMessage = bot.tryEditMessage(c.Message, "There are no items in this shop yet.", bot.shopMenu(ctx, shop, &ShopItem{})) + // } + shopView.Message = shopMessage + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + log.Infof("[🛍 shop] %s entered shop %s.", GetUserStr(user.Telegram), shop.ID) +} + +// shopSelectDelete is invoked when the user has chosen a shop to delete +func (bot *TipBot) shopSelectDelete(ctx context.Context, c *tb.Callback) { + shop, _ := bot.getShop(ctx, c.Data) + user := LoadUser(ctx) + shops, err := bot.getUserShops(ctx, user) + if err != nil { + return + } + // first, delete from Shops + for i, shopId := range shops.Shops { + if shopId == shop.ID { + if i == len(shops.Shops)-1 { + shops.Shops = shops.Shops[:i] + } else { + shops.Shops = append(shops.Shops[:i], shops.Shops[i+1:]...) + } + break + } + } + runtime.IgnoreError(shops.Set(shops, bot.ShopBunt)) + + // then, delete shop + runtime.IgnoreError(shop.Delete(shop, bot.ShopBunt)) + + // then update buttons + bot.shopsDeleteShopBrowser(ctx, c) + log.Infof("[🛍 shop] %s deleted shop %s.", GetUserStr(user.Telegram), shop.ID) +} + +// shopsBrowser makes a button list of all shops the user can browse +func (bot *TipBot) shopsBrowser(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + shops, err := bot.getUserShops(ctx, shopView.ShopOwner) + if err != nil { + return + } + var s []*Shop + for _, shopId := range shops.Shops { + shop, _ := bot.getShop(ctx, shopId) + s = append(s, shop) + } + shopShopsButton := shopKeyboard.Data("⬅️ Back", "shops_shops", shops.ID) + shopKeyboard.Inline(buttonWrapper(append(bot.makseShopSelectionButtons(s, "select_shop"), shopShopsButton), shopKeyboard, 1)...) + shopMessage := bot.tryEditMessage(c.Message, "Select a shop you want to browse.", shopKeyboard) + shopView, err = bot.getUserShopview(ctx, user) + if err != nil { + shopView.Message = shopMessage + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + } + +} + +// shopItemSettingsHandler is invoked when the user presses the shop settings button +func (bot *TipBot) shopSettingsHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shopView, err := bot.getUserShopview(ctx, user) + if err != nil { + return + } + shops, err := bot.getUserShops(ctx, user) + if err != nil { + return + } + if shops.ID != c.Data || shops.Owner.Telegram.ID != user.Telegram.ID { + log.Error("[shopSettingsHandler] item id mismatch") + return + } + bot.tryEditMessage(shopView.Message, shopView.Message.Text, bot.shopsSettingsMenu(ctx, shops)) +} + +// shopNewShopHandler is invoked when the user presses the new shop button +func (bot *TipBot) shopNewShopHandler(ctx context.Context, c *tb.Callback) { + user := LoadUser(ctx) + shops, err := bot.getUserShops(ctx, user) + if err != nil { + log.Errorf("[shopNewShopHandler] %s", err) + return + } + if len(shops.Shops) >= shops.MaxShops { + bot.trySendMessage(c.Sender, fmt.Sprintf("🚫 You can only have %d shops. Delete a shop to create a new one.", shops.MaxShops)) + return + } + shop, err := bot.addUserShop(ctx, user) + // We need to save the pay state in the user state so we can load the payment in the next handler + SetUserState(user, bot, lnbits.UserEnterShopTitle, shop.ID) + bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("⌨️ Enter the name of your shop."), tb.ForceReply) +} + +// enterShopTitleHandler is invoked when the user enters the shop title +func (bot *TipBot) enterShopTitleHandler(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + // read item from user.StateData + shop, err := bot.getShop(ctx, user.StateData) + if err != nil { + return + } + if shop.Owner.Telegram.ID != m.Sender.ID { + return + } + if len(m.Text) == 0 { + ResetUserState(user, bot) + bot.sendStatusMessageAndDelete(ctx, m.Sender, "🚫 Action cancelled.") + go func() { + time.Sleep(time.Duration(5) * time.Second) + bot.shopViewDeleteAllStatusMsgs(ctx, user, 1) + }() + return + } + // crop shop title + m.Text = strings.Replace(m.Text, "\n", " ", -1) + if len(m.Text) > SHOP_TITLE_MAX_LENGTH { + m.Text = m.Text[:SHOP_TITLE_MAX_LENGTH] + } + shop.Title = m.Text + runtime.IgnoreError(shop.Set(shop, bot.ShopBunt)) + bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("✅ Shop added.")) + ResetUserState(user, bot) + // go func() { + // time.Sleep(time.Duration(5) * time.Second) + // bot.shopViewDeleteAllStatusMsgs(ctx, user) + // }() + bot.shopsHandler(ctx, m) + bot.tryDeleteMessage(m) + log.Infof("[🛍 shop] %s added new shop %s.", GetUserStr(user.Telegram), shop.ID) +} diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go new file mode 100644 index 00000000..fc71d7b8 --- /dev/null +++ b/internal/telegram/shop_helpers.go @@ -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 +} diff --git a/internal/telegram/state.go b/internal/telegram/state.go new file mode 100644 index 00000000..ed14c409 --- /dev/null +++ b/internal/telegram/state.go @@ -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, + } +} diff --git a/internal/telegram/text.go b/internal/telegram/text.go index a1d06a08..f8ce6de3 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -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: From 3eba0de851e1c898b979e4b18f90ab644880b7c5 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 23 Dec 2021 10:43:43 +0100 Subject: [PATCH 096/541] Tiptopptiptiptopbranch (#182) * speed up tx lock * tiptooltip better Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/tip.go | 1 - internal/telegram/tooltip.go | 15 +++++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index a495533a..10383440 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -117,7 +117,6 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { // update tooltip if necessary messageHasTip := tipTooltipHandler(m, bot, amount, to.Initialized) - log.Debugf("[tip] Has tip: %t", messageHasTip) log.Infof("[💸 tip] Tip from %s to %s (%d sat).", fromUserStr, toUserStr, amount) diff --git a/internal/telegram/tooltip.go b/internal/telegram/tooltip.go index 758a8f66..f1aaa909 100644 --- a/internal/telegram/tooltip.go +++ b/internal/telegram/tooltip.go @@ -26,6 +26,7 @@ const ( type TipTooltip struct { Message + ID string `json:"id"` TipAmount int64 `json:"tip_amount"` Ntips int `json:"ntips"` LastTip time.Time `json:"last_tip"` @@ -33,7 +34,7 @@ type TipTooltip struct { } func (ttt TipTooltip) Key() string { - return fmt.Sprintf("tip-tool-tip:%s", strconv.Itoa(ttt.Message.Message.ReplyTo.ID)) + return fmt.Sprintf("tip-tool-tip:%s", ttt.ID) } const maxNamesInTipperMessage = 5 @@ -54,6 +55,7 @@ func Tips(nTips int) TipTooltipOption { func NewTipTooltip(m *tb.Message, opts ...TipTooltipOption) *TipTooltip { tipTooltip := &TipTooltip{ + ID: fmt.Sprintf("%d-%d", m.Chat.ID, m.ReplyTo.ID), Message: Message{ Message: m, }, @@ -103,8 +105,8 @@ func getTippersString(tippers []*tb.User) string { } // tipTooltipExists checks if this tip is already known -func tipTooltipExists(replyToId int, bot *TipBot) (bool, *TipTooltip) { - message := NewTipTooltip(&tb.Message{ReplyTo: &tb.Message{ID: replyToId}}) +func tipTooltipExists(m *tb.Message, bot *TipBot) (bool, *TipTooltip) { + message := NewTipTooltip(m) err := bot.Bunt.Get(message) if err != nil { return false, message @@ -116,7 +118,8 @@ func tipTooltipExists(replyToId int, bot *TipBot) (bool, *TipTooltip) { // tipTooltipHandler function to update the tooltip below a tipped message. either updates or creates initial tip tool tip func tipTooltipHandler(m *tb.Message, bot *TipBot, amount int64, initializedWallet bool) (hasTip bool) { // todo: this crashes if the tooltip message (maybe also the original tipped message) was deleted in the mean time!!! need to check for existence! - hasTip, ttt := tipTooltipExists(m.ReplyTo.ID, bot) + hasTip, ttt := tipTooltipExists(m, bot) + log.Debugf("[tip] %s has tip: %t", ttt.ID, hasTip) if hasTip { // update the tooltip with new tippers err := ttt.updateTooltip(bot, m.Sender, amount, !initializedWallet) @@ -183,6 +186,6 @@ func tipTooltipInitializedHandler(user *tb.User, bot TipBot) { // editTooltip updates the tooltip message with the new tip amount and tippers and edits it func (ttt *TipTooltip) editTooltip(bot *TipBot, notInitializedWallet bool) { tipToolTip := ttt.getUpdatedTipTooltipMessage(GetUserStrMd(bot.Telegram.Me), notInitializedWallet) - m := bot.tryEditMessage(ttt.Message.Message, tipToolTip) - ttt.Message.Message.Text = m.Text + bot.tryEditMessage(ttt.Message.Message, tipToolTip) + // ttt.Message.Message.Text = m.Text } From ee656dc5f0ec4d20a62e259d26ad2ec880bddd34 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 23 Dec 2021 11:16:26 +0100 Subject: [PATCH 097/541] Mutex fix 420 (#183) * speed up tx lock * handling Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/interceptor.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 29cb8480..8c5231a6 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -3,10 +3,12 @@ package telegram import ( "context" "fmt" + "reflect" + "strconv" + "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" @@ -38,7 +40,7 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context runtime.Unlock(strconv.FormatInt(user.ID, 10)) log.Tracef("[User mutex] Unlocked user %d", user.ID) } - return ctx, nil + return nil, invalidTypeError } // lockInterceptor invoked as first before interceptor @@ -81,6 +83,8 @@ func getTelegramUserFromInterface(i interface{}) (user *tb.User) { user = i.(*tb.Callback).Sender case *tb.Message: user = i.(*tb.Message).Sender + default: + log.Tracef("[getTelegramUserFromInterface] invalid type %s", reflect.TypeOf(i).String()) } return } From 2469898f9fdabd8e39c9985fddc10ae378738ce5 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 23 Dec 2021 15:12:49 +0100 Subject: [PATCH 098/541] Inline lock fix (#184) * speed up tx lock * fix get Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/amounts.go | 2 ++ internal/telegram/inline_faucet.go | 4 +++- internal/telegram/inline_receive.go | 3 +++ internal/telegram/inline_send.go | 1 + internal/telegram/inline_tipjar.go | 1 + internal/telegram/lnurl-pay.go | 1 + internal/telegram/lnurl-withdraw.go | 3 +++ internal/telegram/pay.go | 1 + internal/telegram/send.go | 2 ++ internal/telegram/shop_helpers.go | 4 ++-- 10 files changed, 19 insertions(+), 3 deletions(-) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 3091e3f0..610f56a9 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -162,6 +162,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { if err != nil { return } + defer transaction.Unlock(tx.ID) LnurlPayState := sn.(*LnurlPayState) LnurlPayState.Amount = amount * 1000 // mSat // add result to persistent struct @@ -182,6 +183,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { if err != nil { return } + defer transaction.Unlock(tx.ID) LnurlWithdrawState := sn.(*LnurlWithdrawState) LnurlWithdrawState.Amount = amount * 1000 // mSat // add result to persistent struct diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index cf2f8c58..e23d6cec 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -347,6 +347,8 @@ func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback log.Debugf("[cancelInlineFaucetHandler] %s", err) return } + defer transaction.Unlock(tx.ID) + inlineFaucet := fn.(*InlineFaucet) if c.Sender.ID == inlineFaucet.From.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) @@ -354,7 +356,7 @@ func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.Active = false inlineFaucet.InTransaction = false runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) + log.Debugf("[faucet] Faucet %s canceled.", inlineFaucet.ID) } - log.Debugf("[faucet] Faucet %s canceled.", inlineFaucet.ID) return } diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 55f3a4b6..62077f8e 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -149,6 +149,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac log.Errorf("[getInlineReceive] %s", err) return } + defer transaction.Unlock(tx.ID) inlineReceive := rn.(*InlineReceive) if !inlineReceive.Active { log.Errorf("[acceptInlineReceiveHandler] inline receive not active anymore") @@ -307,6 +308,7 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac log.Errorf("[getInlineReceive] %s", err) return } + defer transaction.Unlock(tx.ID) inlineReceive := rn.(*InlineReceive) from := inlineReceive.From @@ -343,6 +345,7 @@ func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callbac log.Errorf("[cancelInlineReceiveHandler] %s", err) return } + defer transaction.Unlock(tx.ID) inlineReceive := rn.(*InlineReceive) if c.Sender.ID == inlineReceive.To.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 58d1ab63..3e43a90d 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -258,6 +258,7 @@ func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) log.Errorf("[cancelInlineSendHandler] %s", err) return } + defer transaction.Unlock(tx.ID) inlineSend := sn.(*InlineSend) if c.Sender.ID == inlineSend.From.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 91bf0bec..aa3c3928 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -341,6 +341,7 @@ func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback log.Errorf("[cancelInlineTipjarHandler] %s", err) return } + defer transaction.Unlock(tx.ID) inlineTipjar := fn.(*InlineTipjar) if c.Sender.ID == inlineTipjar.To.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index 1a000623..68c132b1 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -133,6 +133,7 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } + defer transaction.Unlock(tx.ID) lnurlPayState := fn.(*LnurlPayState) // LnurlPayState loaded diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 0683e5c0..0009fabe 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -145,6 +145,7 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } + defer transaction.Unlock(tx.ID) var lnurlWithdrawState *LnurlWithdrawState switch fn.(type) { case *LnurlWithdrawState: @@ -188,6 +189,7 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { log.Errorf("[confirmWithdrawHandler] Error: %s", err.Error()) return } + var lnurlWithdrawState *LnurlWithdrawState switch fn.(type) { case *LnurlWithdrawState: @@ -313,6 +315,7 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { log.Errorf("[cancelWithdrawHandler] Error: %s", err.Error()) return } + defer transaction.Unlock(tx.ID) var lnurlWithdrawState *LnurlWithdrawState switch fn.(type) { case *LnurlWithdrawState: diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 2d6ee7d7..0c33d0b5 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -243,6 +243,7 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { log.Errorf("[cancelPaymentHandler] %s", err.Error()) return } + defer transaction.Unlock(tx.ID) payData := sn.(*PayData) // onnly the correct user can press if payData.From.Telegram.ID != c.Sender.ID { diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 4145087d..4c460b57 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -310,6 +310,8 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { log.Errorf("[acceptSendHandler] %s", err) return } + defer transaction.Unlock(tx.ID) + sendData := sn.(*SendData) // onnly the correct user can press if sendData.From.Telegram.ID != c.Sender.ID { diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index fc71d7b8..d23239f8 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -244,7 +244,7 @@ func (bot *TipBot) getUserShops(ctx context.Context, user *lnbits.User) (*Shops, log.Errorf("[getUserShops] User: %s (%d): %s", GetUserStr(user.Telegram), user.Telegram.ID, err) return &Shops{}, err } - transaction.Unlock(tx.ID) + defer transaction.Unlock(tx.ID) shops := sn.(*Shops) return shops, nil } @@ -282,7 +282,7 @@ func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) { log.Errorf("[getShop] %s", err) return &Shop{}, err } - transaction.Unlock(tx.ID) + defer transaction.Unlock(tx.ID) shop := sn.(*Shop) if shop.Owner == nil { return &Shop{}, fmt.Errorf("shop has no owner") From dd88c93474df5cc28fe01b1220a3b33f93a4fd10 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Thu, 23 Dec 2021 15:47:18 +0100 Subject: [PATCH 099/541] add mutex package (#185) * add mutex package * fix cancelSendHandler --- internal/runtime/{ => mutex}/mutex.go | 2 +- internal/storage/transaction/transaction.go | 13 ------------- internal/telegram/amounts.go | 7 +++++-- internal/telegram/inline_faucet.go | 6 +++++- internal/telegram/inline_receive.go | 18 ++++++++++++------ internal/telegram/inline_send.go | 8 ++++++-- internal/telegram/inline_tipjar.go | 6 +++++- internal/telegram/interceptor.go | 6 +++--- internal/telegram/lnurl-pay.go | 4 +++- internal/telegram/lnurl-withdraw.go | 9 +++++++-- internal/telegram/pay.go | 8 ++++++-- internal/telegram/send.go | 6 +++++- internal/telegram/shop_helpers.go | 17 ++++++++++------- 13 files changed, 68 insertions(+), 42 deletions(-) rename internal/runtime/{ => mutex}/mutex.go (96%) diff --git a/internal/runtime/mutex.go b/internal/runtime/mutex/mutex.go similarity index 96% rename from internal/runtime/mutex.go rename to internal/runtime/mutex/mutex.go index aa972c4b..f41801b6 100644 --- a/internal/runtime/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -1,4 +1,4 @@ -package runtime +package mutex import ( cmap "github.com/orcaman/concurrent-map" diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index 4ba86d32..86ea4fac 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -2,7 +2,6 @@ package transaction import ( "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime" "time" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -52,13 +51,6 @@ func (tx *Base) Lock(s storage.Storable, db *storage.DB) error { log.Debugf("[Lock] %s", tx.ID) return nil } -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 { // immediatelly set intransaction to block duplicate calls @@ -69,7 +61,6 @@ func (tx *Base) Release(s storage.Storable, db *storage.DB) error { return err } log.Debugf("[Bunt Release] %s", tx.ID) - Unlock(tx.ID) return nil } @@ -85,12 +76,10 @@ func (tx *Base) Inactivate(s storage.Storable, db *storage.DB) error { } func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error) { - Lock(tx.ID) log.Tracef("[TX mutex] Lock %s", tx.ID) err := db.Get(s) if err != nil { - Unlock(tx.ID) return s, err } // to avoid race conditions, we block the call if there is @@ -99,7 +88,6 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error for tx.InTransaction { select { case <-ticker.C: - Unlock(tx.ID) return nil, fmt.Errorf("[Bunt Lock] transaction timeout %s", tx.ID) default: time.Sleep(time.Duration(75) * time.Millisecond) @@ -107,7 +95,6 @@ func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error } } if err != nil { - Unlock(tx.ID) return nil, fmt.Errorf("could not get transaction") } diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 610f56a9..f2fb2b61 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strconv" "strings" @@ -158,11 +159,12 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { switch EnterAmountStateData.Type { case "LnurlPayState": tx := &LnurlPayState{Base: transaction.New(transaction.ID(EnterAmountStateData.ID))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return } - defer transaction.Unlock(tx.ID) LnurlPayState := sn.(*LnurlPayState) LnurlPayState.Amount = amount * 1000 // mSat // add result to persistent struct @@ -179,11 +181,12 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { return case "LnurlWithdrawState": tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(EnterAmountStateData.ID))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return } - defer transaction.Unlock(tx.ID) LnurlWithdrawState := sn.(*LnurlWithdrawState) LnurlWithdrawState.Amount = amount * 1000 // mSat // add result to persistent struct diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index e23d6cec..744d6149 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" "time" @@ -233,6 +234,8 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineFaucet{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[acceptInlineFaucetHandler] %s", err) @@ -342,12 +345,13 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { tx := &InlineFaucet{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[cancelInlineFaucetHandler] %s", err) return } - defer transaction.Unlock(tx.ID) inlineFaucet := fn.(*InlineFaucet) if c.Sender.ID == inlineFaucet.From.Telegram.ID { diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 62077f8e..fc72dc6b 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" "time" @@ -143,13 +144,14 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} - rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) + rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) return } - defer transaction.Unlock(tx.ID) inlineReceive := rn.(*InlineReceive) if !inlineReceive.Active { log.Errorf("[acceptInlineReceiveHandler] inline receive not active anymore") @@ -200,6 +202,8 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -302,13 +306,14 @@ func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} - rn, err := tx.Get(tx, bot.Bunt) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls + rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) return } - defer transaction.Unlock(tx.ID) inlineReceive := rn.(*InlineReceive) from := inlineReceive.From @@ -339,13 +344,14 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} - rn, err := tx.Get(tx, bot.Bunt) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls + rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineReceiveHandler] %s", err) return } - defer transaction.Unlock(tx.ID) inlineReceive := rn.(*InlineReceive) if c.Sender.ID == inlineReceive.To.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 3e43a90d..7d28623f 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" "time" @@ -159,6 +160,8 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineSend{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -252,13 +255,14 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) { tx := &InlineSend{Base: transaction.New(transaction.ID(c.Data))} - sn, err := tx.Get(tx, bot.Bunt) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls + sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineSendHandler] %s", err) return } - defer transaction.Unlock(tx.ID) inlineSend := sn.(*InlineSend) if c.Sender.ID == inlineSend.From.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index aa3c3928..fc59f672 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" "time" @@ -234,6 +235,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback return } tx := &InlineTipjar{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { // log.Errorf("[tipjar] %s", err) @@ -336,12 +339,13 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) { tx := &InlineTipjar{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineTipjarHandler] %s", err) return } - defer transaction.Unlock(tx.ID) inlineTipjar := fn.(*InlineTipjar) if c.Sender.ID == inlineTipjar.To.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 8c5231a6..c294243d 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -3,11 +3,11 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "reflect" "strconv" "github.com/LightningTipBot/LightningTipBot/internal/i18n" - "github.com/LightningTipBot/LightningTipBot/internal/runtime" i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -37,7 +37,7 @@ type Interceptor struct { func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { user := getTelegramUserFromInterface(i) if user != nil { - runtime.Unlock(strconv.FormatInt(user.ID, 10)) + mutex.Unlock(strconv.FormatInt(user.ID, 10)) log.Tracef("[User mutex] Unlocked user %d", user.ID) } return nil, invalidTypeError @@ -47,7 +47,7 @@ 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 { - runtime.Lock(strconv.FormatInt(user.ID, 10)) + mutex.Lock(strconv.FormatInt(user.ID, 10)) log.Tracef("[User mutex] Locked user %d", user.ID) return ctx, nil } diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index 68c132b1..a17fc376 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "io/ioutil" "net/url" "strconv" @@ -127,13 +128,14 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { // use the enter amount state of the user to load the LNURL payment state tx := &LnurlPayState{Base: transaction.New(transaction.ID(enterAmountData.ID))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } - defer transaction.Unlock(tx.ID) lnurlPayState := fn.(*LnurlPayState) // LnurlPayState loaded diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 0009fabe..4dd6d73e 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "io/ioutil" "net/url" @@ -139,13 +140,14 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // use the enter amount state of the user to load the LNURL payment state tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(enterAmountData.ID))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) return } - defer transaction.Unlock(tx.ID) var lnurlWithdrawState *LnurlWithdrawState switch fn.(type) { case *LnurlWithdrawState: @@ -184,6 +186,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[confirmWithdrawHandler] Error: %s", err.Error()) @@ -310,12 +314,13 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelWithdrawHandler] Error: %s", err.Error()) return } - defer transaction.Unlock(tx.ID) var lnurlWithdrawState *LnurlWithdrawState switch fn.(type) { case *LnurlWithdrawState: diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 0c33d0b5..e74c0ed3 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" "github.com/LightningTipBot/LightningTipBot/internal/i18n" @@ -146,6 +147,8 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { tx := &PayData{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -237,13 +240,14 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &PayData{Base: transaction.New(transaction.ID(c.Data))} - sn, err := tx.Get(tx, bot.Bunt) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls + sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelPaymentHandler] %s", err.Error()) return } - defer transaction.Unlock(tx.ID) payData := sn.(*PayData) // onnly the correct user can press if payData.From.Telegram.ID != c.Sender.ID { diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 4c460b57..b8f02f64 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" "github.com/LightningTipBot/LightningTipBot/internal/i18n" @@ -214,6 +215,8 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // sendHandler invoked when user clicked send on payment confirmation func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { tx := &SendData{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) @@ -305,12 +308,13 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &SendData{Base: transaction.New(transaction.ID(c.Data))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) return } - defer transaction.Unlock(tx.ID) sendData := sn.(*SendData) // onnly the correct user can press diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index d23239f8..7d619be2 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "time" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -158,7 +159,7 @@ func (bot *TipBot) getUserShopview(ctx context.Context, user *lnbits.User) (shop 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)) + mutex.Lock(fmt.Sprintf("shopview-delete-%d", user.Telegram.ID)) shopView, err = bot.getUserShopview(ctx, user) if err != nil { return @@ -170,7 +171,7 @@ func (bot *TipBot) shopViewDeleteAllStatusMsgs(ctx context.Context, user *lnbits 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)) + mutex.Unlock(fmt.Sprintf("shopview-delete-%d", user.Telegram.ID)) return } @@ -188,7 +189,7 @@ func (bot *TipBot) sendStatusMessage(ctx context.Context, to tb.Recipient, what id := fmt.Sprintf("shopview-delete-%d", user.Telegram.ID) // write into cache - runtime.Lock(id) + mutex.Lock(id) shopView, err := bot.getUserShopview(ctx, user) if err != nil { return nil @@ -196,7 +197,7 @@ func (bot *TipBot) sendStatusMessage(ctx context.Context, to tb.Recipient, what 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) + mutex.Unlock(id) return statusMsg } @@ -239,12 +240,13 @@ func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops // 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)))} + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.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 } - defer transaction.Unlock(tx.ID) shops := sn.(*Shops) return shops, nil } @@ -276,13 +278,14 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e // 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) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls + sn, err := tx.Get(tx, bot.ShopBunt) if err != nil { log.Errorf("[getShop] %s", err) return &Shop{}, err } - defer transaction.Unlock(tx.ID) shop := sn.(*Shop) if shop.Owner == nil { return &Shop{}, fmt.Errorf("shop has no owner") From fa234d4073b3f6a5b5b41c774d89a0342b536395 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Thu, 23 Dec 2021 15:50:41 +0100 Subject: [PATCH 100/541] remove user mutex log message (#186) --- internal/telegram/interceptor.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index c294243d..968eb22b 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -38,7 +38,6 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context user := getTelegramUserFromInterface(i) if user != nil { mutex.Unlock(strconv.FormatInt(user.ID, 10)) - log.Tracef("[User mutex] Unlocked user %d", user.ID) } return nil, invalidTypeError } @@ -48,7 +47,6 @@ func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.C user := getTelegramUserFromInterface(i) if user != nil { mutex.Lock(strconv.FormatInt(user.ID, 10)) - log.Tracef("[User mutex] Locked user %d", user.ID) return ctx, nil } return nil, invalidTypeError From bc0d3df897944c4fa1e3c5e9678d6782702f9662 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Thu, 23 Dec 2021 15:57:48 +0100 Subject: [PATCH 101/541] Fix log (#187) * remove user mutex log message * remove log * fix bunt locks log messages --- internal/storage/transaction/transaction.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go index 86ea4fac..45c5988f 100644 --- a/internal/storage/transaction/transaction.go +++ b/internal/storage/transaction/transaction.go @@ -48,7 +48,7 @@ func (tx *Base) Lock(s storage.Storable, db *storage.DB) error { log.Debugf("[Bunt Lock] %s Error: %s", tx.ID, err.Error()) return err } - log.Debugf("[Lock] %s", tx.ID) + log.Tracef("[Bunt Lock] %s", tx.ID) return nil } @@ -57,10 +57,10 @@ func (tx *Base) Release(s storage.Storable, db *storage.DB) error { tx.InTransaction = false err := tx.Set(s, db) if err != nil { - log.Debugf("[Bunt Release] %s Error: %s", tx.ID, err.Error()) + log.Tracef("[Bunt Release] %s Error: %s", tx.ID, err.Error()) return err } - log.Debugf("[Bunt Release] %s", tx.ID) + log.Tracef("[Bunt Release] %s", tx.ID) return nil } @@ -68,16 +68,14 @@ func (tx *Base) Inactivate(s storage.Storable, db *storage.DB) error { tx.Active = false err := tx.Set(s, db) if err != nil { - log.Debugf("[Bunt Inactivate] %s Error: %s", tx.ID, err.Error()) + log.Tracef("[Bunt Inactivate] %s Error: %s", tx.ID, err.Error()) return err } - log.Debugf("[Bunt Inactivate] %s", tx.ID) + log.Tracef("[Bunt Inactivate] %s", tx.ID) return nil } func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error) { - log.Tracef("[TX mutex] Lock %s", tx.ID) - err := db.Get(s) if err != nil { return s, err From a644c465cd80d76878fab20a89bfa2737358e23c Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 23 Dec 2021 16:23:57 +0100 Subject: [PATCH 102/541] Disable bunt lock (#188) * speed up tx lock * transaction to storage * to base Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/storage/base.go | 72 +++++++++++++ internal/storage/transaction/transaction.go | 110 -------------------- internal/telegram/amounts.go | 9 +- internal/telegram/inline_faucet.go | 21 ++-- internal/telegram/inline_receive.go | 25 ++--- internal/telegram/inline_send.go | 22 ++-- internal/telegram/inline_tipjar.go | 21 ++-- internal/telegram/lnurl-pay.go | 12 ++- internal/telegram/lnurl-withdraw.go | 27 ++--- internal/telegram/pay.go | 26 ++--- internal/telegram/send.go | 24 ++--- internal/telegram/shop.go | 6 +- internal/telegram/shop_helpers.go | 14 +-- 13 files changed, 156 insertions(+), 233 deletions(-) create mode 100644 internal/storage/base.go delete mode 100644 internal/storage/transaction/transaction.go diff --git a/internal/storage/base.go b/internal/storage/base.go new file mode 100644 index 00000000..8f333cdf --- /dev/null +++ b/internal/storage/base.go @@ -0,0 +1,72 @@ +package storage + +import ( + "fmt" + "time" + + log "github.com/sirupsen/logrus" +) + +type Base struct { + ID string `json:"id"` + Active bool `json:"active"` + CreatedAt time.Time `json:"created"` + UpdatedAt time.Time `json:"updated"` +} + +type Option func(b *Base) + +func ID(id string) Option { + return func(btx *Base) { + btx.ID = id + } +} + +func New(opts ...Option) *Base { + btx := &Base{ + Active: true, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + } + for _, opt := range opts { + opt(btx) + } + return btx +} + +func (tx Base) Key() string { + return tx.ID +} + +func (tx *Base) Inactivate(s Storable, db *DB) error { + tx.Active = false + err := tx.Set(s, db) + if err != nil { + log.Tracef("[Bunt Inactivate] %s Error: %s", tx.ID, err.Error()) + return err + } + log.Tracef("[Bunt Inactivate] %s", tx.ID) + return nil +} + +func (tx *Base) Get(s Storable, db *DB) (Storable, error) { + err := db.Get(s) + if err != nil { + return s, err + } + if err != nil { + return nil, fmt.Errorf("could not get transaction") + } + + return s, nil +} + +func (tx *Base) Set(s Storable, db *DB) error { + tx.UpdatedAt = time.Now() + return db.Set(s) +} + +func (tx *Base) Delete(s Storable, db *DB) error { + tx.UpdatedAt = time.Now() + return db.Delete(s.Key(), s) +} diff --git a/internal/storage/transaction/transaction.go b/internal/storage/transaction/transaction.go deleted file mode 100644 index 45c5988f..00000000 --- a/internal/storage/transaction/transaction.go +++ /dev/null @@ -1,110 +0,0 @@ -package transaction - -import ( - "fmt" - "time" - - "github.com/LightningTipBot/LightningTipBot/internal/storage" - log "github.com/sirupsen/logrus" -) - -type Base struct { - ID string `json:"id"` - Active bool `json:"active"` - InTransaction bool `json:"intransaction"` - CreatedAt time.Time `json:"created"` - UpdatedAt time.Time `json:"updated"` -} - -type Option func(b *Base) - -func ID(id string) Option { - return func(btx *Base) { - btx.ID = id - } -} - -func New(opts ...Option) *Base { - btx := &Base{ - Active: true, - InTransaction: false, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - } - for _, opt := range opts { - opt(btx) - } - return btx -} - -func (tx Base) Key() string { - return tx.ID -} -func (tx *Base) Lock(s storage.Storable, db *storage.DB) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = true - err := tx.Set(s, db) - if err != nil { - log.Debugf("[Bunt Lock] %s Error: %s", tx.ID, err.Error()) - return err - } - log.Tracef("[Bunt Lock] %s", tx.ID) - return nil -} - -func (tx *Base) Release(s storage.Storable, db *storage.DB) error { - // immediatelly set intransaction to block duplicate calls - tx.InTransaction = false - err := tx.Set(s, db) - if err != nil { - log.Tracef("[Bunt Release] %s Error: %s", tx.ID, err.Error()) - return err - } - log.Tracef("[Bunt Release] %s", tx.ID) - return nil -} - -func (tx *Base) Inactivate(s storage.Storable, db *storage.DB) error { - tx.Active = false - err := tx.Set(s, db) - if err != nil { - log.Tracef("[Bunt Inactivate] %s Error: %s", tx.ID, err.Error()) - return err - } - log.Tracef("[Bunt Inactivate] %s", tx.ID) - return nil -} - -func (tx *Base) Get(s storage.Storable, db *storage.DB) (storage.Storable, error) { - err := db.Get(s) - if err != nil { - return s, err - } - // to avoid race conditions, we block the call if there is - // already an active transaction by loop until InTransaction is false - ticker := time.NewTicker(time.Millisecond * 100) - for tx.InTransaction { - select { - case <-ticker.C: - return nil, fmt.Errorf("[Bunt Lock] transaction timeout %s", tx.ID) - default: - time.Sleep(time.Duration(75) * time.Millisecond) - err = db.Get(s) - } - } - if err != nil { - return nil, fmt.Errorf("could not get transaction") - } - - return s, nil -} - -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) -} diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index f2fb2b61..de607bf5 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -5,14 +5,15 @@ import ( "encoding/json" "errors" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strconv" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/price" "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" ) @@ -158,7 +159,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { // we stored this in the EnterAmountStateData before switch EnterAmountStateData.Type { case "LnurlPayState": - tx := &LnurlPayState{Base: transaction.New(transaction.ID(EnterAmountStateData.ID))} + tx := &LnurlPayState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) @@ -180,7 +181,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { bot.lnurlPayHandlerSend(ctx, m) return case "LnurlWithdrawState": - tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(EnterAmountStateData.ID))} + tx := &LnurlWithdrawState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 744d6149..b75fbbf6 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -3,17 +3,18 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/eko/gocache/store" "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" @@ -27,7 +28,7 @@ var ( ) type InlineFaucet struct { - *transaction.Base + *storage.Base Message string `json:"inline_faucet_message"` Amount int64 `json:"inline_faucet_amount"` RemainingAmount int64 `json:"inline_faucet_remainingamount"` @@ -86,7 +87,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User id := fmt.Sprintf("inl-faucet-%d-%d-%s", sender.ID, amount, RandStringRunes(5)) return &InlineFaucet{ - Base: transaction.New(transaction.ID(id)), + Base: storage.New(storage.ID(id)), Message: inlineMessage, Amount: amount, From: fromUser, @@ -233,7 +234,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) - tx := &InlineFaucet{Base: transaction.New(transaction.ID(c.Data))} + tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) @@ -243,12 +244,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } inlineFaucet := fn.(*InlineFaucet) from := inlineFaucet.From - err = inlineFaucet.Lock(inlineFaucet, bot.Bunt) - if err != nil { - log.Errorf("[faucet] LockFaucet %s error: %s", inlineFaucet.ID, err) - return - } - defer inlineFaucet.Release(inlineFaucet, bot.Bunt) + defer inlineFaucet.Set(inlineFaucet, bot.Bunt) if !inlineFaucet.Active { log.Errorf(fmt.Sprintf("[faucet] faucet %s inactive.", inlineFaucet.ID)) return @@ -344,7 +340,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { - tx := &InlineFaucet{Base: transaction.New(transaction.ID(c.Data))} + tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) @@ -358,7 +354,6 @@ func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback bot.tryEditMessage(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false - inlineFaucet.InTransaction = false runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) log.Debugf("[faucet] Faucet %s canceled.", inlineFaucet.ID) } diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index fc72dc6b..900e79f5 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -4,16 +4,17 @@ import ( "bytes" "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/eko/gocache/store" "github.com/skip2/go-qrcode" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" @@ -27,7 +28,7 @@ var ( ) type InlineReceive struct { - *transaction.Base + *storage.Base MessageText string `json:"inline_receive_messagetext"` Message *tb.Message `json:"inline_receive_message"` Amount int64 `json:"inline_receive_amount"` @@ -120,7 +121,7 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { results[i].SetResultID(id) // create persistend inline send struct inlineReceive := InlineReceive{ - Base: transaction.New(transaction.ID(id)), + Base: storage.New(storage.ID(id)), MessageText: inlineMessage, To: to, Memo: memo, @@ -143,7 +144,7 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { } func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) { - tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} + tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) @@ -201,7 +202,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac } func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) { - tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} + tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) rn, err := tx.Get(tx, bot.Bunt) @@ -211,11 +212,6 @@ func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) return } inlineReceive := rn.(*InlineReceive) - err = inlineReceive.Lock(inlineReceive, bot.Bunt) - if err != nil { - log.Errorf("[acceptInlineReceiveHandler] %s", err) - return - } if !inlineReceive.Active { log.Errorf("[acceptInlineReceiveHandler] inline receive not active anymore") @@ -259,7 +255,7 @@ func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) } log.Infof("[💸 inlineReceive] Send from %s to %s (%d sat).", fromUserStr, toUserStr, inlineReceive.Amount) - inlineReceive.Release(inlineReceive, bot.Bunt) + inlineReceive.Set(inlineReceive, bot.Bunt) bot.finishInlineReceiveHandler(ctx, c) } @@ -305,7 +301,7 @@ func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { } func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) { - tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} + tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls @@ -343,7 +339,7 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac } func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { - tx := &InlineReceive{Base: transaction.New(transaction.ID(c.Data))} + tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls @@ -357,7 +353,6 @@ func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callbac bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineReceive inactive inlineReceive.Active = false - inlineReceive.InTransaction = false runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) } return diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 7d28623f..5c839cc3 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -3,16 +3,17 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/eko/gocache/store" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" @@ -26,7 +27,7 @@ var ( ) type InlineSend struct { - *transaction.Base + *storage.Base Message string `json:"inline_send_message"` Amount int64 `json:"inline_send_amount"` From *lnbits.User `json:"inline_send_from"` @@ -133,7 +134,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { // add data to persistent object inlineSend := InlineSend{ - Base: transaction.New(transaction.ID(id)), + Base: storage.New(storage.ID(id)), Message: inlineMessage, From: fromUser, To: toUserDb, @@ -159,7 +160,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) - tx := &InlineSend{Base: transaction.New(transaction.ID(c.Data))} + tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) @@ -171,18 +172,12 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) inlineSend := sn.(*InlineSend) fromUser := inlineSend.From - // immediatelly set intransaction to block duplicate calls - err = inlineSend.Lock(inlineSend, bot.Bunt) - if err != nil { - log.Errorf("[getInlineSend] %s", err) - return - } if !inlineSend.Active { log.Errorf("[acceptInlineSendHandler] inline send not active anymore") return } - defer inlineSend.Release(inlineSend, bot.Bunt) + defer inlineSend.Set(inlineSend, bot.Bunt) amount := inlineSend.Amount @@ -254,7 +249,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) } func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) { - tx := &InlineSend{Base: transaction.New(transaction.ID(c.Data))} + tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls @@ -268,7 +263,6 @@ func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineSend inactive inlineSend.Active = false - inlineSend.InTransaction = false runtime.IgnoreError(inlineSend.Set(inlineSend, bot.Bunt)) } return diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index fc59f672..3f340b10 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -3,16 +3,17 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/eko/gocache/store" "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" @@ -26,7 +27,7 @@ var ( ) type InlineTipjar struct { - *transaction.Base + *storage.Base Message string `json:"inline_tipjar_message"` Amount int64 `json:"inline_tipjar_amount"` GivenAmount int64 `json:"inline_tipjar_givenamount"` @@ -84,7 +85,7 @@ func (bot TipBot) createTipjar(ctx context.Context, text string, sender *tb.User id := fmt.Sprintf("inl-tipjar-%d-%d-%s", sender.ID, amount, RandStringRunes(5)) return &InlineTipjar{ - Base: transaction.New(transaction.ID(id)), + Base: storage.New(storage.ID(id)), Message: inlineMessage, Amount: amount, To: toUser, @@ -234,7 +235,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback if from.Wallet == nil { return } - tx := &InlineTipjar{Base: transaction.New(transaction.ID(c.Data))} + tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) @@ -244,17 +245,10 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback } inlineTipjar := fn.(*InlineTipjar) to := inlineTipjar.To - err = inlineTipjar.Lock(inlineTipjar, bot.Bunt) - if err != nil { - log.Errorf("[tipjar] LockTipjar %s error: %s", inlineTipjar.ID, err) - return - } if !inlineTipjar.Active { log.Errorf(fmt.Sprintf("[tipjar] tipjar %s inactive.", inlineTipjar.ID)) return } - // release tipjar no matter what - defer inlineTipjar.Release(inlineTipjar, bot.Bunt) if from.Telegram.ID == to.Telegram.ID { bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) @@ -338,7 +332,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback } func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) { - tx := &InlineTipjar{Base: transaction.New(transaction.ID(c.Data))} + tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) @@ -351,7 +345,6 @@ func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback bot.tryEditMessage(c.Message, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineTipjar inactive inlineTipjar.Active = false - inlineTipjar.InTransaction = false runtime.IgnoreError(inlineTipjar.Set(inlineTipjar, bot.Bunt)) } return diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index a17fc376..4b63b30b 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -4,15 +4,17 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "io/ioutil" "net/url" "strconv" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" @@ -20,7 +22,7 @@ import ( // LnurlPayState saves the state of the user for an LNURL payment type LnurlPayState struct { - *transaction.Base + *storage.Base From *lnbits.User `json:"from"` LNURLPayParams lnurl.LNURLPayParams `json:"LNURLPayParams"` LNURLPayValues lnurl.LNURLPayValues `json:"LNURLPayValues"` @@ -39,7 +41,7 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams // object that holds all information about the send payment id := fmt.Sprintf("lnurlp-%d-%s", m.Sender.ID, RandStringRunes(5)) lnurlPayState := LnurlPayState{ - Base: transaction.New(transaction.ID(id)), + Base: storage.New(storage.ID(id)), From: user, LNURLPayParams: payParams.LNURLPayParams, LanguageCode: ctx.Value("publicLanguageCode").(string), @@ -127,7 +129,7 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { } // use the enter amount state of the user to load the LNURL payment state - tx := &LnurlPayState{Base: transaction.New(transaction.ID(enterAmountData.ID))} + tx := &LnurlPayState{Base: storage.New(storage.ID(enterAmountData.ID))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 4dd6d73e..21cb70dd 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -4,15 +4,17 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "io/ioutil" "net/url" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + "github.com/LightningTipBot/LightningTipBot/internal/str" lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" @@ -27,7 +29,7 @@ var ( // LnurlWithdrawState saves the state of the user for an LNURL payment type LnurlWithdrawState struct { - *transaction.Base + *storage.Base From *lnbits.User `json:"from"` LNURLWithdrawResponse lnurl.LNURLWithdrawResponse `json:"LNURLWithdrawResponse"` LNURResponse lnurl.LNURLResponse `json:"LNURLResponse"` @@ -61,7 +63,7 @@ func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, with // object that holds all information about the send payment id := fmt.Sprintf("lnurlw-%d-%s", m.Sender.ID, RandStringRunes(5)) LnurlWithdrawState := LnurlWithdrawState{ - Base: transaction.New(transaction.ID(id)), + Base: storage.New(storage.ID(id)), From: user, LNURLWithdrawResponse: withdrawParams.LNURLWithdrawResponse, LanguageCode: ctx.Value("publicLanguageCode").(string), @@ -139,7 +141,7 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa } // use the enter amount state of the user to load the LNURL payment state - tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(enterAmountData.ID))} + tx := &LnurlWithdrawState{Base: storage.New(storage.ID(enterAmountData.ID))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) @@ -185,7 +187,7 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { - tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(c.Data))} + tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) @@ -206,21 +208,13 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { if lnurlWithdrawState.From.Telegram.ID != c.Sender.ID { return } - // immediatelly set intransaction to block duplicate calls - err = lnurlWithdrawState.Lock(lnurlWithdrawState, bot.Bunt) - if err != nil { - log.Errorf("[confirmWithdrawHandler] %s", err) - bot.tryDeleteMessage(c.Message) - bot.tryEditMessage(c.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) - return - } if !lnurlWithdrawState.Active { log.Errorf("[confirmPayHandler] send not active anymore") bot.tryEditMessage(c.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) bot.tryDeleteMessage(c.Message) return } - defer lnurlWithdrawState.Release(lnurlWithdrawState, bot.Bunt) + defer lnurlWithdrawState.Set(lnurlWithdrawState, bot.Bunt) user := LoadUser(ctx) if user.Wallet == nil { @@ -313,7 +307,7 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { // reset state immediately user := LoadUser(ctx) ResetUserState(user, bot) - tx := &LnurlWithdrawState{Base: transaction.New(transaction.ID(c.Data))} + tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) @@ -333,6 +327,5 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { return } bot.tryEditMessage(c.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawCancelled"), &tb.ReplyMarkup{}) - lnurlWithdrawState.InTransaction = false lnurlWithdrawState.Inactivate(lnurlWithdrawState, bot.Bunt) } diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index e74c0ed3..968228eb 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -3,13 +3,15 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + "github.com/LightningTipBot/LightningTipBot/internal/str" decodepay "github.com/fiatjaf/ln-decodepay" log "github.com/sirupsen/logrus" @@ -31,7 +33,7 @@ func helpPayInvoiceUsage(ctx context.Context, errormsg string) string { } type PayData struct { - *transaction.Base + *storage.Base From *lnbits.User `json:"from"` Invoice string `json:"invoice"` Hash string `json:"hash"` @@ -129,7 +131,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { ) payMessage := bot.trySendMessage(m.Chat, confirmText, paymentConfirmationMenu) payData := PayData{ - Base: transaction.New(transaction.ID(id)), + Base: storage.New(storage.ID(id)), From: user, Invoice: paymentRequest, Amount: int64(amount), @@ -146,7 +148,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { - tx := &PayData{Base: transaction.New(transaction.ID(c.Data))} + tx := &PayData{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) @@ -161,21 +163,13 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { if payData.From.Telegram.ID != c.Sender.ID { return } - // immediatelly set intransaction to block duplicate calls - err = payData.Lock(payData, bot.Bunt) - if err != nil { - log.Errorf("[acceptSendHandler] %s", err) - bot.tryDeleteMessage(c.Message) - bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) - return - } if !payData.Active { log.Errorf("[confirmPayHandler] send not active anymore") bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) bot.tryDeleteMessage(c.Message) return } - defer payData.Release(payData, bot.Bunt) + defer payData.Set(payData, bot.Bunt) // remove buttons from confirmation message // bot.tryEditMessage(c.Message, MarkdownEscape(payData.Message), &tb.ReplyMarkup{}) @@ -220,7 +214,6 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { return } payData.Hash = invoice.PaymentHash - payData.InTransaction = false if c.Message.Private() { // if the command was invoked in private chat @@ -239,7 +232,7 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { // reset state immediately user := LoadUser(ctx) ResetUserState(user, bot) - tx := &PayData{Base: transaction.New(transaction.ID(c.Data))} + tx := &PayData{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls @@ -254,6 +247,5 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { return } bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) - payData.InTransaction = false payData.Inactivate(payData, bot.Bunt) } diff --git a/internal/telegram/send.go b/internal/telegram/send.go index b8f02f64..4007e3ed 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -4,13 +4,15 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" @@ -40,7 +42,7 @@ func (bot *TipBot) SendCheckSyntax(ctx context.Context, m *tb.Message) (bool, st } type SendData struct { - *transaction.Base + *storage.Base From *lnbits.User `json:"from"` ToTelegramId int64 `json:"to_telegram_id"` ToTelegramUser string `json:"to_telegram_user"` @@ -174,7 +176,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { id := fmt.Sprintf("send-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) sendData := SendData{ From: user, - Base: transaction.New(transaction.ID(id)), + Base: storage.New(storage.ID(id)), Amount: int64(amount), ToTelegramId: toUserDb.Telegram.ID, ToTelegramUser: toUserStrWithoutAt, @@ -214,7 +216,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // sendHandler invoked when user clicked send on payment confirmation func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { - tx := &SendData{Base: transaction.New(transaction.ID(c.Data))} + tx := &SendData{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) @@ -227,19 +229,12 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { if sendData.From.Telegram.ID != c.Sender.ID { return } - // immediatelly set intransaction to block duplicate calls - err = sendData.Lock(sendData, bot.Bunt) - if err != nil { - log.Errorf("[acceptSendHandler] %s", err) - bot.tryDeleteMessage(c.Message) - return - } if !sendData.Active { log.Errorf("[acceptSendHandler] send not active anymore") // bot.tryDeleteMessage(c.Message) return } - defer sendData.Release(sendData, bot.Bunt) + defer sendData.Set(sendData, bot.Bunt) // // remove buttons from confirmation message // bot.tryEditMessage(c.Message, MarkdownEscape(sendData.Message), &tb.ReplyMarkup{}) @@ -307,7 +302,7 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { // reset state immediately user := LoadUser(ctx) ResetUserState(user, bot) - tx := &SendData{Base: transaction.New(transaction.ID(c.Data))} + tx := &SendData{Base: storage.New(storage.ID(c.Data))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) @@ -323,6 +318,5 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { } // remove buttons from confirmation message bot.tryEditMessage(c.Message, i18n.Translate(sendData.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) - sendData.InTransaction = false sendData.Inactivate(sendData, bot.Bunt) } diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index b21a072c..b235564f 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -10,7 +10,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/storage/transaction" + "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" @@ -43,7 +43,7 @@ type ShopItem struct { } type Shop struct { - *transaction.Base + *storage.Base ID string `json:"ID"` // holds the ID of the tx object in bunt db Owner *lnbits.User `json:"owner"` // owner of the shop Type string `json:"Type"` // type of the shop @@ -57,7 +57,7 @@ type Shop struct { } type Shops struct { - *transaction.Base + *storage.Base ID string `json:"ID"` // holds the ID of the tx object in bunt db Owner *lnbits.User `json:"owner"` // owner of the shop Shops []string `json:"shop"` // diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index 7d619be2..55a604e5 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -3,12 +3,14 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "time" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "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" @@ -227,7 +229,7 @@ func (bot *TipBot) sendStatusMessageAndDelete(ctx context.Context, to tb.Recipie 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)), + Base: storage.New(storage.ID(id)), ID: id, Owner: user, Shops: []string{}, @@ -239,7 +241,7 @@ func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops // 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)))} + tx := &Shops{Base: storage.New(storage.ID(fmt.Sprintf("shops-%d", user.Telegram.ID)))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.ShopBunt) @@ -259,7 +261,7 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e } shopId := fmt.Sprintf("shop-%s", RandStringRunes(10)) shop := &Shop{ - Base: transaction.New(transaction.ID(shopId)), + Base: storage.New(storage.ID(shopId)), ID: shopId, Title: fmt.Sprintf("Shop %d (%s)", len(shops.Shops)+1, shopId), Owner: user, @@ -277,7 +279,7 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e // 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))} + tx := &Shop{Base: storage.New(storage.ID(shopId))} mutex.Lock(tx.ID) defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls From 72381d007142139586bb335043957d2d7b93e8db Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Thu, 23 Dec 2021 16:52:24 +0100 Subject: [PATCH 103/541] Bunt cache (#189) * remove user mutex log message * add bunt cache --- internal/storage/base.go | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/internal/storage/base.go b/internal/storage/base.go index 8f333cdf..5497f0da 100644 --- a/internal/storage/base.go +++ b/internal/storage/base.go @@ -1,12 +1,15 @@ package storage import ( - "fmt" + "github.com/eko/gocache/store" + gocache "github.com/patrickmn/go-cache" "time" log "github.com/sirupsen/logrus" ) +var transactionCache = store.NewGoCache(gocache.New(5*time.Minute, 10*time.Minute), nil) + type Base struct { ID string `json:"id"` Active bool `json:"active"` @@ -50,20 +53,35 @@ func (tx *Base) Inactivate(s Storable, db *DB) error { } func (tx *Base) Get(s Storable, db *DB) (Storable, error) { - err := db.Get(s) - if err != nil { - return s, err - } + cacheTx, err := transactionCache.Get(s.Key()) if err != nil { - return nil, fmt.Errorf("could not get transaction") + log.Errorf("[Bunt Cache] could not get bunt object: %v", err) + err := db.Get(s) + if err != nil { + return s, err + } + log.Tracef("[Bunt] get object %s", s.Key()) + return s, transactionCache.Set(s.Key(), s, &store.Options{Expiration: 5 * time.Minute}) } + log.Tracef("[Bunt Cache] get object %s", s.Key()) + return cacheTx.(Storable), err - return s, nil } func (tx *Base) Set(s Storable, db *DB) error { tx.UpdatedAt = time.Now() - return db.Set(s) + err := db.Set(s) + if err != nil { + log.Errorf("[Bunt] could not set object: %v", err) + return err + } + log.Tracef("[Bunt] set object %s", s.Key()) + err = transactionCache.Set(s.Key(), s, &store.Options{Expiration: 5 * time.Minute}) + if err != nil { + log.Errorf("[Bunt Cache] could not set object: %v", err) + } + log.Tracef("[Bunt Cache] set object: %s", s.Key()) + return err } func (tx *Base) Delete(s Storable, db *DB) error { From d9125413914e88f21b4e1668c896c15e842f1d61 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 23 Dec 2021 16:55:39 +0100 Subject: [PATCH 104/541] Faucet set after validation (#190) * speed up tx lock * faucet set after validation Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index b75fbbf6..85524c36 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -244,7 +244,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } inlineFaucet := fn.(*InlineFaucet) from := inlineFaucet.From - defer inlineFaucet.Set(inlineFaucet, bot.Bunt) + if !inlineFaucet.Active { log.Errorf(fmt.Sprintf("[faucet] faucet %s inactive.", inlineFaucet.ID)) return @@ -264,6 +264,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } } + defer inlineFaucet.Set(inlineFaucet, bot.Bunt) + if inlineFaucet.RemainingAmount >= inlineFaucet.PerUserAmount { toUserStrMd := GetUserStrMd(to.Telegram) fromUserStrMd := GetUserStrMd(from.Telegram) From 1b24072020a380f79b8fe3910ca12c41b2378a12 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Thu, 23 Dec 2021 17:07:41 +0100 Subject: [PATCH 105/541] Interface conversion (#191) * remove user mutex log message * add bunt cache * fix send and pay data pointer conversion --- internal/telegram/pay.go | 2 +- internal/telegram/send.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 968228eb..357d988f 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -130,7 +130,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { cancelButton), ) payMessage := bot.trySendMessage(m.Chat, confirmText, paymentConfirmationMenu) - payData := PayData{ + payData := &PayData{ Base: storage.New(storage.ID(id)), From: user, Invoice: paymentRequest, diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 4007e3ed..1367e5aa 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -174,7 +174,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { } // object that holds all information about the send payment id := fmt.Sprintf("send-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) - sendData := SendData{ + sendData := &SendData{ From: user, Base: storage.New(storage.ID(id)), Amount: int64(amount), From 657fc7af1dac270f61ebcf686203584d09ffc6fc Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Thu, 23 Dec 2021 17:18:04 +0100 Subject: [PATCH 106/541] fix mutex (#192) --- internal/telegram/inline_receive.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 900e79f5..10c28a9b 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -203,8 +203,6 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -302,8 +300,6 @@ func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls rn, err := tx.Get(tx, bot.Bunt) if err != nil { From a9f2728c931e7f1a1898a00360eb9f5bf8723285 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:06:15 +0100 Subject: [PATCH 107/541] add Lock and Unlock soft (#193) --- internal/runtime/mutex/mutex.go | 43 +++++++++++++++++++++++++++++ internal/telegram/amounts.go | 8 +++--- internal/telegram/inline_faucet.go | 8 +++--- internal/telegram/inline_receive.go | 12 +++++--- internal/telegram/inline_send.go | 8 +++--- internal/telegram/inline_tipjar.go | 8 +++--- internal/telegram/interceptor.go | 2 +- internal/telegram/lnurl-pay.go | 4 +-- internal/telegram/lnurl-withdraw.go | 12 ++++---- internal/telegram/pay.go | 8 +++--- internal/telegram/send.go | 8 +++--- internal/telegram/shop_helpers.go | 8 +++--- 12 files changed, 88 insertions(+), 41 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index f41801b6..0dc37d7b 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -1,6 +1,7 @@ package mutex import ( + "fmt" cmap "github.com/orcaman/concurrent-map" log "github.com/sirupsen/logrus" "sync" @@ -12,6 +13,46 @@ func init() { mutexMap = cmap.New() } +// checkSoftLock checks in mutexMap how often an existing mutex was already SoftLocked. +// The counter is there to avoid multiple recursive locking of an object in the mutexMap. +// This happens if multiple handlers call each other and try to lock/unlock multiple times +// the same mutex. +func checkSoftLock(s string) int { + if v, ok := mutexMap.Get(fmt.Sprintf("nLocks:%s", s)); ok { + return v.(int) + } + return 0 +} + +// LockSoft locks a mutex only if it hasn't been locked before. If it has, it increments the +// nLocks in the mutexMap. This is supposed to lock only if nLock == 0. +func LockSoft(s string) { + var nLocks = checkSoftLock(s) + if nLocks == 0 { + Lock(s) + } else { + log.Tracef("[Mutex] skipping LockSoft with nLocks: %d ", nLocks) + } + nLocks++ + mutexMap.Set(fmt.Sprintf("nLocks:%s", s), nLocks) + +} + +// UnlockSoft unlock a mutex only if it has been locked once. If it has been locked more than once +// it only decrements nLocks and skips the unlock of the mutex. This is supposed to unlock only for +// nLocks == 1 +func UnlockSoft(s string) { + var nLocks = checkSoftLock(s) + if nLocks == 1 { + Unlock(s) + } else { + log.Tracef("[Mutex] skipping UnlockSoft with nLocks: %d ", nLocks) + } + nLocks-- + mutexMap.Set(fmt.Sprintf("state:%s", s), nLocks) +} + +// Lock locks a mutex in the mutexMap. func Lock(s string) { if m, ok := mutexMap.Get(s); ok { m.(*sync.Mutex).Lock() @@ -23,9 +64,11 @@ func Lock(s string) { log.Tracef("[Mutex] Lock %s", s) } +// Unlock unlocks a mutex in the mutexMap. func Unlock(s string) { if m, ok := mutexMap.Get(s); ok { log.Tracef("[Mutex] Unlock %s", s) m.(*sync.Mutex).Unlock() + } } diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index de607bf5..bc550b62 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -160,8 +160,8 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { switch EnterAmountStateData.Type { case "LnurlPayState": tx := &LnurlPayState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return @@ -182,8 +182,8 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { return case "LnurlWithdrawState": tx := &LnurlWithdrawState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 85524c36..49b1af68 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -235,8 +235,8 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[acceptInlineFaucetHandler] %s", err) @@ -343,8 +343,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[cancelInlineFaucetHandler] %s", err) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 10c28a9b..580f71c2 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -146,8 +146,8 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) @@ -203,6 +203,8 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -301,6 +303,8 @@ func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) @@ -336,8 +340,8 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) // immediatelly set intransaction to block duplicate calls rn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 5c839cc3..3846916e 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -161,8 +161,8 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -250,8 +250,8 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) { tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 3f340b10..49b9256e 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -236,8 +236,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback return } tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { // log.Errorf("[tipjar] %s", err) @@ -333,8 +333,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) { tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineTipjarHandler] %s", err) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 968eb22b..78cd8501 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -39,7 +39,7 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context if user != nil { mutex.Unlock(strconv.FormatInt(user.ID, 10)) } - return nil, invalidTypeError + return ctx, invalidTypeError } // lockInterceptor invoked as first before interceptor diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index 4b63b30b..aed3a44b 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -130,8 +130,8 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { // use the enter amount state of the user to load the LNURL payment state tx := &LnurlPayState{Base: storage.New(storage.ID(enterAmountData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 21cb70dd..f42ca289 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -142,8 +142,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // use the enter amount state of the user to load the LNURL payment state tx := &LnurlWithdrawState{Base: storage.New(storage.ID(enterAmountData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) @@ -188,8 +188,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[confirmWithdrawHandler] Error: %s", err.Error()) @@ -308,8 +308,8 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelWithdrawHandler] Error: %s", err.Error()) diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 357d988f..c28ad2f9 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -149,8 +149,8 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { tx := &PayData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -233,8 +233,8 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &PayData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 1367e5aa..2b8dc966 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -217,8 +217,8 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // sendHandler invoked when user clicked send on payment confirmation func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { tx := &SendData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) @@ -303,8 +303,8 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &SendData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index 55a604e5..bd992c64 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -242,8 +242,8 @@ func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops // getUserShops returns the Shops for the user func (bot *TipBot) getUserShops(ctx context.Context, user *lnbits.User) (*Shops, error) { tx := &Shops{Base: storage.New(storage.ID(fmt.Sprintf("shops-%d", user.Telegram.ID)))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.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) @@ -280,8 +280,8 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e // getShop returns the Shop for the given ID func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) { tx := &Shop{Base: storage.New(storage.ID(shopId))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.ShopBunt) if err != nil { From e53ef943062c1834d9ee1e30d43cd220a263e871 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:08:06 +0100 Subject: [PATCH 108/541] Revert "add Lock and Unlock soft (#193)" (#194) This reverts commit a9f2728c931e7f1a1898a00360eb9f5bf8723285. --- internal/runtime/mutex/mutex.go | 43 ----------------------------- internal/telegram/amounts.go | 8 +++--- internal/telegram/inline_faucet.go | 8 +++--- internal/telegram/inline_receive.go | 12 +++----- internal/telegram/inline_send.go | 8 +++--- internal/telegram/inline_tipjar.go | 8 +++--- internal/telegram/interceptor.go | 2 +- internal/telegram/lnurl-pay.go | 4 +-- internal/telegram/lnurl-withdraw.go | 12 ++++---- internal/telegram/pay.go | 8 +++--- internal/telegram/send.go | 8 +++--- internal/telegram/shop_helpers.go | 8 +++--- 12 files changed, 41 insertions(+), 88 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 0dc37d7b..f41801b6 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -1,7 +1,6 @@ package mutex import ( - "fmt" cmap "github.com/orcaman/concurrent-map" log "github.com/sirupsen/logrus" "sync" @@ -13,46 +12,6 @@ func init() { mutexMap = cmap.New() } -// checkSoftLock checks in mutexMap how often an existing mutex was already SoftLocked. -// The counter is there to avoid multiple recursive locking of an object in the mutexMap. -// This happens if multiple handlers call each other and try to lock/unlock multiple times -// the same mutex. -func checkSoftLock(s string) int { - if v, ok := mutexMap.Get(fmt.Sprintf("nLocks:%s", s)); ok { - return v.(int) - } - return 0 -} - -// LockSoft locks a mutex only if it hasn't been locked before. If it has, it increments the -// nLocks in the mutexMap. This is supposed to lock only if nLock == 0. -func LockSoft(s string) { - var nLocks = checkSoftLock(s) - if nLocks == 0 { - Lock(s) - } else { - log.Tracef("[Mutex] skipping LockSoft with nLocks: %d ", nLocks) - } - nLocks++ - mutexMap.Set(fmt.Sprintf("nLocks:%s", s), nLocks) - -} - -// UnlockSoft unlock a mutex only if it has been locked once. If it has been locked more than once -// it only decrements nLocks and skips the unlock of the mutex. This is supposed to unlock only for -// nLocks == 1 -func UnlockSoft(s string) { - var nLocks = checkSoftLock(s) - if nLocks == 1 { - Unlock(s) - } else { - log.Tracef("[Mutex] skipping UnlockSoft with nLocks: %d ", nLocks) - } - nLocks-- - mutexMap.Set(fmt.Sprintf("state:%s", s), nLocks) -} - -// Lock locks a mutex in the mutexMap. func Lock(s string) { if m, ok := mutexMap.Get(s); ok { m.(*sync.Mutex).Lock() @@ -64,11 +23,9 @@ func Lock(s string) { log.Tracef("[Mutex] Lock %s", s) } -// Unlock unlocks a mutex in the mutexMap. func Unlock(s string) { if m, ok := mutexMap.Get(s); ok { log.Tracef("[Mutex] Unlock %s", s) m.(*sync.Mutex).Unlock() - } } diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index bc550b62..de607bf5 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -160,8 +160,8 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { switch EnterAmountStateData.Type { case "LnurlPayState": tx := &LnurlPayState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return @@ -182,8 +182,8 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { return case "LnurlWithdrawState": tx := &LnurlWithdrawState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 49b1af68..85524c36 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -235,8 +235,8 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[acceptInlineFaucetHandler] %s", err) @@ -343,8 +343,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[cancelInlineFaucetHandler] %s", err) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 580f71c2..10c28a9b 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -146,8 +146,8 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) @@ -203,8 +203,6 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -303,8 +301,6 @@ func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) @@ -340,8 +336,8 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls rn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 3846916e..5c839cc3 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -161,8 +161,8 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -250,8 +250,8 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) { tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 49b9256e..3f340b10 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -236,8 +236,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback return } tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { // log.Errorf("[tipjar] %s", err) @@ -333,8 +333,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) { tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineTipjarHandler] %s", err) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 78cd8501..968eb22b 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -39,7 +39,7 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context if user != nil { mutex.Unlock(strconv.FormatInt(user.ID, 10)) } - return ctx, invalidTypeError + return nil, invalidTypeError } // lockInterceptor invoked as first before interceptor diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index aed3a44b..4b63b30b 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -130,8 +130,8 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { // use the enter amount state of the user to load the LNURL payment state tx := &LnurlPayState{Base: storage.New(storage.ID(enterAmountData.ID))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index f42ca289..21cb70dd 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -142,8 +142,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // use the enter amount state of the user to load the LNURL payment state tx := &LnurlWithdrawState{Base: storage.New(storage.ID(enterAmountData.ID))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) @@ -188,8 +188,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[confirmWithdrawHandler] Error: %s", err.Error()) @@ -308,8 +308,8 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelWithdrawHandler] Error: %s", err.Error()) diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index c28ad2f9..357d988f 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -149,8 +149,8 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { tx := &PayData{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -233,8 +233,8 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &PayData{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 2b8dc966..1367e5aa 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -217,8 +217,8 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // sendHandler invoked when user clicked send on payment confirmation func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { tx := &SendData{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) @@ -303,8 +303,8 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &SendData{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index bd992c64..55a604e5 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -242,8 +242,8 @@ func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops // getUserShops returns the Shops for the user func (bot *TipBot) getUserShops(ctx context.Context, user *lnbits.User) (*Shops, error) { tx := &Shops{Base: storage.New(storage.ID(fmt.Sprintf("shops-%d", user.Telegram.ID)))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.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) @@ -280,8 +280,8 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e // getShop returns the Shop for the given ID func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) { tx := &Shop{Base: storage.New(storage.ID(shopId))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.ShopBunt) if err != nil { From 4730c94c8450eab8fbbe3615a9ff249e34baefcc Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:25:55 +0100 Subject: [PATCH 109/541] Soft lock (#195) * add Lock and Unlock soft * Update mutex.go Co-authored-by: lngohumble --- internal/runtime/mutex/mutex.go | 43 +++++++++++++++++++++++++++++ internal/telegram/amounts.go | 8 +++--- internal/telegram/inline_faucet.go | 8 +++--- internal/telegram/inline_receive.go | 12 +++++--- internal/telegram/inline_send.go | 8 +++--- internal/telegram/inline_tipjar.go | 8 +++--- internal/telegram/interceptor.go | 2 +- internal/telegram/lnurl-pay.go | 4 +-- internal/telegram/lnurl-withdraw.go | 12 ++++---- internal/telegram/pay.go | 8 +++--- internal/telegram/send.go | 8 +++--- internal/telegram/shop_helpers.go | 8 +++--- 12 files changed, 88 insertions(+), 41 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index f41801b6..05076146 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -1,6 +1,7 @@ package mutex import ( + "fmt" cmap "github.com/orcaman/concurrent-map" log "github.com/sirupsen/logrus" "sync" @@ -12,6 +13,46 @@ func init() { mutexMap = cmap.New() } +// checkSoftLock checks in mutexMap how often an existing mutex was already SoftLocked. +// The counter is there to avoid multiple recursive locking of an object in the mutexMap. +// This happens if multiple handlers call each other and try to lock/unlock multiple times +// the same mutex. +func checkSoftLock(s string) int { + if v, ok := mutexMap.Get(fmt.Sprintf("nLocks:%s", s)); ok { + return v.(int) + } + return 0 +} + +// LockSoft locks a mutex only if it hasn't been locked before. If it has, it increments the +// nLocks in the mutexMap. This is supposed to lock only if nLock == 0. +func LockSoft(s string) { + var nLocks = checkSoftLock(s) + if nLocks == 0 { + Lock(s) + } else { + log.Tracef("[Mutex] skipping LockSoft with nLocks: %d ", nLocks) + } + nLocks++ + mutexMap.Set(fmt.Sprintf("nLocks:%s", s), nLocks) + +} + +// UnlockSoft unlock a mutex only if it has been locked once. If it has been locked more than once +// it only decrements nLocks and skips the unlock of the mutex. This is supposed to unlock only for +// nLocks == 1 +func UnlockSoft(s string) { + var nLocks = checkSoftLock(s) + if nLocks == 1 { + Unlock(s) + } else { + log.Tracef("[Mutex] skipping UnlockSoft with nLocks: %d ", nLocks) + } + nLocks-- + mutexMap.Set(fmt.Sprintf("nLocks:%s", s), nLocks) +} + +// Lock locks a mutex in the mutexMap. func Lock(s string) { if m, ok := mutexMap.Get(s); ok { m.(*sync.Mutex).Lock() @@ -23,9 +64,11 @@ func Lock(s string) { log.Tracef("[Mutex] Lock %s", s) } +// Unlock unlocks a mutex in the mutexMap. func Unlock(s string) { if m, ok := mutexMap.Get(s); ok { log.Tracef("[Mutex] Unlock %s", s) m.(*sync.Mutex).Unlock() + } } diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index de607bf5..bc550b62 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -160,8 +160,8 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { switch EnterAmountStateData.Type { case "LnurlPayState": tx := &LnurlPayState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return @@ -182,8 +182,8 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { return case "LnurlWithdrawState": tx := &LnurlWithdrawState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 85524c36..49b1af68 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -235,8 +235,8 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[acceptInlineFaucetHandler] %s", err) @@ -343,8 +343,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[cancelInlineFaucetHandler] %s", err) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 10c28a9b..580f71c2 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -146,8 +146,8 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) @@ -203,6 +203,8 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -301,6 +303,8 @@ func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) @@ -336,8 +340,8 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) // immediatelly set intransaction to block duplicate calls rn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 5c839cc3..3846916e 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -161,8 +161,8 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -250,8 +250,8 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) { tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 3f340b10..49b9256e 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -236,8 +236,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback return } tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { // log.Errorf("[tipjar] %s", err) @@ -333,8 +333,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) { tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineTipjarHandler] %s", err) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 968eb22b..78cd8501 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -39,7 +39,7 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context if user != nil { mutex.Unlock(strconv.FormatInt(user.ID, 10)) } - return nil, invalidTypeError + return ctx, invalidTypeError } // lockInterceptor invoked as first before interceptor diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index 4b63b30b..aed3a44b 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -130,8 +130,8 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { // use the enter amount state of the user to load the LNURL payment state tx := &LnurlPayState{Base: storage.New(storage.ID(enterAmountData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 21cb70dd..f42ca289 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -142,8 +142,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // use the enter amount state of the user to load the LNURL payment state tx := &LnurlWithdrawState{Base: storage.New(storage.ID(enterAmountData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) @@ -188,8 +188,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[confirmWithdrawHandler] Error: %s", err.Error()) @@ -308,8 +308,8 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelWithdrawHandler] Error: %s", err.Error()) diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 357d988f..c28ad2f9 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -149,8 +149,8 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { tx := &PayData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -233,8 +233,8 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &PayData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 1367e5aa..2b8dc966 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -217,8 +217,8 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // sendHandler invoked when user clicked send on payment confirmation func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { tx := &SendData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) @@ -303,8 +303,8 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &SendData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index 55a604e5..bd992c64 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -242,8 +242,8 @@ func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops // getUserShops returns the Shops for the user func (bot *TipBot) getUserShops(ctx context.Context, user *lnbits.User) (*Shops, error) { tx := &Shops{Base: storage.New(storage.ID(fmt.Sprintf("shops-%d", user.Telegram.ID)))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.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) @@ -280,8 +280,8 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e // getShop returns the Shop for the given ID func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) { tx := &Shop{Base: storage.New(storage.ID(shopId))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockSoft(tx.ID) + defer mutex.UnlockSoft(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.ShopBunt) if err != nil { From 67cff58c631e02c498bb291427b41e608b63f66b Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 23 Dec 2021 19:29:14 +0100 Subject: [PATCH 110/541] Revert "Soft lock (#195)" (#196) This reverts commit 4730c94c8450eab8fbbe3615a9ff249e34baefcc. --- internal/runtime/mutex/mutex.go | 43 ----------------------------- internal/telegram/amounts.go | 8 +++--- internal/telegram/inline_faucet.go | 8 +++--- internal/telegram/inline_receive.go | 12 +++----- internal/telegram/inline_send.go | 8 +++--- internal/telegram/inline_tipjar.go | 8 +++--- internal/telegram/interceptor.go | 2 +- internal/telegram/lnurl-pay.go | 4 +-- internal/telegram/lnurl-withdraw.go | 12 ++++---- internal/telegram/pay.go | 8 +++--- internal/telegram/send.go | 8 +++--- internal/telegram/shop_helpers.go | 8 +++--- 12 files changed, 41 insertions(+), 88 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 05076146..f41801b6 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -1,7 +1,6 @@ package mutex import ( - "fmt" cmap "github.com/orcaman/concurrent-map" log "github.com/sirupsen/logrus" "sync" @@ -13,46 +12,6 @@ func init() { mutexMap = cmap.New() } -// checkSoftLock checks in mutexMap how often an existing mutex was already SoftLocked. -// The counter is there to avoid multiple recursive locking of an object in the mutexMap. -// This happens if multiple handlers call each other and try to lock/unlock multiple times -// the same mutex. -func checkSoftLock(s string) int { - if v, ok := mutexMap.Get(fmt.Sprintf("nLocks:%s", s)); ok { - return v.(int) - } - return 0 -} - -// LockSoft locks a mutex only if it hasn't been locked before. If it has, it increments the -// nLocks in the mutexMap. This is supposed to lock only if nLock == 0. -func LockSoft(s string) { - var nLocks = checkSoftLock(s) - if nLocks == 0 { - Lock(s) - } else { - log.Tracef("[Mutex] skipping LockSoft with nLocks: %d ", nLocks) - } - nLocks++ - mutexMap.Set(fmt.Sprintf("nLocks:%s", s), nLocks) - -} - -// UnlockSoft unlock a mutex only if it has been locked once. If it has been locked more than once -// it only decrements nLocks and skips the unlock of the mutex. This is supposed to unlock only for -// nLocks == 1 -func UnlockSoft(s string) { - var nLocks = checkSoftLock(s) - if nLocks == 1 { - Unlock(s) - } else { - log.Tracef("[Mutex] skipping UnlockSoft with nLocks: %d ", nLocks) - } - nLocks-- - mutexMap.Set(fmt.Sprintf("nLocks:%s", s), nLocks) -} - -// Lock locks a mutex in the mutexMap. func Lock(s string) { if m, ok := mutexMap.Get(s); ok { m.(*sync.Mutex).Lock() @@ -64,11 +23,9 @@ func Lock(s string) { log.Tracef("[Mutex] Lock %s", s) } -// Unlock unlocks a mutex in the mutexMap. func Unlock(s string) { if m, ok := mutexMap.Get(s); ok { log.Tracef("[Mutex] Unlock %s", s) m.(*sync.Mutex).Unlock() - } } diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index bc550b62..de607bf5 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -160,8 +160,8 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { switch EnterAmountStateData.Type { case "LnurlPayState": tx := &LnurlPayState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return @@ -182,8 +182,8 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { return case "LnurlWithdrawState": tx := &LnurlWithdrawState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 49b1af68..85524c36 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -235,8 +235,8 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[acceptInlineFaucetHandler] %s", err) @@ -343,8 +343,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[cancelInlineFaucetHandler] %s", err) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 580f71c2..10c28a9b 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -146,8 +146,8 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) @@ -203,8 +203,6 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -303,8 +301,6 @@ func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) @@ -340,8 +336,8 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls rn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 3846916e..5c839cc3 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -161,8 +161,8 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -250,8 +250,8 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) { tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 49b9256e..3f340b10 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -236,8 +236,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback return } tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { // log.Errorf("[tipjar] %s", err) @@ -333,8 +333,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) { tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineTipjarHandler] %s", err) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 78cd8501..968eb22b 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -39,7 +39,7 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context if user != nil { mutex.Unlock(strconv.FormatInt(user.ID, 10)) } - return ctx, invalidTypeError + return nil, invalidTypeError } // lockInterceptor invoked as first before interceptor diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index aed3a44b..4b63b30b 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -130,8 +130,8 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { // use the enter amount state of the user to load the LNURL payment state tx := &LnurlPayState{Base: storage.New(storage.ID(enterAmountData.ID))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index f42ca289..21cb70dd 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -142,8 +142,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // use the enter amount state of the user to load the LNURL payment state tx := &LnurlWithdrawState{Base: storage.New(storage.ID(enterAmountData.ID))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) @@ -188,8 +188,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[confirmWithdrawHandler] Error: %s", err.Error()) @@ -308,8 +308,8 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelWithdrawHandler] Error: %s", err.Error()) diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index c28ad2f9..357d988f 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -149,8 +149,8 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { tx := &PayData{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -233,8 +233,8 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &PayData{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 2b8dc966..1367e5aa 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -217,8 +217,8 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // sendHandler invoked when user clicked send on payment confirmation func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { tx := &SendData{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) @@ -303,8 +303,8 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &SendData{Base: storage.New(storage.ID(c.Data))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index bd992c64..55a604e5 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -242,8 +242,8 @@ func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops // getUserShops returns the Shops for the user func (bot *TipBot) getUserShops(ctx context.Context, user *lnbits.User) (*Shops, error) { tx := &Shops{Base: storage.New(storage.ID(fmt.Sprintf("shops-%d", user.Telegram.ID)))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.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) @@ -280,8 +280,8 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e // getShop returns the Shop for the given ID func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) { tx := &Shop{Base: storage.New(storage.ID(shopId))} - mutex.LockSoft(tx.ID) - defer mutex.UnlockSoft(tx.ID) + mutex.Lock(tx.ID) + defer mutex.Unlock(tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.ShopBunt) if err != nil { From 036ac12f41b3054699ca5d88e9e77e6137d73268 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 23 Dec 2021 21:52:58 +0100 Subject: [PATCH 111/541] Lnurlpay pointer fix (#197) * speed up tx lock * fix pointer Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/lnurl-pay.go | 2 +- internal/telegram/lnurl.go | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index 4b63b30b..ace4a8ef 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -40,7 +40,7 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams } // object that holds all information about the send payment id := fmt.Sprintf("lnurlp-%d-%s", m.Sender.ID, RandStringRunes(5)) - lnurlPayState := LnurlPayState{ + lnurlPayState := &LnurlPayState{ Base: storage.New(storage.ID(id)), From: user, LNURLPayParams: payParams.LNURLPayParams, diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 73ae41f6..7cde4d25 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -82,10 +82,10 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { } switch params.(type) { case lnurl.LNURLPayParams: - payParams := LnurlPayState{LNURLPayParams: params.(lnurl.LNURLPayParams)} + payParams := &LnurlPayState{LNURLPayParams: params.(lnurl.LNURLPayParams)} log.Infof("[LNURL-p] %s", payParams.LNURLPayParams.Callback) bot.tryDeleteMessage(statusMsg) - bot.lnurlPayHandler(ctx, m, payParams) + bot.lnurlPayHandler(ctx, m, *payParams) return case lnurl.LNURLWithdrawResponse: withdrawParams := LnurlWithdrawState{LNURLWithdrawResponse: params.(lnurl.LNURLWithdrawResponse)} From f9aed7086905548db76fde930486810d00b36807 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 00:12:57 +0100 Subject: [PATCH 112/541] Soft lock (#198) * add Lock and Unlock soft * Update mutex.go * sync mutex * mutex bracket * add idInterceptor * context lock Co-authored-by: lngohumble Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/runtime/mutex/mutex.go | 52 ++++++++++++++++++++++++++++- internal/telegram/amounts.go | 8 ++--- internal/telegram/handler.go | 4 +-- internal/telegram/inline_faucet.go | 8 ++--- internal/telegram/inline_receive.go | 12 ++++--- internal/telegram/inline_send.go | 8 ++--- internal/telegram/inline_tipjar.go | 8 ++--- internal/telegram/interceptor.go | 5 ++- internal/telegram/lnurl-pay.go | 4 +-- internal/telegram/lnurl-withdraw.go | 12 +++---- internal/telegram/pay.go | 8 ++--- internal/telegram/send.go | 8 ++--- internal/telegram/shop_helpers.go | 8 ++--- 13 files changed, 101 insertions(+), 44 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index f41801b6..90d3ec68 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -1,9 +1,12 @@ package mutex import ( + "context" + "fmt" + "sync" + cmap "github.com/orcaman/concurrent-map" log "github.com/sirupsen/logrus" - "sync" ) var mutexMap cmap.ConcurrentMap @@ -12,6 +15,51 @@ func init() { mutexMap = cmap.New() } +// checkSoftLock checks in mutexMap how often an existing mutex was already SoftLocked. +// The counter is there to avoid multiple recursive locking of an object in the mutexMap. +// This happens if multiple handlers call each other and try to lock/unlock multiple times +// the same mutex. +func checkSoftLock(s string) int { + if v, ok := mutexMap.Get(fmt.Sprintf("nLocks:%s", s)); ok { + return v.(int) + } + return 0 +} + +// LockSoft locks a mutex only if it hasn't been locked before. If it has, it increments the +// nLocks in the mutexMap. This is supposed to lock only if nLock == 0. +func LockWithContext(ctx context.Context, s string) { + uid := ctx.Value("uid").(string) + Lock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) + var nLocks = checkSoftLock(uid) + if nLocks == 0 { + Lock(s) + } else { + log.Tracef("[Mutex] LockSoft (nLocks: %d)", nLocks) + } + nLocks++ + mutexMap.Set(fmt.Sprintf("nLocks:%s", uid), nLocks) + Unlock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) +} + +// UnlockSoft unlock a mutex only if it has been locked once. If it has been locked more than once +// it only decrements nLocks and skips the unlock of the mutex. This is supposed to unlock only for +// nLocks == 1 +func UnlockWithContext(ctx context.Context, s string) { + uid := ctx.Value("uid").(string) + Lock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) + var nLocks = checkSoftLock(uid) + nLocks-- + mutexMap.Set(fmt.Sprintf("nLocks:%s", uid), nLocks) + if nLocks == 0 { + Unlock(s) + } else { + log.Tracef("[Mutex] UnlockSoft with nLocks: %d ", nLocks) + } + Unlock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) +} + +// Lock locks a mutex in the mutexMap. func Lock(s string) { if m, ok := mutexMap.Get(s); ok { m.(*sync.Mutex).Lock() @@ -23,9 +71,11 @@ func Lock(s string) { log.Tracef("[Mutex] Lock %s", s) } +// Unlock unlocks a mutex in the mutexMap. func Unlock(s string) { if m, ok := mutexMap.Get(s); ok { log.Tracef("[Mutex] Unlock %s", s) m.(*sync.Mutex).Unlock() + } } diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index de607bf5..b6cdf1da 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -160,8 +160,8 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { switch EnterAmountStateData.Type { case "LnurlPayState": tx := &LnurlPayState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return @@ -182,8 +182,8 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { return case "LnurlWithdrawState": tx := &LnurlWithdrawState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { return diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 54610915..1d649c6a 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -29,7 +29,7 @@ func (bot TipBot) registerTelegramHandlers() { } func getDefaultBeforeInterceptor(bot TipBot) []intercept.Func { - return []intercept.Func{bot.lockInterceptor, bot.localizerInterceptor} + return []intercept.Func{bot.idInterceptor} } func getDefaultDeferInterceptor(bot TipBot) []intercept.Func { return []intercept.Func{bot.unlockInterceptor} @@ -40,7 +40,7 @@ func getDefaultAfterInterceptor(bot TipBot) []intercept.Func { // registerHandlerWithInterceptor will register a handler with all the predefined interceptors, based on the interceptor type func (bot TipBot) registerHandlerWithInterceptor(h Handler) { - //h.Interceptor.Before = append(getDefaultBeforeInterceptor(bot), h.Interceptor.Before...) + h.Interceptor.Before = append(getDefaultBeforeInterceptor(bot), h.Interceptor.Before...) //h.Interceptor.After = append(h.Interceptor.After, getDefaultAfterInterceptor(bot)...) //h.Interceptor.OnDefer = append(h.Interceptor.OnDefer, getDefaultDeferInterceptor(bot)...) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 85524c36..80f3650e 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -235,8 +235,8 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[acceptInlineFaucetHandler] %s", err) @@ -343,8 +343,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[cancelInlineFaucetHandler] %s", err) diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 10c28a9b..43806fa4 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -146,8 +146,8 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) @@ -203,6 +203,8 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -301,6 +303,8 @@ func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err) @@ -336,8 +340,8 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) // immediatelly set intransaction to block duplicate calls rn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 5c839cc3..4c7fc362 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -161,8 +161,8 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) { to := LoadUser(ctx) tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -250,8 +250,8 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) { tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 3f340b10..6a4903f7 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -236,8 +236,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback return } tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { // log.Errorf("[tipjar] %s", err) @@ -333,8 +333,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) { tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineTipjarHandler] %s", err) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 968eb22b..1adb15f6 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -39,7 +39,10 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context if user != nil { mutex.Unlock(strconv.FormatInt(user.ID, 10)) } - return nil, invalidTypeError + return ctx, invalidTypeError +} +func (bot TipBot) idInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + return context.WithValue(ctx, "uid", RandStringRunes(64)), nil } // lockInterceptor invoked as first before interceptor diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index ace4a8ef..e2520e0e 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -130,8 +130,8 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { // use the enter amount state of the user to load the LNURL payment state tx := &LnurlPayState{Base: storage.New(storage.ID(enterAmountData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 21cb70dd..0531cf57 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -142,8 +142,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // use the enter amount state of the user to load the LNURL payment state tx := &LnurlWithdrawState{Base: storage.New(storage.ID(enterAmountData.ID))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) @@ -188,8 +188,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[confirmWithdrawHandler] Error: %s", err.Error()) @@ -308,8 +308,8 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelWithdrawHandler] Error: %s", err.Error()) diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 357d988f..f1ead1e6 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -149,8 +149,8 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { // confirmPayHandler when user clicked pay on payment confirmation func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { tx := &PayData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { @@ -233,8 +233,8 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &PayData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 1367e5aa..70a14b5e 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -217,8 +217,8 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // sendHandler invoked when user clicked send on payment confirmation func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { tx := &SendData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) @@ -303,8 +303,8 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) ResetUserState(user, bot) tx := &SendData{Base: storage.New(storage.ID(c.Data))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err) diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index 55a604e5..891c2e2d 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -242,8 +242,8 @@ func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops // getUserShops returns the Shops for the user func (bot *TipBot) getUserShops(ctx context.Context, user *lnbits.User) (*Shops, error) { tx := &Shops{Base: storage.New(storage.ID(fmt.Sprintf("shops-%d", user.Telegram.ID)))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.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) @@ -280,8 +280,8 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e // getShop returns the Shop for the given ID func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) { tx := &Shop{Base: storage.New(storage.ID(shopId))} - mutex.Lock(tx.ID) - defer mutex.Unlock(tx.ID) + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.ShopBunt) if err != nil { From 8f8644a1d269c3e5115e3aa0ad3dbdd80e3a9efc Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 01:45:07 +0100 Subject: [PATCH 113/541] Faucet panic (#199) * speed up tx lock * remove panic Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/runtime/mutex/mutex.go | 11 ++++++++--- internal/telegram/inline_faucet.go | 16 ++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 90d3ec68..aef00ba0 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -26,10 +26,14 @@ func checkSoftLock(s string) int { return 0 } -// LockSoft locks a mutex only if it hasn't been locked before. If it has, it increments the -// nLocks in the mutexMap. This is supposed to lock only if nLock == 0. +// LockWithContext locks a mutex only if it hasn't been locked before in a context. +// The context carries a uid that is unique the each request (message, button press, etc.). +// If the uid has a lock already *for a certain object*, it increments the +// nLocks in the mutexMap. If not, it locks the object. This is supposed to lock only if nLock == 0. func LockWithContext(ctx context.Context, s string) { uid := ctx.Value("uid").(string) + // sync mutex to sync checkSoftLock with the increment of nLocks + // same user can't lock the same object multiple times Lock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) var nLocks = checkSoftLock(uid) if nLocks == 0 { @@ -42,7 +46,7 @@ func LockWithContext(ctx context.Context, s string) { Unlock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) } -// UnlockSoft unlock a mutex only if it has been locked once. If it has been locked more than once +// UnlockWithContext unlock a mutex only if it has been locked once within a context. If it has been locked more than once // it only decrements nLocks and skips the unlock of the mutex. This is supposed to unlock only for // nLocks == 1 func UnlockWithContext(ctx context.Context, s string) { @@ -57,6 +61,7 @@ func UnlockWithContext(ctx context.Context, s string) { log.Tracef("[Mutex] UnlockSoft with nLocks: %d ", nLocks) } Unlock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) + mutexMap.Remove(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) } // Lock locks a mutex in the mutexMap. diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 80f3650e..41bc39c7 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -247,6 +247,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if !inlineFaucet.Active { log.Errorf(fmt.Sprintf("[faucet] faucet %s inactive.", inlineFaucet.ID)) + bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check return } // release faucet no matter what @@ -296,11 +297,10 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) errMsg := fmt.Sprintf("[faucet] Transaction failed: %s", err) log.Warnln(errMsg) - // if faucet fails, cancel it: - c.Sender.ID = inlineFaucet.From.Telegram.ID // overwrite the sender of the callback to be the faucet owner - log.Debugf("[faucet] Canceling faucet %s...", inlineFaucet.ID) - bot.cancelInlineFaucetHandler(ctx, c) + // c.Sender.ID = inlineFaucet.From.Telegram.ID // overwrite the sender of the callback to be the faucet owner + // log.Debugf("[faucet] Canceling faucet %s...", inlineFaucet.ID) + bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check return } @@ -341,7 +341,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } -func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignoreID bool) { tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -352,7 +352,7 @@ func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback } inlineFaucet := fn.(*InlineFaucet) - if c.Sender.ID == inlineFaucet.From.Telegram.ID { + if ignoreID || c.Sender.ID == inlineFaucet.From.Telegram.ID { bot.tryEditMessage(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false @@ -361,3 +361,7 @@ func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback } return } +func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { + bot.cancelInlineFaucet(ctx, c, false) + return +} From 6aa05c8faa994dd1248f28e0a889acae45149b74 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Fri, 24 Dec 2021 02:22:41 +0100 Subject: [PATCH 114/541] Callback response (#200) * add answerCallbackInterceptor * use answerCallbackInterceptor * fix nil response --- internal/telegram/handler.go | 2 +- internal/telegram/interceptor.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 1d649c6a..1b3092a7 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -63,7 +63,7 @@ func (bot TipBot) registerHandlerWithInterceptor(h Handler) { for _, endpoint := range h.Endpoints { bot.handle(endpoint, intercept.HandlerWithCallback(h.Handler.(func(ctx context.Context, callback *tb.Callback)), intercept.WithBeforeCallback(h.Interceptor.Before...), - intercept.WithAfterCallback(h.Interceptor.After...), + intercept.WithAfterCallback(append(h.Interceptor.After, bot.answerCallbackInterceptor)...), intercept.WithDeferCallback(h.Interceptor.OnDefer...))) } } diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 1adb15f6..51a77482 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -45,6 +45,22 @@ func (bot TipBot) idInterceptor(ctx context.Context, i interface{}) (context.Con return context.WithValue(ctx, "uid", RandStringRunes(64)), nil } +func (bot TipBot) answerCallbackInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + switch i.(type) { + case *tb.Callback: + c := i.(*tb.Callback) + ctxcr := ctx.Value("callback_response") + var res []*tb.CallbackResponse + if ctxcr != nil { + res = append(res, &tb.CallbackResponse{CallbackID: c.ID, Text: ctxcr.(string)}) + } + var err error + err = bot.Telegram.Respond(c, res...) + return ctx, err + } + return ctx, invalidTypeError +} + // lockInterceptor invoked as first before interceptor func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { user := getTelegramUserFromInterface(i) From 78eadf1a0e96b18dc18f566323a0fb82e11ee543 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 02:43:21 +0100 Subject: [PATCH 115/541] Tip fix lock (#201) * speed up tx lock * tip fix lock Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/tooltip.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/tooltip.go b/internal/telegram/tooltip.go index f1aaa909..673fcf3f 100644 --- a/internal/telegram/tooltip.go +++ b/internal/telegram/tooltip.go @@ -106,7 +106,7 @@ func getTippersString(tippers []*tb.User) string { // tipTooltipExists checks if this tip is already known func tipTooltipExists(m *tb.Message, bot *TipBot) (bool, *TipTooltip) { - message := NewTipTooltip(m) + message := NewTipTooltip(&tb.Message{Chat: &tb.Chat{ID: m.Chat.ID}, ReplyTo: &tb.Message{ID: m.ReplyTo.ID}}) err := bot.Bunt.Get(message) if err != nil { return false, message From 47ef5baee3ee1fa7d6afb8537761bb850b82fea3 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Fri, 24 Dec 2021 14:25:35 +0100 Subject: [PATCH 116/541] fix mutex final last v2 maybe (#202) * remove mutex on unlock * add pprof * fix mutex lock * fix logs --- internal/runtime/mutex/mutex.go | 12 +++++++++--- main.go | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index aef00ba0..22dd7391 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -39,7 +39,7 @@ func LockWithContext(ctx context.Context, s string) { if nLocks == 0 { Lock(s) } else { - log.Tracef("[Mutex] LockSoft (nLocks: %d)", nLocks) + log.Tracef("[Mutex] Skip lock (nLocks: %d)", nLocks) } nLocks++ mutexMap.Set(fmt.Sprintf("nLocks:%s", uid), nLocks) @@ -58,7 +58,7 @@ func UnlockWithContext(ctx context.Context, s string) { if nLocks == 0 { Unlock(s) } else { - log.Tracef("[Mutex] UnlockSoft with nLocks: %d ", nLocks) + log.Tracef("[Mutex] Skip unlock (nLocks: %d)", nLocks) } Unlock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) mutexMap.Remove(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) @@ -66,9 +66,13 @@ func UnlockWithContext(ctx context.Context, s string) { // Lock locks a mutex in the mutexMap. func Lock(s string) { + log.Tracef("[Mutex] Attempt Lock %s", s) if m, ok := mutexMap.Get(s); ok { + log.Tracef("[Mutex] Attempt %s already in mutexMap", s) m.(*sync.Mutex).Lock() + mutexMap.Set(s, m) } else { + log.Tracef("[Mutex] Attempt %s not in mutexMap", s) m := &sync.Mutex{} m.Lock() mutexMap.Set(s, m) @@ -80,7 +84,9 @@ func Lock(s string) { func Unlock(s string) { if m, ok := mutexMap.Get(s); ok { log.Tracef("[Mutex] Unlock %s", s) + mutexMap.Remove(s) m.(*sync.Mutex).Unlock() - + } else { + log.Errorf("[Mutex] ⚠⚠⚠️ Unlock %s not in mutexMap. Skip.", s) } } diff --git a/main.go b/main.go index 8d277363..b0985e73 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + "net/http" "runtime/debug" "github.com/LightningTipBot/LightningTipBot/internal/lnbits/webhook" @@ -8,6 +9,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/price" "github.com/LightningTipBot/LightningTipBot/internal/telegram" log "github.com/sirupsen/logrus" + _ "net/http/pprof" ) // setLogger will initialize the log format @@ -22,6 +24,7 @@ func setLogger() { func main() { // set logger setLogger() + go http.ListenAndServe("0.0.0.0:6060", nil) defer withRecovery() bot := telegram.NewBot() webhook.NewServer(&bot) From 2bc9555b53a79d01c1b9b5309b8c52fac722f178 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 14:37:49 +0100 Subject: [PATCH 117/541] Min faucet 1 (#203) * speed up tx lock * reduce min faucet Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 41bc39c7..b05c2e94 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -63,7 +63,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User if err != nil { return nil, errors.New(errors.InvalidAmountError, err) } - if perUserAmount < 5 || amount%perUserAmount != 0 { + if perUserAmount < 1 || amount%perUserAmount != 0 { return nil, errors.New(errors.InvalidAmountPerUserError, fmt.Errorf("invalid amount per user")) } nTotal := int(amount / perUserAmount) From 2edc2953dca687b0b9b78fc9516ed814c78b118e Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 14:46:32 +0100 Subject: [PATCH 118/541] Revert "Min faucet 1 (#203)" (#204) This reverts commit 2bc9555b53a79d01c1b9b5309b8c52fac722f178. --- internal/telegram/inline_faucet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index b05c2e94..41bc39c7 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -63,7 +63,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User if err != nil { return nil, errors.New(errors.InvalidAmountError, err) } - if perUserAmount < 1 || amount%perUserAmount != 0 { + if perUserAmount < 5 || amount%perUserAmount != 0 { return nil, errors.New(errors.InvalidAmountPerUserError, fmt.Errorf("invalid amount per user")) } nTotal := int(amount / perUserAmount) From 5414c823395f67d6b0bfde30edf8865b169332fd Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 15:00:55 +0100 Subject: [PATCH 119/541] Logging mutex fix (#205) * speed up tx lock * better logging Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/runtime/mutex/mutex.go | 15 +++++++++------ internal/telegram/inline_faucet.go | 2 +- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 22dd7391..33bf9574 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -27,6 +27,7 @@ func checkSoftLock(s string) int { } // LockWithContext locks a mutex only if it hasn't been locked before in a context. +// LockWithContext should be used to lock objects like faucets etc. // The context carries a uid that is unique the each request (message, button press, etc.). // If the uid has a lock already *for a certain object*, it increments the // nLocks in the mutexMap. If not, it locks the object. This is supposed to lock only if nLock == 0. @@ -46,7 +47,8 @@ func LockWithContext(ctx context.Context, s string) { Unlock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) } -// UnlockWithContext unlock a mutex only if it has been locked once within a context. If it has been locked more than once +// UnlockWithContext unlock a mutex only if it has been locked once within a context. +// If it has been locked more than once // it only decrements nLocks and skips the unlock of the mutex. This is supposed to unlock only for // nLocks == 1 func UnlockWithContext(ctx context.Context, s string) { @@ -64,29 +66,30 @@ func UnlockWithContext(ctx context.Context, s string) { mutexMap.Remove(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) } -// Lock locks a mutex in the mutexMap. +// Lock locks a mutex in the mutexMap. If the mutex is already in the map, it locks the current call. +// After it another call unlocks the mutex (and deletes it from the mutexMap) the mutex written again into the mutexMap. +// If the mutex was not in the mutexMap before, a new mutext is created and locked and written into the mutexMap. func Lock(s string) { log.Tracef("[Mutex] Attempt Lock %s", s) if m, ok := mutexMap.Get(s); ok { - log.Tracef("[Mutex] Attempt %s already in mutexMap", s) m.(*sync.Mutex).Lock() mutexMap.Set(s, m) } else { - log.Tracef("[Mutex] Attempt %s not in mutexMap", s) m := &sync.Mutex{} m.Lock() mutexMap.Set(s, m) } - log.Tracef("[Mutex] Lock %s", s) + log.Tracef("[Mutex] Locked %s", s) } // Unlock unlocks a mutex in the mutexMap. func Unlock(s string) { if m, ok := mutexMap.Get(s); ok { - log.Tracef("[Mutex] Unlock %s", s) mutexMap.Remove(s) m.(*sync.Mutex).Unlock() + log.Tracef("[Mutex] Unlocked %s", s) } else { + // this should never happen. Mutex should have been in the mutexMap. log.Errorf("[Mutex] ⚠⚠⚠️ Unlock %s not in mutexMap. Skip.", s) } } diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 41bc39c7..ea247eb6 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -198,7 +198,7 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { inlineFaucet, err := bot.makeQueryFaucet(ctx, q, false) if err != nil { - log.Errorf("[handleInlineFaucetQuery] %s", err) + log.Errorf("[handleInlineFaucetQuery] %s", err.Error()) return } urls := []string{ From 09d3af411e920bab8a402fb4f6bb2b4a8d5b70b0 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 15:34:17 +0100 Subject: [PATCH 120/541] Better longging 1337 (#206) * speed up tx lock * better logging and lnurl lightning: prefix Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/lnbits/webhook/webhook.go | 4 ++-- internal/lnurl/lnurl.go | 4 ++-- internal/storage/base.go | 8 ++++---- internal/telegram/inline_faucet.go | 12 ++++++------ internal/telegram/inline_query.go | 2 +- internal/telegram/inline_receive.go | 16 +++++++-------- internal/telegram/inline_send.go | 6 +++--- internal/telegram/inline_tipjar.go | 10 +++++----- internal/telegram/invoice.go | 6 +++--- internal/telegram/link.go | 2 +- internal/telegram/lnurl-withdraw.go | 2 +- internal/telegram/lnurl.go | 7 ++++--- internal/telegram/pay.go | 8 ++++---- internal/telegram/photo.go | 6 +++--- internal/telegram/send.go | 6 +++--- internal/telegram/shop.go | 30 ++++++++++++++--------------- internal/telegram/shop_helpers.go | 2 +- internal/telegram/start.go | 6 +++--- internal/telegram/tip.go | 2 +- pkg/lightning/lightning.go | 2 +- 20 files changed, 71 insertions(+), 70 deletions(-) diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index 27f3bce2..211b9c71 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -87,13 +87,13 @@ func (w *Server) receive(writer http.ResponseWriter, request *http.Request) { request.Header.Del("content-length") err := json.NewDecoder(request.Body).Decode(&webhookEvent) if err != nil { - log.Errorf("[Webhook] Error decoding request: %s", err) + log.Errorf("[Webhook] Error decoding request: %s", err.Error()) writer.WriteHeader(400) return } user, err := w.GetUserByWalletId(webhookEvent.WalletID) if err != nil { - log.Errorf("[Webhook] Error getting user: %s", err) + log.Errorf("[Webhook] Error getting user: %s", err.Error()) writer.WriteHeader(400) return } diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index ba939d6c..26c97027 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -57,7 +57,7 @@ func (w Server) handleLnUrl(writer http.ResponseWriter, request *http.Request) { // check if error was returned from first or second handlers if err != nil { // log the error - log.Errorf("[LNURL] %v", err) + log.Errorf("[LNURL] %v", err.Error()) if response != nil { // there is a valid error response err = writeResponse(writer, response) @@ -161,7 +161,7 @@ func (w Server) serveLNURLpSecond(username string, amount_msat int64, comment st Webhook: w.WebhookServer}, w.c) if err != nil { - err = fmt.Errorf("[serveLNURLpSecond] Couldn't create invoice: %v", err) + err = fmt.Errorf("[serveLNURLpSecond] Couldn't create invoice: %v", err.Error()) resp = &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ Status: statusError, diff --git a/internal/storage/base.go b/internal/storage/base.go index 5497f0da..4841a355 100644 --- a/internal/storage/base.go +++ b/internal/storage/base.go @@ -1,9 +1,10 @@ package storage import ( + "time" + "github.com/eko/gocache/store" gocache "github.com/patrickmn/go-cache" - "time" log "github.com/sirupsen/logrus" ) @@ -55,7 +56,6 @@ func (tx *Base) Inactivate(s Storable, db *DB) error { func (tx *Base) Get(s Storable, db *DB) (Storable, error) { cacheTx, err := transactionCache.Get(s.Key()) if err != nil { - log.Errorf("[Bunt Cache] could not get bunt object: %v", err) err := db.Get(s) if err != nil { return s, err @@ -72,13 +72,13 @@ func (tx *Base) Set(s Storable, db *DB) error { tx.UpdatedAt = time.Now() err := db.Set(s) if err != nil { - log.Errorf("[Bunt] could not set object: %v", err) + log.Errorf("[Bunt] could not set object: %v", err.Error()) return err } log.Tracef("[Bunt] set object %s", s.Key()) err = transactionCache.Set(s.Key(), s, &store.Options{Expiration: 5 * time.Minute}) if err != nil { - log.Errorf("[Bunt Cache] could not set object: %v", err) + log.Errorf("[Bunt Cache] could not set object: %v", err.Error()) } log.Tracef("[Bunt Cache] set object: %s", s.Key()) return err diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index ea247eb6..8e8021a1 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -75,7 +75,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User } // check if fromUser has balance if balance < amount { - return nil, errors.New(errors.BalanceToLowError, fmt.Errorf("[faucet] Balance of user %s too low: %v", fromUserStr, err)) + return nil, errors.New(errors.BalanceToLowError, fmt.Errorf("[faucet] Balance of user %s too low: %v", fromUserStr, err.Error())) } // // check for memo in command memo := GetMemoFromCommand(text, 3) @@ -228,7 +228,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { CacheTime: 1, }) if err != nil { - log.Errorln(err) + log.Errorln(err.Error()) } } @@ -239,7 +239,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Debugf("[acceptInlineFaucetHandler] %s", err) + log.Debugf("[acceptInlineFaucetHandler] %s", err.Error()) return } inlineFaucet := fn.(*InlineFaucet) @@ -295,7 +295,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback success, err := t.Send() if !success { bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) - errMsg := fmt.Sprintf("[faucet] Transaction failed: %s", err) + errMsg := fmt.Sprintf("[faucet] Transaction failed: %s", err.Error()) log.Warnln(errMsg) // if faucet fails, cancel it: // c.Sender.ID = inlineFaucet.From.Telegram.ID // overwrite the sender of the callback to be the faucet owner @@ -312,7 +312,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) if err != nil { - errmsg := fmt.Errorf("[faucet] Error: Send message to %s: %s", toUserStr, err) + errmsg := fmt.Errorf("[faucet] Error: Send message to %s: %s", toUserStr, err.Error()) log.Warnln(errmsg) } @@ -347,7 +347,7 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Debugf("[cancelInlineFaucetHandler] %s", err) + log.Debugf("[cancelInlineFaucetHandler] %s", err.Error()) return } diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index 7326982e..32d45960 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -102,7 +102,7 @@ func (bot TipBot) anyChosenInlineHandler(q *tb.ChosenInlineResult) { inlineObject, err := bot.Cache.Get(q.ResultID) // check error if err != nil { - log.Errorf("[anyChosenInlineHandler] could not find inline object in cache. %v", err) + log.Errorf("[anyChosenInlineHandler] could not find inline object in cache. %v", err.Error()) return } switch inlineObject.(type) { diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 43806fa4..9cad0009 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -150,7 +150,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac defer mutex.UnlockWithContext(ctx, tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[getInlineReceive] %s", err) + log.Errorf("[getInlineReceive] %s", err.Error()) return } inlineReceive := rn.(*InlineReceive) @@ -184,7 +184,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac balance, err := bot.GetUserBalance(from) if err != nil { - errmsg := fmt.Sprintf("[inlineReceive] Error: Could not get user balance: %s", err) + errmsg := fmt.Sprintf("[inlineReceive] Error: Could not get user balance: %s", err.Error()) log.Warnln(errmsg) } @@ -208,7 +208,7 @@ func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) rn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { - // log.Errorf("[getInlineReceive] %s", err) + // log.Errorf("[getInlineReceive] %s", err.Error()) return } inlineReceive := rn.(*InlineReceive) @@ -248,7 +248,7 @@ func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) t.Memo = transactionMemo success, err := t.Send() if !success { - errMsg := fmt.Sprintf("[acceptInlineReceiveHandler] Transaction failed: %s", err) + errMsg := fmt.Sprintf("[acceptInlineReceiveHandler] Transaction failed: %s", err.Error()) log.Errorln(errMsg) bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveFailedMessage"), &tb.ReplyMarkup{}) return @@ -266,7 +266,7 @@ func (bot *TipBot) inlineReceiveInvoice(ctx context.Context, c *tb.Callback, inl } invoice, err := bot.createInvoiceWithEvent(ctx, inlineReceive.To, inlineReceive.Amount, fmt.Sprintf("Pay to %s", GetUserStr(inlineReceive.To.Telegram)), InvoiceCallbackInlineReceive, inlineReceive.ID) if err != nil { - errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) + errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err.Error()) bot.tryEditMessage(inlineReceive.Message, Translate(ctx, "errorTryLaterMessage")) log.Errorln(errmsg) return @@ -275,7 +275,7 @@ func (bot *TipBot) inlineReceiveInvoice(ctx context.Context, c *tb.Callback, inl // create qr code qr, err := qrcode.Encode(invoice.PaymentRequest, qrcode.Medium, 256) if err != nil { - errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err) + errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err.Error()) bot.tryEditMessage(inlineReceive.Message, Translate(ctx, "errorTryLaterMessage")) log.Errorln(errmsg) return @@ -307,7 +307,7 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac defer mutex.UnlockWithContext(ctx, tx.ID) rn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[getInlineReceive] %s", err) + log.Errorf("[getInlineReceive] %s", err.Error()) return } inlineReceive := rn.(*InlineReceive) @@ -345,7 +345,7 @@ func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callbac // immediatelly set intransaction to block duplicate calls rn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[cancelInlineReceiveHandler] %s", err) + log.Errorf("[cancelInlineReceiveHandler] %s", err.Error()) return } inlineReceive := rn.(*InlineReceive) diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 4c7fc362..bc0d62fe 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -166,7 +166,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { - // log.Errorf("[acceptInlineSendHandler] %s", err) + // log.Errorf("[acceptInlineSendHandler] %s", err.Error()) return } inlineSend := sn.(*InlineSend) @@ -222,7 +222,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) t.Memo = transactionMemo success, err := t.Send() if !success { - errMsg := fmt.Sprintf("[sendInline] Transaction failed: %s", err) + errMsg := fmt.Sprintf("[sendInline] Transaction failed: %s", err.Error()) log.Errorln(errMsg) bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "inlineSendFailedMessage"), &tb.ReplyMarkup{}) return @@ -255,7 +255,7 @@ func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[cancelInlineSendHandler] %s", err) + log.Errorf("[cancelInlineSendHandler] %s", err.Error()) return } inlineSend := sn.(*InlineSend) diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 6a4903f7..6a71bee9 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -183,7 +183,7 @@ func (bot TipBot) tipjarHandler(ctx context.Context, m *tb.Message) { ctx = bot.mapTipjarLanguage(ctx, m.Text) inlineTipjar, err := bot.makeTipjar(ctx, m, false) if err != nil { - log.Errorf("[tipjar] %s", err) + log.Errorf("[tipjar] %s", err.Error()) return } toUserStr := GetUserStr(m.Sender) @@ -195,7 +195,7 @@ func (bot TipBot) tipjarHandler(ctx context.Context, m *tb.Message) { func (bot TipBot) handleInlineTipjarQuery(ctx context.Context, q *tb.Query) { inlineTipjar, err := bot.makeQueryTipjar(ctx, q, false) if err != nil { - // log.Errorf("[tipjar] %s", err) + // log.Errorf("[tipjar] %s", err.Error()) return } urls := []string{ @@ -240,7 +240,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { - // log.Errorf("[tipjar] %s", err) + // log.Errorf("[tipjar] %s", err.Error()) return } inlineTipjar := fn.(*InlineTipjar) @@ -276,7 +276,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback success, err := t.Send() if !success { bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) - errMsg := fmt.Sprintf("[tipjar] Transaction failed: %s", err) + errMsg := fmt.Sprintf("[tipjar] Transaction failed: %s", err.Error()) log.Errorln(errMsg) return } @@ -337,7 +337,7 @@ func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[cancelInlineTipjarHandler] %s", err) + log.Errorf("[cancelInlineTipjarHandler] %s", err.Error()) return } inlineTipjar := fn.(*InlineTipjar) diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 3d464b23..30b5d557 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -104,7 +104,7 @@ func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { log.Infof("[/invoice] Creating invoice for %s of %d sat.", userStr, amount) invoice, err := bot.createInvoiceWithEvent(ctx, user, amount, memo, InvoiceCallbackGeneric, "") if err != nil { - errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) + errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err.Error()) bot.tryEditMessage(creatingMsg, Translate(ctx, "errorTryLaterMessage")) log.Errorln(errmsg) return @@ -113,7 +113,7 @@ func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { // create qr code qr, err := qrcode.Encode(invoice.PaymentRequest, qrcode.Medium, 256) if err != nil { - errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err) + errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err.Error()) bot.tryEditMessage(creatingMsg, Translate(ctx, "errorTryLaterMessage")) log.Errorln(errmsg) return @@ -135,7 +135,7 @@ func (bot *TipBot) createInvoiceWithEvent(ctx context.Context, user *lnbits.User Webhook: internal.Configuration.Lnbits.WebhookServer}, bot.Client) if err != nil { - errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err) + errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err.Error()) log.Errorln(errmsg) return InvoiceEvent{}, err } diff --git a/internal/telegram/link.go b/internal/telegram/link.go index d7dcd076..118e81eb 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -40,7 +40,7 @@ func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { // create qr code qr, err := qrcode.Encode(lndhubUrl, qrcode.Medium, 256) if err != nil { - errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err) + errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err.Error()) log.Errorln(errmsg) return } diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 0531cf57..1ae78bf5 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -246,7 +246,7 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { Webhook: internal.Configuration.Lnbits.WebhookServer}, bot.Client) if err != nil { - errmsg := fmt.Sprintf("[lnurlWithdrawHandlerWithdraw] Could not create an invoice: %s", err) + errmsg := fmt.Sprintf("[lnurlWithdrawHandlerWithdraw] Could not create an invoice: %s", err.Error()) log.Errorln(errmsg) bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) return diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 7cde4d25..01491e97 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -72,7 +72,8 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { return } - // assume payment + // get rid of the URI prefix + lnurlSplit = strings.TrimPrefix(lnurlSplit, "lightning:") // HandleLNURL by fiatjaf/go-lnurl _, params, err := bot.HandleLNURL(lnurlSplit) if err != nil { @@ -140,14 +141,14 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { fromUser := LoadUser(ctx) lnurlEncode, err := UserGetLNURL(fromUser) if err != nil { - errmsg := fmt.Sprintf("[userLnurlHandler] Failed to get LNURL: %s", err) + errmsg := fmt.Sprintf("[userLnurlHandler] Failed to get LNURL: %s", err.Error()) log.Errorln(errmsg) bot.Telegram.Send(m.Sender, Translate(ctx, "lnurlNoUsernameMessage")) } // create qr code qr, err := qrcode.Encode(lnurlEncode, qrcode.Medium, 256) if err != nil { - errmsg := fmt.Sprintf("[userLnurlHandler] Failed to create QR code for LNURL: %s", err) + errmsg := fmt.Sprintf("[userLnurlHandler] Failed to create QR code for LNURL: %s", err.Error()) log.Errorln(errmsg) return } diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index f1ead1e6..3a265446 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -63,7 +63,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { if err != nil { NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) - errmsg := fmt.Sprintf("[/pay] Error: Could not getArgumentFromCommand: %s", err) + errmsg := fmt.Sprintf("[/pay] Error: Could not getArgumentFromCommand: %s", err.Error()) log.Errorln(errmsg) return } @@ -75,7 +75,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { bolt11, err := decodepay.Decodepay(paymentRequest) if err != nil { bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) - errmsg := fmt.Sprintf("[/pay] Error: Could not decode invoice: %s", err) + errmsg := fmt.Sprintf("[/pay] Error: Could not decode invoice: %s", err.Error()) log.Errorln(errmsg) return } @@ -92,7 +92,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { balance, err := bot.GetUserBalance(user) if err != nil { NewMessage(m, WithDuration(0, bot)) - errmsg := fmt.Sprintf("[/pay] Error: Could not get user balance: %s", err) + errmsg := fmt.Sprintf("[/pay] Error: Could not get user balance: %s", err.Error()) log.Errorln(errmsg) bot.trySendMessage(m.Sender, Translate(ctx, "errorTryLaterMessage")) return @@ -154,7 +154,7 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { sn, err := tx.Get(tx, bot.Bunt) // immediatelly set intransaction to block duplicate calls if err != nil { - log.Errorf("[confirmPayHandler] %s", err) + log.Errorf("[confirmPayHandler] %s", err.Error()) return } payData := sn.(*PayData) diff --git a/internal/telegram/photo.go b/internal/telegram/photo.go index 155bcb6d..6d341381 100644 --- a/internal/telegram/photo.go +++ b/internal/telegram/photo.go @@ -51,18 +51,18 @@ func (bot *TipBot) photoHandler(ctx context.Context, m *tb.Message) { // get file reader closer from Telegram api reader, err := bot.Telegram.GetFile(m.Photo.MediaFile()) if err != nil { - log.Errorf("[photoHandler] getfile error: %v\n", err) + log.Errorf("[photoHandler] getfile error: %v\n", err.Error()) return } // decode to jpeg image img, err := jpeg.Decode(reader) if err != nil { - log.Errorf("[photoHandler] image.Decode error: %v\n", err) + log.Errorf("[photoHandler] image.Decode error: %v\n", err.Error()) return } data, err := TryRecognizeQrCode(img) if err != nil { - log.Errorf("[photoHandler] tryRecognizeQrCodes error: %v\n", err) + log.Errorf("[photoHandler] tryRecognizeQrCodes error: %v\n", err.Error()) bot.trySendMessage(m.Sender, Translate(ctx, "photoQrNotRecognizedMessage")) return } diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 70a14b5e..973cff77 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -221,7 +221,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[acceptSendHandler] %s", err) + log.Errorf("[acceptSendHandler] %s", err.Error()) return } sendData := sn.(*SendData) @@ -269,7 +269,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { success, err := t.Send() if !success || err != nil { // bot.trySendMessage(c.Sender, sendErrorMessage) - errmsg := fmt.Sprintf("[/send] Error: Transaction failed. %s", err) + errmsg := fmt.Sprintf("[/send] Error: Transaction failed. %s", err.Error()) log.Errorln(errmsg) bot.tryEditMessage(c.Message, i18n.Translate(sendData.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{}) return @@ -307,7 +307,7 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Errorf("[acceptSendHandler] %s", err) + log.Errorf("[acceptSendHandler] %s", err.Error()) return } diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index b235564f..5faa2e6d 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -480,7 +480,7 @@ func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) { var err error shop, err = bot.getShop(ctx, shopID) if err != nil { - log.Errorf("[shopHandler] %s", err) + log.Errorf("[shopHandler] %s", err.Error()) return } } @@ -511,7 +511,7 @@ func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) shop, err := bot.getShop(ctx, c.Data) if err != nil { - log.Errorf("[shopNewItemHandler] %s", err) + log.Errorf("[shopNewItemHandler] %s", err.Error()) return } if shop.Owner.Telegram.ID != c.Sender.ID { @@ -537,7 +537,7 @@ func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) { func (bot *TipBot) addShopItem(ctx context.Context, shopId string) (*Shop, ShopItem, error) { shop, err := bot.getShop(ctx, shopId) if err != nil { - log.Errorf("[addShopItem] %s", err) + log.Errorf("[addShopItem] %s", err.Error()) return shop, ShopItem{}, err } user := LoadUser(ctx) @@ -626,7 +626,7 @@ func (bot *TipBot) shopItemAddItemHandler(ctx context.Context, c *tb.Callback) { shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - log.Errorf("[shopNewItemHandler] %s", err) + log.Errorf("[shopNewItemHandler] %s", err.Error()) return } @@ -656,7 +656,7 @@ func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) { shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - log.Errorf("[shopNewItemHandler] %s", err) + log.Errorf("[shopNewItemHandler] %s", err.Error()) return } @@ -746,7 +746,7 @@ func (bot *TipBot) shopGetItemFilesHandler(ctx context.Context, c *tb.Callback) } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - log.Errorf("[shopNewItemHandler] %s", err) + log.Errorf("[shopNewItemHandler] %s", err.Error()) return } itemID := c.Data @@ -784,7 +784,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) { } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - log.Errorf("[shopConfirmBuyHandler] %s", err) + log.Errorf("[shopConfirmBuyHandler] %s", err.Error()) return } itemID := c.Data @@ -812,7 +812,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) { success, err := t.Send() if !success || err != nil { // bot.trySendMessage(c.Sender, sendErrorMessage) - errmsg := fmt.Sprintf("[shop] Error: Transaction failed. %s", err) + errmsg := fmt.Sprintf("[shop] Error: Transaction failed. %s", err.Error()) log.Errorln(errmsg) bot.trySendMessage(user.Telegram, i18n.Translate(user.Telegram.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{}) return @@ -841,7 +841,7 @@ func (bot *TipBot) shopSendItemFilesToUser(ctx context.Context, toUser *lnbits.U } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - log.Errorf("[shopNewItemHandler] %s", err) + log.Errorf("[shopNewItemHandler] %s", err.Error()) return } item := shop.Items[itemID] @@ -958,7 +958,7 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { if err != nil && user.Telegram.ID == shopOwner.Telegram.ID { shops, err = bot.initUserShops(ctx, user) if err != nil { - log.Errorf("[shopsHandler] %s", err) + log.Errorf("[shopsHandler] %s", err.Error()) return } } @@ -973,7 +973,7 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { for _, shopId := range shops.Shops { shop, err := bot.getShop(ctx, shopId) if err != nil { - log.Errorf("[shopsHandler] %s", err) + log.Errorf("[shopsHandler] %s", err.Error()) return } shopTitles += fmt.Sprintf("\n· %s (%d items)", str.MarkdownEscape(shop.Title), len(shop.Items)) @@ -1140,7 +1140,7 @@ func (bot *TipBot) shopsDescriptionHandler(ctx context.Context, c *tb.Callback) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { - log.Errorf("[shopsDescriptionHandler] %s", err) + log.Errorf("[shopsDescriptionHandler] %s", err.Error()) return } SetUserState(user, bot, lnbits.UserEnterShopsDescription, shops.ID) @@ -1152,7 +1152,7 @@ func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Messa user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { - log.Errorf("[enterShopsDescriptionHandler] %s", err) + log.Errorf("[enterShopsDescriptionHandler] %s", err.Error()) return } if shops.Owner.Telegram.ID != m.Sender.ID { @@ -1189,7 +1189,7 @@ func (bot *TipBot) shopsResetHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { - log.Errorf("[shopsResetHandler] %s", err) + log.Errorf("[shopsResetHandler] %s", err.Error()) return } if shops.Owner.Telegram.ID != c.Sender.ID { @@ -1312,7 +1312,7 @@ func (bot *TipBot) shopNewShopHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { - log.Errorf("[shopNewShopHandler] %s", err) + log.Errorf("[shopNewShopHandler] %s", err.Error()) return } if len(shops.Shops) >= shops.MaxShops { diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index 891c2e2d..cdf112ca 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -285,7 +285,7 @@ func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) { // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.ShopBunt) if err != nil { - log.Errorf("[getShop] %s", err) + log.Errorf("[getShop] %s", err.Error()) return &Shop{}, err } shop := sn.(*Shop) diff --git a/internal/telegram/start.go b/internal/telegram/start.go index 5c109fa1..c15555a3 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -85,7 +85,7 @@ func (bot TipBot) createWallet(user *lnbits.User) error { internal.Configuration.Lnbits.AdminId, UserStr) if err != nil { - errormsg := fmt.Sprintf("[createWallet] Create wallet error: %s", err) + errormsg := fmt.Sprintf("[createWallet] Create wallet error: %s", err.Error()) log.Errorln(errormsg) return err } @@ -94,7 +94,7 @@ func (bot TipBot) createWallet(user *lnbits.User) error { user.Name = u.Name wallet, err := bot.Client.Wallets(*user) if err != nil { - errormsg := fmt.Sprintf("[createWallet] Get wallet error: %s", err) + errormsg := fmt.Sprintf("[createWallet] Get wallet error: %s", err.Error()) log.Errorln(errormsg) return err } @@ -104,7 +104,7 @@ func (bot TipBot) createWallet(user *lnbits.User) error { user.CreatedAt = time.Now() err = UpdateUserRecord(user, bot) if err != nil { - errormsg := fmt.Sprintf("[createWallet] Update user record error: %s", err) + errormsg := fmt.Sprintf("[createWallet] Update user record error: %s", err.Error()) log.Errorln(errormsg) return err } diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index 10383440..070c443f 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -110,7 +110,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if !success { NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, fmt.Sprintf("%s %s", Translate(ctx, "tipErrorMessage"), err)) - errMsg := fmt.Sprintf("[/tip] Transaction failed: %s", err) + errMsg := fmt.Sprintf("[/tip] Transaction failed: %s", err.Error()) log.Warnln(errMsg) return } diff --git a/pkg/lightning/lightning.go b/pkg/lightning/lightning.go index 2858c7fd..51ee95fb 100644 --- a/pkg/lightning/lightning.go +++ b/pkg/lightning/lightning.go @@ -21,7 +21,7 @@ func IsInvoice(message string) bool { func IsLnurl(message string) bool { message = strings.ToLower(message) - if strings.HasPrefix(message, "lnurl") { + if strings.HasPrefix(message, "lnurl") || strings.HasPrefix(message, "lightning:lnurl") { // string must be a single word if !strings.Contains(message, " ") { return true From 46d77de38ebba32a97bbf065e0d75f64633f654c Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Fri, 24 Dec 2021 14:57:23 +0000 Subject: [PATCH 121/541] fix faucet balance to low panic (#207) Co-authored-by: lngohumble --- internal/telegram/inline_faucet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 8e8021a1..c5f8d004 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -75,7 +75,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User } // check if fromUser has balance if balance < amount { - return nil, errors.New(errors.BalanceToLowError, fmt.Errorf("[faucet] Balance of user %s too low: %v", fromUserStr, err.Error())) + return nil, errors.New(errors.BalanceToLowError, fmt.Errorf("[faucet] Balance of user %s too low", fromUserStr)) } // // check for memo in command memo := GetMemoFromCommand(text, 3) From 0ce42e8f1fc496fdd491b97de1e4c35ca15e09bb Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Fri, 24 Dec 2021 16:04:36 +0100 Subject: [PATCH 122/541] do not cancel inactive faucet (#208) * fix logs * remove cancel faucet if not active --- internal/telegram/inline_faucet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index c5f8d004..a7405f90 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -247,7 +247,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if !inlineFaucet.Active { log.Errorf(fmt.Sprintf("[faucet] faucet %s inactive.", inlineFaucet.ID)) - bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check + //bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check return } // release faucet no matter what From 9f1ce26d6eab87fbbb742e8f07cfc051aadda466 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 16:35:27 +0100 Subject: [PATCH 123/541] Even better faucet logging (#209) * speed up tx lock * faucet logging Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index a7405f90..422bb7ee 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -246,13 +246,14 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback from := inlineFaucet.From if !inlineFaucet.Active { - log.Errorf(fmt.Sprintf("[faucet] faucet %s inactive.", inlineFaucet.ID)) + log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive.", inlineFaucet.ID)) //bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check return } // release faucet no matter what if from.Telegram.ID == to.Telegram.ID { + log.Debugf("[faucet] %s is the owner faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) return } @@ -260,7 +261,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback for _, a := range inlineFaucet.To { if a.Telegram.ID == to.Telegram.ID { // to user is already in To slice, has taken from facuet - // log.Infof("[faucet] %s already took from faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) + log.Debugf("[faucet] %s already took from faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) return } } From eb07cdf117c59e296739806ca2b6b8c9cfeb6bb4 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Fri, 24 Dec 2021 16:35:46 +0100 Subject: [PATCH 124/541] disable rl for edit (#210) --- internal/telegram/telegram.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index b1a4e7a0..017f20a2 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -36,7 +36,7 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i } func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message) { - rate.CheckLimit(to) + //rate.CheckLimit(to) var err error msg, err = bot.Telegram.Edit(to, what, options...) if err != nil { From b796db1979a1e26624b2167c23505928c89d1f09 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 16:39:43 +0100 Subject: [PATCH 125/541] Cancel faucet if not empty (#211) * speed up tx lock * cancel Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 422bb7ee..eb89c516 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -247,7 +247,10 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if !inlineFaucet.Active { log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive.", inlineFaucet.ID)) - //bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check + if inlineFaucet.RemainingAmount >= inlineFaucet.PerUserAmount { + // something went wrong with this faucet, let's get rid of it. + bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check + } return } // release faucet no matter what From 192a100f8daf4fe8d2733a5065c73ff905955cfa Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 16:47:54 +0100 Subject: [PATCH 126/541] Cancel faucet if not empty (#212) * speed up tx lock * cancel * check better Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index eb89c516..1141242b 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -246,9 +246,10 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback from := inlineFaucet.From if !inlineFaucet.Active { - log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive.", inlineFaucet.ID)) - if inlineFaucet.RemainingAmount >= inlineFaucet.PerUserAmount { + log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) + if inlineFaucet.RemainingAmount > 0 { // something went wrong with this faucet, let's get rid of it. + log.Errorf(fmt.Sprintf("[faucet] Canceling inactive faucet %s.", inlineFaucet.ID)) bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check } return From 776de8804148d340cf39d1b5be06f203ec2292bc Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 16:54:07 +0100 Subject: [PATCH 127/541] Cancel faucet better (#213) * speed up tx lock * base canceled flag * oops Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/storage/base.go | 1 + internal/telegram/inline_faucet.go | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/storage/base.go b/internal/storage/base.go index 4841a355..90b9b19e 100644 --- a/internal/storage/base.go +++ b/internal/storage/base.go @@ -14,6 +14,7 @@ var transactionCache = store.NewGoCache(gocache.New(5*time.Minute, 10*time.Minut type Base struct { ID string `json:"id"` Active bool `json:"active"` + Canceled bool `json:"canceled"` CreatedAt time.Time `json:"created"` UpdatedAt time.Time `json:"updated"` } diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 1141242b..3aa4d7d1 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -247,7 +247,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if !inlineFaucet.Active { log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) - if inlineFaucet.RemainingAmount > 0 { + if inlineFaucet.RemainingAmount > 0 || !inlineFaucet.Canceled { // something went wrong with this faucet, let's get rid of it. log.Errorf(fmt.Sprintf("[faucet] Canceling inactive faucet %s.", inlineFaucet.ID)) bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check @@ -361,6 +361,7 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor bot.tryEditMessage(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false + inlineFaucet.Canceled = true runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) log.Debugf("[faucet] Faucet %s canceled.", inlineFaucet.ID) } From 32a5a7c264a9fd5dfb7b555f1400dea0f8737e7d Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 17:02:08 +0100 Subject: [PATCH 128/541] Faucet cancel 1338 (#214) * speed up tx lock * yp Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 3aa4d7d1..704850e5 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -342,6 +342,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } bot.tryEditMessage(c.Message, inlineFaucet.Message) inlineFaucet.Active = false + inlineFaucet.Canceled = true + log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) } } From f9683109027fc400c8cb8c9610c98c65f2e7b85a Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 17:10:20 +0100 Subject: [PATCH 129/541] Facuet cancel 1339 (#215) * speed up tx lock * cancel better Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 704850e5..c0613fcd 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -250,7 +250,15 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if inlineFaucet.RemainingAmount > 0 || !inlineFaucet.Canceled { // something went wrong with this faucet, let's get rid of it. log.Errorf(fmt.Sprintf("[faucet] Canceling inactive faucet %s.", inlineFaucet.ID)) - bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check + // faucet is depleted + inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetEndedMessage"), inlineFaucet.Amount, inlineFaucet.NTaken) + if inlineFaucet.UserNeedsWallet { + inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) + } + bot.tryEditMessage(c.Message, inlineFaucet.Message) + inlineFaucet.Active = false + inlineFaucet.Canceled = true + log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) } return } @@ -342,7 +350,6 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } bot.tryEditMessage(c.Message, inlineFaucet.Message) inlineFaucet.Active = false - inlineFaucet.Canceled = true log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) } From 43a77fd1b286f5a2d50accf3627483b0088d8e0e Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 17:14:08 +0100 Subject: [PATCH 130/541] Faucet bla 13340 (#216) * speed up tx lock * yolo master Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index c0613fcd..45309e30 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -246,7 +246,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback from := inlineFaucet.From if !inlineFaucet.Active { - log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) + log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Canceled: %t, Remaining: %d sat", inlineFaucet.ID, inlineFaucet.Canceled, inlineFaucet.RemainingAmount)) if inlineFaucet.RemainingAmount > 0 || !inlineFaucet.Canceled { // something went wrong with this faucet, let's get rid of it. log.Errorf(fmt.Sprintf("[faucet] Canceling inactive faucet %s.", inlineFaucet.ID)) From 73ada1f25b32066f6166542d552bdf68e6d40b2c Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 24 Dec 2021 17:32:01 +0100 Subject: [PATCH 131/541] Faucet 1341 (#217) * speed up tx lock * yolo master * faucet fix Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 45309e30..6c94b5d5 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -246,19 +246,12 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback from := inlineFaucet.From if !inlineFaucet.Active { - log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Canceled: %t, Remaining: %d sat", inlineFaucet.ID, inlineFaucet.Canceled, inlineFaucet.RemainingAmount)) - if inlineFaucet.RemainingAmount > 0 || !inlineFaucet.Canceled { - // something went wrong with this faucet, let's get rid of it. - log.Errorf(fmt.Sprintf("[faucet] Canceling inactive faucet %s.", inlineFaucet.ID)) - // faucet is depleted - inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetEndedMessage"), inlineFaucet.Amount, inlineFaucet.NTaken) - if inlineFaucet.UserNeedsWallet { - inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) - } - bot.tryEditMessage(c.Message, inlineFaucet.Message) - inlineFaucet.Active = false - inlineFaucet.Canceled = true - log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) + log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) + + // hack + editTo := i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage") + if c.Message.Text != editTo { + bot.tryEditMessage(c.Message, editTo, &tb.ReplyMarkup{}) } return } From 2066470cba518f934ebe3ccd18341729ab5baf14 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Fri, 24 Dec 2021 18:10:52 +0100 Subject: [PATCH 132/541] send telegram messages async (#219) --- internal/telegram/inline_faucet.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 6c94b5d5..98baed61 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -314,13 +314,14 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.NTaken += 1 inlineFaucet.To = append(inlineFaucet.To, to) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount - - _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) - _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) - if err != nil { - errmsg := fmt.Errorf("[faucet] Error: Send message to %s: %s", toUserStr, err.Error()) - log.Warnln(errmsg) - } + go func() { + _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) + _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) + if err != nil { + errmsg := fmt.Errorf("[faucet] Error: Send message to %s: %s", toUserStr, err.Error()) + log.Warnln(errmsg) + } + }() // build faucet message inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) From 1b720b498c3284aa6fd9a101662b4c63c3c5aa0c Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Fri, 24 Dec 2021 18:11:00 +0100 Subject: [PATCH 133/541] print faucet link if available (#218) --- internal/telegram/inline_faucet.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 98baed61..62480d3f 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "strconv" "strings" "time" @@ -244,7 +245,10 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } inlineFaucet := fn.(*InlineFaucet) from := inlineFaucet.From - + // log faucet link if possible + if c.Message != nil && c.Message.Chat != nil { + log.Debugf("[faucet] Link: https://t.me/c/%s/%d", strconv.FormatInt(c.Message.Chat.ID, 10)[4:], c.Message.ID) + } if !inlineFaucet.Active { log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) From ca60b271fdeec39b1b39d418efc589357888a73c Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 01:42:43 +0100 Subject: [PATCH 134/541] Dont cancel faucet (#220) * speed up tx lock * yolo master * faucet cancel not Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 62480d3f..9e4813e0 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -251,12 +251,6 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } if !inlineFaucet.Active { log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) - - // hack - editTo := i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage") - if c.Message.Text != editTo { - bot.tryEditMessage(c.Message, editTo, &tb.ReplyMarkup{}) - } return } // release faucet no matter what From 6b746447e573e814559e4c920ac852204a10f0b1 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 02:50:58 +0100 Subject: [PATCH 135/541] add initial main menu keyboard. (#221) Co-authored-by: lngohumble --- internal/telegram/buttons.go | 22 +++++++++++++++++ internal/telegram/handler.go | 8 +++--- internal/telegram/invoice.go | 4 ++- internal/telegram/telegram.go | 46 ++++++++++++++++++++++++++++++++--- 4 files changed, 71 insertions(+), 9 deletions(-) diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index 7bb392cb..dccf2ed4 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -2,6 +2,28 @@ package telegram import tb "gopkg.in/lightningtipbot/telebot.v2" +const ( + CommandSend = "💸 Send" + CommandBalance = "👑 Balance" + CommandInvoice = "⚡️ Invoice" + CommandHelp = "📖 Help" +) + +var ( + mainMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + btnHelpMainMenu = mainMenu.Text(CommandHelp) + btnSendMainMenu = mainMenu.Text(CommandSend) + btnBalanceMainMenu = mainMenu.Text(CommandBalance) + btnInvoiceMainMenu = mainMenu.Text(CommandInvoice) +) + +func init() { + mainMenu.Reply( + mainMenu.Row(btnBalanceMainMenu, btnHelpMainMenu), + mainMenu.Row(btnInvoiceMainMenu, btnSendMainMenu), + ) +} + // buttonWrapper wrap buttons slice in rows of length i func buttonWrapper(buttons []tb.Btn, markup *tb.ReplyMarkup, length int) []tb.Row { buttonLength := len(buttons) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 1b3092a7..67b7d484 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -153,7 +153,7 @@ func (bot TipBot) getHandler() []Handler { }, }, { - Endpoints: []interface{}{"/invoice"}, + Endpoints: []interface{}{"/invoice", &btnInvoiceMainMenu}, Handler: bot.invoiceHandler, Interceptor: &Interceptor{ Type: MessageInterceptor, @@ -200,7 +200,7 @@ func (bot TipBot) getHandler() []Handler { }}, }, { - Endpoints: []interface{}{"/balance"}, + Endpoints: []interface{}{"/balance", &btnBalanceMainMenu}, Handler: bot.balanceHandler, Interceptor: &Interceptor{ Type: MessageInterceptor, @@ -216,7 +216,7 @@ func (bot TipBot) getHandler() []Handler { }, }, { - Endpoints: []interface{}{"/send"}, + Endpoints: []interface{}{"/send", &btnSendMainMenu}, Handler: bot.sendHandler, Interceptor: &Interceptor{ Type: MessageInterceptor, @@ -265,7 +265,7 @@ func (bot TipBot) getHandler() []Handler { }, }, { - Endpoints: []interface{}{"/help"}, + Endpoints: []interface{}{"/help", &btnHelpMainMenu}, Handler: bot.helpHandler, Interceptor: &Interceptor{ Type: MessageInterceptor, diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 30b5d557..9468ef95 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -119,7 +119,9 @@ func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { return } - bot.tryDeleteMessage(creatingMsg) + // deleting messages will delete the main menu. + //bot.tryDeleteMessage(creatingMsg) + // send the invoice data to user bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) log.Printf("[/invoice] Incvoice created. User: %s, amount: %d sat.", userStr, amount) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 017f20a2..ceecdb41 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -6,12 +6,43 @@ import ( "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" + "strconv" "time" ) +// appendMainMenu will check if to (recipient) ID is from private or group chat. +// this function will only add a keyboard if this is a private chat and no force reply. +func appendMainMenu(to int64, options []interface{}) []interface{} { + var isForceReply bool + for _, option := range options { + if option == tb.ForceReply { + isForceReply = true + } + } + if to > 0 && !isForceReply { + options = append(options, mainMenu) + } + return options +} + +// getChatIdFromRecipient will parse the recipient to int64 +func getChatIdFromRecipient(to tb.Recipient) (int64, error) { + chatId, err := strconv.ParseInt(to.Recipient(), 10, 64) + if err != nil { + return 0, err + } + return chatId, nil +} + func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options ...interface{}) (msg *tb.Message) { rate.CheckLimit(to) - msg, err := bot.Telegram.Forward(to, what, options...) + // ChatId is used for the keyboard + chatId, err := getChatIdFromRecipient(to) + if err != nil { + log.Errorf("[tryForwardMessage] error converting message recipient to int64: %v", err) + return + } + msg, err = bot.Telegram.Forward(to, what, appendMainMenu(chatId, options)...) if err != nil { log.Warnln(err.Error()) } @@ -19,7 +50,13 @@ func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options . } func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...interface{}) (msg *tb.Message) { rate.CheckLimit(to) - msg, err := bot.Telegram.Send(to, what, options...) + // ChatId is used for the keyboard + chatId, err := getChatIdFromRecipient(to) + if err != nil { + log.Errorf("[trySendMessage] error converting message recipient to int64: %v", err) + return + } + msg, err = bot.Telegram.Send(to, what, appendMainMenu(chatId, options)...) if err != nil { log.Warnln(err.Error()) } @@ -28,7 +65,7 @@ func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...i func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...interface{}) (msg *tb.Message) { rate.CheckLimit(to) - msg, err := bot.Telegram.Reply(to, what, options...) + msg, err := bot.Telegram.Reply(to, what, appendMainMenu(to.Chat.ID, options)...) if err != nil { log.Warnln(err.Error()) } @@ -38,7 +75,8 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message) { //rate.CheckLimit(to) var err error - msg, err = bot.Telegram.Edit(to, what, options...) + _, chatId := to.MessageSig() + msg, err = bot.Telegram.Edit(to, appendMainMenu(chatId, options), what) if err != nil { log.Warnln(err.Error()) } From 6aca41835d0058f3ace1861b2f67404e1295e813 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 11:39:46 +0100 Subject: [PATCH 136/541] fix options and what (#222) Co-authored-by: lngohumble --- internal/telegram/telegram.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index ceecdb41..ce01ae32 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -76,7 +76,7 @@ func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...in //rate.CheckLimit(to) var err error _, chatId := to.MessageSig() - msg, err = bot.Telegram.Edit(to, appendMainMenu(chatId, options), what) + msg, err = bot.Telegram.Edit(to, what, appendMainMenu(chatId, options)...) if err != nil { log.Warnln(err.Error()) } From 5065f5af865d1241075cbb98a80e02767464e20d Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 12:19:28 +0100 Subject: [PATCH 137/541] Fix button (#223) * fix options and what * fix keyboard Co-authored-by: lngohumble --- internal/telegram/buttons.go | 8 ++++---- internal/telegram/telegram.go | 15 ++++++++++++--- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index dccf2ed4..08504572 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -3,10 +3,10 @@ package telegram import tb "gopkg.in/lightningtipbot/telebot.v2" const ( - CommandSend = "💸 Send" - CommandBalance = "👑 Balance" - CommandInvoice = "⚡️ Invoice" - CommandHelp = "📖 Help" + CommandSend = "💸-Send" + CommandBalance = "👑-Balance" + CommandInvoice = "⚡️-Invoice" + CommandHelp = "📖-Help" ) var ( diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index ce01ae32..2f9f6434 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -13,13 +13,22 @@ import ( // appendMainMenu will check if to (recipient) ID is from private or group chat. // this function will only add a keyboard if this is a private chat and no force reply. func appendMainMenu(to int64, options []interface{}) []interface{} { - var isForceReply bool + appendKeyboard := true for _, option := range options { if option == tb.ForceReply { - isForceReply = true + appendKeyboard = false + } + switch option.(type) { + case *tb.ReplyMarkup: + appendKeyboard = false + //option.(*tb.ReplyMarkup).ReplyKeyboard = mainMenu.ReplyKeyboard + //if option.(*tb.ReplyMarkup).InlineKeyboard == nil { + // options = append(options[:i], options[i+1:]...) + //} } } - if to > 0 && !isForceReply { + // to > 0 is private chats + if to > 0 && appendKeyboard { options = append(options, mainMenu) } return options From 02181917936c9ad6234f3b1f27a0333f2bfdafd0 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 13:53:57 +0100 Subject: [PATCH 138/541] fix button space (#224) Co-authored-by: lngohumble --- internal/telegram/buttons.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index 08504572..ed1ee72a 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -2,11 +2,12 @@ package telegram import tb "gopkg.in/lightningtipbot/telebot.v2" +// we can't use space in the label of buttons, because string splitting will mess everything up. const ( - CommandSend = "💸-Send" - CommandBalance = "👑-Balance" - CommandInvoice = "⚡️-Invoice" - CommandHelp = "📖-Help" + CommandSend = "💸 Send" + CommandBalance = "👑 Balance" + CommandInvoice = "⚡️ Invoice" + CommandHelp = "📖 Help" ) var ( From ec15f26c13d046fd3deb909aacaec77dd8f9b0be Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 14:06:52 +0100 Subject: [PATCH 139/541] Increase rate limit (#225) * speed up tx lock * yolo master * rate limit Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/rate/limiter.go | 7 ++++--- internal/telegram/telegram.go | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/rate/limiter.go b/internal/rate/limiter.go index 41847782..8c687f07 100644 --- a/internal/rate/limiter.go +++ b/internal/rate/limiter.go @@ -2,10 +2,11 @@ package rate import ( "context" - "golang.org/x/time/rate" - tb "gopkg.in/lightningtipbot/telebot.v2" "strconv" "sync" + + "golang.org/x/time/rate" + tb "gopkg.in/lightningtipbot/telebot.v2" ) // Limiter @@ -21,7 +22,7 @@ var globalLimiter *rate.Limiter // NewLimiter creates both chat and global rate limiters. func Start() { - idLimiter = newIdRateLimiter(rate.Limit(0.3), 20) + idLimiter = newIdRateLimiter(rate.Limit(1), 20) globalLimiter = rate.NewLimiter(rate.Limit(30), 30) } diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 2f9f6434..144dee42 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -2,12 +2,13 @@ package telegram import ( "fmt" + "strconv" + "time" + "github.com/LightningTipBot/LightningTipBot/internal/rate" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" - "strconv" - "time" ) // appendMainMenu will check if to (recipient) ID is from private or group chat. @@ -82,7 +83,7 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i } func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message) { - //rate.CheckLimit(to) + rate.CheckLimit(to) var err error _, chatId := to.MessageSig() msg, err = bot.Telegram.Edit(to, what, appendMainMenu(chatId, options)...) From 39ff25becaf36e0ff5ada43a8a5d9e76a5be7103 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 14:14:53 +0100 Subject: [PATCH 140/541] Increase rate limit (#226) * speed up tx lock * yolo master * rate limit * decrease Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> From fe1a921440c4b8ec9ca1aaa9a460f8b6e8985dea Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 14:30:25 +0100 Subject: [PATCH 141/541] Faucet cool (#227) * speed up tx lock * yolo master * coool Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/rate/limiter.go | 2 +- internal/telegram/inline_faucet.go | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/rate/limiter.go b/internal/rate/limiter.go index 8c687f07..33ba8b95 100644 --- a/internal/rate/limiter.go +++ b/internal/rate/limiter.go @@ -22,7 +22,7 @@ var globalLimiter *rate.Limiter // NewLimiter creates both chat and global rate limiters. func Start() { - idLimiter = newIdRateLimiter(rate.Limit(1), 20) + idLimiter = newIdRateLimiter(rate.Limit(0.3), 20) globalLimiter = rate.NewLimiter(rate.Limit(30), 30) } diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 9e4813e0..b4059088 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -304,7 +304,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // if faucet fails, cancel it: // c.Sender.ID = inlineFaucet.From.Telegram.ID // overwrite the sender of the callback to be the faucet owner // log.Debugf("[faucet] Canceling faucet %s...", inlineFaucet.ID) - bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check + // bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check return } @@ -332,7 +332,9 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } // update message log.Infoln(inlineFaucet.Message) - bot.tryEditMessage(c.Message, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) + go func() { + bot.tryEditMessage(c.Message, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) + }() } if inlineFaucet.RemainingAmount < inlineFaucet.PerUserAmount { // faucet is depleted From 13957342df86836cb5f0f0f5bc6750230990f59b Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 14:43:36 +0100 Subject: [PATCH 142/541] Fix faucet again 1337 (#228) * speed up tx lock * yolo master * fix it Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index b4059088..34f0a9cf 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -332,9 +332,13 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } // update message log.Infoln(inlineFaucet.Message) - go func() { - bot.tryEditMessage(c.Message, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) - }() + + // update the message if the faucet still has some sats left after this tx + if inlineFaucet.RemainingAmount >= inlineFaucet.PerUserAmount { + go func() { + bot.tryEditMessage(c.Message, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) + }() + } } if inlineFaucet.RemainingAmount < inlineFaucet.PerUserAmount { // faucet is depleted @@ -342,7 +346,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if inlineFaucet.UserNeedsWallet { inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } - bot.tryEditMessage(c.Message, inlineFaucet.Message) + bot.tryEditMessage(c.Message, inlineFaucet.Message, &tb.ReplyMarkup{}) inlineFaucet.Active = false log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) } From 737ab115bd49681fc9e5fd85b72af379f48d194d Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 14:52:54 +0100 Subject: [PATCH 143/541] Fix faucet again 1337 (#230) * speed up tx lock * yolo master * fix it * decrease limit Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/rate/limiter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/rate/limiter.go b/internal/rate/limiter.go index 33ba8b95..05f3e725 100644 --- a/internal/rate/limiter.go +++ b/internal/rate/limiter.go @@ -22,7 +22,7 @@ var globalLimiter *rate.Limiter // NewLimiter creates both chat and global rate limiters. func Start() { - idLimiter = newIdRateLimiter(rate.Limit(0.3), 20) + idLimiter = newIdRateLimiter(rate.Limit(0.25), 20) globalLimiter = rate.NewLimiter(rate.Limit(30), 30) } From 24108f6a1f9508da546ee9d9d034fb5f8e3c0ef3 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 25 Dec 2021 23:11:31 +0100 Subject: [PATCH 144/541] Finish the faucet (#232) * speed up tx lock * yolo master * finish the faucet when inactive * delete comments Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_faucet.go | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 34f0a9cf..307e1aec 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -251,6 +251,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } if !inlineFaucet.Active { log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) + bot.finishFaucet(ctx, c, inlineFaucet) return } // release faucet no matter what @@ -342,13 +343,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback } if inlineFaucet.RemainingAmount < inlineFaucet.PerUserAmount { // faucet is depleted - inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetEndedMessage"), inlineFaucet.Amount, inlineFaucet.NTaken) - if inlineFaucet.UserNeedsWallet { - inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) - } - bot.tryEditMessage(c.Message, inlineFaucet.Message, &tb.ReplyMarkup{}) - inlineFaucet.Active = false - log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) + bot.finishFaucet(ctx, c, inlineFaucet) } } @@ -374,6 +369,17 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor } return } + +func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFaucet *InlineFaucet) { + inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetEndedMessage"), inlineFaucet.Amount, inlineFaucet.NTaken) + if inlineFaucet.UserNeedsWallet { + inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) + } + bot.tryEditMessage(c.Message, inlineFaucet.Message, &tb.ReplyMarkup{}) + inlineFaucet.Active = false + log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) +} + func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { bot.cancelInlineFaucet(ctx, c, false) return From 151ed39b04af88a7cdd89f55fc6cb40f4eb45c63 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 26 Dec 2021 00:23:58 +0100 Subject: [PATCH 145/541] add once + singleton click interceptor (#229) * speed up tx lock * yolo master * add once + singleton click interceptor * yoooo; * stuff Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/runtime/once/once.go | 48 ++++++++++++++++++++++++++++++ internal/telegram/handler.go | 1 + internal/telegram/inline_faucet.go | 3 ++ internal/telegram/interceptor.go | 15 +++++++++- 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 internal/runtime/once/once.go diff --git a/internal/runtime/once/once.go b/internal/runtime/once/once.go new file mode 100644 index 00000000..680a1743 --- /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 67b7d484..d36279ea 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 307e1aec..49d8feca 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 51a77482..750a1d7c 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) From 373ee844da7ff537dd75ac5361736a507f7ff274 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Dec 2021 00:33:00 +0100 Subject: [PATCH 146/541] Log oncemap (#233) * speed up tx lock * yolo master * logging Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/runtime/once/once.go | 3 ++- internal/telegram/inline_faucet.go | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/runtime/once/once.go b/internal/runtime/once/once.go index 680a1743..547fdc1d 100644 --- a/internal/runtime/once/once.go +++ b/internal/runtime/once/once.go @@ -28,6 +28,7 @@ func Once(k1, k2 string) error { } userMap := cmap.New() onceMap.Set(k1, userMap) + log.Tracef("[Once] Added key %s to onceMap (len=%d)", k1, len(onceMap.Keys())) return setOrReturn(userMap, k2) } @@ -44,5 +45,5 @@ func setOrReturn(objectMap cmap.ConcurrentMap, k2 string) error { // the object k1 finished. func Remove(k1 string) { onceMap.Remove(k1) - log.Tracef("Removed key %s from onceMap (len=%d)", k1, len(onceMap.Keys())) + log.Tracef("[Once] Removed key %s from onceMap (len=%d)", k1, len(onceMap.Keys())) } diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/inline_faucet.go index 49d8feca..e0e34972 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/inline_faucet.go @@ -222,7 +222,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { results[i].SetResultID(inlineFaucet.ID) bot.Cache.Set(inlineFaucet.ID, inlineFaucet, &store.Options{Expiration: 5 * time.Minute}) - log.Infof("[faucet] %s created inline faucet %s: %d sat (%d per user)", GetUserStr(inlineFaucet.From.Telegram), inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) + log.Infof("[faucet] %s:%d created inline faucet %s: %d sat (%d per user)", GetUserStr(inlineFaucet.From.Telegram), inlineFaucet.From.Telegram.ID, inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) } err = bot.Telegram.Answer(q, &tb.QueryResponse{ @@ -266,7 +266,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback for _, a := range inlineFaucet.To { if a.Telegram.ID == to.Telegram.ID { // to user is already in To slice, has taken from facuet - log.Debugf("[faucet] %s already took from faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) + log.Debugf("[faucet] %s:%d already took from faucet %s", GetUserStr(to.Telegram), to.Telegram.ID, inlineFaucet.ID) return } } @@ -310,7 +310,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback return } - log.Infof("[💸 faucet] Faucet %s from %s to %s (%d sat).", inlineFaucet.ID, fromUserStr, toUserStr, inlineFaucet.PerUserAmount) + log.Infof("[💸 faucet] Faucet %s from %s to %s:%d (%d sat).", inlineFaucet.ID, fromUserStr, toUserStr, to.Telegram.ID, inlineFaucet.PerUserAmount) inlineFaucet.NTaken += 1 inlineFaucet.To = append(inlineFaucet.To, to) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount From cbedc5c14725d28ea7c5c596fb9ef79810244825 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Dec 2021 03:05:25 +0100 Subject: [PATCH 147/541] Balance button (#236) * speed up tx lock * yolo master * blanace button Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/balance.go | 7 ++++-- internal/telegram/buttons.go | 2 +- internal/telegram/send.go | 5 +++- internal/telegram/telegram.go | 40 ++++++++++++++++++++++++------- internal/telegram/text.go | 10 ++++++++ internal/telegram/transaction.go | 41 ++++++++++++++++++++++---------- internal/telegram/users.go | 2 +- 7 files changed, 81 insertions(+), 26 deletions(-) diff --git a/internal/telegram/balance.go b/internal/telegram/balance.go index 9850fae6..8c8b85cd 100644 --- a/internal/telegram/balance.go +++ b/internal/telegram/balance.go @@ -9,9 +9,12 @@ import ( tb "gopkg.in/lightningtipbot/telebot.v2" ) -func (bot TipBot) balanceHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) balanceHandler(ctx context.Context, m *tb.Message) { // check and print all commands - bot.anyTextHandler(ctx, m) + if len(m.Text) > 0 { + bot.anyTextHandler(ctx, m) + } + // reply only in private message if m.Chat.Type != tb.ChatPrivate { // delete message diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index ed1ee72a..d7d9594c 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -5,7 +5,7 @@ import tb "gopkg.in/lightningtipbot/telebot.v2" // we can't use space in the label of buttons, because string splitting will mess everything up. const ( CommandSend = "💸 Send" - CommandBalance = "👑 Balance" + CommandBalance = "Balance" CommandInvoice = "⚡️ Invoice" CommandHelp = "📖 Help" ) diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 973cff77..93758cc8 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -283,7 +283,10 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { // bot.trySendMessage(from.Telegram, fmt.Sprintf(Translate(ctx, "sendSentMessage"), amount, toUserStrMd)) if c.Message.Private() { // if the command was invoked in private chat - bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd), &tb.ReplyMarkup{}) + // the edit below was cool, but we need to get rid of the replymarkup inline keyboard thingy for the main menu button update to work (for the new balance) + // bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd), &tb.ReplyMarkup{}) + bot.tryDeleteMessage(c.Message) + bot.trySendMessage(c.Sender, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) } else { // if the command was invoked in group chat bot.trySendMessage(c.Sender, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 144dee42..3ab9535f 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -5,6 +5,7 @@ import ( "strconv" "time" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/rate" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" @@ -13,7 +14,30 @@ import ( // appendMainMenu will check if to (recipient) ID is from private or group chat. // this function will only add a keyboard if this is a private chat and no force reply. -func appendMainMenu(to int64, options []interface{}) []interface{} { +func (bot *TipBot) appendMainMenu(to int64, recipient interface{}, options []interface{}) []interface{} { + + var user *lnbits.User + var err error + if user, err = getCachedUser(&tb.User{ID: to}, *bot); err != nil { + user, err = GetLnbitsUser(&tb.User{ID: to}, *bot) + if err != nil { + return options + } + updateCachedUser(user, *bot) + } + if user.Wallet != nil { + amount, err := bot.GetUserBalanceCached(user) + if err == nil { + log.Infof("[appendMainMenu] user %s balance %d sat", GetUserStr(user.Telegram), amount) + CommandBalance := fmt.Sprintf("%s %d sat", CommandBalance, amount) + btnBalanceMainMenu = mainMenu.Text(CommandBalance) + mainMenu.Reply( + mainMenu.Row(btnBalanceMainMenu), + mainMenu.Row(btnInvoiceMainMenu, btnSendMainMenu, btnHelpMainMenu), + ) + } + } + appendKeyboard := true for _, option := range options { if option == tb.ForceReply { @@ -36,7 +60,7 @@ func appendMainMenu(to int64, options []interface{}) []interface{} { } // getChatIdFromRecipient will parse the recipient to int64 -func getChatIdFromRecipient(to tb.Recipient) (int64, error) { +func (bot *TipBot) getChatIdFromRecipient(to tb.Recipient) (int64, error) { chatId, err := strconv.ParseInt(to.Recipient(), 10, 64) if err != nil { return 0, err @@ -47,12 +71,12 @@ func getChatIdFromRecipient(to tb.Recipient) (int64, error) { func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options ...interface{}) (msg *tb.Message) { rate.CheckLimit(to) // ChatId is used for the keyboard - chatId, err := getChatIdFromRecipient(to) + chatId, err := bot.getChatIdFromRecipient(to) if err != nil { log.Errorf("[tryForwardMessage] error converting message recipient to int64: %v", err) return } - msg, err = bot.Telegram.Forward(to, what, appendMainMenu(chatId, options)...) + msg, err = bot.Telegram.Forward(to, what, bot.appendMainMenu(chatId, to, options)...) if err != nil { log.Warnln(err.Error()) } @@ -61,12 +85,12 @@ func (bot TipBot) tryForwardMessage(to tb.Recipient, what tb.Editable, options . func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...interface{}) (msg *tb.Message) { rate.CheckLimit(to) // ChatId is used for the keyboard - chatId, err := getChatIdFromRecipient(to) + chatId, err := bot.getChatIdFromRecipient(to) if err != nil { log.Errorf("[trySendMessage] error converting message recipient to int64: %v", err) return } - msg, err = bot.Telegram.Send(to, what, appendMainMenu(chatId, options)...) + msg, err = bot.Telegram.Send(to, what, bot.appendMainMenu(chatId, to, options)...) if err != nil { log.Warnln(err.Error()) } @@ -75,7 +99,7 @@ func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...i func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...interface{}) (msg *tb.Message) { rate.CheckLimit(to) - msg, err := bot.Telegram.Reply(to, what, appendMainMenu(to.Chat.ID, options)...) + msg, err := bot.Telegram.Reply(to, what, bot.appendMainMenu(to.Chat.ID, to, options)...) if err != nil { log.Warnln(err.Error()) } @@ -86,7 +110,7 @@ func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...in rate.CheckLimit(to) var err error _, chatId := to.MessageSig() - msg, err = bot.Telegram.Edit(to, what, appendMainMenu(chatId, options)...) + msg, err = bot.Telegram.Edit(to, what, bot.appendMainMenu(chatId, to, options)...) if err != nil { log.Warnln(err.Error()) } diff --git a/internal/telegram/text.go b/internal/telegram/text.go index f8ce6de3..acd70cac 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -24,6 +24,16 @@ func (bot *TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { return } + // check if the user clicked on the balance button + if strings.HasPrefix(m.Text, CommandBalance) { + bot.tryDeleteMessage(m) + // overwrite the message text so it doesn't cause an infinite loop + // because balanceHandler calls anyTextHAndler... + m.Text = "" + bot.balanceHandler(ctx, m) + return + } + // could be an invoice anyText := strings.ToLower(m.Text) if lightning.IsInvoice(anyText) { diff --git a/internal/telegram/transaction.go b/internal/telegram/transaction.go index c38aa87f..6493a01b 100644 --- a/internal/telegram/transaction.go +++ b/internal/telegram/transaction.go @@ -91,19 +91,19 @@ func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits t.FromWallet = from.Wallet.ID t.FromLNbitsID = from.ID - // // check if fromUser has balance - // balance, err := bot.GetUserBalance(from) - // if err != nil { - // errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) - // log.Errorln(errmsg) - // return false, err - // } - // // check if fromUser has balance - // if balance < amount { - // errmsg := fmt.Sprintf("balance too low.") - // log.Warnf("Balance of user %s too low", fromUserStr) - // return false, fmt.Errorf(errmsg) - // } + // check if fromUser has balance + balance, err := bot.GetUserBalance(from) + if err != nil { + errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) + log.Errorln(errmsg) + return false, err + } + // check if fromUser has balance + if balance < amount { + errmsg := fmt.Sprintf("balance too low.") + log.Warnf("Balance of user %s too low", fromUserStr) + return false, fmt.Errorf(errmsg) + } t.ToWallet = to.ID t.ToLNbitsID = to.ID @@ -128,5 +128,20 @@ func (t *Transaction) SendTransaction(bot *TipBot, from *lnbits.User, to *lnbits log.Warnf(errmsg) return false, err } + + // check if fromUser has balance + _, err = bot.GetUserBalance(from) + if err != nil { + errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) + log.Errorln(errmsg) + return false, err + } + _, err = bot.GetUserBalance(to) + if err != nil { + errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) + log.Errorln(errmsg) + return false, err + } + return true, err } diff --git a/internal/telegram/users.go b/internal/telegram/users.go index be9ed7d1..944db0d9 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -89,7 +89,7 @@ func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int64, err error) { bot.Cache.Set( fmt.Sprintf("%s_balance", user.Name), amount, - &store.Options{Expiration: 30 * time.Second}, + &store.Options{Expiration: 1 * time.Hour}, ) return } From 6851badfaba649b6be13b9a12dd81f7ae681c419 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Dec 2021 03:30:41 +0100 Subject: [PATCH 148/541] Balance on invoice (#237) * speed up tx lock * yolo master * balance check on invoice Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/invoice.go | 7 +++++++ internal/telegram/pay.go | 13 ++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 9468ef95..1be9ce1b 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -157,6 +157,13 @@ func (bot *TipBot) createInvoiceWithEvent(ctx context.Context, user *lnbits.User } func (bot *TipBot) notifyInvoiceReceivedEvent(invoiceEvent *InvoiceEvent) { + // do balance check for keyboard update + _, err := bot.GetUserBalance(invoiceEvent.User) + if err != nil { + errmsg := fmt.Sprintf("could not get balance of user %s", GetUserStr(invoiceEvent.User.Telegram)) + log.Errorln(errmsg) + } + bot.trySendMessage(invoiceEvent.User.Telegram, fmt.Sprintf(i18n.Translate(invoiceEvent.User.Telegram.LanguageCode, "invoiceReceivedMessage"), invoiceEvent.Amount)) } diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 3a265446..f78c2bc5 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -215,9 +215,20 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { } payData.Hash = invoice.PaymentHash + // do balance check for keyboard update + _, err = bot.GetUserBalance(user) + if err != nil { + errmsg := fmt.Sprintf("could not get balance of user %s", userStr) + log.Errorln(errmsg) + } + if c.Message.Private() { // if the command was invoked in private chat - bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) + // if the command was invoked in private chat + // the edit below was cool, but we need to get rid of the replymarkup inline keyboard thingy for the main menu button update to work (for the new balance) + // bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) + bot.tryDeleteMessage(c.Message) + bot.trySendMessage(c.Sender, i18n.Translate(payData.LanguageCode, "invoicePaidMessage")) } else { // if the command was invoked in group chat bot.trySendMessage(c.Sender, i18n.Translate(payData.LanguageCode, "invoicePaidMessage")) From 24e4212a934ec59af67927b55e83dc4bf43fc29b Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 26 Dec 2021 14:20:50 +0100 Subject: [PATCH 149/541] add mutex http endpoints (#238) --- internal/runtime/mutex/mutex.go | 15 +++++++++++++++ main.go | 8 +++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 33bf9574..77e42181 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -3,6 +3,7 @@ package mutex import ( "context" "fmt" + "net/http" "sync" cmap "github.com/orcaman/concurrent-map" @@ -15,6 +16,20 @@ func init() { mutexMap = cmap.New() } +func ServeHTTP(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(fmt.Sprintf("Current number of locks: %d\nLocks: %+v\nUse /mutex/unlock endpoint to unlock all users", len(mutexMap.Keys()), mutexMap.Keys()))) +} + +func UnlockHTTP(w http.ResponseWriter, r *http.Request) { + var i []string + for k, m := range mutexMap.Items() { + i = append(i, k) + m.(*sync.Mutex).Unlock() + } + w.Write([]byte(fmt.Sprintf("Unlocked %d mutexes. \n%+v\nCurrent number of locks: %d\nLocks: %+v", + len(i), i, len(mutexMap.Keys()), mutexMap.Keys()))) +} + // checkSoftLock checks in mutexMap how often an existing mutex was already SoftLocked. // The counter is there to avoid multiple recursive locking of an object in the mutexMap. // This happens if multiple handlers call each other and try to lock/unlock multiple times diff --git a/main.go b/main.go index b0985e73..f37a34ad 100644 --- a/main.go +++ b/main.go @@ -1,6 +1,8 @@ package main import ( + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/gorilla/mux" "net/http" "runtime/debug" @@ -24,7 +26,11 @@ func setLogger() { func main() { // set logger setLogger() - go http.ListenAndServe("0.0.0.0:6060", nil) + router := mux.NewRouter() + router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux) + router.Handle("/mutex", http.HandlerFunc(mutex.ServeHTTP)) + router.Handle("/mutex/unlock", http.HandlerFunc(mutex.UnlockHTTP)) + go http.ListenAndServe("0.0.0.0:6060", router) defer withRecovery() bot := telegram.NewBot() webhook.NewServer(&bot) From 2ef798e56a4a7739815d9f618ed8dc3a07837df0 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Dec 2021 14:46:09 +0100 Subject: [PATCH 150/541] Faucet balance update (#239) * speed up tx lock * yolo master * balance check faucet * limit reset Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/rate/limiter.go | 2 +- internal/telegram/buttons.go | 80 ++++++++++++++++--- .../telegram/{inline_faucet.go => faucet.go} | 8 +- internal/telegram/inline_receive.go | 4 +- internal/telegram/inline_send.go | 4 +- internal/telegram/inline_tipjar.go | 4 +- internal/telegram/lnurl.go | 2 +- internal/telegram/pay.go | 1 - internal/telegram/start.go | 2 +- internal/telegram/telegram.go | 48 ----------- internal/telegram/text.go | 2 +- internal/telegram/tip.go | 6 +- 12 files changed, 84 insertions(+), 79 deletions(-) rename internal/telegram/{inline_faucet.go => faucet.go} (96%) diff --git a/internal/rate/limiter.go b/internal/rate/limiter.go index 05f3e725..6a0d63aa 100644 --- a/internal/rate/limiter.go +++ b/internal/rate/limiter.go @@ -22,7 +22,7 @@ var globalLimiter *rate.Limiter // NewLimiter creates both chat and global rate limiters. func Start() { - idLimiter = newIdRateLimiter(rate.Limit(0.25), 20) + idLimiter = newIdRateLimiter(rate.Limit(0.29), 19) globalLimiter = rate.NewLimiter(rate.Limit(30), 30) } diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index d7d9594c..52975233 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -1,21 +1,27 @@ package telegram -import tb "gopkg.in/lightningtipbot/telebot.v2" +import ( + "fmt" + + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + log "github.com/sirupsen/logrus" + tb "gopkg.in/lightningtipbot/telebot.v2" +) // we can't use space in the label of buttons, because string splitting will mess everything up. const ( - CommandSend = "💸 Send" - CommandBalance = "Balance" - CommandInvoice = "⚡️ Invoice" - CommandHelp = "📖 Help" + MainMenuCommandSend = "💸 Send" + MainMenuCommandBalance = "Balance" + MainMenuCommandInvoice = "⚡️ Invoice" + MainMenuCommandHelp = "📖 Help" ) var ( mainMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} - btnHelpMainMenu = mainMenu.Text(CommandHelp) - btnSendMainMenu = mainMenu.Text(CommandSend) - btnBalanceMainMenu = mainMenu.Text(CommandBalance) - btnInvoiceMainMenu = mainMenu.Text(CommandInvoice) + btnHelpMainMenu = mainMenu.Text(MainMenuCommandHelp) + btnSendMainMenu = mainMenu.Text(MainMenuCommandSend) + btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) + btnInvoiceMainMenu = mainMenu.Text(MainMenuCommandInvoice) ) func init() { @@ -45,3 +51,59 @@ func buttonWrapper(buttons []tb.Btn, markup *tb.ReplyMarkup, length int) []tb.Ro rows = append(rows, markup.Row(buttons...)) return rows } + +// mainMenuBalanceButtonUpdate updates the balance button in the mainMenu +func (bot *TipBot) mainMenuBalanceButtonUpdate(to int64) { + var user *lnbits.User + var err error + if user, err = getCachedUser(&tb.User{ID: to}, *bot); err != nil { + user, err = GetLnbitsUser(&tb.User{ID: to}, *bot) + if err != nil { + return + } + updateCachedUser(user, *bot) + } + if user.Wallet != nil { + amount, err := bot.GetUserBalanceCached(user) + if err == nil { + log.Infof("[appendMainMenu] user %s balance %d sat", GetUserStr(user.Telegram), amount) + MainMenuCommandBalance := fmt.Sprintf("%s %d sat", MainMenuCommandBalance, amount) + btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) + mainMenu.Reply( + mainMenu.Row(btnBalanceMainMenu), + mainMenu.Row(btnInvoiceMainMenu, btnSendMainMenu, btnHelpMainMenu), + ) + } + } +} + +// appendMainMenu will check if to (recipient) ID is from private or group chat. +// appendMainMenu is called in telegram.go every time a user receives a PM from the bot. +// this function will only add a keyboard if this is a private chat and no force reply. +func (bot *TipBot) appendMainMenu(to int64, recipient interface{}, options []interface{}) []interface{} { + + // update the balance button + if to > 0 { + bot.mainMenuBalanceButtonUpdate(to) + } + + appendKeyboard := true + for _, option := range options { + if option == tb.ForceReply { + appendKeyboard = false + } + switch option.(type) { + case *tb.ReplyMarkup: + appendKeyboard = false + //option.(*tb.ReplyMarkup).ReplyKeyboard = mainMenu.ReplyKeyboard + //if option.(*tb.ReplyMarkup).InlineKeyboard == nil { + // options = append(options[:i], options[i+1:]...) + //} + } + } + // to > 0 is private chats + if to > 0 && appendKeyboard { + options = append(options, mainMenu) + } + return options +} diff --git a/internal/telegram/inline_faucet.go b/internal/telegram/faucet.go similarity index 96% rename from internal/telegram/inline_faucet.go rename to internal/telegram/faucet.go index e0e34972..f871e759 100644 --- a/internal/telegram/inline_faucet.go +++ b/internal/telegram/faucet.go @@ -315,12 +315,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.To = append(inlineFaucet.To, to) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount go func() { - _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) - _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) - if err != nil { - errmsg := fmt.Errorf("[faucet] Error: Send message to %s: %s", toUserStr, err.Error()) - log.Warnln(errmsg) - } + bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) + bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) }() // build faucet message diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 9cad0009..83646ec5 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -329,8 +329,8 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac bot.tryEditMessage(inlineReceive.Message, inlineReceive.MessageText, &tb.ReplyMarkup{}) // notify users - _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, inlineReceive.Amount)) - _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), inlineReceive.Amount, toUserStrMd)) + bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, inlineReceive.Amount)) + bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), inlineReceive.Amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[acceptInlineReceiveHandler] Error: Receive message to %s: %s", toUserStr, err) log.Warnln(errmsg) diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index bc0d62fe..64b2e518 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -240,8 +240,8 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) } bot.tryEditMessage(c.Message, inlineSend.Message, &tb.ReplyMarkup{}) // notify users - _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) - _, err = bot.Telegram.Send(fromUser.Telegram, fmt.Sprintf(i18n.Translate(fromUser.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) + bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) + bot.trySendMessage(fromUser.Telegram, fmt.Sprintf(i18n.Translate(fromUser.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[sendInline] Error: Send message to %s: %s", toUserStr, err) log.Warnln(errmsg) diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/inline_tipjar.go index 6a71bee9..1aad55df 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/inline_tipjar.go @@ -286,8 +286,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback inlineTipjar.From = append(inlineTipjar.From, from) inlineTipjar.GivenAmount = inlineTipjar.GivenAmount + inlineTipjar.PerUserAmount - _, err = bot.Telegram.Send(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineTipjarReceivedMessage"), fromUserStrMd, inlineTipjar.PerUserAmount)) - _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineTipjarSentMessage"), inlineTipjar.PerUserAmount, toUserStrMd)) + bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineTipjarReceivedMessage"), fromUserStrMd, inlineTipjar.PerUserAmount)) + bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineTipjarSentMessage"), inlineTipjar.PerUserAmount, toUserStrMd)) if err != nil { errmsg := fmt.Errorf("[tipjar] Error: Send message to %s: %s", toUserStr, err) log.Warnln(errmsg) diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 01491e97..a9c0a8a0 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -143,7 +143,7 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { if err != nil { errmsg := fmt.Sprintf("[userLnurlHandler] Failed to get LNURL: %s", err.Error()) log.Errorln(errmsg) - bot.Telegram.Send(m.Sender, Translate(ctx, "lnurlNoUsernameMessage")) + bot.trySendMessage(m.Sender, Translate(ctx, "lnurlNoUsernameMessage")) } // create qr code qr, err := qrcode.Encode(lnurlEncode, qrcode.Medium, 256) diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index f78c2bc5..c496d5f4 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -223,7 +223,6 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { } if c.Message.Private() { - // if the command was invoked in private chat // if the command was invoked in private chat // the edit below was cool, but we need to get rid of the replymarkup inline keyboard thingy for the main menu button update to work (for the new balance) // bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) diff --git a/internal/telegram/start.go b/internal/telegram/start.go index c15555a3..baf6453e 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -25,7 +25,7 @@ func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) { // WILL RESULT IN AN ENDLESS LOOP OTHERWISE // bot.helpHandler(m) log.Printf("[⭐️ /start] New user: %s (%d)\n", GetUserStr(m.Sender), m.Sender.ID) - walletCreationMsg, err := bot.Telegram.Send(m.Sender, Translate(ctx, "startSettingWalletMessage")) + walletCreationMsg := bot.trySendMessage(m.Sender, Translate(ctx, "startSettingWalletMessage")) user, err := bot.initWallet(m.Sender) if err != nil { log.Errorln(fmt.Sprintf("[startHandler] Error with initWallet: %s", err.Error())) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 3ab9535f..d2a74448 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -5,60 +5,12 @@ import ( "strconv" "time" - "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/rate" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" ) -// appendMainMenu will check if to (recipient) ID is from private or group chat. -// this function will only add a keyboard if this is a private chat and no force reply. -func (bot *TipBot) appendMainMenu(to int64, recipient interface{}, options []interface{}) []interface{} { - - var user *lnbits.User - var err error - if user, err = getCachedUser(&tb.User{ID: to}, *bot); err != nil { - user, err = GetLnbitsUser(&tb.User{ID: to}, *bot) - if err != nil { - return options - } - updateCachedUser(user, *bot) - } - if user.Wallet != nil { - amount, err := bot.GetUserBalanceCached(user) - if err == nil { - log.Infof("[appendMainMenu] user %s balance %d sat", GetUserStr(user.Telegram), amount) - CommandBalance := fmt.Sprintf("%s %d sat", CommandBalance, amount) - btnBalanceMainMenu = mainMenu.Text(CommandBalance) - mainMenu.Reply( - mainMenu.Row(btnBalanceMainMenu), - mainMenu.Row(btnInvoiceMainMenu, btnSendMainMenu, btnHelpMainMenu), - ) - } - } - - appendKeyboard := true - for _, option := range options { - if option == tb.ForceReply { - appendKeyboard = false - } - switch option.(type) { - case *tb.ReplyMarkup: - appendKeyboard = false - //option.(*tb.ReplyMarkup).ReplyKeyboard = mainMenu.ReplyKeyboard - //if option.(*tb.ReplyMarkup).InlineKeyboard == nil { - // options = append(options[:i], options[i+1:]...) - //} - } - } - // to > 0 is private chats - if to > 0 && appendKeyboard { - options = append(options, mainMenu) - } - return options -} - // getChatIdFromRecipient will parse the recipient to int64 func (bot *TipBot) getChatIdFromRecipient(to tb.Recipient) (int64, error) { chatId, err := strconv.ParseInt(to.Recipient(), 10, 64) diff --git a/internal/telegram/text.go b/internal/telegram/text.go index acd70cac..bfccdc50 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -25,7 +25,7 @@ func (bot *TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { } // check if the user clicked on the balance button - if strings.HasPrefix(m.Text, CommandBalance) { + if strings.HasPrefix(m.Text, MainMenuCommandBalance) { bot.tryDeleteMessage(m) // overwrite the message text so it doesn't cause an infinite loop // because balanceHandler calls anyTextHAndler... diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index 070c443f..67a4accb 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -121,11 +121,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { log.Infof("[💸 tip] Tip from %s to %s (%d sat).", fromUserStr, toUserStr, amount) // notify users - _, err = bot.Telegram.Send(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "tipSentMessage"), amount, toUserStrMd)) - if err != nil { - errmsg := fmt.Errorf("[/tip] Error: Send message to %s: %s", toUserStr, err) - log.Warnln(errmsg) - } + bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "tipSentMessage"), amount, toUserStrMd)) // forward tipped message to user once if !messageHasTip { From 07ccc6cc2886d6b76e98e3dcdbd2668e9986c162 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Dec 2021 14:51:10 +0100 Subject: [PATCH 151/541] Unlock admin (#240) * add mutex http endpoints * remove mutex by id * remove mutex by id Co-authored-by: lngohumble --- internal/runtime/mutex/mutex.go | 12 +++++++----- main.go | 2 +- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 77e42181..b92bede3 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -3,6 +3,7 @@ package mutex import ( "context" "fmt" + "github.com/gorilla/mux" "net/http" "sync" @@ -21,13 +22,14 @@ func ServeHTTP(w http.ResponseWriter, r *http.Request) { } func UnlockHTTP(w http.ResponseWriter, r *http.Request) { - var i []string - for k, m := range mutexMap.Items() { - i = append(i, k) + vars := mux.Vars(r) + if m, ok := mutexMap.Get(vars["id"]); ok { m.(*sync.Mutex).Unlock() + w.Write([]byte(fmt.Sprintf("Unlocked %s mutexe.\nCurrent number of locks: %d\nLocks: %+v", + vars["id"], len(mutexMap.Keys()), mutexMap.Keys()))) + return } - w.Write([]byte(fmt.Sprintf("Unlocked %d mutexes. \n%+v\nCurrent number of locks: %d\nLocks: %+v", - len(i), i, len(mutexMap.Keys()), mutexMap.Keys()))) + w.Write([]byte(fmt.Sprintf("Mutex %s not found!", vars["id"]))) } // checkSoftLock checks in mutexMap how often an existing mutex was already SoftLocked. diff --git a/main.go b/main.go index f37a34ad..960e5ab3 100644 --- a/main.go +++ b/main.go @@ -29,7 +29,7 @@ func main() { router := mux.NewRouter() router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux) router.Handle("/mutex", http.HandlerFunc(mutex.ServeHTTP)) - router.Handle("/mutex/unlock", http.HandlerFunc(mutex.UnlockHTTP)) + router.Handle("/mutex/unlock/{id}", http.HandlerFunc(mutex.UnlockHTTP)) go http.ListenAndServe("0.0.0.0:6060", router) defer withRecovery() bot := telegram.NewBot() From 35d5ed8d8e8e7bf6c69b0dbaebff4be46d408abe Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 26 Dec 2021 15:12:18 +0100 Subject: [PATCH 152/541] fix nLocks (#241) --- internal/runtime/mutex/mutex.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index b92bede3..2757d649 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -79,8 +79,9 @@ func UnlockWithContext(ctx context.Context, s string) { } else { log.Tracef("[Mutex] Skip unlock (nLocks: %d)", nLocks) } + mutexMap.Remove(fmt.Sprintf("nLocks:%s", uid)) Unlock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) - mutexMap.Remove(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) + //mutexMap.Remove(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) } // Lock locks a mutex in the mutexMap. If the mutex is already in the map, it locks the current call. From c23d69e8f106d187790371f7ce8c61a7ac26ad44 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 26 Dec 2021 15:38:26 +0100 Subject: [PATCH 153/541] using shop.base.id (#242) * using shop.base.id * using shop.base.id * using shop.base.id --- internal/telegram/shop.go | 2 -- internal/telegram/shop_helpers.go | 2 -- 2 files changed, 4 deletions(-) diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 5faa2e6d..49294a66 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -44,7 +44,6 @@ type ShopItem struct { type Shop struct { *storage.Base - ID string `json:"ID"` // holds the ID of the tx object in bunt db Owner *lnbits.User `json:"owner"` // owner of the shop Type string `json:"Type"` // type of the shop Title string `json:"title"` // Title of the item @@ -58,7 +57,6 @@ type Shop struct { type Shops struct { *storage.Base - ID string `json:"ID"` // holds the ID of the tx object in bunt db Owner *lnbits.User `json:"owner"` // owner of the shop Shops []string `json:"shop"` // MaxShops int `json:"maxShops"` diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index cdf112ca..dc1d0b4f 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -230,7 +230,6 @@ func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops id := fmt.Sprintf("shops-%d", user.Telegram.ID) shops := &Shops{ Base: storage.New(storage.ID(id)), - ID: id, Owner: user, Shops: []string{}, MaxShops: MAX_SHOPS, @@ -262,7 +261,6 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e shopId := fmt.Sprintf("shop-%s", RandStringRunes(10)) shop := &Shop{ Base: storage.New(storage.ID(shopId)), - ID: shopId, Title: fmt.Sprintf("Shop %d (%s)", len(shops.Shops)+1, shopId), Owner: user, Type: "photo", From fc7247c72a616894fc2655a6ce0460d96d685196 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Dec 2021 16:30:27 +0100 Subject: [PATCH 154/541] Adjust log levels (#244) * speed up tx lock * yolo master * better log Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/runtime/once/once.go | 2 +- internal/telegram/buttons.go | 2 +- internal/telegram/database.go | 2 +- internal/telegram/faucet.go | 10 ++++++++-- main.go | 10 ++++++---- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/internal/runtime/once/once.go b/internal/runtime/once/once.go index 547fdc1d..23533745 100644 --- a/internal/runtime/once/once.go +++ b/internal/runtime/once/once.go @@ -35,7 +35,7 @@ func Once(k1, k2 string) error { // 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) + return fmt.Errorf("[Once] %s already consumed object", k2) } objectMap.Set(k2, true) return nil diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index 52975233..822606b2 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -66,7 +66,7 @@ func (bot *TipBot) mainMenuBalanceButtonUpdate(to int64) { if user.Wallet != nil { amount, err := bot.GetUserBalanceCached(user) if err == nil { - log.Infof("[appendMainMenu] user %s balance %d sat", GetUserStr(user.Telegram), amount) + log.Debugf("[appendMainMenu] user %s balance %d sat", GetUserStr(user.Telegram), amount) MainMenuCommandBalance := fmt.Sprintf("%s %d sat", MainMenuCommandBalance, amount) btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) mainMenu.Reply( diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 8bdc98a7..9bc09a48 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -163,7 +163,7 @@ func UpdateUserRecord(user *lnbits.User, bot TipBot) error { log.Errorln(errmsg) return tx.Error } - log.Debugf("[UpdateUserRecord] Records of user %s updated.", GetUserStr(user.Telegram)) + log.Tracef("[UpdateUserRecord] Records of user %s updated.", GetUserStr(user.Telegram)) if bot.Cache.GoCacheStore != nil { updateCachedUser(user, bot) } diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index f871e759..041c900d 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -192,8 +192,14 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { return } fromUserStr := GetUserStr(m.Sender) - bot.trySendMessage(m.Chat, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) + mFaucet := bot.trySendMessage(m.Chat, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) log.Infof("[faucet] %s created faucet %s: %d sat (%d per user)", fromUserStr, inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) + + // log faucet link if possible + if mFaucet != nil && mFaucet.Chat != nil { + log.Infof("[faucet] Link: https://t.me/c/%s/%d", strconv.FormatInt(mFaucet.Chat.ID, 10)[4:], mFaucet.ID) + } + runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) } @@ -248,7 +254,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback from := inlineFaucet.From // log faucet link if possible if c.Message != nil && c.Message.Chat != nil { - log.Debugf("[faucet] Link: https://t.me/c/%s/%d", strconv.FormatInt(c.Message.Chat.ID, 10)[4:], c.Message.ID) + log.Infof("[faucet] Link: https://t.me/c/%s/%d", strconv.FormatInt(c.Message.Chat.ID, 10)[4:], c.Message.ID) } if !inlineFaucet.Active { log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) diff --git a/main.go b/main.go index 960e5ab3..62abd045 100644 --- a/main.go +++ b/main.go @@ -1,22 +1,24 @@ package main import ( - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" - "github.com/gorilla/mux" "net/http" "runtime/debug" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/gorilla/mux" + + _ "net/http/pprof" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits/webhook" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/price" "github.com/LightningTipBot/LightningTipBot/internal/telegram" log "github.com/sirupsen/logrus" - _ "net/http/pprof" ) // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.TraceLevel) + log.SetLevel(log.DebugLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true From 9c6062f31796bca7a559fd42cc21d9d7dd1fa495 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 26 Dec 2021 16:37:43 +0100 Subject: [PATCH 155/541] fix edit signature rate limiter (#234) --- internal/rate/limiter.go | 6 ++++++ internal/telegram/telegram.go | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/rate/limiter.go b/internal/rate/limiter.go index 6a0d63aa..74f5ff04 100644 --- a/internal/rate/limiter.go +++ b/internal/rate/limiter.go @@ -2,6 +2,7 @@ package rate import ( "context" + log "github.com/sirupsen/logrus" "strconv" "sync" @@ -42,6 +43,8 @@ func CheckLimit(to interface{}) { globalLimiter.Wait(context.Background()) var id string switch to.(type) { + case string: + id = to.(string) case *tb.Chat: id = strconv.FormatInt(to.(*tb.Chat).ID, 10) case *tb.User: @@ -54,8 +57,11 @@ func CheckLimit(to interface{}) { } } if len(id) > 0 { + log.Tracef("[Check Limit] limiter for %+v", id) idLimiter.GetLimiter(id).Wait(context.Background()) + return } + log.Tracef("[Check Limit] skipping id limiter for %+v", to) } // Add creates a new rate limiter and adds it to the keys map, diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index d2a74448..035b5649 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -59,7 +59,11 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i } func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message) { - rate.CheckLimit(to) + sig, chat := to.MessageSig() + if chat != 0 { + sig = strconv.FormatInt(chat, 10) + } + rate.CheckLimit(sig) var err error _, chatId := to.MessageSig() msg, err = bot.Telegram.Edit(to, what, bot.appendMainMenu(chatId, to, options)...) From 1e5f7b11692a81e19f244eb4b9cc6195980e4b03 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Dec 2021 16:46:04 +0100 Subject: [PATCH 156/541] Skip duplicate edits (#245) * speed up tx lock * yolo master * skip duplicate edit Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/telegram.go | 9 +++++++++ main.go | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 035b5649..5dee3c42 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -59,6 +59,15 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i } func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message) { + // do not attempt edit if the message did not change + switch to.(type) { + case *tb.Message: + if to.(*tb.Message).Text == what.(string) { + log.Tracef("[tryEditMessage] message did not change, not attempting to edit") + return + } + } + sig, chat := to.MessageSig() if chat != 0 { sig = strconv.FormatInt(chat, 10) diff --git a/main.go b/main.go index 62abd045..17016b74 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.DebugLevel) + log.SetLevel(log.TraceLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true From f1df1e6377817d834cecbca43fd60098a24ba570 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 26 Dec 2021 16:58:05 +0100 Subject: [PATCH 157/541] only remove on unlock (#246) --- internal/runtime/mutex/mutex.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 2757d649..201f58c5 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -76,10 +76,10 @@ func UnlockWithContext(ctx context.Context, s string) { mutexMap.Set(fmt.Sprintf("nLocks:%s", uid), nLocks) if nLocks == 0 { Unlock(s) + mutexMap.Remove(fmt.Sprintf("nLocks:%s", uid)) } else { log.Tracef("[Mutex] Skip unlock (nLocks: %d)", nLocks) } - mutexMap.Remove(fmt.Sprintf("nLocks:%s", uid)) Unlock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) //mutexMap.Remove(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) } From 01423827b7c157d892762fd3a1ebda4cdac7e5e0 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 26 Dec 2021 20:46:56 +0100 Subject: [PATCH 158/541] Fix shop v1 (#243) * using shop.base.id * using shop.base.id * using shop.base.id * remove index from delete status messages * fix delete status messages --- internal/storage/base.go | 2 ++ internal/telegram/files.go | 2 +- internal/telegram/shop.go | 6 +++--- internal/telegram/shop_helpers.go | 15 ++++++--------- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/internal/storage/base.go b/internal/storage/base.go index 90b9b19e..dd3bcda3 100644 --- a/internal/storage/base.go +++ b/internal/storage/base.go @@ -1,6 +1,7 @@ package storage import ( + "github.com/LightningTipBot/LightningTipBot/internal/runtime" "time" "github.com/eko/gocache/store" @@ -87,5 +88,6 @@ func (tx *Base) Set(s Storable, db *DB) error { func (tx *Base) Delete(s Storable, db *DB) error { tx.UpdatedAt = time.Now() + runtime.IgnoreError(transactionCache.Delete(s.Key())) return db.Delete(s.Key(), s) } diff --git a/internal/telegram/files.go b/internal/telegram/files.go index ae456569..3f97ebdf 100644 --- a/internal/telegram/files.go +++ b/internal/telegram/files.go @@ -19,7 +19,7 @@ func (bot *TipBot) fileHandler(ctx context.Context, m *tb.Message) { ticker.Do(func() { ResetUserState(user, bot) // removing ticker asap done - bot.shopViewDeleteAllStatusMsgs(ctx, user, 0) + bot.shopViewDeleteAllStatusMsgs(ctx, user) runtime.RemoveTicker(user.ID) }) } else { diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 49294a66..8982881c 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -236,7 +236,7 @@ func (bot *TipBot) enterShopItemTitleHandler(ctx context.Context, m *tb.Message) bot.sendStatusMessageAndDelete(ctx, m.Sender, "🚫 Action cancelled.") go func() { time.Sleep(time.Duration(5) * time.Second) - bot.shopViewDeleteAllStatusMsgs(ctx, user, 1) + bot.shopViewDeleteAllStatusMsgs(ctx, user) }() return } @@ -1161,7 +1161,7 @@ func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Messa bot.sendStatusMessageAndDelete(ctx, m.Sender, "🚫 Action cancelled.") go func() { time.Sleep(time.Duration(5) * time.Second) - bot.shopViewDeleteAllStatusMsgs(ctx, user, 1) + bot.shopViewDeleteAllStatusMsgs(ctx, user) }() return } @@ -1339,7 +1339,7 @@ func (bot *TipBot) enterShopTitleHandler(ctx context.Context, m *tb.Message) { bot.sendStatusMessageAndDelete(ctx, m.Sender, "🚫 Action cancelled.") go func() { time.Sleep(time.Duration(5) * time.Second) - bot.shopViewDeleteAllStatusMsgs(ctx, user, 1) + bot.shopViewDeleteAllStatusMsgs(ctx, user) }() return } diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index dc1d0b4f..af226b41 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -160,26 +160,23 @@ func (bot *TipBot) getUserShopview(ctx context.Context, user *lnbits.User) (shop shopView = sv.(ShopView) return } -func (bot *TipBot) shopViewDeleteAllStatusMsgs(ctx context.Context, user *lnbits.User, start int) (shopView ShopView, err error) { +func (bot *TipBot) shopViewDeleteAllStatusMsgs(ctx context.Context, user *lnbits.User) (shopView ShopView, err error) { mutex.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]...) + deleteStatusMessages(shopView.StatusMessages, bot) + shopView.StatusMessages = make([]*tb.Message, 0) bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) - - deleteStatusMessages(start, statusMessages, bot) mutex.Unlock(fmt.Sprintf("shopview-delete-%d", user.Telegram.ID)) return } -func deleteStatusMessages(start int, messages []*tb.Message, bot *TipBot) { +func deleteStatusMessages(messages []*tb.Message, bot *TipBot) { // delete all status messages from telegram - for _, msg := range messages[start:] { + for _, msg := range messages { bot.tryDeleteMessage(msg) } } @@ -213,7 +210,7 @@ func (bot *TipBot) sendStatusMessageAndDelete(ctx context.Context, to tb.Recipie ticker := runtime.GetTicker(id, runtime.WithDuration(5*time.Second)) if !ticker.Started { ticker.Do(func() { - bot.shopViewDeleteAllStatusMsgs(ctx, user, 1) + bot.shopViewDeleteAllStatusMsgs(ctx, user) // removing ticker asap done runtime.RemoveTicker(id) }) From 25dc9d7b34c3ada9b0ad853258b17062fef1f57b Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 26 Dec 2021 20:57:38 +0100 Subject: [PATCH 159/541] add edit stack lifo (#231) * add edit stack lifo * remove log message * Finish the faucet (#232) * speed up tx lock * yolo master * finish the faucet when inactive * delete comments Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * add once + singleton click interceptor (#229) * speed up tx lock * yolo master * add once + singleton click interceptor * yoooo; * stuff Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * Log oncemap (#233) * speed up tx lock * yolo master * logging Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * Balance button (#236) * speed up tx lock * yolo master * blanace button Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * Balance on invoice (#237) * speed up tx lock * yolo master * balance check on invoice Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * add mutex http endpoints (#238) * Faucet balance update (#239) * speed up tx lock * yolo master * balance check faucet * limit reset Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * Unlock admin (#240) * add mutex http endpoints * remove mutex by id * remove mutex by id Co-authored-by: lngohumble * fix nLocks (#241) * using shop.base.id (#242) * using shop.base.id * using shop.base.id * using shop.base.id * Adjust log levels (#244) * speed up tx lock * yolo master * better log Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * fix edit signature rate limiter (#234) * Skip duplicate edits (#245) * speed up tx lock * yolo master * skip duplicate edit Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * only remove on unlock (#246) * add edit stack lifo * finish faucet corrected * improve edit stack * edit.go * uncomment faucet check Co-authored-by: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/bot.go | 1 + internal/telegram/edit.go | 73 +++++++++++++++++++++++++++++++++++ internal/telegram/faucet.go | 7 ++-- internal/telegram/shop.go | 10 ++--- internal/telegram/telegram.go | 11 +----- 5 files changed, 84 insertions(+), 18 deletions(-) create mode 100644 internal/telegram/edit.go diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 6dcec788..5640b532 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -89,6 +89,7 @@ func (bot *TipBot) Start() { if err != nil { log.Errorf("Could not initialize bot wallet: %s", err.Error()) } + bot.startEditWorker() bot.registerTelegramHandlers() initInvoiceEventCallbacks(bot) initializeStateCallbackMessage(bot) diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go new file mode 100644 index 00000000..91372858 --- /dev/null +++ b/internal/telegram/edit.go @@ -0,0 +1,73 @@ +package telegram + +import ( + "fmt" + cmap "github.com/orcaman/concurrent-map" + log "github.com/sirupsen/logrus" + tb "gopkg.in/lightningtipbot/telebot.v2" + "time" +) + +var editStack cmap.ConcurrentMap + +type edit struct { + to tb.Editable + what interface{} + options []interface{} + lastEdit time.Time + edited bool +} + +func init() { + editStack = cmap.New() +} + +const resultTrueError = "telebot: result is True" + +// startEditWorker will loop through the editStack and run tryEditMessage on not edited messages. +// if editFromStack is older than 5 seconds, editFromStack will be removed. +func (bot TipBot) startEditWorker() { + go func() { + for { + for _, k := range editStack.Keys() { + if e, ok := editStack.Get(k); ok { + editFromStack := e.(edit) + if !editFromStack.edited { + _, err := bot.tryEditMessage(editFromStack.to, editFromStack.what, editFromStack.options...) + if err != nil && err.Error() != resultTrueError { + return + } + log.Tracef("[startEditWorker] message from stack edited %+v", editFromStack) + editFromStack.lastEdit = time.Now() + editFromStack.edited = true + editStack.Set(k, editFromStack) + } else { + if editFromStack.lastEdit.Before(time.Now().Add(-(time.Duration(5) * time.Second))) { + log.Tracef("[startEditWorker] removing message edit from stack %+v", editFromStack) + editStack.Remove(k) + } + } + } + } + time.Sleep(time.Second) + } + }() + +} + +// tryEditStack will add the editable to the edit stack, if what (message) changed. +func (bot TipBot) tryEditStack(to tb.Editable, what interface{}, options ...interface{}) { + msgSig, chat := to.MessageSig() + var sig = fmt.Sprintf("%s-%d", msgSig, chat) + if e, ok := editStack.Get(sig); ok { + editFromStack := e.(edit) + + if editFromStack.what == what.(string) { + log.Tracef("[tryEditMessage] message did not change, not attempting to edit") + return + } + } + e := edit{options: options, what: what, to: to} + + editStack.Set(sig, e) +} diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 041c900d..72a2ce7f 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -340,9 +340,10 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // update the message if the faucet still has some sats left after this tx if inlineFaucet.RemainingAmount >= inlineFaucet.PerUserAmount { go func() { - bot.tryEditMessage(c.Message, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) + bot.tryEditStack(c.Message, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) }() } + } if inlineFaucet.RemainingAmount < inlineFaucet.PerUserAmount { // faucet is depleted @@ -363,7 +364,7 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor inlineFaucet := fn.(*InlineFaucet) if ignoreID || c.Sender.ID == inlineFaucet.From.Telegram.ID { - bot.tryEditMessage(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditStack(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false inlineFaucet.Canceled = true @@ -379,7 +380,7 @@ func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFauce if inlineFaucet.UserNeedsWallet { inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } - bot.tryEditMessage(c.Message, inlineFaucet.Message, &tb.ReplyMarkup{}) + bot.tryEditStack(c.Message, inlineFaucet.Message, &tb.ReplyMarkup{}) inlineFaucet.Active = false log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) once.Remove(inlineFaucet.ID) diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 8982881c..8a03dcb7 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -415,7 +415,7 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho if len(shop.Items) == 0 { no_items_message := "There are no items in this shop yet." if len(shopView.Message.Text) > 0 { - shopView.Message = bot.tryEditMessage(shopView.Message, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) + shopView.Message, _ = bot.tryEditMessage(shopView.Message, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) } else { bot.tryDeleteMessage(shopView.Message) shopView.Message = bot.trySendMessage(shopView.Message.Chat, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) @@ -434,14 +434,14 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho if item.TbPhoto != nil { if shopView.Message.Photo != nil { // can only edit photo messages with another photo - shopView.Message = bot.tryEditMessage(shopView.Message, item.TbPhoto, bot.shopMenu(ctx, shop, &item)) + shopView.Message, _ = bot.tryEditMessage(shopView.Message, item.TbPhoto, bot.shopMenu(ctx, shop, &item)) } else { // if editing failes bot.tryDeleteMessage(shopView.Message) shopView.Message = bot.trySendMessage(shopView.Message.Chat, item.TbPhoto, bot.shopMenu(ctx, shop, &item)) } } else if item.Title != "" { - shopView.Message = bot.tryEditMessage(shopView.Message, item.Title, bot.shopMenu(ctx, shop, &item)) + shopView.Message, _ = bot.tryEditMessage(shopView.Message, item.Title, bot.shopMenu(ctx, shop, &item)) if shopView.Message == nil { shopView.Message = bot.trySendMessage(shopView.Message.Chat, item.Title, bot.shopMenu(ctx, shop, &item)) } @@ -1011,7 +1011,7 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { if err == nil && !strings.HasPrefix(strings.Split(m.Text, " ")[0], "/shop") { // the user is returning to a shops view from a back button callback if shopView.Message.Photo == nil { - shopsMsg = bot.tryEditMessage(shopView.Message, ShopsText, bot.shopsMainMenu(ctx, shops)) + shopsMsg, _ = bot.tryEditMessage(shopView.Message, ShopsText, bot.shopsMainMenu(ctx, shops)) } if shopsMsg == nil { // if editing has failed, we will send a new message @@ -1278,7 +1278,7 @@ func (bot *TipBot) shopsBrowser(ctx context.Context, c *tb.Callback) { } shopShopsButton := shopKeyboard.Data("⬅️ Back", "shops_shops", shops.ID) shopKeyboard.Inline(buttonWrapper(append(bot.makseShopSelectionButtons(s, "select_shop"), shopShopsButton), shopKeyboard, 1)...) - shopMessage := bot.tryEditMessage(c.Message, "Select a shop you want to browse.", shopKeyboard) + shopMessage, _ := bot.tryEditMessage(c.Message, "Select a shop you want to browse.", shopKeyboard) shopView, err = bot.getUserShopview(ctx, user) if err != nil { shopView.Message = shopMessage diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 5dee3c42..e05291ef 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -58,29 +58,20 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i return } -func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message) { +func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message, err error) { // do not attempt edit if the message did not change - switch to.(type) { - case *tb.Message: - if to.(*tb.Message).Text == what.(string) { - log.Tracef("[tryEditMessage] message did not change, not attempting to edit") - return - } - } sig, chat := to.MessageSig() if chat != 0 { sig = strconv.FormatInt(chat, 10) } rate.CheckLimit(sig) - var err error _, chatId := to.MessageSig() msg, err = bot.Telegram.Edit(to, what, bot.appendMainMenu(chatId, to, options)...) if err != nil { log.Warnln(err.Error()) } return - } func (bot TipBot) tryDeleteMessage(msg tb.Editable) { From 9fe0d776c78b9b50e454e1b891e236407f663d4a Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 26 Dec 2021 23:37:46 +0100 Subject: [PATCH 160/541] Mini fixes123 (#247) * speed up tx lock * yolo master * fixes Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/buttons.go | 2 +- internal/telegram/edit.go | 5 +++-- main.go | 2 +- translations/en.toml | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index 822606b2..b54b7db8 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -66,7 +66,7 @@ func (bot *TipBot) mainMenuBalanceButtonUpdate(to int64) { if user.Wallet != nil { amount, err := bot.GetUserBalanceCached(user) if err == nil { - log.Debugf("[appendMainMenu] user %s balance %d sat", GetUserStr(user.Telegram), amount) + log.Tracef("[appendMainMenu] user %s balance %d sat", GetUserStr(user.Telegram), amount) MainMenuCommandBalance := fmt.Sprintf("%s %d sat", MainMenuCommandBalance, amount) btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) mainMenu.Reply( diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go index 91372858..12cef231 100644 --- a/internal/telegram/edit.go +++ b/internal/telegram/edit.go @@ -2,10 +2,11 @@ package telegram import ( "fmt" + "time" + cmap "github.com/orcaman/concurrent-map" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" - "time" ) var editStack cmap.ConcurrentMap @@ -49,7 +50,7 @@ func (bot TipBot) startEditWorker() { } } } - time.Sleep(time.Second) + time.Sleep(time.Millisecond * 100) } }() diff --git a/main.go b/main.go index 17016b74..d2988588 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.TraceLevel) + log.SetLevel(log.InfoLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true diff --git a/translations/en.toml b/translations/en.toml index bd76728a..f4d81fc0 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -275,7 +275,7 @@ inlineFaucetEndedMessage = """🚰 Faucet empty 🍺\n\n🏅 %d s inlineFaucetAppendMemo = """\n✉️ %s""" inlineFaucetCreateWalletMessage = """Chat with %s 👈 to manage your wallet.""" inlineFaucetCancelledMessage = """🚫 Faucet cancelled.""" -inlineFaucetInvalidPeruserAmountMessage = """🚫 Peruser amount not divisor of capacity.""" +inlineFaucetInvalidPeruserAmountMessage = """🚫 Peruser amount not divisor of capacity or too low (min 5 sat).""" inlineFaucetInvalidAmountMessage = """🚫 Invalid amount.""" inlineFaucetSentMessage = """🚰 %d sat sent to %s.""" inlineFaucetReceivedMessage = """🚰 %s sent you %d sat.""" From 95c733459d19b6a148035cb97168343c2512dba8 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 27 Dec 2021 18:28:06 +0100 Subject: [PATCH 161/541] Fix faucet edit (#248) * speed up tx lock * yolo master * better printing Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/edit.go | 4 +++- main.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go index 12cef231..ada10ad2 100644 --- a/internal/telegram/edit.go +++ b/internal/telegram/edit.go @@ -36,6 +36,7 @@ func (bot TipBot) startEditWorker() { if !editFromStack.edited { _, err := bot.tryEditMessage(editFromStack.to, editFromStack.what, editFromStack.options...) if err != nil && err.Error() != resultTrueError { + log.Tracef("[startEditWorker] Edit error: %s", err.Error()) return } log.Tracef("[startEditWorker] message from stack edited %+v", editFromStack) @@ -64,11 +65,12 @@ func (bot TipBot) tryEditStack(to tb.Editable, what interface{}, options ...inte editFromStack := e.(edit) if editFromStack.what == what.(string) { - log.Tracef("[tryEditMessage] message did not change, not attempting to edit") + log.Tracef("[tryEditStack] Message already in edit stack. Skipping") return } } e := edit{options: options, what: what, to: to} editStack.Set(sig, e) + log.Tracef("[tryEditStack] Added message %s to edit stack. len(editStack)=%d", sig, len(editStack.Keys())) } diff --git a/main.go b/main.go index d2988588..62abd045 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.InfoLevel) + log.SetLevel(log.DebugLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true From d65d6aad2876d9a473815b59347e7751422d1a10 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 27 Dec 2021 18:31:25 +0100 Subject: [PATCH 162/541] Fix faucet edit (#249) * speed up tx lock * yolo master * better printing * trace Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 62abd045..17016b74 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.DebugLevel) + log.SetLevel(log.TraceLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true From a9fecc1e8fd2396209ef7a963a9aa8a2b501f6ef Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 27 Dec 2021 18:48:58 +0100 Subject: [PATCH 163/541] Fix faucet again 123 (#250) * speed up tx lock * yolo master * skip edit error * do not return Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/edit.go | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go index ada10ad2..23a3c08f 100644 --- a/internal/telegram/edit.go +++ b/internal/telegram/edit.go @@ -2,6 +2,7 @@ package telegram import ( "fmt" + "strings" "time" cmap "github.com/orcaman/concurrent-map" @@ -24,6 +25,7 @@ func init() { } const resultTrueError = "telebot: result is True" +const editSameStringError = "specified new message content and reply markup are exactly the same as a current content and reply markup of the message" // startEditWorker will loop through the editStack and run tryEditMessage on not edited messages. // if editFromStack is older than 5 seconds, editFromStack will be removed. @@ -35,14 +37,19 @@ func (bot TipBot) startEditWorker() { editFromStack := e.(edit) if !editFromStack.edited { _, err := bot.tryEditMessage(editFromStack.to, editFromStack.what, editFromStack.options...) - if err != nil && err.Error() != resultTrueError { - log.Tracef("[startEditWorker] Edit error: %s", err.Error()) - return + if err != nil && err.Error() != resultTrueError && !strings.Contains(err.Error(), editSameStringError) { + // any other error should not be ignored + log.Tracef("[startEditWorker] Skip edit error: %s", err.Error()) + + } else { + if err != nil { + log.Tracef("[startEditWorker] Edit error: %s", err.Error()) + } + log.Tracef("[startEditWorker] message from stack edited %+v", editFromStack) + editFromStack.lastEdit = time.Now() + editFromStack.edited = true + editStack.Set(k, editFromStack) } - log.Tracef("[startEditWorker] message from stack edited %+v", editFromStack) - editFromStack.lastEdit = time.Now() - editFromStack.edited = true - editStack.Set(k, editFromStack) } else { if editFromStack.lastEdit.Before(time.Now().Add(-(time.Duration(5) * time.Second))) { log.Tracef("[startEditWorker] removing message edit from stack %+v", editFromStack) @@ -51,7 +58,7 @@ func (bot TipBot) startEditWorker() { } } } - time.Sleep(time.Millisecond * 100) + time.Sleep(time.Millisecond * 500) } }() @@ -63,7 +70,6 @@ func (bot TipBot) tryEditStack(to tb.Editable, what interface{}, options ...inte var sig = fmt.Sprintf("%s-%d", msgSig, chat) if e, ok := editStack.Get(sig); ok { editFromStack := e.(edit) - if editFromStack.what == what.(string) { log.Tracef("[tryEditStack] Message already in edit stack. Skipping") return From fdb58a7f2bd0ecb7cc94965ee19a47e88447a1bb Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 27 Dec 2021 19:13:07 +0100 Subject: [PATCH 164/541] Fix faucet again (#251) * speed up tx lock * yolo master * fix faucet again Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/edit.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go index 23a3c08f..6002f1d0 100644 --- a/internal/telegram/edit.go +++ b/internal/telegram/edit.go @@ -26,6 +26,7 @@ func init() { const resultTrueError = "telebot: result is True" const editSameStringError = "specified new message content and reply markup are exactly the same as a current content and reply markup of the message" +const retryAfterError = "retry after" // startEditWorker will loop through the editStack and run tryEditMessage on not edited messages. // if editFromStack is older than 5 seconds, editFromStack will be removed. @@ -37,22 +38,22 @@ func (bot TipBot) startEditWorker() { editFromStack := e.(edit) if !editFromStack.edited { _, err := bot.tryEditMessage(editFromStack.to, editFromStack.what, editFromStack.options...) - if err != nil && err.Error() != resultTrueError && !strings.Contains(err.Error(), editSameStringError) { - // any other error should not be ignored - log.Tracef("[startEditWorker] Skip edit error: %s", err.Error()) + if err != nil && strings.Contains(err.Error(), retryAfterError) { + // ignore any other error than retry after + log.Errorf("[startEditWorker] Edit error: %s. len(editStack)=%d", err.Error(), len(editStack.Keys())) } else { if err != nil { - log.Tracef("[startEditWorker] Edit error: %s", err.Error()) + log.Errorf("[startEditWorker] Ignoring edit error: %s. len(editStack)=%d", err.Error(), len(editStack.Keys())) } - log.Tracef("[startEditWorker] message from stack edited %+v", editFromStack) + log.Tracef("[startEditWorker] message from stack edited %+v. len(editStack)=%d", editFromStack, len(editStack.Keys())) editFromStack.lastEdit = time.Now() editFromStack.edited = true editStack.Set(k, editFromStack) } } else { if editFromStack.lastEdit.Before(time.Now().Add(-(time.Duration(5) * time.Second))) { - log.Tracef("[startEditWorker] removing message edit from stack %+v", editFromStack) + log.Tracef("[startEditWorker] removing message edit from stack %+v. len(editStack)=%d", editFromStack, len(editStack.Keys())) editStack.Remove(k) } } From 3d3d7ab2f6ac249eaeb3e9817743c6d472c22d4b Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 28 Dec 2021 12:20:47 +0100 Subject: [PATCH 165/541] Send keyboard recommendation (#252) * speed up tx lock * yolo master * keyboard with recent contacts * translate message * better query * keyboard pops up * log level * skip keyboard if no entry Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/buttons.go | 30 ++++++++++++++++++++++++ internal/telegram/handler.go | 19 ++++++++++++++- internal/telegram/pay.go | 5 +++- internal/telegram/send.go | 45 ++++++++++++++++++++++++++++++++++-- internal/telegram/text.go | 2 +- internal/telegram/users.go | 2 +- main.go | 2 +- 7 files changed, 98 insertions(+), 7 deletions(-) diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index b54b7db8..57982ee2 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -1,6 +1,7 @@ package telegram import ( + "context" "fmt" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -14,6 +15,7 @@ const ( MainMenuCommandBalance = "Balance" MainMenuCommandInvoice = "⚡️ Invoice" MainMenuCommandHelp = "📖 Help" + SendMenuCommandEnter = "👤 Enter" ) var ( @@ -22,6 +24,10 @@ var ( btnSendMainMenu = mainMenu.Text(MainMenuCommandSend) btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) btnInvoiceMainMenu = mainMenu.Text(MainMenuCommandInvoice) + + sendToMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + sendToButtons = []tb.Btn{} + btnSendMenuEnter = mainMenu.Text(SendMenuCommandEnter) ) func init() { @@ -77,6 +83,30 @@ func (bot *TipBot) mainMenuBalanceButtonUpdate(to int64) { } } +// makeContactsButtons will create a slice of buttons for the send menu +// it will show the 5 most recently interacted contacts and one button to use a custom contact +func (bot *TipBot) makeContactsButtons(ctx context.Context) []tb.Btn { + var records []Transaction + + sendToButtons = []tb.Btn{} + user := LoadUser(ctx) + // get 5 most recent transactions by from_id with distint to_user + // where to_user starts with an @ and is not the user itself + bot.logger.Where("from_id = ? AND to_user LIKE ? AND to_user <> ?", user.Telegram.ID, "@%", GetUserStr(user.Telegram)).Distinct("to_user").Order("id desc").Limit(5).Find(&records) + log.Debugf("[makeContactsButtons] found %d records", len(records)) + + // get all contacts and add them to the buttons + for i, r := range records { + log.Tracef("[makeContactsButtons] toNames[%d] = %s (id=%d)", i, r.ToUser, r.ID) + sendToButtons = append(sendToButtons, tb.Btn{Text: fmt.Sprintf("%s", r.ToUser)}) + } + + // add the "enter a username" button to the end + sendToButtons = append(sendToButtons, tb.Btn{Text: SendMenuCommandEnter}) + sendToMenu.Reply(buttonWrapper(sendToButtons, sendToMenu, 3)...) + return sendToButtons +} + // appendMainMenu will check if to (recipient) ID is from private or group chat. // appendMainMenu is called in telegram.go every time a user receives a PM from the bot. // this function will only add a keyboard if this is a private chat and no force reply. diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index d36279ea..d9dc0f56 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -216,7 +216,7 @@ func (bot TipBot) getHandler() []Handler { }, }, { - Endpoints: []interface{}{"/send", &btnSendMainMenu}, + Endpoints: []interface{}{"/send", &btnSendMenuEnter}, Handler: bot.sendHandler, Interceptor: &Interceptor{ Type: MessageInterceptor, @@ -232,6 +232,23 @@ func (bot TipBot) getHandler() []Handler { }, }, }, + { + Endpoints: []interface{}{&btnSendMainMenu}, + Handler: bot.keyboardSendHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.logMessageInterceptor, + bot.requireUserInterceptor, + bot.loadReplyToInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, { Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan", "/grifo"}, Handler: bot.faucetHandler, diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index c496d5f4..204a5dd1 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -256,6 +256,9 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { if payData.From.Telegram.ID != c.Sender.ID { return } - bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) + // delete and send instead of edit for the keyboard to pop up after sending + bot.tryDeleteMessage(c.Message) + bot.trySendMessage(c.Message.Chat, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage")) + // bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) payData.Inactivate(payData, bot.Bunt) } diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 93758cc8..19b719f9 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -214,6 +214,46 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { } } +// keyboardSendHandler will be called when the user presses the Send button on the keyboard +// it will pop up a new keyboard with the last interacted contacts to send funds to +// then, the flow is handled as if the user entered /send (then ask for contacts from keyboard or entry, +// then ask for an amount). +func (bot *TipBot) keyboardSendHandler(ctx context.Context, m *tb.Message) { + user := LoadUser(ctx) + if user.Wallet == nil { + return // errors.New("user has no wallet"), 0 + } + enterUserStateData := &EnterUserStateData{ + ID: "id", + Type: "CreateSendState", + OiringalCommand: "/send", + } + // set LNURLPayParams in the state of the user + stateDataJson, err := json.Marshal(enterUserStateData) + if err != nil { + log.Errorln(err) + return + } + SetUserState(user, bot, lnbits.UserEnterUser, string(stateDataJson)) + sendToButtons = bot.makeContactsButtons(ctx) + + // if no contact is found (one entry will always be inside, it's the enter user button) + // immediatelly go to the send handler + if len(sendToButtons) == 1 { + m.Text = "/send" + bot.sendHandler(ctx, m) + return + } + + // Attention! We need to ues the original Telegram.Send command here! + // bot.trySendMessage will replace the keyboard with the default one and we want to send a different keyboard here + // this is suboptimal because Telegram.Send is not rate limited etc. but it's the only way to send a custom keyboard for now + _, err = bot.Telegram.Send(user.Telegram, Translate(ctx, "enterUserMessage"), sendToMenu) + if err != nil { + log.Errorln(err.Error()) + } +} + // sendHandler invoked when user clicked send on payment confirmation func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { tx := &SendData{Base: storage.New(storage.ID(c.Data))} @@ -319,7 +359,8 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { if sendData.From.Telegram.ID != c.Sender.ID { return } - // remove buttons from confirmation message - bot.tryEditMessage(c.Message, i18n.Translate(sendData.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) + // delete and send instead of edit for the keyboard to pop up after sending + bot.tryDeleteMessage(c.Message) + bot.trySendMessage(c.Message.Chat, i18n.Translate(sendData.LanguageCode, "sendCancelledMessage")) sendData.Inactivate(sendData, bot.Bunt) } diff --git a/internal/telegram/text.go b/internal/telegram/text.go index bfccdc50..7c14ce08 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -96,7 +96,7 @@ func (bot *TipBot) enterUserHandler(ctx context.Context, m *tb.Message) { ResetUserState(user, bot) return // errors.New("user state does not match"), 0 } - if len(m.Text) < 4 || strings.HasPrefix(m.Text, "/") { + if len(m.Text) < 4 || strings.HasPrefix(m.Text, "/") || m.Text == SendMenuCommandEnter { ResetUserState(user, bot) return } diff --git a/internal/telegram/users.go b/internal/telegram/users.go index 944db0d9..e71a9b00 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -83,7 +83,7 @@ func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int64, err error) { } // msat to sat amount = int64(wallet.Balance) / 1000 - log.Infof("[GetUserBalance] %s's balance: %d sat\n", GetUserStr(user.Telegram), amount) + log.Debugf("[GetUserBalance] %s's balance: %d sat\n", GetUserStr(user.Telegram), amount) // update user balance in cache bot.Cache.Set( diff --git a/main.go b/main.go index 17016b74..62abd045 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.TraceLevel) + log.SetLevel(log.DebugLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true From 1459fcb68ef6f266ad9dfce7e7224a26eccb713e Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 28 Dec 2021 14:09:45 +0000 Subject: [PATCH 166/541] Fix link delete message (#253) * fix Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/link.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/link.go b/internal/telegram/link.go index 118e81eb..1e93a2c6 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -51,8 +51,8 @@ func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { go func() { time.Sleep(time.Second * 60) bot.tryDeleteMessage(linkmsg) + bot.trySendMessage(m.Sender, Translate(ctx, "linkHiddenMessage")) }() - bot.trySendMessage(m.Sender, Translate(ctx, "linkHiddenMessage")) // auto delete the message // NewMessage(linkmsg, WithDuration(time.Second*time.Duration(internal.Configuration.Telegram.MessageDisposeDuration), bot)) From 113fc02b28887a44da325009a238a8bf786c49c7 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 29 Dec 2021 10:04:17 +0000 Subject: [PATCH 167/541] Faucet problem 123 (#254) * speed up tx lock * yolo master * logging Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/faucet.go | 2 +- main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 72a2ce7f..19883656 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -247,7 +247,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { - log.Debugf("[acceptInlineFaucetHandler] %s", err.Error()) + log.Errorf("[acceptInlineFaucetHandler] c.Data: %s, Error: %s", c.Data, err.Error()) return } inlineFaucet := fn.(*InlineFaucet) diff --git a/main.go b/main.go index 62abd045..17016b74 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.DebugLevel) + log.SetLevel(log.TraceLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true From d0da1560fac257d1a59b4a6a2d741492445ed085 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 29 Dec 2021 10:13:24 +0000 Subject: [PATCH 168/541] New logo (#255) * speed up tx lock * yolo master * upload new logo Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- resources/logo_round.png | Bin 22725 -> 39918 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/resources/logo_round.png b/resources/logo_round.png index c587840ef1164651d2d6ceeb99b6ce9f7471a90e..faf10b3c671b67734f2303b201a23289d69de1a2 100644 GIT binary patch literal 39918 zcmV(_K-9m9P)aY`oRH~e-UVb;7uxIUk(mwaSDwU+Fl5A_=_1;bA zo_o%@`@g^R|Gtmc$Lr(u@%nguygpvm5b1}OiaTDvf(s0hU!nzvLYZySduvpI&A>`v z6c|qamH^9>$0I-;L={v?Al&5RA<#q60p`5^x4MD1_ASn~r?H>2uX1A|i|;LVd47^A{xd=8yP!q@wsQ zy{Ty4zFuqNta{WOk^7oFx^JzaXU+!T9$*)+0$2rXOMp=rmjj%hi1W8o)*5Di(s~p> z)F65mII00e{Px9~QFXR^3` zf(HA)jPL6LvOhw zTpdcSihij;0SNXaFMi8>X_8ETF%Jk7|BGz@wh^D7iqh#k zek8B{(6+>Xn@Eh2;?Y=Bf1Ur3O)+zZ|A9y-$CO$Re}rwKLGq3a5D>91@k;b^iSdZ zz2eG@H4!NQgiMvXqD1He0-1D~51M{ykw4F0(=OZmeVOe;X5VEWTXM2WukDHmbx@)H zTp<2j8~h@1Y4aT-LB7)fqFt{)2K@5`BnzyJ^QDF;fTB2I#T6#3S>099wwO1~ZCgT! z!oC&)h$w)_mJlT0l}izOZ<(rONsr=w`fMo`JDCkg%;XMLq|~1()JGKREBX_!t61MY zV>|Fa06%G;(PgEEz=A{aXaW6w;R&|DL>5@DP*}x_V3}_@kWA=)DPez7rr%o#3fd}H zya#D)3M|PX=kHTx7sE{6Sc>SID0~P**BKkdrdg$AI;(WUU{VCVD}CXyL?o`-Z0hw*)&*bdU+w3qX(En@i2kjLl43rXiV{QG>XnfB zIf>%_l8wwT5D~Ey2}ML#^$?MGzlzlRdy)SI2>%FUt9*NoZ&!e5RqEG)4_ZrXm7sioM|JOyRP?3fdOvubf7cs6zTE&a z2K>*!&tGXH8yxh3Lz=ZZej-I`%*u?v)W0Ib7bQ!-beXSW zQLsNOSyxcq(sQuDAQR42!nG1oBBdadiqMIO%)uxeDXFU(gJFfLD$$54EK*IDM#C%) zn;djb^0(esm{zkmk)su`w?#_5EhN;ES04Ycek$_hU zl#I_Qa-icxI1jMj!|4ArQQ)~OEFV8Aiock|b_ zaroG2KIJ?@U6u*MFuo2-en1d?0QfE7H-Of){*iC=_;vuu+koFD<|4hMkx#b#KZ!Jn zCw~f%92|z@YDo^W8sikUizjC~QBwn1&vsdq4)Z2?zf9;?sd7(KCXo^~DSoPf06iHJ zIU&f%9A8zGsZ*k=$~2=o%X&jBrb)x8GFYm!sMch((WE|9queZk{?qKPx`K)WRS&<@ z!uPsV7ggvz)x-BYl!P09zdmmzlXKN}nD}Mjhk;)K{^G_>g=;^)l>wsZ;{Pk~voGaC zMfHk4&yht1x4_|==T1xwH$aJr;WjN?X5c7N%8PbJzcjE_CW^o)c%(!IIuZ~V!&yRC z21Eip>6qheP+@H}$Y?acKv<oE zfD1i>V?OO}htr)4)WQm5w=G7@IvhB2nCIM6H01!j$j7qAaxg2g8u%}X9r(-Ns%qD_ zDnRr_{{tPqdAU2tRT%ofkxGSFU2-yT#Gugk8Vs&ct8(CExfQm(vVaXyun=ru*nmjD zpw*(-NLDCqCrIsVmDDIxA+ZgjL6(KX42uR0ugX}Z$g&A|qhW(7H1idCbEfRYxC zGR`(>E11<8C0(W`rkR_VV`^%KGqdM8)ji8ZG)+hN%*ZxKhYxT20M(T>*r(VtzMl!F z&7d5>5B-8#S%@LU&))~UN5Q`V{*P<$+uzXftp*UCg!D&Au!6cm{$pSxo1ctr{hV=O}?6g?RjkCFBcXwOnjm08RHgQ`j` zsxTT1u!2z*yG$xU{oXUa2Ze)7Dr7+qXzY`!cbUenKQz&$_Rsv2DPCo zjb;tmEW?0=Y9bMofC9OJY^7hgtn zMS$=mRav7fDpX{h#bOj!xbZo-l1{0^j&Ln3_Yh|#2adhK3Fi`3QK6;U1{m2THHTA- zjH_%JG7**gfCI?__jfP%cf66~TLmEh68PO~wX+-$^hu9g#c~oUAZ08mQgFED&y7JL z+*pdp_%&gSb_X1RFCu&=G=R{rmxM=Ec}6wT09YCfv6LYOWt{=78daMtZHzKF)Sx_6 zMhun`8$p4m57vReg_c&0Le!}?)8o_3P0lhoJ;jN+vmEc9HmcDqEiN$0dB&L&9ivfc zsZ^F_YNA0!G^ollB~ii^UJQDb*h2byw0)zsFt@`}e~{IKOHiW<_3#{f+t1OfC^Uo$ zShy@>WZoM(mEdoI-%ozm*YtR;14Q$s{uA*2>$7{AfhCTAex~j574l!C9q*1FG`(UKmj9sSH=#(xLA5%>w<>Fe|RzVh)}28dRv{yxd^e#OJ~Ud<;D<~BjB8aYWoEmfUl zCNVte;<+BZQpkW)W0WRiG+7aiFd7b0cdHDS23T4hVRT@SO0$Zm4dp=(Y9a=LD`SsQ zSH;=n7X!MJGe#oJOwMv{<^spsr#a_eWG0$pf@#ihi3u@n)S`e0Asr)?Ts%Bcq9H~Y zG(V@x)EceRg~mS>8fu}41dfc5ggBuFyD|p2-dcOY92H9seGA=)fvCd9(sI~XHi^2A zT{^(|(hLon%&9q2GGh*a{FzcV5m8-^lz1EPWa1M3o9jEBUfy`E0^|b;IIj6vh@P05 zm6lnZ1qq!D9Lb;K8gRT>?&N!;JE;xV7#?ZTY!2XPwP?_RhPDr44wF!BY#+=jI2{m7 z&T(;KoQso}n4X*AQhS19{uxe3=a>?6%!;;=1~aq>g+e;9)Y3YCO$^hpBK?sy5d)>4 z^w zr7<#rym%7p^U$w{>2?T1OV_VQF=P^_f?}(`g67r{n09#P_(5K9&odwzbfS*=ZdLeB zOM)nsgXHSj?7y9qN&4{h`|V%)c&!2C4_|8F@8u8C=iu#=9%-GZl)IF;eM5=iBh{DJ ziFW&RPIL+OcNo7k&Z$dhINCnNg>Zs7v?Q3~tQu!h%;AdwpUAjx%d!%K!O%Knvbx3- z-oQaAdMb#gzDfX+Og3fR%usRA5LH~QGa8Hj&Lp0rh^Zn;AP*sQu7=3AizD;OI2W2> zjm?c!u+o8jJ)UhHW2Vw&h?;Tl#@?#<*zz2YOu@?3EDEaEi{^j)RN|$4=WC^0(Q64H z|3Dm6y1t{JW94q&JV+;VRU-pQZinbORtqY4BV|O}V`k4e{_N04Ijl}FsZ}GQ2}E&1Q9Q|F$zbd6`TtyvSzr>X`O_}tf|(;x5@pUsWoK=| zBuWs8D7H^hqNY%^+%7xYt605lx!L;Ub0^sE9H%B51S-f}xvEb>q{m{tGViCnf~u7c zodW$>LLnNz?(^YA#%lo}dRhBAaOd^@A~8>DSoVj_ytTMi(u#HH$9jSfP^H=^BO;g6 zGv|0l$Gs{p4Yf%7jLf`8I&yspJS%o_?CaBT=U)fTd5ZQgJRg1(4Xo z^2iZOK(ikc=#0~3wbV$JBb9U7aho*TlJPf5|KT$sQ^ZVp) z0ZG&>J-LWxk^EjNR9+BQ6f=N&?p0x-1f-rjWperM3pM)@%HMSQugDTLWyU} zR%L`rWJZ*Hr;4Nc>3-8iLI*gd2FNlNZ&_?2^oI{0<+yv1hEpd9!z*4@)vv)`oT{nq>aHN@Jloh$!%;?DtaYh&Hc>oJLIb=tQ)`9)Y6_AvVq4 zHgN>2a6uRa`Qro_M!F;(sZ<^7SEmd(RO~KRmS;#Mud#r-pB}SHJYeZa;bHcbNe^r2 z(2S!PQ3LEKt%3C(99KL!b%=>_n-Ntfh=TM>u2K|;1pt#pX|7h9=z3*)lFF8cZuDGv zH2@+K$?+QsPaM!@aOl`kPJ2@f$OgTzSD@v{3=Db9C4E2s@_q+;-Xdb2^t-w-l-ML&leP~xWZY(pove@cKHf?H#7vwO~b2T zg(P~c#lG$f__c@z4!$C{$E=u9Ms+t8T~aS#{93=D5PdwY0%)!dF}mi!SAiXIhS&8U zHxeK^kK;Wz@cXVf%!F4a9V3%31z_0p_avQFm%V9(RT}l0*5qkmQ8=CsXFuO^ehS^(g^|C4cPgGu22l1*BH50=BeP zvht>7X6w_>o#BXkp1RX80M>49Lw(UVWjQfap8JyIrDtkeN`)^|{U zYA#%9$Q$t`z`q9m*$w)x1;@ggu%!xQloKo_Rxy;QC;gDB#Q=#E?YTBx=_gU}IN+Cg zs#KJ_d85z*MJ{zb zD#wkGAm8`eBB2%4sp32RtY0gaT*OiE5!IkluNX2@yVEtbqsYp0CNU0j7jI$0TX7LP z4w5QZmzNsJ|H~}Bc|Bc{$ha)>r26KiZ6P=^R#0zQc zn<&jG(X{y}$;Ve#2BoqpyW}h{#4EEZn_pbT4-EPfc!&Jn&(v=*^gp-tO~`SvsW!$S=uG! zGOm`EAXT>ZU^TeH-d#)_Hi7Eo2|Cvf9jX`6S^Pl>x zjCn)lfw}(T*e3Sj_1M^1#@f{@psYCe{25+w&e3$6Mha++!2mN7x;7e zASay*$jGAPh-7(5sq`X>Qx@l)!8IE?+3L@d^xbPZt_MJNCkgqlp^>*fD-4UMIA5}e z!pa*-W$I8hD-CcX{5jtwJjK>~)R!F8$G*)-hmn=o)X&geSWQ}(uWNCbQvTIduEW{RmpU7qoe)2b?lDvF_AjgfYZmB9#08zU@PHOlbvCaiP`OA*We zaT!k>JH$cn1U2CmjPtA-ePRfS6zS@kxOx{FKMs5$QMs?4JWj)4qEHHNAUv!ZrpIDOFLX$P+cG!NY`5(O+n)v0QV9TMR zb5|9>r0;am>bBEkYi|`Rwyr=m6lc$!r(CP>-tygy1p|y#8;q=PQd?PvB`!22omLMu z7*KJ_oD0uzT%9%a|H#ni`|Xe7(Po}QRSXnf!|~@yAo1w6+U2zd$Y0EFe)(D+!UBuP zRytwbWa9DBc@u-wsn@GDSz_4e^CWaY`3gz`SZZ zRHG7JqhzYTO}2FQVA~XM*1oBXkCaS~0wKd@2CF_vbwygzi^0S`pM@K3Mt$VRIE7ai zQ|Yq3wU!O5R}yVhrUGO0EyI+`C72R$PN4S9arER7j?SLsOfYVWV!!*gw^13X5I#D~ zv)!Wz&&x?KDaEAEI7eOX%3qhUBWZ#0@2|zquN6S*Nine3{OET<+Qh_c5~q>j#Mq2| z{E;d8=II!SFPZF2(~>=0Tq1f27s=`uV#NXC*K@!m6hd>UGV8(ee7CNrhcWnl%;67iz0tedMoX%TOm zv#wM^sEyJh4~`43hilxxQxrNPL$GB5L+RgpoelqRy#{2X}P8>TSB{tOuea?mJzo^FbS}UIja(4 zU##+^-lcw~YyabUBeO9q(4+}feMY+-oUEx5XPD%-S|4FxrbO4#fj@^vX^>%OgfXXq zBRoT7oUY8VwY`*8OBbWc5tGlHXCFs!owDJN=``Hb-m$;0-y1jo(61|6ZcD19{OK#% z$+ZX+-VoB*D;k*&lQ)`4N71V_KmMDlVKpiYyMu9M#8yZ*>X~}Cj!HAXovadLS_NJv z)#1|;LMeC2$$F3EDqSHOvq0?PYKF4Xbo2O18eojF*n1R3bi=NRl)FxyR$0<0kFuh) zhDGiY%A!VSN)Po+EJQ^i>pF|5ZW=&Lx;%aSAg8@as-hYfp(wV%Lom9@+S# zME!r+(O<#>HEb zUIqv^lN@di7MZk3kZw|)F||pGZQR5>V#_?D@q*H1AtfeV!_!BRHC6kosl*wM8VoTY z;=&WjpH`iuqAR9ar*)Izt^gC&UhSEg8l@+j3j!)l$Gn`KsZD2sC5 z20>SR7kz(5g1A{-Uzva=Q?~4?DF{pT6NPiR@+hv(S(>gQkJKw=P2#?aDp~SYPRZBL z^;Fjt>r0VjGb`{m*We5sIDdq*&a^3-9fU#7DN{L#CjFi)H^r{wd*h`D2@U0Cjh73M zRX46gXg(GOMZX6smI!g~v=z+|xymFmGF>sBq4+$JAX+t&(s+F;jk5WLXXjd`H(*&y z5b~ZOrt*7VgPl~mq~OVW8#+1rwa_j)PxiG0;Ji_oVtuY*HSd*<7zvV^Y4%5b#u z=?R&2TCC`dux|MZq7mpkJIRyLVFKM6E(-F`swg92rsY%n%THvk=T~Rkp2X>1@_4yk ztUtMtr@0U)ME=mSGOQO9l3{G0i$bH#tWS8M7O!2rI`1rHiaHA&Sgujl012Se>gW^@ zQ(81igM>-d57S(eIR9HW+0-5$&EwLBv8yMLa1&KWW|ebur*E7un-*WVatJXV%6gu9 zvW(;q{iI2&ilZo!>Enz>E!|AFkSt|45P>ORJ+gf<%t@Ym{sj)Z=Z$f$i#UV@kc7D; zQBmQWZRxSlIo%+M^2fmHm-?ZXBSCHkzWZf-?j?`;62p3>PfA#pLt<#>%+eGb1=}KR zx|yYn7S$e+0wpo7hpAJpmrSBmuh%mTHcfX9;TXx`CTf=M9a45nrpKR7W%YE`cH=J1 zjd+ROCD*FEm7F7&u{rGVVw1}D9Q8AXGXkYGnPZ?NDZ{#vg6lIJ)>vO(0jnjv5c0&_ zK`zNTb8M*uNpo95OU;UurDCaK(d{B*r30@T001BWNklNjh6$EU%wG& z)jvTl4-C6mrpP9|gUk)%6c7bNS!RtmrR|orE4wF>Mc2a8&Gk(T#8!?jInmzN>mBX??26R&M6Z-)LyPM&px!lQOJKuYqUi3?2uQ(-J5>aUw2gAn*V>uf z9h|Y8TMcQJB}>{vCTC(1gRHGCWyy+B2A2=QN)MiQ znEc8tUuB`$eQpG*cRt1&*Sfarx_JN7L0jsHCHRuVl|uLUqxx4_(Az z%jg<(=Z4^GQ#{>!0biS7+K3N>LLu(L<5XH$U8pX11h3Ng&Fck_RY@@SfpDCy1hP|HHf(xLU$4ql*J3%GA~2N3(xh4aUDEF|5rng_;y$v9t9Ue}?i z@VXuK16T7UF9jgKcs-B0-xyEt3KVoZh?jM!O&j&+^bNa^S^6xK3@PnH$lMJp=G!15 zAtCBLS3-|UrEGNWuHWO{9k);)Yrr6=8m2iWGY*`Hn7lMjd%Dfg8%AN;WsZ*VVwu)w zS+TYBnCx1%3;XJ>X5uK@3)aV@&0{`!^~E9~z4(2hqZ7mGY%Q;W4J9}k^5oYLOP{CjNymiD27476Q5iKRsSGyobT2~PnDbOb`>`p` zpSr-|sS~F6;0#^fvE~gd8(0OW0@`7hNH?ZRL4jB>K-qu(FK%rVhYg5)U%4ARS3egB zkgAlmESPiK?DV&=ZOsPMV8qOqFY*+Jk&b5y-$hYcWTKB=a@nI5*@VxJO(g_N8z>ZV zT8gUTH6Ql^8-eFvY{yp%kRQ7dQS*Lqq&z9RAxu_tDQz#7x@G%1LuH{rtJjB9P_y*C zYe|s2A9`xMn}TVPq^${imjc|cO4Gr4;`l{QojS{ri4z=%j&jMHVN!Nb!eyKA2E`l8gZ*#Oqr2|G1eCSXu`a%JkE=mW)d1j@fl*uGaB zR|}ATd##Ty9rG27T&>ED@nZZ!^acK8A=Y6B`)mngyL_DtSZ-!}xP`vx>Aq{K$|-M> zznS?W>pnHcMKQw*@+=pf8G7Xirz46{xSph}XFuo7ox1d5r-87?qs+`+m4TfA)c zNej_8udcy75Y}J8m7Ia1Z&69&Fg-V5Wi-ak%}uez`Amn$=MFMc?lGd%eWKjJw|Q3S z6C36!0aKB-n2tSl5ozRKWFWCtip)X5^#_XPX#I2DpZH1}T@66)xe<)rbh6~&$knPe z=OVQ$vhK5_9;qT_q#r!*lTkw4T#_WbDUn2#tD?pp=NMlu@1rEkjJTsLa#m51 zHKURl-jWEJlO05-f>+l4$Q>r8rc8%h9Z>A*ti#JHPW?2Dmp2WHic_Z0WfH=%0;mO* zR4){cr0x1_Yp-F&&gG_{-0^3Rao9P-fHROc&nZmSKIe%gqoQQ;r|l$!X~hsX5;ZO0 zRC=cA%8(Rm7rEWiZ>Ml=fl^B7Qdg|Nb2R|@xmS85mmB@kqu(Hx=?W%)q;+EL z=UKDOK2T(j^+a(B(k6>;r)rh3CYCl@Ra}L3rMt>d-Ah9EGA|i@UH{5DXl8VB2eWN0 zaH$NG&AK!@8*{ewSsC3uYp8~mBfikZx%$vxW`q~)jMUaxfVphi6pdVE0mkwQU1mNkWV>bW z30nsCOOVu?mIqFCg+-0_o!zwzZCM0u$>HaZaLl{Npfg~a1IbKvit^kUVuyoCIX22u zDixi4!P9I39rrOL%9zHImua;~l}Y#B`26j3W0Ht}4$_5O!>?fc;41;h_rH>FzTD_{ zYYJhl8lq2n^ixFA{EKAebJDE6BvHxKiIv*m3<;c&KqeE~Ozd1QHsupCH7ow z4pFX_8RJ|37HKJ=cz^n9ai6Aq$43V@rvGYu1f(Eo983)fs+m&qsQ&Nv&W- zFv?gsXkJ%YI}ouDL^Btx>|1d&7iD1r_E`y9kd)%Qk>$-Xf=WO%-R5j~iNFg@UE09P z)U&hjqKqtP0VS&z|58@RzPyq`z%&njrZreHwN<*NyVS<+GB)g3gDNX7JadZYoYU0Z zhB=r-c{0~Op1?BctsYAbo7f^Ul`3yLo!6t!+e1Vme)P-qX30JDb}WUNb~_q+enQtD1u&Q?(q^AEY8m>{9C~ zZPCO;$-Xm(IO&cvB**YWKl@FTB4+{^J5S-J;x5X;AurRwA-fm(HAwsK>XJ#h8cHnf zO+%pM-tvYEp^}~>mygE3#PMTETx8z35pC5Z!mV?XConxALN2w}85_ z&{q9{-U6M#u0m)fF=fnJrx5MV!4XT3vMkZ6^w=7$Vg`=-_^pQ5MaDF)hAkyE}Ra}%4oM*!Dw4x;6J4$jbjKm?HOj;gh?UYp#L9lv- zIF{s7^FSLb7UuIRZ#N)qMbUC}khGC(n9Cz1;KY5VB- zDx|4QQFcLMM*w|G91H&L_Pkc8J1$2(aToeP(m{090#l)AH7qc1t{^x1_96NxB=c3P zv|@;z!;u0c28#zJ$$(SmN4jrg^!rw$)(d)nH^s;Ie}*rpZ!#@gxrZbZ(pLhC-GYrQ z$R)^o8rnf`yM=-Np>-{(wrb;xkO4Vjim!FhDY~(qqLf#%-p$dA<8{ZdI-nZ9qn?!TUtB0?UtQ{TO!m)+kE8NkMcM20Tz`OQx~I_ zNKb8w^dd(IWYUY9c4W0GzGu{jbm>ZgV^t6_o9We0|C|+MR5Qdr6$t|ttx826Tqw`5 zh^i?75y<3tM3hbZ48)0Q5k>Jt6e&|y74)s-$5)6DLa8{S#u;c=sRbp%Fjgy525~jEOk?ZU#QqlNupATUn~KMDw^p1nKb>NNu`Xv4e$=&|Lvc* zmjlRq3bG;#;@LnD1(+yX)hNJ4^+6|o++SJHj_TU+4Oiq=d`L>sPBDabP!IjdgLf)Jjw; zUM*FWiuQ!5s?~MfG1e^Z5+`&_^x6E#j$pbn%X`Hpe#pNEr&J*-#m0RK#&lolWzBxb z%wJ%|MXbQ0S0%Zmky>f?&GM%yWi1M-cpVBaXZFi`lUi@>7X!$MK)f-V(3XCe-&^j^%-}9M zKnu_MN`0m)7}=U6tf+Q>u10^osTd$7N4sOR3^vpf_%M&!8$9KFbo zCy~y_kdl^+Xo)WM$VCLoydT$%`h23^-@Rk(;-Mz+9u0mlO# zZ#~bYa*NSwjW9HXl4KX@WDk>yS=wc7$%E9k=mSU^2usmaWt6~CiBu&`3PKoZmD(nmOi3CAsmwE&E7l^Xvxt1{5r~pPZRrmh zE@Xpj%O|!iBP?L_Co*NO+5|p*1<(reBl~Qg@Kj{v=H)y4352dCJsekeoR!?=Y^A(& z*aYUDJpK$%%Oea*tv|=P3pmPD8JQ?|C`Ue<{3-{_lQ@n?xhv?H1faO%lIN8;UzuX7 zYVe`iyE*1f^4Il!^n{0}c~?C@Nhi%$A-xLYd0Z?{@GgHH@1MPuv*lTSzxEJkq8aLb znJK0j?k+|SmbtVIhPRgq=K`Zb3F~4TDP%lu3OVZFw-t0n-jvC1CXxot^!2Pcv@diR zeZXZ&P^0U0;-)`YbS28lfg06EDCR_mE&fWDZCpy|1k4_pWVklOo5U{D13pSTf4Tae zG60f^m`=_-ex3u;isB@(Pt)vl&S`V9G|r`Hn!4{%@`YKXOn*ZmGsQ}SBS;(MGHzPEfoM^H{FKkb!IMllZI-DaQx?;# z?vBHSQj5Ci@GskUvn5>1L-GKB?0%ClD51i*n6nNhp7W*{Q6c|IzLib4-3k*PoQ?Qh z?`y0K#!Q{4P{k8lpNOVtXm;-@?;_iS8b4dPpT>7@W5=-?Gy6LHvimVg!3aY^jW5)m z<=)QMxc?6&Gb1OpxXRM}q^l*D$8;?qY*uLAG>Tg()A`&4)q@>+ekWeh?U+oe9vA~% z(U^7Fd+}-hw)QZioxvpiHFuJR78yiGWLzJ1i4BOG8U%xFSnlwiRd2za6ZBd=B41Z+ zj|yI)d6uQP%hBa0lD8Fzvo@o#6%O*@(S zrKObj`n>B8@8UDz(;RhYsOh+Yo?a>~66YF8)Dly?&EG}+jcW-$yomB&&QW{91=*#m zdJKeBj&gy2>wSc`ocSi=j4;7V9i)?d-h-yd~Jp?d2_SGN9b3 zQeIh(C%m?2BJ#-Gf&tbSbL{tRY9>p>fCyMTw}LyL+Q}Q9-OtCIud}auoNA|p^reyd zvhOC677{Ws;=?`{a+HuFrAVCsA_qtooFq2#6K|YI;w~0`)Bjuh58t)VBN-d=$(;g# zf^XgCBZ;gsNA4c&cjMZRbJZ&Mi4?N_EYhJV{)}tYu5wK?d)=FRwZDkF7H@+Uz(YP? zZSCcZH^DNoEK!>xT*u?0JI!)ma98~%THim0yP;0>=?S{g99`Klfhq0ezv#}eJo0#B zbr}uXA!=(O$g%ilnxi%DK;_0-GjUE-_k_3LwI_c zuR2e0fmsHkA?AV(i-Sds2NV2-E&(J(mXD%~%6V>yIj_Y#xrfHOCY>Rd>Zby}cJ49$ z7x^)Ci8`4&2$HL)L{8ZVrFJg|jLZ$QJXk^}?8JHUDeEk8ug2<&BnU&6%3%(LM;ZUj zQ`8@9;MCm2U2G93uiy9q?20a3{Hyxm#auQQ0C=4Tegh-z9diHakAi`%-y_6 z#$O5^Lo3lMz0>2i&IXq5SOK$&BL`k!kMo>qC#EM#Xru=SM>6e9@YddHR&3dT+)%~& zW{V@|pXXukAcIles2jTe?u0wZJJmK;uUrq?s`z{7ki!nqZDsB_yoDvf*G(}s?NKo` zt@JrKf(yT>wRvpncN$FScteK@+h*^Y{+1~pd88cAT<*iqG} z%6Vs+_l7%o*T@^0z12gW_fh*U@CpCx$f(ZR&NBS4XJ*@!Y%?R<)TuJ!*9{d!vu#xn z87D$J>KS#x5iz|+niV+A)Dk_xhNA}m6n%}q3qEa%a@S{TCeKA*hg?&z-7H0is7=LL zSi#C2IO!Z{h$_pDk8$VYH}Q_`Z-I9=82F*ZeE6@wo0i{WU->vSRYq$0uf2fAXqO2n z9Am6IiMK24cNVAd6>?`mBvOa4%voV3hC?D#9K-cWV_Z=Za8yd&mvpCE5Fl^695@_1 z&pXS{uu@ztNl+mDr3Pr9y^?}MSoViVD;gRj-3CnJ+0sQ6JjIG=nA-+6!)6Db4taR$ z2~N8cjIo$b)Q#5>M|-Wh)K$P;?iS?jW9GQZ-{|ti?jt6Ypj4$Cl9i zo~3->$L{2WJ8rH;NqerM7zU0=i)lv*6CunC)|uEz4va97AYl%ZOxhTTq(RKYF&}?U zX3XNu91x~6)t{%Qm;)r9$-8nnV1vW96|(dqi%94cl9Ku>mKSW4KE`P(nKJrZS|_)V z02Tk~5}#9En>Wv`VdEWZP@dw_GiUgwc!rX!8r#Fz2b$vUxSVm%bC17>ZA-Qg-CV;t z+NJgUX&&ThWK=O}r20ahb;tRxa22~ox6#{G!JQ5Wrvt?49;MqGxZ4)<_EWp~l(P@z z3>pdGI1a~37kOW>iQAX%Cb+#$<&@9d!4rI5JV`mI86H-U^xepaiHSEV&270LCfVpW zd0Y7wI(OBPoe1{K@yXs-IT=l|)^8XIblN-5k2-JUSNH)|FImS#>pVZv`j33sIn0n> zN!*}uUPtZ>A=AbUGgih^C=gm{)0OExlLI577!*-by46rulF3~Nj`q2v3%<%UeOKkV zB8Jo`^DNH+(L|LZzSP@C>7>U`{?i8$@2RtW+Ximkzm~7p4w)dT3<6`QJ1Pz$I)(x5 z#74S^ERf`VP8?D&YPZj1C^)wbWQFkZHAl5h$9YPJ-e49&N-T5ly20G zC!xgTfkdLk?cB`Zt`$T}D%Ad}#b+lU<(uvamUUMcT8MBQLZ`zUqs@$LSxIkomEI#0 zlnzeNo^lYoDg?Jn-t_P-?3()$``vMdsTzJ#!;!pMZKJevjA&_v^2fV8G`@$g$b*b^ z7SRp5iN8P0VgyE4*FO3LWNmkv`_&s*wSEK9wmR-pJx(1v$S0ylsQ2o|*gnH~-sjxH z@6~@6?+4c~y|c#H?=R*}PpsopwZ|EeHKZ$yS~}^q=*fs>YJ?hP69lz0F{O&tC_QQ7 z7)A2CsXl6aK~<7{Dst|n%ozvPZ<#hR)e24++XCkI)Afvn%Zkb>z9|o}egEU!yZg;J zw+yiRz&f5*r_6QHZr}8p#Zn-t0i3>;dbBcXij(uXRK}T3Y_5nfN`sH_YndbzDfcT5 z7$?fu#svYAoZRYs5=+b8klCb7OLp1?*is6JouwwNA`5oRW4`)kwOz*U<6*&5t8WauE5oM~1#g!*m{VkIK}ENMK|}T~!zit2m(`3PXY@LPm+sphS`dSa7%GzmtqJ z5FN)&7y!v4rWwVNhy$-t>?IO*41(AJ5E*8HgQ%3*H+O{FpW994&OsKB3^OuSXVUA$ zY>G(64hRDTCjmlOkRG8`@-!=?Es~(y_Q^s8aXLE9cLS5T+ItzskY?mC=y zc=E)vrs~F^7@({BRS>HY7?$b`w{~~1WZfFn#yZt+^w@vq8NTEkU~zB6czX14&bZUu zt#-3|XZ)#oI84&-zfhzKQVhhZ~5lE_%{!rZt-~YbGzBd*YTyI2G68+83Hfi|G^N^R-wGFd1zk4y*6-W%P+&LuktZmZ%R_h}tE z!Dj$NK)k=yqX^$43<9DsAPH!voB#kI07*naRB$?U%K_f`fVtl}LF3^zzcc=y`G?x$ zEbA^e3&Q#GH18^H=MTJp#;CKL2Y7^^cm9mC{x~=L%h?<)WrZ`wxoC>R;yi~d6U;>+ zi#r2IUlI60#tJ5_z-T93>6%%tXJR&UrI41RThO^wlu^>?%FwLT!$E^u&oyxy?K}`d zST5H%-k^D6HMh$7(lqDBFS3Rvjo~_j7b{Gax@K+}>U)x;6(wp?h@0`Y!o(|Qz@b&E zC@5q+nh>K9nM>YS9xASnOj1N5&7Y4{z8L8GZvq`H<)MBFvQ0qKrhnuYsOW-q6DDm+ zIgd;GyM=u5ewC@ey2gT!(<8AAdy**kY`Mqo&MLOATSvG^p*}gy*Ze1Gd!dPPo2q}> zyQJF|J1rIk4sR&!AiQhPtVm){hc9#=<(QmgZLkVo^^8PXNrSg~H{;(rfLrr$9+~B# znMZk{F=2>%rw$xu_-LK>tyOkDa5HZ?znMR)?qO>*%DwC&x}#}g8s$eKzSMq%Jsf4N zzk<22WePnZT;q5Am_pPc+UUls>h=8q8saiO+ySm96W zpJ(7em61JFKIc8kA633Y-5_^q}Azr;>-%{u*dSP7krBbg@ z)h+F0OhVO;5SzkM=s}XGQF(=VtvR^nddGbiwcQe!dmURb9gy(jP-y~=lnrc`=o2~3 zarr_v$m*0sQV^|$J{ECHG7XNBEYWd&I!-{v5tMX3x0DI*M;I@U@r;|a%)C@+e33*# z^T`22!lWp2=o777cT{!Y&faEfw~fHK;E6*Ac*;3ULpGAqJ7L1Pa=7GP;=W)B8=d(OcDX19NsJ=*-+$r9qwzFe+E79$B+%rC{{b%^c@L`7A!+cX5=BdeN zxc{+Dhz||IO@qAiD|hfml?T~9cN2Fk*^Pf|757+=)I!%|Dr!>q6Pj*i7bKW0Pt~;DlEf#kc zGZ5Czb-C!x@E*0Dn?`mp^~MsFtz|CkkNCNDKZ)4Bh~5%KudJBWlWjU+;LsxO-n*W= zpWn%?-4*=%`ezxB0_u?$^Vi5Hbj2wVR^Wx&d3FXPysv#5Zywss@(pWn)-_?+Gt?B$ z^!WY*)4XuvdHy*3BHyf>XN4LgGRy;!tmaWd>UW4_WUA)Zbk+Pa1}t-KkFaE7l?e+# zCeiXZZkcjMLt{I|sR41*pO~5wn0kH6aOirvLvPSE1^a5DXBf|sN(hd|7Ar~*`=(Gs z3jt)fsBhYZ$BIp8wB!i9h;`0V)`v@ts%4~z_E)DhXfr)F)$xkab}YWvp=Uv{on?t13eC$UMp0 z*~QS7<@oDsRKMi&*xASUw0MGL;h3?fbaS3Fa+0A+&G5@+RhNceXH_t2W>HW0l-&xB ztGLhI#n8%Sbk|i8ZbbDROXzte%9o&YKA@xxlBz>%u|xEZ0j6#V@jtKlz*paeY772d z?cuVYz#?M2SK}U(t)yN#`Pm`~&Zm$2vB;!&N5WLjJlbk( zL4eqrSZ+M$LBSLxbF#~(Xa)ac`M+WPrd9Yugy1^x9 zhTUojchq+nBU{de@YpP$_aEgXlWg&asmclude89O>F3z_)Jmeehmczb`L)M?n969A z)}7^S|DOoH&P3Q_AP}(xF~Pa~%(5aBuE%7l!%miSf9*~>_cjb&oo}>wp#2CZm|}gn z%IMms%a?d_X*2(m|AX{GpWpC4%2V)V&&ynBrHkJPAo z8~E|Rzn3S2zoz3GY3rt#p zpTv{Z!)IAA%qC|QtZU+|av+G(a_n~S^(Cjd7EvOyQk14TKwH3<6C_mKUz`1Amg7Y+sIzt4?Hxsqx&T)^}$f|W4>20o2 z+Us-lFY(vqfPRsLRNnBF>R7yJgP1Q{2U!tXi`ke@7kX*)HdgJAZ*sP>dUP%A-F1SKJsN*9!KY7r zj^7g>=K!af4%=~{vKO-P+$Z^^FZ>Aa?7o}Qk1j^uH3-ka{l~WP58hr=LRH5TwB2#K zG|v0NE&SZb``&QD2`S`uJHsa_k@ZoOp~2-WjHozN9`5pAXTHJ1;xMbjRc3BXxHJ5;dJ`K~Y-Z}`25CGIGW6IWi-HSt&9KhOLXzh3=&s!@}n z-XN!(v;1%BFZth}sIl|6Z>2Ri$N#53$?4K8>$}Segm`^nJmna={b%LFbRO)$HymVD zW&}(-A#9wRWdG57JQ&5w3UOGRz zr6!pWbJU$7!`qpQT5-;{h=Xq9-bLQ-FJt41jj*eRINW9Wg_C?n?Zpc!CMKc3Z`_&U zty-Mi|P8XnHR86Fo0IObkrI9y~L?B~5pyj|SPj{0WfWRQ~)!6WDRzoSnw z-4$$Xt)ks&nMk~0B6uEK%A0teL+ouo%UdtKlh!&B$7Jwn(?Q@Gfa+ZSo5CShHkLED z!^N8pai3RQ?4IW%-d7n@Ygpe~M!VNxks9Fz_Xt0&ewX{7zKQ4MSsp8&XN$i>ThYco zaFRwxX{vHVl-bJ>_69FdnRY47l;VmhnFn*eU`SJsv&5{l8EI;V5MO`IuhXixPItb* z6jj-zDF&HSZEF^%zS^iD6cX4~761r~7gmP)WQy?=L{zA<)Tt4f;EF}m((Ez010N16h(=oXiK&=PMY8_b3FE*@pxxvXFR)W&l%f0 zJC3t!+15y7Te3ykq9{@%C1yfk0>J=~07!z!L`HX`Q+0(G?%i|ayRWJNf+VtZaT?uS zUE$TM_uoH#|M!t4$X_)8*5)JsojRVLd=+mwhtLytA>z;v4!dR^?qXM=xMl{}%mT7c zP2lO#SMdsVqA%<~tg28vvCKyq9F6LDhP;j&-&zOn#uDdztnKovhc@fx1S+Zc#@#0P)EnZVDu*W#Mq z6^PfB;O=dpvE>jRbY4IZ6J$7}yaVJt2Y0&HAb&+K==>tG8zMY=_!Ye7?8D4jFQ#f! z2pUq6mTw?}Le*=a$LYjn{tU#O1hf{znP52J4dVozL@(`>$XO;4P`pG+BMRaZ1U^ME z(~d6$H{b#P4eam_qcbX?J<4NFV-AktIKJw>f;QHU`Sk_J@!5M831hZ_jvSj7yB_-7 zLBy`|D*XDf8qB20LJTFsV1?&)p7U`4dJl>KFwh!>k?jqk3SRhjKunCjcN!; zVHyJ=?WYcZl2}?2dGWSQ4h3Wc5|nF&`>HqMLmElKmFpU!dw+I2upOgMLk<~ zp##wco`U1FBc!nqa?#ysS*v$()E~pjxDTs3mLgnJgmW|iwvOY8_%)2t039(A4XPSx zppWJ-oAsbN=)pbEKz8FK9t&Q=5i)@VQ75Xrd(D$<4taKqo&4q_x*OAL{U125a*A_&V7cWoZ zvG8Raawo8$(T+;c5Y-&0UJ`%Y5E;d44C30Jl?diIpnIxljGVxF_7kr=`bRON-ig6r5dHChRP%)g z-nz{=t|6i@!%Ljdf@MKS26>y9gM%iuZlmKQ#T`9nP9$Pk_Az3E?2Q%Rlp9A#Lby%{ zmvV#42w-M|Gt=u$OOIeM+eBK{v>v2*O8c0`)R>b06<{$A4RCCJqd1lNXv!E#bW9S- zV|AoyL3>Tn8Hs!=qP*!A$O~4%3-s}D@8Nkqro2h4X)M9ah4Tetp_WX%|QUUjkU@Me+J_l6w^qlP4BPMs*N+$0lf=EwLnTZx{9vk}hC!+)uP z9mn6nv(7ej2OZ+w4`CEHdJD0(yb`tRa`47uVDls%k6*!v+dvP^h%lt;*KwP(T)^|I z%ZtdoNAcdiH?hHe7k!N`)YTNCoVasEx83z!G`t!<;!m9KV9};ISddwZ#yw@=_9DV-%DC~30o?f7tvI;*J$$$NBK|tJ z5u+qVcdaPO=;HI{GZ(xO=(+}GBaMX{K~`*vkz&;1W-LguiKHvy^hI`vDy|f2h2I8d zqOKJI=?TRbWpRoNnW6b}F;}qlf9kK9=?o-)s(OJ$(ik7{%tWB`MbpPoNftjD99T#B$tHpLIc@@43oqol8275UB5uRl+!7J9b2bm> z%?6$iUdJ9XiiPoIn209@83MxP_TA=EN zSnBrU`pj}vSLNWBGH^Fm@zm6-*iMdOel#0X@s!+_I1vT!yJT`0%1q!oXCA)L{wdV2 z$sryMQGPGR^XyIRB*U1+2GDRC0vW(7gEJa^*zA3PUy1(^|N6Q6aNX9`xNPMj)Nd?e z@>5wjx3pvCrfcvUKUj`s`{&?4W*)>84Unf^;z+?7&MTuqGmNEQ_@gEI=;o2~M z+kZfKphD1w!Jto|Z#BERYZWLsTVh|M6FZ$F__g>?@r6BWuqC@6k2~AYU+YCA5_*_f z7-qsp39{s+-D8;{e3`Dsl37a-Tvdd>E5M0;d+{W;pb!?tnH|L>R(f-AYwjvct;vX) z828mFJXzm}9nKII(zznM81qN*6Kok)b}vJ?x(FN%(Aa(uuev)>j7o+Gnj;(dIpW3< zWjXsvh+n0j#;oO6pmv#$_=O47M}{OkKg=QUD2^8=MLeI6bC^LpaCv<$-pd}wzxMtD z>qZvfrstO9&ew0kyj9B)-O>hfQ#+y_irc<%H-2s35dNRcGbo0g!X*&JuPQc2*FnXv z!SN`{ZUM{<zlE|AHj@p zCTh-k;AQ2valNgo)jo4+70~J8`?S3iO%|xNSaiFGqM}@@2eCc4KxtC@!Wk zuYz0I4OlvJ2^v?IfSnCY?%0D3Y#Z8Wn+)L`2SwMzwcb*A%X;7pOFyCE`}jCcbXKPyCV4(E>JH{;v!+1^iK@-EpJ?RbUa(fUTb?C(KG+=DKX!>?5D zMmHZ4IiMZUc#*UEQDSIZ)A_+h5Q~o9B#sY*!=Ujp?*8Uz=OXu4_rgA{NP-4 z2R;~w-bV?hk3;c;T#V&(F6P%~!truy!%UBCo50jc&~=odOTQL>ilkuD(0z*~UgWP~ zBuc$|-J{s*?U&&grKzjiQfX1fXcT*$=?uhjIksuQ+TA5>%or+I9?iwS&-^52u9yXM zOBO&mUz7GW&G6!M6VX~kUbun>ivJn{?h)YS{cU&v8>CW`aUQZX$uG)--UwFGPOR!$ zig;}q-th?aok#J5>}GgQ30{bw|kzG{%`b2B3Dk_5Y6ZNCCBbs?Ll8xY zSe}QTs0ZDo+w4L-R(JoL5$ zcsPCu9H+rU!V0Tnw%du%WY!?QrUb7oi~K)U@STYlvC-RuzIwkn=uvM9tI1qk-?j?D z+5(($3TKCd=l!?wE}Ie!FwjW)3n%njR6@(XyS@HU>uTlidj z7sii=@aN}*7MaW^l&*@xVsq8psZ0&4*<$>r^D}6_rW>?Vme9fM5`TJX@uZ8`mrU~@ zm%4?jWV}L9dv_eaf8qgw!z&_VwGPzvsp*g`XJsFPZ6L8}wAg+`lv16P7pb!VrP#+z zw-@Ly0W*mhj1kNN)2og0NqfUE;#V$^~f+_@RJ2Yht?Wm!%|A;q}S)u){lwIjk2>hKbsK6ca-vU_KIER_0fK>|!iiLnRE+-6$dm zgM=K7^H&iyE~~j>A+hTjKZ6t58ai@W{6_U|tctI|*8BndkL-HH4av6H;pTBWu7tO| z3(m{}a*s6d-0$3*ezb?xN`zOjn%lia~Z-l1-M57;O!CofNny?i7~*vn&Sv^ zhg-1}m+|C$VHT*xsI}MeCGSogt4(5;HzYhFXK5)qMcjp1xGi3aPjy`f=bk}0cXc4# zIEnUWC-8qZ{vNM*`!SOaVz83M(*6aQ_;MMs@1XNL3-C|gx*ktw{~8@$JN(cC2Z=jf?KDqMEfoM2ySP{9Lw>E zHqj$7sKe7*r`c5(Q(}&o=*aKRp*_r?N~!{CV#pp+j9zv&k_JZEIY@|R#8Av?NGV^;9$b3VJiA`mz&P9T@G zVO21xiUItDdi(;6D88P59?x?Eh)Pbxv!XXcVr!>KXfb(6S@UA_<_h?4@t5$~1$P1q zdhwafRU94~!q+peph)t#Hogouw_lC=^+mW-5%5|C8|s@eLK^6d98tPqa{tDKMAH}R*=4=_Jih=4=_UpGOg(AP%bmwk-Q z9K+owZpZH)8pTu2J0JxP_5&8i9r#7uh-+u8Lw!Xag*9aqHpLizeH*^vJdXk`3%Lgf z@8M%AQ^z&&LR{6e1koA>wk5#(>vv)l6GE1PeTZ?SvE3&RK+ILFt6d!I8M$~j+Kul! zFAEv#v~f@sqOr6?kWZwCNKzi8#tc55SW;N}!eC}NC8MOVz8O4&hwAH5bn?iMtV+tg z7ST-CiZdNzPYdc(KqO(nsDl;R`S^vw&mv2*IR4%tY@_>-&*hD{qpXTawvGuhiJPl~ zm^1%!c&o}lB?epQU~uVD%&)EDi35xgKT^;8q7%|W)ixNO4!exCgij`=mw=!z91 zNJ4!eL5Mux=cUt;%qV`ndMEB$a62mhWtM0lw*S@?IyfO(iMOsASW~|YJ&R_8T-^ck z{1o=@--;*jhJb1Ck=kK4iZygTZZ2Mp`mK4O4v>wLc%tzNKFCjENkf855BsC|Y`hv* z4lGA>WghXNI>OCWWY?8Y{pmcs>*rwkfter`=^euoLpaMpZIFwKMu;9dfzBr<@U5W- z@t@oWFf*Kkwx}q2L%VWEv19aoEP3)8M8DJrT$crNdhuI@e}SJmu@@7x2A47nWV_LJ z`3y{bs(|9EGV-s5h`zQH|DpO<*yC4mWqqC~iiqNjD`z{dtu8_PiXOnt;?SlK@R!HG zgHcj3XbBk=w8E2G6X}EukJ&>{w&sXHT|{D>noHshlW~+umk6(_td{6R+W$}n=AkW-exvuNjE5T+ zpD*E^+v2>c4#Hxw}t7I4s=LYpHy!3j~BoZ+S_m*I(Kjxo#%x-hjM56a23AFN{U z;mvrg@Fu$B4pg0Sbj3M1zK`*p74$yK@YLi>c-7s5E5dmQ!cgq^i|Gu^&h}$68>9D? z5C?Z|$K&ps=!?1~0k`L(+b!T0x&qG1PI#Re)ZZ9E^!723Z9dvo_9IxHL9p0^lkw!| z9W%t^0W!~4klQqg1H0eHAJ-qoSDa_j6AfZU)GKBtat>#h%;3+R$FS__`B+lA5#%S^ zK?i}U4?@g5+K$1g7%b-?p5-IB+(%pNfc!%h`yP22zf=1f*5^LJ%3uXpBri1HIId*^ zETn^2Td(Jsk^lf807*naR9udD1>o+ac=dy~a3nK@9=}H*l*POZ(Je$p1a8uiWOr8$ zfrO9OR6bP>slLR_IiD_MK0GIbe%2=LCy2yNZzO@qrwdXBe;rI`AQMT2AStO)CAvsp zjW2Rhc>%nXFcT4-XiFwUlzI6^%?G($VQ@rzNpVa_K@>$v?+BGtBD5;_(2P?df+>*u zv!EWSn!Lt0B@h+IWKJLD83Xe!hr5l|cg{sB+n z?c8DXHTt9x7>-;0&lOcXS%q}bU=dy|?7(MUKZMNIF}yXj1OL|f3U;tzVF3G`2|Qlf zj8DD!Hs)`;9B)|lOmk)2!9zpi1#wXxV$k( zY&x9S?s_^K*SB4TfOp}?C`zx^@OwwTf!*vdZryVwu6$|<20LcJFUiR~T8&W~ox}?h z@8HeGJ6P{*$5wX;bEAdm4BOC%>Pk-pm>Ue@{laeiYW&Cejep#QJKy~@I&Yebc%F}V zi4V9rIJ}QEL4YSKIQY^wJiPM>Jm9~G!}$=)>&uV{dXRLYWPdtYx`BL3Dq==|AE55-+q6{p_$PKH! zt$t+%SL)}+bOtiaOz+b84!P+Xyu94GBXz`7F@8r(c?=FQaGdmvI!VYLv|5xi&1fzk zxN1ZpsvMeKkA-@wkRc$&N_@7v|a|S|}ipF6&CL?qsY|I@*IqFA=6$Hd3=0Y=6%NJWQtYYn(5f9+O%qDzr>BRG72mX2R`&eC{i>LGNVjrEvU}FHas3tCw4wSL4egwbH{si|8Z@?ik zg2(gQ(Od5k)idAc@mz%4J982If&Os-oh4lAKoe)#Xvj@SJq|;b*eCmgm9kkyBN<;VokUhxAk5N zdYuPnbByQqzlH<3QS`7r1W`kv6w-S)O-ipUy;0j6w*j;^g;%=7o5(A~B5yScU#Tyv zuL7&8B`=U_i-9n(V>%KP6}huo zf?|mNs22<4nPO-viX(B#xDmbO9>FW#ZnV|9Ff-~yEv^f`Ea}e!3^WFCAR5Mh^&bSB zB4*cSA;+@fwX1Fem$PoH^_L>Nwj_w>yw~b@to{OqvH{jKu0k!E!f-T#{q7;amoYj& zw|Q2;AX|VXQHf6~2~+@Yths99QKaV#LLbB42-^nwxO&zjT{O; zoT*}>7@;@r#WnS7;Ksfb>!D0dA$pk1SJ8(K`~tlT#n1Hs6$krYco)yRTab4PXwbT# zKh-*fnk`;~jgpJi){#frIGQeQ;}PuHb;#x+Q*0=P`D9w}VOO+x7TD@Rj_3=Am?TJw z%fOUIRlA zz+!aJE_BA-vd=?Y$@u7JFyxQoi_vl{y?hzs<$1X8HgNpFUcAKKMQ5WEEMV}%3}&(! zil|MJe#=ZxjzNpJM`N|gc#1(WNq(g#aX>-`ZL|X`X$L|QpyEtn+zo`G`^aEUG>9B6 z$ff{g>Rj+HGnbF>Fq)qb@@>Ca{Sp@5wG?y~1^H$L_m4h_W7$dcpjQsLsqjetlNWB% z{Im4an>>hZAW7Jj_9B|~J%+`tg@huASs_-7LCT9^ayk!kBEi=(g>S{?JXo7 zi>sjQRFRJfH6WI-pa^4ONtmUfCjwqT?cXUfF~Kos3h#{U!-Bnw5ntuu=C@a2=hQ*G zQP_h{R)!l8A#E0`L|JyN<)xq@D@0)hn5j%CYz(pMW=g(H8@~l4QWQ8k+`FM`<2Rm0jDsnk3cgK*+@PC&tDyT=@kDvmQ!F#WI20;?2UOJ27oye1wHrkGgXduv zDImv@-;UMgR?X9xQN#V~x&+ls4P$-sv(cQ}tUrpgY<`qY+hddUGwp3Z$go-cImi4$4Qng9BTC- z4CA&~iurJfZ6w6Rhg8u5Gevo+UACTJ=NGIHXtZ?BQ%1t^TFm;L!vJ`4;{nf-x3GG{ zLJZDZh_25M;LG2>3;Ei5Y|8II=yRm0FQw6#EzTbyB64R=^v87tq`Bjo{HCuBGx?Vq z5hG@zQF~S*APYoDoN;f#<8DsCCTKFbq3CinwMYQeE@umIB6Sob5xAjJ4T~cRc=e9Y zoPRo8fOP{`qp_v{e9%Dj-f_Gbz75YUz~lTuyrj#SBwaZhLp1%Un3qoU(&f3ZbF`$G zp*z}IL8>`nX4++|#TB>&)4eNa;}=G=aZ~Lo+%@Y)wB0ciaZdpEA%=%ve-wZ1`~W4l zU2H0xzf337hIKx*vnR&EOp?gRPRcJ%-JZ2Zy__hQ-6%dpMehr{kL8g3{V{0u*9B!6}k9A1Zl z>b;oCmNgF`uS?1Ds4rBaz;*h)*t&~$%p0q@l+cYa8BAg#tcdp|`C%Oqo(idb2S$cW zJiqG0BwVrfYs2)AI0Bf1WOGXr>G_TsD6^#m0)brYxP|bG7 zq)0p;rV%H>QC3Yn)7$i@)aEOCdAyk0f$Yu)@N0kfGw|;2hX18Dd}`+{xMAPb7@8av zJJIy>VCj$Fvh!3=#N;9^8@i;-OqI^820T$EYlt302(RMc&z*n7v+iqR3|P}*@(|QW zCabd{g}M|52o@reM1tpT2Hyow`U4OhoyL{2C` zn_rIa!MglPOmzhYO$;{uxk#Ta+f=6eOG{f-gnwN0r>#&%xw{jXkpb%ofLZ`NPjKMP zo%q|yhw-@cD!SZ06i5M6+&Fk>rHdSD=q_eDtkM;z={*RH7?2%q*&fBpA)0fTCZl0? znAVi%sAn6~KidYfooDY1ET{LiWcO7Zr@||+#6Z*z$cX+~N%b<=MEoY~l3s*ayOgYi zX%pdlF0Y92>#yXt;Ro3*IKFojU;f7Du<)8?z!DdknPm*JcEy(0dU1cEW#=LG(%4Q% z4X*G$2!|7lxtRRkVFddF0pHCVoKIih)4;H60%JH8Ja*)H_OePYw8`2ntq7UH(jT9oeVM{h5YMM|CGXedW( zqdI-c*mO6ZR8C^&DAO4NwMdy#&OrWrY&rv}0N4g#wT(B@Pi$nA70e{1 z9;Inil-3(D5id0iMA~4q3J{~ZV^57zlA!M)47G?N<5F}v-FPjt7rQE7!*%PI;JVkA zV_x5E_~k5I&zU}&Z+06^E9FU2_hre4&(aSU9>tMzX@1@V^%&wFhPFjLI9?vdIGaM* z;|!0DMDa+&rxkWiyiZXQm$*#IbbgcoyP*Y4T@TbH0)BoySpx4`31}?82|QVS9b3I) zSQ)Jl4OUJfy{fhtrK@{UpV0t)sR3Gx#Hg*%46$}xTU*rxcO)?;W*rM@F(}JlbGrya zMAZo6jgrbS0IOn_Kpv_!?& zlHr>wwneHGw_RBzNp+`L>UUSgrjZ+?lXRis)$!eUBi2{n!oc26k*P5RevG`Em1S{~l)-iNNZgjp z;FyAkj-B=E>&joo&EU8D%87SuVR8{#rLfVFMQ zg}PquJs(fLupS$tH;_kGbdU8rv)`k%1jk%3_ReimS*v`SiqzHbJV6f-VT@F8gpHtv z2<=WA2HiOVQd;BC4jQLB*G=5It$hoa>9~Fnw^IlzV+mwh&sp?rCSC=@SuH=Wb4+oZoZqk#&a|yhIwuxCd8aj z;ClMp0WqBa0UH+yIa8sYE`Fa3B1^?wrHR8n_ywC0?0Ct540PW> z(^^`)2%2^YdEhGEx2D;Q9tjU`Y>^;GhCI*`5T1)_wvL(g9-)6IW-;nDfkhLck>jZk zTfH(tQZccP36P@@P-6DXI#=94xd!2)dN-~sEJb5&3DJ=N#rGAe9vtNr9Z>>~T#AM#=%G<)WZ;Jmj%LRt7g!Jp z=sW+%$5umOAfaRpu|lDQFhN$nLSa6ksU8|{#9|W}Xo8Q(GzgV<(K!cWrg8?8J2ZI( zmcA~Vuhdr4na)g3Y8ZMQw|{9ikmsj+gnlxcg)ZsBHXk$*A9ieZYmzb`I~Iq5D?6$t zE>EVb%79w6iodQ@3yQ3AsG_qpp>$H|uzj$K`jTo9C1zw-i;p@=c}XYDuAYoEc|sC) zak&95f+=`*reairnwaM2C0^c~qiPf36453+Ar{%0*chO46Pkors!t?di$HId-g zH;y7Vp25MRJMlExivGA)c&EUNuqc>=#eMU@W-!E?Cb2g-0>6|~i9VAwI-ZnT;EG!u z70?mo(G!%>6_+p=w4Q zuhpTznmh+pA-D1zEWO1cm3XJGw$`7fyB=HblzyhfgR8aOeh#ncLGKcf5rB1{s(cEh zDI1xbacnx0WGKl|1O~eHZ|@F{$xL)K?5b=o~3WG+}W} zw*lq!i(dh&K?}_ ztHLNn?jT`)35AZSG)dVriEpy<6sAmpX&Frzk06aBfr8j5Txvp49U4>PDeR15`m1ny zvq)xsrnL|A9Hi-YwVs*QK&%JzE@JoecG-HMrW#_^_8ExbK$0cmWQUI-Md6dcgBnVD zt!pREL=}jsHGHL5GUU(6aKB7E?Ext3C>fICfu@yVl1+nHiL*`x4coWoaAHZPoxcQE zRLbYF!ayb_D;Q;yVz`Tlp)XHOU?wv)f(TmewfcCB*W>*Fk|2u>Q$R$}*XYIfGH+uW z-7Ajas1svBv{0OzC<6?zZd_SjBud|86xcek2S;<0m{}^Lvr#}#rHt;lh`vlay8AlN zHM{Fmr0YDh?|U^A*Vl_dQZ zWF)qA%*A7wxt|Rwu{R55U%2+hcUtT83~HLc@bd2a_6;ot0$_t($@;L>nmWN;umms{ zib>pMM@4xP*+a3X!>Ce?qsCyQZl0#pAx3PH7@)4nN321R>B{>&yfWjm!bq4hOjiXy zP}myzI&v09dN|>%93U6_C=~KyfN6Yu5|d6HS&|VK4}TUOVtH0rY+)*<1zfJa`1lWVb^nS<3aZYz{?5m#fjJWzc@8V z5gPTTl(?oh?X>i8Ee^6;0 zjF~bl(ak;6!_mhrb+*fK9&P2Kc>d}{RaBB36&AY0@{GFJV2aqDyc9@Mm5EKU!8&lN z@)OQZPJPU9W(e3lpTQR)izjD2mXdkE{33$U3Topu+_Gvl+B@0-j%qJb$q-ZZfuR^v z+s1Kt^cY4OW0>1H3!S%iBa1w$&kf;QQ%@tzd1!Y^n208&$DpyOX7T((<|I*qdovyH zcv}2al50`bEvF){Fs1xyJJ@z1gS2>zvuqUhdb;_B=?rB1G`D?>#gaTk#f2Kjr4xQh zASbtGN~oeF)y~&3d5Fd+$q*}@N!X3HbdOMlfGVER;Z?%!=ol!7cIfi?VkL{0md@Fv(Q*P8>Z~ zqY%>w4hK%mCkR;2(BBfnC{QQ9_@j6{YDey+)|Gk&}r$T<-bS}SfEufd2}A8Q zq*2HUR;YU-DhtuxBLz!O<&=qMNQ}Y$OY%FhjU!chJ*P%tua-8)bOyrLcL4mSPx4AE z6V|rZ}v7mYYztnk^#ytP@^~zObGEHu52Lzit>aqousSY4pr2en!(;g+e~-V ztM&nQ#Ro9#PGHQf!p~&U?RH_Fw*X<0 zp^fEmt8*pDx(vMiE_VN5J07Ahpjhk>ugOP)xv;769yAH9q-jJ(u{UC|0TQ02O)W41LWKQU! zP~Sr-S3;NHh0C2m(edZRdW{^%!c*KbwgkQR_6iW#_;(KD8&ls$Q1k>@#AGxn*hK`V zt>~l&qDdIZRNaJGItQVM(-3l4m?=AivOGfyDq^k-?J|;$;XJSJP51IAGZ1b~CjfMP z(qlfAkuY=NYS-$Ri9quxOD;>-M~POqw&~PuzAM&xy0yVMN&=Vz15FKAJcdHGBSo@J zNy3K(F(AYQYk7N?_dp23i3S=hl>Eb-nFSiMq7K7RWEYGm_C`*_Ly}$B0^&K!cxiG* zOtmffU1F#B4qika;4<7figvD^{dbd}}t2){5vk zTrW)bQYSMIKD7QEfS)=O$9FO#F(-&=Be6Rs>T+lHqjjRMe@jMcQ{~*$Yb4=UvX8kU zzku1XWq75H!;~b6Vh|^M&?`cj=FpjM6Ql^iWFT-^(%_^a^M)aFRDRB()3D^gYQh?79z3;H3?S zw!DB9u&6G%Mzaz#)}`mg9Z{aOlTZGCKsc#F0qy0IIBT_f164N=W3etL&U0fi4$SF6 zJx0($i&);6gUiBx^wBnK_x9m2Z=-;HbM_G9p}3Ng05+T}r480oVySsr8T)eY>Gam9@LdokCMqiI=GPI%+pePJOdl0Ij zC9(Ogn}2b0LY{(`H%Y_Va;{Jp9X7D8Hc?X@yi++a) zt*8pGAPq%?QfkIu=t7#(u8o^&HjLKr$rNZ?A z?myjqk{^f(J4`gy@?7@w*ibiOhhnNMET{WiRbrNPD>1vGcSuOXh!wC-S8RA5kwX@l z&aCXBV+wB8!CJOJ==1fuZ5Zh4LD!s4cysbVkAv8aQOgF%x<0B~LO91c8@w+Nww!}Y zV26b)T~g&C3QA54*@CUesz8&v9j^e7u|lv>KSn(h;+~N;xcZu9V5>aPf0)EKk3Npw zg<rUU=%vU@SJy$5br7U&_M1un+p0~Ht8UB&3p6UYwcke`{wq55$gc81aB6h-VW zkh|PK1%j6e^7&YiNDzhMmzA+3)FKLMqf2$v@OCi~F^*;{SU0{1cMPsYd}kK!OTgn> zpT~ymcJ#S3QH>iyMN6253Z+7wh?^dWHVoDIP{v_MkF1ebfTg@da@Lzakqgo=^qRJM zrPqfU$ahtk<$lQh&e}Si2zC0fE%7`i?+_oc?9hwFG<#MuyFnG0PKGzaAOF28%MFQeJ*ZUc>|wp!m-LQrnvHx zBb8SrZc7`XWUPncF^0ONq~5%1SXKy2D9;WPC)toD|}@jPHO#$(Kv`bA%Al_+y9h_1a^Y$}x14 zoGj_mK$UG}Sf%q04M;Qzxn&p$U#?{wEow~Dw}mf)weM63I!?nt`1-30-9S#gk`Iru zrb>|V8iG#L5c4pDERKn++W3@V!kNNzcmuu9mr>c!z=7&fY_ILc0dfo@ZUxm$go0B< zm)nirTuBIMmfXD5k{gaN9E~B)$xzC%y5*7*$+hw;-5;_w9c>ajVn&1=$p?yBwvL}3 zy9rlcyAo^}0ePT`Zyb3H?`Mx;hC2flD*GJ?&PtEg+KoU>_^fjbYaX2?1%YIo)h?J^ zl*Yl=H$UtSr(+=BQ)7sIXXcpB?Med}sUabLIKYOG6*?SDV}=cL zm;^kgAnO^v`}UJ~DzgPWUO#HI zA#hfah0e8sh{8}5(YToz2Q`{~`g>D1c(7rTNYI@`-) zzGP_R1jgJ7GQ3~Hl3XImEs~)y$Ko$GHtAefVnd zJ%pYkO48N1s`3o&F#=P@)nS#^)X-na`LH`o3C_x17r77lFQ;>-(>0KB0N?s#$8#Yq z4Wvt&UMb2UXx>;&jj2_OB4H>E+K^4QST@wC8A*+mL;_H9@^HF*V2WY5I*OVTSU52! zH2_95Yl`tA%1{*Pw`N+=w{xgVBNO5alQ&}JwJSlFFyOH&{^scSu**M=4yOZET2lyJ zDYVbMhUsnC9>YLLVlyAE`yB~C?sUd+QJcq((*@+7u7U9N2Os}uL37a3&QeIZ*5u0*ol-h}7Yes?x zC=Enti}DyKc7o*?f}t8FQCCcp8mzDS8LIi`DFYx@9wIrqWN0O1?YK;xn-O`(qHs2T zp>!9VpJ;=#+r``KU&r5)Cy{qM#3ju=2iFReCaeqritkDhy9tk|l5?BckXro?V0sDj zi`-zi_QwOKd*_d0AbkCAAN_dF|6)zG7uNjG;)1pRMS8&0N?PNlm|Yl%bsx@D$k{}? z_CHnB}}V<)xd zyB4IB7gRe65}#EChNezn%4x`%5lL%eVELNv)Yab|=`lP_eM+Plj?aZLb5ppjx(avC zxf%2h7tTw-gYP_v=Q1+y;B)1?6y5CIWC%zN=rA!3i+I6ctmr9?;}S5KKl-Q-_&5f_ zp(k5D#^cWGn)U-vi>9QBuuWcKNp=|x3!53x>KhO$nCEABT|m1Fj75T)G$P5X8=`DY zX{4)F`hTTxr41$LW-yhnVR1AIKcD{$ocl^}_W9WS!`JZl&NC>xo#I$TQf|0&rw<2> zr?bqz1-gEc`S44?IJmsWpM2B@eH;Vf>z6;~<1V<>IvJ;KV@YL;rIJ{h&Q+|igmc?= z#Ch}=JAphyG+f6J9Rqa|;TBWe)tz$DBy^K6-T5G{V6|`L0(3iN{Bn2?y6>4KKCzKU z_Tg*wM-loS@}wZj&bmUCa3Ds82{9=s*apOG6KOJlOD^x=>$hxq=hIyu*FbnK^8GV% z*ynJaENoz@pOZ8`p+K|eAd-ZhF-tI#FhnFqAL>>BEtf~tLC$;?1NZI zc+vxLRnEbifyfX9nFwF3+=Rt9EC*e{z49{49QkRu-_={eu)WFNS`Jq2!* zS9Q~nka~OK=h1YSv^Bmf|E@74&;*!W@uaRdl&?6q@ggbojwU>L5CqE15D?xRQ{a&_g({>QTq zpGcflo0G#Y*6%>i=Vplm76en&&-beJ-(o&jv}CbXrPkc3u>TKdv! z2HNYNev$`&QUl><_@8~k=QuCRz5tR7z6s1t(s)(U^b4_XcYr+2ptDfsKrBQfH5`bK z3qm@uWTqpjTxrCv^~Gs}G6XaeOxYvxoj)E zhaFZSDMhFACuu!u0}pO{ z0_!u|(ChXKam?ntBeE)05u2`0!&3bYcIko;?e)PYrO(9~G7!H0`5Ahi3u=*+u_Qt| zhG|~q6MVT$5+YKzJGLe>p?X zb0vpjN~7N|imGBJjVQx`*0j-v0;C0xfc3}GyS zM>jwhO8D9EHniV80K_haALn_;!vbfOB{|dYh-q`RD&0X>q6sNh7F)LimnI0&UVne4 zfVF4JK%j=IE<&6ZY5Ly$SCZ;qm^pFFBj}lpE~f<2&UHF~gSBG}z#bI4wDS(m zDgEU$^<-z#K=`^3lC1oqTqdL&hP{yXS%`$fenq^)^}P0^F) z&yB5q$A{+~m)QDAOv66=ao3qP5Kiv!#f#~D+g!Nm+gm+_`I#ghfcGspl%$K5(Od2W zG8EOJDypO*6LdxleBH4+=S0kRcmij&s6L1r5L66dU9JcNU)atS>^E!Yw1)rj5ffd56e7 zRM(p)>7_V_Vn<%En2e2#W6YTn>bH&x!Q7-pD1Ya3<+%`@tcWi*Zb0WfGl0m$k;ix8 z>+z$2pF;*&;XRCT*d6jF1=<8oVq>hDLaa+W@7Q=YkmhH@K=}F%HTQeLu9IS%79J?i^i-g*RRSHj5y9WDI{h`WTL8rUai=jnGO}c0Y|x z*MA(~O(u;al^L)^~gfg;qBz^0#xc z&b5Jz0=PRTX?ziMo?5g_eSqpp}v2M~HNRue;Aj z9(z6v1gb}J4STCFZ=5>*G@P3D!D_6#VC<(2mq3VfE&T+E5qjg;(26Nx} zV$nHY2Ey0-%1ADNa0}CiR?R>~GY2BaW4RY8&kCZ=rno9B^g@X`(aA7 zXLFrT1A+1+R|7bD{tkhpJqID_+el(4rfJB3cZiq)=|l;h&;f}t*{Fef5P1i6W864= z1?Jy84`h~)`oqKcyU8aooNb`pZO4@9cSH$uJX)m*bXsnBxJ=%GABDW*j{H z4Fqbaeg%Mc&c%z7rn0VFY4#G53b^c9!sJ#SUX^JMGqPP^Z3GynsIw3({I${WRXr9Y7z~@8I(tLfj@y5lsujusY^xF5onu)g=x>oCL7uLf|^M z5DbK`VdY8AD3Wrt(tDWeM^$^XR6;G_C1C!{&HNj?TUSU=@Jf7r?QIbs-su zUSC!S&2zF&s?ZHHN{MmJbmL zx)T$W^F^S`Lr|}wC)a^>OI9IX3;1t%cy-gOcr3FCU0$!qJL-|lJEUfLXn78+?;+|~ z$>?kXLQErcFCi!i_ZC0>QA9P*^}3J^1Pao;48YgU#*7mI=L z_19|0Ib-F&ljkBycw_e;G~6RVgf7yCa!(0i5QDV4@aJWK=UhDU?$dZ9yB9rfuP_c? z(ly>ge2>I;32Sf8J1nFw5-pNT2xoObktjiM9Ikk1s}|3a2I;WY%gLPpDU*?q08?@jWz_7geT@Qw2}$B?0>|E6RdWiWpOV9_Vh>Np?kVlxoEen%O~GiUmVZMqz4j3n_MJQ(6yA3d}UD7kR1 z@R2`Qz{q2J@zse(5M(6(RW+^|?-9}@#FF7vs5<76nJMKGwRJ z4MeX$2e25x|M;X&WCb9f!AE%Zz%p|??ZUreH-mg2PWewVH})^ zT^L4F<;s+H6)-W?6f_kpTWtD<%(9^WniF!yA4 zG)OCzp$qb93)XJ1S!(3gW;@~ExZcMJHi00jVb=QvJbLg+{55$JMW;ht-VFs|mj+<> zI${xC8RCU$8iGk)L@!p-g>9`7r^m+>{U?(HdDwMAZsqR_i(Dkn@|6Ta_^~S z0~0}Dsmp3I23L*LN+Ew2p8PJPnLQ!t%#CM~jv^aVR7rq#uMb7Hga!>jsix#%7U)pp zthhngBAXUNi86{)Rjn>y&%wtP{s6##zW|QqjI5*VHSsi;iU(<4y!-wa>VxBNY=-`O zvw=V>Mrq0)L;Zuq(`cqe0&y|(tJfnL)VMCXQx;@zbs=sOO+(icr3Rpqa;^jvPc(z!SFeqCjk5zfalNm!JO-LsV3gdw1JR+ zNkHx;Ae_|BBTOjS{L00EnL5); zC+R=x24a_ZIb+|Q1myFCkSp~(x?|`^6Rxb(vslph6j*LN(ED9C~G=HI19$XA?ACPQ9Ppv5zZqI*~PJb9AcRpK9Yf?mNEuzAlEaA zJ(|rJ|DEv;el}$+>gQxyae71e1O`G?e`2RHitQ?Ndgsz)2Rgg!$0634TYJP8!{5ng z6qk4}2T(FF-ofu?U_AilfZ;L*R^n|Z0G#b14~TA~$AlD*0gqDk6{@eg`kGR>v%|_O za4CrqrBDAJfP)wP+5GYP@%r)l@%r)l@w)gIfd3yOOC?6A0WqTh0000$Nmqy7}Eb{Peefn?`ir=(5GbZj(JUp^t-WE_EjO40LTJZ z1fWx!t?GzBr56god}~9xoy7cSGk_id<3dE6%xhXJM7(FWAFZ|92kgHHLTmF0IzCNX zbDz=X3-r3;bCZ0A=~-wbLLk|xTVr3x79&^!7}Zb@0(u0%LjWED^!WeP z3;TafuvP*%0}!jU)+Ym43ZNx&PqEe{NMIdee$TL0GArtmK`aDjwa89^@wFt7Z3V6B8HUlE9c*R(M# zB7x8%-02iYV^+jJY@Vzs+zO%B3&_WS=odokUq$~*P2vAW!MYH@C4jt$eI*hJS@u0$ zzA0MfbM97KQ)l#4x(`vZ9X;v0>F*j9(B2>xma+luM?_KW14 z`+aCgo$g7p`yNgGZZn;pw30;XF7plQY-WeaX zNZ*CSx`h4FY<9f{1-=U4lN#uC8#t0RR*8PjqT`_KbU#-MfWHB7BOuNK`#=_A)cltw zvy>rlz`Di>Yb7P@J&wFVaqxj#W*dt9F!Ljg3DY7~eem7 z4Fiq}Z+4x?J89DPeIe4vfgLpMezX;DDJY;gRYSj>rT#Gh6I$4#c1BW{=wV4`=3f=6 zRGJU)iqTiHL3vR2a^Ti`0Bizq1u4|MSb_yg=+x&<1wz8za>;IX#9ol}+Ry&50^wLu z_-}U09OGxx%`drnbT$e4_Bm}=@R_>rz_%j+>(Kr^Lj-%^JM%pNhZlfnGQS+mk7Q? z-xH$jc*5gn9j4NK33lQIn`p9%Mm-5@#EnTd0;)`yny5xVU!fuXQUk9Buy#ML^+8!j zn#QVQfqG&pp^}V*JKf+kO~#8)XbRY%2<=Ub`!RYW{O4ZRLY9~>Lf%D0H64q9Xp%M1kV zFTtLiAR&vC@cK2<;-n1p8Ug(yeS|5ABk5pH1#o?ej!(VP1dF|q;J+tp8pZzy`6#Ji zfkjl8MTV>;*z|baBGT<@?|5;fDQ$p|a4JYTcb=2WTJe`7Ada+mbEI*&r9{Cv=@O_Y zCeTVT7N$^0VvM1H5tvkC0X4`%qX9Ld5>iDBZh#;ZVcl}0wN3)?dI0wxoZGW^>qrr- zPXM@lHXl@cS#_EOp0voX(W${CAIY&9G>akI8`69tb?CKl=8oTI$#SMJ($?>>ib|Fu z?)0ABiFTo?uo^a(M0R^5y%H(`VihBg0`?$JKDp6wHOOHBYS1d%v7n&~OY0XPGdhCX zcHWPN#Rw*hZ>D{sC^X>90NexMBYZs%>pI+mHHS^#InVr27k0}ataeLT!ziSGnxnu6 zB5k7CcX7Iig~zxokxn6tqE3~Yx1#1k3764R-V(+E=xW|mkvmKcg#-nyQBXxppg_m0 zDTdSJ#MdE%`KZOhOgFk}=VN|D2Nu^hV_s7i7PT%zOMNF$*9g?-fF!}nA6FxBFR;GebD&j;{24tx&FI-G)aE`VzRw9Wi6=1v(ugl=SvY>*_j!ck#`ODhtN zj1~c`z_;C*G-(5ItF`zw!J0`Myxzu!1{gKUB{3#UTdhM{P-cj~rAbs082nUeffzCJ z@i^l~8db=k6;+snCUjOcqO-mYZ58!s&o-bv*M#}4b1ircq!M z{QrgKgd%LcW(a*4My=?!dRuVNp>+zNXsg2$f#(RV9|Z6+0Cya`TR8nXT!MA^-l=N` z03>u#CF>(q@*e2@H1H3Ra?LqE4*#Lq5hoi*Qs?q|QIPl3QbH=D#bS)AaVQjNTg4zT zU5y-?P=gLMV{uJ87F0E3QEfXGGwL^@pNHDkE})~{d>E)@7MLXW3D^x_X90V* z_G0sn&FJ0PgNOR>!Qk#~c-IR)hK{q3!JeyE<13qgima$Z1tJtDY3wOi6M)+Rd>p{X z_Wf4v+d3SA^>OwCX0>$Snmg3o!wj6qCI5%CPJl-0 z?A0~?_b7moJVyI=VaISU21a_Yt$#gwcJyOw-&Sniy%jz2PHe&e@(|dD9-M>Yad}q< z4DNW%E#Jc*u?O?z631Gda#4VxOB3Y+;-i2z`tQ?+=>F0=)Pi+0EUjhMD@_+C3|^+b zQ{>-sx-0$Xkfw}IP%!3CWRcSX4L71fHlacZ zOh}*N*_-vR0M6&1L$VH~U|qAfLgjF78)C(Du#$NS=DnRH6Y29jHYkn$RLE(N)=mCAD)gzquQWnmaM4sT&Kr76A>NKvNcI46ahl8fZcz z?(IkK>aEz)zZo0*H(_JnCT!Tb8QVttumyeCjS&o!<}<-Y4ms4I3YDmm-Kdros1r8z zOe_d((K&-zt&4l{hR&DZxYN%727zz?@=830-RPE!pc4By04af-<|EE=#IlU_l%Z945c`fW@4T-bCKa+i3t57kFHD;{(k-|a8%Ta z`E4C&ZSO#ReGC1F=2n}HtmUg={GMU+6JyVgEf{=c6E+TP!4m`Pv2CCiy~F+3KHiV* zg?`e0!x+a9CJ>YUtRex*p#fRhf=1(`nRE^5(TMVggpV5M+rjRze6ctaV|Q^Bi?I+N zJMC@2@xZ3*?!jk!euAb<7i!`vj8j^tU`;tg^8t$|rSt^CrlG@^UeAn>mmTbg@lXiX ztpJWXu$wHoSS(N}vCf438Lg333VY%qyrt#2_~1F0TR!PxHd{q@S9SxVPj5zV|3+-x zz6M)%ZpBkOd$3`s7hCc>(1(5uV}i8L1SPjC$z*1bLxreEC0fy@t>2bNN$+Ho0W^&k zjgg6M4j{=0LfRqN@z*J@UPlr_CKy(Icv;g~Xg+B<3HN{8@ilD37#5197|oc-I8Rz` zdM|5M%K)j@F(7@=*1;C6AMU+Sh0$D^?@Kk6Epae6(nLp*hELkMj9^CD8&-rQj?}T$ z^GE~e4&d%;9+0R&4w0c3N6pBJ3>pxj z+2>6%6geoSx5UcA7=oaN0uotZz8+A_Xih{ch6Ai7^`VG1`x6)~PGAw{;cw4-4PCKq zKY0W{*>yYG#e9gwq=Jfe`;J2In6WM&AOiEKIapz;5JkZJX}-Fruxs+_{l3G~tb-|7 zMpHe1@4qG{{VvMPp5Xjm0#(K{upyyM_zL8OMx~C>-rPmCG>`q}Pq^f#Z@>`7AyH47 z$BK}N`;r%->k4iY_(0pG>#>kLv z!=;L(#&BUb&Zu04x19T03Kkx_;b*vJ=vQcrj`9zVDwm|P>l0F@;n@-Pc<3CaU}ONe zfbql+@9TY2#h@~yjepaONM zLM;h})-i#eN((0QDJz(h0s}GS5opH42n#FIn=?U}f^W3Zeice$iyFeEt!JU(^!bFi zfAQw8U;wpfmN^un0-LsjwhUPP(F48l11(sm1NfKy*?^{iS|Mv_C5*ER;n<>y~Ehe4A*}#J14%PWm%l!k*X1*r`8Y z>p%)t8v~$+eF?tc31Td$Y64oDfgz1;gT2Ti$9!_@vnXaGfM4?h&Vrx=|Ek2?1WU0% zrC$cyqsgB&VlpAK$ix|Bk{CmJ82`BRGR%3=N?=orFZ|b)=tC7+k}Pr7bzlPBaU7&| zJ6CldkaZvh>sPRN^nR_$m#y3-b-!t!7c76*Xpcl&=e4V3$6TQ)}T% zEMldQFbU5cO(JYLyfHcSK5wVU0lvw2JBK>`v8a`jmQN+M(N<%4^-&i?KBpD&Osqy`EP-WvFVZ{|WlE{G8#G29)#mDaoE@rt_awXJ$uLB`i z$Ih0dH=E`1f7+KjV{$Xs_<1gz&(g9t>fOkS&ux44-N8 zhs@2(Sl=p9RyZvsM*HZ{{20m%{q6+Km{iSdD~X`UtPBotL82k~MG~9z2u{L6{Kc6s zBwca$9XH^1{0Xhmd?ZG%YRiyKM8cbLep9ebChxdXjdYh0*BrSTq`g}IdLWd<{t8wD zo4^CM!W%Av|Js!nMTG8XN$)8FBQ;@IwI(90ZJ`YFhFv=`gd#H1BILoBEu>^jVWP-i z@}|yvIOQbf@?%8$y;=A{CaB0G!3S4fhWgXy08ftN+xPxEq?m^aUE|w>D0ggrM}mF6 zG`Zzkis&*^=!?$uUg)cI-2|Y0@4uF={S~ZlFbnjMEaym1S~|S(_A*ZBA{x7Ff@PA+uQ)L05iWPzZ6!+9rELtPKz?+>aAB(8 zsRx4O5-p0*6y?=g(SpXNcCw}eL;b`KO$@8R2#-jJIzv^*O7M?lkTv*0z!tThl@H+) zp~>Gu(4B$)lp^rfjhVG(JZtz~aZKNR%c@I(Raq1s-j4r#=vxqSF6!C*8DGtrTj#sW zL~{ZgoOQ5J2$^3IBN0VwX0@;ojjx5)LD9)xt-o~LdiG`Qk6_)lAK#tDvX@X!GKtT8 zk&HGeAVz0JBhc0Xi~&8ndWdzE<{hU)vDtTF0d@bT`|L9wi{aEOfvOl#8DMKXfal;O zoOk?-2xY(FH$TL$u@#+}R&wa`Q`6aJ+_w|{1H7vDht|BWf@S(AEI-g^VwW!aOHbiL zlzoFu7$NSIvlB#K=&o!8noSeOJo-las17EpG-(=hA0$u$CBX5UrpQi)|58WVY1f%a zQAV(gW*ZTvluO~g$G-wNqY`*%6hC<6XV9_>l}VOJd{QUA5u1;4wVDYUE6EI`EZcqX ztR}Ao0RC=YzPK-fWn%s>?aR)lUt!kI9g-6$i-w0W;8d5)7CTrIC4?f5LQAfiBCh=S zZuF}m(pVIPo5(M6utB;~5l(`?Qf7er(f&BK=iwt1t=Ze^X;(yGLqcXydw~nJ{q)*;nzc`vlzkDb zchBN8d$Bli>$vD8fI}cyI%^pN!R1O9cMdiFmk}pY&9(;5yzS{F$45I;i#z)W{Yg|~p5^t+L6Dw355|vn!E2lpK9sXZn%7CQQ@P)pr zE)o8;t-y?D^trvc8hgXXG9}z^-J6Z4Yx2|YeiObakYJxeg$kb2JDS*JF$G>VstNB1c(aU zdChn5#-6XEC~A>StkW+QH@ejBn%Sl!j&mglp0MnTG)<|gb*8)WG5wWZ*Yd`drT0bo z&#~8Pem8vi4*+G2u%=miBUqR0#b>8jsc@wLpzAgjD$X=H9L8JDW0+RO3gJ7!nzTz7 znlP`mld6gPcJ~qrMU^}TtCFRB%2Kd1-Dl$aE4v8Lm6d5OmzO1eOxk)I#GEkSA626` zCAS10KKmVHOiXh8%rh>*InTMwrhO$>JU7~MvZ`C_v8!hgb=i7k@>zQnR2YJiWAu%0 zb&9_id_dto6P{OT!@0ECuJhQ^7H;Whb$N7YaLcdG=5Ei{6dD11WBH3y#TaR_L6@js z9jM`~;Y$gDoUpwU!uMU{#>mwa3OAlac;wxU4RPJo{FBJ6U&FbYhjz3yc2V@Tb+`|) zNj{s;MlsQWy?f^2g_%H&QlCrf^y2$-WTt~}B4WD8K$35Fd}Rk}O)rK9AZh@zrl@t- zAh!PLkGS*3Yq0b2wa~2q9^ZwpJ$WsJs70lD0EE2`^n=kcbJO4n>7RlDhP zpo1VC!L2HFpeIKyc8iMlneMfmWeRrQ4LCZdq8X^Gp)#(X;T?!jO@fuU=4Q%~kIT(8 zO13}w2xoeYw_)lT6_C{$b=gL&AKr|6Z@Ll<+gDfr$ao7c$ZvKwsb^@xb z>163O@ej;msgpMY_}a85&ug}V_4lPavKKuQmb?2m?YpXErbFp%aE3_H^JU68Lu@kQz+$l#-3I=AKw74B)h>?LTTTwL)aV=q$EG3h(Io9*R99D{K9RDWNo;VNcPs8~3!`C8%M(U+%%(ZE5l4uK^C~f;s7_CJRtx0Mfx}=Bx+8iOd>4f-31BtcLz$aUJT%9l(z;FpJf!BjV@tt>A=7BkNQ$B z!nk$%3ott$>pb_1X=~_yCVx?Ix>I_CB?AEq$sUxppr4rP>Rd4A{3++Bq`#*)jUCm- zxT7QZnTlf>*?nmPHRLd-zJs8KU4#7?F>b94Qn7Ade!qdyE9vNmveyGM95ixEZ-elf z&kY9vw;Revm53@UiI;B}q`TD~{MF*KQFZ1V>L7Z<13yLvZK%*0(!k}GfHW0Fo{D~t zBCZAC7MA9CB58}(EcYLrgtk8!%|8Z}*N)qk@y#|Ps@Kj@+CTC>uIA)=+6Z>}3&IA=!rKI@{afxjRflJS=*^^*kq2kAHxsomw&6b-p&PjZZ7 zdwp7SqggoYiN}y4gK<$rZBl_HwKg%orN0Nmh*6o5)bN&kiNx-Q>M6|QAlX-8rl;?q zB~r&6VcaKNPC%xOafQ91!Z)I{{Zb>oX2!phiMhE&48;AoFuMY;Iq!7@;QsEL-@&i) zPhfs#aayKjw4Kt3BKA$SWj7Svwc}tR3G}Mg3?z8YRv3w=f-1QAuZ#H-Tb~r%fAzyU}6k?c)cp)E*-Aw7XN9CZnzlUsqO^7#7w-$q_Eq9Uq9LXlm; z;&swQf{Oes_^yM}4+3!hl;n1liGfoj`--ZRlTjXcIp0danbX8q5r~opKj!pS=yZ^4 zbRHLAJhNbhgk8Q^ua$v=9tH2YCo{bZk^h7>G5pkMXTjq%_f>KTrKeoNW9PRW#E@a( zkbcgMbrxbF__gMxJu)jwl75wHIP|4I0LW!l|tE zJUr~7J)mn#4V@d!k62b>PkuL6VG+(h?jmYNa@)06<4!z{&gx?j#}Ne*5fyuIF^9?j zv37|(3#|kz@y8LyfVORBy3#2cDJmyeQABRQf_9HlscYax(U+CcUzg2Vu)^n-jlfI8 z_ft8R{u6?=hvND^@8Y@em3$NHS82bUNs2j$szj-eqMffIJUUP3s_b`-mXO?H+ZyKy zbXT_kt#!a2jo!f>H2x$jA`3NW`-HPrOhrdbJARB8;*2XuR>xVet^fyQD@ zCAkNvI~0%JN_$C!PMt zn}B9O?;gTnVJC#h63b4yUJA{ zpDFsNIMMb8tcaCVwiuR6_{nSvl<>_opO@qo0pwc=$t^S8olxmb(%O&6Q{Eq@d)B0( z4Px-KXB4cm^{VOLa?Z6I65?c9rD`)(%A5m~Wts}!1j!&8qx&lZ6`v$&^keRkvK@>G zgEEmIJ%Ppnw7yu12s71aDe$=uC{U!Z_Y}^vh89s1Q?Z001BWNkl=BUzAplzvrWLsVeS)w6b5v~Z3`SjRb<&yco@()1@no4?FohD4bJ$)X6 zZOwW6B3P?ZMe44X2wrh>OPz}vBW;Eq;O$!Y2*znFEz=-?^j$FBHsez??Dw$44JN)h zKg;+b%7CKu1WFPrgBO`dtVz_;K@1{~oNS|;thm>xEu&YIv<30$E%-PZkJGu- zMinR~MSlz~cyQH)p-`0|aKSkWrs&Z9lmdJ56F43VaM8)<)3M!s$BnoPt1-8F8AL2t z3)}B2?FJl}<0^8@X4@YnNsb45+6T!*&NPyR@h96(2~_XsMyNPJwFfDE;yq%`7dwk! zuBlQU0t!Udg)Y92y}7RmL`P}hGp%3+5-P-88+}FIW;vh46_vW@+NE1a`TCTOv*}=G zJ9Uu5IWx{@^2`92Bh=2^MBZ1+2TZ@*eMq5j`YX|mV3{^5i z#5}{BiQ)oP@Jtj6m@rhDiYjE|Eb1~#Xm4!B&!4^z8}@8B6^Lx^?DmU{NLOm< z%~8afz#cPa*`$RBBI6oF#+*kO%#Y&*9cQ8PIdiC2={JA(Bf66HNiC84s3nQAf}1mf zqHJ9wA}gFSk=C?E%4-#R9wW{I2<}DCz$`tWB8=|%+8corwmF^06}f!PbYvBg8LJXm zz8Pz_M7TZ?#4ckl8iBZHO$UTdCs->4&=oNa!{JT|CtJdtz7s6X!W6MUDCZjm=Fn*| z=>?L#HpvhIH_LD?DQP?7pmHQ=?Sd2Bgiw|!yG~k5H0@TMEkK}h?00lJU8u68PT#musK78+?Td8}ppytO}x{4Qp zf&ox4MTnv{5m`4NiHNN#qoOxiv9?-~nuoxpdUT4|3!D0}vhN~E>j1>kC9W-mV+6aP zY}6Nd&B^LPhloLj@dJWTx-$^RPBopsM6iN30cVmW7cRnJV3QGPrJZ{o%BzDOQb11# zJ6*Fh^?} zNU(P78Nep&LSbwd7Db&nzU5S$G4FV+Tyg>y*Ud*RmiX??U&aS^UX8A{qk+yA+<3=Z;O>pv=yS|OWXA2CJzw+Nbzc@7bW{RCF9rqVo5MA9`-zDXIGU@9N9 z)3a#$pUyb8JJ;hSXht2{8k%W84N(llS@sf~b<{~{UV1DrFO$w9XayFmTa1yN z12}r#Lh8qU$K&^56yx~D#qY%<{hRRG+dl@0D$-z&Zdr@w$_9)k!XTF+d#Qy%OpWr+BU!xI6VPMxf`c4Jxpv?q{zs`!$ ze}y7H))8Y&%Ci|<9z$@?2$u&D49cn?y3+xP#$_=Q-cpg~YS?YY)Gs1JyKF)|GMJdQ zMaMzy?8C<1?f9ExUyNffIuFJB@4}ll{RM`{C-BJN zI{f=x*CH21*iqPp#WmeHrF{uD4)tO|#~hrx=vmm(w+(kb`5^kn2T>7asl)M5ya%sb z`f60Im`mqX-B5>*FMSVMlMZNO){H@q?6tHejX-wZ#BUQwiNX1#B%ZBH?}QCSx5;&aO( z?@5Bit3s4iz)BBxMr9Fz3~(vQ6SqXD(L_4ehQfOvF$u3u;VX56Xo`1hDZbpgQ3{_9 zmA>DCvI3W}f7iqknamh4wg;G7iKxAXMs)Z6?s5F)p*wNcy4!Kf?qA`x^_Sse7oHBR z-ihCDdjM^igEJSb!~>fiLAA=_%1hn@G$3flM!TDOQlaSDuqmK2w~2MwNZWZA(nbx0QyoNw7U5FArAm}Q7&g^XU>456irPxte?Qm{w3%B zJby{p1{^Fb6cWf{>PrcfFdP)mHF0So^Te5_E23<^&F7eWT2V?=*VJOuSRZb_=|9l1 z`!qbT=^@#aZwh~Be3QPp^r^$*`R$yquI;_*%Q58jG^7G1;)lHR< zw zw>^Fb)}aq?T68Q>SAjondK|m-c092CahRgR9m50o@4;Ij(2nJq1&HGU@?-jPo?b{X27fJI(!#I?#!B>Mt$X11nu6 zg}SHAC|Hp-Q0*DUW$XhI`{CR^MXUt|FoKg>m*J&tr=vb!fRYx#bN1cOSC^IsOjMO9 zVl7nUgfJJRimX3LGkjZU*Vsb@0xrBjTELVi&l4oV6*I&O7c{xlpy(B5^ewgaQ;3Da zP^O6E7Oq6?$%|=h$)7eoNEHkxEjWfgv!!o4gu%fonrL@5Su-52+weUpS}I*8q>53K zGvGxBY3uP~kp#9Plf@8r;?&wjcxC5l)OUU&TcCg>(pE$g;zBt;vdH;9#nhLSV&o_= znpKL^WaJ45<*C--NZe`ngq!EOz#~ePr<97`+lnkqY~{KW~^P9Gc~QS zJZq3C^AEypgohI_5K1yhGcC!A+xR=T@509X1{@{l*eP+6z;q@IjJ%4s(2gp8W+O}dTY zn(p?O9=kAsRx5tK{dUZ`Sz^(yvrsg7!#H7RKwDVAL#WE>bI@@tl-7+lhn1@fk z?jL}@7(e*ajW|!N!f_{@V!4*<3)p<;?`?Za7CC7F5v8!(v5d67v|9S2Sa3$9$kVgWo^-Fvd|ZQweeJ_DAv4 zAN~L@J^Pio*u|FK}`lMjq?IyFUIVJUp=;wKNUGazQ2_Fia!G_Yg`}Ma>awyuxyOxi z5iFe3P5XOj{gX8Oq)Jp^BpJm!e(@>%_@Q5;qp2Hf`ZiEK;`?s=9Bn_53b{-c4Rvk! z!^8LEt3SUA^;Na_?~Qli(ZZv6W5cU(=JPM5p#|6d``2*8#64)KITn5Saj19#waB8) zOk}N`j}w}f;KYt)*tuanzA<(`dYTe6O;`ZNXy1ZLeBT&tJMmJn1u1ZmP&JQEtadZW z?nDKQjX)dNN+fPWPT_*G(Yg6YL#48oD=JJo%}Qid6=EF`bes5x5)rZRJChoASsiUE1rqYd9mK|ao zaw^M8D_bZkbjdw!A6&szJ+VxD(phlATydh=@VHFZ!GyM%qe-*cjV~@MHcs_viA)sB zuKe`0nb63TFO&bOkS+K@&&|*`4xsbNRfMq9RCkA0d1G#gd%_z)O*9n%KGIQvp%hWk zDsT($La0pYqlZ061=17ccGxb{&v{+;_bdk~FAA97x&kX-b3RaC37yHKAzOhT|KVnA zxcxiWt;Wb{w@NgFYEe(EyG^pz_(q0p(1d17U=b2|UBqv1x(TcM)?jXS2^FXgCcE+c z%2ROcs&nYdZXDW(m(6_vUOfMWSh4hY)XiB6bm!7;_=EgFo&@Y#y$g?FJ#w-JPzBc3 zY|PkSRHQ##$%^ELY|8WS6P0>UF1@B4y6&r`8DZBcj6q3h6`DHQ8aojsPI|D^6t7Jq zSP_KLSTSYE33aL9E}#Y4JqO4{hN}<}9*_mxH~0(Wet#`oXVAzqQJqPXzI;^&uQ=Zz zM5@4;RH%YH3{ir6xr>*2WFZqw2Y48$G$D_z*p833y#p)%`h2KIwqxy{4JhU$KL6lV zbRZcfdZWiv_jlArx8YNA@SX4)~VC(t1p@#fQC z2h7hv=YX>=x&&vPbBPrmWd&(u-6m|_y#;^VumPB2BFMbU1ncBu=Ut!uOC9m%#`vQDz+ zY6yCEj*X2-d?6moOX%cln-t`N344>>uyC6S)7;orATlgcmzFoWS*H2SLXtD@G6;vWIh6&`g#!@ulG}UMw z2@|$&2lAV?;fXbm;7@D*fCqb@!s@{%@dzHnX*dyo@$RcBU;4o3uf>LBT?cxITk%!u@1)!eu@oyQ}%VVL5mT5I;F8UW&r{H|qjI*s&gea9(>am_j4)aAJAZ#jp)T56lf-*uBbzGei1%<-p9~*`a&dk58;YCzX-^BWGbpqoJj1r z7Hx~JVr>b3E-hoNhkdAGYW0!ofAE+V$tEy?rEF`Yf~DEFGKp_%Dpi^((k{zl#VB^I zHv>!Hx+<1ANRuS4to%zpY8AGwaRIC=P|!$sOtcGa`8=edXgWho_)Nsq*i2BuP1Ij= z(_F=l$71-|Ac_N1W0d7sk+2cJC=ZztD3bD$v{T#1z}#HRf=ZO@Ng#YwDze)9Ub{!e za87O&8kQ`g%9w{Y|A~-5BiOMNOrPdRmfK%xKSL+B=)QE7EF5aHushCuwF%)f8GIvo z7~i@3PVAv%NDXRaEow3=P?xKR$mY>ExEXJ5x(LTU?*h68-}=d?aTC_znCcUeFBTcm z7fhou0v0n4Xz9mFrEE~^YzO_4oj=97SVOi<+yf_4O$KI+rd#5&d-|Rv7r=^J%$hAV zDKTdWRN#?;uIkK!RbyXEkr7#D1lY^6YFKCJ1f;8Yanz2W1Az8E0CSdfS{G=maqP`= z2$;F3LR{-n;rsg*V+M6oV}M$Ly)hQP^lJJZ)#^srQ`+~_C532NZDQo z#thb=3P;UbPQK5R>sRAx>_laDE>x@`W5xK5Eiw($wqylmimSOu-d!A@ePb<64M`HK zO{GlyTprWjCXVya1&x0^?VV_T&QZWq1^mPJ{~k|b0*j-$$P>IESxd(Z>T(BnrUuh! zM84*B5-i^}zQYOkMRhd<^t?`yib?yYf>YIDpZJt4;Dkx?x#?%Z-=`BS&Jb&u6l*oVcI6IjfqTLQ$u0- zk+HIMhT};A9Z@4zwp-lyiJmpsg*>`Ug`NwrlrC}HOK(Hj@4?6X1dBkU`|~ix2?je7 z5><)G>I~v^xe}M2axu^i-1EgB;s;~DL|gV`#IdxkgA|u%nQW}Ec0ntLzB_udk8;?| zrG_dL7z7}5>1*0YwA_?e$D7ufH5QOO1)|33Q_8cK+KnCIZgqmN zA{C646&^bY0W`rAvaA`ghv)Km{^WuS;(oJyBkiZQn;j;0>KoSxYTKR8XbWR5g)O`w zv`i%^8dO?kFc^>H*s5+c&Yw@v?_<4d$@kF=ODl>h&o@wuZq%a&6V$6AAwe{n$Qhf&m%Izl zHYqK)(gC~>kVMN`Um_5Bhk?2KLzZ3eh&}(vPcXiysUS8ME}M~xj7$I6qZKpzSb*MO z8np2E{7Fj(VVCRl<8oQdxwAJbm|?7i#l7InLU5MC^{ASn@KXvmrj~c)r=14PY>nwt zlKlfq`cd4K#rQ-;Gh;@_=rqxAOEu*cp6q{$usL74?=JQfU4v1kv1VQ_Q(;q6`ADEP z)-pX|cr3;q&kz2IWH;y?Jor}0~?!Q$*`Kr%vwm9cinx+XtLYXqRCosH;5+c~|j zUEss7ICn}1kGh_uOPv>-qypjqW`F-w@NJiKE%nt((;AemDOu%kgW2MEcu}XkuzZ8# zvIMDSRUHMz`VRSp4`&@X=iHdIA46!rsUwUOzLQFt=Sq1;4mZxQ>2;+`#tfOXM=@)H z@dB1LFQ7t(J!`k3uegiyfoXDsYwdh*0}H(R2gXfI4X=|g+;#a#u1HeO3yG-Jqk(2O z$GdT2b~)bj+_%s%Z2qr%@VSjwqe;#~93>cKb8o&H3(LnMIDpIFZS%I$J`)aObEeVV zc2}b1ka$d^xdeB=LC}^-dgojPrC!=U)aku(0;xiA9iCCJ6tKaS@|2=B<-a+0XexZl z;Zd3?EZRBJ@VJ$Kl62{|r*sNM+N{DyE?gC5TXTE=8bzYsBzERF<6lNW%VL5`)Z&;W ztB8ZO_NIpjQL4;z&~_ubnLsgP3kIAy73`mhvgeku<(d*j9y820Lj% z4!_3ZXv!Xq37v;9#UJFhGZRzsNMOd4z-=b&C_PJeQS1g-ttLu_AxNYI_~}fzL+ley zvWZEQ7%(42c}G8?Igjx7KHL_s-R1i!H-%>1AO2l>fTe$_a?R$^YU5iufYdpJDmd`c zEed}@N;Xy+FKUqni!Rg`))5u9Up3tz+wIS&B8fgs;Q5tv(Au$(O22-;^-)MvGgm9- z{#6M^;|UBCUQ|R&rVf$Fq_Ib z(ckPI2B&C8=N#9KiWI2SEU-nN&K^nltKGPL{brpaCTrC0!Pg z_SkSfn=aLOQ>^>TUz&>DV9@tHww?Xd%WXUkPQ+IiX^sek3g%R>Wbhh%d=gF4SLsQF zq(ImE$pe7&A?CZ=Zb1|o6?v4KY58+(^E_aF1huIjkMCJW?17w)FsAe9jdx)JV_1j| zTvB};L~#gr6xU-oqnRBMUgt7_n&Ox>_1?08=(c1VzP#+cSa{B9R2cP@t3QU{qaSUV zlcAELMfqKOA#EdV?UTPQ>SBKGY zresYeSf-3@jQ~yx9YS|trH2_hhpu<8W~xULyyu?4a-1db&-fKduDEpF)hMcSX4E;; zk{hFGu2-Z=CXM~7;w5@^hTFDocx-)h+Sla4rq8&%*FYLsrSc*44=M^-E z_Q`AR#uxjpK|}N`GS_DO3Kdfb>!c?-#ReHzvltaz&a_knyx4HzsXGwq;Uu;aB5m<# zBg}N_M;e)@t;Ri6{gSn5co zTz3S~kH3M?Y!aPe5FSrrtK#=CmW*L8T5#;VW9a?6H{64@*nk!B0$e=*0=(wfi*Vk` zi!EnXGc-J~2n{#3pcp@dNVKGt;l65^drU!>p%dwiF@eMw#_@qwuSLaaiwL~A@}_^q zc2r?OR1FGU5o|60AJqS}}&Q~f0Z6_Nn?*nT`QZD;!5b?PP-rc*Y z`3#fRBr}K^1nXV^e}iXMgMK*--DSQr{sIW*nNQ_VI%Al+_Abfk9a5J{a#iAv9O(QG zWy{1eY$7>>g55fg^XlfKe$GO|`?{MJ;6sbvj<=ljRxCd41Y4;y0gODo8G{o;nD^{e z(1Sx*Gu}fJ|036<)++@5uW&;PMA=zLdeR4zKD?#%61?<x zppp6<;nLT<1D7_iQ!)f>z3l;9_s1XLJ5ODOzg_lby!F&o*uH)(p2h}LW$M|-lH8EG zG;U4V1StWQ7-{FODo9I?qhl2?O8@{EbV)=(R0%cs#5r#Tj;_YQU90iF)t^97cA{NZ zBT;#d`go$5^RIMTT&n$#@PUBxS6BLn*Tr*UvJL`#`X{v`>6s8f#O6`w#*|Vmz|=o7)lqYJgfW}R6+7V5T;`fH(`o(1lO6) zCW2$mQx+nPnxqo*RD(5f%LEA%9`D0%et#=&fABW^rvF|%fSr)2!>X3E$>ZO;c{8?S z4{8m0(U=YeR77t6jy;~lchhnGBP_?v5KXpX2$y$WhL+=wp`i0?Kl>6M!wBY66?L8f zP@8sSP*?6;IMM%FoF`pphKy#jV&6?}abhSLJ3kY}-hcJ{eTXF5JE2 z4!rP|W!Urk8eDVVwYa(O0X&AyW~eXfYueB~-h?Hp8!P86q62(n=TjJiKvjrwm)GY7 zhv)izn$PwmJ8@d}7<}ZycTxe}nrm*xS9acnPPqiRVuXn(=DeIpJHW7}53g0Ku}V#2 znkSfzftsxYv)}8k=_Gb0!FqVQoZ-}`QhqXl52AGmG{LV1Mf>R%QW0@JhT?qHHrL z%Ajp7hZ^8TrCwX`u<(a0UM5Sf&tYCY)9rpH!MX~-oA>>=b=mJBvg=~Oe5!pcc_^6m z;Jy+0C$(tU48&Tsy+K_=R~=;Ei324RHF_M&)S^Ez{RdjHB)b?nO6DqzW`>bbS)%rK zm_7|n&4e}e3~V-)g#=r;X|E90S24n%WSTd#ET&52l*SJAG~U+oYMl1MOQ|OKTVMYa zeuRh7m05*I#YCw#^;I?qk)C~-;(3m_z<=q)w@tH=s9>59sMOS58SSTd9JclA>2^Gm zVBN`MTHE(uu%^;zDbSGO${_;m1Tn3wPMGga>;#`hOQ8mc(3F^7wlTHhmKJ@qeAvW6 z7LxemZ8dd3lR(@*fWG2xY5*dF{c4{l<4l6f<7gnOVr;`WTG52RKl#h_{Y3k0G&gRlV*T2#l^+Uc)@!Bt^+vzj)X}I>_ z9&H%fhWM0wwN&c~etJXVnZp%zSqSct3B|3f`6d!$LPu1x5%lU7nc9aiYe>J|c>uS`?MO4A$ z=ol^)6XqP1P9Go#D5QJ%AF-~V=^&p;uuFSL?L#8X68gp zG_m{umGYN39pZ9nwz?&zN*e=37++~D9z~bv#B$SmC=YDeYRcyYbqNgT6^bMS+t7}y zSO{8(FscfKalPxf*8|5#P>&AbgZKO!_J}sL=?dgcvwvyFDkLty8AO6#B`R>K+Vsa0 z{$?Su#A~u6eEr~=4sSMs^{c~sj-{|_h%SejF^BW$pudCjx!m-K6hE?`Sz6ykHN5e* zK0Lj9BQ2tJ3&%xE@WBh-No|1pfB7W7u=a; T;OK*3;E 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 Date: Wed, 29 Dec 2021 21:17:59 +0000 Subject: [PATCH 169/541] Hotfix edit bug (#256) * fix edit bug and log faucet better Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/donate.go | 7 +++++-- internal/telegram/faucet.go | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/internal/telegram/donate.go b/internal/telegram/donate.go index bee1b80f..a776b647 100644 --- a/internal/telegram/donate.go +++ b/internal/telegram/donate.go @@ -74,8 +74,11 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { bot.tryEditMessage(msg, Translate(ctx, "donationErrorMessage")) return } - bot.tryEditMessage(msg, Translate(ctx, "donationSuccess")) - + // hotfix because the edit doesn't work! + // todo: fix edit + // bot.tryEditMessage(msg, Translate(ctx, "donationSuccess")) + bot.tryDeleteMessage(msg) + bot.trySendMessage(m.Chat, Translate(ctx, "donationSuccess")) } func init() { diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 19883656..9b22467a 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -253,14 +253,14 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet := fn.(*InlineFaucet) from := inlineFaucet.From // log faucet link if possible - if c.Message != nil && c.Message.Chat != nil { - log.Infof("[faucet] Link: https://t.me/c/%s/%d", strconv.FormatInt(c.Message.Chat.ID, 10)[4:], c.Message.ID) - } if !inlineFaucet.Active { log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) bot.finishFaucet(ctx, c, inlineFaucet) return } + if c.Message != nil && c.Message.Chat != nil { + log.Infof("[faucet] Link: https://t.me/c/%s/%d", strconv.FormatInt(c.Message.Chat.ID, 10)[4:], c.Message.ID) + } // release faucet no matter what if from.Telegram.ID == to.Telegram.ID { From a7cf6f957e7bc12b8be9935961fc22e58919628e Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 1 Jan 2022 21:26:56 +0000 Subject: [PATCH 170/541] edit stack fix (#257) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/edit.go | 23 ++++++++++++++--------- internal/telegram/faucet.go | 9 +++++---- internal/telegram/telegram.go | 2 ++ 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go index 6002f1d0..6445fedb 100644 --- a/internal/telegram/edit.go +++ b/internal/telegram/edit.go @@ -1,7 +1,7 @@ package telegram import ( - "fmt" + "strconv" "strings" "time" @@ -14,6 +14,7 @@ var editStack cmap.ConcurrentMap type edit struct { to tb.Editable + key string what interface{} options []interface{} lastEdit time.Time @@ -59,25 +60,29 @@ func (bot TipBot) startEditWorker() { } } } - time.Sleep(time.Millisecond * 500) + time.Sleep(time.Millisecond * 1000) } }() } // tryEditStack will add the editable to the edit stack, if what (message) changed. -func (bot TipBot) tryEditStack(to tb.Editable, what interface{}, options ...interface{}) { - msgSig, chat := to.MessageSig() - var sig = fmt.Sprintf("%s-%d", msgSig, chat) - if e, ok := editStack.Get(sig); ok { +func (bot TipBot) tryEditStack(to tb.Editable, key string, what interface{}, options ...interface{}) { + sig, chat := to.MessageSig() + if chat != 0 { + sig = strconv.FormatInt(chat, 10) + } + log.Debugf("[tryEditStack] sig=%s, key=%s, what=%+v, options=%+v", sig, key, what, options) + // var sig = fmt.Sprintf("%s-%d", msgSig, chat) + if e, ok := editStack.Get(key); ok { editFromStack := e.(edit) if editFromStack.what == what.(string) { log.Tracef("[tryEditStack] Message already in edit stack. Skipping") return } } - e := edit{options: options, what: what, to: to} + e := edit{options: options, key: key, what: what, to: to} - editStack.Set(sig, e) - log.Tracef("[tryEditStack] Added message %s to edit stack. len(editStack)=%d", sig, len(editStack.Keys())) + editStack.Set(key, e) + log.Tracef("[tryEditStack] Added message %s to edit stack. len(editStack)=%d", key, len(editStack.Keys())) } diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 9b22467a..23570be4 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -306,7 +306,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback success, err := t.Send() if !success { - bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) + // bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) errMsg := fmt.Sprintf("[faucet] Transaction failed: %s", err.Error()) log.Warnln(errMsg) // if faucet fails, cancel it: @@ -340,12 +340,13 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // update the message if the faucet still has some sats left after this tx if inlineFaucet.RemainingAmount >= inlineFaucet.PerUserAmount { go func() { - bot.tryEditStack(c.Message, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) + bot.tryEditStack(c.Message, inlineFaucet.ID, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) }() } } if inlineFaucet.RemainingAmount < inlineFaucet.PerUserAmount { + log.Debugf(fmt.Sprintf("[faucet] faucet %s empty. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) // faucet is depleted bot.finishFaucet(ctx, c, inlineFaucet) } @@ -364,7 +365,7 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor inlineFaucet := fn.(*InlineFaucet) if ignoreID || c.Sender.ID == inlineFaucet.From.Telegram.ID { - bot.tryEditStack(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditStack(c.Message, inlineFaucet.ID, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false inlineFaucet.Canceled = true @@ -380,7 +381,7 @@ func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFauce if inlineFaucet.UserNeedsWallet { inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } - bot.tryEditStack(c.Message, inlineFaucet.Message, &tb.ReplyMarkup{}) + bot.tryEditStack(c.Message, inlineFaucet.ID, inlineFaucet.Message, &tb.ReplyMarkup{}) inlineFaucet.Active = false log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) once.Remove(inlineFaucet.ID) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index e05291ef..274115f7 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -42,6 +42,7 @@ func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...i log.Errorf("[trySendMessage] error converting message recipient to int64: %v", err) return } + log.Tracef("[trySendMessage] chatId: %d", chatId) msg, err = bot.Telegram.Send(to, what, bot.appendMainMenu(chatId, to, options)...) if err != nil { log.Warnln(err.Error()) @@ -67,6 +68,7 @@ func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...in } rate.CheckLimit(sig) _, chatId := to.MessageSig() + log.Tracef("[tryEditMessage] sig: %s, chatId: %d", sig, chatId) msg, err = bot.Telegram.Edit(to, what, bot.appendMainMenu(chatId, to, options)...) if err != nil { log.Warnln(err.Error()) From 615b90ab631c25cba6e7a070a64b031c7b364195 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 4 Jan 2022 11:56:38 +0000 Subject: [PATCH 171/541] print banned (#258) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/link.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/telegram/link.go b/internal/telegram/link.go index 1e93a2c6..3bb7b4a1 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -31,7 +31,8 @@ func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(m.Sender, Translate(ctx, "walletConnectMessage")) // do not respond to banned users - if strings.HasPrefix(fromUser.Wallet.Adminkey, "banned") || strings.HasPrefix(fromUser.Wallet.Adminkey, "_") { + if strings.HasPrefix(fromUser.Wallet.Adminkey, "banned") || strings.Contains(fromUser.Wallet.Adminkey, "_") { + log.Warnln("[lndhubHandler] user is banned. not responding.") return } From c6a064b332fec68291b4c5fcf54c9ff4c9a86b80 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 4 Jan 2022 12:32:10 +0000 Subject: [PATCH 172/541] Banned entirely (#259) * do not react to banned users * do not respond to banned Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/handler.go | 2 +- internal/telegram/interceptor.go | 9 +++++++++ internal/telegram/link.go | 3 +-- internal/telegram/users.go | 14 ++++++++++++++ main.go | 2 +- 5 files changed, 26 insertions(+), 4 deletions(-) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index d9dc0f56..48d7eac1 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -21,7 +21,7 @@ func (bot TipBot) registerTelegramHandlers() { telegramHandlerRegistration.Do(func() { // Set up handlers for _, h := range bot.getHandler() { - log.Debugln("registering", h.Endpoints) + log.Traceln("registering", h.Endpoints) bot.register(h) } diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 750a1d7c..03086be6 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -92,6 +92,11 @@ func (bot TipBot) requireUserInterceptor(ctx context.Context, i interface{}) (co u := getTelegramUserFromInterface(i) if u != nil { user, err = GetUser(u, bot) + // do not respond to banned users + if bot.UserIsBanned(user) { + ctx = context.WithValue(ctx, "banned", true) + return context.WithValue(ctx, "user", user), invalidTypeError + } if user != nil { return context.WithValue(ctx, "user", user), err } @@ -101,6 +106,10 @@ func (bot TipBot) requireUserInterceptor(ctx context.Context, i interface{}) (co func (bot TipBot) loadUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { ctx, _ = bot.requireUserInterceptor(ctx, i) + // if user is banned, also loadUserInterceptor will return an error + if ctx.Value("banned").(bool) { + return nil, invalidTypeError + } return ctx, nil } diff --git a/internal/telegram/link.go b/internal/telegram/link.go index 3bb7b4a1..0446b454 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "strings" "time" "github.com/LightningTipBot/LightningTipBot/internal" @@ -31,7 +30,7 @@ func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(m.Sender, Translate(ctx, "walletConnectMessage")) // do not respond to banned users - if strings.HasPrefix(fromUser.Wallet.Adminkey, "banned") || strings.Contains(fromUser.Wallet.Adminkey, "_") { + if bot.UserIsBanned(fromUser) { log.Warnln("[lndhubHandler] user is banned. not responding.") return } diff --git a/internal/telegram/users.go b/internal/telegram/users.go index e71a9b00..07945456 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -3,6 +3,7 @@ package telegram import ( "errors" "fmt" + "strings" "time" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -119,3 +120,16 @@ func (bot *TipBot) UserExists(user *tb.User) (*lnbits.User, bool) { } return lnbitUser, true } + +func (bot *TipBot) UserIsBanned(user *lnbits.User) bool { + // do not respond to banned users + if user.Wallet == nil { + log.Errorf("[UserIsBanned] User %s has no wallet.\n", GetUserStr(user.Telegram)) + return false + } + if strings.HasPrefix(user.Wallet.Adminkey, "banned") || strings.Contains(user.Wallet.Adminkey, "_") { + log.Debugf("[UserIsBanned] User %s is banned. Not responding.", GetUserStr(user.Telegram)) + return true + } + return false +} diff --git a/main.go b/main.go index 17016b74..62abd045 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,7 @@ import ( // setLogger will initialize the log format func setLogger() { - log.SetLevel(log.TraceLevel) + log.SetLevel(log.DebugLevel) customFormatter := new(log.TextFormatter) customFormatter.TimestampFormat = "2006-01-02 15:04:05" customFormatter.FullTimestamp = true From ec319cc691d3f9138dd7d70c1d4d5ac97d7f9cfd Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Tue, 4 Jan 2022 12:49:31 +0000 Subject: [PATCH 173/541] fix ban (#260) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/interceptor.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 03086be6..dd6b916b 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -107,7 +107,7 @@ func (bot TipBot) requireUserInterceptor(ctx context.Context, i interface{}) (co func (bot TipBot) loadUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { ctx, _ = bot.requireUserInterceptor(ctx, i) // if user is banned, also loadUserInterceptor will return an error - if ctx.Value("banned").(bool) { + if ctx.Value("banned") != nil && ctx.Value("banned").(bool) { return nil, invalidTypeError } return ctx, nil From 01710f3385071c11cf9415985e72604beba3a08a Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 5 Jan 2022 00:09:11 +0100 Subject: [PATCH 174/541] separate lnurl and api server (#261) * separate lnurl and api server * add lndhub handler and proxy * update proxy * fix AppendRoute with empty methods * add logs * fix trace * dump request * parse bearer token * exclude banned users * exclude banned users * fix log levels --- internal/api/middleware.go | 23 +++++++++++ internal/api/proxy.go | 72 +++++++++++++++++++++++++++++++++ internal/api/server.go | 63 +++++++++++++++++++++++++++++ internal/lndhub/lndhub.go | 64 +++++++++++++++++++++++++++++ internal/lnurl/lnurl.go | 82 +++++++++++++++++++++++++------------- internal/lnurl/server.go | 82 -------------------------------------- main.go | 39 +++++++++++++----- 7 files changed, 306 insertions(+), 119 deletions(-) create mode 100644 internal/api/middleware.go create mode 100644 internal/api/proxy.go create mode 100644 internal/api/server.go create mode 100644 internal/lndhub/lndhub.go delete mode 100644 internal/lnurl/server.go diff --git a/internal/api/middleware.go b/internal/api/middleware.go new file mode 100644 index 00000000..8c051d47 --- /dev/null +++ b/internal/api/middleware.go @@ -0,0 +1,23 @@ +package api + +import ( + log "github.com/sirupsen/logrus" + "net/http" + "net/http/httputil" +) + +func LoggingMiddleware(prefix string, next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + log.Tracef("[%s]\n%s", prefix, dump(r)) + r.BasicAuth() + next.ServeHTTP(w, r) + } +} + +func dump(r *http.Request) string { + x, err := httputil.DumpRequest(r, true) + if err != nil { + return "" + } + return string(x) +} diff --git a/internal/api/proxy.go b/internal/api/proxy.go new file mode 100644 index 00000000..10b41211 --- /dev/null +++ b/internal/api/proxy.go @@ -0,0 +1,72 @@ +package api + +import ( + "fmt" + log "github.com/sirupsen/logrus" + "io" + "net/http" + "net/url" + "time" +) + +func Proxy(wr http.ResponseWriter, req *http.Request, rawUrl string) error { + + client := &http.Client{Timeout: time.Second * 10} + + //http: Request.RequestURI can't be set in client requests. + //http://golang.org/src/pkg/net/http/client.go + req.RequestURI = "" + + u, err := url.Parse(rawUrl) + if err != nil { + http.Error(wr, "Server Error", http.StatusInternalServerError) + log.Println("ServeHTTP:", err) + return err + } + req.URL.Host = u.Host + req.URL.Scheme = u.Scheme + req.Host = req.URL.Host + resp, err := client.Do(req) + if err != nil { + http.Error(wr, "Server Error", http.StatusInternalServerError) + log.Println("ServeHTTP:", err) + return err + } + defer resp.Body.Close() + log.Tracef("[Proxy] Proxy request status: %s", resp.Status) + if resp.StatusCode > 300 { + return fmt.Errorf("invalid response") + } + delHopHeaders(resp.Header) + copyHeader(wr.Header(), resp.Header) + wr.WriteHeader(resp.StatusCode) + _, err = io.Copy(wr, resp.Body) + return err +} + +// Hop-by-hop headers. These are removed when sent to the backend. +// http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html +var hopHeaders = []string{ + "Connection", + "Keep-Alive", + "Proxy-Authenticate", + "Proxy-Authorization", + "Te", // canonicalized version of "TE" + "Trailers", + "Transfer-Encoding", + "Upgrade", +} + +func copyHeader(dst, src http.Header) { + for k, vv := range src { + for _, v := range vv { + dst.Add(k, v) + } + } +} + +func delHopHeaders(header http.Header) { + for _, h := range hopHeaders { + header.Del(h) + } +} diff --git a/internal/api/server.go b/internal/api/server.go new file mode 100644 index 00000000..a9adcb45 --- /dev/null +++ b/internal/api/server.go @@ -0,0 +1,63 @@ +package api + +import ( + "encoding/json" + "net/http" + "time" + + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" +) + +type Server struct { + httpServer *http.Server + router *mux.Router +} + +const ( + StatusError = "ERROR" + StatusOk = "OK" +) + +func NewServer() *Server { + srv := &http.Server{ + Addr: internal.Configuration.Bot.LNURLServerUrl.Host, + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + apiServer := &Server{ + httpServer: srv, + } + apiServer.router = mux.NewRouter() + apiServer.httpServer.Handler = apiServer.router + go apiServer.httpServer.ListenAndServe() + log.Infof("[LNURL] Server started at %s", internal.Configuration.Bot.LNURLServerUrl.Host) + return apiServer +} + +func (w *Server) ListenAndServe() { + go w.httpServer.ListenAndServe() +} +func (w *Server) AppendRoute(path string, handler func(http.ResponseWriter, *http.Request), methods ...string) { + r := w.router.HandleFunc(path, LoggingMiddleware("API", handler)) + if len(methods) > 0 { + r.Methods(methods...) + } +} + +func NotFoundHandler(writer http.ResponseWriter, err error) { + log.Errorln(err) + // return 404 on any error + http.Error(writer, "404 page not found", http.StatusNotFound) +} + +func WriteResponse(writer http.ResponseWriter, response interface{}) error { + jsonResponse, err := json.Marshal(response) + if err != nil { + return err + } + _, err = writer.Write(jsonResponse) + return err +} diff --git a/internal/lndhub/lndhub.go b/internal/lndhub/lndhub.go new file mode 100644 index 00000000..896caa2e --- /dev/null +++ b/internal/lndhub/lndhub.go @@ -0,0 +1,64 @@ +package lndhub + +import ( + "encoding/base64" + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/api" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/telegram" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" + "net/http" + "strings" +) + +type LndHub struct { + database *gorm.DB +} + +func New(bot *telegram.TipBot) LndHub { + return LndHub{database: bot.Database} +} +func (w LndHub) Handle(writer http.ResponseWriter, request *http.Request) { + auth := request.Header.Get("Authorization") + if auth == "" { + return + } + username, password, ok := parseBearerAuth(auth) + if !ok { + return + } + log.Debugf("[LNDHUB] %s, %s", username, password) + if strings.Contains(password, "_") || strings.HasPrefix(password, "banned_") { + log.Warnf("[LNDHUB] Banned user. Not forwarding request") + return + } + user := &lnbits.User{} + tx := w.database.Where("wallet_adminkey = ? COLLATE NOCASE", password).First(user) + if tx.Error != nil { + log.Warnf("[LNDHUB] wallet admin key (%s) not found: %v", password, tx.Error) + return + } + api.Proxy(writer, request, internal.Configuration.Lnbits.Url) + +} + +// parseBasicAuth parses an HTTP Basic Authentication string. +// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). +func parseBearerAuth(auth string) (username, password string, ok bool) { + const prefix = "Bearer " + // Case insensitive prefix match. See Issue 22736. + if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) { + return + } + c, err := base64.StdEncoding.DecodeString(auth[len(prefix):]) + if err != nil { + return + } + cs := string(c) + s := strings.IndexByte(cs, ':') + if s < 0 { + return + } + return cs[:s], cs[s+1:], true +} diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 26c97027..6a65d6e0 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -4,6 +4,10 @@ import ( "crypto/sha256" "encoding/hex" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/api" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "gorm.io/gorm" "net/http" "net/url" "strconv" @@ -17,7 +21,15 @@ import ( log "github.com/sirupsen/logrus" ) -type LNURLInvoice struct { +const ( + PayRequestTag = "payRequest" + Endpoint = ".well-known/lnurlp" + MinSendable = 1000 // mSat + MaxSendable = 1_000_000_000 + CommentAllowed = 500 +) + +type Invoice struct { *telegram.Invoice Comment string `json:"comment"` User *lnbits.User `json:"user"` @@ -25,12 +37,28 @@ type LNURLInvoice struct { Paid bool `json:"paid"` PaidAt time.Time `json:"paid_at"` } +type Lnurl struct { + c *lnbits.Client + database *gorm.DB + callbackHostname *url.URL + buntdb *storage.DB + WebhookServer string +} -func (lnurlInvoice LNURLInvoice) Key() string { +func New(bot *telegram.TipBot) Lnurl { + return Lnurl{ + c: bot.Client, + database: bot.Database, + callbackHostname: internal.Configuration.Bot.LNURLHostUrl, + WebhookServer: internal.Configuration.Lnbits.WebhookServer, + buntdb: bot.Bunt, + } +} +func (lnurlInvoice Invoice) Key() string { return fmt.Sprintf("lnurl-p:%s", lnurlInvoice.PaymentHash) } -func (w Server) handleLnUrl(writer http.ResponseWriter, request *http.Request) { +func (w Lnurl) Handle(writer http.ResponseWriter, request *http.Request) { var err error var response interface{} username := mux.Vars(request)["username"] @@ -39,17 +67,17 @@ func (w Server) handleLnUrl(writer http.ResponseWriter, request *http.Request) { } else { stringAmount := request.FormValue("amount") if stringAmount == "" { - NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Form value 'amount' is not set")) + api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Form value 'amount' is not set")) return } amount, parseError := strconv.Atoi(stringAmount) if parseError != nil { - NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't cast amount to int %v", parseError)) + api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't cast amount to int %v", parseError)) return } comment := request.FormValue("comment") if len(comment) > CommentAllowed { - NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Comment is too long")) + api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Comment is too long")) return } response, err = w.serveLNURLpSecond(username, int64(amount), comment) @@ -60,35 +88,35 @@ func (w Server) handleLnUrl(writer http.ResponseWriter, request *http.Request) { log.Errorf("[LNURL] %v", err.Error()) if response != nil { // there is a valid error response - err = writeResponse(writer, response) + err = api.WriteResponse(writer, response) if err != nil { - NotFoundHandler(writer, err) + api.NotFoundHandler(writer, err) } } return } // no error from first or second handler - err = writeResponse(writer, response) + err = api.WriteResponse(writer, response) if err != nil { - NotFoundHandler(writer, err) + api.NotFoundHandler(writer, err) } } // serveLNURLpFirst serves the first part of the LNURLp protocol with the endpoint // to call and the metadata that matches the description hash of the second response -func (w Server) serveLNURLpFirst(username string) (*lnurl.LNURLPayParams, error) { +func (w Lnurl) serveLNURLpFirst(username string) (*lnurl.LNURLPayParams, error) { log.Infof("[LNURL] Serving endpoint for user %s", username) - callbackURL, err := url.Parse(fmt.Sprintf("%s/%s/%s", w.callbackHostname.String(), lnurlEndpoint, username)) + callbackURL, err := url.Parse(fmt.Sprintf("%s/%s/%s", w.callbackHostname.String(), Endpoint, username)) if err != nil { return nil, err } metadata := w.metaData(username) return &lnurl.LNURLPayParams{ - LNURLResponse: lnurl.LNURLResponse{Status: statusOk}, - Tag: payRequestTag, + LNURLResponse: lnurl.LNURLResponse{Status: api.StatusOk}, + Tag: PayRequestTag, Callback: callbackURL.String(), - MinSendable: minSendable, + MinSendable: MinSendable, MaxSendable: MaxSendable, EncodedMetadata: metadata.Encode(), CommentAllowed: CommentAllowed, @@ -97,21 +125,21 @@ func (w Server) serveLNURLpFirst(username string) (*lnurl.LNURLPayParams, error) } // serveLNURLpSecond serves the second LNURL response with the payment request with the correct description hash -func (w Server) serveLNURLpSecond(username string, amount_msat int64, comment string) (*lnurl.LNURLPayValues, error) { +func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment string) (*lnurl.LNURLPayValues, error) { log.Infof("[LNURL] Serving invoice for user %s", username) - if amount_msat < minSendable || amount_msat > MaxSendable { + if amount_msat < MinSendable || amount_msat > MaxSendable { // amount is not ok return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ - Status: statusError, - Reason: fmt.Sprintf("Amount out of bounds (min: %d mSat, max: %d mSat).", minSendable, MaxSendable)}, + Status: api.StatusError, + Reason: fmt.Sprintf("Amount out of bounds (min: %d mSat, max: %d mSat).", MinSendable, MinSendable)}, }, fmt.Errorf("amount out of bounds") } // check comment length if len(comment) > CommentAllowed { return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ - Status: statusError, + Status: api.StatusError, Reason: fmt.Sprintf("Comment too long (max: %d characters).", CommentAllowed)}, }, fmt.Errorf("comment too long") } @@ -131,14 +159,14 @@ func (w Server) serveLNURLpSecond(username string, amount_msat int64, comment st if tx.Error != nil { return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ - Status: statusError, + Status: api.StatusError, Reason: fmt.Sprintf("Invalid user.")}, }, fmt.Errorf("[GetUser] Couldn't fetch user info from database: %v", tx.Error) } if user.Wallet == nil { return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ - Status: statusError, + Status: api.StatusError, Reason: fmt.Sprintf("Invalid user.")}, }, fmt.Errorf("[serveLNURLpSecond] user %s not found", username) } @@ -164,7 +192,7 @@ func (w Server) serveLNURLpSecond(username string, amount_msat int64, comment st err = fmt.Errorf("[serveLNURLpSecond] Couldn't create invoice: %v", err.Error()) resp = &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ - Status: statusError, + Status: api.StatusError, Reason: "Couldn't create invoice."}, } return resp, err @@ -176,7 +204,7 @@ func (w Server) serveLNURLpSecond(username string, amount_msat int64, comment st } // save lnurl invoice struct for later use (will hold the comment or other metdata for a notification when paid) runtime.IgnoreError(w.buntdb.Set( - LNURLInvoice{ + Invoice{ Invoice: invoiceStruct, User: user, Comment: comment, @@ -191,7 +219,7 @@ func (w Server) serveLNURLpSecond(username string, amount_msat int64, comment st })) return &lnurl.LNURLPayValues{ - LNURLResponse: lnurl.LNURLResponse{Status: statusOk}, + LNURLResponse: lnurl.LNURLResponse{Status: api.StatusOk}, PR: invoice.PaymentRequest, Routes: make([]struct{}, 0), SuccessAction: &lnurl.SuccessAction{Message: "Payment received!", Tag: "message"}, @@ -200,7 +228,7 @@ func (w Server) serveLNURLpSecond(username string, amount_msat int64, comment st } // descriptionHash is the SHA256 hash of the metadata -func (w Server) descriptionHash(metadata lnurl.Metadata) (string, error) { +func (w Lnurl) descriptionHash(metadata lnurl.Metadata) (string, error) { hash := sha256.Sum256([]byte(metadata.Encode())) hashString := hex.EncodeToString(hash[:]) return hashString, nil @@ -208,7 +236,7 @@ func (w Server) descriptionHash(metadata lnurl.Metadata) (string, error) { // metaData returns the metadata that is sent in the first response // and is used again in the second response to verify the description hash -func (w Server) metaData(username string) lnurl.Metadata { +func (w Lnurl) metaData(username string) lnurl.Metadata { return lnurl.Metadata{ Description: fmt.Sprintf("Pay to %s@%s", username, w.callbackHostname.Hostname()), LightningAddress: fmt.Sprintf("%s@%s", username, w.callbackHostname.Hostname()), diff --git a/internal/lnurl/server.go b/internal/lnurl/server.go deleted file mode 100644 index 0de73de8..00000000 --- a/internal/lnurl/server.go +++ /dev/null @@ -1,82 +0,0 @@ -package lnurl - -import ( - "encoding/json" - "net/http" - "net/url" - "time" - - "github.com/LightningTipBot/LightningTipBot/internal" - "github.com/LightningTipBot/LightningTipBot/internal/telegram" - - "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - "github.com/LightningTipBot/LightningTipBot/internal/storage" - "github.com/gorilla/mux" - log "github.com/sirupsen/logrus" - "gorm.io/gorm" -) - -type Server struct { - httpServer *http.Server - bot *telegram.TipBot - c *lnbits.Client - database *gorm.DB - callbackHostname *url.URL - buntdb *storage.DB - WebhookServer string -} - -const ( - statusError = "ERROR" - statusOk = "OK" - payRequestTag = "payRequest" - lnurlEndpoint = ".well-known/lnurlp" - minSendable = 1000 // mSat - MaxSendable = 1_000_000_000 - CommentAllowed = 500 -) - -func NewServer(bot *telegram.TipBot) *Server { - srv := &http.Server{ - Addr: internal.Configuration.Bot.LNURLServerUrl.Host, - // Good practice: enforce timeouts for servers you create! - WriteTimeout: 15 * time.Second, - ReadTimeout: 15 * time.Second, - } - apiServer := &Server{ - c: bot.Client, - database: bot.Database, - bot: bot, - httpServer: srv, - callbackHostname: internal.Configuration.Bot.LNURLHostUrl, - WebhookServer: internal.Configuration.Lnbits.WebhookServer, - buntdb: bot.Bunt, - } - - apiServer.httpServer.Handler = apiServer.newRouter() - go apiServer.httpServer.ListenAndServe() - log.Infof("[LNURL] Server started at %s", internal.Configuration.Bot.LNURLServerUrl.Host) - return apiServer -} - -func (w *Server) newRouter() *mux.Router { - router := mux.NewRouter() - router.HandleFunc("/.well-known/lnurlp/{username}", w.handleLnUrl).Methods(http.MethodGet) - router.HandleFunc("/@{username}", w.handleLnUrl).Methods(http.MethodGet) - return router -} - -func NotFoundHandler(writer http.ResponseWriter, err error) { - log.Errorln(err) - // return 404 on any error - http.Error(writer, "404 page not found", http.StatusNotFound) -} - -func writeResponse(writer http.ResponseWriter, response interface{}) error { - jsonResponse, err := json.Marshal(response) - if err != nil { - return err - } - _, err = writer.Write(jsonResponse) - return err -} diff --git a/main.go b/main.go index 62abd045..b5ea0a8f 100644 --- a/main.go +++ b/main.go @@ -1,16 +1,17 @@ package main import ( - "net/http" - "runtime/debug" - + "github.com/LightningTipBot/LightningTipBot/internal/api" + "github.com/LightningTipBot/LightningTipBot/internal/lndhub" + "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/gorilla/mux" + "net/http" + "runtime/debug" _ "net/http/pprof" "github.com/LightningTipBot/LightningTipBot/internal/lnbits/webhook" - "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/price" "github.com/LightningTipBot/LightningTipBot/internal/telegram" log "github.com/sirupsen/logrus" @@ -28,17 +29,35 @@ func setLogger() { func main() { // set logger setLogger() + + defer withRecovery() + price.NewPriceWatcher().Start() + bot := telegram.NewBot() + startApiServer(&bot) + bot.Start() +} +func startApiServer(bot *telegram.TipBot) { + // start internal webhook server + webhook.NewServer(bot) + // start external api server + s := api.NewServer() + + // append lnurl handler functions + lnUrl := lnurl.New(bot) + s.AppendRoute("/.well-known/lnurlp/{username}", lnUrl.Handle, http.MethodGet) + s.AppendRoute("/@{username}", lnUrl.Handle, http.MethodGet) + + // append lndhub handler functions + hub := lndhub.New(bot) + s.AppendRoute(`/lndhub/ext/{.*}`, hub.Handle) + s.AppendRoute(`/lndhub/ext`, hub.Handle) + + // start internal admin server router := mux.NewRouter() router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux) router.Handle("/mutex", http.HandlerFunc(mutex.ServeHTTP)) router.Handle("/mutex/unlock/{id}", http.HandlerFunc(mutex.UnlockHTTP)) go http.ListenAndServe("0.0.0.0:6060", router) - defer withRecovery() - bot := telegram.NewBot() - webhook.NewServer(&bot) - lnurl.NewServer(&bot) - price.NewPriceWatcher().Start() - bot.Start() } func withRecovery() { From e275b8760a60052465776aa61b37d9118f977e1e Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 5 Jan 2022 00:28:17 +0000 Subject: [PATCH 175/541] anon_id_sha256 (#262) * anon_id_sha256 * refactor sha256 hash to strings package * oops Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/database/migrations.go | 24 +++++++++++++++++++++--- internal/lnbits/types.go | 21 +++++++++++---------- internal/lnurl/lnurl.go | 15 ++++++++++----- internal/str/strings.go | 10 ++++++++++ internal/telegram/database.go | 18 ++++++++++++++++-- internal/telegram/lnurl.go | 9 +-------- internal/telegram/start.go | 1 + 7 files changed, 70 insertions(+), 28 deletions(-) diff --git a/internal/database/migrations.go b/internal/database/migrations.go index 38536808..7fbc7dec 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -9,15 +9,33 @@ import ( "gorm.io/gorm" ) -func MigrateAnonIdHash(db *gorm.DB) error { +func MigrateAnonIdInt32Hash(db *gorm.DB) error { users := []lnbits.User{} _ = db.Find(&users) for _, u := range users { - log.Info(u.ID, str.Int32Hash(u.ID)) + log.Infof("[MigrateAnonIdInt32Hash] %d -> %d", u.ID, str.Int32Hash(u.ID)) u.AnonID = fmt.Sprint(str.Int32Hash(u.ID)) tx := db.Save(u) if tx.Error != nil { - errmsg := fmt.Sprintf("[MigrateAnonIdHash] Error: Couldn't migrate user %s (%d)", u.Telegram.Username, u.Telegram.ID) + errmsg := fmt.Sprintf("[MigrateAnonIdInt32Hash] Error: Couldn't migrate user %s (%d)", u.Telegram.Username, u.Telegram.ID) + log.Errorln(errmsg) + return tx.Error + } + } + return nil +} + +func MigrateAnonIdSha265Hash(db *gorm.DB) error { + users := []lnbits.User{} + _ = db.Find(&users) + for _, u := range users { + pw := u.Wallet.ID + anon_id := str.AnonIdSha256(&u) + log.Infof("[MigrateAnonIdSha265Hash] %s -> %s", pw, anon_id) + u.AnonIDSha256 = anon_id + tx := db.Save(u) + if tx.Error != nil { + errmsg := fmt.Sprintf("[MigrateAnonIdSha265Hash] Error: Couldn't migrate user %s (%s)", u.Telegram.Username, pw) log.Errorln(errmsg) return tx.Error } diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index d9d4e49a..30dee1e4 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -15,16 +15,17 @@ type Client struct { } type User struct { - ID string `json:"id"` - Name string `json:"name" gorm:"primaryKey"` - Initialized bool `json:"initialized"` - Telegram *tb.User `gorm:"embedded;embeddedPrefix:telegram_"` - Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"` - StateKey UserStateKey `json:"stateKey"` - StateData string `json:"stateData"` - CreatedAt time.Time `json:"created"` - UpdatedAt time.Time `json:"updated"` - AnonID string `jsin:"anonid"` + ID string `json:"id"` + Name string `json:"name" gorm:"primaryKey"` + Initialized bool `json:"initialized"` + Telegram *tb.User `gorm:"embedded;embeddedPrefix:telegram_"` + Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"` + StateKey UserStateKey `json:"stateKey"` + StateData string `json:"stateData"` + CreatedAt time.Time `json:"created"` + UpdatedAt time.Time `json:"updated"` + AnonID string `json:"anon_id"` + AnonIDSha256 string `json:"anon_id_sha256"` } const ( diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 6a65d6e0..09379c20 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -4,15 +4,17 @@ import ( "crypto/sha256" "encoding/hex" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal" - "github.com/LightningTipBot/LightningTipBot/internal/api" - "github.com/LightningTipBot/LightningTipBot/internal/storage" - "gorm.io/gorm" "net/http" "net/url" "strconv" + "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/api" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "gorm.io/gorm" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/internal/telegram" @@ -149,8 +151,11 @@ func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment str // check if "username" is actually the user ID tx := w.database if _, err := strconv.ParseInt(username, 10, 64); err == nil { - // asume it's a user ID + // asume it's anon_id tx = w.database.Where("anon_id = ?", username).First(user) + } else if strings.HasPrefix(username, "0x") { + // asume it's anon_id_sha256 + tx = w.database.Where("anon_id_sha256 = ?", username).First(user) } else { // assume it's a string @username tx = w.database.Where("telegram_username = ? COLLATE NOCASE", username).First(user) diff --git a/internal/str/strings.go b/internal/str/strings.go index c1e5d4b1..be8eb074 100644 --- a/internal/str/strings.go +++ b/internal/str/strings.go @@ -1,9 +1,12 @@ package str import ( + "crypto/sha256" "fmt" "hash/fnv" "strings" + + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" ) var markdownV2Escapes = []string{"_", "[", "]", "(", ")", "~", "`", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"} @@ -38,3 +41,10 @@ func Int64Hash(s string) uint64 { h.Write([]byte(s)) return h.Sum64() } + +func AnonIdSha256(u *lnbits.User) string { + h := sha256.Sum256([]byte(u.Wallet.ID)) + hash := fmt.Sprintf("%x", h) + anon_id := fmt.Sprintf("0x%s", hash[:16]) // starts with 0x because that can't be a valid telegram username + return anon_id +} diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 9bc09a48..dcc4c77b 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -39,16 +39,30 @@ func createBunt(file string) *storage.DB { func ColumnMigrationTasks(db *gorm.DB) error { var err error + // anon_id migration (2021-11-01) if !db.Migrator().HasColumn(&lnbits.User{}, "anon_id") { // first we need to auto migrate the user. This will create anon_id column err = db.AutoMigrate(&lnbits.User{}) if err != nil { panic(err) } - log.Info("Running ano_id database migrations ...") + log.Info("Running anon_id database migrations ...") // run the migration on anon_id - err = database.MigrateAnonIdHash(db) + err = database.MigrateAnonIdInt32Hash(db) } + + // sha256_anon_id migration (2022-01-01) + if !db.Migrator().HasColumn(&lnbits.User{}, "sha256_anon_id") { + // first we need to auto migrate the user. This will create anon_id column + err = db.AutoMigrate(&lnbits.User{}) + if err != nil { + panic(err) + } + log.Info("Running sha256_anon_id database migrations ...") + // run the migration on anon_id + err = database.MigrateAnonIdSha265Hash(db) + } + // todo -- add more database field migrations here in the future return err } diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index a9c0a8a0..2e80330a 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -114,17 +114,10 @@ func (bot *TipBot) UserGetLightningAddress(user *lnbits.User) (string, error) { } func (bot *TipBot) UserGetAnonLightningAddress(user *lnbits.User) (string, error) { - return fmt.Sprintf("%s@%s", fmt.Sprint(user.AnonID), strings.ToLower(internal.Configuration.Bot.LNURLHostUrl.Hostname())), nil + return fmt.Sprintf("%s@%s", fmt.Sprint(user.AnonIDSha256), strings.ToLower(internal.Configuration.Bot.LNURLHostUrl.Hostname())), nil } func UserGetLNURL(user *lnbits.User) (string, error) { - // before: we used the username for the LNURL - // name := strings.ToLower(strings.ToLower(user.Telegram.Username)) - // if len(name) == 0 { - // name = fmt.Sprint(user.AnonID) - // // return "", fmt.Errorf("user has no username.") - // } - // now: use only the anon ID as LNURL name := fmt.Sprint(user.AnonID) callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, name) log.Debugf("[lnurlReceiveHandler] %s's LNURL: %s", GetUserStr(user.Telegram), callback) diff --git a/internal/telegram/start.go b/internal/telegram/start.go index baf6453e..4264e829 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -100,6 +100,7 @@ func (bot TipBot) createWallet(user *lnbits.User) error { } user.Wallet = &wallet[0] user.AnonID = fmt.Sprint(str.Int32Hash(user.ID)) + user.AnonIDSha256 = str.AnonIdSha256(user) user.Initialized = false user.CreatedAt = time.Now() err = UpdateUserRecord(user, bot) From 8f60cb82f3a6737513aa8fadd6ed8e2ed04cc10d Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 5 Jan 2022 01:40:37 +0100 Subject: [PATCH 176/541] add admin service (#263) * add admin server + service * string fixes * ban and unban user * add return * ban using admin key * ban reason * remove redundant func call * add http status * add log * anon_id_sha256 (#262) * anon_id_sha256 * refactor sha256 hash to strings package * oops Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * add admin server + service * ban and unban user * string fixes * add return * ban using admin key * ban reason * remove redundant func call * add http status * add log Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> Co-authored-by: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> --- internal/api/admin/admin.go | 16 ++++++++ internal/api/admin/ban.go | 65 +++++++++++++++++++++++++++++++++ internal/api/server.go | 7 +++- internal/lnbits/types.go | 21 ++++++----- internal/runtime/mutex/mutex.go | 9 +++-- main.go | 18 +++++---- 6 files changed, 113 insertions(+), 23 deletions(-) create mode 100644 internal/api/admin/admin.go create mode 100644 internal/api/admin/ban.go diff --git a/internal/api/admin/admin.go b/internal/api/admin/admin.go new file mode 100644 index 00000000..9968af70 --- /dev/null +++ b/internal/api/admin/admin.go @@ -0,0 +1,16 @@ +package admin + +import ( + "github.com/LightningTipBot/LightningTipBot/internal/telegram" + "gorm.io/gorm" +) + +type Service struct { + db *gorm.DB +} + +func New(b *telegram.TipBot) Service { + return Service{ + db: b.Database, + } +} diff --git a/internal/api/admin/ban.go b/internal/api/admin/ban.go new file mode 100644 index 00000000..219859de --- /dev/null +++ b/internal/api/admin/ban.go @@ -0,0 +1,65 @@ +package admin + +import ( + "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" + "net/http" + "strings" +) + +func (s Service) UnbanUser(w http.ResponseWriter, r *http.Request) { + user, err := s.getUserByTelegramId(r) + if err != nil { + log.Errorf("[ADMIN] could not ban user: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + if !user.Banned { + log.Infof("[ADMIN] user already banned") + w.WriteHeader(http.StatusBadRequest) + return + } + user.Banned = false + adminSlice := strings.Split(user.Wallet.Adminkey, "_") + user.Wallet.Adminkey = adminSlice[len(adminSlice)-1] + s.db.Save(user) + log.Infof("[ADMIN] Unbanned user (%s)", user.ID) + w.WriteHeader(http.StatusOK) +} + +func (s Service) BanUser(w http.ResponseWriter, r *http.Request) { + user, err := s.getUserByTelegramId(r) + if err != nil { + log.Errorf("[ADMIN] could not ban user: %v", err) + w.WriteHeader(http.StatusBadRequest) + return + } + if user.Banned { + w.WriteHeader(http.StatusBadRequest) + log.Infof("[ADMIN] user already banned") + return + } + user.Banned = true + if reason := r.URL.Query().Get("reason"); reason != "" { + user.Wallet.Adminkey = fmt.Sprintf("%s_%s", reason, user.Wallet.Adminkey) + } + user.Wallet.Adminkey = fmt.Sprintf("%s_%s", "banned", user.Wallet.Adminkey) + s.db.Save(user) + log.Infof("[ADMIN] Banned user (%s)", user.ID) + w.WriteHeader(http.StatusOK) +} + +func (s Service) getUserByTelegramId(r *http.Request) (*lnbits.User, error) { + user := &lnbits.User{} + v := mux.Vars(r) + if v["id"] == "" { + return nil, fmt.Errorf("invalid id") + } + tx := s.db.Where("telegram_id = ? COLLATE NOCASE", v["id"]).First(user) + if tx.Error != nil { + return nil, tx.Error + } + return user, nil +} diff --git a/internal/api/server.go b/internal/api/server.go index a9adcb45..f9a38ab9 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -20,9 +20,9 @@ const ( StatusOk = "OK" ) -func NewServer() *Server { +func NewServer(address string) *Server { srv := &http.Server{ - Addr: internal.Configuration.Bot.LNURLServerUrl.Host, + Addr: address, // Good practice: enforce timeouts for servers you create! WriteTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second, @@ -40,6 +40,9 @@ func NewServer() *Server { func (w *Server) ListenAndServe() { go w.httpServer.ListenAndServe() } +func (w *Server) PathPrefix(path string, handler http.Handler) { + w.router.PathPrefix(path).Handler(handler) +} func (w *Server) AppendRoute(path string, handler func(http.ResponseWriter, *http.Request), methods ...string) { r := w.router.HandleFunc(path, LoggingMiddleware("API", handler)) if len(methods) > 0 { diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 30dee1e4..0445a225 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -15,17 +15,18 @@ type Client struct { } type User struct { - ID string `json:"id"` - Name string `json:"name" gorm:"primaryKey"` - Initialized bool `json:"initialized"` - Telegram *tb.User `gorm:"embedded;embeddedPrefix:telegram_"` - Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"` - StateKey UserStateKey `json:"stateKey"` - StateData string `json:"stateData"` - CreatedAt time.Time `json:"created"` - UpdatedAt time.Time `json:"updated"` - AnonID string `json:"anon_id"` + ID string `json:"id"` + Name string `json:"name" gorm:"primaryKey"` + Initialized bool `json:"initialized"` + Telegram *tb.User `gorm:"embedded;embeddedPrefix:telegram_"` + Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"` + StateKey UserStateKey `json:"stateKey"` + StateData string `json:"stateData"` + CreatedAt time.Time `json:"created"` + UpdatedAt time.Time `json:"updated"` + AnonID string `json:"anon_id"` AnonIDSha256 string `json:"anon_id_sha256"` + Banned bool `json:"banned"` } const ( diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 201f58c5..e01d97c6 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -3,10 +3,11 @@ package mutex import ( "context" "fmt" - "github.com/gorilla/mux" "net/http" "sync" + "github.com/gorilla/mux" + cmap "github.com/orcaman/concurrent-map" log "github.com/sirupsen/logrus" ) @@ -18,14 +19,14 @@ func init() { } func ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Write([]byte(fmt.Sprintf("Current number of locks: %d\nLocks: %+v\nUse /mutex/unlock endpoint to unlock all users", len(mutexMap.Keys()), mutexMap.Keys()))) + w.Write([]byte(fmt.Sprintf("Current number of locks: %d\nLocks: %+v\nUse /mutex/unlock/{id} endpoint to mutex", len(mutexMap.Keys()), mutexMap.Keys()))) } func UnlockHTTP(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) if m, ok := mutexMap.Get(vars["id"]); ok { m.(*sync.Mutex).Unlock() - w.Write([]byte(fmt.Sprintf("Unlocked %s mutexe.\nCurrent number of locks: %d\nLocks: %+v", + w.Write([]byte(fmt.Sprintf("Unlocked mutex %s.\nCurrent number of locks: %d\nLocks: %+v", vars["id"], len(mutexMap.Keys()), mutexMap.Keys()))) return } @@ -108,6 +109,6 @@ func Unlock(s string) { log.Tracef("[Mutex] Unlocked %s", s) } else { // this should never happen. Mutex should have been in the mutexMap. - log.Errorf("[Mutex] ⚠⚠⚠️ Unlock %s not in mutexMap. Skip.", s) + log.Errorf("[Mutex] ⚠️⚠️⚠️ Unlock %s not in mutexMap. Skip.", s) } } diff --git a/main.go b/main.go index b5ea0a8f..d60dd028 100644 --- a/main.go +++ b/main.go @@ -1,11 +1,12 @@ package main import ( + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/api" + "github.com/LightningTipBot/LightningTipBot/internal/api/admin" "github.com/LightningTipBot/LightningTipBot/internal/lndhub" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" - "github.com/gorilla/mux" "net/http" "runtime/debug" @@ -40,7 +41,7 @@ func startApiServer(bot *telegram.TipBot) { // start internal webhook server webhook.NewServer(bot) // start external api server - s := api.NewServer() + s := api.NewServer(internal.Configuration.Bot.LNURLServerUrl.Host) // append lnurl handler functions lnUrl := lnurl.New(bot) @@ -53,11 +54,14 @@ func startApiServer(bot *telegram.TipBot) { s.AppendRoute(`/lndhub/ext`, hub.Handle) // start internal admin server - router := mux.NewRouter() - router.PathPrefix("/debug/pprof/").Handler(http.DefaultServeMux) - router.Handle("/mutex", http.HandlerFunc(mutex.ServeHTTP)) - router.Handle("/mutex/unlock/{id}", http.HandlerFunc(mutex.UnlockHTTP)) - go http.ListenAndServe("0.0.0.0:6060", router) + adminService := admin.New(bot) + internalAdminServer := api.NewServer("0.0.0.0:6060") + internalAdminServer.AppendRoute("/mutex", mutex.ServeHTTP) + internalAdminServer.AppendRoute("/mutex/unlock/{id}", mutex.UnlockHTTP) + internalAdminServer.AppendRoute("/admin/ban/{id}", adminService.BanUser) + internalAdminServer.AppendRoute("/admin/unban/{id}", adminService.UnbanUser) + internalAdminServer.PathPrefix("/debug/pprof/", http.DefaultServeMux) + } func withRecovery() { From 88143b9a6625fbb5f794f0f6beda0c5a692d748f Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 5 Jan 2022 00:43:33 +0000 Subject: [PATCH 177/541] fix migration (#264) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/database.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/telegram/database.go b/internal/telegram/database.go index dcc4c77b..8eab190b 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -51,14 +51,14 @@ func ColumnMigrationTasks(db *gorm.DB) error { err = database.MigrateAnonIdInt32Hash(db) } - // sha256_anon_id migration (2022-01-01) - if !db.Migrator().HasColumn(&lnbits.User{}, "sha256_anon_id") { + // anon_id_sha256 migration (2022-01-01) + if !db.Migrator().HasColumn(&lnbits.User{}, "anon_id_sha256") { // first we need to auto migrate the user. This will create anon_id column err = db.AutoMigrate(&lnbits.User{}) if err != nil { panic(err) } - log.Info("Running sha256_anon_id database migrations ...") + log.Info("Running anon_id_sha256 database migrations ...") // run the migration on anon_id err = database.MigrateAnonIdSha265Hash(db) } From b42a9ae8b4075fac07a358563839430500fde7be Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 5 Jan 2022 01:53:45 +0100 Subject: [PATCH 178/541] using UpdateUserRecord (#265) --- internal/api/admin/admin.go | 5 ++--- internal/api/admin/ban.go | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/internal/api/admin/admin.go b/internal/api/admin/admin.go index 9968af70..b34ca610 100644 --- a/internal/api/admin/admin.go +++ b/internal/api/admin/admin.go @@ -2,15 +2,14 @@ package admin import ( "github.com/LightningTipBot/LightningTipBot/internal/telegram" - "gorm.io/gorm" ) type Service struct { - db *gorm.DB + bot *telegram.TipBot } func New(b *telegram.TipBot) Service { return Service{ - db: b.Database, + bot: b, } } diff --git a/internal/api/admin/ban.go b/internal/api/admin/ban.go index 219859de..4b90c4b5 100644 --- a/internal/api/admin/ban.go +++ b/internal/api/admin/ban.go @@ -3,6 +3,7 @@ package admin import ( "fmt" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/telegram" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" "net/http" @@ -24,7 +25,11 @@ func (s Service) UnbanUser(w http.ResponseWriter, r *http.Request) { user.Banned = false adminSlice := strings.Split(user.Wallet.Adminkey, "_") user.Wallet.Adminkey = adminSlice[len(adminSlice)-1] - s.db.Save(user) + err = telegram.UpdateUserRecord(user, *s.bot) + if err != nil { + log.Errorf("[ADMIN] could not update user: %v", err) + return + } log.Infof("[ADMIN] Unbanned user (%s)", user.ID) w.WriteHeader(http.StatusOK) } @@ -46,7 +51,12 @@ func (s Service) BanUser(w http.ResponseWriter, r *http.Request) { user.Wallet.Adminkey = fmt.Sprintf("%s_%s", reason, user.Wallet.Adminkey) } user.Wallet.Adminkey = fmt.Sprintf("%s_%s", "banned", user.Wallet.Adminkey) - s.db.Save(user) + err = telegram.UpdateUserRecord(user, *s.bot) + if err != nil { + log.Errorf("[ADMIN] could not update user: %v", err) + return + } + log.Infof("[ADMIN] Banned user (%s)", user.ID) w.WriteHeader(http.StatusOK) } @@ -57,7 +67,7 @@ func (s Service) getUserByTelegramId(r *http.Request) (*lnbits.User, error) { if v["id"] == "" { return nil, fmt.Errorf("invalid id") } - tx := s.db.Where("telegram_id = ? COLLATE NOCASE", v["id"]).First(user) + tx := s.bot.Database.Where("telegram_id = ? COLLATE NOCASE", v["id"]).First(user) if tx.Error != nil { return nil, tx.Error } From 1e66dd6a68c3bd4ae04a74b4169a6e5b1f8ac6e4 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 5 Jan 2022 01:32:39 +0000 Subject: [PATCH 179/541] fix auth (#266) * fix auth * comments Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/lndhub/lndhub.go | 42 ++++++++++++++++++++------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/internal/lndhub/lndhub.go b/internal/lndhub/lndhub.go index 896caa2e..13304984 100644 --- a/internal/lndhub/lndhub.go +++ b/internal/lndhub/lndhub.go @@ -2,14 +2,15 @@ package lndhub import ( "encoding/base64" + "net/http" + "strings" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/api" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/telegram" log "github.com/sirupsen/logrus" "gorm.io/gorm" - "net/http" - "strings" ) type LndHub struct { @@ -21,26 +22,27 @@ func New(bot *telegram.TipBot) LndHub { } func (w LndHub) Handle(writer http.ResponseWriter, request *http.Request) { auth := request.Header.Get("Authorization") - if auth == "" { - return - } - username, password, ok := parseBearerAuth(auth) - if !ok { - return - } - log.Debugf("[LNDHUB] %s, %s", username, password) - if strings.Contains(password, "_") || strings.HasPrefix(password, "banned_") { - log.Warnf("[LNDHUB] Banned user. Not forwarding request") - return - } - user := &lnbits.User{} - tx := w.database.Where("wallet_adminkey = ? COLLATE NOCASE", password).First(user) - if tx.Error != nil { - log.Warnf("[LNDHUB] wallet admin key (%s) not found: %v", password, tx.Error) - return + + // check if the user is banned + if auth != "" { + username, password, ok := parseBearerAuth(auth) + if !ok { + return + } + log.Debugf("[LNDHUB] %s, %s", username, password) + if strings.Contains(password, "_") || strings.HasPrefix(password, "banned_") { + log.Warnf("[LNDHUB] Banned user. Not forwarding request") + return + } + user := &lnbits.User{} + tx := w.database.Where("wallet_adminkey = ? COLLATE NOCASE", password).First(user) + if tx.Error != nil { + log.Warnf("[LNDHUB] wallet admin key (%s) not found: %v", password, tx.Error) + return + } } + // if not, proxy the request api.Proxy(writer, request, internal.Configuration.Lnbits.Url) - } // parseBasicAuth parses an HTTP Basic Authentication string. From 53c384834acdfc84505f1c148462ed111a3b07e9 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 5 Jan 2022 09:14:24 +0000 Subject: [PATCH 180/541] lnurl update (#267) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/lndhub/lndhub.go | 14 ++++++++------ internal/telegram/invoice.go | 2 +- internal/telegram/lnurl.go | 2 +- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/lndhub/lndhub.go b/internal/lndhub/lndhub.go index 13304984..fce284fd 100644 --- a/internal/lndhub/lndhub.go +++ b/internal/lndhub/lndhub.go @@ -25,28 +25,30 @@ func (w LndHub) Handle(writer http.ResponseWriter, request *http.Request) { // check if the user is banned if auth != "" { - username, password, ok := parseBearerAuth(auth) + _, password, ok := parseBearerAuth(auth) if !ok { return } - log.Debugf("[LNDHUB] %s, %s", username, password) + // first we make sure that the password is not already "banned_" if strings.Contains(password, "_") || strings.HasPrefix(password, "banned_") { - log.Warnf("[LNDHUB] Banned user. Not forwarding request") + log.Warnf("[LNDHUB] Banned user %s. Not forwarding request", password) return } + // then we check whether the "normal" password provided is in the database (it should be not if the user is banned) user := &lnbits.User{} tx := w.database.Where("wallet_adminkey = ? COLLATE NOCASE", password).First(user) if tx.Error != nil { - log.Warnf("[LNDHUB] wallet admin key (%s) not found: %v", password, tx.Error) + log.Warnf("[LNDHUB] Could not get wallet admin key %s: %v", password, tx.Error) return } + log.Debugf("[LNDHUB] User: %s", telegram.GetUserStr(user.Telegram)) } // if not, proxy the request api.Proxy(writer, request, internal.Configuration.Lnbits.Url) } -// parseBasicAuth parses an HTTP Basic Authentication string. -// "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). +// parseBearerAuth parses an HTTP Basic Authentication string. +// "Bearer QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true). func parseBearerAuth(auth string) (username, password string, ok bool) { const prefix = "Bearer " // Case insensitive prefix match. See Issue 22736. diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 1be9ce1b..cfaa5d58 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -101,7 +101,7 @@ func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { } creatingMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlGettingUserMessage")) - log.Infof("[/invoice] Creating invoice for %s of %d sat.", userStr, amount) + log.Debugf("[/invoice] Creating invoice for %s of %d sat.", userStr, amount) invoice, err := bot.createInvoiceWithEvent(ctx, user, amount, memo, InvoiceCallbackGeneric, "") if err != nil { errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err.Error()) diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 2e80330a..88663b08 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -118,7 +118,7 @@ func (bot *TipBot) UserGetAnonLightningAddress(user *lnbits.User) (string, error } func UserGetLNURL(user *lnbits.User) (string, error) { - name := fmt.Sprint(user.AnonID) + name := fmt.Sprint(user.AnonIDSha256) callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, name) log.Debugf("[lnurlReceiveHandler] %s's LNURL: %s", GetUserStr(user.Telegram), callback) From 8c41fba2774318a1912c5bfbeafea53646187be1 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 5 Jan 2022 21:05:18 +0000 Subject: [PATCH 181/541] no edit stack (#268) * no edit stack * fix typo Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/faucet.go | 10 +++++----- translations/de.toml | 2 +- translations/en.toml | 2 +- translations/es.toml | 2 +- translations/fr.toml | 2 +- translations/id.toml | 2 +- translations/it.toml | 2 +- translations/nl.toml | 2 +- translations/pt-br.toml | 2 +- translations/ru.toml | 2 +- translations/tr.toml | 2 +- 11 files changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 23570be4..3ba11f13 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -82,7 +82,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User // // check for memo in command memo := GetMemoFromCommand(text, 3) - inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), perUserAmount, amount, amount, 0, nTotal, MakeProgressbar(amount, amount)) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), perUserAmount, GetUserStrMd(sender), amount, amount, 0, nTotal, MakeProgressbar(amount, amount)) if len(memo) > 0 { inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineFaucetAppendMemo"), memo) } @@ -326,7 +326,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback }() // build faucet message - inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) + inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, GetUserStrMd(inlineFaucet.From.Telegram), inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) memo := inlineFaucet.Memo if len(memo) > 0 { inlineFaucet.Message = inlineFaucet.Message + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetAppendMemo"), memo) @@ -340,7 +340,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // update the message if the faucet still has some sats left after this tx if inlineFaucet.RemainingAmount >= inlineFaucet.PerUserAmount { go func() { - bot.tryEditStack(c.Message, inlineFaucet.ID, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) + bot.tryEditMessage(c.Message, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) }() } @@ -365,7 +365,7 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor inlineFaucet := fn.(*InlineFaucet) if ignoreID || c.Sender.ID == inlineFaucet.From.Telegram.ID { - bot.tryEditStack(c.Message, inlineFaucet.ID, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false inlineFaucet.Canceled = true @@ -381,7 +381,7 @@ func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFauce if inlineFaucet.UserNeedsWallet { inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } - bot.tryEditStack(c.Message, inlineFaucet.ID, inlineFaucet.Message, &tb.ReplyMarkup{}) + bot.tryEditMessage(c.Message, inlineFaucet.Message, &tb.ReplyMarkup{}) inlineFaucet.Active = false log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) once.Remove(inlineFaucet.ID) diff --git a/translations/de.toml b/translations/de.toml index 93d1d7a0..17759d5b 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -262,7 +262,7 @@ inlineQueryFaucetDescription = """Befehl: @%s faucet """ inlineResultFaucetTitle = """🚰 Erzeuge einen %d sat Zapfhahn.""" inlineResultFaucetDescription = """👉 Klicke hier um den Zapfhahn in diesen Chat zu senden.""" -inlineFaucetMessage = """Drücke ✅ um %d sat aus diesem Zapfhahn zu zapfen. +inlineFaucetMessage = """Drücke ✅ um %d sat aus %s's Zapfhahn zu zapfen. 🚰 Verbleibend: %d/%d sat (%d/%d gezapft) %s""" diff --git a/translations/en.toml b/translations/en.toml index f4d81fc0..6e648943 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -267,7 +267,7 @@ inlineQueryFaucetDescription = """Usage: @%s faucet """ inlineResultFaucetTitle = """🚰 Create a %d sat faucet.""" inlineResultFaucetDescription = """👉 Click here to create a faucet in this chat.""" -inlineFaucetMessage = """Press ✅ to collect %d sat from this faucet. +inlineFaucetMessage = """Press ✅ to collect %d sat from %s's faucet. 🚰 Remaining: %d/%d sat (given to %d/%d users) %s""" diff --git a/translations/es.toml b/translations/es.toml index f7c5586e..f9b974e9 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -262,7 +262,7 @@ inlineQueryFaucetDescription = """Uso: @%s faucet """ inlineResultFaucetTitle = """🚰 Crear un grifo %d sat.""" inlineResultFaucetDescription = """👉 Haz clic aquí para crear un grifo en este chat.""" -inlineFaucetMessage = """Pulsa ✅ para cobrar %d sat de este grifo. +inlineFaucetMessage = """Pulsa ✅ para cobrar %d sat de este grifo de %s. 🚰 Restante: %d/%d sat (dado a %d/%d usuarios) %s""" diff --git a/translations/fr.toml b/translations/fr.toml index 05ad2245..cffcbf84 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -254,7 +254,7 @@ inlineQueryFaucetDescription = """Usage: @%s faucet """ inlineResultFaucetTitle = """🚰 Crea una distribuzione per un totale di %d sat.""" inlineResultFaucetDescription = """👉 Clicca qui per creare una distribuzione di fondi in questa chat.""" -inlineFaucetMessage = """Premi ✅ per riscuotere %d sat da questa distribuzione. +inlineFaucetMessage = """Premi ✅ per riscuotere %d sat da questa distribuzione da %s. 🚰 Rimanente: %d/%d sat (distribuiti a %d/%d utenti) %s""" diff --git a/translations/nl.toml b/translations/nl.toml index 34fca649..33085619 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -262,7 +262,7 @@ inlineQueryFaucetDescription = """Gebruik: @%s faucet """ inlineResultFaucetTitle = """🚰 Maak een %d sat kraan.""" inlineResultFaucetDescription = """👉 Klik hier om een kraan in deze chat te maken..""" -inlineFaucetMessage = """Druk op ✅ om %d sat te verzamelen van deze kraan. +inlineFaucetMessage = """Druk op ✅ om %d sat te verzamelen van deze kraan van %s. 🚰 Remaining: %d/%d sat (given to %d/%d users) %s""" diff --git a/translations/pt-br.toml b/translations/pt-br.toml index a4f3c059..792940ca 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -262,7 +262,7 @@ inlineQueryFaucetDescription = """Uso: @%s faucet "" inlineResultFaucetTitle = """🚰 Criar uma torneira %d sat.""" inlineResultFaucetDescription = """👉 Aperte aqui para criar uma torneira neste chat.""" -inlineFaucetMessage = """Aperte ✅ para coletar %d sat desta torneira. +inlineFaucetMessage = """Aperte ✅ para coletar %d sat desta torneira de %s. 🚰 Restante: %d/%d sat (para %d/%d usuários) %s""" diff --git a/translations/ru.toml b/translations/ru.toml index 80d836a1..50db436a 100644 --- a/translations/ru.toml +++ b/translations/ru.toml @@ -266,7 +266,7 @@ inlineQueryFaucetDescription = """Использование: @%s faucet Date: Thu, 6 Jan 2022 10:33:01 +0100 Subject: [PATCH 182/541] update edit stack (#269) * update edit stack * update faucet * better debug logging Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/edit.go | 8 ++++---- internal/telegram/faucet.go | 24 ++++++++++-------------- internal/telegram/helpers.go | 4 ++-- internal/telegram/telegram.go | 6 +++--- internal/telegram/users.go | 2 +- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go index 6445fedb..cb1afdcc 100644 --- a/internal/telegram/edit.go +++ b/internal/telegram/edit.go @@ -47,14 +47,14 @@ func (bot TipBot) startEditWorker() { if err != nil { log.Errorf("[startEditWorker] Ignoring edit error: %s. len(editStack)=%d", err.Error(), len(editStack.Keys())) } - log.Tracef("[startEditWorker] message from stack edited %+v. len(editStack)=%d", editFromStack, len(editStack.Keys())) + log.Debugf("[startEditWorker] message from stack edited %+v. len(editStack)=%d", editFromStack, len(editStack.Keys())) editFromStack.lastEdit = time.Now() editFromStack.edited = true editStack.Set(k, editFromStack) } } else { if editFromStack.lastEdit.Before(time.Now().Add(-(time.Duration(5) * time.Second))) { - log.Tracef("[startEditWorker] removing message edit from stack %+v. len(editStack)=%d", editFromStack, len(editStack.Keys())) + log.Debugf("[startEditWorker] removing message edit from stack %+v. len(editStack)=%d", editFromStack, len(editStack.Keys())) editStack.Remove(k) } } @@ -77,12 +77,12 @@ func (bot TipBot) tryEditStack(to tb.Editable, key string, what interface{}, opt if e, ok := editStack.Get(key); ok { editFromStack := e.(edit) if editFromStack.what == what.(string) { - log.Tracef("[tryEditStack] Message already in edit stack. Skipping") + log.Debugf("[tryEditStack] Message already in edit stack. Skipping") return } } e := edit{options: options, key: key, what: what, to: to} editStack.Set(key, e) - log.Tracef("[tryEditStack] Added message %s to edit stack. len(editStack)=%d", key, len(editStack.Keys())) + log.Debugf("[tryEditStack] Added message %s to edit stack. len(editStack)=%d", key, len(editStack.Keys())) } diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 3ba11f13..55b66c00 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -86,7 +86,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User if len(memo) > 0 { inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineFaucetAppendMemo"), memo) } - id := fmt.Sprintf("inl-faucet-%d-%d-%s", sender.ID, amount, RandStringRunes(5)) + id := fmt.Sprintf("faucet:%s:%d", RandStringRunes(10), amount) return &InlineFaucet{ Base: storage.New(storage.ID(id)), @@ -167,10 +167,8 @@ func (bot TipBot) makeQueryFaucet(ctx context.Context, q *tb.Query, query bool) func (bot TipBot) makeFaucetKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { // inlineFaucetMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} - acceptInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "collectButtonMessage"), "confirm_faucet_inline") - cancelInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_faucet_inline") - acceptInlineFaucetButton.Data = id - cancelInlineFaucetButton.Data = id + acceptInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "collectButtonMessage"), "confirm_faucet_inline", id) + cancelInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_faucet_inline", id) inlineFaucetMenu.Inline( inlineFaucetMenu.Row( acceptInlineFaucetButton, @@ -250,6 +248,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback log.Errorf("[acceptInlineFaucetHandler] c.Data: %s, Error: %s", c.Data, err.Error()) return } + log.Debugf("[acceptInlineFaucetHandler] Callback c.Data: %d tx.ID: %s", c.Data, tx.ID) + inlineFaucet := fn.(*InlineFaucet) from := inlineFaucet.From // log faucet link if possible @@ -320,10 +320,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.NTaken += 1 inlineFaucet.To = append(inlineFaucet.To, to) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount - go func() { - bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) - bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) - }() + bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) + bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) // build faucet message inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, GetUserStrMd(inlineFaucet.From.Telegram), inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) @@ -339,9 +337,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // update the message if the faucet still has some sats left after this tx if inlineFaucet.RemainingAmount >= inlineFaucet.PerUserAmount { - go func() { - bot.tryEditMessage(c.Message, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) - }() + bot.tryEditStack(c.Message, inlineFaucet.ID, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) } } @@ -365,7 +361,7 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor inlineFaucet := fn.(*InlineFaucet) if ignoreID || c.Sender.ID == inlineFaucet.From.Telegram.ID { - bot.tryEditMessage(c.Message, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditStack(c.Message, inlineFaucet.ID, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineFaucet inactive inlineFaucet.Active = false inlineFaucet.Canceled = true @@ -381,7 +377,7 @@ func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFauce if inlineFaucet.UserNeedsWallet { inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } - bot.tryEditMessage(c.Message, inlineFaucet.Message, &tb.ReplyMarkup{}) + bot.tryEditStack(c.Message, inlineFaucet.ID, inlineFaucet.Message, &tb.ReplyMarkup{}) inlineFaucet.Active = false log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) once.Remove(inlineFaucet.ID) diff --git a/internal/telegram/helpers.go b/internal/telegram/helpers.go index 74e42828..8a6128d4 100644 --- a/internal/telegram/helpers.go +++ b/internal/telegram/helpers.go @@ -35,7 +35,7 @@ func GetMemoFromCommand(command string, fromWord int) string { } func MakeProgressbar(current int64, total int64) string { - MAX_BARS := 16 + MAX_BARS := 10 progress := math.Round((float64(current) / float64(total)) * float64(MAX_BARS)) progressbar := strings.Repeat("🟩", int(progress)) progressbar += strings.Repeat("⬜️", MAX_BARS-int(progress)) @@ -43,7 +43,7 @@ func MakeProgressbar(current int64, total int64) string { } func MakeTipjarbar(current int64, total int64) string { - MAX_BARS := 16 + MAX_BARS := 10 progress := math.Round((float64(current) / float64(total)) * float64(MAX_BARS)) progressbar := strings.Repeat("🍯", int(progress)) progressbar += strings.Repeat("⬜️", MAX_BARS-int(progress)) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 274115f7..e48fccfe 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -60,15 +60,15 @@ func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...i } func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...interface{}) (msg *tb.Message, err error) { - // do not attempt edit if the message did not change - + // get a sig for the rate limiter sig, chat := to.MessageSig() if chat != 0 { sig = strconv.FormatInt(chat, 10) } rate.CheckLimit(sig) + _, chatId := to.MessageSig() - log.Tracef("[tryEditMessage] sig: %s, chatId: %d", sig, chatId) + log.Debugf("[tryEditMessage] sig: %s, chatId: %d", sig, chatId) msg, err = bot.Telegram.Edit(to, what, bot.appendMainMenu(chatId, to, options)...) if err != nil { log.Warnln(err.Error()) diff --git a/internal/telegram/users.go b/internal/telegram/users.go index 07945456..244327d5 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -124,7 +124,7 @@ func (bot *TipBot) UserExists(user *tb.User) (*lnbits.User, bool) { func (bot *TipBot) UserIsBanned(user *lnbits.User) bool { // do not respond to banned users if user.Wallet == nil { - log.Errorf("[UserIsBanned] User %s has no wallet.\n", GetUserStr(user.Telegram)) + log.Tracef("[UserIsBanned] User %s has no wallet.\n", GetUserStr(user.Telegram)) return false } if strings.HasPrefix(user.Wallet.Adminkey, "banned") || strings.Contains(user.Wallet.Adminkey, "_") { From 02eb0716aa48c4b4cc96a41b24549c10d3873e9d Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 6 Jan 2022 09:39:25 +0000 Subject: [PATCH 183/541] lil update (#270) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/api/middleware.go | 4 +++- internal/telegram/helpers.go | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/api/middleware.go b/internal/api/middleware.go index 8c051d47..7c9a1a92 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -1,13 +1,15 @@ package api import ( - log "github.com/sirupsen/logrus" "net/http" "net/http/httputil" + + log "github.com/sirupsen/logrus" ) func LoggingMiddleware(prefix string, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + log.Debugf("[%s] %s %s", prefix, r.Method, r.URL.Path) log.Tracef("[%s]\n%s", prefix, dump(r)) r.BasicAuth() next.ServeHTTP(w, r) diff --git a/internal/telegram/helpers.go b/internal/telegram/helpers.go index 8a6128d4..74e42828 100644 --- a/internal/telegram/helpers.go +++ b/internal/telegram/helpers.go @@ -35,7 +35,7 @@ func GetMemoFromCommand(command string, fromWord int) string { } func MakeProgressbar(current int64, total int64) string { - MAX_BARS := 10 + MAX_BARS := 16 progress := math.Round((float64(current) / float64(total)) * float64(MAX_BARS)) progressbar := strings.Repeat("🟩", int(progress)) progressbar += strings.Repeat("⬜️", MAX_BARS-int(progress)) @@ -43,7 +43,7 @@ func MakeProgressbar(current int64, total int64) string { } func MakeTipjarbar(current int64, total int64) string { - MAX_BARS := 10 + MAX_BARS := 16 progress := math.Round((float64(current) / float64(total)) * float64(MAX_BARS)) progressbar := strings.Repeat("🍯", int(progress)) progressbar += strings.Repeat("⬜️", MAX_BARS-int(progress)) From 39c727d3c483f644e27c7c5f7b3ccd97a7244c1e Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 7 Jan 2022 11:13:58 +0000 Subject: [PATCH 184/541] fix the keyboard, fix the world (#272) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/edit.go | 6 +----- internal/telegram/faucet.go | 9 +++++---- internal/telegram/inline_receive.go | 1 + internal/telegram/inline_send.go | 1 + internal/telegram/{inline_tipjar.go => tipjar.go} | 10 ++-------- 5 files changed, 10 insertions(+), 17 deletions(-) rename internal/telegram/{inline_tipjar.go => tipjar.go} (95%) diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go index cb1afdcc..efee4e01 100644 --- a/internal/telegram/edit.go +++ b/internal/telegram/edit.go @@ -1,7 +1,6 @@ package telegram import ( - "strconv" "strings" "time" @@ -69,10 +68,7 @@ func (bot TipBot) startEditWorker() { // tryEditStack will add the editable to the edit stack, if what (message) changed. func (bot TipBot) tryEditStack(to tb.Editable, key string, what interface{}, options ...interface{}) { sig, chat := to.MessageSig() - if chat != 0 { - sig = strconv.FormatInt(chat, 10) - } - log.Debugf("[tryEditStack] sig=%s, key=%s, what=%+v, options=%+v", sig, key, what, options) + log.Debugf("[tryEditStack] sig=%s, chat=%d, key=%s, what=%+v, options=%+v", sig, chat, key, what, options) // var sig = fmt.Sprintf("%s-%d", msgSig, chat) if e, ok := editStack.Get(key); ok { editFromStack := e.(edit) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 55b66c00..06bce548 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -166,7 +166,7 @@ func (bot TipBot) makeQueryFaucet(ctx context.Context, q *tb.Query, query bool) } func (bot TipBot) makeFaucetKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { - // inlineFaucetMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} + inlineFaucetMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} acceptInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "collectButtonMessage"), "confirm_faucet_inline", id) cancelInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_faucet_inline", id) inlineFaucetMenu.Inline( @@ -320,9 +320,10 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.NTaken += 1 inlineFaucet.To = append(inlineFaucet.To, to) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount - bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) - bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) - + go func() { + bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) + bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) + }() // build faucet message inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, GetUserStrMd(inlineFaucet.From.Telegram), inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) memo := inlineFaucet.Memo diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 83646ec5..a62bbe0a 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -40,6 +40,7 @@ type InlineReceive struct { } func (bot TipBot) makeReceiveKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { + inlineReceiveMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} acceptInlineReceiveButton := inlineReceiveMenu.Data(Translate(ctx, "payReceiveButtonMessage"), "confirm_receive_inline") cancelInlineReceiveButton := inlineReceiveMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_receive_inline") acceptInlineReceiveButton.Data = id diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index 64b2e518..b17827ca 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -38,6 +38,7 @@ type InlineSend struct { } func (bot TipBot) makeSendKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { + inlineSendMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} acceptInlineSendButton := inlineSendMenu.Data(Translate(ctx, "receiveButtonMessage"), "confirm_send_inline") cancelInlineSendButton := inlineSendMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_send_inline") acceptInlineSendButton.Data = id diff --git a/internal/telegram/inline_tipjar.go b/internal/telegram/tipjar.go similarity index 95% rename from internal/telegram/inline_tipjar.go rename to internal/telegram/tipjar.go index 1aad55df..00886897 100644 --- a/internal/telegram/inline_tipjar.go +++ b/internal/telegram/tipjar.go @@ -82,7 +82,7 @@ func (bot TipBot) createTipjar(ctx context.Context, text string, sender *tb.User if len(memo) > 0 { inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineTipjarAppendMemo"), memo) } - id := fmt.Sprintf("inl-tipjar-%d-%d-%s", sender.ID, amount, RandStringRunes(5)) + id := fmt.Sprintf("tipjar:%s:%d", RandStringRunes(10), amount) return &InlineTipjar{ Base: storage.New(storage.ID(id)), @@ -161,7 +161,7 @@ func (bot TipBot) makeQueryTipjar(ctx context.Context, q *tb.Query, query bool) } func (bot TipBot) makeTipjarKeyboard(ctx context.Context, inlineTipjar *InlineTipjar) *tb.ReplyMarkup { - // inlineTipjarMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} + inlineTipjarMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} // slice of buttons buttons := make([]tb.Btn, 0) cancelInlineTipjarButton := inlineTipjarMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_tipjar_inline", inlineTipjar.ID) @@ -307,9 +307,6 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback if len(memo) > 0 { inlineTipjar.Message = inlineTipjar.Message + fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarAppendMemo"), memo) } - // if inlineTipjar.UserNeedsWallet { - // inlineTipjar.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) - // } // update message log.Infoln(inlineTipjar.Message) bot.tryEditMessage(c.Message, inlineTipjar.Message, bot.makeTipjarKeyboard(ctx, inlineTipjar)) @@ -322,9 +319,6 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback inlineTipjar.Amount, inlineTipjar.NGiven, ) - // if inlineTipjar.UserNeedsWallet { - // inlineTipjar.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) - // } bot.tryEditMessage(c.Message, inlineTipjar.Message) inlineTipjar.Active = false } From e70344892fa638c69d1a5c15996aa986e93d1766 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 9 Jan 2022 10:08:27 +0000 Subject: [PATCH 185/541] shop fixes (#274) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/interceptor.go | 1 + internal/telegram/shop.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index dd6b916b..7ac5f096 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -58,6 +58,7 @@ func (bot TipBot) idInterceptor(ctx context.Context, i interface{}) (context.Con return context.WithValue(ctx, "uid", RandStringRunes(64)), nil } +// answerCallbackInterceptor will answer the callback with the given text in the context func (bot TipBot) answerCallbackInterceptor(ctx context.Context, i interface{}) (context.Context, error) { switch i.(type) { case *tb.Callback: diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 8a03dcb7..1643809e 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -528,7 +528,7 @@ func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) { return } SetUserState(user, bot, lnbits.UserStateShopItemSendPhoto, string(paramsJson)) - bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("🌄 Send me an image.")) + bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("🌄 Send me a cover image.")) } // addShopItem is a helper function for creating a shop item in the database @@ -983,7 +983,7 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { // shows "your shop" or "@other's shop" shopOwnerText := "your" if shopOwner.Telegram.ID != user.Telegram.ID { - shopOwnerText = fmt.Sprintf("%s's", GetUserStrMd(shopOwner.Telegram)) + shopOwnerText = fmt.Sprintf("%s's", GetUserStr(shopOwner.Telegram)) } ShopsText = fmt.Sprintf(ShopsTextWelcome, shopOwnerText) if len(shops.Description) > 0 { From 31250273b0aa3bc8d53f3b07668d1cb3b3c8a215 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 9 Jan 2022 11:16:05 +0000 Subject: [PATCH 186/541] better log (#275) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/edit.go | 8 ++++---- internal/telegram/faucet.go | 2 +- internal/telegram/shop.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go index efee4e01..b0a28219 100644 --- a/internal/telegram/edit.go +++ b/internal/telegram/edit.go @@ -46,14 +46,14 @@ func (bot TipBot) startEditWorker() { if err != nil { log.Errorf("[startEditWorker] Ignoring edit error: %s. len(editStack)=%d", err.Error(), len(editStack.Keys())) } - log.Debugf("[startEditWorker] message from stack edited %+v. len(editStack)=%d", editFromStack, len(editStack.Keys())) + log.Tracef("[startEditWorker] message from stack edited %+v. len(editStack)=%d", editFromStack, len(editStack.Keys())) editFromStack.lastEdit = time.Now() editFromStack.edited = true editStack.Set(k, editFromStack) } } else { if editFromStack.lastEdit.Before(time.Now().Add(-(time.Duration(5) * time.Second))) { - log.Debugf("[startEditWorker] removing message edit from stack %+v. len(editStack)=%d", editFromStack, len(editStack.Keys())) + log.Tracef("[startEditWorker] removing message edit from stack %+v. len(editStack)=%d", editFromStack, len(editStack.Keys())) editStack.Remove(k) } } @@ -73,12 +73,12 @@ func (bot TipBot) tryEditStack(to tb.Editable, key string, what interface{}, opt if e, ok := editStack.Get(key); ok { editFromStack := e.(edit) if editFromStack.what == what.(string) { - log.Debugf("[tryEditStack] Message already in edit stack. Skipping") + log.Tracef("[tryEditStack] Message already in edit stack. Skipping") return } } e := edit{options: options, key: key, what: what, to: to} editStack.Set(key, e) - log.Debugf("[tryEditStack] Added message %s to edit stack. len(editStack)=%d", key, len(editStack.Keys())) + log.Tracef("[tryEditStack] Added message %s to edit stack. len(editStack)=%d", key, len(editStack.Keys())) } diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 06bce548..c3024d71 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -248,7 +248,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback log.Errorf("[acceptInlineFaucetHandler] c.Data: %s, Error: %s", c.Data, err.Error()) return } - log.Debugf("[acceptInlineFaucetHandler] Callback c.Data: %d tx.ID: %s", c.Data, tx.ID) + log.Debugf("[acceptInlineFaucetHandler] Callback c.Data: %s tx.ID: %s", c.Data, tx.ID) inlineFaucet := fn.(*InlineFaucet) from := inlineFaucet.From diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 1643809e..452dc8fa 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -594,7 +594,7 @@ func (bot *TipBot) addShopItemPhoto(ctx context.Context, m *tb.Message) { runtime.IgnoreError(shop.Set(shop, bot.ShopBunt)) bot.tryDeleteMessage(m) - bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("✅ Image added.")) + bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("✅ Image added. You can now add files to this item. Don't forget to set a title and a price.")) ResetUserState(user, bot) // go func() { // time.Sleep(time.Duration(5) * time.Second) From 7ed34f656b9521dc483956c53bee698352cba35f Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 9 Jan 2022 18:52:18 +0000 Subject: [PATCH 187/541] fix one panic (#276) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/edit.go | 2 +- internal/telegram/faucet.go | 2 +- internal/telegram/shop.go | 17 +++++++++-------- internal/telegram/shop_helpers.go | 9 ++++----- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go index b0a28219..6aa0cb41 100644 --- a/internal/telegram/edit.go +++ b/internal/telegram/edit.go @@ -68,7 +68,7 @@ func (bot TipBot) startEditWorker() { // tryEditStack will add the editable to the edit stack, if what (message) changed. func (bot TipBot) tryEditStack(to tb.Editable, key string, what interface{}, options ...interface{}) { sig, chat := to.MessageSig() - log.Debugf("[tryEditStack] sig=%s, chat=%d, key=%s, what=%+v, options=%+v", sig, chat, key, what, options) + log.Tracef("[tryEditStack] sig=%s, chat=%d, key=%s, what=%+v, options=%+v", sig, chat, key, what, options) // var sig = fmt.Sprintf("%s-%d", msgSig, chat) if e, ok := editStack.Get(key); ok { editFromStack := e.(edit) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index c3024d71..5d3aac15 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -248,7 +248,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback log.Errorf("[acceptInlineFaucetHandler] c.Data: %s, Error: %s", c.Data, err.Error()) return } - log.Debugf("[acceptInlineFaucetHandler] Callback c.Data: %s tx.ID: %s", c.Data, tx.ID) + log.Tracef("[acceptInlineFaucetHandler] Callback c.Data: %s tx.ID: %s", c.Data, tx.ID) inlineFaucet := fn.(*InlineFaucet) from := inlineFaucet.From diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 452dc8fa..4ec855a4 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -618,13 +618,13 @@ func (bot *TipBot) shopItemAddItemHandler(ctx context.Context, c *tb.Callback) { } shopView, err := bot.getUserShopview(ctx, user) if err != nil { - log.Errorf("[addItemFileHandler] %s", err.Error()) + log.Errorf("[shopItemAddItemHandler] %s", err.Error()) return } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - log.Errorf("[shopNewItemHandler] %s", err.Error()) + log.Errorf("[shopItemAddItemHandler] %s", err.Error()) return } @@ -651,10 +651,9 @@ func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) { log.Errorf("[addItemFileHandler] %s", err.Error()) return } - shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - log.Errorf("[shopNewItemHandler] %s", err.Error()) + log.Errorf("[addItemFileHandler] %s", err.Error()) return } @@ -739,12 +738,12 @@ func (bot *TipBot) shopGetItemFilesHandler(ctx context.Context, c *tb.Callback) } shopView, err := bot.getUserShopview(ctx, user) if err != nil { - log.Errorf("[addItemFileHandler] %s", err.Error()) + log.Errorf("[shopGetItemFilesHandler] %s", err.Error()) return } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - log.Errorf("[shopNewItemHandler] %s", err.Error()) + log.Errorf("[shopGetItemFilesHandler] %s", err.Error()) return } itemID := c.Data @@ -1010,12 +1009,14 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { var shopsMsg *tb.Message if err == nil && !strings.HasPrefix(strings.Split(m.Text, " ")[0], "/shop") { // the user is returning to a shops view from a back button callback - if shopView.Message.Photo == nil { + if shopView.Message != nil && shopView.Message.Photo == nil { shopsMsg, _ = bot.tryEditMessage(shopView.Message, ShopsText, bot.shopsMainMenu(ctx, shops)) } if shopsMsg == nil { // if editing has failed, we will send a new message - bot.tryDeleteMessage(shopView.Message) + if shopView.Message != nil { + bot.tryDeleteMessage(shopView.Message) + } shopsMsg = bot.trySendMessage(m.Chat, ShopsText, bot.shopsMainMenu(ctx, shops)) } diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index af226b41..e972a453 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -70,7 +70,7 @@ func (bot TipBot) shopItemSettingsMenu(ctx context.Context, shop *Shop, item *Sh 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) + shopItemAddFileButton = shopKeyboard.Data("💾 Add files ...", "shop_itemaddfile", item.ID) shopItemSettingsBackButton = shopKeyboard.Data("⬅️ Back", "shop_itemsettingsback", item.ID) user := LoadUser(ctx) buttons := []tb.Row{} @@ -162,15 +162,14 @@ func (bot *TipBot) getUserShopview(ctx context.Context, user *lnbits.User) (shop } func (bot *TipBot) shopViewDeleteAllStatusMsgs(ctx context.Context, user *lnbits.User) (shopView ShopView, err error) { mutex.Lock(fmt.Sprintf("shopview-delete-%d", user.Telegram.ID)) + defer mutex.Unlock(fmt.Sprintf("shopview-delete-%d", user.Telegram.ID)) shopView, err = bot.getUserShopview(ctx, user) if err != nil { return } - deleteStatusMessages(shopView.StatusMessages, bot) shopView.StatusMessages = make([]*tb.Message, 0) bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) - mutex.Unlock(fmt.Sprintf("shopview-delete-%d", user.Telegram.ID)) return } @@ -189,6 +188,7 @@ func (bot *TipBot) sendStatusMessage(ctx context.Context, to tb.Recipient, what // write into cache mutex.Lock(id) + defer mutex.Unlock(id) shopView, err := bot.getUserShopview(ctx, user) if err != nil { return nil @@ -196,7 +196,6 @@ func (bot *TipBot) sendStatusMessage(ctx context.Context, to tb.Recipient, what statusMsg := bot.trySendMessage(to, what, options...) shopView.StatusMessages = append(shopView.StatusMessages, statusMsg) bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) - mutex.Unlock(id) return statusMsg } @@ -272,7 +271,7 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e return shop, nil } -// getShop returns the Shop for the given ID +// getShop returns the Shop of a given ID func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) { tx := &Shop{Base: storage.New(storage.ID(shopId))} mutex.LockWithContext(ctx, tx.ID) From 192930a289169576f439c620aa01d71fc3bb9dfa Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 9 Jan 2022 19:01:26 +0000 Subject: [PATCH 188/541] Shop panic attempt 2 (#277) * less log * fix shop first view Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/shop.go | 12 +----------- internal/telegram/telegram.go | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 4ec855a4..0572e9e6 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -489,18 +489,8 @@ func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) { Page: 0, ShopOwner: shopOwner, } - // bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) - shopView.Message = bot.displayShopItem(ctx, m, shop) - // shopMessage := &tb.Message{Chat: m.Chat} - // if len(shop.ItemIds) > 0 { - // // item := shop.Items[shop.ItemIds[shopView.Page]] - // // shopMessage = bot.trySendMessage(m.Chat, item.TbPhoto, bot.shopMenu(ctx, shop, &item)) - // shopMessage = bot.displayShopItem(ctx, m, shop) - // } else { - // shopMessage = bot.trySendMessage(m.Chat, "No items in shop.", bot.shopMenu(ctx, shop, &ShopItem{})) - // } - // shopView.Message = shopMessage bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + shopView.Message = bot.displayShopItem(ctx, m, shop) return } diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index e48fccfe..6c148cb2 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -68,7 +68,7 @@ func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...in rate.CheckLimit(sig) _, chatId := to.MessageSig() - log.Debugf("[tryEditMessage] sig: %s, chatId: %d", sig, chatId) + log.Tracef("[tryEditMessage] sig: %s, chatId: %d", sig, chatId) msg, err = bot.Telegram.Edit(to, what, bot.appendMainMenu(chatId, to, options)...) if err != nil { log.Warnln(err.Error()) From 4e812f7897fb0a062df02f2edc35a551ad63a1c0 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:39:09 +0100 Subject: [PATCH 189/541] add errors to handler (#273) * add errors to handler * add errors to handlers * shop fixes (#274) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * better log (#275) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * add errors to map * more errors * rename NoWalletError * fix one panic (#276) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * add invalid type error * Shop panic attempt 2 (#277) * less log * fix shop first view Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * make errors private * add errors to handler * add errors to handlers * add errors to map * more errors * rename NoWalletError * add invalid type error * make errors private * patch * return handlers * add noPrivateChat * add invalid amount err Co-authored-by: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/errors/errors.go | 21 +- internal/errors/types.go | 75 ++++++ internal/telegram/amounts.go | 46 ++-- internal/telegram/balance.go | 12 +- internal/telegram/donate.go | 16 +- internal/telegram/faucet.go | 48 ++-- internal/telegram/files.go | 9 +- internal/telegram/handler.go | 6 +- internal/telegram/help.go | 12 +- internal/telegram/inline_query.go | 17 +- internal/telegram/inline_receive.go | 64 ++--- internal/telegram/inline_send.go | 47 ++-- internal/telegram/intercept/callback.go | 8 +- internal/telegram/intercept/message.go | 11 +- internal/telegram/intercept/query.go | 8 +- internal/telegram/interceptor.go | 23 +- internal/telegram/invoice.go | 17 +- internal/telegram/link.go | 10 +- internal/telegram/lnurl-withdraw.go | 35 +-- internal/telegram/lnurl.go | 34 +-- internal/telegram/pay.go | 41 +-- internal/telegram/photo.go | 24 +- internal/telegram/send.go | 65 ++--- internal/telegram/shop.go | 333 +++++++++++++----------- internal/telegram/start.go | 13 +- internal/telegram/state.go | 2 +- internal/telegram/text.go | 36 ++- internal/telegram/tip.go | 21 +- internal/telegram/tipjar.go | 46 ++-- 29 files changed, 620 insertions(+), 480 deletions(-) create mode 100644 internal/errors/types.go diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 72a21e59..b6931194 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -1,20 +1,17 @@ package errors -import "encoding/json" - -type TipBotErrorType int - -const ( - DecodeAmountError TipBotErrorType = 1000 + iota - DecodePerUserAmountError - InvalidAmountError - InvalidAmountPerUserError - GetBalanceError - BalanceToLowError +import ( + "encoding/json" ) +func Create(code TipBotErrorType) TipBotError { + return errMap[code] +} func New(code TipBotErrorType, err error) TipBotError { - return TipBotError{Err: err, Message: err.Error(), Code: code} + if err != nil { + return TipBotError{Err: err, Message: err.Error(), Code: code} + } + return Create(code) } type TipBotError struct { diff --git a/internal/errors/types.go b/internal/errors/types.go new file mode 100644 index 00000000..89014cc9 --- /dev/null +++ b/internal/errors/types.go @@ -0,0 +1,75 @@ +package errors + +import "fmt" + +type TipBotErrorType int + +const ( + UnknownError TipBotErrorType = iota + NoReplyMessageError + InvalidSyntaxError + MaxReachedError + NoPhotoError + NoFileFoundError + NotActiveError + InvalidTypeError +) + +const ( + UserNoWalletError TipBotErrorType = 2000 + iota + BalanceToLowError + SelfPaymentError + NoPrivateChatError + GetBalanceError + DecodeAmountError + DecodePerUserAmountError + InvalidAmountError + InvalidAmountPerUserError +) + +const ( + NoShopError TipBotErrorType = 3000 + iota + NotShopOwnerError + ShopNoOwnerError + ItemIdMismatchError +) + +var errMap = map[TipBotErrorType]TipBotError{ + UserNoWalletError: userNoWallet, + NoReplyMessageError: noReplyMessage, + InvalidSyntaxError: invalidSyntax, + InvalidAmountPerUserError: invalidAmount, + InvalidAmountError: invalidAmountPerUser, + NoPrivateChatError: noPrivateChat, + ShopNoOwnerError: shopNoOwner, + NotShopOwnerError: notShopOwner, + MaxReachedError: maxReached, + NoShopError: noShop, + SelfPaymentError: selfPayment, + NoPhotoError: noPhoto, + ItemIdMismatchError: itemIdMismatch, + NoFileFoundError: noFileFound, + UnknownError: unknown, + NotActiveError: notActive, + InvalidTypeError: invalidType, +} + +var ( + userNoWallet = TipBotError{Err: fmt.Errorf("user has no wallet")} + noReplyMessage = TipBotError{Err: fmt.Errorf("no reply message")} + invalidSyntax = TipBotError{Err: fmt.Errorf("invalid syntax")} + invalidAmount = TipBotError{Err: fmt.Errorf("invalid amount")} + invalidAmountPerUser = TipBotError{Err: fmt.Errorf("invalid amount per user")} + noPrivateChat = TipBotError{Err: fmt.Errorf("no private chat")} + shopNoOwner = TipBotError{Err: fmt.Errorf("shop has no owner")} + notShopOwner = TipBotError{Err: fmt.Errorf("user is not shop owner")} + maxReached = TipBotError{Err: fmt.Errorf("maximum reached")} + noShop = TipBotError{Err: fmt.Errorf("user has no shop")} + selfPayment = TipBotError{Err: fmt.Errorf("can't pay yourself")} + noPhoto = TipBotError{Err: fmt.Errorf("no photo in message")} + itemIdMismatch = TipBotError{Err: fmt.Errorf("item id mismatch")} + noFileFound = TipBotError{Err: fmt.Errorf("no file found")} + unknown = TipBotError{Err: fmt.Errorf("unknown error")} + notActive = TipBotError{Err: fmt.Errorf("element not active")} + invalidType = TipBotError{Err: fmt.Errorf("invalid type")} +) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index b6cdf1da..431a8153 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -3,8 +3,8 @@ package telegram import ( "context" "encoding/json" - "errors" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "strconv" "strings" @@ -20,7 +20,7 @@ import ( func getArgumentFromCommand(input string, which int) (output string, err error) { if len(strings.Split(input, " ")) < which+1 { - return "", errors.New("message doesn't contain enough arguments") + return "", fmt.Errorf("message doesn't contain enough arguments") } output = strings.Split(input, " ")[which] return output, nil @@ -30,7 +30,7 @@ func decodeAmountFromCommand(input string) (amount int64, err error) { if len(strings.Split(input, " ")) < 2 { errmsg := "message doesn't contain any amount" // log.Errorln(errmsg) - return 0, errors.New(errmsg) + return 0, fmt.Errorf(errmsg) } amount, err = getAmount(strings.Split(input, " ")[1]) return amount, err @@ -61,7 +61,7 @@ func getAmount(input string) (amount int64, err error) { return 0, err } if !(price.Price[currency] > 0) { - return 0, errors.New("price is zero") + return 0, fmt.Errorf("price is zero") } amount = int64(fmount / price.Price[currency] * float64(100_000_000)) return amount, nil @@ -74,7 +74,7 @@ func getAmount(input string) (amount int64, err error) { return 0, err } if amount <= 0 { - return 0, errors.New("amount must be greater than 0") + return 0, fmt.Errorf("amount must be greater than 0") } return amount, err } @@ -119,15 +119,15 @@ func (bot *TipBot) askForAmount(ctx context.Context, id string, eventType string // enterAmountHandler is invoked in anyTextHandler when the user needs to enter an amount // the amount is then stored as an entry in the user's stateKey in the user database // any other handler that relies on this, needs to load the resulting amount from the database -func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { - return // errors.New("user has no wallet"), 0 + return ctx, errors.Create(errors.UserNoWalletError) } if !(user.StateKey == lnbits.UserEnterAmount) { ResetUserState(user, bot) - return // errors.New("user state does not match"), 0 + return ctx, fmt.Errorf("invalid statekey") } var EnterAmountStateData EnterAmountStateData @@ -135,7 +135,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { if err != nil { log.Errorf("[enterAmountHandler] %s", err.Error()) ResetUserState(user, bot) - return + return ctx, err } amount, err := getAmount(m.Text) @@ -143,7 +143,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { log.Warnf("[enterAmountHandler] %s", err.Error()) bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) ResetUserState(user, bot) - return //err, 0 + return ctx, err } // amount not in allowed range from LNURL if EnterAmountStateData.AmountMin > 0 && EnterAmountStateData.AmountMax >= EnterAmountStateData.AmountMin && // this line checks whether min_max is set at all @@ -152,7 +152,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { log.Warnf("[enterAmountHandler] %s", err.Error()) bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), EnterAmountStateData.AmountMin/1000, EnterAmountStateData.AmountMax/1000)) ResetUserState(user, bot) - return + return ctx, errors.Create(errors.InvalidSyntaxError) } // find out which type the object in bunt has waiting for an amount @@ -164,7 +164,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { - return + return ctx, err } LnurlPayState := sn.(*LnurlPayState) LnurlPayState.Amount = amount * 1000 // mSat @@ -175,18 +175,17 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { StateDataJson, err := json.Marshal(EnterAmountStateData) if err != nil { log.Errorln(err) - return + return ctx, err } SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(StateDataJson)) bot.lnurlPayHandlerSend(ctx, m) - return case "LnurlWithdrawState": tx := &LnurlWithdrawState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { - return + return ctx, err } LnurlWithdrawState := sn.(*LnurlWithdrawState) LnurlWithdrawState.Amount = amount * 1000 // mSat @@ -197,35 +196,32 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) { StateDataJson, err := json.Marshal(EnterAmountStateData) if err != nil { log.Errorln(err) - return + return ctx, err } SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(StateDataJson)) bot.lnurlWithdrawHandlerWithdraw(ctx, m) - return case "CreateInvoiceState": m.Text = fmt.Sprintf("/invoice %d", amount) SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") - bot.invoiceHandler(ctx, m) - return + return bot.invoiceHandler(ctx, m) case "CreateDonationState": m.Text = fmt.Sprintf("/donate %d", amount) SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") - bot.donationHandler(ctx, m) - return + return bot.donationHandler(ctx, m) case "CreateSendState": splits := strings.SplitAfterN(EnterAmountStateData.OiringalCommand, " ", 2) if len(splits) > 1 { m.Text = fmt.Sprintf("/send %d %s", amount, splits[1]) SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") - bot.sendHandler(ctx, m) - return + return bot.sendHandler(ctx, m) } - return + return ctx, errors.Create(errors.InvalidSyntaxError) default: ResetUserState(user, bot) - return + return ctx, errors.Create(errors.InvalidSyntaxError) } // // reset database entry // ResetUserState(user, bot) // return + return ctx, nil } diff --git a/internal/telegram/balance.go b/internal/telegram/balance.go index 8c8b85cd..def024da 100644 --- a/internal/telegram/balance.go +++ b/internal/telegram/balance.go @@ -3,13 +3,14 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" ) -func (bot *TipBot) balanceHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) balanceHandler(ctx context.Context, m *tb.Message) (context.Context, error) { // check and print all commands if len(m.Text) > 0 { bot.anyTextHandler(ctx, m) @@ -23,12 +24,11 @@ func (bot *TipBot) balanceHandler(ctx context.Context, m *tb.Message) { // first check whether the user is initialized user := LoadUser(ctx) if user.Wallet == nil { - return + return ctx, errors.Create(errors.UserNoWalletError) } if !user.Initialized { - bot.startHandler(ctx, m) - return + return bot.startHandler(ctx, m) } usrStr := GetUserStr(m.Sender) @@ -36,10 +36,10 @@ func (bot *TipBot) balanceHandler(ctx context.Context, m *tb.Message) { if err != nil { log.Errorf("[/balance] Error fetching %s's balance: %s", usrStr, err) bot.trySendMessage(m.Sender, Translate(ctx, "balanceErrorMessage")) - return + return ctx, err } log.Infof("[/balance] %s's balance: %d sat\n", usrStr, balance) bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "balanceMessage"), balance)) - return + return ctx, nil } diff --git a/internal/telegram/donate.go b/internal/telegram/donate.go index a776b647..5d1afd31 100644 --- a/internal/telegram/donate.go +++ b/internal/telegram/donate.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "io" "io/ioutil" "net/http" @@ -32,19 +33,19 @@ func helpDonateUsage(ctx context.Context, errormsg string) string { } } -func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { +func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) (context.Context, error) { // check and print all commands bot.anyTextHandler(ctx, m) user := LoadUser(ctx) if user.Wallet == nil { - return + return ctx, errors.Create(errors.UserNoWalletError) } // if no amount is in the command, ask for it amount, err := decodeAmountFromCommand(m.Text) if (err != nil || amount < 1) && m.Chat.Type == tb.ChatPrivate { // // no amount was entered, set user state and ask for amount - bot.askForAmount(ctx, "", "CreateDonationState", 0, 0, m.Text) - return + _, err = bot.askForAmount(ctx, "", "CreateDonationState", 0, 0, m.Text) + return ctx, err } // command is valid @@ -54,13 +55,13 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { if err != nil { log.Errorln(err) bot.tryEditMessage(msg, Translate(ctx, "donationErrorMessage")) - return + return ctx, err } body, err := ioutil.ReadAll(resp.Body) if err != nil { log.Errorln(err) bot.tryEditMessage(msg, Translate(ctx, "donationErrorMessage")) - return + return ctx, err } // send donation invoice @@ -72,13 +73,14 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) { errmsg := fmt.Sprintf("[/donate] Donation failed for user %s: %s", userStr, err) log.Errorln(errmsg) bot.tryEditMessage(msg, Translate(ctx, "donationErrorMessage")) - return + return ctx, err } // hotfix because the edit doesn't work! // todo: fix edit // bot.tryEditMessage(msg, Translate(ctx, "donationSuccess")) bot.tryDeleteMessage(msg) bot.trySendMessage(m.Chat, Translate(ctx, "donationSuccess")) + return ctx, nil } func init() { diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 5d3aac15..0ac66648 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -18,7 +18,6 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" ) @@ -177,17 +176,17 @@ func (bot TipBot) makeFaucetKeyboard(ctx context.Context, id string) *tb.ReplyMa return inlineFaucetMenu } -func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { +func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) (context.Context, error) { bot.anyTextHandler(ctx, m) if m.Private() { bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetHelpFaucetInGroup"))) - return + return ctx, errors.Create(errors.NoPrivateChatError) } ctx = bot.mapFaucetLanguage(ctx, m.Text) inlineFaucet, err := bot.makeFaucet(ctx, m, false) if err != nil { log.Warnf("[faucet] %s", err.Error()) - return + return ctx, err } fromUserStr := GetUserStr(m.Sender) mFaucet := bot.trySendMessage(m.Chat, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) @@ -197,15 +196,14 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) { if mFaucet != nil && mFaucet.Chat != nil { log.Infof("[faucet] Link: https://t.me/c/%s/%d", strconv.FormatInt(mFaucet.Chat.ID, 10)[4:], mFaucet.ID) } - - runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) + return ctx, inlineFaucet.Set(inlineFaucet, bot.Bunt) } -func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { +func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) (context.Context, error) { inlineFaucet, err := bot.makeQueryFaucet(ctx, q, false) if err != nil { log.Errorf("[handleInlineFaucetQuery] %s", err.Error()) - return + return ctx, err } urls := []string{ queryImage, @@ -235,10 +233,12 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) { }) if err != nil { log.Errorln(err.Error()) + return ctx, err } + return ctx, nil } -func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { to := LoadUser(ctx) tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) @@ -246,7 +246,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptInlineFaucetHandler] c.Data: %s, Error: %s", c.Data, err.Error()) - return + return ctx, err } log.Tracef("[acceptInlineFaucetHandler] Callback c.Data: %s tx.ID: %s", c.Data, tx.ID) @@ -256,7 +256,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if !inlineFaucet.Active { log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) bot.finishFaucet(ctx, c, inlineFaucet) - return + return ctx, errors.Create(errors.NotActiveError) } if c.Message != nil && c.Message.Chat != nil { log.Infof("[faucet] Link: https://t.me/c/%s/%d", strconv.FormatInt(c.Message.Chat.ID, 10)[4:], c.Message.ID) @@ -266,14 +266,14 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if from.Telegram.ID == to.Telegram.ID { log.Debugf("[faucet] %s is the owner faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) - return + return ctx, errors.Create(errors.SelfPaymentError) } // check if to user has already taken from the faucet for _, a := range inlineFaucet.To { if a.Telegram.ID == to.Telegram.ID { // to user is already in To slice, has taken from facuet log.Debugf("[faucet] %s:%d already took from faucet %s", GetUserStr(to.Telegram), to.Telegram.ID, inlineFaucet.ID) - return + return ctx, errors.Create(errors.UnknownError) } } @@ -291,7 +291,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if err != nil { errmsg := fmt.Errorf("[faucet] Error: Could not create wallet for %s", toUserStr) log.Errorln(errmsg) - return + return ctx, err } } @@ -313,7 +313,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // c.Sender.ID = inlineFaucet.From.Telegram.ID // overwrite the sender of the callback to be the faucet owner // log.Debugf("[faucet] Canceling faucet %s...", inlineFaucet.ID) // bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check - return + return ctx, errors.New(errors.UnknownError, err) } log.Infof("[💸 faucet] Faucet %s from %s to %s:%d (%d sat).", inlineFaucet.ID, fromUserStr, toUserStr, to.Telegram.ID, inlineFaucet.PerUserAmount) @@ -347,17 +347,17 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // faucet is depleted bot.finishFaucet(ctx, c, inlineFaucet) } - + return ctx, nil } -func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignoreID bool) { +func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignoreID bool) (context.Context, error) { tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Debugf("[cancelInlineFaucetHandler] %s", err.Error()) - return + return ctx, err } inlineFaucet := fn.(*InlineFaucet) @@ -366,11 +366,14 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor // set the inlineFaucet inactive inlineFaucet.Active = false inlineFaucet.Canceled = true - runtime.IgnoreError(inlineFaucet.Set(inlineFaucet, bot.Bunt)) + err = inlineFaucet.Set(inlineFaucet, bot.Bunt) + if err != nil { + return ctx, err + } log.Debugf("[faucet] Faucet %s canceled.", inlineFaucet.ID) once.Remove(inlineFaucet.ID) } - return + return ctx, nil } func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFaucet *InlineFaucet) { @@ -384,7 +387,6 @@ func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFauce once.Remove(inlineFaucet.ID) } -func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) { - bot.cancelInlineFaucet(ctx, c, false) - return +func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + return bot.cancelInlineFaucet(ctx, c, false) } diff --git a/internal/telegram/files.go b/internal/telegram/files.go index 3f97ebdf..9fa6a067 100644 --- a/internal/telegram/files.go +++ b/internal/telegram/files.go @@ -2,13 +2,14 @@ package telegram import ( "context" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/runtime" tb "gopkg.in/lightningtipbot/telebot.v2" ) -func (bot *TipBot) fileHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) fileHandler(ctx context.Context, m *tb.Message) (context.Context, error) { if m.Chat.Type != tb.ChatPrivate { - return + return ctx, errors.Create(errors.NoPrivateChatError) } user := LoadUser(ctx) if c := stateCallbackMessage[user.StateKey]; c != nil { @@ -26,7 +27,7 @@ func (bot *TipBot) fileHandler(ctx context.Context, m *tb.Message) { ticker.ResetChan <- struct{}{} } - c(ctx, m) - return + return c(ctx, m) } + return ctx, errors.Create(errors.NoFileFoundError) } diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 48d7eac1..cc0be5d2 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -47,21 +47,21 @@ func (bot TipBot) registerHandlerWithInterceptor(h Handler) { switch h.Interceptor.Type { case MessageInterceptor: for _, endpoint := range h.Endpoints { - bot.handle(endpoint, intercept.HandlerWithMessage(h.Handler.(func(ctx context.Context, query *tb.Message)), + bot.handle(endpoint, intercept.HandlerWithMessage(h.Handler.(func(ctx context.Context, query *tb.Message) (context.Context, error)), intercept.WithBeforeMessage(h.Interceptor.Before...), intercept.WithAfterMessage(h.Interceptor.After...), intercept.WithDeferMessage(h.Interceptor.OnDefer...))) } case QueryInterceptor: for _, endpoint := range h.Endpoints { - bot.handle(endpoint, intercept.HandlerWithQuery(h.Handler.(func(ctx context.Context, query *tb.Query)), + bot.handle(endpoint, intercept.HandlerWithQuery(h.Handler.(func(ctx context.Context, query *tb.Query) (context.Context, error)), intercept.WithBeforeQuery(h.Interceptor.Before...), intercept.WithAfterQuery(h.Interceptor.After...), intercept.WithDeferQuery(h.Interceptor.OnDefer...))) } case CallbackInterceptor: for _, endpoint := range h.Endpoints { - bot.handle(endpoint, intercept.HandlerWithCallback(h.Handler.(func(ctx context.Context, callback *tb.Callback)), + bot.handle(endpoint, intercept.HandlerWithCallback(h.Handler.(func(ctx context.Context, callback *tb.Callback) (context.Context, error)), intercept.WithBeforeCallback(h.Interceptor.Before...), intercept.WithAfterCallback(append(h.Interceptor.After, bot.answerCallbackInterceptor)...), intercept.WithDeferCallback(h.Interceptor.OnDefer...))) diff --git a/internal/telegram/help.go b/internal/telegram/help.go index ee9a554c..e947ec89 100644 --- a/internal/telegram/help.go +++ b/internal/telegram/help.go @@ -26,7 +26,7 @@ func (bot TipBot) makeHelpMessage(ctx context.Context, m *tb.Message) string { return fmt.Sprintf(helpMessage, dynamicHelpMessage) } -func (bot TipBot) helpHandler(ctx context.Context, m *tb.Message) { +func (bot TipBot) helpHandler(ctx context.Context, m *tb.Message) (context.Context, error) { // check and print all commands bot.anyTextHandler(ctx, m) if !m.Private() { @@ -34,10 +34,10 @@ func (bot TipBot) helpHandler(ctx context.Context, m *tb.Message) { bot.tryDeleteMessage(m) } bot.trySendMessage(m.Sender, bot.makeHelpMessage(ctx, m), tb.NoPreview) - return + return ctx, nil } -func (bot TipBot) basicsHandler(ctx context.Context, m *tb.Message) { +func (bot TipBot) basicsHandler(ctx context.Context, m *tb.Message) (context.Context, error) { // check and print all commands bot.anyTextHandler(ctx, m) if !m.Private() { @@ -45,7 +45,7 @@ func (bot TipBot) basicsHandler(ctx context.Context, m *tb.Message) { bot.tryDeleteMessage(m) } bot.trySendMessage(m.Sender, Translate(ctx, "basicsMessage"), tb.NoPreview) - return + return ctx, nil } func (bot TipBot) makeAdvancedHelpMessage(ctx context.Context, m *tb.Message) string { @@ -74,7 +74,7 @@ func (bot TipBot) makeAdvancedHelpMessage(ctx context.Context, m *tb.Message) st ) } -func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) { +func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) (context.Context, error) { // check and print all commands bot.anyTextHandler(ctx, m) if !m.Private() { @@ -82,5 +82,5 @@ func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) { bot.tryDeleteMessage(m) } bot.trySendMessage(m.Sender, bot.makeAdvancedHelpMessage(ctx, m), tb.NoPreview) - return + return ctx, nil } diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index 32d45960..a99ecb56 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -18,7 +18,7 @@ import ( const queryImage = "https://avatars.githubusercontent.com/u/88730856?v=4" -func (bot TipBot) inlineQueryInstructions(ctx context.Context, q *tb.Query) { +func (bot TipBot) inlineQueryInstructions(ctx context.Context, q *tb.Query) (context.Context, error) { instructions := []struct { url string title string @@ -70,6 +70,7 @@ func (bot TipBot) inlineQueryInstructions(ctx context.Context, q *tb.Query) { if err != nil { log.Errorln(err) } + return ctx, err } func (bot TipBot) inlineQueryReplyWithError(q *tb.Query, message string, help string) { @@ -133,10 +134,9 @@ func (bot TipBot) commandTranslationMap(ctx context.Context, command string) con return ctx } -func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) { +func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) (context.Context, error) { if q.Text == "" { - bot.inlineQueryInstructions(ctx, q) - return + return bot.inlineQueryInstructions(ctx, q) } // create the inline send result @@ -144,7 +144,7 @@ func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) { q.Text = strings.TrimPrefix(q.Text, "/") } if strings.HasPrefix(q.Text, "send") || strings.HasPrefix(q.Text, "pay") { - bot.handleInlineSendQuery(ctx, q) + return bot.handleInlineSendQuery(ctx, q) } if strings.HasPrefix(q.Text, "faucet") || strings.HasPrefix(q.Text, "zapfhahn") || strings.HasPrefix(q.Text, "kraan") || strings.HasPrefix(q.Text, "grifo") { @@ -152,17 +152,18 @@ func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) { c := strings.Split(q.Text, " ")[0] ctx = bot.commandTranslationMap(ctx, c) } - bot.handleInlineFaucetQuery(ctx, q) + return bot.handleInlineFaucetQuery(ctx, q) } if strings.HasPrefix(q.Text, "tipjar") || strings.HasPrefix(q.Text, "spendendose") { if len(strings.Split(q.Text, " ")) > 1 { c := strings.Split(q.Text, " ")[0] ctx = bot.commandTranslationMap(ctx, c) } - bot.handleInlineTipjarQuery(ctx, q) + return bot.handleInlineTipjarQuery(ctx, q) } if strings.HasPrefix(q.Text, "receive") || strings.HasPrefix(q.Text, "get") || strings.HasPrefix(q.Text, "payme") || strings.HasPrefix(q.Text, "request") { - bot.handleInlineReceiveQuery(ctx, q) + return bot.handleInlineReceiveQuery(ctx, q) } + return ctx, nil } diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index a62bbe0a..c6670128 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "time" @@ -54,16 +55,16 @@ func (bot TipBot) makeReceiveKeyboard(ctx context.Context, id string) *tb.ReplyM return inlineReceiveMenu } -func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { +func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) (context.Context, error) { to := LoadUser(ctx) amount, err := decodeAmountFromCommand(q.Text) if err != nil { bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineQueryReceiveTitle"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) - return + return ctx, err } if amount < 1 { bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) - return + return ctx, errors.Create(errors.InvalidAmountError) } toUserStr := GetUserStr(&q.From) @@ -83,7 +84,7 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { fmt.Sprintf(TranslateUser(ctx, "sendUserHasNoWalletMessage"), from_username), fmt.Sprintf(TranslateUser(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) - return + return ctx, err } memo_argn = 3 // assume that memo starts after the from_username from_SpecificUser = true @@ -141,10 +142,12 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) { }) if err != nil { log.Errorln(err) + return ctx, err } + return ctx, nil } -func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls mutex.LockWithContext(ctx, tx.ID) @@ -152,12 +155,12 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err.Error()) - return + return ctx, err } inlineReceive := rn.(*InlineReceive) if !inlineReceive.Active { log.Errorf("[acceptInlineReceiveHandler] inline receive not active anymore") - return + return ctx, errors.Create(errors.NotActiveError) } // user `from` is the one who is SENDING @@ -167,7 +170,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac if inlineReceive.From_SpecificUser { if inlineReceive.From.Telegram.ID != from.Telegram.ID { // log.Infof("User %d is not User %d", inlineReceive.From.Telegram.ID, from.Telegram.ID) - return + return ctx, errors.Create(errors.UnknownError) } } else { // otherwise, we just set it to the user who has clicked @@ -180,7 +183,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac to := inlineReceive.To if from.Telegram.ID == to.Telegram.ID { bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) - return + return ctx, errors.Create(errors.SelfPaymentError) } balance, err := bot.GetUserBalance(from) @@ -194,15 +197,15 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac bot.tryEditMessage(inlineReceive.Message, inlineReceive.MessageText, &tb.ReplyMarkup{}) // runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) bot.inlineReceiveInvoice(ctx, c, inlineReceive) - return + return ctx, errors.Create(errors.BalanceToLowError) } else { // else, do an internal transaction - bot.sendInlineReceiveHandler(ctx, c) - return + return bot.sendInlineReceiveHandler(ctx, c) + } } -func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -210,13 +213,13 @@ func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) // immediatelly set intransaction to block duplicate calls if err != nil { // log.Errorf("[getInlineReceive] %s", err.Error()) - return + return ctx, err } inlineReceive := rn.(*InlineReceive) if !inlineReceive.Active { log.Errorf("[acceptInlineReceiveHandler] inline receive not active anymore") - return + return ctx, errors.Create(errors.NotActiveError) } // defer inlineReceive.Release(inlineReceive, bot.Bunt) @@ -231,13 +234,13 @@ func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) if err != nil { errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) log.Errorln(errmsg) - return + return ctx, err } // check if fromUser has balance if balance < inlineReceive.Amount { log.Errorf("[acceptInlineReceiveHandler] balance of user %s too low", fromUserStr) bot.trySendMessage(from.Telegram, Translate(ctx, "inlineSendBalanceLowMessage")) - return + return ctx, errors.Create(errors.BalanceToLowError) } // set inactive to avoid double-sends @@ -252,12 +255,13 @@ func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) errMsg := fmt.Sprintf("[acceptInlineReceiveHandler] Transaction failed: %s", err.Error()) log.Errorln(errMsg) bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveFailedMessage"), &tb.ReplyMarkup{}) - return + return ctx, errors.Create(errors.UnknownError) } log.Infof("[💸 inlineReceive] Send from %s to %s (%d sat).", fromUserStr, toUserStr, inlineReceive.Amount) inlineReceive.Set(inlineReceive, bot.Bunt) - bot.finishInlineReceiveHandler(ctx, c) + return bot.finishInlineReceiveHandler(ctx, c) + } func (bot *TipBot) inlineReceiveInvoice(ctx context.Context, c *tb.Callback, inlineReceive *InlineReceive) { @@ -301,7 +305,7 @@ func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { bot.finishInlineReceiveHandler(nil, &tb.Callback{Data: string(invoiceEvent.CallbackData)}) } -func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls mutex.LockWithContext(ctx, tx.ID) @@ -309,7 +313,7 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err.Error()) - return + return ctx, err } inlineReceive := rn.(*InlineReceive) @@ -335,11 +339,13 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac if err != nil { errmsg := fmt.Errorf("[acceptInlineReceiveHandler] Error: Receive message to %s: %s", toUserStr, err) log.Warnln(errmsg) + return ctx, err } + return ctx, nil // inlineReceive.Release(inlineReceive, bot.Bunt) } -func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -347,14 +353,14 @@ func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callbac rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineReceiveHandler] %s", err.Error()) - return + return ctx, err } inlineReceive := rn.(*InlineReceive) - if c.Sender.ID == inlineReceive.To.Telegram.ID { - bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) - // set the inlineReceive inactive - inlineReceive.Active = false - runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) + if c.Sender.ID != inlineReceive.To.Telegram.ID { + return ctx, errors.Create(errors.UnknownError) } - return + bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) + // set the inlineReceive inactive + inlineReceive.Active = false + return ctx, inlineReceive.Set(inlineReceive, bot.Bunt) } diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index b17827ca..a66b1e33 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "time" @@ -15,7 +16,6 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" ) @@ -52,17 +52,17 @@ func (bot TipBot) makeSendKeyboard(ctx context.Context, id string) *tb.ReplyMark return inlineSendMenu } -func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { +func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) (context.Context, error) { // inlineSend := NewInlineSend() // var err error amount, err := decodeAmountFromCommand(q.Text) if err != nil { bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQuerySendTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) - return + return ctx, err } if amount < 1 { bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) - return + return ctx, errors.Create(errors.InvalidAmountError) } fromUser := LoadUser(ctx) fromUserStr := GetUserStr(&q.From) @@ -70,13 +70,13 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { if err != nil { errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) log.Errorln(errmsg) - return + return ctx, err } // check if fromUser has balance if balance < amount { log.Errorf("Balance of user %s too low", fromUserStr) bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) - return + return ctx, errors.Create(errors.InvalidAmountError) } // check whether the 3rd argument is a username @@ -95,7 +95,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { fmt.Sprintf(TranslateUser(ctx, "sendUserHasNoWalletMessage"), to_username), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) - return + return ctx, err } memo_argn = 3 // assume that memo starts after the to_username to_SpecificUser = true @@ -156,10 +156,12 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) { }) if err != nil { log.Errorln(err) + return ctx, err } + return ctx, nil } -func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { to := LoadUser(ctx) tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) @@ -168,14 +170,14 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) // immediatelly set intransaction to block duplicate calls if err != nil { // log.Errorf("[acceptInlineSendHandler] %s", err.Error()) - return + return ctx, err } inlineSend := sn.(*InlineSend) fromUser := inlineSend.From if !inlineSend.Active { log.Errorf("[acceptInlineSendHandler] inline send not active anymore") - return + return ctx, errors.Create(errors.NotActiveError) } defer inlineSend.Set(inlineSend, bot.Bunt) @@ -186,7 +188,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) if inlineSend.To_SpecificUser { if inlineSend.To.Telegram.ID != to.Telegram.ID { // log.Infof("User %d is not User %d", inlineSend.To.Telegram.ID, to.Telegram.ID) - return + return ctx, errors.Create(errors.UnknownError) } } else { // otherwise, we just set it to the user who has clicked @@ -195,7 +197,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) if fromUser.Telegram.ID == to.Telegram.ID { bot.trySendMessage(fromUser.Telegram, Translate(ctx, "sendYourselfMessage")) - return + return ctx, errors.Create(errors.UnknownError) } toUserStrMd := GetUserStrMd(to.Telegram) @@ -211,7 +213,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) if err != nil { errmsg := fmt.Errorf("[sendInline] Error: Could not create wallet for %s", toUserStr) log.Errorln(errmsg) - return + return ctx, err } } // set inactive to avoid double-sends @@ -226,7 +228,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) errMsg := fmt.Sprintf("[sendInline] Transaction failed: %s", err.Error()) log.Errorln(errMsg) bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "inlineSendFailedMessage"), &tb.ReplyMarkup{}) - return + return ctx, errors.Create(errors.UnknownError) } log.Infof("[💸 sendInline] Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount) @@ -247,9 +249,10 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) errmsg := fmt.Errorf("[sendInline] Error: Send message to %s: %s", toUserStr, err) log.Warnln(errmsg) } + return ctx, nil } -func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -257,14 +260,14 @@ func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineSendHandler] %s", err.Error()) - return + return ctx, err } inlineSend := sn.(*InlineSend) - if c.Sender.ID == inlineSend.From.Telegram.ID { - bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) - // set the inlineSend inactive - inlineSend.Active = false - runtime.IgnoreError(inlineSend.Set(inlineSend, bot.Bunt)) + if c.Sender.ID != inlineSend.From.Telegram.ID { + return ctx, errors.Create(errors.UnknownError) } - return + bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) + // set the inlineSend inactive + inlineSend.Active = false + return ctx, inlineSend.Set(inlineSend, bot.Bunt) } diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go index d9698be2..64bfcc48 100644 --- a/internal/telegram/intercept/callback.go +++ b/internal/telegram/intercept/callback.go @@ -6,7 +6,7 @@ import ( tb "gopkg.in/lightningtipbot/telebot.v2" ) -type CallbackFuncHandler func(ctx context.Context, message *tb.Callback) +type CallbackFuncHandler func(ctx context.Context, message *tb.Callback) (context.Context, error) type Func func(ctx context.Context, message interface{}) (context.Context, error) type handlerCallbackInterceptor struct { @@ -63,7 +63,11 @@ func HandlerWithCallback(handler CallbackFuncHandler, option ...CallbackIntercep return } defer interceptCallback(ctx, c, hm.onDefer) - hm.handler(ctx, c) + ctx, err = hm.handler(ctx, c) + if err != nil { + log.Traceln(err) + return + } _, err = interceptCallback(ctx, c, hm.after) if err != nil { log.Traceln(err) diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go index 293c09fd..c6c3f3cc 100644 --- a/internal/telegram/intercept/message.go +++ b/internal/telegram/intercept/message.go @@ -7,10 +7,7 @@ import ( tb "gopkg.in/lightningtipbot/telebot.v2" ) -type MessageInterface interface { - a() func(ctx context.Context, message *tb.Message) -} -type MessageFuncHandler func(ctx context.Context, message *tb.Message) +type MessageFuncHandler func(ctx context.Context, message *tb.Message) (context.Context, error) type handlerMessageInterceptor struct { handler MessageFuncHandler @@ -66,7 +63,11 @@ func HandlerWithMessage(handler MessageFuncHandler, option ...MessageInterceptOp return } defer interceptMessage(ctx, message, hm.onDefer) - hm.handler(ctx, message) + ctx, err = hm.handler(ctx, message) + if err != nil { + log.Traceln(err) + return + } _, err = interceptMessage(ctx, message, hm.after) if err != nil { log.Traceln(err) diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go index 4dfc4264..6aa8a6d2 100644 --- a/internal/telegram/intercept/query.go +++ b/internal/telegram/intercept/query.go @@ -7,7 +7,7 @@ import ( tb "gopkg.in/lightningtipbot/telebot.v2" ) -type QueryFuncHandler func(ctx context.Context, message *tb.Query) +type QueryFuncHandler func(ctx context.Context, message *tb.Query) (context.Context, error) type handlerQueryInterceptor struct { handler QueryFuncHandler @@ -63,7 +63,11 @@ func HandlerWithQuery(handler QueryFuncHandler, option ...QueryInterceptOption) return } defer interceptQuery(ctx, query, hm.onDefer) - hm.handler(ctx, query) + ctx, err = hm.handler(ctx, query) + if err != nil { + log.Traceln(err) + return + } _, err = interceptQuery(ctx, query, hm.after) if err != nil { log.Traceln(err) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 7ac5f096..af6c2234 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "reflect" "strconv" @@ -26,8 +27,6 @@ const ( QueryInterceptor ) -var invalidTypeError = fmt.Errorf("invalid type") - type Interceptor struct { Type InterceptorType Before []intercept.Func @@ -43,7 +42,7 @@ func (bot TipBot) singletonCallbackInterceptor(ctx context.Context, i interface{ c := i.(*tb.Callback) return ctx, once.Once(c.Data, strconv.FormatInt(c.Sender.ID, 10)) } - return ctx, invalidTypeError + return ctx, errors.Create(errors.InvalidTypeError) } // unlockInterceptor invoked as onDefer interceptor @@ -52,7 +51,7 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context if user != nil { mutex.Unlock(strconv.FormatInt(user.ID, 10)) } - return ctx, invalidTypeError + return ctx, errors.Create(errors.InvalidTypeError) } func (bot TipBot) idInterceptor(ctx context.Context, i interface{}) (context.Context, error) { return context.WithValue(ctx, "uid", RandStringRunes(64)), nil @@ -72,7 +71,7 @@ func (bot TipBot) answerCallbackInterceptor(ctx context.Context, i interface{}) err = bot.Telegram.Respond(c, res...) return ctx, err } - return ctx, invalidTypeError + return ctx, errors.Create(errors.InvalidTypeError) } // lockInterceptor invoked as first before interceptor @@ -82,7 +81,7 @@ func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.C mutex.Lock(strconv.FormatInt(user.ID, 10)) return ctx, nil } - return nil, invalidTypeError + return nil, errors.Create(errors.InvalidTypeError) } // requireUserInterceptor will return an error if user is not found @@ -96,20 +95,20 @@ func (bot TipBot) requireUserInterceptor(ctx context.Context, i interface{}) (co // do not respond to banned users if bot.UserIsBanned(user) { ctx = context.WithValue(ctx, "banned", true) - return context.WithValue(ctx, "user", user), invalidTypeError + return context.WithValue(ctx, "user", user), errors.Create(errors.InvalidTypeError) } if user != nil { return context.WithValue(ctx, "user", user), err } } - return nil, invalidTypeError + return nil, errors.Create(errors.InvalidTypeError) } func (bot TipBot) loadUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { ctx, _ = bot.requireUserInterceptor(ctx, i) // if user is banned, also loadUserInterceptor will return an error if ctx.Value("banned") != nil && ctx.Value("banned").(bool) { - return nil, invalidTypeError + return nil, errors.Create(errors.InvalidTypeError) } return ctx, nil } @@ -143,7 +142,7 @@ func (bot TipBot) loadReplyToInterceptor(ctx context.Context, i interface{}) (co } return ctx, nil } - return ctx, invalidTypeError + return ctx, errors.Create(errors.InvalidTypeError) } func (bot TipBot) localizerInterceptor(ctx context.Context, i interface{}) (context.Context, error) { @@ -190,7 +189,7 @@ func (bot TipBot) requirePrivateChatInterceptor(ctx context.Context, i interface } return ctx, nil } - return nil, invalidTypeError + return nil, errors.Create(errors.InvalidTypeError) } const photoTag = "" @@ -214,7 +213,7 @@ func (bot TipBot) logMessageInterceptor(ctx context.Context, i interface{}) (con log.Infof("[Callback %s:%d] Data: %s", GetUserStr(m.Sender), m.Sender.ID, m.Data) return ctx, nil } - return nil, invalidTypeError + return nil, errors.Create(errors.InvalidTypeError) } // LoadUser from context diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index cfaa5d58..e191d0ea 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "time" @@ -67,25 +68,25 @@ func helpInvoiceUsage(ctx context.Context, errormsg string) string { } } -func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) (context.Context, error) { // check and print all commands bot.anyTextHandler(ctx, m) user := LoadUser(ctx) if user.Wallet == nil { - return + return ctx, errors.Create(errors.UserNoWalletError) } userStr := GetUserStr(user.Telegram) if m.Chat.Type != tb.ChatPrivate { // delete message bot.tryDeleteMessage(m) - return + return ctx, errors.Create(errors.NoPrivateChatError) } // if no amount is in the command, ask for it amount, err := decodeAmountFromCommand(m.Text) if (err != nil || amount < 1) && m.Chat.Type == tb.ChatPrivate { // // no amount was entered, set user state and ask fo""r amount - bot.askForAmount(ctx, "", "CreateInvoiceState", 0, 0, m.Text) - return + _, err = bot.askForAmount(ctx, "", "CreateInvoiceState", 0, 0, m.Text) + return ctx, err } // check for memo in command @@ -107,7 +108,7 @@ func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err.Error()) bot.tryEditMessage(creatingMsg, Translate(ctx, "errorTryLaterMessage")) log.Errorln(errmsg) - return + return ctx, err } // create qr code @@ -116,7 +117,7 @@ func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err.Error()) bot.tryEditMessage(creatingMsg, Translate(ctx, "errorTryLaterMessage")) log.Errorln(errmsg) - return + return ctx, err } // deleting messages will delete the main menu. @@ -125,7 +126,7 @@ func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) { // send the invoice data to user bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) log.Printf("[/invoice] Incvoice created. User: %s, amount: %d sat.", userStr, amount) - return + return ctx, nil } func (bot *TipBot) createInvoiceWithEvent(ctx context.Context, user *lnbits.User, amount int64, memo string, callback int, callbackData string) (InvoiceEvent, error) { diff --git a/internal/telegram/link.go b/internal/telegram/link.go index 0446b454..4647a72a 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -13,10 +13,10 @@ import ( tb "gopkg.in/lightningtipbot/telebot.v2" ) -func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) (context.Context, error) { if internal.Configuration.Lnbits.LnbitsPublicUrl == "" { bot.trySendMessage(m.Sender, Translate(ctx, "couldNotLinkMessage")) - return + return ctx, fmt.Errorf("invalid configuration") } // check and print all commands bot.anyTextHandler(ctx, m) @@ -32,7 +32,7 @@ func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { // do not respond to banned users if bot.UserIsBanned(fromUser) { log.Warnln("[lndhubHandler] user is banned. not responding.") - return + return ctx, fmt.Errorf("user is banned") } lndhubUrl := fmt.Sprintf("lndhub://admin:%s@%slndhub/ext/", fromUser.Wallet.Adminkey, internal.Configuration.Lnbits.LnbitsPublicUrl) @@ -42,7 +42,7 @@ func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { if err != nil { errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err.Error()) log.Errorln(errmsg) - return + return ctx, err } // send the link to the user @@ -55,5 +55,5 @@ func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) { }() // auto delete the message // NewMessage(linkmsg, WithDuration(time.Second*time.Duration(internal.Configuration.Telegram.MessageDisposeDuration), bot)) - + return ctx, nil } diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 1ae78bf5..e728ff19 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "io/ioutil" "net/url" @@ -186,14 +187,14 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa } // confirmPayHandler when user clicked pay on payment confirmation -func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[confirmWithdrawHandler] Error: %s", err.Error()) - return + return ctx, err } var lnurlWithdrawState *LnurlWithdrawState @@ -202,24 +203,24 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { lnurlWithdrawState = fn.(*LnurlWithdrawState) default: log.Errorf("[confirmWithdrawHandler] invalid type") - return + return ctx, errors.Create(errors.InvalidTypeError) } // onnly the correct user can press if lnurlWithdrawState.From.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } if !lnurlWithdrawState.Active { log.Errorf("[confirmPayHandler] send not active anymore") bot.tryEditMessage(c.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) bot.tryDeleteMessage(c.Message) - return + return ctx, errors.Create(errors.NotActiveError) } defer lnurlWithdrawState.Set(lnurlWithdrawState, bot.Bunt) user := LoadUser(ctx) if user.Wallet == nil { bot.tryDeleteMessage(c.Message) - return + return ctx, errors.Create(errors.UserNoWalletError) } // reset state immediately @@ -233,7 +234,7 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) - return + return ctx, err } // generate an invoice and add the pr to the request @@ -249,7 +250,7 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { errmsg := fmt.Sprintf("[lnurlWithdrawHandlerWithdraw] Could not create an invoice: %s", err.Error()) log.Errorln(errmsg) bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) - return + return ctx, err } lnurlWithdrawState.Invoice = invoice @@ -265,21 +266,21 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) - return + return ctx, err } res, err := client.Get(callbackUrl.String()) if err != nil || res.StatusCode >= 300 { log.Errorf("[lnurlWithdrawHandlerWithdraw] Failed.") // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) - return + return ctx, errors.New(errors.UnknownError, err) } body, err := ioutil.ReadAll(res.Body) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) - return + return ctx, err } // parse the response @@ -293,17 +294,17 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) { log.Errorf("[lnurlWithdrawHandlerWithdraw] LNURLWithdraw failed.") // update button text bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawFailed")) - return + return ctx, errors.New(errors.UnknownError, fmt.Errorf("LNURLWithdraw failed")) } // add response to persistent struct lnurlWithdrawState.LNURResponse = response2 - runtime.IgnoreError(lnurlWithdrawState.Set(lnurlWithdrawState, bot.Bunt)) + return ctx, lnurlWithdrawState.Set(lnurlWithdrawState, bot.Bunt) } // cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { // reset state immediately user := LoadUser(ctx) ResetUserState(user, bot) @@ -313,7 +314,7 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelWithdrawHandler] Error: %s", err.Error()) - return + return ctx, err } var lnurlWithdrawState *LnurlWithdrawState switch fn.(type) { @@ -324,8 +325,8 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) { } // onnly the correct user can press if lnurlWithdrawState.From.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } bot.tryEditMessage(c.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawCancelled"), &tb.ReplyMarkup{}) - lnurlWithdrawState.Inactivate(lnurlWithdrawState, bot.Bunt) + return ctx, lnurlWithdrawState.Inactivate(lnurlWithdrawState, bot.Bunt) } diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 88663b08..9111c477 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -3,8 +3,8 @@ package telegram import ( "bytes" "context" - "errors" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "io/ioutil" "net/http" "net/url" @@ -36,24 +36,23 @@ func (bot TipBot) cancelLnUrlHandler(c *tb.Callback) { } // lnurlHandler is invoked on /lnurl command -func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Context, error) { // commands: // /lnurl // /lnurl // or /lnurl if m.Chat.Type != tb.ChatPrivate { - return + return ctx, errors.Create(errors.NoPrivateChatError) } log.Infof("[lnurlHandler] %s", m.Text) user := LoadUser(ctx) if user.Wallet == nil { - return + return ctx, errors.Create(errors.UserNoWalletError) } // if only /lnurl is entered, show the lnurl of the user if m.Text == "/lnurl" { - bot.lnurlReceiveHandler(ctx, m) - return + return bot.lnurlReceiveHandler(ctx, m) } statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlResolvingUrlMessage")) @@ -69,7 +68,7 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { } else { bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), "Could not parse command.")) log.Warnln("[/lnurl] Could not parse command.") - return + return ctx, errors.Create(errors.InvalidSyntaxError) } // get rid of the URI prefix @@ -79,7 +78,7 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { if err != nil { bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), err.Error())) log.Warnf("[HandleLNURL] Error: %s", err.Error()) - return + return ctx, err } switch params.(type) { case lnurl.LNURLPayParams: @@ -87,7 +86,7 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { log.Infof("[LNURL-p] %s", payParams.LNURLPayParams.Callback) bot.tryDeleteMessage(statusMsg) bot.lnurlPayHandler(ctx, m, *payParams) - return + case lnurl.LNURLWithdrawResponse: withdrawParams := LnurlWithdrawState{LNURLWithdrawResponse: params.(lnurl.LNURLWithdrawResponse)} log.Infof("[LNURL-w] %s", withdrawParams.LNURLWithdrawResponse.Callback) @@ -95,13 +94,14 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) { bot.lnurlWithdrawHandler(ctx, m, withdrawParams) default: if err == nil { - err = errors.New("Invalid LNURL type.") + err = fmt.Errorf("invalid LNURL type") } log.Warnln(err) bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), err.Error())) // bot.trySendMessage(m.Sender, err.Error()) - return + return ctx, err } + return ctx, nil } func (bot *TipBot) UserGetLightningAddress(user *lnbits.User) (string, error) { @@ -130,25 +130,27 @@ func UserGetLNURL(user *lnbits.User) (string, error) { } // lnurlReceiveHandler outputs the LNURL of the user -func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) { +func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) (context.Context, error) { fromUser := LoadUser(ctx) lnurlEncode, err := UserGetLNURL(fromUser) if err != nil { errmsg := fmt.Sprintf("[userLnurlHandler] Failed to get LNURL: %s", err.Error()) log.Errorln(errmsg) bot.trySendMessage(m.Sender, Translate(ctx, "lnurlNoUsernameMessage")) + return ctx, err } // create qr code qr, err := qrcode.Encode(lnurlEncode, qrcode.Medium, 256) if err != nil { errmsg := fmt.Sprintf("[userLnurlHandler] Failed to create QR code for LNURL: %s", err.Error()) log.Errorln(errmsg) - return + return ctx, err } bot.trySendMessage(m.Sender, Translate(ctx, "lnurlReceiveInfoText")) // send the lnurl data to user bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lnurlEncode)}) + return ctx, nil } // fiatjaf/go-lnurl 1.8.4 with proxy @@ -181,7 +183,7 @@ func (bot TipBot) HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error lnurl_str, ok := lnurl.FindLNURLInText(rawlnurl) if !ok { return "", nil, - errors.New("invalid bech32-encoded lnurl: " + rawlnurl) + fmt.Errorf("invalid bech32-encoded lnurl: " + rawlnurl) } rawurl, err = lnurl.LNURLDecode(lnurl_str) if err != nil { @@ -221,7 +223,7 @@ func (bot TipBot) HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error return rawurl, nil, err } if resp.StatusCode >= 300 { - return rawurl, nil, errors.New("HTTP error: " + resp.Status) + return rawurl, nil, fmt.Errorf("HTTP error: " + resp.Status) } b, err := ioutil.ReadAll(resp.Body) @@ -249,6 +251,6 @@ func (bot TipBot) HandleLNURL(rawlnurl string) (string, lnurl.LNURLParams, error // value, err := lnurl.HandleChannel(b) // return rawurl, value, err default: - return rawurl, nil, errors.New("Unkown LNURL response.") + return rawurl, nil, fmt.Errorf("Unkown LNURL response.") } } diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 204a5dd1..9a93c8d1 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" @@ -46,17 +47,17 @@ type PayData struct { } // payHandler invoked on "/pay lnbc..." command -func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Context, error) { // check and print all commands bot.anyTextHandler(ctx, m) user := LoadUser(ctx) if user.Wallet == nil { - return + return ctx, errors.Create(errors.UserNoWalletError) } if len(strings.Split(m.Text, " ")) < 2 { NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, "")) - return + return ctx, errors.Create(errors.InvalidSyntaxError) } userStr := GetUserStr(m.Sender) paymentRequest, err := getArgumentFromCommand(m.Text, 1) @@ -65,7 +66,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) errmsg := fmt.Sprintf("[/pay] Error: Could not getArgumentFromCommand: %s", err.Error()) log.Errorln(errmsg) - return + return ctx, errors.New(errors.InvalidSyntaxError, err) } paymentRequest = strings.ToLower(paymentRequest) // get rid of the URI prefix @@ -77,7 +78,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) errmsg := fmt.Sprintf("[/pay] Error: Could not decode invoice: %s", err.Error()) log.Errorln(errmsg) - return + return ctx, errors.New(errors.InvalidSyntaxError, err) } amount := int64(bolt11.MSatoshi / 1000) @@ -85,7 +86,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(m.Sender, Translate(ctx, "invoiceNoAmountMessage")) errmsg := fmt.Sprint("[/pay] Error: invoice without amount") log.Warnln(errmsg) - return + return ctx, errors.Create(errors.InvalidAmountError) } // check user balance first @@ -95,13 +96,13 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { errmsg := fmt.Sprintf("[/pay] Error: Could not get user balance: %s", err.Error()) log.Errorln(errmsg) bot.trySendMessage(m.Sender, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, errors.New(errors.GetBalanceError, err) } if amount > balance { NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "insufficientFundsMessage"), balance, amount)) - return + return ctx, errors.Create(errors.InvalidSyntaxError) } // send warning that the invoice might fail due to missing fee reserve if float64(amount) > float64(balance)*0.99 { @@ -144,10 +145,11 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) { runtime.IgnoreError(payData.Set(payData, bot.Bunt)) SetUserState(user, bot, lnbits.UserStateConfirmPayment, paymentRequest) + return ctx, nil } // confirmPayHandler when user clicked pay on payment confirmation -func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { tx := &PayData{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -155,19 +157,19 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { // immediatelly set intransaction to block duplicate calls if err != nil { log.Errorf("[confirmPayHandler] %s", err.Error()) - return + return ctx, err } payData := sn.(*PayData) // onnly the correct user can press if payData.From.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } if !payData.Active { log.Errorf("[confirmPayHandler] send not active anymore") bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) bot.tryDeleteMessage(c.Message) - return + return ctx, errors.Create(errors.NotActiveError) } defer payData.Set(payData, bot.Bunt) @@ -177,7 +179,7 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { user := LoadUser(ctx) if user.Wallet == nil { bot.tryDeleteMessage(c.Message) - return + return ctx, errors.Create(errors.UserNoWalletError) } invoiceString := payData.Invoice @@ -211,7 +213,7 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { // } // bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), str.MarkdownEscape(err.Error())), &tb.ReplyMarkup{}) log.Errorln(errmsg) - return + return ctx, err } payData.Hash = invoice.PaymentHash @@ -234,11 +236,11 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) { bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePublicPaidMessage"), userStr), &tb.ReplyMarkup{}) } log.Infof("[⚡️ pay] User %s paid invoice %s (%d sat)", userStr, payData.ID, payData.Amount) - return + return ctx, nil } // cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { // reset state immediately user := LoadUser(ctx) ResetUserState(user, bot) @@ -249,16 +251,17 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) { sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelPaymentHandler] %s", err.Error()) - return + return ctx, err } payData := sn.(*PayData) // onnly the correct user can press if payData.From.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } // delete and send instead of edit for the keyboard to pop up after sending bot.tryDeleteMessage(c.Message) bot.trySendMessage(c.Message.Chat, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage")) // bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) - payData.Inactivate(payData, bot.Bunt) + return ctx, payData.Inactivate(payData, bot.Bunt) + } diff --git a/internal/telegram/photo.go b/internal/telegram/photo.go index 6d341381..136cab60 100644 --- a/internal/telegram/photo.go +++ b/internal/telegram/photo.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "image" "image/jpeg" "strings" @@ -34,48 +35,47 @@ 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) (context.Context, error) { if m.Chat.Type != tb.ChatPrivate { - return + return ctx, errors.Create(errors.NoPrivateChatError) } if m.Photo == nil { - return + return ctx, errors.Create(errors.NoPhotoError) } user := LoadUser(ctx) if c := stateCallbackMessage[user.StateKey]; c != nil { - c(ctx, m) + ctx, err := c(ctx, m) ResetUserState(user, bot) - return + return ctx, err } // get file reader closer from Telegram api reader, err := bot.Telegram.GetFile(m.Photo.MediaFile()) if err != nil { log.Errorf("[photoHandler] getfile error: %v\n", err.Error()) - return + return ctx, err } // decode to jpeg image img, err := jpeg.Decode(reader) if err != nil { log.Errorf("[photoHandler] image.Decode error: %v\n", err.Error()) - return + return ctx, err } data, err := TryRecognizeQrCode(img) if err != nil { log.Errorf("[photoHandler] tryRecognizeQrCodes error: %v\n", err.Error()) bot.trySendMessage(m.Sender, Translate(ctx, "photoQrNotRecognizedMessage")) - return + return ctx, err } bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "photoQrRecognizedMessage"), data.String())) // invoke payment handler if lightning.IsInvoice(data.String()) { m.Text = fmt.Sprintf("/pay %s", data.String()) - bot.payHandler(ctx, m) - return + return bot.payHandler(ctx, m) } else if lightning.IsLnurl(data.String()) { m.Text = fmt.Sprintf("/lnurl %s", data.String()) - bot.lnurlHandler(ctx, m) - return + return bot.lnurlHandler(ctx, m) } + return ctx, nil } diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 19b719f9..41af8384 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" @@ -53,11 +54,11 @@ type SendData struct { } // sendHandler invoked on "/send 123 @user" command -func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Context, error) { bot.anyTextHandler(ctx, m) user := LoadUser(ctx) if user.Wallet == nil { - return + return ctx, errors.Create(errors.UserNoWalletError) } // reset state immediately @@ -67,8 +68,8 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // If the send is a reply, then trigger /tip handler if m.IsReply() && m.Chat.Type != tb.ChatPrivate { - bot.tipHandler(ctx, m) - return + return bot.tipHandler(ctx, m) + } // if ok, errstr := bot.SendCheckSyntax(ctx, m); !ok { @@ -95,24 +96,24 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { err = bot.sendToLightningAddress(ctx, m, arg, amount) if err != nil { log.Errorln(err.Error()) - return + return ctx, err } - return + return ctx, err } } // is a user given? arg, err = getArgumentFromCommand(m.Text, 1) if err != nil && m.Chat.Type == tb.ChatPrivate { - bot.askForUser(ctx, "", "CreateSendState", m.Text) - return + _, err = bot.askForUser(ctx, "", "CreateSendState", m.Text) + return ctx, err } // is an amount given? amount, err = decodeAmountFromCommand(m.Text) if (err != nil || amount < 1) && m.Chat.Type == tb.ChatPrivate { - bot.askForAmount(ctx, "", "CreateSendState", 0, 0, m.Text) - return + _, err = bot.askForAmount(ctx, "", "CreateSendState", 0, 0, m.Text) + return ctx, err } // ASSUME INTERNAL SEND TO TELEGRAM USER @@ -122,7 +123,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { // immediately delete if the amount is bullshit NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, helpSendUsage(ctx, Translate(ctx, "sendValidAmountMessage"))) - return + return ctx, err } // SEND COMMAND IS VALID @@ -140,7 +141,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { toUserStrWithoutAt, err = getArgumentFromCommand(m.Text, 2) if err != nil { log.Errorln(err.Error()) - return + return ctx, err } toUserStrWithoutAt = strings.TrimPrefix(toUserStrWithoutAt, "@") toUserStrMention = "@" + toUserStrWithoutAt @@ -148,7 +149,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { err = bot.parseCmdDonHandler(ctx, m) if err == nil { - return + return ctx, errors.Create(errors.InvalidSyntaxError) } toUserDb, err := GetUserByTelegramUsername(toUserStrWithoutAt, *bot) @@ -159,12 +160,12 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { toUserStrMention = toUserStrMention[:100] } bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), str.MarkdownEscape(toUserStrMention))) - return + return ctx, err } if user.ID == toUserDb.ID { bot.trySendMessage(m.Sender, Translate(ctx, "sendYourselfMessage")) - return + return ctx, errors.Create(errors.SelfPaymentError) } // entire text of the inline object @@ -192,7 +193,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { NewMessage(m, WithDuration(0, bot)) log.Printf("[/send] Error: %s\n", err.Error()) bot.trySendMessage(m.Sender, fmt.Sprint(Translate(ctx, "errorTryLaterMessage"))) - return + return ctx, err } // save the send data to the Database // log.Debug(sendData) @@ -212,16 +213,17 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) { } else { bot.tryReplyMessage(m, confirmText, sendConfirmationMenu) } + return ctx, nil } // keyboardSendHandler will be called when the user presses the Send button on the keyboard // it will pop up a new keyboard with the last interacted contacts to send funds to // then, the flow is handled as if the user entered /send (then ask for contacts from keyboard or entry, // then ask for an amount). -func (bot *TipBot) keyboardSendHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) keyboardSendHandler(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { - return // errors.New("user has no wallet"), 0 + return ctx, errors.Create(errors.UserNoWalletError) } enterUserStateData := &EnterUserStateData{ ID: "id", @@ -232,7 +234,7 @@ func (bot *TipBot) keyboardSendHandler(ctx context.Context, m *tb.Message) { stateDataJson, err := json.Marshal(enterUserStateData) if err != nil { log.Errorln(err) - return + return ctx, err } SetUserState(user, bot, lnbits.UserEnterUser, string(stateDataJson)) sendToButtons = bot.makeContactsButtons(ctx) @@ -241,8 +243,7 @@ func (bot *TipBot) keyboardSendHandler(ctx context.Context, m *tb.Message) { // immediatelly go to the send handler if len(sendToButtons) == 1 { m.Text = "/send" - bot.sendHandler(ctx, m) - return + return bot.sendHandler(ctx, m) } // Attention! We need to ues the original Telegram.Send command here! @@ -252,27 +253,28 @@ func (bot *TipBot) keyboardSendHandler(ctx context.Context, m *tb.Message) { if err != nil { log.Errorln(err.Error()) } + return ctx, nil } // sendHandler invoked when user clicked send on payment confirmation -func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { tx := &SendData{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err.Error()) - return + return ctx, err } sendData := sn.(*SendData) // onnly the correct user can press if sendData.From.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } if !sendData.Active { log.Errorf("[acceptSendHandler] send not active anymore") // bot.tryDeleteMessage(c.Message) - return + return ctx, errors.Create(errors.NotActiveError) } defer sendData.Set(sendData, bot.Bunt) @@ -295,7 +297,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { if err != nil { log.Errorln(err.Error()) bot.tryDeleteMessage(c.Message) - return + return ctx, err } toUserStrMd := GetUserStrMd(to.Telegram) fromUserStrMd := GetUserStrMd(from.Telegram) @@ -312,7 +314,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { errmsg := fmt.Sprintf("[/send] Error: Transaction failed. %s", err.Error()) log.Errorln(errmsg) bot.tryEditMessage(c.Message, i18n.Translate(sendData.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{}) - return + return ctx, errors.Create(errors.UnknownError) } sendData.Inactivate(sendData, bot.Bunt) @@ -337,11 +339,11 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) { bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", str.MarkdownEscape(sendMemo))) } - return + return ctx, nil } // cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { // reset state immediately user := LoadUser(ctx) ResetUserState(user, bot) @@ -351,16 +353,17 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) { sn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[acceptSendHandler] %s", err.Error()) - return + return ctx, err } sendData := sn.(*SendData) // onnly the correct user can press if sendData.From.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } // delete and send instead of edit for the keyboard to pop up after sending bot.tryDeleteMessage(c.Message) bot.trySendMessage(c.Message.Chat, i18n.Translate(sendData.LanguageCode, "sendCancelledMessage")) sendData.Inactivate(sendData, bot.Bunt) + return ctx, nil } diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 0572e9e6..eadeb142 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "time" @@ -111,49 +112,47 @@ var ( ) // shopItemPriceHandler is invoked when the user presses the item settings button to set a price -func (bot *TipBot) shopItemPriceHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopItemPriceHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) if shop.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } item := shop.Items[shop.ItemIds[shopView.Page]] // sanity check if item.ID != c.Data { log.Error("[shopItemPriceHandler] item id mismatch") - return + return ctx, errors.Create(errors.ItemIdMismatchError) } // We need to save the pay state in the user state so we can load the payment in the next handler SetUserState(user, bot, lnbits.UserStateShopItemSendPrice, item.ID) bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("💯 Enter a price."), tb.ForceReply) + return ctx, nil } // enterShopItemPriceHandler is invoked when the user enters a price amount -func (bot *TipBot) enterShopItemPriceHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) enterShopItemPriceHandler(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - return + return ctx, err } if shop.Owner.Telegram.ID != m.Sender.ID { - return + return ctx, errors.Create(errors.NotShopOwnerError) } item := shop.Items[shop.ItemIds[shopView.Page]] // sanity check if item.ID != user.StateData { log.Error("[shopItemPriceHandler] item id mismatch") - return - } - if shop.Owner.Telegram.ID != m.Sender.ID { - return + return ctx, fmt.Errorf("item id mismatch") } var amount int64 @@ -165,7 +164,7 @@ func (bot *TipBot) enterShopItemPriceHandler(ctx context.Context, m *tb.Message) log.Warnf("[enterShopItemPriceHandler] %s", err.Error()) bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) ResetUserState(user, bot) - return //err, 0 + return ctx, err } } @@ -184,52 +183,51 @@ func (bot *TipBot) enterShopItemPriceHandler(ctx context.Context, m *tb.Message) // bot.shopViewDeleteAllStatusMsgs(ctx, user) // }() bot.displayShopItem(ctx, shopView.Message, shop) + return ctx, nil } // shopItemPriceHandler is invoked when the user presses the item settings button to set a item title -func (bot *TipBot) shopItemTitleHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopItemTitleHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) if shop.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } item := shop.Items[shop.ItemIds[shopView.Page]] // sanity check if item.ID != c.Data { log.Error("[shopItemTitleHandler] item id mismatch") - return + return ctx, errors.Create(errors.ItemIdMismatchError) } // We need to save the pay state in the user state so we can load the payment in the next handler SetUserState(user, bot, lnbits.UserStateShopItemSendTitle, item.ID) bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("⌨️ Enter item title."), tb.ForceReply) + return ctx, nil } // enterShopItemTitleHandler is invoked when the user enters a title of the item -func (bot *TipBot) enterShopItemTitleHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) enterShopItemTitleHandler(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - return + return ctx, err } if shop.Owner.Telegram.ID != m.Sender.ID { - return + return ctx, errors.Create(errors.NotShopOwnerError) } item := shop.Items[shop.ItemIds[shopView.Page]] // sanity check if item.ID != user.StateData { log.Error("[enterShopItemTitleHandler] item id mismatch") - return - } - if shop.Owner.Telegram.ID != m.Sender.ID { - return + return ctx, errors.Create(errors.ItemIdMismatchError) } if len(m.Text) == 0 { ResetUserState(user, bot) @@ -238,7 +236,7 @@ func (bot *TipBot) enterShopItemTitleHandler(ctx context.Context, m *tb.Message) time.Sleep(time.Duration(5) * time.Second) bot.shopViewDeleteAllStatusMsgs(ctx, user) }() - return + return ctx, errors.Create(errors.InvalidSyntaxError) } // crop item title if len(m.Text) > ITEM_TITLE_MAX_LENGTH { @@ -256,45 +254,47 @@ func (bot *TipBot) enterShopItemTitleHandler(ctx context.Context, m *tb.Message) // bot.shopViewDeleteAllStatusMsgs(ctx, user) // }() bot.displayShopItem(ctx, shopView.Message, shop) + return ctx, nil } // shopItemSettingsHandler is invoked when the user presses the item settings button -func (bot *TipBot) shopItemSettingsHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopItemSettingsHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) item := shop.Items[shop.ItemIds[shopView.Page]] // sanity check if item.ID != c.Data { log.Error("[shopItemSettingsHandler] item id mismatch") - return + return ctx, errors.Create(errors.ItemIdMismatchError) } if item.TbPhoto != nil { item.TbPhoto.Caption = bot.getItemTitle(ctx, &item) } - bot.tryEditMessage(shopView.Message, item.TbPhoto, bot.shopItemSettingsMenu(ctx, shop, &item)) + _, err = bot.tryEditMessage(shopView.Message, item.TbPhoto, bot.shopItemSettingsMenu(ctx, shop, &item)) + return ctx, err } // shopItemPriceHandler is invoked when the user presses the item settings button to set a item title -func (bot *TipBot) shopItemDeleteHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopItemDeleteHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - return + return ctx, err } if shop.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } item := shop.Items[shop.ItemIds[shopView.Page]] if shop.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } // delete ItemID of item @@ -323,16 +323,20 @@ func (bot *TipBot) shopItemDeleteHandler(ctx context.Context, c *tb.Callback) { } bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) bot.displayShopItem(ctx, shopView.Message, shop) + return ctx, nil } // displayShopItemHandler is invoked when the user presses the back button in the item settings -func (bot *TipBot) displayShopItemHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) displayShopItemHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) + if err != nil { + return ctx, err + } // item := shop.Items[shop.ItemIds[shopView.Page]] // // sanity check // if item.ID != c.Data { @@ -340,36 +344,41 @@ func (bot *TipBot) displayShopItemHandler(ctx context.Context, c *tb.Callback) { // return // } bot.displayShopItem(ctx, c.Message, shop) + return ctx, nil } // shopNextItemHandler is invoked when the user presses the next item button -func (bot *TipBot) shopNextItemButtonHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopNextItemButtonHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) // shopView, err := bot.Cache.Get(fmt.Sprintf("shopview-%d", user.Telegram.ID)) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) if shopView.Page < len(shop.Items)-1 { shopView.Page++ bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) shop, err = bot.getShop(ctx, shopView.ShopID) + if err != nil { + return ctx, err + } bot.displayShopItem(ctx, c.Message, shop) } + return ctx, nil } // shopPrevItemButtonHandler is invoked when the user presses the previous item button -func (bot *TipBot) shopPrevItemButtonHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopPrevItemButtonHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } if shopView.Page == 0 { c.Message.Text = "/shops " + shopView.ShopOwner.Telegram.Username - bot.shopsHandler(ctx, c.Message) - return + return bot.shopsHandler(ctx, c.Message) + } if shopView.Page > 0 { shopView.Page-- @@ -377,6 +386,7 @@ func (bot *TipBot) shopPrevItemButtonHandler(ctx context.Context, c *tb.Callback bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) shop, err := bot.getShop(ctx, shopView.ShopID) bot.displayShopItem(ctx, c.Message, shop) + return ctx, nil } func (bot *TipBot) getItemTitle(ctx context.Context, item *ShopItem) string { @@ -460,9 +470,9 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho } // shopHandler is invoked when the user enters /shop -func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) (context.Context, error) { if !m.Private() { - return + return ctx, errors.Create(errors.NoPrivateChatError) } user := LoadUser(ctx) shopOwner := user @@ -470,8 +480,7 @@ func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) { // when no argument is given, i.e. command is only /shop, load /shops shop := &Shop{} if len(strings.Split(m.Text, " ")) < 2 || !strings.HasPrefix(strings.Split(m.Text, " ")[1], "shop-") { - bot.shopsHandler(ctx, m) - return + return bot.shopsHandler(ctx, m) } else { // else: get shop by shop ID shopID := strings.Split(m.Text, " ")[1] @@ -479,7 +488,7 @@ func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) { shop, err = bot.getShop(ctx, shopID) if err != nil { log.Errorf("[shopHandler] %s", err.Error()) - return + return ctx, err } } shopOwner = shop.Owner @@ -491,23 +500,33 @@ func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) { } bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) shopView.Message = bot.displayShopItem(ctx, m, shop) - return + // shopMessage := &tb.Message{Chat: m.Chat} + // if len(shop.ItemIds) > 0 { + // // item := shop.Items[shop.ItemIds[shopView.Page]] + // // shopMessage = bot.trySendMessage(m.Chat, item.TbPhoto, bot.shopMenu(ctx, shop, &item)) + // shopMessage = bot.displayShopItem(ctx, m, shop) + // } else { + // shopMessage = bot.trySendMessage(m.Chat, "No items in shop.", bot.shopMenu(ctx, shop, &ShopItem{})) + // } + // shopView.Message = shopMessage + bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + return ctx, nil } // shopNewItemHandler is invoked when the user presses the new item button -func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shop, err := bot.getShop(ctx, c.Data) if err != nil { log.Errorf("[shopNewItemHandler] %s", err.Error()) - return + return ctx, err } if shop.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } if len(shop.Items) >= shop.MaxItems { bot.trySendMessage(c.Sender, fmt.Sprintf("🚫 You can only have %d items in this shop. Delete an item to add a new one.", shop.MaxItems)) - return + return ctx, errors.Create(errors.MaxReachedError) } // We need to save the pay state in the user state so we can load the payment in the next handler @@ -515,10 +534,11 @@ func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) { if err != nil { log.Errorf("[lnurlWithdrawHandler] Error: %s", err.Error()) // bot.trySendMessage(m.Sender, err.Error()) - return + return ctx, err } SetUserState(user, bot, lnbits.UserStateShopItemSendPhoto, string(paramsJson)) bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("🌄 Send me a cover image.")) + return ctx, nil } // addShopItem is a helper function for creating a shop item in the database @@ -552,10 +572,10 @@ func (bot *TipBot) addShopItem(ctx context.Context, shopId string) (*Shop, ShopI } // addShopItemPhoto is invoked when the users sends a photo as a new item -func (bot *TipBot) addShopItemPhoto(ctx context.Context, m *tb.Message) { +func (bot *TipBot) addShopItemPhoto(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { - return // errors.New("user has no wallet"), 0 + return ctx, errors.Create(errors.UserNoWalletError) } // read item from user.StateData @@ -564,15 +584,15 @@ func (bot *TipBot) addShopItemPhoto(ctx context.Context, m *tb.Message) { if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) bot.trySendMessage(m.Sender, Translate(ctx, "errorTryLaterMessage"), Translate(ctx, "errorTryLaterMessage")) - return + return ctx, err } if state_shop.Owner.Telegram.ID != m.Sender.ID { - return + return ctx, errors.Create(errors.NotShopOwnerError) } if m.Photo == nil { bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("🚫 That didn't work. You need to send an image (not a file).")) ResetUserState(user, bot) - return + return ctx, errors.Create(errors.NoPhotoError) } shop, item, err := bot.addShopItem(ctx, state_shop.ID) @@ -597,25 +617,26 @@ func (bot *TipBot) addShopItemPhoto(ctx context.Context, m *tb.Message) { bot.displayShopItem(ctx, shopView.Message, shop) log.Infof("[🛍 shop] %s added an item %s:%s.", GetUserStr(user.Telegram), shop.ID, item.ID) + return ctx, nil } // ------------------- item files ---------- // shopItemAddItemHandler is invoked when the user presses the new item button -func (bot *TipBot) shopItemAddItemHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopItemAddItemHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { - return // errors.New("user has no wallet"), 0 + return ctx, errors.Create(errors.UserNoWalletError) } shopView, err := bot.getUserShopview(ctx, user) if err != nil { log.Errorf("[shopItemAddItemHandler] %s", err.Error()) - return + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { log.Errorf("[shopItemAddItemHandler] %s", err.Error()) - return + return ctx, err } itemID := c.Data @@ -624,27 +645,29 @@ func (bot *TipBot) shopItemAddItemHandler(ctx context.Context, c *tb.Callback) { if len(item.FileIDs) >= item.MaxFiles { bot.trySendMessage(c.Sender, fmt.Sprintf("🚫 You can only have %d files in this item.", item.MaxFiles)) - return + return ctx, errors.Create(errors.NoFileFoundError) } SetUserState(user, bot, lnbits.UserStateShopItemSendItemFile, c.Data) bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("💾 Send me one or more files.")) + return ctx, err } // addItemFileHandler is invoked when the users sends a new file for the item -func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { - return // errors.New("user has no wallet"), 0 + return ctx, errors.Create(errors.UserNoWalletError) } shopView, err := bot.getUserShopview(ctx, user) if err != nil { log.Errorf("[addItemFileHandler] %s", err.Error()) - return + return ctx, err } + shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - log.Errorf("[addItemFileHandler] %s", err.Error()) - return + log.Errorf("[shopNewItemHandler] %s", err.Error()) + return ctx, err } itemID := user.StateData @@ -674,7 +697,7 @@ func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) { item.FileTypes = append(item.FileTypes, "sticker") } else { log.Errorf("[addItemFileHandler] no file found") - return + return ctx, errors.Create(errors.NoFileFoundError) } shop.Items[item.ID] = item @@ -719,22 +742,23 @@ func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) { // }() bot.displayShopItem(ctx, shopView.Message, shop) log.Infof("[🛍 shop] %s added a file to shop:item %s:%s.", GetUserStr(user.Telegram), shop.ID, item.ID) + return ctx, nil } -func (bot *TipBot) shopGetItemFilesHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopGetItemFilesHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { - return // errors.New("user has no wallet"), 0 + return ctx, errors.Create(errors.UserNoWalletError) } shopView, err := bot.getUserShopview(ctx, user) if err != nil { - log.Errorf("[shopGetItemFilesHandler] %s", err.Error()) - return + log.Errorf("[addItemFileHandler] %s", err.Error()) + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { log.Errorf("[shopGetItemFilesHandler] %s", err.Error()) - return + return ctx, err } itemID := c.Data item := shop.Items[itemID] @@ -755,30 +779,30 @@ func (bot *TipBot) shopGetItemFilesHandler(ctx context.Context, c *tb.Callback) // bot.sendFileByID(ctx, c.Sender, fileID, item.FileTypes[i]) // } // log.Infof("[🛍 shop] %s got %d items from %s's item %s (for %d sat).", GetUserStr(user.Telegram), len(item.FileIDs), GetUserStr(shop.Owner.Telegram), item.ID, item.Price) - + return ctx, nil } // shopConfirmBuyHandler is invoked when the user has confirmed to pay for an item -func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { - return // errors.New("user has no wallet"), 0 + return ctx, errors.Create(errors.UserNoWalletError) } shopView, err := bot.getUserShopview(ctx, user) if err != nil { log.Errorf("[shopConfirmBuyHandler] %s", err.Error()) - return + return ctx, err } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { log.Errorf("[shopConfirmBuyHandler] %s", err.Error()) - return + return ctx, err } itemID := c.Data item := shop.Items[itemID] if item.Owner.ID != shop.Owner.ID { log.Errorf("[shopConfirmBuyHandler] Owners do not match.") - return + return ctx, errors.Create(errors.NotShopOwnerError) } from := user to := shop.Owner @@ -790,7 +814,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) { amount := item.Price if amount <= 0 { log.Errorf("[shopConfirmBuyHandler] item has no price.") - return + return ctx, errors.Create(errors.InvalidAmountError) } transactionMemo := fmt.Sprintf("Buy item %s (%d sat).", toUserStr, amount) t := NewTransaction(bot, from, to, amount, TransactionType("shop")) @@ -802,7 +826,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) { errmsg := fmt.Sprintf("[shop] Error: Transaction failed. %s", err.Error()) log.Errorln(errmsg) bot.trySendMessage(user.Telegram, i18n.Translate(user.Telegram.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{}) - return + return ctx, errors.New(errors.UnknownError, err) } // bot.trySendMessage(user.Telegram, fmt.Sprintf("🛍 %d sat sent to %s.", amount, toUserStrMd), &tb.ReplyMarkup{}) shopItemTitle := "an item" @@ -813,6 +837,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) { bot.trySendMessage(from.Telegram, fmt.Sprintf("🛍 You bought `%s` from %s's shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), toUserStrMd, str.MarkdownEscape(shop.Title), amount)) log.Infof("[🛍 shop] %s bought `%s` from %s's shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), toUserStrMd, str.MarkdownEscape(shop.Title), amount) bot.shopSendItemFilesToUser(ctx, user, itemID) + return ctx, nil } // shopSendItemFilesToUser is a handler function to send itemID's files to the user @@ -823,12 +848,12 @@ func (bot *TipBot) shopSendItemFilesToUser(ctx context.Context, toUser *lnbits.U } shopView, err := bot.getUserShopview(ctx, user) if err != nil { - log.Errorf("[addItemFileHandler] %s", err.Error()) + log.Errorf("[shopGetItemFilesHandler] %s", err.Error()) return } shop, err := bot.getShop(ctx, shopView.ShopID) if err != nil { - log.Errorf("[shopNewItemHandler] %s", err.Error()) + log.Errorf("[addItemFileHandler] %s", err.Error()) return } item := shop.Items[itemID] @@ -885,14 +910,14 @@ var ShopsTextHelp = "⚠️ Shops are still in beta. Expect bugs." var ShopsNoShopsText = "*There are no shops here yet.*" // shopsHandlerCallback is a warpper for shopsHandler for callbacks -func (bot *TipBot) shopsHandlerCallback(ctx context.Context, c *tb.Callback) { - bot.shopsHandler(ctx, c.Message) +func (bot *TipBot) shopsHandlerCallback(ctx context.Context, c *tb.Callback) (context.Context, error) { + return bot.shopsHandler(ctx, c.Message) } // shopsHandler is invoked when the user enters /shops -func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) (context.Context, error) { if !m.Private() { - return + return ctx, errors.Create(errors.NoPrivateChatError) } user := LoadUser(ctx) shopOwner := user @@ -911,7 +936,7 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { toUserStrWithoutAt, err = getArgumentFromCommand(m.Text, 1) if err != nil { log.Errorln(err.Error()) - return + return ctx, err } toUserStrWithoutAt = strings.TrimPrefix(toUserStrWithoutAt, "@") toUserStrMention = "@" + toUserStrWithoutAt @@ -925,7 +950,7 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { toUserStrMention = toUserStrMention[:100] } bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), str.MarkdownEscape(toUserStrMention))) - return + return ctx, err } // overwrite user with the one from db shopOwner = toUserDb @@ -939,20 +964,20 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { if shopOwner == nil { log.Error("[shopsHandler] shopOwner is nil") - return + return ctx, errors.Create(errors.ShopNoOwnerError) } shops, err := bot.getUserShops(ctx, shopOwner) if err != nil && user.Telegram.ID == shopOwner.Telegram.ID { shops, err = bot.initUserShops(ctx, user) if err != nil { log.Errorf("[shopsHandler] %s", err.Error()) - return + return ctx, err } } if len(shops.Shops) == 0 && user.Telegram.ID != shopOwner.Telegram.ID { bot.trySendMessage(m.Chat, fmt.Sprintf("This user has no shops yet.")) - return + return ctx, errors.Create(errors.NoShopError) } // build shop list @@ -961,7 +986,7 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { shop, err := bot.getShop(ctx, shopId) if err != nil { log.Errorf("[shopsHandler] %s", err.Error()) - return + return ctx, err } shopTitles += fmt.Sprintf("\n· %s (%d items)", str.MarkdownEscape(shop.Title), len(shop.Items)) @@ -1026,21 +1051,21 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) { StatusMessages: shopView.StatusMessages, // keep the old status messages } bot.Cache.Set(shopViewNew.ID, shopViewNew, &store.Options{Expiration: 24 * time.Hour}) - return + return ctx, nil } // shopsDeleteShopBrowser is invoked when the user clicks on "delete shops" and makes a list of all shops -func (bot *TipBot) shopsDeleteShopBrowser(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopsDeleteShopBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { - return + return ctx, err } var s []*Shop for _, shopId := range shops.Shops { shop, _ := bot.getShop(ctx, shopId) if shop.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } s = append(s, shop) } @@ -1048,10 +1073,11 @@ func (bot *TipBot) shopsDeleteShopBrowser(ctx context.Context, c *tb.Callback) { shopResetShopAskButton = shopKeyboard.Data("⚠️ Delete all shops", "shops_reset_ask", shops.ID) shopKeyboard.Inline(buttonWrapper(append(bot.makseShopSelectionButtons(s, "delete_shop"), shopResetShopAskButton, shopShopsButton), shopKeyboard, 1)...) - bot.tryEditMessage(c.Message, "Which shop do you want to delete?", shopKeyboard) + _, err = bot.tryEditMessage(c.Message, "Which shop do you want to delete?", shopKeyboard) + return ctx, err } -func (bot *TipBot) shopsAskDeleteAllShopsHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopsAskDeleteAllShopsHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { shopResetShopButton := shopKeyboard.Data("⚠️ Delete all shops", "shops_reset", c.Data) buttons := []tb.Row{ shopKeyboard.Row(shopResetShopButton), @@ -1061,91 +1087,97 @@ func (bot *TipBot) shopsAskDeleteAllShopsHandler(ctx context.Context, c *tb.Call buttons..., ) bot.tryEditMessage(c.Message, "Are you sure you want to delete all shops?\nYou will lose all items as well.", shopKeyboard) + return ctx, nil } // shopsLinkShopBrowser is invoked when the user clicks on "shop links" and makes a list of all shops -func (bot *TipBot) shopsLinkShopBrowser(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopsLinkShopBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { - return + return ctx, err } var s []*Shop for _, shopId := range shops.Shops { shop, _ := bot.getShop(ctx, shopId) if shop.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } s = append(s, shop) } shopShopsButton := shopKeyboard.Data("⬅️ Back", "shops_shops", shops.ID) shopKeyboard.Inline(buttonWrapper(append(bot.makseShopSelectionButtons(s, "link_shop"), shopShopsButton), shopKeyboard, 1)...) - bot.tryEditMessage(c.Message, "Select the shop you want to get the link of.", shopKeyboard) + _, err = bot.tryEditMessage(c.Message, "Select the shop you want to get the link of.", shopKeyboard) + return ctx, err } // shopSelectLink is invoked when the user has chosen a shop to get the link of -func (bot *TipBot) shopSelectLink(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopSelectLink(ctx context.Context, c *tb.Callback) (context.Context, error) { shop, _ := bot.getShop(ctx, c.Data) if shop.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } bot.trySendMessage(c.Sender, fmt.Sprintf("*%s*: `/shop %s`", shop.Title, shop.ID)) + return ctx, nil } // shopsLinkShopBrowser is invoked when the user clicks on "shop links" and makes a list of all shops -func (bot *TipBot) shopsRenameShopBrowser(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopsRenameShopBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { - return + return ctx, err } var s []*Shop for _, shopId := range shops.Shops { shop, _ := bot.getShop(ctx, shopId) if shop.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } s = append(s, shop) } shopShopsButton := shopKeyboard.Data("⬅️ Back", "shops_shops", shops.ID) shopKeyboard.Inline(buttonWrapper(append(bot.makseShopSelectionButtons(s, "rename_shop"), shopShopsButton), shopKeyboard, 1)...) - bot.tryEditMessage(c.Message, "Select the shop you want to rename.", shopKeyboard) + _, err = bot.tryEditMessage(c.Message, "Select the shop you want to rename.", shopKeyboard) + return ctx, err } // shopSelectLink is invoked when the user has chosen a shop to get the link of -func (bot *TipBot) shopSelectRename(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopSelectRename(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shop, _ := bot.getShop(ctx, c.Data) if shop.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } // We need to save the pay state in the user state so we can load the payment in the next handler SetUserState(user, bot, lnbits.UserEnterShopTitle, shop.ID) bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("⌨️ Enter the name of your shop."), tb.ForceReply) + return ctx, nil } // shopsDescriptionHandler is invoked when the user clicks on "description" to set a shop description -func (bot *TipBot) shopsDescriptionHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopsDescriptionHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { log.Errorf("[shopsDescriptionHandler] %s", err.Error()) - return + return ctx, err } SetUserState(user, bot, lnbits.UserEnterShopsDescription, shops.ID) bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("⌨️ Enter a description."), tb.ForceReply) + return ctx, nil } // enterShopsDescriptionHandler is invoked when the user enters the shop title -func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { log.Errorf("[enterShopsDescriptionHandler] %s", err.Error()) - return + return ctx, err } if shops.Owner.Telegram.ID != m.Sender.ID { - return + return ctx, errors.Create(errors.NotShopOwnerError) } if len(m.Text) == 0 { ResetUserState(user, bot) @@ -1154,7 +1186,7 @@ func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Messa time.Sleep(time.Duration(5) * time.Second) bot.shopViewDeleteAllStatusMsgs(ctx, user) }() - return + return ctx, errors.Create(errors.InvalidSyntaxError) } // crop shop title @@ -1171,18 +1203,19 @@ func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Messa // }() bot.shopsHandler(ctx, m) bot.tryDeleteMessage(m) + return ctx, nil } // shopsResetHandler is invoked when the user clicks button to reset shops completely -func (bot *TipBot) shopsResetHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopsResetHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { log.Errorf("[shopsResetHandler] %s", err.Error()) - return + return ctx, err } if shops.Owner.Telegram.ID != c.Sender.ID { - return + return ctx, errors.Create(errors.UnknownError) } runtime.IgnoreError(shops.Delete(shops, bot.ShopBunt)) bot.sendStatusMessageAndDelete(ctx, c.Sender, fmt.Sprintf("✅ Shops reset.")) @@ -1190,11 +1223,11 @@ func (bot *TipBot) shopsResetHandler(ctx context.Context, c *tb.Callback) { // time.Sleep(time.Duration(5) * time.Second) // bot.shopViewDeleteAllStatusMsgs(ctx, user) // }() - bot.shopsHandlerCallback(ctx, c) + return bot.shopsHandlerCallback(ctx, c) } // shopSelect is invoked when the user has selected a shop to browse -func (bot *TipBot) shopSelect(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopSelect(ctx context.Context, c *tb.Callback) (context.Context, error) { shop, _ := bot.getShop(ctx, c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -1204,7 +1237,7 @@ func (bot *TipBot) shopSelect(ctx context.Context, c *tb.Callback) { ShopID: shop.ID, Page: 0, } - bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + return ctx, bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) } shopView.Page = 0 shopView.ShopID = shop.ID @@ -1218,17 +1251,17 @@ func (bot *TipBot) shopSelect(ctx context.Context, c *tb.Callback) { // shopMessage = bot.tryEditMessage(c.Message, "There are no items in this shop yet.", bot.shopMenu(ctx, shop, &ShopItem{})) // } shopView.Message = shopMessage - bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) - log.Infof("[🛍 shop] %s entered shop %s.", GetUserStr(user.Telegram), shop.ID) + log.Infof("[🛍 shop] %s erntering shop %s.", GetUserStr(user.Telegram), shop.ID) + return ctx, bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) } // shopSelectDelete is invoked when the user has chosen a shop to delete -func (bot *TipBot) shopSelectDelete(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopSelectDelete(ctx context.Context, c *tb.Callback) (context.Context, error) { shop, _ := bot.getShop(ctx, c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { - return + return ctx, err } // first, delete from Shops for i, shopId := range shops.Shops { @@ -1246,21 +1279,21 @@ func (bot *TipBot) shopSelectDelete(ctx context.Context, c *tb.Callback) { // then, delete shop runtime.IgnoreError(shop.Delete(shop, bot.ShopBunt)) - // then update buttons - bot.shopsDeleteShopBrowser(ctx, c) log.Infof("[🛍 shop] %s deleted shop %s.", GetUserStr(user.Telegram), shop.ID) + // then update buttons + return bot.shopsDeleteShopBrowser(ctx, c) } // shopsBrowser makes a button list of all shops the user can browse -func (bot *TipBot) shopsBrowser(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopsBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } shops, err := bot.getUserShops(ctx, shopView.ShopOwner) if err != nil { - return + return ctx, err } var s []*Shop for _, shopId := range shops.Shops { @@ -1273,57 +1306,60 @@ func (bot *TipBot) shopsBrowser(ctx context.Context, c *tb.Callback) { shopView, err = bot.getUserShopview(ctx, user) if err != nil { shopView.Message = shopMessage - bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + // todo -- check if this is possible (me like) + return ctx, fmt.Errorf("%v:%v", err, bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour})) } - + return ctx, nil } // shopItemSettingsHandler is invoked when the user presses the shop settings button -func (bot *TipBot) shopSettingsHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopSettingsHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { - return + return ctx, err } shops, err := bot.getUserShops(ctx, user) if err != nil { - return + return ctx, err } if shops.ID != c.Data || shops.Owner.Telegram.ID != user.Telegram.ID { log.Error("[shopSettingsHandler] item id mismatch") - return + return ctx, errors.Create(errors.ItemIdMismatchError) } - bot.tryEditMessage(shopView.Message, shopView.Message.Text, bot.shopsSettingsMenu(ctx, shops)) + _, err = bot.tryEditMessage(shopView.Message, shopView.Message.Text, bot.shopsSettingsMenu(ctx, shops)) + return ctx, err } // shopNewShopHandler is invoked when the user presses the new shop button -func (bot *TipBot) shopNewShopHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) shopNewShopHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { log.Errorf("[shopNewShopHandler] %s", err.Error()) - return + return ctx, err } if len(shops.Shops) >= shops.MaxShops { bot.trySendMessage(c.Sender, fmt.Sprintf("🚫 You can only have %d shops. Delete a shop to create a new one.", shops.MaxShops)) - return + return ctx, errors.Create(errors.MaxReachedError) } shop, err := bot.addUserShop(ctx, user) // We need to save the pay state in the user state so we can load the payment in the next handler SetUserState(user, bot, lnbits.UserEnterShopTitle, shop.ID) bot.sendStatusMessage(ctx, c.Sender, fmt.Sprintf("⌨️ Enter the name of your shop."), tb.ForceReply) + return ctx, nil } // enterShopTitleHandler is invoked when the user enters the shop title -func (bot *TipBot) enterShopTitleHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) enterShopTitleHandler(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) // read item from user.StateData shop, err := bot.getShop(ctx, user.StateData) if err != nil { - return + return ctx, errors.Create(errors.NoShopError) } if shop.Owner.Telegram.ID != m.Sender.ID { - return + return ctx, errors.Create(errors.ShopNoOwnerError) } if len(m.Text) == 0 { ResetUserState(user, bot) @@ -1332,7 +1368,7 @@ func (bot *TipBot) enterShopTitleHandler(ctx context.Context, m *tb.Message) { time.Sleep(time.Duration(5) * time.Second) bot.shopViewDeleteAllStatusMsgs(ctx, user) }() - return + return ctx, errors.Create(errors.InvalidSyntaxError) } // crop shop title m.Text = strings.Replace(m.Text, "\n", " ", -1) @@ -1350,4 +1386,5 @@ func (bot *TipBot) enterShopTitleHandler(ctx context.Context, m *tb.Message) { bot.shopsHandler(ctx, m) bot.tryDeleteMessage(m) log.Infof("[🛍 shop] %s added new shop %s.", GetUserStr(user.Telegram), shop.ID) + return ctx, nil } diff --git a/internal/telegram/start.go b/internal/telegram/start.go index 4264e829..7155c5df 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -2,8 +2,9 @@ package telegram import ( "context" - "errors" + stderrors "errors" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "strconv" "time" @@ -17,9 +18,9 @@ import ( "gorm.io/gorm" ) -func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) { +func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) (context.Context, error) { if !m.Private() { - return + return ctx, errors.Create(errors.NoPrivateChatError) } // ATTENTION: DO NOT CALL ANY HANDLER BEFORE THE WALLET IS CREATED // WILL RESULT IN AN ENDLESS LOOP OTHERWISE @@ -30,7 +31,7 @@ func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) { if err != nil { log.Errorln(fmt.Sprintf("[startHandler] Error with initWallet: %s", err.Error())) bot.tryEditMessage(walletCreationMsg, Translate(ctx, "startWalletErrorMessage")) - return + return ctx, err } bot.tryDeleteMessage(walletCreationMsg) ctx = context.WithValue(ctx, "user", user) @@ -42,12 +43,12 @@ func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) { if len(m.Sender.Username) == 0 { bot.trySendMessage(m.Sender, Translate(ctx, "startNoUsernameMessage"), tb.NoPreview) } - return + return ctx, nil } func (bot TipBot) initWallet(tguser *tb.User) (*lnbits.User, error) { user, err := GetUser(tguser, bot) - if errors.Is(err, gorm.ErrRecordNotFound) { + if stderrors.Is(err, gorm.ErrRecordNotFound) { user = &lnbits.User{Telegram: tguser} err = bot.createWallet(user) if err != nil { diff --git a/internal/telegram/state.go b/internal/telegram/state.go index ed14c409..fd6ee125 100644 --- a/internal/telegram/state.go +++ b/internal/telegram/state.go @@ -6,7 +6,7 @@ import ( tb "gopkg.in/lightningtipbot/telebot.v2" ) -type StateCallbackMessage map[lnbits.UserStateKey]func(ctx context.Context, m *tb.Message) +type StateCallbackMessage map[lnbits.UserStateKey]func(ctx context.Context, m *tb.Message) (context.Context, error) var stateCallbackMessage StateCallbackMessage diff --git a/internal/telegram/text.go b/internal/telegram/text.go index 7c14ce08..2d2c2098 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -12,16 +13,15 @@ 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) (context.Context, error) { if m.Chat.Type != tb.ChatPrivate { - return + return ctx, errors.Create(errors.NoPrivateChatError) } // check if user is in Database, if not, initialize wallet user := LoadUser(ctx) if user.Wallet == nil || !user.Initialized { - bot.startHandler(ctx, m) - return + return bot.startHandler(ctx, m) } // check if the user clicked on the balance button @@ -30,26 +30,24 @@ func (bot *TipBot) anyTextHandler(ctx context.Context, m *tb.Message) { // overwrite the message text so it doesn't cause an infinite loop // because balanceHandler calls anyTextHAndler... m.Text = "" - bot.balanceHandler(ctx, m) - return + return bot.balanceHandler(ctx, m) } // could be an invoice anyText := strings.ToLower(m.Text) if lightning.IsInvoice(anyText) { m.Text = "/pay " + anyText - bot.payHandler(ctx, m) - return + return bot.payHandler(ctx, m) } if lightning.IsLnurl(anyText) { m.Text = "/lnurl " + anyText - bot.lnurlHandler(ctx, m) - return + return bot.lnurlHandler(ctx, m) } if c := stateCallbackMessage[user.StateKey]; c != nil { - c(ctx, m) + return c(ctx, m) //ResetUserState(user, bot) } + return ctx, nil } type EnterUserStateData struct { @@ -86,19 +84,19 @@ func (bot *TipBot) askForUser(ctx context.Context, id string, eventType string, // enterAmountHandler is invoked in anyTextHandler when the user needs to enter an amount // the amount is then stored as an entry in the user's stateKey in the user database // any other handler that relies on this, needs to load the resulting amount from the database -func (bot *TipBot) enterUserHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) enterUserHandler(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { - return // errors.New("user has no wallet"), 0 + return ctx, errors.Create(errors.UserNoWalletError) } if !(user.StateKey == lnbits.UserEnterUser) { ResetUserState(user, bot) - return // errors.New("user state does not match"), 0 + return ctx, errors.Create(errors.InvalidSyntaxError) } if len(m.Text) < 4 || strings.HasPrefix(m.Text, "/") || m.Text == SendMenuCommandEnter { ResetUserState(user, bot) - return + return ctx, errors.Create(errors.InvalidSyntaxError) } var EnterUserStateData EnterUserStateData @@ -106,7 +104,7 @@ func (bot *TipBot) enterUserHandler(ctx context.Context, m *tb.Message) { if err != nil { log.Errorf("[EnterUserHandler] %s", err.Error()) ResetUserState(user, bot) - return + return ctx, err } userstr := m.Text @@ -116,10 +114,10 @@ func (bot *TipBot) enterUserHandler(ctx context.Context, m *tb.Message) { switch EnterUserStateData.Type { case "CreateSendState": m.Text = fmt.Sprintf("/send %s", userstr) - bot.sendHandler(ctx, m) - return + return bot.sendHandler(ctx, m) default: ResetUserState(user, bot) - return + return ctx, errors.Create(errors.InvalidSyntaxError) } + return ctx, nil } diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index 67a4accb..21e19fd4 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "time" @@ -30,12 +31,12 @@ func TipCheckSyntax(ctx context.Context, m *tb.Message) (bool, string) { return true, "" } -func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { +func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) (context.Context, error) { // check and print all commands bot.anyTextHandler(ctx, m) user := LoadUser(ctx) if user.Wallet == nil { - return + return ctx, fmt.Errorf("user has no wallet") } // only if message is a reply @@ -43,13 +44,13 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { bot.tryDeleteMessage(m) bot.trySendMessage(m.Sender, helpTipUsage(ctx, Translate(ctx, "tipDidYouReplyMessage"))) bot.trySendMessage(m.Sender, Translate(ctx, "tipInviteGroupMessage")) - return + return ctx, errors.Create(errors.NoReplyMessageError) } if ok, err := TipCheckSyntax(ctx, m); !ok { bot.trySendMessage(m.Sender, helpTipUsage(ctx, err)) NewMessage(m, WithDuration(0, bot)) - return + return ctx, errors.Create(errors.InvalidSyntaxError) } // get tip amount @@ -60,12 +61,12 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, helpTipUsage(ctx, Translate(ctx, "tipValidAmountMessage"))) log.Warnln(errmsg) - return + return ctx, errors.Create(errors.InvalidAmountError) } err = bot.parseCmdDonHandler(ctx, m) if err == nil { - return + return ctx, fmt.Errorf("invalid parseCmdDonHandler") } // TIP COMMAND IS VALID from := LoadUser(ctx) @@ -74,7 +75,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if from.Telegram.ID == to.Telegram.ID { NewMessage(m, WithDuration(0, bot)) bot.trySendMessage(m.Sender, Translate(ctx, "tipYourselfMessage")) - return + return ctx, fmt.Errorf("cannot tip yourself") } toUserStrMd := GetUserStrMd(to.Telegram) @@ -88,7 +89,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { if err != nil { errmsg := fmt.Errorf("[/tip] Error: Could not create wallet for %s", toUserStr) log.Errorln(errmsg) - return + return ctx, fmt.Errorf("could not create wallet for %s", toUserStr) } } @@ -112,7 +113,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { bot.trySendMessage(m.Sender, fmt.Sprintf("%s %s", Translate(ctx, "tipErrorMessage"), err)) errMsg := fmt.Sprintf("[/tip] Transaction failed: %s", err.Error()) log.Warnln(errMsg) - return + return ctx, fmt.Errorf("could not create wallet for %s", toUserStr) } // update tooltip if necessary @@ -134,5 +135,5 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) { } // delete the tip message after a few seconds, this is default behaviour NewMessage(m, WithDuration(time.Second*time.Duration(internal.Configuration.Telegram.MessageDisposeDuration), bot)) - return + return ctx, nil } diff --git a/internal/telegram/tipjar.go b/internal/telegram/tipjar.go index 00886897..8f910d9c 100644 --- a/internal/telegram/tipjar.go +++ b/internal/telegram/tipjar.go @@ -15,7 +15,6 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" ) @@ -174,29 +173,29 @@ func (bot TipBot) makeTipjarKeyboard(ctx context.Context, inlineTipjar *InlineTi return inlineTipjarMenu } -func (bot TipBot) tipjarHandler(ctx context.Context, m *tb.Message) { +func (bot TipBot) tipjarHandler(ctx context.Context, m *tb.Message) (context.Context, error) { bot.anyTextHandler(ctx, m) if m.Private() { bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineTipjarHelpText"), Translate(ctx, "inlineTipjarHelpTipjarInGroup"))) - return + return ctx, errors.Create(errors.NoPrivateChatError) } ctx = bot.mapTipjarLanguage(ctx, m.Text) inlineTipjar, err := bot.makeTipjar(ctx, m, false) if err != nil { log.Errorf("[tipjar] %s", err.Error()) - return + return ctx, err } toUserStr := GetUserStr(m.Sender) bot.trySendMessage(m.Chat, inlineTipjar.Message, bot.makeTipjarKeyboard(ctx, inlineTipjar)) log.Infof("[tipjar] %s created tipjar %s: %d sat (%d per user)", toUserStr, inlineTipjar.ID, inlineTipjar.Amount, inlineTipjar.PerUserAmount) - runtime.IgnoreError(inlineTipjar.Set(inlineTipjar, bot.Bunt)) + return ctx, inlineTipjar.Set(inlineTipjar, bot.Bunt) } -func (bot TipBot) handleInlineTipjarQuery(ctx context.Context, q *tb.Query) { +func (bot TipBot) handleInlineTipjarQuery(ctx context.Context, q *tb.Query) (context.Context, error) { inlineTipjar, err := bot.makeQueryTipjar(ctx, q, false) if err != nil { // log.Errorf("[tipjar] %s", err.Error()) - return + return ctx, err } urls := []string{ queryImage, @@ -227,13 +226,15 @@ func (bot TipBot) handleInlineTipjarQuery(ctx context.Context, q *tb.Query) { }) if err != nil { log.Errorln(err) + return ctx, err } + return ctx, nil } -func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { from := LoadUser(ctx) if from.Wallet == nil { - return + return ctx, errors.Create(errors.UserNoWalletError) } tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) @@ -241,25 +242,25 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback fn, err := tx.Get(tx, bot.Bunt) if err != nil { // log.Errorf("[tipjar] %s", err.Error()) - return + return ctx, err } inlineTipjar := fn.(*InlineTipjar) to := inlineTipjar.To if !inlineTipjar.Active { log.Errorf(fmt.Sprintf("[tipjar] tipjar %s inactive.", inlineTipjar.ID)) - return + return ctx, errors.Create(errors.NotActiveError) } if from.Telegram.ID == to.Telegram.ID { bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) - return + return ctx, errors.Create(errors.SelfPaymentError) } // // check if to user has already given to the tipjar for _, a := range inlineTipjar.From { if a.Telegram.ID == from.Telegram.ID { // to user is already in To slice, has taken from facuet // log.Infof("[tipjar] %s already gave to tipjar %s", GetUserStr(to.Telegram), inlineTipjar.ID) - return + return ctx, errors.Create(errors.UnknownError) } } if inlineTipjar.GivenAmount < inlineTipjar.Amount { @@ -278,7 +279,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) errMsg := fmt.Sprintf("[tipjar] Transaction failed: %s", err.Error()) log.Errorln(errMsg) - return + return ctx, errors.New(errors.UnknownError, err) } log.Infof("[💸 tipjar] Tipjar %s from %s to %s (%d sat).", inlineTipjar.ID, fromUserStr, toUserStr, inlineTipjar.PerUserAmount) @@ -322,24 +323,25 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback bot.tryEditMessage(c.Message, inlineTipjar.Message) inlineTipjar.Active = false } + return ctx, nil } -func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) { +func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) fn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[cancelInlineTipjarHandler] %s", err.Error()) - return + return ctx, err } inlineTipjar := fn.(*InlineTipjar) - if c.Sender.ID == inlineTipjar.To.Telegram.ID { - bot.tryEditMessage(c.Message, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) - // set the inlineTipjar inactive - inlineTipjar.Active = false - runtime.IgnoreError(inlineTipjar.Set(inlineTipjar, bot.Bunt)) + if c.Sender.ID != inlineTipjar.To.Telegram.ID { + return ctx, errors.Create(errors.UnknownError) } - return + bot.tryEditMessage(c.Message, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) + // set the inlineTipjar inactive + inlineTipjar.Active = false + return ctx, inlineTipjar.Set(inlineTipjar, bot.Bunt) } From b6d1ed97f116499ab9491a3e8d4113c3bdea08f2 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 9 Jan 2022 20:43:02 +0000 Subject: [PATCH 190/541] failsaves (#278) * failsaves * failsafe2 Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/runtime/mutex/mutex.go | 24 ++++++++++++++++++++---- internal/telegram/users.go | 5 +++++ 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index e01d97c6..99003a81 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -51,6 +51,14 @@ func checkSoftLock(s string) int { // nLocks in the mutexMap. If not, it locks the object. This is supposed to lock only if nLock == 0. func LockWithContext(ctx context.Context, s string) { uid := ctx.Value("uid").(string) + if len(uid) == 0 { + log.Error("[Mutex] LockWithContext: uid is empty!") + return + } + if len(s) == 0 { + log.Error("[Mutex] LockWithContext: s is empty!") + return + } // sync mutex to sync checkSoftLock with the increment of nLocks // same user can't lock the same object multiple times Lock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) @@ -58,7 +66,7 @@ func LockWithContext(ctx context.Context, s string) { if nLocks == 0 { Lock(s) } else { - log.Tracef("[Mutex] Skip lock (nLocks: %d)", nLocks) + log.Debugf("[Mutex] Skip lock (nLocks: %d)", nLocks) } nLocks++ mutexMap.Set(fmt.Sprintf("nLocks:%s", uid), nLocks) @@ -71,6 +79,14 @@ func LockWithContext(ctx context.Context, s string) { // nLocks == 1 func UnlockWithContext(ctx context.Context, s string) { uid := ctx.Value("uid").(string) + if len(uid) == 0 { + log.Error("[Mutex] UnlockWithContext: uid is empty!") + return + } + if len(s) == 0 { + log.Error("[Mutex] UnlockWithContext: s is empty!") + return + } Lock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) var nLocks = checkSoftLock(uid) nLocks-- @@ -79,7 +95,7 @@ func UnlockWithContext(ctx context.Context, s string) { Unlock(s) mutexMap.Remove(fmt.Sprintf("nLocks:%s", uid)) } else { - log.Tracef("[Mutex] Skip unlock (nLocks: %d)", nLocks) + log.Debugf("[Mutex] Skip unlock (nLocks: %d)", nLocks) } Unlock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) //mutexMap.Remove(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) @@ -98,7 +114,7 @@ func Lock(s string) { m.Lock() mutexMap.Set(s, m) } - log.Tracef("[Mutex] Locked %s", s) + log.Debugf("[Mutex] Locked %s", s) } // Unlock unlocks a mutex in the mutexMap. @@ -106,7 +122,7 @@ func Unlock(s string) { if m, ok := mutexMap.Get(s); ok { mutexMap.Remove(s) m.(*sync.Mutex).Unlock() - log.Tracef("[Mutex] Unlocked %s", s) + log.Debugf("[Mutex] Unlocked %s", s) } else { // this should never happen. Mutex should have been in the mutexMap. log.Errorf("[Mutex] ⚠️⚠️⚠️ Unlock %s not in mutexMap. Skip.", s) diff --git a/internal/telegram/users.go b/internal/telegram/users.go index 244327d5..4d0b6bbd 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -96,6 +96,10 @@ func (bot *TipBot) GetUserBalance(user *lnbits.User) (amount int64, err error) { } func (bot *TipBot) CreateWalletForTelegramUser(tbUser *tb.User) (*lnbits.User, error) { + // failsafe: do not create wallet for existing user + if _, exists := bot.UserExists(tbUser); exists { + return nil, fmt.Errorf("user already exists") + } user := &lnbits.User{Telegram: tbUser} userStr := GetUserStr(tbUser) log.Printf("[CreateWalletForTelegramUser] Creating wallet for user %s ... ", userStr) @@ -105,6 +109,7 @@ func (bot *TipBot) CreateWalletForTelegramUser(tbUser *tb.User) (*lnbits.User, e log.Errorln(errmsg) return user, err } + // todo: remove this. we're doing this already in bot.createWallet(). err = UpdateUserRecord(user, *bot) if err != nil { return nil, err From 5a25f07bfb1070dea4a4f437bdc88686ec843542 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 9 Jan 2022 21:20:42 +0000 Subject: [PATCH 191/541] callback responses (#279) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/faucet.go | 12 +++++++++--- internal/telegram/handler.go | 4 ++-- internal/telegram/intercept/callback.go | 3 ++- internal/telegram/intercept/message.go | 2 +- internal/telegram/intercept/query.go | 2 +- internal/telegram/interceptor.go | 4 +++- internal/telegram/shop.go | 8 ++++++-- translations/en.toml | 1 + 8 files changed, 25 insertions(+), 11 deletions(-) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 0ac66648..583d8d49 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -265,7 +265,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if from.Telegram.ID == to.Telegram.ID { log.Debugf("[faucet] %s is the owner faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) - bot.trySendMessage(from.Telegram, Translate(ctx, "sendYourselfMessage")) + ctx = context.WithValue(ctx, "callback_response", Translate(ctx, "sendYourselfMessage")) return ctx, errors.Create(errors.SelfPaymentError) } // check if to user has already taken from the faucet @@ -273,6 +273,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if a.Telegram.ID == to.Telegram.ID { // to user is already in To slice, has taken from facuet log.Debugf("[faucet] %s:%d already took from faucet %s", GetUserStr(to.Telegram), to.Telegram.ID, inlineFaucet.ID) + ctx = context.WithValue(ctx, "callback_response", Translate(ctx, "inlineFaucetAlreadyTookMessage")) return ctx, errors.Create(errors.UnknownError) } } @@ -309,6 +310,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) errMsg := fmt.Sprintf("[faucet] Transaction failed: %s", err.Error()) log.Warnln(errMsg) + ctx = context.WithValue(ctx, "callback_response", Translate(ctx, "errorTryLaterMessage")) // if faucet fails, cancel it: // c.Sender.ID = inlineFaucet.From.Telegram.ID // overwrite the sender of the callback to be the faucet owner // log.Debugf("[faucet] Canceling faucet %s...", inlineFaucet.ID) @@ -321,7 +323,9 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.To = append(inlineFaucet.To, to) inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount go func() { - bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount)) + to_message := fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount) + ctx = context.WithValue(ctx, "callback_response", to_message) + bot.trySendMessage(to.Telegram, to_message) bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) }() // build faucet message @@ -362,7 +366,9 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor inlineFaucet := fn.(*InlineFaucet) if ignoreID || c.Sender.ID == inlineFaucet.From.Telegram.ID { - bot.tryEditStack(c.Message, inlineFaucet.ID, i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage"), &tb.ReplyMarkup{}) + faucet_cancelled_message := i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage") + bot.tryEditStack(c.Message, inlineFaucet.ID, faucet_cancelled_message, &tb.ReplyMarkup{}) + ctx = context.WithValue(ctx, "callback_response", faucet_cancelled_message) // set the inlineFaucet inactive inlineFaucet.Active = false inlineFaucet.Canceled = true diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index cc0be5d2..96fef6e0 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -63,8 +63,8 @@ func (bot TipBot) registerHandlerWithInterceptor(h Handler) { for _, endpoint := range h.Endpoints { bot.handle(endpoint, intercept.HandlerWithCallback(h.Handler.(func(ctx context.Context, callback *tb.Callback) (context.Context, error)), intercept.WithBeforeCallback(h.Interceptor.Before...), - intercept.WithAfterCallback(append(h.Interceptor.After, bot.answerCallbackInterceptor)...), - intercept.WithDeferCallback(h.Interceptor.OnDefer...))) + intercept.WithAfterCallback(h.Interceptor.After...), + intercept.WithDeferCallback(append(h.Interceptor.OnDefer, bot.answerCallbackInterceptor)...))) } } } diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go index 64bfcc48..f75eecb4 100644 --- a/internal/telegram/intercept/callback.go +++ b/internal/telegram/intercept/callback.go @@ -2,6 +2,7 @@ package intercept import ( "context" + log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" ) @@ -62,8 +63,8 @@ func HandlerWithCallback(handler CallbackFuncHandler, option ...CallbackIntercep log.Traceln(err) return } - defer interceptCallback(ctx, c, hm.onDefer) ctx, err = hm.handler(ctx, c) + defer interceptCallback(ctx, c, hm.onDefer) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go index c6c3f3cc..1109c25c 100644 --- a/internal/telegram/intercept/message.go +++ b/internal/telegram/intercept/message.go @@ -62,8 +62,8 @@ func HandlerWithMessage(handler MessageFuncHandler, option ...MessageInterceptOp log.Traceln(err) return } - defer interceptMessage(ctx, message, hm.onDefer) ctx, err = hm.handler(ctx, message) + defer interceptMessage(ctx, message, hm.onDefer) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go index 6aa8a6d2..20916273 100644 --- a/internal/telegram/intercept/query.go +++ b/internal/telegram/intercept/query.go @@ -62,8 +62,8 @@ func HandlerWithQuery(handler QueryFuncHandler, option ...QueryInterceptOption) log.Traceln(err) return } - defer interceptQuery(ctx, query, hm.onDefer) ctx, err = hm.handler(ctx, query) + defer interceptQuery(ctx, query, hm.onDefer) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index af6c2234..98e530d7 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -3,10 +3,11 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "reflect" "strconv" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/runtime/once" @@ -50,6 +51,7 @@ func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context user := getTelegramUserFromInterface(i) if user != nil { mutex.Unlock(strconv.FormatInt(user.ID, 10)) + return ctx, nil } return ctx, errors.Create(errors.InvalidTypeError) } diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index eadeb142..93bbb475 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" @@ -825,7 +826,8 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) (c // bot.trySendMessage(c.Sender, sendErrorMessage) errmsg := fmt.Sprintf("[shop] Error: Transaction failed. %s", err.Error()) log.Errorln(errmsg) - bot.trySendMessage(user.Telegram, i18n.Translate(user.Telegram.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{}) + ctx = context.WithValue(ctx, "callback_response", i18n.Translate(user.Telegram.LanguageCode, "sendErrorMessage")) + // bot.trySendMessage(user.Telegram, i18n.Translate(user.Telegram.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{}) return ctx, errors.New(errors.UnknownError, err) } // bot.trySendMessage(user.Telegram, fmt.Sprintf("🛍 %d sat sent to %s.", amount, toUserStrMd), &tb.ReplyMarkup{}) @@ -833,6 +835,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) (c if len(item.Title) > 0 { shopItemTitle = fmt.Sprintf("%s", item.Title) } + ctx = context.WithValue(ctx, "callback_response", "🛍 Purchase successful.") bot.trySendMessage(to.Telegram, fmt.Sprintf("🛍 Someone bought `%s` from your shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), str.MarkdownEscape(shop.Title), amount)) bot.trySendMessage(from.Telegram, fmt.Sprintf("🛍 You bought `%s` from %s's shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), toUserStrMd, str.MarkdownEscape(shop.Title), amount)) log.Infof("[🛍 shop] %s bought `%s` from %s's shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), toUserStrMd, str.MarkdownEscape(shop.Title), amount) @@ -1252,6 +1255,7 @@ func (bot *TipBot) shopSelect(ctx context.Context, c *tb.Callback) (context.Cont // } shopView.Message = shopMessage log.Infof("[🛍 shop] %s erntering shop %s.", GetUserStr(user.Telegram), shop.ID) + ctx = context.WithValue(ctx, "callback_response", fmt.Sprintf("🛍 You are browsing %s", shop.Title)) return ctx, bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) } diff --git a/translations/en.toml b/translations/en.toml index 6e648943..bcb0ef42 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -280,6 +280,7 @@ inlineFaucetInvalidAmountMessage = """🚫 Invalid amount.""" inlineFaucetSentMessage = """🚰 %d sat sent to %s.""" inlineFaucetReceivedMessage = """🚰 %s sent you %d sat.""" inlineFaucetHelpFaucetInGroup = """Create a faucet in a group with the bot inside or use 👉 inline command (/advanced for more).""" +inlineFaucetAlreadyTookMessage = """🚫 You already took from this faucet.""" inlineFaucetHelpText = """📖 Oops, that didn't work. %s *Usage:* `/faucet ` From c1fb07fcda5f57152fd97840c4c0d92e4e33e004 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 9 Jan 2022 22:44:06 +0000 Subject: [PATCH 192/541] mutex return (#280) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/runtime/mutex/mutex.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 99003a81..17f77d10 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -53,11 +53,9 @@ func LockWithContext(ctx context.Context, s string) { uid := ctx.Value("uid").(string) if len(uid) == 0 { log.Error("[Mutex] LockWithContext: uid is empty!") - return } if len(s) == 0 { log.Error("[Mutex] LockWithContext: s is empty!") - return } // sync mutex to sync checkSoftLock with the increment of nLocks // same user can't lock the same object multiple times From a7b9506a9f4296cd5053829155c406a9a39f5eff Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 10 Jan 2022 23:34:59 +0000 Subject: [PATCH 193/541] better log (#282) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/api/middleware.go | 2 +- internal/runtime/mutex/mutex.go | 8 ++++---- internal/telegram/shop.go | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/api/middleware.go b/internal/api/middleware.go index 7c9a1a92..3bc7670d 100644 --- a/internal/api/middleware.go +++ b/internal/api/middleware.go @@ -9,7 +9,7 @@ import ( func LoggingMiddleware(prefix string, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - log.Debugf("[%s] %s %s", prefix, r.Method, r.URL.Path) + log.Tracef("[%s] %s %s", prefix, r.Method, r.URL.Path) log.Tracef("[%s]\n%s", prefix, dump(r)) r.BasicAuth() next.ServeHTTP(w, r) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 17f77d10..7eedce27 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -64,7 +64,7 @@ func LockWithContext(ctx context.Context, s string) { if nLocks == 0 { Lock(s) } else { - log.Debugf("[Mutex] Skip lock (nLocks: %d)", nLocks) + log.Tracef("[Mutex] Skip lock (nLocks: %d)", nLocks) } nLocks++ mutexMap.Set(fmt.Sprintf("nLocks:%s", uid), nLocks) @@ -93,7 +93,7 @@ func UnlockWithContext(ctx context.Context, s string) { Unlock(s) mutexMap.Remove(fmt.Sprintf("nLocks:%s", uid)) } else { - log.Debugf("[Mutex] Skip unlock (nLocks: %d)", nLocks) + log.Tracef("[Mutex] Skip unlock (nLocks: %d)", nLocks) } Unlock(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) //mutexMap.Remove(fmt.Sprintf("mutex-sync:%s:%s", s, uid)) @@ -112,7 +112,7 @@ func Lock(s string) { m.Lock() mutexMap.Set(s, m) } - log.Debugf("[Mutex] Locked %s", s) + log.Tracef("[Mutex] Locked %s", s) } // Unlock unlocks a mutex in the mutexMap. @@ -120,7 +120,7 @@ func Unlock(s string) { if m, ok := mutexMap.Get(s); ok { mutexMap.Remove(s) m.(*sync.Mutex).Unlock() - log.Debugf("[Mutex] Unlocked %s", s) + log.Tracef("[Mutex] Unlocked %s", s) } else { // this should never happen. Mutex should have been in the mutexMap. log.Errorf("[Mutex] ⚠️⚠️⚠️ Unlock %s not in mutexMap. Skip.", s) diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 93bbb475..848bd65a 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -838,7 +838,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) (c ctx = context.WithValue(ctx, "callback_response", "🛍 Purchase successful.") bot.trySendMessage(to.Telegram, fmt.Sprintf("🛍 Someone bought `%s` from your shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), str.MarkdownEscape(shop.Title), amount)) bot.trySendMessage(from.Telegram, fmt.Sprintf("🛍 You bought `%s` from %s's shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), toUserStrMd, str.MarkdownEscape(shop.Title), amount)) - log.Infof("[🛍 shop] %s bought `%s` from %s's shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), toUserStrMd, str.MarkdownEscape(shop.Title), amount) + log.Infof("[🛍 shop] %s bought from %s shop: %s item: %s for %d sat.", toUserStr, GetUserStr(to.Telegram), shop.Title, shopItemTitle, amount) bot.shopSendItemFilesToUser(ctx, user, itemID) return ctx, nil } From 95a22401b24e684dd2ffcf5a325d58944974c78a Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 10 Jan 2022 23:44:07 +0000 Subject: [PATCH 194/541] shop logging (#283) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/shop.go | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 848bd65a..4b16db19 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -114,6 +114,7 @@ var ( // shopItemPriceHandler is invoked when the user presses the item settings button to set a price func (bot *TipBot) shopItemPriceHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopItemPriceHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -137,6 +138,7 @@ func (bot *TipBot) shopItemPriceHandler(ctx context.Context, c *tb.Callback) (co // enterShopItemPriceHandler is invoked when the user enters a price amount func (bot *TipBot) enterShopItemPriceHandler(ctx context.Context, m *tb.Message) (context.Context, error) { + log.Debugf("[enterShopItemPriceHandler] %s", m.Text) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -189,6 +191,7 @@ func (bot *TipBot) enterShopItemPriceHandler(ctx context.Context, m *tb.Message) // shopItemPriceHandler is invoked when the user presses the item settings button to set a item title func (bot *TipBot) shopItemTitleHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopItemTitleHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -212,6 +215,7 @@ func (bot *TipBot) shopItemTitleHandler(ctx context.Context, c *tb.Callback) (co // enterShopItemTitleHandler is invoked when the user enters a title of the item func (bot *TipBot) enterShopItemTitleHandler(ctx context.Context, m *tb.Message) (context.Context, error) { + log.Debugf("[enterShopItemTitleHandler] %s", m.Text) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -260,6 +264,7 @@ func (bot *TipBot) enterShopItemTitleHandler(ctx context.Context, m *tb.Message) // shopItemSettingsHandler is invoked when the user presses the item settings button func (bot *TipBot) shopItemSettingsHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopItemSettingsHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -281,6 +286,7 @@ func (bot *TipBot) shopItemSettingsHandler(ctx context.Context, c *tb.Callback) // shopItemPriceHandler is invoked when the user presses the item settings button to set a item title func (bot *TipBot) shopItemDeleteHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopItemDeleteHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -329,6 +335,7 @@ func (bot *TipBot) shopItemDeleteHandler(ctx context.Context, c *tb.Callback) (c // displayShopItemHandler is invoked when the user presses the back button in the item settings func (bot *TipBot) displayShopItemHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[displayShopItemHandler] c.Data: %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -350,6 +357,7 @@ func (bot *TipBot) displayShopItemHandler(ctx context.Context, c *tb.Callback) ( // shopNextItemHandler is invoked when the user presses the next item button func (bot *TipBot) shopNextItemButtonHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopNextItemButtonHandler] c.Data: %s", c.Data) user := LoadUser(ctx) // shopView, err := bot.Cache.Get(fmt.Sprintf("shopview-%d", user.Telegram.ID)) shopView, err := bot.getUserShopview(ctx, user) @@ -371,6 +379,7 @@ func (bot *TipBot) shopNextItemButtonHandler(ctx context.Context, c *tb.Callback // shopPrevItemButtonHandler is invoked when the user presses the previous item button func (bot *TipBot) shopPrevItemButtonHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopPrevItemButtonHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -412,6 +421,7 @@ func (bot *TipBot) getItemTitle(ctx context.Context, item *ShopItem) string { // requires that the shopview page is already set accordingly // m is the message that will be edited func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Shop) *tb.Message { + log.Debugf("[displayShopItem] shop: %+v", shop) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -422,7 +432,7 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho if shopView.Page >= len(shop.Items) { shopView.Page = len(shop.Items) - 1 } - + log.Debugf("[displayShopItem] shop: %s page: %d", shop.ID, shopView.Page) if len(shop.Items) == 0 { no_items_message := "There are no items in this shop yet." if len(shopView.Message.Text) > 0 { @@ -472,6 +482,7 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho // shopHandler is invoked when the user enters /shop func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) (context.Context, error) { + log.Debugf("[shopHandler] %s", m.Text) if !m.Private() { return ctx, errors.Create(errors.NoPrivateChatError) } @@ -516,6 +527,7 @@ func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) (context.Cont // shopNewItemHandler is invoked when the user presses the new item button func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopNewItemHandler] %s", c.Data) user := LoadUser(ctx) shop, err := bot.getShop(ctx, c.Data) if err != nil { @@ -544,6 +556,7 @@ func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) (cont // addShopItem is a helper function for creating a shop item in the database func (bot *TipBot) addShopItem(ctx context.Context, shopId string) (*Shop, ShopItem, error) { + log.Debugf("[addShopItem] shopId: %s", shopId) shop, err := bot.getShop(ctx, shopId) if err != nil { log.Errorf("[addShopItem] %s", err.Error()) @@ -574,6 +587,7 @@ func (bot *TipBot) addShopItem(ctx context.Context, shopId string) (*Shop, ShopI // addShopItemPhoto is invoked when the users sends a photo as a new item func (bot *TipBot) addShopItemPhoto(ctx context.Context, m *tb.Message) (context.Context, error) { + log.Debugf("[addShopItemPhoto] ") user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -624,6 +638,7 @@ func (bot *TipBot) addShopItemPhoto(ctx context.Context, m *tb.Message) (context // ------------------- item files ---------- // shopItemAddItemHandler is invoked when the user presses the new item button func (bot *TipBot) shopItemAddItemHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopItemAddItemHandler] %s", c.Data) user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -655,6 +670,7 @@ func (bot *TipBot) shopItemAddItemHandler(ctx context.Context, c *tb.Callback) ( // addItemFileHandler is invoked when the users sends a new file for the item func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) (context.Context, error) { + log.Debugf("[addItemFileHandler] ") user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -747,6 +763,7 @@ func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) (conte } func (bot *TipBot) shopGetItemFilesHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopGetItemFilesHandler] %s", c.Data) user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -785,6 +802,7 @@ func (bot *TipBot) shopGetItemFilesHandler(ctx context.Context, c *tb.Callback) // shopConfirmBuyHandler is invoked when the user has confirmed to pay for an item func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopConfirmBuyHandler] %s", c.Data) user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -845,6 +863,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) (c // shopSendItemFilesToUser is a handler function to send itemID's files to the user func (bot *TipBot) shopSendItemFilesToUser(ctx context.Context, toUser *lnbits.User, itemID string) { + log.Debugf("[shopSendItemFilesToUser] %s -> %s", GetUserStr(toUser.Telegram), itemID) user := LoadUser(ctx) if user.Wallet == nil { return // errors.New("user has no wallet"), 0 @@ -919,6 +938,7 @@ func (bot *TipBot) shopsHandlerCallback(ctx context.Context, c *tb.Callback) (co // shopsHandler is invoked when the user enters /shops func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) (context.Context, error) { + log.Debugf("[shopsHandler] %s", GetUserStr(m.Sender)) if !m.Private() { return ctx, errors.Create(errors.NoPrivateChatError) } @@ -1059,6 +1079,7 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) (context.Con // shopsDeleteShopBrowser is invoked when the user clicks on "delete shops" and makes a list of all shops func (bot *TipBot) shopsDeleteShopBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopsDeleteShopBrowser] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { @@ -1081,6 +1102,7 @@ func (bot *TipBot) shopsDeleteShopBrowser(ctx context.Context, c *tb.Callback) ( } func (bot *TipBot) shopsAskDeleteAllShopsHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopsAskDeleteAllShopsHandler] %s", c.Data) shopResetShopButton := shopKeyboard.Data("⚠️ Delete all shops", "shops_reset", c.Data) buttons := []tb.Row{ shopKeyboard.Row(shopResetShopButton), @@ -1095,6 +1117,7 @@ func (bot *TipBot) shopsAskDeleteAllShopsHandler(ctx context.Context, c *tb.Call // shopsLinkShopBrowser is invoked when the user clicks on "shop links" and makes a list of all shops func (bot *TipBot) shopsLinkShopBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopsLinkShopBrowser] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { @@ -1116,6 +1139,7 @@ func (bot *TipBot) shopsLinkShopBrowser(ctx context.Context, c *tb.Callback) (co // shopSelectLink is invoked when the user has chosen a shop to get the link of func (bot *TipBot) shopSelectLink(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopSelectLink] %s", c.Data) shop, _ := bot.getShop(ctx, c.Data) if shop.Owner.Telegram.ID != c.Sender.ID { return ctx, errors.Create(errors.UnknownError) @@ -1126,6 +1150,7 @@ func (bot *TipBot) shopSelectLink(ctx context.Context, c *tb.Callback) (context. // shopsLinkShopBrowser is invoked when the user clicks on "shop links" and makes a list of all shops func (bot *TipBot) shopsRenameShopBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopsRenameShopBrowser] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { @@ -1147,6 +1172,7 @@ func (bot *TipBot) shopsRenameShopBrowser(ctx context.Context, c *tb.Callback) ( // shopSelectLink is invoked when the user has chosen a shop to get the link of func (bot *TipBot) shopSelectRename(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopSelectRename] %s", c.Data) user := LoadUser(ctx) shop, _ := bot.getShop(ctx, c.Data) if shop.Owner.Telegram.ID != c.Sender.ID { @@ -1160,6 +1186,7 @@ func (bot *TipBot) shopSelectRename(ctx context.Context, c *tb.Callback) (contex // shopsDescriptionHandler is invoked when the user clicks on "description" to set a shop description func (bot *TipBot) shopsDescriptionHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopsDescriptionHandler] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { @@ -1173,6 +1200,7 @@ func (bot *TipBot) shopsDescriptionHandler(ctx context.Context, c *tb.Callback) // enterShopsDescriptionHandler is invoked when the user enters the shop title func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Message) (context.Context, error) { + log.Debugf("[enterShopsDescriptionHandler] %s", m.Text) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { @@ -1211,6 +1239,7 @@ func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Messa // shopsResetHandler is invoked when the user clicks button to reset shops completely func (bot *TipBot) shopsResetHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopsResetHandler] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { @@ -1231,6 +1260,7 @@ func (bot *TipBot) shopsResetHandler(ctx context.Context, c *tb.Callback) (conte // shopSelect is invoked when the user has selected a shop to browse func (bot *TipBot) shopSelect(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopSelect] %s", c.Data) shop, _ := bot.getShop(ctx, c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -1261,6 +1291,7 @@ func (bot *TipBot) shopSelect(ctx context.Context, c *tb.Callback) (context.Cont // shopSelectDelete is invoked when the user has chosen a shop to delete func (bot *TipBot) shopSelectDelete(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopSelectDelete] %s", c.Data) shop, _ := bot.getShop(ctx, c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) @@ -1290,6 +1321,7 @@ func (bot *TipBot) shopSelectDelete(ctx context.Context, c *tb.Callback) (contex // shopsBrowser makes a button list of all shops the user can browse func (bot *TipBot) shopsBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopsBrowser] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -1318,6 +1350,7 @@ func (bot *TipBot) shopsBrowser(ctx context.Context, c *tb.Callback) (context.Co // shopItemSettingsHandler is invoked when the user presses the shop settings button func (bot *TipBot) shopSettingsHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopSettingsHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -1337,6 +1370,7 @@ func (bot *TipBot) shopSettingsHandler(ctx context.Context, c *tb.Callback) (con // shopNewShopHandler is invoked when the user presses the new shop button func (bot *TipBot) shopNewShopHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + log.Debugf("[shopNewShopHandler] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) if err != nil { @@ -1356,6 +1390,7 @@ func (bot *TipBot) shopNewShopHandler(ctx context.Context, c *tb.Callback) (cont // enterShopTitleHandler is invoked when the user enters the shop title func (bot *TipBot) enterShopTitleHandler(ctx context.Context, m *tb.Message) (context.Context, error) { + log.Debugf("[enterShopTitleHandler] %s", m.Text) user := LoadUser(ctx) // read item from user.StateData shop, err := bot.getShop(ctx, user.StateData) From 7c95daabfdc4e6e98286c72945a84f77f905853a Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 12 Jan 2022 11:16:47 +0000 Subject: [PATCH 195/541] shop stuck fix (#284) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/shop.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 4b16db19..1038ee56 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -429,9 +429,12 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho return nil } // failsafe: if the page is out of bounds, reset it - if shopView.Page >= len(shop.Items) { + if len(shop.Items) > 0 && shopView.Page >= len(shop.Items) { shopView.Page = len(shop.Items) - 1 + } else if len(shop.Items) == 0 { + shopView.Page = 0 } + log.Debugf("[displayShopItem] shop: %s page: %d", shop.ID, shopView.Page) if len(shop.Items) == 0 { no_items_message := "There are no items in this shop yet." From c6087021b705fcec51a480b1f2640a97e6d0c6ae Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 12 Jan 2022 11:20:00 +0000 Subject: [PATCH 196/541] fix again (#285) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/shop.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 1038ee56..c2361901 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -438,10 +438,12 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho log.Debugf("[displayShopItem] shop: %s page: %d", shop.ID, shopView.Page) if len(shop.Items) == 0 { no_items_message := "There are no items in this shop yet." - if len(shopView.Message.Text) > 0 { + if shopView.Message != nil && len(shopView.Message.Text) > 0 { shopView.Message, _ = bot.tryEditMessage(shopView.Message, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) } else { - bot.tryDeleteMessage(shopView.Message) + if shopView.Message != nil { + bot.tryDeleteMessage(shopView.Message) + } shopView.Message = bot.trySendMessage(shopView.Message.Chat, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) } shopView.Page = 0 From 9d876cf5d54bf76539f6dc8fc2ef24647b52341b Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 12 Jan 2022 11:26:22 +0000 Subject: [PATCH 197/541] stuck fix (#286) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/shop.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index c2361901..7d765f52 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -26,6 +26,7 @@ type ShopView struct { Page int Message *tb.Message StatusMessages []*tb.Message + Chat *tb.Chat } type ShopItem struct { @@ -435,7 +436,7 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho shopView.Page = 0 } - log.Debugf("[displayShopItem] shop: %s page: %d", shop.ID, shopView.Page) + log.Debugf("[displayShopItem] shop: %s page: %d items: %d", shop.ID, shopView.Page, len(shop.Items)) if len(shop.Items) == 0 { no_items_message := "There are no items in this shop yet." if shopView.Message != nil && len(shopView.Message.Text) > 0 { @@ -444,7 +445,7 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho if shopView.Message != nil { bot.tryDeleteMessage(shopView.Message) } - shopView.Message = bot.trySendMessage(shopView.Message.Chat, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) + shopView.Message = bot.trySendMessage(shopView.Chat, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) } shopView.Page = 0 return shopView.Message @@ -514,6 +515,7 @@ func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) (context.Cont ShopID: shop.ID, Page: 0, ShopOwner: shopOwner, + Chat: m.Chat, } bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) shopView.Message = bot.displayShopItem(ctx, m, shop) From 545d20d043bb6bc3b07316c24042a7b74886a4e9 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 14 Jan 2022 19:34:20 +0000 Subject: [PATCH 198/541] Mutex fix 329823hf (#287) * big change * mutex * global lock * another lock * fix log * trySendMessageEditable added * defer before return Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/runtime/mutex/mutex.go | 10 ++++++++ internal/telegram/amounts.go | 11 ++++----- internal/telegram/donate.go | 5 ++-- internal/telegram/intercept/callback.go | 2 +- internal/telegram/intercept/message.go | 2 +- internal/telegram/intercept/query.go | 2 +- internal/telegram/interceptor.go | 20 ++++++++-------- internal/telegram/invoice.go | 5 ++-- internal/telegram/lnurl-pay.go | 32 ++++++++++++------------- internal/telegram/lnurl-withdraw.go | 18 +++++++------- internal/telegram/lnurl.go | 8 ++++--- internal/telegram/pay.go | 7 +++--- internal/telegram/send.go | 7 +++--- internal/telegram/shop.go | 2 +- internal/telegram/shop_helpers.go | 4 ---- internal/telegram/start.go | 7 ++++-- internal/telegram/telegram.go | 11 ++++++++- 17 files changed, 88 insertions(+), 65 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 7eedce27..79acda83 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -13,9 +13,11 @@ import ( ) var mutexMap cmap.ConcurrentMap +var mutexMapSync sync.Mutex func init() { mutexMap = cmap.New() + mutexMapSync = sync.Mutex{} } func ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -106,17 +108,24 @@ func Lock(s string) { log.Tracef("[Mutex] Attempt Lock %s", s) if m, ok := mutexMap.Get(s); ok { m.(*sync.Mutex).Lock() + // write into mutex map + mutexMapSync.Lock() mutexMap.Set(s, m) + mutexMapSync.Unlock() } else { m := &sync.Mutex{} m.Lock() + // write into mutex map + mutexMapSync.Lock() mutexMap.Set(s, m) + mutexMapSync.Unlock() } log.Tracef("[Mutex] Locked %s", s) } // Unlock unlocks a mutex in the mutexMap. func Unlock(s string) { + mutexMapSync.Lock() if m, ok := mutexMap.Get(s); ok { mutexMap.Remove(s) m.(*sync.Mutex).Unlock() @@ -125,4 +134,5 @@ func Unlock(s string) { // this should never happen. Mutex should have been in the mutexMap. log.Errorf("[Mutex] ⚠️⚠️⚠️ Unlock %s not in mutexMap. Skip.", s) } + mutexMapSync.Unlock() } diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 431a8153..135229dd 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "strconv" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -178,7 +179,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) (conte return ctx, err } SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(StateDataJson)) - bot.lnurlPayHandlerSend(ctx, m) + return bot.lnurlPayHandlerSend(ctx, m) case "LnurlWithdrawState": tx := &LnurlWithdrawState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} mutex.LockWithContext(ctx, tx.ID) @@ -199,7 +200,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) (conte return ctx, err } SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(StateDataJson)) - bot.lnurlWithdrawHandlerWithdraw(ctx, m) + return bot.lnurlWithdrawHandlerWithdraw(ctx, m) case "CreateInvoiceState": m.Text = fmt.Sprintf("/invoice %d", amount) SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") @@ -220,8 +221,4 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) (conte ResetUserState(user, bot) return ctx, errors.Create(errors.InvalidSyntaxError) } - // // reset database entry - // ResetUserState(user, bot) - // return - return ctx, nil } diff --git a/internal/telegram/donate.go b/internal/telegram/donate.go index 5d1afd31..b4eb93b0 100644 --- a/internal/telegram/donate.go +++ b/internal/telegram/donate.go @@ -3,12 +3,13 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "io" "io/ioutil" "net/http" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" @@ -49,7 +50,7 @@ func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) (context.C } // command is valid - msg := bot.trySendMessage(m.Chat, Translate(ctx, "donationProgressMessage")) + msg := bot.trySendMessageEditable(m.Chat, Translate(ctx, "donationProgressMessage")) // get invoice resp, err := http.Get(fmt.Sprintf(donationEndpoint, amount, GetUserStr(user.Telegram), GetUserStr(bot.Telegram.Me))) if err != nil { diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go index f75eecb4..a8d6e913 100644 --- a/internal/telegram/intercept/callback.go +++ b/internal/telegram/intercept/callback.go @@ -59,12 +59,12 @@ func HandlerWithCallback(handler CallbackFuncHandler, option ...CallbackIntercep return func(c *tb.Callback) { ctx := context.Background() ctx, err := interceptCallback(ctx, c, hm.before) + defer interceptCallback(ctx, c, hm.onDefer) if err != nil { log.Traceln(err) return } ctx, err = hm.handler(ctx, c) - defer interceptCallback(ctx, c, hm.onDefer) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go index 1109c25c..20f09b74 100644 --- a/internal/telegram/intercept/message.go +++ b/internal/telegram/intercept/message.go @@ -58,12 +58,12 @@ func HandlerWithMessage(handler MessageFuncHandler, option ...MessageInterceptOp return func(message *tb.Message) { ctx := context.Background() ctx, err := interceptMessage(ctx, message, hm.before) + defer interceptMessage(ctx, message, hm.onDefer) if err != nil { log.Traceln(err) return } ctx, err = hm.handler(ctx, message) - defer interceptMessage(ctx, message, hm.onDefer) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go index 20916273..b5d0df53 100644 --- a/internal/telegram/intercept/query.go +++ b/internal/telegram/intercept/query.go @@ -58,12 +58,12 @@ func HandlerWithQuery(handler QueryFuncHandler, option ...QueryInterceptOption) return func(query *tb.Query) { ctx := context.Background() ctx, err := interceptQuery(context.Background(), query, hm.before) + defer interceptQuery(ctx, query, hm.onDefer) if err != nil { log.Traceln(err) return } ctx, err = hm.handler(ctx, query) - defer interceptQuery(ctx, query, hm.onDefer) if err != nil { log.Traceln(err) return diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 98e530d7..ae241577 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -46,6 +46,16 @@ func (bot TipBot) singletonCallbackInterceptor(ctx context.Context, i interface{ return ctx, errors.Create(errors.InvalidTypeError) } +// lockInterceptor invoked as first before interceptor +func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + user := getTelegramUserFromInterface(i) + if user != nil { + mutex.Lock(strconv.FormatInt(user.ID, 10)) + return ctx, nil + } + return nil, errors.Create(errors.InvalidTypeError) +} + // unlockInterceptor invoked as onDefer interceptor func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { user := getTelegramUserFromInterface(i) @@ -76,16 +86,6 @@ func (bot TipBot) answerCallbackInterceptor(ctx context.Context, i interface{}) return ctx, errors.Create(errors.InvalidTypeError) } -// lockInterceptor invoked as first before interceptor -func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - user := getTelegramUserFromInterface(i) - if user != nil { - mutex.Lock(strconv.FormatInt(user.ID, 10)) - return ctx, nil - } - return nil, errors.Create(errors.InvalidTypeError) -} - // requireUserInterceptor will return an error if user is not found // user is here an lnbits.User func (bot TipBot) requireUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index e191d0ea..4ee6f2d2 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -4,10 +4,11 @@ import ( "bytes" "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal" log "github.com/sirupsen/logrus" @@ -101,7 +102,7 @@ func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) (context.C memo = memo + tag } - creatingMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlGettingUserMessage")) + creatingMsg := bot.trySendMessageEditable(m.Sender, Translate(ctx, "lnurlGettingUserMessage")) log.Debugf("[/invoice] Creating invoice for %s of %d sat.", userStr, amount) invoice, err := bot.createInvoiceWithEvent(ctx, user, amount, memo, InvoiceCallbackGeneric, "") if err != nil { diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index e2520e0e..a712d3d3 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -9,6 +9,7 @@ import ( "strconv" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -105,10 +106,10 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams } // lnurlPayHandlerSend is invoked when the user has delivered an amount and is ready to pay -func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { +func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { - return + return ctx, errors.Create(errors.UserNoWalletError) } statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlGettingUserMessage")) @@ -116,7 +117,7 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { if user.StateKey != lnbits.UserHasEnteredAmount { log.Errorln("[lnurlPayHandlerSend] state keys don't match") bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, fmt.Errorf("wrong state key") } // read the enter amount state from user.StateData @@ -125,9 +126,8 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, err } - // use the enter amount state of the user to load the LNURL payment state tx := &LnurlPayState{Base: storage.New(storage.ID(enterAmountData.ID))} mutex.LockWithContext(ctx, tx.ID) @@ -136,7 +136,7 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, err } lnurlPayState := fn.(*LnurlPayState) @@ -146,13 +146,13 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, err } callbackUrl, err := url.Parse(lnurlPayState.LNURLPayParams.Callback) if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, err } qs := callbackUrl.Query() // add amount to query string @@ -168,13 +168,13 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, err } body, err := ioutil.ReadAll(res.Body) if err != nil { log.Errorf("[lnurlPayHandlerSend] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, err } var response2 lnurl.LNURLPayValues @@ -186,7 +186,7 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { } log.Errorf("[lnurlPayHandler] Error in LNURLPayValues: %s", error_reason) bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "lnurlPaymentFailed"), error_reason)) - return + return ctx, fmt.Errorf("Error in LNURLPayValues: %s", error_reason) } lnurlPayState.LNURLPayValues = response2 @@ -194,13 +194,13 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) { runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) bot.Telegram.Delete(statusMsg) m.Text = fmt.Sprintf("/pay %s", response2.PR) - bot.payHandler(ctx, m) + return bot.payHandler(ctx, m) } -func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, address string, amount int64) error { +func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, address string, amount int64) (context.Context, error) { split := strings.Split(address, "@") if len(split) != 2 { - return fmt.Errorf("lightning address format wrong") + return ctx, fmt.Errorf("lightning address format wrong") } host := strings.ToLower(split[1]) name := strings.ToLower(split[0]) @@ -212,7 +212,7 @@ func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, ad lnurl, err := lnurl.LNURLEncode(callback) if err != nil { - return err + return ctx, err } if amount > 0 { @@ -233,5 +233,5 @@ func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, ad m.Text = fmt.Sprintf("/lnurl %s", lnurl) } bot.lnurlHandler(ctx, m) - return nil + return ctx, nil } diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index e728ff19..b2a89544 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "io/ioutil" "net/url" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -118,18 +119,18 @@ func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, with } // lnurlWithdrawHandlerWithdraw is invoked when the user has delivered an amount and is ready to pay -func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Message) { +func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Message) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { - return + return ctx, errors.Create(errors.UserNoWalletError) } - statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlPreparingWithdraw")) + statusMsg := bot.trySendMessageEditable(m.Sender, Translate(ctx, "lnurlPreparingWithdraw")) // assert that user has entered an amount if user.StateKey != lnbits.UserHasEnteredAmount { log.Errorln("[lnurlWithdrawHandlerWithdraw] state keys don't match") bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, fmt.Errorf("wrong state key") } // read the enter amount state from user.StateData @@ -138,7 +139,7 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, err } // use the enter amount state of the user to load the LNURL payment state @@ -149,7 +150,7 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, err } var lnurlWithdrawState *LnurlWithdrawState switch fn.(type) { @@ -158,7 +159,7 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa default: log.Errorf("[lnurlWithdrawHandlerWithdraw] invalid type") bot.tryEditMessage(statusMsg, Translate(ctx, "errorTryLaterMessage")) - return + return ctx, fmt.Errorf("invalid type") } confirmText := fmt.Sprintf(Translate(ctx, "confirmLnurlWithdrawMessage"), lnurlWithdrawState.Amount/1000) @@ -184,6 +185,7 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa // // add response to persistent struct // lnurlWithdrawState.LNURResponse = response2 runtime.IgnoreError(lnurlWithdrawState.Set(lnurlWithdrawState, bot.Bunt)) + return ctx, nil } // confirmPayHandler when user clicked pay on payment confirmation diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 9111c477..b38dbbfa 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -4,12 +4,13 @@ import ( "bytes" "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "io/ioutil" "net/http" "net/url" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/tidwall/gjson" @@ -54,7 +55,7 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con if m.Text == "/lnurl" { return bot.lnurlReceiveHandler(ctx, m) } - statusMsg := bot.trySendMessage(m.Sender, Translate(ctx, "lnurlResolvingUrlMessage")) + statusMsg := bot.trySendMessageEditable(m.Sender, Translate(ctx, "lnurlResolvingUrlMessage")) var lnurlSplit string split := strings.Split(m.Text, " ") @@ -76,7 +77,8 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con // HandleLNURL by fiatjaf/go-lnurl _, params, err := bot.HandleLNURL(lnurlSplit) if err != nil { - bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), err.Error())) + bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), "LNURL error.")) + // bot.tryEditMessage(statusMsg, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), err.Error())) log.Warnf("[HandleLNURL] Error: %s", err.Error()) return ctx, err } diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 9a93c8d1..203a02f1 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -3,9 +3,10 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -130,7 +131,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte payButton, cancelButton), ) - payMessage := bot.trySendMessage(m.Chat, confirmText, paymentConfirmationMenu) + payMessage := bot.trySendMessageEditable(m.Chat, confirmText, paymentConfirmationMenu) payData := &PayData{ Base: storage.New(storage.ID(id)), From: user, @@ -226,7 +227,7 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) (conte if c.Message.Private() { // if the command was invoked in private chat - // the edit below was cool, but we need to get rid of the replymarkup inline keyboard thingy for the main menu button update to work (for the new balance) + // the edit below was cool, but we need to pop up the keyboard again // bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) bot.tryDeleteMessage(c.Message) bot.trySendMessage(c.Sender, i18n.Translate(payData.LanguageCode, "invoicePaidMessage")) diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 41af8384..695d258b 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -4,9 +4,10 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -93,7 +94,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Cont if err == nil { if lightning.IsLightningAddress(arg) { // lightning address, send to that address - err = bot.sendToLightningAddress(ctx, m, arg, amount) + ctx, err = bot.sendToLightningAddress(ctx, m, arg, amount) if err != nil { log.Errorln(err.Error()) return ctx, err @@ -325,7 +326,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) (cont // bot.trySendMessage(from.Telegram, fmt.Sprintf(Translate(ctx, "sendSentMessage"), amount, toUserStrMd)) if c.Message.Private() { // if the command was invoked in private chat - // the edit below was cool, but we need to get rid of the replymarkup inline keyboard thingy for the main menu button update to work (for the new balance) + // the edit below was cool, but we need to get rid of the replymarkup inline keyboard thingy for the main menu to pop up // bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd), &tb.ReplyMarkup{}) bot.tryDeleteMessage(c.Message) bot.trySendMessage(c.Sender, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 7d765f52..01ebcef9 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -422,8 +422,8 @@ func (bot *TipBot) getItemTitle(ctx context.Context, item *ShopItem) string { // requires that the shopview page is already set accordingly // m is the message that will be edited func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Shop) *tb.Message { - log.Debugf("[displayShopItem] shop: %+v", shop) user := LoadUser(ctx) + log.Debugf("[displayShopItem] User: %d shop: %s", GetUserStr(user.Telegram), shop.ID) shopView, err := bot.getUserShopview(ctx, user) if err != nil { log.Errorf("[displayShopItem] %s", err.Error()) diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index e972a453..c04c2479 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -237,8 +237,6 @@ func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops // getUserShops returns the Shops for the user func (bot *TipBot) getUserShops(ctx context.Context, user *lnbits.User) (*Shops, error) { tx := &Shops{Base: storage.New(storage.ID(fmt.Sprintf("shops-%d", user.Telegram.ID)))} - mutex.LockWithContext(ctx, tx.ID) - defer mutex.UnlockWithContext(ctx, tx.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) @@ -274,8 +272,6 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e // getShop returns the Shop of a given ID func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) { tx := &Shop{Base: storage.New(storage.ID(shopId))} - mutex.LockWithContext(ctx, tx.ID) - defer mutex.UnlockWithContext(ctx, tx.ID) // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.ShopBunt) if err != nil { diff --git a/internal/telegram/start.go b/internal/telegram/start.go index 7155c5df..8b8d803c 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -4,10 +4,11 @@ import ( "context" stderrors "errors" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "strconv" "time" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal" log "github.com/sirupsen/logrus" @@ -26,7 +27,7 @@ func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) (context.Cont // WILL RESULT IN AN ENDLESS LOOP OTHERWISE // bot.helpHandler(m) log.Printf("[⭐️ /start] New user: %s (%d)\n", GetUserStr(m.Sender), m.Sender.ID) - walletCreationMsg := bot.trySendMessage(m.Sender, Translate(ctx, "startSettingWalletMessage")) + walletCreationMsg := bot.trySendMessageEditable(m.Sender, Translate(ctx, "startSettingWalletMessage")) user, err := bot.initWallet(m.Sender) if err != nil { log.Errorln(fmt.Sprintf("[startHandler] Error with initWallet: %s", err.Error())) @@ -54,6 +55,8 @@ func (bot TipBot) initWallet(tguser *tb.User) (*lnbits.User, error) { if err != nil { return user, err } + // set user initialized + user, err := GetUser(tguser, bot) user.Initialized = true err = UpdateUserRecord(user, bot) if err != nil { diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 6c148cb2..ed3d2539 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -50,6 +50,15 @@ func (bot TipBot) trySendMessage(to tb.Recipient, what interface{}, options ...i return } +func (bot TipBot) trySendMessageEditable(to tb.Recipient, what interface{}, options ...interface{}) (msg *tb.Message) { + rate.CheckLimit(to) + msg, err := bot.Telegram.Send(to, what, options...) + if err != nil { + log.Warnln(err.Error()) + } + return +} + func (bot TipBot) tryReplyMessage(to *tb.Message, what interface{}, options ...interface{}) (msg *tb.Message) { rate.CheckLimit(to) msg, err := bot.Telegram.Reply(to, what, bot.appendMainMenu(to.Chat.ID, to, options)...) @@ -69,7 +78,7 @@ func (bot TipBot) tryEditMessage(to tb.Editable, what interface{}, options ...in _, chatId := to.MessageSig() log.Tracef("[tryEditMessage] sig: %s, chatId: %d", sig, chatId) - msg, err = bot.Telegram.Edit(to, what, bot.appendMainMenu(chatId, to, options)...) + msg, err = bot.Telegram.Edit(to, what, options...) if err != nil { log.Warnln(err.Error()) } From 33776f2616cc141980ba00754abb57266d8f9a24 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 14 Jan 2022 19:38:45 +0000 Subject: [PATCH 199/541] oops (#288) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/intercept/callback.go | 2 +- internal/telegram/intercept/message.go | 2 +- internal/telegram/intercept/query.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go index a8d6e913..ea00e097 100644 --- a/internal/telegram/intercept/callback.go +++ b/internal/telegram/intercept/callback.go @@ -59,11 +59,11 @@ func HandlerWithCallback(handler CallbackFuncHandler, option ...CallbackIntercep return func(c *tb.Callback) { ctx := context.Background() ctx, err := interceptCallback(ctx, c, hm.before) - defer interceptCallback(ctx, c, hm.onDefer) if err != nil { log.Traceln(err) return } + defer interceptCallback(ctx, c, hm.onDefer) ctx, err = hm.handler(ctx, c) if err != nil { log.Traceln(err) diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go index 20f09b74..c6c3f3cc 100644 --- a/internal/telegram/intercept/message.go +++ b/internal/telegram/intercept/message.go @@ -58,11 +58,11 @@ func HandlerWithMessage(handler MessageFuncHandler, option ...MessageInterceptOp return func(message *tb.Message) { ctx := context.Background() ctx, err := interceptMessage(ctx, message, hm.before) - defer interceptMessage(ctx, message, hm.onDefer) if err != nil { log.Traceln(err) return } + defer interceptMessage(ctx, message, hm.onDefer) ctx, err = hm.handler(ctx, message) if err != nil { log.Traceln(err) diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go index b5d0df53..6aa8a6d2 100644 --- a/internal/telegram/intercept/query.go +++ b/internal/telegram/intercept/query.go @@ -58,11 +58,11 @@ func HandlerWithQuery(handler QueryFuncHandler, option ...QueryInterceptOption) return func(query *tb.Query) { ctx := context.Background() ctx, err := interceptQuery(context.Background(), query, hm.before) - defer interceptQuery(ctx, query, hm.onDefer) if err != nil { log.Traceln(err) return } + defer interceptQuery(ctx, query, hm.onDefer) ctx, err = hm.handler(ctx, query) if err != nil { log.Traceln(err) From dedc959f9488123b8d4227c74c6d96a2872dd484 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 14 Jan 2022 23:58:59 +0000 Subject: [PATCH 200/541] faucet summary (#289) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/faucet.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 583d8d49..52fde644 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -391,6 +391,23 @@ func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFauce inlineFaucet.Active = false log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) once.Remove(inlineFaucet.ID) + + // send update to faucet creator + if inlineFaucet.From.Telegram.ID != 0 { + bot.trySendMessage(inlineFaucet.From.Telegram, listFaucetTakers(inlineFaucet)) + } + +} + +func listFaucetTakers(inlineFaucet *InlineFaucet) string { + var to_str string + to_str = fmt.Sprintf("🚰 *Faucet summary*\n\nMemo: %s\nCapacity: %d sat\nTakers: %d\nRemaining: %d sat\n\n*Takers:*\n\n", inlineFaucet.Memo, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.RemainingAmount) + to_str += "```\n" + for _, to := range inlineFaucet.To { + to_str += fmt.Sprintf("%s\n", GetUserStrMd(to.Telegram)) + } + to_str += "```" + return to_str } func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { From 83eb185634b4370b138ed13945e0f59b252b6ae0 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 15 Jan 2022 00:11:11 +0000 Subject: [PATCH 201/541] echo fix (#290) * echo fix * echo fix Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/faucet.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 52fde644..b5197fb7 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -252,12 +252,13 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet := fn.(*InlineFaucet) from := inlineFaucet.From - // log faucet link if possible + // failsafe for queued users if !inlineFaucet.Active { - log.Debugf(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) + log.Tracef(fmt.Sprintf("[faucet] faucet %s inactive. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) bot.finishFaucet(ctx, c, inlineFaucet) return ctx, errors.Create(errors.NotActiveError) } + // log faucet link if possible if c.Message != nil && c.Message.Chat != nil { log.Infof("[faucet] Link: https://t.me/c/%s/%d", strconv.FormatInt(c.Message.Chat.ID, 10)[4:], c.Message.ID) } @@ -388,15 +389,14 @@ func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFauce inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } bot.tryEditStack(c.Message, inlineFaucet.ID, inlineFaucet.Message, &tb.ReplyMarkup{}) - inlineFaucet.Active = false + log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) once.Remove(inlineFaucet.ID) - // send update to faucet creator - if inlineFaucet.From.Telegram.ID != 0 { + if inlineFaucet.Active && inlineFaucet.From.Telegram.ID != 0 { bot.trySendMessage(inlineFaucet.From.Telegram, listFaucetTakers(inlineFaucet)) } - + inlineFaucet.Active = false } func listFaucetTakers(inlineFaucet *InlineFaucet) string { From 72e13e841d3cccaa94b846ea1767bb91289e2cf3 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 15 Jan 2022 01:11:25 +0000 Subject: [PATCH 202/541] faucet fix (#291) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/faucet.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index b5197fb7..8da5dfc9 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -404,7 +404,7 @@ func listFaucetTakers(inlineFaucet *InlineFaucet) string { to_str = fmt.Sprintf("🚰 *Faucet summary*\n\nMemo: %s\nCapacity: %d sat\nTakers: %d\nRemaining: %d sat\n\n*Takers:*\n\n", inlineFaucet.Memo, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.RemainingAmount) to_str += "```\n" for _, to := range inlineFaucet.To { - to_str += fmt.Sprintf("%s\n", GetUserStrMd(to.Telegram)) + to_str += fmt.Sprintf("%s\n", GetUserStr(to.Telegram)) } to_str += "```" return to_str From a112c1b25de9a3b9ee2eed676e13689d553afe32 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Wed, 19 Jan 2022 10:25:37 +0000 Subject: [PATCH 203/541] add shop db path (#292) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- config.yaml.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config.yaml.example b/config.yaml.example index f7c6c507..57b4a45d 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -14,4 +14,5 @@ lnbits: database: db_path: "data/bot.db" buntdb_path: "data/bunt.db" - transactions_path: "data/transactions.db" \ No newline at end of file + transactions_path: "data/transactions.db" + shop_buntdb_path: "data/shop.db" \ No newline at end of file From 95224182721dc975b0a017241177dc4ae5a63f01 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 22 Jan 2022 13:04:48 +0000 Subject: [PATCH 204/541] finish faucet (#295) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/faucet.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 8da5dfc9..f9bf3ed3 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -316,6 +316,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // c.Sender.ID = inlineFaucet.From.Telegram.ID // overwrite the sender of the callback to be the faucet owner // log.Debugf("[faucet] Canceling faucet %s...", inlineFaucet.ID) // bot.cancelInlineFaucet(ctx, c, true) // cancel without ID check + bot.finishFaucet(ctx, c, inlineFaucet) return ctx, errors.New(errors.UnknownError, err) } From 96292604c7601692ffca371cac2b5123c190c693 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sat, 22 Jan 2022 23:25:21 +0100 Subject: [PATCH 205/541] adding lnurl auth (#294) * adding lnurl auth poc * add response and translation * refactor * simpler * using http bot client with proxy transport * functions * translations * clean * comment * revert Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- go.mod | 1 + go.sum | 2 + internal/lnbits/types.go | 55 ++++++++-- internal/telegram/handler.go | 30 ++++++ internal/telegram/lnurl-auth.go | 151 ++++++++++++++++++++++++++++ internal/telegram/lnurl-withdraw.go | 7 +- internal/telegram/lnurl.go | 8 ++ internal/telegram/pay.go | 6 +- translations/de.toml | 7 ++ translations/en.toml | 7 ++ translations/es.toml | 4 + 11 files changed, 259 insertions(+), 19 deletions(-) create mode 100644 internal/telegram/lnurl-auth.go diff --git a/go.mod b/go.mod index 2666e8ee..48c6a4ea 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/BurntSushi/toml v0.3.1 + github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 // indirect github.com/eko/gocache v1.2.0 github.com/fiatjaf/go-lnurl v1.8.4 github.com/fiatjaf/ln-decodepay v1.1.0 diff --git a/go.sum b/go.sum index c06a9c1a..d4eeb198 100644 --- a/go.sum +++ b/go.sum @@ -83,6 +83,7 @@ github.com/btcsuite/snappy-go v1.0.0 h1:ZxaA6lo2EpxGddsA8JwWOcxlzRybb444sgmeJQMJ github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3SkEwmHoWBmX1DNXhXZqlTpq6s4tyJGc= github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= @@ -259,6 +260,7 @@ github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQ github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko= github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc= diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 0445a225..2f2be686 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -1,8 +1,13 @@ package lnbits import ( + "crypto/sha256" + "encoding/hex" + "fmt" "time" + "github.com/btcsuite/btcd/btcec" + "github.com/imroc/req" tb "gopkg.in/lightningtipbot/telebot.v2" ) @@ -15,18 +20,18 @@ type Client struct { } type User struct { - ID string `json:"id"` - Name string `json:"name" gorm:"primaryKey"` - Initialized bool `json:"initialized"` - Telegram *tb.User `gorm:"embedded;embeddedPrefix:telegram_"` - Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"` - StateKey UserStateKey `json:"stateKey"` - StateData string `json:"stateData"` - CreatedAt time.Time `json:"created"` - UpdatedAt time.Time `json:"updated"` - AnonID string `json:"anon_id"` + ID string `json:"id"` + Name string `json:"name" gorm:"primaryKey"` + Initialized bool `json:"initialized"` + Telegram *tb.User `gorm:"embedded;embeddedPrefix:telegram_"` + Wallet *Wallet `gorm:"embedded;embeddedPrefix:wallet_"` + StateKey UserStateKey `json:"stateKey"` + StateData string `json:"stateData"` + CreatedAt time.Time `json:"created"` + UpdatedAt time.Time `json:"updated"` + AnonID string `json:"anon_id"` AnonIDSha256 string `json:"anon_id_sha256"` - Banned bool `json:"banned"` + Banned bool `json:"banned"` } const ( @@ -103,3 +108,31 @@ type BitInvoice struct { PaymentHash string `json:"payment_hash"` PaymentRequest string `json:"payment_request"` } + +// from fiatjaf/lnurl-go +func (u User) LinkingKey(domain string) (*btcec.PrivateKey, *btcec.PublicKey) { + seedhash := sha256.Sum256([]byte( + fmt.Sprintf("lnurlkeyseed:%s:%s", + domain, u.ID))) + return btcec.PrivKeyFromBytes(btcec.S256(), seedhash[:]) +} + +func (u User) SignKeyAuth(domain string, k1hex string) (key string, sig string, err error) { + // lnurl-auth: create a key based on the user id and sign with it + sk, pk := u.LinkingKey(domain) + + k1, err := hex.DecodeString(k1hex) + if err != nil { + return "", "", fmt.Errorf("invalid k1 hex '%s': %w", k1hex, err) + } + + signature, err := sk.Sign(k1) + if err != nil { + return "", "", fmt.Errorf("error signing k1: %w", err) + } + + sig = hex.EncodeToString(signature.Serialize()) + key = hex.EncodeToString(pk.SerializeCompressed()) + + return key, sig, nil +} diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 96fef6e0..1e33d2f1 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -651,6 +651,36 @@ func (bot TipBot) getHandler() []Handler { }, }, }, + { + Endpoints: []interface{}{&btnAuth}, + Handler: bot.confirmLnurlAuthHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.requireUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, + { + Endpoints: []interface{}{&btnCancelAuth}, + Handler: bot.cancelLnurlAuthHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.requireUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, { Endpoints: []interface{}{&shopNewShopButton}, Handler: bot.shopNewShopHandler, diff --git a/internal/telegram/lnurl-auth.go b/internal/telegram/lnurl-auth.go new file mode 100644 index 00000000..6747a0c8 --- /dev/null +++ b/internal/telegram/lnurl-auth.go @@ -0,0 +1,151 @@ +package telegram + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/imroc/req" + + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + + lnurl "github.com/fiatjaf/go-lnurl" + log "github.com/sirupsen/logrus" + tb "gopkg.in/lightningtipbot/telebot.v2" +) + +var ( + authConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + btnCancelAuth = paymentConfirmationMenu.Data("🚫 Cancel", "cancel_login") + btnAuth = paymentConfirmationMenu.Data("✅ Login", "confirm_login") +) + +type LnurlAuthState struct { + *storage.Base + From *lnbits.User `json:"from"` + LNURLAuthParams lnurl.LNURLAuthParams `json:"LNURLAuthParams"` + Comment string `json:"comment"` + LanguageCode string `json:"languagecode"` + Message *tb.Message `json:"message"` +} + +// lnurlPayHandler1 is invoked when the first lnurl response was a lnurlpay response +// at this point, the user hans't necessarily entered an amount yet +func (bot *TipBot) lnurlAuthHandler(ctx context.Context, m *tb.Message, authParams LnurlAuthState) (context.Context, error) { + user := LoadUser(ctx) + if user.Wallet == nil { + return ctx, errors.Create(errors.UserNoWalletError) + } + // object that holds all information about the send payment + id := fmt.Sprintf("lnurlauth-%d-%s", m.Sender.ID, RandStringRunes(5)) + lnurlAuthState := &LnurlAuthState{ + Base: storage.New(storage.ID(id)), + From: user, + LNURLAuthParams: authParams.LNURLAuthParams, + LanguageCode: ctx.Value("publicLanguageCode").(string), + } + // // // create inline buttons + btnAuth = paymentConfirmationMenu.Data(Translate(ctx, "loginButtonMessage"), "confirm_login", id) + btnCancelAuth = paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_login", id) + + paymentConfirmationMenu.Inline( + paymentConfirmationMenu.Row( + btnAuth, + btnCancelAuth), + ) + lnurlAuthState.Message = bot.trySendMessageEditable(m.Chat, + fmt.Sprintf(Translate(ctx, "confirmLnurlAuthMessager"), + lnurlAuthState.LNURLAuthParams.CallbackURL.Host, + ), + paymentConfirmationMenu, + ) + + // save to bunt + runtime.IgnoreError(lnurlAuthState.Set(lnurlAuthState, bot.Bunt)) + return ctx, nil +} + +func (bot *TipBot) confirmLnurlAuthHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + tx := &LnurlAuthState{Base: storage.New(storage.ID(c.Data))} + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) + sn, err := tx.Get(tx, bot.Bunt) + // immediatelly set intransaction to block duplicate calls + if err != nil { + log.Errorf("[confirmPayHandler] %s", err.Error()) + return ctx, err + } + lnurlAuthState := sn.(*LnurlAuthState) + + if !lnurlAuthState.Active { + return ctx, fmt.Errorf("LnurlAuthData not active.") + } + + user := LoadUser(ctx) + if user.Wallet == nil { + return ctx, errors.Create(errors.UserNoWalletError) + } + + // statusMsg := bot.trySendMessageEditable(c.Sender, + // Translate(ctx, "lnurlResolvingUrlMessage"), + // ) + bot.editSingleButton(ctx, c.Message, lnurlAuthState.Message.Text, Translate(ctx, "lnurlResolvingUrlMessage")) + + // from fiatjaf/go-lnurl + p := lnurlAuthState.LNURLAuthParams + key, sig, err := user.SignKeyAuth(p.Host, p.K1) + if err != nil { + return ctx, err + } + + var sentsigres lnurl.LNURLResponse + client, err := bot.GetHttpClient() + if err != nil { + return ctx, err + } + r := req.New() + r.SetClient(client) + res, err := r.Get(p.CallbackURL.String(), url.Values{"sig": {sig}, "key": {key}}) + if err != nil { + return ctx, err + } + err = json.Unmarshal(res.Bytes(), &sentsigres) + if err != nil { + return ctx, err + } + if sentsigres.Status == "ERROR" { + bot.tryEditMessage(c.Message, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), sentsigres.Reason)) + return ctx, err + } + bot.editSingleButton(ctx, c.Message, lnurlAuthState.Message.Text, Translate(ctx, "lnurlSuccessfulLogin")) + return ctx, lnurlAuthState.Inactivate(lnurlAuthState, bot.Bunt) +} + +// cancelPaymentHandler invoked when user clicked cancel on payment confirmation +func (bot *TipBot) cancelLnurlAuthHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + tx := &LnurlAuthState{Base: storage.New(storage.ID(c.Data))} + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) + sn, err := tx.Get(tx, bot.Bunt) + // immediatelly set intransaction to block duplicate calls + if err != nil { + log.Errorf("[confirmPayHandler] %s", err.Error()) + return ctx, err + } + lnurlAuthState := sn.(*LnurlAuthState) + + // onnly the correct user can press + if lnurlAuthState.From.Telegram.ID != c.Sender.ID { + return ctx, errors.Create(errors.UnknownError) + } + // delete and send instead of edit for the keyboard to pop up after sending + bot.tryEditMessage(c.Message, i18n.Translate(lnurlAuthState.LanguageCode, "loginCancelledMessage"), &tb.ReplyMarkup{}) + // bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) + return ctx, lnurlAuthState.Inactivate(lnurlAuthState, bot.Bunt) +} diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index b2a89544..1afe5779 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -43,6 +43,7 @@ type LnurlWithdrawState struct { Message string `json:"message"` } +// editSingleButton edits a message to display a single button (for something like a progress indicator) func (bot *TipBot) editSingleButton(ctx context.Context, m *tb.Message, message string, button string) { bot.tryEditMessage( m, @@ -169,10 +170,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa lnurlWithdrawState.Message = confirmText // create inline buttons - withdrawButton := paymentConfirmationMenu.Data(Translate(ctx, "withdrawButtonMessage"), "confirm_withdraw") - btnCancelWithdraw := paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_withdraw") - withdrawButton.Data = lnurlWithdrawState.ID - btnCancelWithdraw.Data = lnurlWithdrawState.ID + withdrawButton := paymentConfirmationMenu.Data(Translate(ctx, "withdrawButtonMessage"), "confirm_withdraw", lnurlWithdrawState.ID) + btnCancelWithdraw := paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_withdraw", lnurlWithdrawState.ID) withdrawConfirmationMenu.Inline( withdrawConfirmationMenu.Row( diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index b38dbbfa..bd45249a 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -74,6 +74,8 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con // get rid of the URI prefix lnurlSplit = strings.TrimPrefix(lnurlSplit, "lightning:") + + log.Debugf("[lnurlHandler] lnurlSplit: %s", lnurlSplit) // HandleLNURL by fiatjaf/go-lnurl _, params, err := bot.HandleLNURL(lnurlSplit) if err != nil { @@ -83,6 +85,12 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con return ctx, err } switch params.(type) { + case lnurl.LNURLAuthParams: + authParams := &LnurlAuthState{LNURLAuthParams: params.(lnurl.LNURLAuthParams)} + log.Infof("[LNURL-auth] %s", authParams.LNURLAuthParams.Callback) + bot.tryDeleteMessage(statusMsg) + return bot.lnurlAuthHandler(ctx, m, *authParams) + case lnurl.LNURLPayParams: payParams := &LnurlPayState{LNURLPayParams: params.(lnurl.LNURLPayParams)} log.Infof("[LNURL-p] %s", payParams.LNURLPayParams.Callback) diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 203a02f1..f018d884 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -121,10 +121,8 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte id := fmt.Sprintf("pay-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) // // // create inline buttons - payButton := paymentConfirmationMenu.Data(Translate(ctx, "payButtonMessage"), "confirm_pay") - cancelButton := paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_pay") - payButton.Data = id - cancelButton.Data = id + payButton := paymentConfirmationMenu.Data(Translate(ctx, "payButtonMessage"), "confirm_pay", id) + cancelButton := paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_pay", id) paymentConfirmationMenu.Inline( paymentConfirmationMenu.Row( diff --git a/translations/de.toml b/translations/de.toml index 17759d5b..61c2b972 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -237,6 +237,13 @@ lnurlHelpText = """📖 Ups, das hat nicht geklappt. %s *Befehl:* `/lnurl [betrag] ` *Beispiel:* `/lnurl LNURL1DP68GUR...`""" +# LNURL LOGIN + +confirmLnurlAuthMessager = """Möchtest du dich bei %s einloggen?""" +lnurlSuccessfulLogin = """✅ Login erfolgreich.""" +loginButtonMessage = """✅ Login""" +loginCancelledMessage = """🚫 Login abgebrochen.""" + # LNURL WITHDRAW confirmLnurlWithdrawMessage = """Möchtest du diese Abhebung tätigen?\n\n💸 Betrag: %d sat""" diff --git a/translations/en.toml b/translations/en.toml index bcb0ef42..ca6f8610 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -241,6 +241,13 @@ lnurlHelpText = """📖 Oops, that didn't work. %s *Usage:* `/lnurl [amount] ` *Example:* `/lnurl LNURL1DP68GUR...`""" +# LNURL LOGIN + +confirmLnurlAuthMessager = """Do you want to login to %s?""" +lnurlSuccessfulLogin = """✅ Login successful.""" +loginButtonMessage = """✅ Login""" +loginCancelledMessage = """🚫 Login cancelled.""" + # LNURL WITHDRAW confirmLnurlWithdrawMessage = """Do you want to make this withdrawal?\n\n💸 Amount: %d sat""" diff --git a/translations/es.toml b/translations/es.toml index f9b974e9..5077b7f8 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -237,6 +237,10 @@ lnurlHelpText = """📖 Oops, eso no funcionó. %s *Uso:* `/lnurl [monto] ` *Ejemplo:* `/lnurl LNURL1DP68GUR...`""" +# LNURL LOGIN + +lnurlSuccessfulLogin = """✅ Inicio de sesión exitoso.""" + # LNURL WITHDRAW confirmLnurlWithdrawMessage = """¿Desea realizar este retiro?\n\n💸 Monto: %d sat""" From 902928f7945075b082323dca2977658d0e01855a Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 23 Jan 2022 00:48:27 +0100 Subject: [PATCH 206/541] perform graceful shutdown on SIGTERM and SIGSTOP (#281) * perform graceful shutdown on SIGTERM and SIGSTOP * comments Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/runtime/mutex/mutex.go | 3 ++ internal/telegram/bot.go | 51 +++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/internal/runtime/mutex/mutex.go b/internal/runtime/mutex/mutex.go index 79acda83..fd2a12db 100644 --- a/internal/runtime/mutex/mutex.go +++ b/internal/runtime/mutex/mutex.go @@ -19,6 +19,9 @@ func init() { mutexMap = cmap.New() mutexMapSync = sync.Mutex{} } +func IsEmpty() bool { + return mutexMap.Count() == 0 +} func ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("Current number of locks: %d\nLocks: %+v\nUse /mutex/unlock/{id} endpoint to mutex", len(mutexMap.Keys()), mutexMap.Keys()))) diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 5640b532..3f571ca0 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -2,9 +2,14 @@ package telegram import ( "fmt" + "os" + "os/signal" "sync" + "syscall" "time" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + limiter "github.com/LightningTipBot/LightningTipBot/internal/rate" "github.com/eko/gocache/store" @@ -81,6 +86,29 @@ func (bot TipBot) initBotWallet() error { return nil } +// GracefulShutdown will gracefully shutdown the bot +// It will wait for all mutex locks to unlock before shutdown. +func (bot *TipBot) GracefulShutdown() { + t := time.NewTicker(time.Second * 10) + log.Infof("[shutdown] Graceful shutdown (timeout=10s).") + for { + select { + case <-t.C: + // timer expired + log.Infof("[shutdown] Graceful shutdown timeout reached. Forcing shutdown.") + return + default: + // check if all mutex locks are unlocked + if mutex.IsEmpty() { + log.Infof("[shutdown] Graceful shutdown successful.") + return + } + } + time.Sleep(time.Second) + log.Tracef("[shutdown] Trying graceful shutdown...") + } +} + // Start will initialize the Telegram bot and lnbits. func (bot *TipBot) Start() { log.Infof("[Telegram] Authorized on account @%s", bot.Telegram.Me.Username) @@ -89,9 +117,28 @@ func (bot *TipBot) Start() { if err != nil { log.Errorf("Could not initialize bot wallet: %s", err.Error()) } - bot.startEditWorker() + + // register telegram handlers bot.registerTelegramHandlers() + + // edit worker collects messages to edit and + // periodically edits them + bot.startEditWorker() + + // register callbacks for invoices initInvoiceEventCallbacks(bot) + + // register callbacks for user state changes initializeStateCallbackMessage(bot) - bot.Telegram.Start() + + // start the telegram bot + go bot.Telegram.Start() + + // gracefully shutdown + exit := make(chan os.Signal, 1) // we need to reserve to buffer size 1, so the notifier are not blocked + // we need to catch SIGTERM and SIGSTOP + signal.Notify(exit, os.Interrupt, syscall.SIGTERM, syscall.SIGSTOP) + <-exit + // gracefully shutdown + bot.GracefulShutdown() } From c72111dfb783085df5c60df293b6a30148d0ec55 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 22 Jan 2022 23:57:17 +0000 Subject: [PATCH 207/541] add (#296) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- translations/en.toml | 6 +++--- translations/es.toml | 5 ++++- translations/pt-br.toml | 9 ++++++++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/translations/en.toml b/translations/en.toml index ca6f8610..dfdce937 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -82,7 +82,7 @@ _This bot charges no fees but costs Satoshis to operate. If you like the bot, pl */help* 📖 Read this help.""" infoHelpMessage = """ℹ️ *Info*""" -infoYourLightningAddress = """Your Lightning Address is `%s`""" +infoYourLightningAddress = """Your Lightning address is `%s`""" basicsMessage = """🧡 *Bitcoin* _Bitcoin is the currency of the internet. It is permissionless and decentralized and has no masters and no controling authority. Bitcoin is sound money that is faster, more secure, and more inclusive than the legacy financial system._ @@ -195,10 +195,10 @@ invoiceHelpText = """📖 Oops, that didn't work. %s paymentCancelledMessage = """🚫 Payment cancelled.""" invoicePaidMessage = """⚡️ Payment sent.""" invoicePublicPaidMessage = """⚡️ Payment sent by %s.""" -invalidInvoiceHelpMessage = """Did you enter a valid Lightning invoice? Try /send if you want to send to a Telegram user or Lightning address.""" +invalidInvoiceHelpMessage = """Did you enter a valid Lightning invoice? Try /send if you want to send to a Telegram user or to a Lightning address.""" invoiceNoAmountMessage = """🚫 Can't pay invoices without an amount.""" insufficientFundsMessage = """🚫 Insufficient funds. You have %d sat but you need at least %d sat.""" -feeReserveMessage = """⚠️ Sending your entire balance might fail because of network fees. If it fails, try sending a bit less.""" +feeReserveMessage = """⚠️ Sending your entire balance might fail because of network fees. Reserve at least 1% for fees.""" invoicePaymentFailedMessage = """🚫 Payment failed: %s""" invoiceUndefinedErrorMessage = """Could not pay invoice.""" confirmPayInvoiceMessage = """Do you want to send this payment?\n\n💸 Amount: %d sat""" diff --git a/translations/es.toml b/translations/es.toml index 5077b7f8..b7e8a509 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -239,7 +239,10 @@ lnurlHelpText = """📖 Oops, eso no funcionó. %s # LNURL LOGIN -lnurlSuccessfulLogin = """✅ Inicio de sesión exitoso.""" +confirmLnurlAuthMessager = """¿Desea iniciar sesión en %s?""" +lnurlSuccessfulLogin = """✅ Inicio de sesión exitoso.""" +loginButtonMessage = """✅ Inicio de sesión""" +loginCancelledMessage = """🚫 Inicio de sesión cancelado.""" # LNURL WITHDRAW diff --git a/translations/pt-br.toml b/translations/pt-br.toml index 792940ca..87066598 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -78,7 +78,7 @@ _Este bot não cobra nenhuma comissão, mas custa Satoshis para funcionar. Se vo */help* 📖 Ler esta ajuda.""" infoHelpMessage = """ℹ️ *Info*""" -infoYourLightningAddress = """Seu Lightning Address _(Endereço Lightning)_ é `%s`.""" +infoYourLightningAddress = """Seu Lightning address _(endereço lightning)_ é `%s`.""" basicsMessage = """🧡 *Bitcoin* _Bitcoin é a moeda da Internet. É uma moeda sem permissão, descentralizada, sem proprietários e sem autoridade de controle. Bitcoin é dinheiro sólido, mais rápido, mais seguro e mais inclusivo do que o sistema financeiro fiat._ @@ -237,6 +237,13 @@ lnurlHelpText = """📖 Opa, isso não funcionou. %s *Uso:* `/lnurl [quantidade] ` *Exemplo:* `/lnurl LNURL1DP68GUR...`""" +# LNURL LOGIN + +confirmLnurlAuthMessager = """Você quer fazer login em %s?""" +lnurlSuccessfulLogin = """✅ Login com sucesso.""" +loginButtonMessage = """✅ Login""" +loginCancelledMessage = """🚫 Login cancelado.""" + # LNURL WITHDRAW confirmLnurlWithdrawMessage = """Você quer fazer esse saque?\n💸 Quantia: %d sat""" From d786dc58e0f3191b9f16e8a740d3c6879d7eb590 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sun, 30 Jan 2022 19:41:12 +0000 Subject: [PATCH 208/541] Unban fix (#300) * unban fix * fix Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/api/admin/ban.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/api/admin/ban.go b/internal/api/admin/ban.go index 4b90c4b5..a5b24dff 100644 --- a/internal/api/admin/ban.go +++ b/internal/api/admin/ban.go @@ -2,12 +2,13 @@ package admin import ( "fmt" + "net/http" + "strings" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/telegram" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" - "net/http" - "strings" ) func (s Service) UnbanUser(w http.ResponseWriter, r *http.Request) { @@ -17,8 +18,8 @@ func (s Service) UnbanUser(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) return } - if !user.Banned { - log.Infof("[ADMIN] user already banned") + if !user.Banned && !strings.HasPrefix(user.Wallet.Adminkey, "banned_") { + log.Infof("[ADMIN] user is not banned. Aborting.") w.WriteHeader(http.StatusBadRequest) return } @@ -43,7 +44,7 @@ func (s Service) BanUser(w http.ResponseWriter, r *http.Request) { } if user.Banned { w.WriteHeader(http.StatusBadRequest) - log.Infof("[ADMIN] user already banned") + log.Infof("[ADMIN] user is already banned. Aborting.") return } user.Banned = true From 70ecf335f2a2a290e13e8315f06557832f50dee6 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 7 Feb 2022 20:30:02 +0000 Subject: [PATCH 209/541] honkhonk (#302) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/handler.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 1e33d2f1..549d3382 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -120,7 +120,7 @@ func (bot TipBot) getHandler() []Handler { }, }, { - Endpoints: []interface{}{"/tip"}, + Endpoints: []interface{}{"/tip", "/t", "/honk"}, Handler: bot.tipHandler, Interceptor: &Interceptor{ Type: MessageInterceptor, From 54beceba32862540bb7a34983a37e6a366ef7a27 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Mon, 7 Feb 2022 22:03:17 +0100 Subject: [PATCH 210/541] add empty callback response (#301) * add empty response * commend Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/interceptor.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index ae241577..515da7c8 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -79,6 +79,10 @@ func (bot TipBot) answerCallbackInterceptor(ctx context.Context, i interface{}) if ctxcr != nil { res = append(res, &tb.CallbackResponse{CallbackID: c.ID, Text: ctxcr.(string)}) } + // if the context wasn't set, still respond with an empty callback response + if len(res) == 0 { + res = append(res, &tb.CallbackResponse{CallbackID: c.ID, Text: ""}) + } var err error err = bot.Telegram.Respond(c, res...) return ctx, err From 2b09fc95dd21f42414639c0907cc9fc9117e4d77 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Mon, 7 Feb 2022 22:12:03 +0100 Subject: [PATCH 211/541] print debug stack on empty annon id update (#299) * print debug stack on empty annon id update * write stack to file * use AnnonID * set ids * comments Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/database.go | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 8eab190b..ff89ceca 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -1,11 +1,18 @@ package telegram import ( + "bufio" + "crypto/sha1" + "encoding/base64" "fmt" + "os" "reflect" + "runtime/debug" "strconv" "time" + "github.com/LightningTipBot/LightningTipBot/internal/str" + "github.com/eko/gocache/store" "github.com/LightningTipBot/LightningTipBot/internal" @@ -168,9 +175,49 @@ func telegramUserChanged(apiUser, stateUser *tb.User) bool { } return true } +func debugStack() { + stack := debug.Stack() + go func() { + hasher := sha1.New() + hasher.Write(stack) + sha := base64.URLEncoding.EncodeToString(hasher.Sum(nil)) + fo, err := os.Create(fmt.Sprintf("trace_%s.txt", sha)) + log.Infof("[debugStack] ⚠️ Writing stack trace to %s", fmt.Sprintf("trace_%s.txt", sha)) + if err != nil { + panic(err) + } + defer func() { + if err := fo.Close(); err != nil { + panic(err) + } + }() + w := bufio.NewWriter(fo) + if _, err := w.Write(stack); err != nil { + panic(err) + } + if err = w.Flush(); err != nil { + panic(err) + } + }() +} func UpdateUserRecord(user *lnbits.User, bot TipBot) error { user.UpdatedAt = time.Now() + + // There is a weird bug that makes the AnonID vanish. This is a workaround. + // TODO -- Remove this after empty anon id bug is identified + if user.AnonIDSha256 == "" { + debugStack() + user.AnonIDSha256 = str.AnonIdSha256(user) + log.Errorf("[UpdateUserRecord] AnonIDSha256 empty! Setting to: %s", user.AnonID) + } + // TODO -- Remove this after empty anon id bug is identified + if user.AnonID == "" { + debugStack() + user.AnonID = fmt.Sprint(str.Int32Hash(user.ID)) + log.Errorf("[UpdateUserRecord] AnonID empty! Setting to: %s", user.AnonID) + } + tx := bot.Database.Save(user) if tx.Error != nil { errmsg := fmt.Sprintf("[UpdateUserRecord] Error: Couldn't update %s's info in Database.", GetUserStr(user.Telegram)) From 725af545ab1a43c509db30b94bcdf77559f7dce9 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Fri, 11 Feb 2022 10:04:08 +0100 Subject: [PATCH 212/541] WIP: adding downloadMyProfilePicture and botProfilePicture to LNURL Meta (#297) * adding downloadMyProfilePicture and botProfilePicture to serveLNURLFirst (pay) * fix image download to ram * add downloadProfilePicture * add cache and download limit * error checks * user picture * empty entry bug check for uuid * check * fix example Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- config.yaml.example | 3 +- go.mod | 1 + go.sum | 2 + internal/config.go | 1 + internal/database/migrations.go | 18 +++++ internal/lnbits/types.go | 1 + internal/lnurl/lnurl.go | 114 ++++++++++++++++++++++++++------ internal/str/strings.go | 7 ++ internal/telegram/bot.go | 3 + internal/telegram/database.go | 20 +++++- internal/telegram/faucet.go | 4 +- internal/telegram/lnurl.go | 16 ++++- internal/telegram/photo.go | 77 ++++++++++++++++++++- internal/telegram/start.go | 3 + 14 files changed, 244 insertions(+), 26 deletions(-) diff --git a/config.yaml.example b/config.yaml.example index 57b4a45d..e94b56b3 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -2,6 +2,7 @@ bot: http_proxy: "" lnurl_public_host_name: "mylnurl.com" lnurl_server: "https://mylnurl.com" + lnurl_image: true telegram: message_dispose_duration: 10 api_key: "1234" @@ -15,4 +16,4 @@ database: db_path: "data/bot.db" buntdb_path: "data/bunt.db" transactions_path: "data/transactions.db" - shop_buntdb_path: "data/shop.db" \ No newline at end of file + shop_buntdb_path: "data/shop.db" diff --git a/go.mod b/go.mod index 48c6a4ea..84420c1e 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/imroc/req v0.3.0 github.com/jinzhu/configor v1.2.1 github.com/makiuchi-d/gozxing v0.0.2 + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect 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 diff --git a/go.sum b/go.sum index d4eeb198..2ae27308 100644 --- a/go.sum +++ b/go.sum @@ -365,6 +365,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/nicksnyder/go-i18n/v2 v2.1.2 h1:QHYxcUJnGHBaq7XbvgunmZ2Pn0focXFqTD61CkH146c= github.com/nicksnyder/go-i18n/v2 v2.1.2/go.mod h1:d++QJC9ZVf7pa48qrsRWhMJ5pSHIPmS3OLqK1niyLxs= github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= diff --git a/internal/config.go b/internal/config.go index 9d7b436e..797208a5 100644 --- a/internal/config.go +++ b/internal/config.go @@ -22,6 +22,7 @@ type BotConfiguration struct { LNURLServerUrl *url.URL `yaml:"-"` LNURLHostName string `yaml:"lnurl_public_host_name"` LNURLHostUrl *url.URL `yaml:"-"` + LNURLSendImage bool `yaml:"lnurl_image"` } type TelegramConfiguration struct { diff --git a/internal/database/migrations.go b/internal/database/migrations.go index 7fbc7dec..022302bd 100644 --- a/internal/database/migrations.go +++ b/internal/database/migrations.go @@ -42,3 +42,21 @@ func MigrateAnonIdSha265Hash(db *gorm.DB) error { } return nil } + +func MigrateUUIDSha265Hash(db *gorm.DB) error { + users := []lnbits.User{} + _ = db.Find(&users) + for _, u := range users { + pw := u.Wallet.ID + uuid := str.UUIDSha256(&u) + log.Infof("[MigrateUUIDSha265Hash] %s -> %s", pw, uuid) + u.UUID = uuid + tx := db.Save(u) + if tx.Error != nil { + errmsg := fmt.Sprintf("[MigrateUUIDSha265Hash] Error: Couldn't migrate user %s (%s)", u.Telegram.Username, pw) + log.Errorln(errmsg) + return tx.Error + } + } + return nil +} diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 2f2be686..a50ba8ed 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -31,6 +31,7 @@ type User struct { UpdatedAt time.Time `json:"updated"` AnonID string `json:"anon_id"` AnonIDSha256 string `json:"anon_id_sha256"` + UUID string `json:"uuid"` Banned bool `json:"banned"` } diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 09379c20..23e0076b 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -10,6 +10,9 @@ import ( "strings" "time" + "github.com/eko/gocache/store" + tb "gopkg.in/lightningtipbot/telebot.v2" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/api" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -40,11 +43,13 @@ type Invoice struct { PaidAt time.Time `json:"paid_at"` } type Lnurl struct { + telegram *tb.Bot c *lnbits.Client database *gorm.DB callbackHostname *url.URL buntdb *storage.DB WebhookServer string + cache telegram.Cache } func New(bot *telegram.TipBot) Lnurl { @@ -54,6 +59,8 @@ func New(bot *telegram.TipBot) Lnurl { callbackHostname: internal.Configuration.Bot.LNURLHostUrl, WebhookServer: internal.Configuration.Lnbits.WebhookServer, buntdb: bot.Bunt, + telegram: bot.Telegram, + cache: bot.Cache, } } func (lnurlInvoice Invoice) Key() string { @@ -103,6 +110,30 @@ func (w Lnurl) Handle(writer http.ResponseWriter, request *http.Request) { api.NotFoundHandler(writer, err) } } +func (w Lnurl) getMetaDataCached(username string) lnurl.Metadata { + key := fmt.Sprintf("lnurl_metadata_%s", username) + + // load metadata from cache + if m, err := w.cache.Get(key); err == nil { + return m.(lnurl.Metadata) + } + + // otherwise, create new metadata + metadata := w.metaData(username) + + // load the user profile picture + if internal.Configuration.Bot.LNURLSendImage { + // get the user from the database + user, tx := findUser(w.database, username) + if tx.Error == nil && user.Telegram != nil { + addImageToMetaData(w.telegram, &metadata, username, user.Telegram) + } + } + + // save into cache + runtime.IgnoreError(w.cache.Set(key, metadata, &store.Options{Expiration: 12 * time.Hour})) + return metadata +} // serveLNURLpFirst serves the first part of the LNURLp protocol with the endpoint // to call and the metadata that matches the description hash of the second response @@ -112,7 +143,9 @@ func (w Lnurl) serveLNURLpFirst(username string) (*lnurl.LNURLPayParams, error) if err != nil { return nil, err } - metadata := w.metaData(username) + + // produce the metadata including the image + metadata := w.getMetaDataCached(username) return &lnurl.LNURLPayParams{ LNURLResponse: lnurl.LNURLResponse{Status: api.StatusOk}, @@ -123,7 +156,6 @@ func (w Lnurl) serveLNURLpFirst(username string) (*lnurl.LNURLPayParams, error) EncodedMetadata: metadata.Encode(), CommentAllowed: CommentAllowed, }, nil - } // serveLNURLpSecond serves the second LNURL response with the payment request with the correct description hash @@ -145,22 +177,7 @@ func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment str Reason: fmt.Sprintf("Comment too long (max: %d characters).", CommentAllowed)}, }, fmt.Errorf("comment too long") } - - // now check for the user - user := &lnbits.User{} - // check if "username" is actually the user ID - tx := w.database - if _, err := strconv.ParseInt(username, 10, 64); err == nil { - // asume it's anon_id - tx = w.database.Where("anon_id = ?", username).First(user) - } else if strings.HasPrefix(username, "0x") { - // asume it's anon_id_sha256 - tx = w.database.Where("anon_id_sha256 = ?", username).First(user) - } else { - // assume it's a string @username - tx = w.database.Where("telegram_username = ? COLLATE NOCASE", username).First(user) - } - + user, tx := findUser(w.database, username) if tx.Error != nil { return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ @@ -181,7 +198,8 @@ func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment str var resp *lnurl.LNURLPayValues // the same description_hash needs to be built in the second request - metadata := w.metaData(username) + metadata := w.getMetaDataCached(username) + descriptionHash, err := w.descriptionHash(metadata) if err != nil { return nil, err @@ -242,8 +260,66 @@ func (w Lnurl) descriptionHash(metadata lnurl.Metadata) (string, error) { // metaData returns the metadata that is sent in the first response // and is used again in the second response to verify the description hash func (w Lnurl) metaData(username string) lnurl.Metadata { + // this is a bit stupid but if the address is a UUID starting with 1x... + // we actually want to find the users username so it looks nicer in the + // metadata description + if strings.HasPrefix(username, "1x") { + user, _ := findUser(w.database, username) + if user.Telegram.Username != "" { + username = user.Telegram.Username + } + } + return lnurl.Metadata{ Description: fmt.Sprintf("Pay to %s@%s", username, w.callbackHostname.Hostname()), LightningAddress: fmt.Sprintf("%s@%s", username, w.callbackHostname.Hostname()), } } + +// addImageMetaData add images an image to the LNURL metadata +func addImageToMetaData(tb *tb.Bot, metadata *lnurl.Metadata, username string, user *tb.User) { + metadata.Image.Ext = "jpeg" + + // if the username is anonymous, add the bot's avatar + if isAnonUsername(username) { + metadata.Image.Bytes = telegram.BotProfilePicture + return + } + + // if the user has a profile picture, add it + picture, err := telegram.DownloadProfilePicture(tb, user) + if err != nil { + log.Errorf("[LNURL] Couldn't download user %s's profile picture: %v", username, err) + return + } + metadata.Image.Bytes = picture +} + +func isAnonUsername(username string) bool { + if _, err := strconv.ParseInt(username, 10, 64); err == nil { + return true + } else { + return strings.HasPrefix(username, "0x") + } +} + +func findUser(database *gorm.DB, username string) (*lnbits.User, *gorm.DB) { + // now check for the user + user := &lnbits.User{} + // check if "username" is actually the user ID + tx := database + if _, err := strconv.ParseInt(username, 10, 64); err == nil { + // asume it's anon_id + tx = database.Where("anon_id = ?", username).First(user) + } else if strings.HasPrefix(username, "0x") { + // asume it's anon_id_sha256 + tx = database.Where("anon_id_sha256 = ?", username).First(user) + } else if strings.HasPrefix(username, "1x") { + // asume it's uuid + tx = database.Where("uuid = ?", username).First(user) + } else { + // assume it's a string @username + tx = database.Where("telegram_username = ? COLLATE NOCASE", username).First(user) + } + return user, tx +} diff --git a/internal/str/strings.go b/internal/str/strings.go index be8eb074..0cdee213 100644 --- a/internal/str/strings.go +++ b/internal/str/strings.go @@ -48,3 +48,10 @@ func AnonIdSha256(u *lnbits.User) string { anon_id := fmt.Sprintf("0x%s", hash[:16]) // starts with 0x because that can't be a valid telegram username return anon_id } + +func UUIDSha256(u *lnbits.User) string { + h := sha256.Sum256([]byte(u.Wallet.ID)) + hash := fmt.Sprintf("%x", h) + anon_id := fmt.Sprintf("1x%s", hash[len(hash)-16:]) // starts with 1x because that can't be a valid telegram username + return anon_id +} diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 3f571ca0..0aff8af9 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -121,6 +121,9 @@ func (bot *TipBot) Start() { // register telegram handlers bot.registerTelegramHandlers() + // download bot avatar once + bot.downloadMyProfilePicture() + // edit worker collects messages to edit and // periodically edits them bot.startEditWorker() diff --git a/internal/telegram/database.go b/internal/telegram/database.go index ff89ceca..9c31f34e 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -70,6 +70,18 @@ func ColumnMigrationTasks(db *gorm.DB) error { err = database.MigrateAnonIdSha265Hash(db) } + // uuid migration (2022-02-11) + if !db.Migrator().HasColumn(&lnbits.User{}, "uuid") { + // first we need to auto migrate the user. This will create uuid column + err = db.AutoMigrate(&lnbits.User{}) + if err != nil { + panic(err) + } + log.Info("Running UUID database migrations ...") + // run the migration on uuid + err = database.MigrateUUIDSha265Hash(db) + } + // todo -- add more database field migrations here in the future return err } @@ -209,7 +221,7 @@ func UpdateUserRecord(user *lnbits.User, bot TipBot) error { if user.AnonIDSha256 == "" { debugStack() user.AnonIDSha256 = str.AnonIdSha256(user) - log.Errorf("[UpdateUserRecord] AnonIDSha256 empty! Setting to: %s", user.AnonID) + log.Errorf("[UpdateUserRecord] AnonIDSha256 empty! Setting to: %s", user.AnonIDSha256) } // TODO -- Remove this after empty anon id bug is identified if user.AnonID == "" { @@ -217,6 +229,12 @@ func UpdateUserRecord(user *lnbits.User, bot TipBot) error { user.AnonID = fmt.Sprint(str.Int32Hash(user.ID)) log.Errorf("[UpdateUserRecord] AnonID empty! Setting to: %s", user.AnonID) } + // TODO -- Remove this after empty anon id bug is identified + if user.UUID == "" { + debugStack() + user.UUID = str.UUIDSha256(user) + log.Errorf("[UpdateUserRecord] UUID empty! Setting to: %s", user.UUID) + } tx := bot.Database.Save(user) if tx.Error != nil { diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index f9bf3ed3..3048a6aa 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -331,13 +331,13 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) }() // build faucet message - inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, GetUserStrMd(inlineFaucet.From.Telegram), inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) + inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, GetUserStr(inlineFaucet.From.Telegram), inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) memo := inlineFaucet.Memo if len(memo) > 0 { inlineFaucet.Message = inlineFaucet.Message + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetAppendMemo"), memo) } if inlineFaucet.UserNeedsWallet { - inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) + inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStr(bot.Telegram.Me)) } // update message log.Infoln(inlineFaucet.Message) diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index bd45249a..8ba1faf4 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -95,6 +95,18 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con payParams := &LnurlPayState{LNURLPayParams: params.(lnurl.LNURLPayParams)} log.Infof("[LNURL-p] %s", payParams.LNURLPayParams.Callback) bot.tryDeleteMessage(statusMsg) + + // display the metadata image from the first LNURL-p response + if len(payParams.LNURLPayParams.Metadata.Image.Bytes) > 0 { + bot.trySendMessage(m.Sender, &tb.Photo{ + File: tb.File{FileReader: bytes.NewReader(payParams.LNURLPayParams.Metadata.Image.Bytes)}, + Caption: fmt.Sprintf("%s", payParams.LNURLPayParams.Metadata.Description)}) + } else if len(payParams.LNURLPayParams.Metadata.Description) > 0 { + // display the metadata text from the first LNURL-p response + // if there was no photo in the last step + bot.trySendMessage(m.Sender, fmt.Sprintf("`%s`", payParams.LNURLPayParams.Metadata.Description)) + } + // ask whether to make payment bot.lnurlPayHandler(ctx, m, *payParams) case lnurl.LNURLWithdrawResponse: @@ -128,7 +140,7 @@ func (bot *TipBot) UserGetAnonLightningAddress(user *lnbits.User) (string, error } func UserGetLNURL(user *lnbits.User) (string, error) { - name := fmt.Sprint(user.AnonIDSha256) + name := fmt.Sprint(user.UUID) callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, name) log.Debugf("[lnurlReceiveHandler] %s's LNURL: %s", GetUserStr(user.Telegram), callback) @@ -158,7 +170,7 @@ func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) (conte } bot.trySendMessage(m.Sender, Translate(ctx, "lnurlReceiveInfoText")) - // send the lnurl data to user + // send the lnurl QR code bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", lnurlEncode)}) return ctx, nil } diff --git a/internal/telegram/photo.go b/internal/telegram/photo.go index 136cab60..589ecf69 100644 --- a/internal/telegram/photo.go +++ b/internal/telegram/photo.go @@ -1,16 +1,21 @@ package telegram import ( + "bytes" "context" + "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "image" "image/jpeg" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/pkg/lightning" "github.com/makiuchi-d/gozxing" "github.com/makiuchi-d/gozxing/qrcode" + "github.com/nfnt/resize" + log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v2" ) @@ -79,3 +84,73 @@ func (bot *TipBot) photoHandler(ctx context.Context, m *tb.Message) (context.Con } return ctx, nil } + +// DownloadProfilePicture downloads a profile picture from Telegram. +// This is a public function because it is used in another package (lnurl) +func DownloadProfilePicture(telegram *tb.Bot, user *tb.User) ([]byte, error) { + photo, err := ProfilePhotosOf(telegram, user) + if err != nil { + log.Errorf("[DownloadProfilePicture] %v", err) + return nil, err + } + if len(photo) == 0 { + log.Error("[DownloadProfilePicture] No profile picture found") + return nil, err + } + buf := new(bytes.Buffer) + reader, err := telegram.GetFile(&photo[0].File) + if err != nil { + log.Errorf("[DownloadProfilePicture] %v", err) + return nil, err + } + img, err := jpeg.Decode(reader) + if err != nil { + log.Errorf("[DownloadProfilePicture] %v", err) + return nil, err + } + + // resize image + img = resize.Thumbnail(100, 100, img, resize.Lanczos3) + + err = jpeg.Encode(buf, img, nil) + return buf.Bytes(), nil +} + +var BotProfilePicture []byte + +// downloadMyProfilePicture downloads the profile picture of the bot +// and saves it in `BotProfilePicture` +func (bot *TipBot) downloadMyProfilePicture() error { + picture, err := DownloadProfilePicture(bot.Telegram, bot.Telegram.Me) + if err != nil { + log.Errorf("[downloadMyProfilePicture] %v", err) + return err + } + BotProfilePicture = picture + return nil +} + +// ProfilePhotosOf returns list of profile pictures for a user. +func ProfilePhotosOf(bot *tb.Bot, user *tb.User) ([]tb.Photo, error) { + params := map[string]interface { + }{ + "user_id": user.Recipient(), + "limit": 1, + } + + data, err := bot.Raw("getUserProfilePhotos", params) + if err != nil { + return nil, err + } + + var resp struct { + Result struct { + Count int `json:"total_count"` + Photos []tb.Photo `json:"photos"` + } + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + return resp.Result.Photos, nil +} diff --git a/internal/telegram/start.go b/internal/telegram/start.go index 8b8d803c..5e0de372 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -103,8 +103,11 @@ func (bot TipBot) createWallet(user *lnbits.User) error { return err } user.Wallet = &wallet[0] + user.AnonID = fmt.Sprint(str.Int32Hash(user.ID)) user.AnonIDSha256 = str.AnonIdSha256(user) + user.UUID = str.UUIDSha256(user) + user.Initialized = false user.CreatedAt = time.Now() err = UpdateUserRecord(user, bot) From 24e321e5eda087e2591fadf76e87a99fda49a0f9 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 11 Feb 2022 09:47:40 +0000 Subject: [PATCH 213/541] bot pic (#303) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/lnurl/lnurl.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 23e0076b..4f273a7d 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -131,7 +131,7 @@ func (w Lnurl) getMetaDataCached(username string) lnurl.Metadata { } // save into cache - runtime.IgnoreError(w.cache.Set(key, metadata, &store.Options{Expiration: 12 * time.Hour})) + runtime.IgnoreError(w.cache.Set(key, metadata, &store.Options{Expiration: 30 * time.Minute})) return metadata } @@ -280,7 +280,7 @@ func (w Lnurl) metaData(username string) lnurl.Metadata { func addImageToMetaData(tb *tb.Bot, metadata *lnurl.Metadata, username string, user *tb.User) { metadata.Image.Ext = "jpeg" - // if the username is anonymous, add the bot's avatar + // if the username is anonymous, add the bot's picture if isAnonUsername(username) { metadata.Image.Bytes = telegram.BotProfilePicture return @@ -289,7 +289,9 @@ func addImageToMetaData(tb *tb.Bot, metadata *lnurl.Metadata, username string, u // if the user has a profile picture, add it picture, err := telegram.DownloadProfilePicture(tb, user) if err != nil { - log.Errorf("[LNURL] Couldn't download user %s's profile picture: %v", username, err) + log.Debugf("[LNURL] Couldn't download user %s's profile picture: %v", username, err) + // in case the user has no image, use bot's picture + metadata.Image.Bytes = telegram.BotProfilePicture return } metadata.Image.Bytes = picture From fa46f06f2efa720a4428d38d7c2e6114f27e9e8c Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 11 Feb 2022 09:52:57 +0000 Subject: [PATCH 214/541] resize image (#304) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/photo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/photo.go b/internal/telegram/photo.go index 589ecf69..b35b4092 100644 --- a/internal/telegram/photo.go +++ b/internal/telegram/photo.go @@ -110,7 +110,7 @@ func DownloadProfilePicture(telegram *tb.Bot, user *tb.User) ([]byte, error) { } // resize image - img = resize.Thumbnail(100, 100, img, resize.Lanczos3) + img = resize.Thumbnail(160, 160, img, resize.Lanczos3) err = jpeg.Encode(buf, img, nil) return buf.Bytes(), nil From 373b4a97524bb1508a95b4d7de85927561a2f794 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Fri, 11 Feb 2022 12:16:46 +0100 Subject: [PATCH 215/541] payerdata receive (#305) --- internal/lnurl/lnurl.go | 36 +++++++++++++++++++++++++++++++++--- internal/telegram/invoice.go | 9 ++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 4f273a7d..adc2de86 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -3,6 +3,7 @@ package lnurl import ( "crypto/sha256" "encoding/hex" + "encoding/json" "fmt" "net/http" "net/url" @@ -41,6 +42,7 @@ type Invoice struct { CreatedAt time.Time `json:"created_at"` Paid bool `json:"paid"` PaidAt time.Time `json:"paid_at"` + From string `json:"from"` } type Lnurl struct { telegram *tb.Bot @@ -89,7 +91,16 @@ func (w Lnurl) Handle(writer http.ResponseWriter, request *http.Request) { api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Comment is too long")) return } - response, err = w.serveLNURLpSecond(username, int64(amount), comment) + + // payer data + payerdata := request.FormValue("payerdata") + var payerData lnurl.PayerDataValues + err := json.Unmarshal([]byte(payerdata), &payerData) + if err != nil { + api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't parse payerdata %v", err)) + } + + response, err = w.serveLNURLpSecond(username, int64(amount), comment, payerData) } // check if error was returned from first or second handlers if err != nil { @@ -155,11 +166,16 @@ func (w Lnurl) serveLNURLpFirst(username string) (*lnurl.LNURLPayParams, error) MaxSendable: MaxSendable, EncodedMetadata: metadata.Encode(), CommentAllowed: CommentAllowed, + PayerData: &lnurl.PayerDataSpec{ + FreeName: &lnurl.PayerDataItemSpec{}, + LightningAddress: &lnurl.PayerDataItemSpec{}, + Email: &lnurl.PayerDataItemSpec{}, + }, }, nil } // serveLNURLpSecond serves the second LNURL response with the payment request with the correct description hash -func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment string) (*lnurl.LNURLPayValues, error) { +func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment string, payerData lnurl.PayerDataValues) (*lnurl.LNURLPayValues, error) { log.Infof("[LNURL] Serving invoice for user %s", username) if amount_msat < MinSendable || amount_msat > MaxSendable { // amount is not ok @@ -225,13 +241,14 @@ func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment str PaymentHash: invoice.PaymentHash, Amount: amount_msat / 1000, } - // save lnurl invoice struct for later use (will hold the comment or other metdata for a notification when paid) + // save lnurl invoice struct for later use (will hold the comment or other metadata for a notification when paid) runtime.IgnoreError(w.buntdb.Set( Invoice{ Invoice: invoiceStruct, User: user, Comment: comment, CreatedAt: time.Now(), + From: extractSenderFromPayerdata(payerData), })) // save the invoice Event that will be loaded when the invoice is paid and trigger the comment display callback runtime.IgnoreError(w.buntdb.Set( @@ -325,3 +342,16 @@ func findUser(database *gorm.DB, username string) (*lnbits.User, *gorm.DB) { } return user, tx } + +func extractSenderFromPayerdata(payer lnurl.PayerDataValues) string { + if payer.LightningAddress != "" { + return payer.LightningAddress + } + if payer.Email != "" { + return payer.Email + } + if payer.FreeName != "" { + return payer.FreeName + } + return "" +} diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 4ee6f2d2..211155f1 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -176,6 +176,7 @@ type LNURLInvoice struct { CreatedAt time.Time `json:"created_at"` Paid bool `json:"paid"` PaidAt time.Time `json:"paid_at"` + From string `json:"from"` } func (lnurlInvoice LNURLInvoice) Key() string { @@ -189,7 +190,13 @@ func (bot *TipBot) lnurlReceiveEvent(invoiceEvent *InvoiceEvent) { log.Debugf("[lnurl-p] Received invoice for %s of %d sat.", GetUserStr(invoiceEvent.User.Telegram), tx.Amount) if err == nil { if len(tx.Comment) > 0 { - bot.trySendMessage(tx.User.Telegram, fmt.Sprintf(`✉️ %s`, str.MarkdownEscape(tx.Comment))) + if len(tx.From) == 0 { + bot.trySendMessage(tx.User.Telegram, fmt.Sprintf("✉️ %s", str.MarkdownEscape(tx.Comment))) + } else { + bot.trySendMessage(tx.User.Telegram, fmt.Sprintf("✉️ From `%s`: %s", tx.From, str.MarkdownEscape(tx.Comment))) + } + } else if len(tx.From) > 0 { + bot.trySendMessage(tx.User.Telegram, fmt.Sprintf("From %s", str.MarkdownEscape(tx.From))) } } } From 00b07998e662575757acdce5ff1351d4503e58f3 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 11 Feb 2022 11:40:18 +0000 Subject: [PATCH 216/541] Payerdata receive (#306) * payerdata receive * more Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/lnurl/lnurl.go | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index adc2de86..9a49b353 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -97,7 +97,9 @@ func (w Lnurl) Handle(writer http.ResponseWriter, request *http.Request) { var payerData lnurl.PayerDataValues err := json.Unmarshal([]byte(payerdata), &payerData) if err != nil { - api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't parse payerdata %v", err)) + // api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't parse payerdata: %v", err)) + fmt.Errorf("[handleLnUrl] Couldn't parse payerdata: %v", err) + fmt.Errorf("[handleLnUrl] payerdata: %v", payerdata) } response, err = w.serveLNURLpSecond(username, int64(amount), comment, payerData) @@ -216,10 +218,16 @@ func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment str // the same description_hash needs to be built in the second request metadata := w.getMetaDataCached(username) - descriptionHash, err := w.descriptionHash(metadata) + payerDataString, err := json.Marshal(payerData) if err != nil { return nil, err } + + descriptionHash, err := w.descriptionHash(metadata, string(payerDataString)) + if err != nil { + return nil, err + } + invoice, err := user.Wallet.Invoice( lnbits.InvoiceParams{ Amount: amount_msat / 1000, @@ -268,9 +276,16 @@ func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment str } // descriptionHash is the SHA256 hash of the metadata -func (w Lnurl) descriptionHash(metadata lnurl.Metadata) (string, error) { - hash := sha256.Sum256([]byte(metadata.Encode())) - hashString := hex.EncodeToString(hash[:]) +func (w Lnurl) descriptionHash(metadata lnurl.Metadata, payerData string) (string, error) { + var hashString string + var hash [32]byte + if len(payerData) == 0 { + hash = sha256.Sum256([]byte(metadata.Encode())) + hashString = hex.EncodeToString(hash[:]) + } else { + hash = sha256.Sum256([]byte(metadata.Encode() + payerData)) + hashString = hex.EncodeToString(hash[:]) + } return hashString, nil } From 6e7886e02991a2d7d76bbabf16d7a76d500ce703 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Fri, 11 Feb 2022 12:56:30 +0100 Subject: [PATCH 217/541] payerdata hash fix (#307) --- internal/lnurl/lnurl.go | 14 ++++++++++---- internal/telegram/invoice.go | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 9a49b353..28560321 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -218,12 +218,18 @@ func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment str // the same description_hash needs to be built in the second request metadata := w.getMetaDataCached(username) - payerDataString, err := json.Marshal(payerData) - if err != nil { - return nil, err + var payerDataByte []byte + var err error + if payerData.Email != "" || payerData.LightningAddress != "" || payerData.FreeName != "" { + payerDataByte, err = json.Marshal(payerData) + if err != nil { + return nil, err + } + } else { + payerDataByte = []byte("") } - descriptionHash, err := w.descriptionHash(metadata, string(payerDataString)) + descriptionHash, err := w.descriptionHash(metadata, string(payerDataByte)) if err != nil { return nil, err } diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 211155f1..81915bad 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -196,7 +196,7 @@ func (bot *TipBot) lnurlReceiveEvent(invoiceEvent *InvoiceEvent) { bot.trySendMessage(tx.User.Telegram, fmt.Sprintf("✉️ From `%s`: %s", tx.From, str.MarkdownEscape(tx.Comment))) } } else if len(tx.From) > 0 { - bot.trySendMessage(tx.User.Telegram, fmt.Sprintf("From %s", str.MarkdownEscape(tx.From))) + bot.trySendMessage(tx.User.Telegram, fmt.Sprintf("From `%s`", str.MarkdownEscape(tx.From))) } } } From 205e75d2d6cb066a5c15a428c17f86540fbf1846 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Fri, 11 Feb 2022 13:24:03 +0100 Subject: [PATCH 218/541] markdown fix (#308) --- internal/telegram/faucet.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 3048a6aa..98283685 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -81,7 +81,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User // // check for memo in command memo := GetMemoFromCommand(text, 3) - inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), perUserAmount, GetUserStrMd(sender), amount, amount, 0, nTotal, MakeProgressbar(amount, amount)) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), perUserAmount, GetUserStr(sender), amount, amount, 0, nTotal, MakeProgressbar(amount, amount)) if len(memo) > 0 { inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineFaucetAppendMemo"), memo) } @@ -330,6 +330,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback bot.trySendMessage(to.Telegram, to_message) bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) }() + // build faucet message inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, GetUserStr(inlineFaucet.From.Telegram), inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) memo := inlineFaucet.Memo From afcbca28f2772b52565a9174ae10034687b8a840 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Mon, 14 Feb 2022 19:44:34 +0000 Subject: [PATCH 219/541] faucet md (#309) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/faucet.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 98283685..4b3ddeff 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -81,7 +81,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User // // check for memo in command memo := GetMemoFromCommand(text, 3) - inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), perUserAmount, GetUserStr(sender), amount, amount, 0, nTotal, MakeProgressbar(amount, amount)) + inlineMessage := fmt.Sprintf(Translate(ctx, "inlineFaucetMessage"), perUserAmount, GetUserStrMd(sender), amount, amount, 0, nTotal, MakeProgressbar(amount, amount)) if len(memo) > 0 { inlineMessage = inlineMessage + fmt.Sprintf(Translate(ctx, "inlineFaucetAppendMemo"), memo) } @@ -332,7 +332,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback }() // build faucet message - inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, GetUserStr(inlineFaucet.From.Telegram), inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) + inlineFaucet.Message = fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetMessage"), inlineFaucet.PerUserAmount, GetUserStrMd(inlineFaucet.From.Telegram), inlineFaucet.RemainingAmount, inlineFaucet.Amount, inlineFaucet.NTaken, inlineFaucet.NTotal, MakeProgressbar(inlineFaucet.RemainingAmount, inlineFaucet.Amount)) memo := inlineFaucet.Memo if len(memo) > 0 { inlineFaucet.Message = inlineFaucet.Message + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetAppendMemo"), memo) From 0dcf4bd8454b5289b7033445a39bf1ea4ee98db2 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 17 Feb 2022 16:14:25 +0000 Subject: [PATCH 220/541] fix the tipjar and send a summary (#312) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/faucet.go | 1 - internal/telegram/tipjar.go | 25 +++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 4b3ddeff..ff396d0f 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -262,7 +262,6 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if c.Message != nil && c.Message.Chat != nil { log.Infof("[faucet] Link: https://t.me/c/%s/%d", strconv.FormatInt(c.Message.Chat.ID, 10)[4:], c.Message.ID) } - // release faucet no matter what if from.Telegram.ID == to.Telegram.ID { log.Debugf("[faucet] %s is the owner faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) diff --git a/internal/telegram/tipjar.go b/internal/telegram/tipjar.go index 8f910d9c..7abae3b6 100644 --- a/internal/telegram/tipjar.go +++ b/internal/telegram/tipjar.go @@ -248,6 +248,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback to := inlineTipjar.To if !inlineTipjar.Active { log.Errorf(fmt.Sprintf("[tipjar] tipjar %s inactive.", inlineTipjar.ID)) + bot.tryEditMessage(c.Message, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) return ctx, errors.Create(errors.NotActiveError) } @@ -263,6 +264,9 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback return ctx, errors.Create(errors.UnknownError) } } + + defer inlineTipjar.Set(inlineTipjar, bot.Bunt) + if inlineTipjar.GivenAmount < inlineTipjar.Amount { toUserStrMd := GetUserStrMd(to.Telegram) fromUserStrMd := GetUserStrMd(from.Telegram) @@ -321,6 +325,10 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback inlineTipjar.NGiven, ) bot.tryEditMessage(c.Message, inlineTipjar.Message) + // send update to tipjar creator + if inlineTipjar.Active && inlineTipjar.To.Telegram.ID != 0 { + bot.trySendMessage(inlineTipjar.To.Telegram, listTipjarGivers(inlineTipjar)) + } inlineTipjar.Active = false } return ctx, nil @@ -341,7 +349,24 @@ func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback return ctx, errors.Create(errors.UnknownError) } bot.tryEditMessage(c.Message, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) + + // send update to tipjar creator + if inlineTipjar.Active && inlineTipjar.To.Telegram.ID != 0 { + bot.trySendMessage(inlineTipjar.To.Telegram, listTipjarGivers(inlineTipjar)) + } + // set the inlineTipjar inactive inlineTipjar.Active = false return ctx, inlineTipjar.Set(inlineTipjar, bot.Bunt) } + +func listTipjarGivers(inlineTipjar *InlineTipjar) string { + var from_str string + from_str = fmt.Sprintf("🍯 *Tipjar summary*\n\nMemo: %s\nCapacity: %d sat\nGivers: %d\nCollected: %d sat\n\n*Givers:*\n\n", inlineTipjar.Memo, inlineTipjar.Amount, inlineTipjar.NGiven, inlineTipjar.GivenAmount) + from_str += "```\n" + for _, from := range inlineTipjar.From { + from_str += fmt.Sprintf("%s\n", GetUserStr(from.Telegram)) + } + from_str += "```" + return from_str +} From a5d8c5d7e576732b486faa932fd2b9b49c4eb49d Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Fri, 25 Feb 2022 14:11:05 +0300 Subject: [PATCH 221/541] lnurl amounts same (#315) --- internal/telegram/lnurl-pay.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index a712d3d3..6bfed02d 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -86,7 +86,10 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) // now we actualy check whether the amount was in the command and if not, ask for it - if amount_err != nil || amount < 1 { + if lnurlPayState.LNURLPayParams.MinSendable == lnurlPayState.LNURLPayParams.MaxSendable { + amount = lnurlPayState.LNURLPayParams.MaxSendable / 1000 + lnurlPayState.Amount = amount * 1000 // save as mSat + } else if amount_err != nil || amount < 1 { // // no amount was entered, set user state and ask for amount bot.askForAmount(ctx, id, "LnurlPayState", lnurlPayState.LNURLPayParams.MinSendable, lnurlPayState.LNURLPayParams.MaxSendable, m.Text) return From 1179800a4b9ce2c7fccd410718931833339cf771 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sat, 26 Feb 2022 08:53:15 +0100 Subject: [PATCH 222/541] =?UTF-8?q?=F0=9F=AA=84=20fix=20withdraw=20state?= =?UTF-8?q?=20handler=20(#316)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/telegram/lnurl-withdraw.go | 30 ++++++++++++++--------------- internal/telegram/lnurl.go | 2 +- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 1afe5779..3bc5e96f 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -58,19 +58,17 @@ func (bot *TipBot) editSingleButton(ctx context.Context, m *tb.Message, message // lnurlWithdrawHandler is invoked when the first lnurl response was a lnurl-withdraw response // at this point, the user hans't necessarily entered an amount yet -func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, withdrawParams LnurlWithdrawState) { +func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, withdrawParams *LnurlWithdrawState) { user := LoadUser(ctx) if user.Wallet == nil { return } // object that holds all information about the send payment id := fmt.Sprintf("lnurlw-%d-%s", m.Sender.ID, RandStringRunes(5)) - LnurlWithdrawState := LnurlWithdrawState{ - Base: storage.New(storage.ID(id)), - From: user, - LNURLWithdrawResponse: withdrawParams.LNURLWithdrawResponse, - LanguageCode: ctx.Value("publicLanguageCode").(string), - } + + withdrawParams.Base = storage.New(storage.ID(id)) + withdrawParams.From = user + withdrawParams.LanguageCode = ctx.Value("publicLanguageCode").(string) // first we check whether an amount is present in the command amount, amount_err := decodeAmountFromCommand(m.Text) @@ -78,36 +76,36 @@ func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, with // amount is already present in the command, i.e., /lnurl // amount not in allowed range from LNURL if amount_err == nil && - (int64(amount) > (LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable/1000) || int64(amount) < (LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable/1000)) && - (LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable != 0 && LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable != 0) { // only if max and min are set + (int64(amount) > (withdrawParams.LNURLWithdrawResponse.MaxWithdrawable/1000) || int64(amount) < (withdrawParams.LNURLWithdrawResponse.MinWithdrawable/1000)) && + (withdrawParams.LNURLWithdrawResponse.MaxWithdrawable != 0 && withdrawParams.LNURLWithdrawResponse.MinWithdrawable != 0) { // only if max and min are set err := fmt.Errorf("amount not in range") log.Warnf("[lnurlWithdrawHandler] Error: %s", err.Error()) - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable/1000, LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable/1000)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), withdrawParams.LNURLWithdrawResponse.MinWithdrawable/1000, withdrawParams.LNURLWithdrawResponse.MaxWithdrawable/1000)) ResetUserState(user, bot) return } // if no amount is entered, and if only one amount is possible, we use it - if amount_err != nil && LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable == LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable { - amount = int64(LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable / 1000) + if amount_err != nil && withdrawParams.LNURLWithdrawResponse.MaxWithdrawable == withdrawParams.LNURLWithdrawResponse.MinWithdrawable { + amount = int64(withdrawParams.LNURLWithdrawResponse.MaxWithdrawable / 1000) amount_err = nil } // set also amount in the state of the user - LnurlWithdrawState.Amount = amount * 1000 // save as mSat + withdrawParams.Amount = amount * 1000 // save as mSat // add result to persistent struct - runtime.IgnoreError(LnurlWithdrawState.Set(LnurlWithdrawState, bot.Bunt)) + runtime.IgnoreError(withdrawParams.Set(withdrawParams, bot.Bunt)) // now we actualy check whether the amount was in the command and if not, ask for it if amount_err != nil || amount < 1 { // // no amount was entered, set user state and ask for amount - bot.askForAmount(ctx, id, "LnurlWithdrawState", LnurlWithdrawState.LNURLWithdrawResponse.MinWithdrawable, LnurlWithdrawState.LNURLWithdrawResponse.MaxWithdrawable, m.Text) + bot.askForAmount(ctx, id, "LnurlWithdrawState", withdrawParams.LNURLWithdrawResponse.MinWithdrawable, withdrawParams.LNURLWithdrawResponse.MaxWithdrawable, m.Text) return } // We need to save the pay state in the user state so we can load the payment in the next handler - paramsJson, err := json.Marshal(LnurlWithdrawState) + paramsJson, err := json.Marshal(withdrawParams) if err != nil { log.Errorf("[lnurlWithdrawHandler] Error: %s", err.Error()) // bot.trySendMessage(m.Sender, err.Error()) diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 8ba1faf4..e21fdaf4 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -110,7 +110,7 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con bot.lnurlPayHandler(ctx, m, *payParams) case lnurl.LNURLWithdrawResponse: - withdrawParams := LnurlWithdrawState{LNURLWithdrawResponse: params.(lnurl.LNURLWithdrawResponse)} + withdrawParams := &LnurlWithdrawState{LNURLWithdrawResponse: params.(lnurl.LNURLWithdrawResponse)} log.Infof("[LNURL-w] %s", withdrawParams.LNURLWithdrawResponse.Callback) bot.tryDeleteMessage(statusMsg) bot.lnurlWithdrawHandler(ctx, m, withdrawParams) From a9f6539eafe037c02ad6c3e3488bc767703f2b0c Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 27 Feb 2022 13:33:04 +0100 Subject: [PATCH 223/541] =?UTF-8?q?=F0=9F=AA=84=20fix=20LNURL=20states=20r?= =?UTF-8?q?edundant=20initialization=20(#318)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/telegram/lnurl-auth.go | 18 +++++++--------- internal/telegram/lnurl-pay.go | 37 +++++++++++++++------------------ internal/telegram/lnurl.go | 4 ++-- 3 files changed, 27 insertions(+), 32 deletions(-) diff --git a/internal/telegram/lnurl-auth.go b/internal/telegram/lnurl-auth.go index 6747a0c8..30422cb8 100644 --- a/internal/telegram/lnurl-auth.go +++ b/internal/telegram/lnurl-auth.go @@ -37,19 +37,17 @@ type LnurlAuthState struct { // lnurlPayHandler1 is invoked when the first lnurl response was a lnurlpay response // at this point, the user hans't necessarily entered an amount yet -func (bot *TipBot) lnurlAuthHandler(ctx context.Context, m *tb.Message, authParams LnurlAuthState) (context.Context, error) { +func (bot *TipBot) lnurlAuthHandler(ctx context.Context, m *tb.Message, authParams *LnurlAuthState) (context.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) } // object that holds all information about the send payment id := fmt.Sprintf("lnurlauth-%d-%s", m.Sender.ID, RandStringRunes(5)) - lnurlAuthState := &LnurlAuthState{ - Base: storage.New(storage.ID(id)), - From: user, - LNURLAuthParams: authParams.LNURLAuthParams, - LanguageCode: ctx.Value("publicLanguageCode").(string), - } + authParams.Base = storage.New(storage.ID(id)) + authParams.From = user + authParams.LanguageCode = ctx.Value("publicLanguageCode").(string) + // // // create inline buttons btnAuth = paymentConfirmationMenu.Data(Translate(ctx, "loginButtonMessage"), "confirm_login", id) btnCancelAuth = paymentConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_login", id) @@ -59,15 +57,15 @@ func (bot *TipBot) lnurlAuthHandler(ctx context.Context, m *tb.Message, authPara btnAuth, btnCancelAuth), ) - lnurlAuthState.Message = bot.trySendMessageEditable(m.Chat, + authParams.Message = bot.trySendMessageEditable(m.Chat, fmt.Sprintf(Translate(ctx, "confirmLnurlAuthMessager"), - lnurlAuthState.LNURLAuthParams.CallbackURL.Host, + authParams.LNURLAuthParams.CallbackURL.Host, ), paymentConfirmationMenu, ) // save to bunt - runtime.IgnoreError(lnurlAuthState.Set(lnurlAuthState, bot.Bunt)) + runtime.IgnoreError(authParams.Set(authParams, bot.Bunt)) return ctx, nil } diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index 6bfed02d..f85e4ca1 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -34,19 +34,16 @@ type LnurlPayState struct { // lnurlPayHandler1 is invoked when the first lnurl response was a lnurlpay response // at this point, the user hans't necessarily entered an amount yet -func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams LnurlPayState) { +func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams *LnurlPayState) { user := LoadUser(ctx) if user.Wallet == nil { return } // object that holds all information about the send payment id := fmt.Sprintf("lnurlp-%d-%s", m.Sender.ID, RandStringRunes(5)) - lnurlPayState := &LnurlPayState{ - Base: storage.New(storage.ID(id)), - From: user, - LNURLPayParams: payParams.LNURLPayParams, - LanguageCode: ctx.Value("publicLanguageCode").(string), - } + payParams.Base = storage.New(storage.ID(id)) + payParams.From = user + payParams.LanguageCode = ctx.Value("publicLanguageCode").(string) // first we check whether an amount is present in the command amount, amount_err := decodeAmountFromCommand(m.Text) @@ -61,42 +58,42 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams // check if memo is present in command memo := GetMemoFromCommand(m.Text, memoStartsAt) // shorten memo to allowed length - if len(memo) > int(lnurlPayState.LNURLPayParams.CommentAllowed) { - memo = memo[:lnurlPayState.LNURLPayParams.CommentAllowed] + if len(memo) > int(payParams.LNURLPayParams.CommentAllowed) { + memo = memo[:payParams.LNURLPayParams.CommentAllowed] } if len(memo) > 0 { - lnurlPayState.Comment = memo + payParams.Comment = memo } // amount is already present in the command, i.e., /lnurl // amount not in allowed range from LNURL if amount_err == nil && - (int64(amount) > (lnurlPayState.LNURLPayParams.MaxSendable/1000) || int64(amount) < (lnurlPayState.LNURLPayParams.MinSendable/1000)) && - (lnurlPayState.LNURLPayParams.MaxSendable != 0 && lnurlPayState.LNURLPayParams.MinSendable != 0) { // only if max and min are set + (int64(amount) > (payParams.LNURLPayParams.MaxSendable/1000) || int64(amount) < (payParams.LNURLPayParams.MinSendable/1000)) && + (payParams.LNURLPayParams.MaxSendable != 0 && payParams.LNURLPayParams.MinSendable != 0) { // only if max and min are set err := fmt.Errorf("amount not in range") log.Warnf("[lnurlPayHandler] Error: %s", err.Error()) - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), lnurlPayState.LNURLPayParams.MinSendable/1000, lnurlPayState.LNURLPayParams.MaxSendable/1000)) + bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), payParams.LNURLPayParams.MinSendable/1000, payParams.LNURLPayParams.MaxSendable/1000)) ResetUserState(user, bot) return } // set also amount in the state of the user - lnurlPayState.Amount = amount * 1000 // save as mSat + payParams.Amount = amount * 1000 // save as mSat // add result to persistent struct - runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) + runtime.IgnoreError(payParams.Set(payParams, bot.Bunt)) // now we actualy check whether the amount was in the command and if not, ask for it - if lnurlPayState.LNURLPayParams.MinSendable == lnurlPayState.LNURLPayParams.MaxSendable { - amount = lnurlPayState.LNURLPayParams.MaxSendable / 1000 - lnurlPayState.Amount = amount * 1000 // save as mSat + if payParams.LNURLPayParams.MinSendable == payParams.LNURLPayParams.MaxSendable { + amount = payParams.LNURLPayParams.MaxSendable / 1000 + payParams.Amount = amount * 1000 // save as mSat } else if amount_err != nil || amount < 1 { // // no amount was entered, set user state and ask for amount - bot.askForAmount(ctx, id, "LnurlPayState", lnurlPayState.LNURLPayParams.MinSendable, lnurlPayState.LNURLPayParams.MaxSendable, m.Text) + bot.askForAmount(ctx, id, "LnurlPayState", payParams.LNURLPayParams.MinSendable, payParams.LNURLPayParams.MaxSendable, m.Text) return } // We need to save the pay state in the user state so we can load the payment in the next handler - paramsJson, err := json.Marshal(lnurlPayState) + paramsJson, err := json.Marshal(payParams) if err != nil { log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) // bot.trySendMessage(m.Sender, err.Error()) diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index e21fdaf4..15069b88 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -89,7 +89,7 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con authParams := &LnurlAuthState{LNURLAuthParams: params.(lnurl.LNURLAuthParams)} log.Infof("[LNURL-auth] %s", authParams.LNURLAuthParams.Callback) bot.tryDeleteMessage(statusMsg) - return bot.lnurlAuthHandler(ctx, m, *authParams) + return bot.lnurlAuthHandler(ctx, m, authParams) case lnurl.LNURLPayParams: payParams := &LnurlPayState{LNURLPayParams: params.(lnurl.LNURLPayParams)} @@ -107,7 +107,7 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con bot.trySendMessage(m.Sender, fmt.Sprintf("`%s`", payParams.LNURLPayParams.Metadata.Description)) } // ask whether to make payment - bot.lnurlPayHandler(ctx, m, *payParams) + bot.lnurlPayHandler(ctx, m, payParams) case lnurl.LNURLWithdrawResponse: withdrawParams := &LnurlWithdrawState{LNURLWithdrawResponse: params.(lnurl.LNURLWithdrawResponse)} From 71d8aa202c3d68afad62ccbab29e661c0985ad53 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Thu, 17 Mar 2022 00:21:28 +0100 Subject: [PATCH 224/541] Group invite link management (#310) * group invite link * link * works * ticketinvoiceevent type * Add invoice event interface * add AssertEventType * generalize AssertEventType invocation * wip: payment button * all new * callback * fix TicketEvent Key() * invoiceEvent in ticketEvent * fix errors * add payment button * commission * minimum price * remove redundant ticket event data * commission * fixes * update balance * notify user if ticket < 10 * commission rounding error * refactor * refactor * /join * add /join handler * fix handler * fix /join * fix handler * fix /join interceptor * add startUserInterceptor * delete qr * no test blyad * fix defer * message Co-authored-by: gohumble --- config.yaml.example | 1 + internal/config.go | 1 + internal/lnbits/webhook/webhook.go | 8 +- internal/telegram/bot.go | 4 +- internal/telegram/database.go | 13 +- internal/telegram/groups.go | 455 ++++++++++++++++++++++++++++ internal/telegram/handler.go | 49 +++ internal/telegram/inline_receive.go | 3 +- internal/telegram/interceptor.go | 23 ++ internal/telegram/invoice.go | 60 +++- internal/telegram/pay.go | 4 +- internal/telegram/telegram.go | 45 +++ translations/en.toml | 1 + 13 files changed, 647 insertions(+), 20 deletions(-) create mode 100644 internal/telegram/groups.go diff --git a/config.yaml.example b/config.yaml.example index e94b56b3..af668175 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -17,3 +17,4 @@ database: buntdb_path: "data/bunt.db" transactions_path: "data/transactions.db" shop_buntdb_path: "data/shop.db" + groupsdb_path: "data/groups.db" diff --git a/internal/config.go b/internal/config.go index 797208a5..9c31b453 100644 --- a/internal/config.go +++ b/internal/config.go @@ -34,6 +34,7 @@ type DatabaseConfiguration struct { ShopBuntDbPath string `yaml:"shop_buntdb_path"` BuntDbPath string `yaml:"buntdb_path"` TransactionsPath string `yaml:"transactions_path"` + GroupsDbPath string `yaml:"groupsdb_path"` } type LnbitsConfiguration struct { diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index 211b9c71..473186eb 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -108,8 +108,12 @@ func (w *Server) receive(writer http.ResponseWriter, request *http.Request) { log.Errorln(err) } else { // do something with the event - if c := telegram.InvoiceCallback[txInvoiceEvent.Callback]; c != nil { - c(txInvoiceEvent) + if c := telegram.InvoiceCallback[txInvoiceEvent.Callback]; c.Function != nil { + if err := telegram.AssertEventType(txInvoiceEvent, c.Type); err != nil { + log.Errorln(err) + return + } + c.Function(txInvoiceEvent) return } } diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 0aff8af9..4eb2b1f7 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -27,6 +27,7 @@ type TipBot struct { Database *gorm.DB Bunt *storage.DB ShopBunt *storage.DB + GroupsDb *gorm.DB logger *gorm.DB Telegram *tb.Bot Client *lnbits.Client @@ -47,7 +48,7 @@ func NewBot() TipBot { gocacheClient := gocache.New(5*time.Minute, 10*time.Minute) gocacheStore := store.NewGoCache(gocacheClient, nil) // create sqlite databases - db, txLogger := AutoMigration() + db, txLogger, groupsDb := AutoMigration() limiter.Start() return TipBot{ Database: db, @@ -57,6 +58,7 @@ func NewBot() TipBot { ShopBunt: createBunt(internal.Configuration.Database.ShopBuntDbPath), Telegram: newTelegramBot(), Cache: Cache{GoCacheStore: gocacheStore}, + GroupsDb: groupsDb, } } diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 9c31f34e..6c0ce367 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -86,7 +86,7 @@ func ColumnMigrationTasks(db *gorm.DB) error { return err } -func AutoMigration() (db *gorm.DB, txLogger *gorm.DB) { +func AutoMigration() (db *gorm.DB, txLogger *gorm.DB, groupsDb *gorm.DB) { orm, err := gorm.Open(sqlite.Open(internal.Configuration.Database.DbPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) if err != nil { panic("Initialize orm failed.") @@ -108,7 +108,16 @@ func AutoMigration() (db *gorm.DB, txLogger *gorm.DB) { if err != nil { panic(err) } - return orm, txLogger + + groupsDb, err = gorm.Open(sqlite.Open(internal.Configuration.Database.GroupsDbPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) + if err != nil { + panic("Initialize orm failed.") + } + err = groupsDb.AutoMigrate(&Group{}) + if err != nil { + panic(err) + } + return orm, txLogger, groupsDb } func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.User, error) { diff --git a/internal/telegram/groups.go b/internal/telegram/groups.go new file mode 100644 index 00000000..dfebafb1 --- /dev/null +++ b/internal/telegram/groups.go @@ -0,0 +1,455 @@ +package telegram + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/i18n" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/LightningTipBot/LightningTipBot/internal/str" + log "github.com/sirupsen/logrus" + "github.com/skip2/go-qrcode" + tb "gopkg.in/lightningtipbot/telebot.v2" +) + +type Ticket struct { + Price int64 `json:"price"` + Memo string `json:"memo"` + Creator *lnbits.User `gorm:"embedded;embeddedPrefix:creator_"` + Cut int `json:"cut"` // Percent to cut from ticket price +} +type Group struct { + Name string `json:"name"` + Title string `json:"title"` + ID int64 `json:"id" gorm:"primaryKey"` + Owner *tb.User `gorm:"embedded;embeddedPrefix:owner_"` + // Chat *tb.Chat `gorm:"embedded;embeddedPrefix:chat_"` + Ticket *Ticket `gorm:"embedded;embeddedPrefix:ticket_"` +} +type CreateChatInviteLink struct { + ChatID int64 `json:"chat_id"` + Name string `json:"name"` + ExpiryDate int `json:"expiry_date"` + MemberLimit int `json:"member_limit"` + CreatesJoinRequest bool `json:"creates_join_request"` +} +type Creator struct { + ID int64 `json:"id"` + IsBot bool `json:"is_bot"` + Firstname string `json:"first_name"` + Username string `json:"username"` +} +type Result struct { + InviteLink string `json:"invite_link"` + Name string `json:"name"` + Creator Creator `json:"creator"` + CreatesJoinRequest bool `json:"creates_join_request"` + IsPrimary bool `json:"is_primary"` + IsRevoked bool `json:"is_revoked"` +} +type ChatInviteLink struct { + Ok bool `json:"ok"` + Result Result `json:"result"` +} + +type TicketEvent struct { + *storage.Base + *InvoiceEvent + Group *Group `gorm:"embedded;embeddedPrefix:group_"` +} + +func (ticketEvent TicketEvent) Type() EventType { + return EventTypeTicketInvoice +} +func (ticketEvent TicketEvent) Key() string { + return ticketEvent.Base.ID +} + +var ( + ticketPayConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + btnPayTicket = paymentConfirmationMenu.Data("✅ Pay", "pay_ticket") +) + +var ( + groupAddGroupHelpMessage = "📖 Oops, that didn't work. Please try again.\nUsage: `/group add []`\nExample: `/group add TheBestBitcoinGroup 1000`" + grouJoinGroupHelpMessage = "📖 Oops, that didn't work. Please try again.\nUsage: `/join `\nExample: `/join TheBestBitcoinGroup`" + groupClickToJoinMessage = "🎟 [Click here](%s) 👈 to join `%s`." + groupInvoiceMemo = "Ticket for group %s" + groupPayInvoiceMessage = "🎟 To join the group %s, pay the invoice above." + groupBotIsNotAdminMessage = "🚫 Oops, that didn't work. You must make me admin and grant me rights to invite users." + groupNameExists = "🚫 A group with this name already exists. Please choose a different name." + groupAddedMessage = "🎟 Tickets for group `%s` added.\nAlias: `%s` Price: %d sat\n\nTo request a ticket for this group, start a private chat with %s and write `/join %s`." + groupNotFoundMessage = "🚫 Could not find a group with this name." + groupReceiveTicketInvoiceCommission = "🎟 You received *%d sat* (excl. %d sat commission) for a ticket for group `%s` paid by user %s." + groupReceiveTicketInvoice = "🎟 You received *%d sat* for a ticket for group `%s` paid by user %s." +) + +func (bot TipBot) groupHandler(ctx context.Context, m *tb.Message) (context.Context, error) { + splits := strings.Split(m.Text, " ") + if len(splits) == 1 { + return ctx, nil + } else if len(splits) > 1 { + if splits[1] == "join" { + return bot.groupRequestJoinHandler(ctx, m) + } + if splits[1] == "add" { + return bot.addGroupHandler(ctx, m) + } + if splits[1] == "remove" { + // todo -- implement this + // return bot.addGroupHandler(ctx, m) + } + } + return ctx, nil +} + +// groupRequestJoinHandler sends a payment request to the user who wants to join a group +func (bot TipBot) groupRequestJoinHandler(ctx context.Context, m *tb.Message) (context.Context, error) { + user := LoadUser(ctx) + // // reply only in private message + if m.Chat.Type != tb.ChatPrivate { + return ctx, fmt.Errorf("not private chat") + } + splits := strings.Split(m.Text, " ") + // if the command was /group join + splitIdx := 1 + // we also have the simpler command /join that can be used + // also by users who don't have an account with the bot yet + if splits[0] == "/join" { + splitIdx = 0 + } + if len(splits) != splitIdx+2 || len(m.Text) > 100 { + bot.trySendMessage(m.Chat, grouJoinGroupHelpMessage) + return ctx, nil + } + groupName := strings.ToLower(splits[splitIdx+1]) + + group := &Group{} + tx := bot.GroupsDb.Where("name = ? COLLATE NOCASE", groupName).First(group) + if tx.Error != nil { + bot.trySendMessage(m.Chat, groupNotFoundMessage) + return ctx, fmt.Errorf("group not found") + } + + // create tickets + id := fmt.Sprintf("ticket:%d", group.ID) + invoiceEvent := &InvoiceEvent{ + Base: storage.New(storage.ID(id)), + User: group.Ticket.Creator, + LanguageCode: ctx.Value("publicLanguageCode").(string), + Payer: user, + Chat: &tb.Chat{ID: group.ID}, + CallbackData: id, + } + ticketEvent := &TicketEvent{ + Base: storage.New(storage.ID(id)), + InvoiceEvent: invoiceEvent, + Group: group, + } + // if no price is set, then we don't need to pay + if group.Ticket.Price == 0 { + // save ticketevent for later + runtime.IgnoreError(ticketEvent.Set(ticketEvent, bot.Bunt)) + bot.groupGetInviteLinkHandler(invoiceEvent) + return ctx, nil + } + + // create an invoice + memo := fmt.Sprintf(groupInvoiceMemo, groupName) + var err error + invoiceEvent, err = bot.createGroupTicketInvoice(ctx, user, group, memo, InvoiceCallbackGroupTicket, id) + if err != nil { + errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err.Error()) + bot.trySendMessage(user.Telegram, Translate(ctx, "errorTryLaterMessage")) + log.Errorln(errmsg) + return ctx, err + } + + ticketEvent.InvoiceEvent = invoiceEvent + // save ticketevent for later + defer ticketEvent.Set(ticketEvent, bot.Bunt) + + // // if the user has enough balance, we send him a payment button + balance, err := bot.GetUserBalance(user) + if err != nil { + errmsg := fmt.Sprintf("[/group] Error: Could not get user balance: %s", err.Error()) + log.Errorln(errmsg) + bot.trySendMessage(m.Sender, Translate(ctx, "errorTryLaterMessage")) + return ctx, errors.New(errors.GetBalanceError, err) + } + if balance >= group.Ticket.Price { + return bot.groupSendPayButtonHandler(ctx, m, *ticketEvent) + } + + // otherwise we send a payment request + + // create qr code + qr, err := qrcode.Encode(invoiceEvent.PaymentRequest, qrcode.Medium, 256) + if err != nil { + errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err.Error()) + bot.trySendMessage(user.Telegram, Translate(ctx, "errorTryLaterMessage")) + log.Errorln(errmsg) + return ctx, err + } + ticketEvent.Message = bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoiceEvent.PaymentRequest)}) + bot.trySendMessage(m.Sender, fmt.Sprintf(groupPayInvoiceMessage, groupName)) + return ctx, nil +} + +func (bot *TipBot) groupSendPayButtonHandler(ctx context.Context, m *tb.Message, ticket TicketEvent) (context.Context, error) { + // object that holds all information about the send payment + // // // create inline buttons + btnPayTicket := ticketPayConfirmationMenu.Data(Translate(ctx, "payButtonMessage"), "pay_ticket", ticket.Base.ID) + ticketPayConfirmationMenu.Inline( + ticketPayConfirmationMenu.Row( + btnPayTicket), + ) + confirmText := fmt.Sprintf(Translate(ctx, "confirmPayInvoiceMessage"), ticket.Group.Ticket.Price) + if len(ticket.Group.Ticket.Memo) > 0 { + confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmPayAppendMemo"), str.MarkdownEscape(ticket.Group.Ticket.Memo)) + } + bot.trySendMessageEditable(m.Chat, confirmText, ticketPayConfirmationMenu) + return ctx, nil +} + +func (bot *TipBot) groupConfirmPayButtonHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { + tx := TicketEvent{Base: storage.New(storage.ID(c.Data))} + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) + sn, err := tx.Get(tx, bot.Bunt) + // immediatelly set intransaction to block duplicate calls + if err != nil { + log.Errorf("[groupConfirmPayButtonHandler] %s", err.Error()) + return ctx, err + } + ticketEvent := sn.(TicketEvent) + + // onnly the correct user can press + if ticketEvent.Payer.Telegram.ID != c.Sender.ID { + return ctx, errors.Create(errors.UnknownError) + } + if !ticketEvent.Active { + log.Errorf("[confirmPayHandler] send not active anymore") + bot.tryEditMessage(c.Message, i18n.Translate(ticketEvent.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) + bot.tryDeleteMessage(c.Message) + return ctx, errors.Create(errors.NotActiveError) + } + defer ticketEvent.Set(ticketEvent, bot.Bunt) + + user := LoadUser(ctx) + if user.Wallet == nil { + bot.tryDeleteMessage(c.Message) + return ctx, errors.Create(errors.UserNoWalletError) + } + + log.Infof("[/pay] Attempting %s's invoice %s (%d sat)", GetUserStr(user.Telegram), ticketEvent.ID, ticketEvent.Group.Ticket.Price) + // // pay invoice + _, err = user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: ticketEvent.Invoice.PaymentRequest}, bot.Client) + if err != nil { + errmsg := fmt.Sprintf("[/pay] Could not pay invoice of %s: %s", GetUserStr(user.Telegram), err) + err = fmt.Errorf(i18n.Translate(ticketEvent.LanguageCode, "invoiceUndefinedErrorMessage")) + bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(ticketEvent.LanguageCode, "invoicePaymentFailedMessage"), err.Error()), &tb.ReplyMarkup{}) + log.Errorln(errmsg) + return ctx, err + } + + // update the message and remove the button + bot.tryEditMessage(c.Message, i18n.Translate(ticketEvent.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) + return ctx, nil +} + +// groupGetInviteLinkHandler is called when the invoice is paid and sends a one-time group invite link to the payer +func (bot *TipBot) groupGetInviteLinkHandler(event Event) { + invoiceEvent := event.(*InvoiceEvent) + // take a cut + // amount_bot := int64(ticketEvent.Group.Ticket.Price * int64(ticketEvent.Group.Ticket.Cut) / 100) + + log.Infof(invoiceEvent.CallbackData) + ticketEvent := &TicketEvent{Base: storage.New(storage.ID(invoiceEvent.CallbackData))} + err := bot.Bunt.Get(ticketEvent) + if err != nil { + log.Errorf("[groupGetInviteLinkHandler] %s", err.Error()) + return + } + + log.Infof("[groupGetInviteLinkHandler] group: %d", ticketEvent.Chat.ID) + params := map[string]interface { + }{ + "chat_id": ticketEvent.Group.ID, // must be the chat ID of the group + "name": fmt.Sprintf("%s link for %s", GetUserStr(bot.Telegram.Me), GetUserStr(ticketEvent.Payer.Telegram)), // the name of the invite link + "member_limit": 1, // only one user can join with this link + // "expire_date": time.Now().AddDate(0, 0, 1), // expiry date of the invite link, add one day + // "creates_join_request": false, // True, if users joining the chat via the link need to be approved by chat administrators. If True, member_limit can't be specified + } + data, err := bot.Telegram.Raw("createChatInviteLink", params) + if err != nil { + return + } + + var resp ChatInviteLink + if err := json.Unmarshal(data, &resp); err != nil { + return + } + + if ticketEvent.Message != nil { + bot.tryDeleteMessage(ticketEvent.Message) + // do balance check for keyboard update + _, err = bot.GetUserBalance(ticketEvent.Payer) + if err != nil { + errmsg := fmt.Sprintf("could not get balance of user %s", GetUserStr(ticketEvent.Payer.Telegram)) + log.Errorln(errmsg) + } + bot.trySendMessage(ticketEvent.Payer.Telegram, i18n.Translate(ticketEvent.LanguageCode, "invoicePaidText")) + } + + bot.trySendMessage(ticketEvent.Payer.Telegram, fmt.Sprintf(groupClickToJoinMessage, resp.Result.InviteLink, ticketEvent.Group.Title)) + + // take a commission + ticketSat := ticketEvent.Group.Ticket.Price + if ticketEvent.Group.Ticket.Price > 10 { + me, err := GetUser(bot.Telegram.Me, *bot) + if err != nil { + log.Errorf("[groupGetInviteLinkHandler] Could not get bot user from DB: %s", err.Error()) + return + } + commissionSat := ticketEvent.Group.Ticket.Price * int64(ticketEvent.Group.Ticket.Cut) / 100 + ticketSat = ticketEvent.Group.Ticket.Price - commissionSat + invoice, err := me.Wallet.Invoice( + lnbits.InvoiceParams{ + Out: false, + Amount: commissionSat, + Memo: "Ticket commission for group " + ticketEvent.Group.Title, + Webhook: internal.Configuration.Lnbits.WebhookServer}, + bot.Client) + if err != nil { + errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err.Error()) + log.Errorln(errmsg) + return + } + _, err = ticketEvent.User.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoice.PaymentRequest}, bot.Client) + if err != nil { + errmsg := fmt.Sprintf("[groupGetInviteLinkHandler] Could not pay commission of %s: %s", GetUserStr(ticketEvent.User.Telegram), err) + err = fmt.Errorf(i18n.Translate(ticketEvent.LanguageCode, "invoiceUndefinedErrorMessage")) + log.Errorln(errmsg) + return + } + // do balance check for keyboard update + _, err = bot.GetUserBalance(ticketEvent.User) + if err != nil { + errmsg := fmt.Sprintf("could not get balance of user %s", GetUserStr(ticketEvent.Payer.Telegram)) + log.Errorln(errmsg) + } + bot.trySendMessage(ticketEvent.User.Telegram, fmt.Sprintf(groupReceiveTicketInvoiceCommission, ticketSat, commissionSat, ticketEvent.Group.Title, GetUserStr(ticketEvent.Payer.Telegram))) + } else { + bot.trySendMessage(ticketEvent.User.Telegram, fmt.Sprintf(groupReceiveTicketInvoice, ticketSat, ticketEvent.Group.Title, GetUserStr(ticketEvent.Payer.Telegram))) + } + return +} + +func (bot TipBot) addGroupHandler(ctx context.Context, m *tb.Message) (context.Context, error) { + if m.Chat.Type == tb.ChatPrivate { + return ctx, fmt.Errorf("not in group") + } + // parse command "/group add []" + splits := strings.Split(m.Text, " ") + if len(splits) < 3 || len(m.Text) > 100 { + bot.trySendMessage(m.Chat, groupAddGroupHelpMessage) + return ctx, nil + } + groupName := strings.ToLower(splits[2]) + + user := LoadUser(ctx) + // check if the user is the owner of the group + if !bot.isOwner(m.Chat, user.Telegram) { + return ctx, fmt.Errorf("not owner") + } + + if !bot.isAdminAndCanInviteUsers(m.Chat, bot.Telegram.Me) { + bot.trySendMessage(m.Chat, groupBotIsNotAdminMessage) + return ctx, fmt.Errorf("bot is not admin") + } + + // check if the group with this name is already in db + // only if a group with this name is owned by this user, it can be overwritten + group := &Group{} + tx := bot.GroupsDb.Where("name = ? COLLATE NOCASE", groupName).First(group) + if tx.Error == nil { + // if it is already added, check if this user is the admin + if user.Telegram.ID != group.Owner.ID || group.ID != m.Chat.ID { + bot.trySendMessage(m.Chat, groupNameExists) + return ctx, fmt.Errorf("not owner") + } + } + + amount := int64(0) // default amount is zero + if amount_str, err := getArgumentFromCommand(m.Text, 3); err == nil { + amount, err = getAmount(amount_str) + if err != nil { + bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) + return ctx, err + } + } + + ticket := &Ticket{ + Price: amount, + Memo: "Ticket for group " + groupName, + Creator: user, + Cut: 10, + } + + group = &Group{ + Name: groupName, + Title: m.Chat.Title, + ID: m.Chat.ID, + Owner: user.Telegram, + Ticket: ticket, + } + + bot.GroupsDb.Save(group) + log.Infof("[group] Ticket of %d sat added to group %s.", group.Ticket.Price, group.Name) + bot.trySendMessage(m.Chat, fmt.Sprintf(groupAddedMessage, str.MarkdownEscape(m.Chat.Title), group.Name, group.Ticket.Price, GetUserStrMd(bot.Telegram.Me), group.Name)) + + return ctx, nil +} + +func (bot *TipBot) createGroupTicketInvoice(ctx context.Context, payer *lnbits.User, group *Group, memo string, callback int, callbackData string) (*InvoiceEvent, error) { + invoice, err := group.Ticket.Creator.Wallet.Invoice( + lnbits.InvoiceParams{ + Out: false, + Amount: group.Ticket.Price, + Memo: memo, + Webhook: internal.Configuration.Lnbits.WebhookServer}, + bot.Client) + if err != nil { + errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err.Error()) + log.Errorln(errmsg) + return &InvoiceEvent{}, err + } + + // save the invoice event + id := fmt.Sprintf("invoice:%s", invoice.PaymentHash) + invoiceEvent := &InvoiceEvent{ + Base: storage.New(storage.ID(id)), + Invoice: &Invoice{PaymentHash: invoice.PaymentHash, + PaymentRequest: invoice.PaymentRequest, + Amount: group.Ticket.Price, + Memo: memo}, + User: group.Ticket.Creator, + Callback: callback, + CallbackData: callbackData, + LanguageCode: ctx.Value("publicLanguageCode").(string), + Payer: payer, + Chat: &tb.Chat{ID: group.ID}, + } + // add result to persistent struct + runtime.IgnoreError(invoiceEvent.Set(invoiceEvent, bot.Bunt)) + return invoiceEvent, nil +} diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 549d3382..813bc0a0 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -377,6 +377,55 @@ func (bot TipBot) getHandler() []Handler { }, }, }, + // group tickets + { + Endpoints: []interface{}{"/group"}, + Handler: bot.groupHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.logMessageInterceptor, + bot.requireUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, + { + Endpoints: []interface{}{"/join"}, + Handler: bot.groupRequestJoinHandler, + Interceptor: &Interceptor{ + Type: MessageInterceptor, + Before: []intercept.Func{ + bot.requirePrivateChatInterceptor, + bot.localizerInterceptor, + bot.logMessageInterceptor, + bot.startUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, + { + Endpoints: []interface{}{&btnPayTicket}, + Handler: bot.groupConfirmPayButtonHandler, + Interceptor: &Interceptor{ + Type: CallbackInterceptor, + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.requireUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, { Endpoints: []interface{}{tb.OnPhoto}, Handler: bot.photoHandler, diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index c6670128..aa685336 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -299,7 +299,8 @@ func (bot *TipBot) inlineReceiveInvoice(ctx context.Context, c *tb.Callback, inl log.Printf("[/invoice] Incvoice created. User: %s, amount: %d sat.", GetUserStr(inlineReceive.To.Telegram), inlineReceive.Amount) } -func (bot *TipBot) inlineReceiveEvent(invoiceEvent *InvoiceEvent) { +func (bot *TipBot) inlineReceiveEvent(event Event) { + invoiceEvent := event.(*InvoiceEvent) bot.tryDeleteMessage(invoiceEvent.InvoiceMessage) bot.notifyInvoiceReceivedEvent(invoiceEvent) bot.finishInlineReceiveHandler(nil, &tb.Callback{Data: string(invoiceEvent.CallbackData)}) diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 515da7c8..62ce3a1e 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -110,6 +110,29 @@ func (bot TipBot) requireUserInterceptor(ctx context.Context, i interface{}) (co return nil, errors.Create(errors.InvalidTypeError) } +// startUserInterceptor will invoke /start if user not exists. +func (bot TipBot) startUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { + ctx, err := bot.loadUserInterceptor(ctx, i) + if err != nil { + // user banned + return ctx, err + } + // load user + u := ctx.Value("user") + // check user nil + if u != nil { + user := u.(*lnbits.User) + // check wallet nil or !initialized + if user.Wallet == nil || !user.Initialized { + ctx, err = bot.startHandler(ctx, i.(*tb.Message)) + if err != nil { + return ctx, err + } + return ctx, nil + } + } + return ctx, nil +} func (bot TipBot) loadUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { ctx, _ = bot.requireUserInterceptor(ctx, i) // if user is banned, also loadUserInterceptor will return an error diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 81915bad..28e72c10 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -8,6 +8,7 @@ import ( "time" "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/LightningTipBot/LightningTipBot/internal" @@ -21,15 +22,21 @@ import ( tb "gopkg.in/lightningtipbot/telebot.v2" ) -type InvoiceEventCallback map[int]func(*InvoiceEvent) +type InvoiceEventCallback map[int]EventHandler + +type EventHandler struct { + Function func(event Event) + Type EventType +} var InvoiceCallback InvoiceEventCallback func initInvoiceEventCallbacks(bot *TipBot) { InvoiceCallback = InvoiceEventCallback{ - InvoiceCallbackGeneric: bot.notifyInvoiceReceivedEvent, - InvoiceCallbackInlineReceive: bot.inlineReceiveEvent, - InvoiceCallbackLNURLPayReceive: bot.lnurlReceiveEvent, + InvoiceCallbackGeneric: EventHandler{Function: bot.notifyInvoiceReceivedEvent, Type: EventTypeInvoice}, + InvoiceCallbackInlineReceive: EventHandler{Function: bot.inlineReceiveEvent, Type: EventTypeInvoice}, + InvoiceCallbackLNURLPayReceive: EventHandler{Function: bot.lnurlReceiveEvent, Type: EventTypeInvoice}, + InvoiceCallbackGroupTicket: EventHandler{Function: bot.groupGetInviteLinkHandler, Type: EventTypeInvoice}, } } @@ -39,8 +46,23 @@ const ( InvoiceCallbackGeneric = iota + 1 InvoiceCallbackInlineReceive InvoiceCallbackLNURLPayReceive + InvoiceCallbackGroupTicket ) +const ( + EventTypeInvoice EventType = "invoice" + EventTypeTicketInvoice EventType = "ticket-invoice" +) + +type EventType string + +func AssertEventType(event Event, eventType EventType) error { + if event.Type() != eventType { + return fmt.Errorf("invalid event type") + } + return nil +} + type Invoice struct { PaymentHash string `json:"payment_hash"` PaymentRequest string `json:"payment_request"` @@ -49,12 +71,23 @@ type Invoice struct { } type InvoiceEvent struct { *Invoice - User *lnbits.User `json:"user"` - Message *tb.Message `json:"message"` - InvoiceMessage *tb.Message `json:"invoice_message"` - LanguageCode string `json:"languagecode"` - Callback int `json:"func"` - CallbackData string `json:"callbackdata"` + *storage.Base + User *lnbits.User `json:"user"` // the user that is being paid + Message *tb.Message `json:"message,omitempty"` // the message that the invoice replies to + InvoiceMessage *tb.Message `json:"invoice_message,omitempty"` // the message that displays the invoice + LanguageCode string `json:"languagecode"` // language code of the user + Callback int `json:"func"` // which function to call if the invoice is paid + CallbackData string `json:"callbackdata"` // add some data for the callback + Chat *tb.Chat `json:"chat,omitempty"` // if invoice is supposed to be sent to a particular chat + Payer *lnbits.User `json:"payer,omitempty"` // if a particular user is supposed to pay this +} + +func (invoiceEvent InvoiceEvent) Type() EventType { + return EventTypeInvoice +} + +type Event interface { + Type() EventType } func (invoiceEvent InvoiceEvent) Key() string { @@ -158,7 +191,8 @@ func (bot *TipBot) createInvoiceWithEvent(ctx context.Context, user *lnbits.User return invoiceEvent, nil } -func (bot *TipBot) notifyInvoiceReceivedEvent(invoiceEvent *InvoiceEvent) { +func (bot *TipBot) notifyInvoiceReceivedEvent(event Event) { + invoiceEvent := event.(*InvoiceEvent) // do balance check for keyboard update _, err := bot.GetUserBalance(invoiceEvent.User) if err != nil { @@ -183,7 +217,9 @@ func (lnurlInvoice LNURLInvoice) Key() string { return fmt.Sprintf("lnurl-p:%s", lnurlInvoice.PaymentHash) } -func (bot *TipBot) lnurlReceiveEvent(invoiceEvent *InvoiceEvent) { +func (bot *TipBot) lnurlReceiveEvent(event Event) { + invoiceEvent := event.(*InvoiceEvent) + bot.notifyInvoiceReceivedEvent(invoiceEvent) tx := &LNURLInvoice{Invoice: &Invoice{PaymentHash: invoiceEvent.PaymentHash}} err := bot.Bunt.Get(tx) diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index f018d884..92af9196 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -106,7 +106,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte return ctx, errors.Create(errors.InvalidSyntaxError) } // send warning that the invoice might fail due to missing fee reserve - if float64(amount) > float64(balance)*0.99 { + if float64(amount) > float64(balance)*0.98 { bot.trySendMessage(m.Sender, Translate(ctx, "feeReserveMessage")) } @@ -118,7 +118,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte log.Infof("[/pay] Invoice entered. User: %s, amount: %d sat.", userStr, amount) // object that holds all information about the send payment - id := fmt.Sprintf("pay-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) + id := fmt.Sprintf("pay:%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) // // // create inline buttons payButton := paymentConfirmationMenu.Data(Translate(ctx, "payButtonMessage"), "confirm_pay", id) diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index ed3d2539..5b1d95d0 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -137,3 +137,48 @@ func isAdminAndCanDelete(members []tb.ChatMember, me *tb.User) bool { } return false } + +// isOwner will check if user is owner of group +func (bot *TipBot) isOwner(chat *tb.Chat, me *tb.User) bool { + members, err := bot.Telegram.AdminsOf(chat) + if err != nil { + log.Warnln(err.Error()) + return false + } + for _, admin := range members { + if admin.User.ID == me.ID && admin.Role == "creator" { + return true + } + } + return false +} + +// isAdmin will check if user is admin in a group +func (bot *TipBot) isAdmin(chat *tb.Chat, me *tb.User) bool { + members, err := bot.Telegram.AdminsOf(chat) + if err != nil { + log.Warnln(err.Error()) + return false + } + for _, admin := range members { + if admin.User.ID == me.ID { + return true + } + } + return false +} + +// isAdmin will check if user is admin in a group +func (bot *TipBot) isAdminAndCanInviteUsers(chat *tb.Chat, me *tb.User) bool { + members, err := bot.Telegram.AdminsOf(chat) + if err != nil { + log.Warnln(err.Error()) + return false + } + for _, admin := range members { + if admin.User.ID == me.ID { + return admin.CanInviteUsers + } + } + return false +} diff --git a/translations/en.toml b/translations/en.toml index dfdce937..ad3ca0e3 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -189,6 +189,7 @@ invoiceHelpText = """📖 Oops, that didn't work. %s *Usage:* `/invoice []` *Example:* `/invoice 1000 Thank you!`""" +invoicePaidText = """✅ Invoice paid.""" # PAY From 4a93019428567f1497dcb2e322fc0541196a5d10 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Thu, 17 Mar 2022 00:29:06 +0100 Subject: [PATCH 225/541] ticket type fix (#321) --- internal/telegram/groups.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/telegram/groups.go b/internal/telegram/groups.go index dfebafb1..e0841fe0 100644 --- a/internal/telegram/groups.go +++ b/internal/telegram/groups.go @@ -221,7 +221,7 @@ func (bot *TipBot) groupSendPayButtonHandler(ctx context.Context, m *tb.Message, } func (bot *TipBot) groupConfirmPayButtonHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { - tx := TicketEvent{Base: storage.New(storage.ID(c.Data))} + tx := &TicketEvent{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) @@ -230,7 +230,7 @@ func (bot *TipBot) groupConfirmPayButtonHandler(ctx context.Context, c *tb.Callb log.Errorf("[groupConfirmPayButtonHandler] %s", err.Error()) return ctx, err } - ticketEvent := sn.(TicketEvent) + ticketEvent := sn.(*TicketEvent) // onnly the correct user can press if ticketEvent.Payer.Telegram.ID != c.Sender.ID { From 9895498237b2a0a37540dfc26fd908a9f5e6ae31 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Thu, 17 Mar 2022 09:36:47 +0100 Subject: [PATCH 226/541] lower fee (#322) --- internal/telegram/groups.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/groups.go b/internal/telegram/groups.go index e0841fe0..43828f10 100644 --- a/internal/telegram/groups.go +++ b/internal/telegram/groups.go @@ -402,7 +402,7 @@ func (bot TipBot) addGroupHandler(ctx context.Context, m *tb.Message) (context.C Price: amount, Memo: "Ticket for group " + groupName, Creator: user, - Cut: 10, + Cut: 2, } group = &Group{ From 7085b4dbf68ac9047c4525e87ed8d8a95964a532 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Thu, 17 Mar 2022 10:22:47 +0100 Subject: [PATCH 227/541] base_fee (#323) --- internal/telegram/groups.go | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/internal/telegram/groups.go b/internal/telegram/groups.go index 43828f10..5a868949 100644 --- a/internal/telegram/groups.go +++ b/internal/telegram/groups.go @@ -21,10 +21,13 @@ import ( ) type Ticket struct { - Price int64 `json:"price"` - Memo string `json:"memo"` - Creator *lnbits.User `gorm:"embedded;embeddedPrefix:creator_"` - Cut int `json:"cut"` // Percent to cut from ticket price + Price int64 `json:"price"` + Memo string `json:"memo"` + Creator *lnbits.User `gorm:"embedded;embeddedPrefix:creator_"` + Cut int64 `json:"cut"` // Percent to cut from ticket price + BaseFee int64 `json:"base_fee"` + CutCheap int64 `json:"cut_cheap"` // Percent to cut from ticket price + BaseFeeCheap int64 `json:"base_fee_cheap"` } type Group struct { Name string `json:"name"` @@ -314,13 +317,20 @@ func (bot *TipBot) groupGetInviteLinkHandler(event Event) { // take a commission ticketSat := ticketEvent.Group.Ticket.Price - if ticketEvent.Group.Ticket.Price > 10 { + if ticketEvent.Group.Ticket.Price > 20 { me, err := GetUser(bot.Telegram.Me, *bot) if err != nil { log.Errorf("[groupGetInviteLinkHandler] Could not get bot user from DB: %s", err.Error()) return } - commissionSat := ticketEvent.Group.Ticket.Price * int64(ticketEvent.Group.Ticket.Cut) / 100 + + // 2% cut + 100 sat base fee + commissionSat := ticketEvent.Group.Ticket.Price*ticketEvent.Group.Ticket.Cut/100 + ticketEvent.Group.Ticket.BaseFee + if ticketEvent.Group.Ticket.Price <= 1000 { + // if < 1000, then 10% cut + 10 sat base fee + commissionSat = ticketEvent.Group.Ticket.Price*ticketEvent.Group.Ticket.CutCheap/100 + ticketEvent.Group.Ticket.BaseFeeCheap + } + ticketSat = ticketEvent.Group.Ticket.Price - commissionSat invoice, err := me.Wallet.Invoice( lnbits.InvoiceParams{ @@ -399,10 +409,13 @@ func (bot TipBot) addGroupHandler(ctx context.Context, m *tb.Message) (context.C } ticket := &Ticket{ - Price: amount, - Memo: "Ticket for group " + groupName, - Creator: user, - Cut: 2, + Price: amount, + Memo: "Ticket for group " + groupName, + Creator: user, + Cut: 2, + BaseFee: 100, + CutCheap: 10, + BaseFeeCheap: 10, } group = &Group{ From 23db317c0da3617a598c6b82477402b451a8cbe8 Mon Sep 17 00:00:00 2001 From: bassim Date: Sun, 27 Mar 2022 11:26:40 +0200 Subject: [PATCH 228/541] Update en.toml (#326) fixed typos in photoQrNotRecognizedMessage --- translations/en.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translations/en.toml b/translations/en.toml index ad3ca0e3..1580584c 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -224,7 +224,7 @@ donateHelpText = """📖 Oops, that didn't work. %s # PHOTO -photoQrNotRecognizedMessage = """🚫 Could not regocognize a Lightning invoice or an LNURL. Try to center the QR code, crop the photo, or zoom in.""" +photoQrNotRecognizedMessage = """🚫 Could not recognize a Lightning invoice or a LNURL. Try to center the QR code, crop the photo, or zoom in.""" photoQrRecognizedMessage = """✅ QR code: `%s`""" From bf643f103d2102a281b16492d68e135ffd66fcd8 Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Sun, 27 Mar 2022 22:19:38 +0200 Subject: [PATCH 229/541] =?UTF-8?q?=F0=9F=AA=84=20update=20telebot=20v3=20?= =?UTF-8?q?=F0=9F=A4=96=20(#325)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚧 minor changes to all handlers... * 🚧 passing callback to tryEditStack * 🚧 edit and delete callback messages * 🚧 refactor again * 🚧 uncomment this * 🚧 using ctx.Message().Text * 🚧 refactor * 🚧 refactor * 🚧 uncomment * context not queruy * 🚧 update forked lightningtipbot * 🚧 reset ArticleResult * fix inline receive * readd shop * remove chat from shopView Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- go.mod | 2 +- go.sum | 23 +++ internal/lnbits/types.go | 2 +- internal/lnbits/webhook/webhook.go | 2 +- internal/lnurl/lnurl.go | 2 +- internal/rate/limiter.go | 2 +- internal/telegram/amounts.go | 27 ++-- internal/telegram/balance.go | 17 ++- internal/telegram/bot.go | 2 +- internal/telegram/buttons.go | 6 +- internal/telegram/database.go | 2 +- internal/telegram/donate.go | 15 +- internal/telegram/edit.go | 2 +- internal/telegram/faucet.go | 73 +++++----- internal/telegram/files.go | 11 +- internal/telegram/groups.go | 52 ++++--- internal/telegram/handler.go | 179 ++++++++++------------- internal/telegram/help.go | 33 +++-- internal/telegram/inline_query.go | 67 +++++---- internal/telegram/inline_receive.go | 69 ++++----- internal/telegram/inline_send.go | 39 ++--- internal/telegram/intercept/callback.go | 77 ---------- internal/telegram/intercept/context.go | 84 +++++++++++ internal/telegram/intercept/message.go | 77 ---------- internal/telegram/intercept/query.go | 77 ---------- internal/telegram/interceptor.go | 186 ++++++++++-------------- internal/telegram/invoice.go | 8 +- internal/telegram/link.go | 9 +- internal/telegram/lnurl-auth.go | 15 +- internal/telegram/lnurl-pay.go | 21 +-- internal/telegram/lnurl-withdraw.go | 35 +++-- internal/telegram/lnurl.go | 19 ++- internal/telegram/message.go | 2 +- internal/telegram/pay.go | 81 ++++++----- internal/telegram/photo.go | 21 +-- internal/telegram/send.go | 100 ++++++------- internal/telegram/shop.go | 145 +++++++++++------- internal/telegram/shop_helpers.go | 39 +++-- internal/telegram/start.go | 25 ++-- internal/telegram/state.go | 5 +- internal/telegram/telegram.go | 2 +- internal/telegram/text.go | 23 +-- internal/telegram/tip.go | 10 +- internal/telegram/tipjar.go | 52 ++++--- internal/telegram/tooltip.go | 2 +- internal/telegram/tooltip_test.go | 2 +- internal/telegram/transaction.go | 2 +- internal/telegram/users.go | 2 +- main.go | 4 +- 49 files changed, 834 insertions(+), 918 deletions(-) delete mode 100644 internal/telegram/intercept/callback.go create mode 100644 internal/telegram/intercept/context.go delete mode 100644 internal/telegram/intercept/message.go delete mode 100644 internal/telegram/intercept/query.go diff --git a/go.mod b/go.mod index 84420c1e..e3bc764b 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,7 @@ require ( github.com/tidwall/gjson v1.10.2 golang.org/x/text v0.3.5 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 - gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211217193303-c005cce171ac + gopkg.in/lightningtipbot/telebot.v3 v3.0.0-20220326213923-f323bb71ac8e // indirect gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.12 ) diff --git a/go.sum b/go.sum index 2ae27308..fbf01931 100644 --- a/go.sum +++ b/go.sum @@ -136,6 +136,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.10.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fiatjaf/go-lnurl v1.4.0 h1:hVFEEJD2A9D6ojEcqLyD54CM2ZJ9Tzs2jNKw/GNq52A= github.com/fiatjaf/go-lnurl v1.4.0/go.mod h1:BqA8WXAOzntF7Z3EkVO7DfP4y5rhWUmJ/Bu9KBke+rs= github.com/fiatjaf/go-lnurl v1.8.3 h1:ONQUQsXIZKkzrzax2SMUr5W0PreluhO4tct1JM0V/MA= @@ -170,10 +172,15 @@ github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1 github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/strfmt v0.19.5/go.mod h1:eftuHTlB/dI8Uq8JJOyRlieZf+WkkxUuk0dgdHXr2Qk= github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/go-redis/redis/v8 v8.8.2 h1:O/NcHqobw7SEptA0yA6up6spZVFtwE06SXM8rgLtsP8= github.com/go-redis/redis/v8 v8.8.2/go.mod h1:F7resOH5Kdug49Otu24RjHWwgK7u9AmtqWMnCV1iP5Y= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/goccy/go-yaml v1.9.5/go.mod h1:U/jl18uSupI5rdI2jmuCswEA2htH9eXfferR3KfscvA= github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= @@ -306,6 +313,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= github.com/lightninglabs/neutrino v0.11.0/go.mod h1:CuhF0iuzg9Sp2HO6ZgXgayviFTn1QHdSTJlMncK80wg= @@ -331,8 +339,13 @@ github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN github.com/makiuchi-d/gozxing v0.0.2 h1:TGSCQRXd9QL1ze1G1JE9sZBMEr6/HLx7m5ADlLUgq7E= github.com/makiuchi-d/gozxing v0.0.2/go.mod h1:Tt5nF+kNliU+5MDxqPpsFrtsWNdABQho/xdCZZVKCQc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= @@ -641,6 +654,7 @@ golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -649,6 +663,10 @@ golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= +golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= @@ -738,12 +756,16 @@ gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211207185736-224e4f70c5ad h1:HR3v gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211207185736-224e4f70c5ad/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211217193303-c005cce171ac h1:EcXyrTUaQJYiHtLQstXdW04sRH9HwH9mpaxy9Lkcomc= gopkg.in/lightningtipbot/telebot.v2 v2.4.2-0.20211217193303-c005cce171ac/go.mod h1:lUH49SRidDcQGyXPIL9tR2cN3vztcLBo47AqFUgHgIA= +gopkg.in/lightningtipbot/telebot.v3 v3.0.0-20220326213923-f323bb71ac8e h1:x58PfjVq6dbYSzM4URM9QfKDKVru1CpngxCM5pGqwbk= +gopkg.in/lightningtipbot/telebot.v3 v3.0.0-20220326213923-f323bb71ac8e/go.mod h1:kIC/Dlp2OjWhVrOkZDN4iEoCaqoac5w1UjpKY6Ftf+U= gopkg.in/macaroon-bakery.v2 v2.0.1/go.mod h1:B4/T17l+ZWGwxFSZQmlBwp25x+og7OkhETfr3S9MbIA= gopkg.in/macaroon.v2 v2.0.0/go.mod h1:+I6LnTMkm/uV5ew/0nsulNjL16SK4+C8yDmRUzHR17I= gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/telebot.v3 v3.0.0 h1:UgHIiE/RdjoDi6nf4xACM7PU3TqiPVV9vvTydCEnrTo= +gopkg.in/telebot.v3 v3.0.0/go.mod h1:7rExV8/0mDDNu9epSrDm/8j22KLaActH1Tbee6YjzWg= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 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= @@ -761,6 +783,7 @@ gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.1.4 h1:PDzwYE+sI6De2+mxAneV9Xs11+ZyKV6oxD3wDGkaNvM= gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9DYw= gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index a50ba8ed..78959526 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -9,7 +9,7 @@ import ( "github.com/btcsuite/btcd/btcec" "github.com/imroc/req" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) type Client struct { diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index 473186eb..b17e2b92 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -17,7 +17,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/gorilla/mux" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" "github.com/LightningTipBot/LightningTipBot/internal/i18n" ) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 28560321..a92e8aa8 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -12,7 +12,7 @@ import ( "time" "github.com/eko/gocache/store" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/api" diff --git a/internal/rate/limiter.go b/internal/rate/limiter.go index 74f5ff04..6851a1d8 100644 --- a/internal/rate/limiter.go +++ b/internal/rate/limiter.go @@ -7,7 +7,7 @@ import ( "sync" "golang.org/x/time/rate" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) // Limiter diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 135229dd..7241a393 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strconv" "strings" @@ -16,7 +17,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/price" "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) func getArgumentFromCommand(input string, which int) (output string, err error) { @@ -120,7 +121,7 @@ func (bot *TipBot) askForAmount(ctx context.Context, id string, eventType string // enterAmountHandler is invoked in anyTextHandler when the user needs to enter an amount // the amount is then stored as an entry in the user's stateKey in the user database // any other handler that relies on this, needs to load the resulting amount from the database -func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) enterAmountHandler(ctx intercept.Context) (intercept.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -139,10 +140,10 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) (conte return ctx, err } - amount, err := getAmount(m.Text) + amount, err := getAmount(ctx.Message().Text) if err != nil { log.Warnf("[enterAmountHandler] %s", err.Error()) - bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) + bot.trySendMessage(ctx.Message().Sender, Translate(ctx, "lnurlInvalidAmountMessage")) ResetUserState(user, bot) return ctx, err } @@ -151,7 +152,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) (conte (amount > int64(EnterAmountStateData.AmountMax/1000) || amount < int64(EnterAmountStateData.AmountMin/1000)) { // this line then checks whether the amount is in the range err = fmt.Errorf("amount not in range") log.Warnf("[enterAmountHandler] %s", err.Error()) - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), EnterAmountStateData.AmountMin/1000, EnterAmountStateData.AmountMax/1000)) + bot.trySendMessage(ctx.Sender(), fmt.Sprintf(Translate(ctx, "lnurlInvalidAmountRangeMessage"), EnterAmountStateData.AmountMin/1000, EnterAmountStateData.AmountMax/1000)) ResetUserState(user, bot) return ctx, errors.Create(errors.InvalidSyntaxError) } @@ -179,7 +180,7 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) (conte return ctx, err } SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(StateDataJson)) - return bot.lnurlPayHandlerSend(ctx, m) + return bot.lnurlPayHandlerSend(ctx) case "LnurlWithdrawState": tx := &LnurlWithdrawState{Base: storage.New(storage.ID(EnterAmountStateData.ID))} mutex.LockWithContext(ctx, tx.ID) @@ -200,21 +201,21 @@ func (bot *TipBot) enterAmountHandler(ctx context.Context, m *tb.Message) (conte return ctx, err } SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(StateDataJson)) - return bot.lnurlWithdrawHandlerWithdraw(ctx, m) + return bot.lnurlWithdrawHandlerWithdraw(ctx) case "CreateInvoiceState": - m.Text = fmt.Sprintf("/invoice %d", amount) + ctx.Message().Text = fmt.Sprintf("/invoice %d", amount) SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") - return bot.invoiceHandler(ctx, m) + return bot.invoiceHandler(ctx) case "CreateDonationState": - m.Text = fmt.Sprintf("/donate %d", amount) + ctx.Message().Text = fmt.Sprintf("/donate %d", amount) SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") - return bot.donationHandler(ctx, m) + return bot.donationHandler(ctx) case "CreateSendState": splits := strings.SplitAfterN(EnterAmountStateData.OiringalCommand, " ", 2) if len(splits) > 1 { - m.Text = fmt.Sprintf("/send %d %s", amount, splits[1]) + ctx.Message().Text = fmt.Sprintf("/send %d %s", amount, splits[1]) SetUserState(user, bot, lnbits.UserHasEnteredAmount, "") - return bot.sendHandler(ctx, m) + return bot.sendHandler(ctx) } return ctx, errors.Create(errors.InvalidSyntaxError) default: diff --git a/internal/telegram/balance.go b/internal/telegram/balance.go index def024da..1e898c45 100644 --- a/internal/telegram/balance.go +++ b/internal/telegram/balance.go @@ -1,19 +1,20 @@ package telegram import ( - "context" "fmt" "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) -func (bot *TipBot) balanceHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) balanceHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() // check and print all commands if len(m.Text) > 0 { - bot.anyTextHandler(ctx, m) + bot.anyTextHandler(ctx) } // reply only in private message @@ -28,18 +29,18 @@ func (bot *TipBot) balanceHandler(ctx context.Context, m *tb.Message) (context.C } if !user.Initialized { - return bot.startHandler(ctx, m) + return bot.startHandler(ctx) } - usrStr := GetUserStr(m.Sender) + usrStr := GetUserStr(ctx.Sender()) balance, err := bot.GetUserBalance(user) if err != nil { log.Errorf("[/balance] Error fetching %s's balance: %s", usrStr, err) - bot.trySendMessage(m.Sender, Translate(ctx, "balanceErrorMessage")) + bot.trySendMessage(ctx.Sender(), Translate(ctx, "balanceErrorMessage")) return ctx, err } log.Infof("[/balance] %s's balance: %d sat\n", usrStr, balance) - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "balanceMessage"), balance)) + bot.trySendMessage(ctx.Sender(), fmt.Sprintf(Translate(ctx, "balanceMessage"), balance)) return ctx, nil } diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index 4eb2b1f7..e324d612 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -19,7 +19,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/storage" gocache "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" "gorm.io/gorm" ) diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index 57982ee2..c842d3e4 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -6,7 +6,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) // we can't use space in the label of buttons, because string splitting will mess everything up. @@ -19,13 +19,13 @@ const ( ) var ( - mainMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + mainMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnHelpMainMenu = mainMenu.Text(MainMenuCommandHelp) btnSendMainMenu = mainMenu.Text(MainMenuCommandSend) btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) btnInvoiceMainMenu = mainMenu.Text(MainMenuCommandInvoice) - sendToMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + sendToMenu = &tb.ReplyMarkup{ResizeKeyboard: true} sendToButtons = []tb.Btn{} btnSendMenuEnter = mainMenu.Text(SendMenuCommandEnter) ) diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 6c0ce367..4d78619e 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -23,7 +23,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" "gorm.io/driver/sqlite" "gorm.io/gorm" ) diff --git a/internal/telegram/donate.go b/internal/telegram/donate.go index b4eb93b0..787a3184 100644 --- a/internal/telegram/donate.go +++ b/internal/telegram/donate.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "io" "io/ioutil" "net/http" @@ -14,7 +15,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) // PLEASE DO NOT CHANGE THE CODE IN THIS FILE @@ -34,9 +35,10 @@ func helpDonateUsage(ctx context.Context, errormsg string) string { } } -func (bot TipBot) donationHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot TipBot) donationHandler(ctx intercept.Context) (intercept.Context, error) { // check and print all commands - bot.anyTextHandler(ctx, m) + m := ctx.Message() + bot.anyTextHandler(ctx) user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -118,7 +120,8 @@ func (rot13 rot13Reader) Read(b []byte) (int, error) { return n, err } -func (bot TipBot) parseCmdDonHandler(ctx context.Context, m *tb.Message) error { +func (bot TipBot) parseCmdDonHandler(ctx intercept.Context) error { + m := ctx.Message() arg := "" if strings.HasPrefix(strings.ToLower(m.Text), "/send") { arg, _ = getArgumentFromCommand(m.Text, 2) @@ -150,7 +153,7 @@ func (bot TipBot) parseCmdDonHandler(ctx context.Context, m *tb.Message) error { bot.trySendMessage(m.Sender, str.MarkdownEscape(donationInterceptMessage)) m.Text = fmt.Sprintf("/donate %d", amount) - bot.donationHandler(ctx, m) - // returning nil here will abort the parent handler (/pay or /tip) + bot.donationHandler(ctx) + // returning nil here will abort the parent ctx (/pay or /tip) return nil } diff --git a/internal/telegram/edit.go b/internal/telegram/edit.go index 6aa0cb41..1c8ac0e3 100644 --- a/internal/telegram/edit.go +++ b/internal/telegram/edit.go @@ -6,7 +6,7 @@ import ( cmap "github.com/orcaman/concurrent-map" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) var editStack cmap.ConcurrentMap diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index ff396d0f..86103138 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/runtime/once" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -19,11 +21,11 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) var ( - inlineFaucetMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + inlineFaucetMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnCancelInlineFaucet = inlineFaucetMenu.Data("🚫 Cancel", "cancel_faucet_inline") btnAcceptInlineFaucet = inlineFaucetMenu.Data("✅ Collect", "confirm_faucet_inline") ) @@ -136,28 +138,28 @@ func (bot TipBot) makeFaucet(ctx context.Context, m *tb.Message, query bool) (*I return faucet, err } -func (bot TipBot) makeQueryFaucet(ctx context.Context, q *tb.Query, query bool) (*InlineFaucet, error) { - faucet, err := bot.createFaucet(ctx, q.Text, &q.From) +func (bot TipBot) makeQueryFaucet(ctx intercept.Context) (*InlineFaucet, error) { + faucet, err := bot.createFaucet(ctx, ctx.Query().Text, ctx.Query().Sender) if err != nil { switch err.(errors.TipBotError).Code { case errors.DecodeAmountError: - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return nil, err case errors.DecodePerUserAmountError: - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return nil, err case errors.InvalidAmountError: - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return nil, err case errors.InvalidAmountPerUserError: - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineFaucetInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineFaucetInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return nil, err case errors.GetBalanceError: - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineQueryFaucetTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return nil, err case errors.BalanceToLowError: log.Errorf(err.Error()) - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryFaucetDescription"), bot.Telegram.Me.Username)) return nil, err } } @@ -165,7 +167,7 @@ func (bot TipBot) makeQueryFaucet(ctx context.Context, q *tb.Query, query bool) } func (bot TipBot) makeFaucetKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { - inlineFaucetMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} + inlineFaucetMenu := &tb.ReplyMarkup{ResizeKeyboard: true} acceptInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "collectButtonMessage"), "confirm_faucet_inline", id) cancelInlineFaucetButton := inlineFaucetMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_faucet_inline", id) inlineFaucetMenu.Inline( @@ -176,20 +178,20 @@ func (bot TipBot) makeFaucetKeyboard(ctx context.Context, id string) *tb.ReplyMa return inlineFaucetMenu } -func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) (context.Context, error) { - bot.anyTextHandler(ctx, m) - if m.Private() { - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetHelpFaucetInGroup"))) +func (bot TipBot) faucetHandler(ctx intercept.Context) (intercept.Context, error) { + bot.anyTextHandler(ctx) + if ctx.Message().Private() { + bot.trySendMessage(ctx.Message().Sender, fmt.Sprintf(Translate(ctx, "inlineFaucetHelpText"), Translate(ctx, "inlineFaucetHelpFaucetInGroup"))) return ctx, errors.Create(errors.NoPrivateChatError) } - ctx = bot.mapFaucetLanguage(ctx, m.Text) - inlineFaucet, err := bot.makeFaucet(ctx, m, false) + ctx.Context = bot.mapFaucetLanguage(ctx, ctx.Text()) + inlineFaucet, err := bot.makeFaucet(ctx, ctx.Message(), false) if err != nil { log.Warnf("[faucet] %s", err.Error()) return ctx, err } - fromUserStr := GetUserStr(m.Sender) - mFaucet := bot.trySendMessage(m.Chat, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) + fromUserStr := GetUserStr(ctx.Message().Sender) + mFaucet := bot.trySendMessage(ctx.Message().Chat, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) log.Infof("[faucet] %s created faucet %s: %d sat (%d per user)", fromUserStr, inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) // log faucet link if possible @@ -199,8 +201,8 @@ func (bot TipBot) faucetHandler(ctx context.Context, m *tb.Message) (context.Con return ctx, inlineFaucet.Set(inlineFaucet, bot.Bunt) } -func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) (context.Context, error) { - inlineFaucet, err := bot.makeQueryFaucet(ctx, q, false) +func (bot TipBot) handleInlineFaucetQuery(ctx intercept.Context) (intercept.Context, error) { + inlineFaucet, err := bot.makeQueryFaucet(ctx) if err != nil { log.Errorf("[handleInlineFaucetQuery] %s", err.Error()) return ctx, err @@ -218,7 +220,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) (con // required for photos ThumbURL: url, } - result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: bot.makeFaucetKeyboard(ctx, inlineFaucet.ID).InlineKeyboard} + result.ReplyMarkup = &tb.ReplyMarkup{InlineKeyboard: bot.makeFaucetKeyboard(ctx, inlineFaucet.ID).InlineKeyboard} results[i] = result // needed to set a unique string ID for each result results[i].SetResultID(inlineFaucet.ID) @@ -227,7 +229,7 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) (con log.Infof("[faucet] %s:%d created inline faucet %s: %d sat (%d per user)", GetUserStr(inlineFaucet.From.Telegram), inlineFaucet.From.Telegram.ID, inlineFaucet.ID, inlineFaucet.Amount, inlineFaucet.PerUserAmount) } - err = bot.Telegram.Answer(q, &tb.QueryResponse{ + err = bot.Telegram.Answer(ctx.Query(), &tb.QueryResponse{ Results: results, CacheTime: 1, }) @@ -238,7 +240,8 @@ func (bot TipBot) handleInlineFaucetQuery(ctx context.Context, q *tb.Query) (con return ctx, nil } -func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) acceptInlineFaucetHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() to := LoadUser(ctx) tx := &InlineFaucet{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) @@ -265,7 +268,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if from.Telegram.ID == to.Telegram.ID { log.Debugf("[faucet] %s is the owner faucet %s", GetUserStr(to.Telegram), inlineFaucet.ID) - ctx = context.WithValue(ctx, "callback_response", Translate(ctx, "sendYourselfMessage")) + ctx.Context = context.WithValue(ctx, "callback_response", Translate(ctx, "sendYourselfMessage")) return ctx, errors.Create(errors.SelfPaymentError) } // check if to user has already taken from the faucet @@ -273,7 +276,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback if a.Telegram.ID == to.Telegram.ID { // to user is already in To slice, has taken from facuet log.Debugf("[faucet] %s:%d already took from faucet %s", GetUserStr(to.Telegram), to.Telegram.ID, inlineFaucet.ID) - ctx = context.WithValue(ctx, "callback_response", Translate(ctx, "inlineFaucetAlreadyTookMessage")) + ctx.Context = context.WithValue(ctx, "callback_response", Translate(ctx, "inlineFaucetAlreadyTookMessage")) return ctx, errors.Create(errors.UnknownError) } } @@ -310,7 +313,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // bot.trySendMessage(from.Telegram, Translate(ctx, "sendErrorMessage")) errMsg := fmt.Sprintf("[faucet] Transaction failed: %s", err.Error()) log.Warnln(errMsg) - ctx = context.WithValue(ctx, "callback_response", Translate(ctx, "errorTryLaterMessage")) + ctx.Context = context.WithValue(ctx, "callback_response", Translate(ctx, "errorTryLaterMessage")) // if faucet fails, cancel it: // c.Sender.ID = inlineFaucet.From.Telegram.ID // overwrite the sender of the callback to be the faucet owner // log.Debugf("[faucet] Canceling faucet %s...", inlineFaucet.ID) @@ -325,7 +328,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback inlineFaucet.RemainingAmount = inlineFaucet.RemainingAmount - inlineFaucet.PerUserAmount go func() { to_message := fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "inlineFaucetReceivedMessage"), fromUserStrMd, inlineFaucet.PerUserAmount) - ctx = context.WithValue(ctx, "callback_response", to_message) + ctx.Context = context.WithValue(ctx, "callback_response", to_message) bot.trySendMessage(to.Telegram, to_message) bot.trySendMessage(from.Telegram, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "inlineFaucetSentMessage"), inlineFaucet.PerUserAmount, toUserStrMd)) }() @@ -344,9 +347,8 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx context.Context, c *tb.Callback // update the message if the faucet still has some sats left after this tx if inlineFaucet.RemainingAmount >= inlineFaucet.PerUserAmount { - bot.tryEditStack(c.Message, inlineFaucet.ID, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) + bot.tryEditStack(c, inlineFaucet.ID, inlineFaucet.Message, bot.makeFaucetKeyboard(ctx, inlineFaucet.ID)) } - } if inlineFaucet.RemainingAmount < inlineFaucet.PerUserAmount { log.Debugf(fmt.Sprintf("[faucet] faucet %s empty. Remaining: %d sat", inlineFaucet.ID, inlineFaucet.RemainingAmount)) @@ -369,7 +371,7 @@ func (bot *TipBot) cancelInlineFaucet(ctx context.Context, c *tb.Callback, ignor inlineFaucet := fn.(*InlineFaucet) if ignoreID || c.Sender.ID == inlineFaucet.From.Telegram.ID { faucet_cancelled_message := i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCancelledMessage") - bot.tryEditStack(c.Message, inlineFaucet.ID, faucet_cancelled_message, &tb.ReplyMarkup{}) + bot.tryEditStack(c, inlineFaucet.ID, faucet_cancelled_message, &tb.ReplyMarkup{}) ctx = context.WithValue(ctx, "callback_response", faucet_cancelled_message) // set the inlineFaucet inactive inlineFaucet.Active = false @@ -389,7 +391,7 @@ func (bot *TipBot) finishFaucet(ctx context.Context, c *tb.Callback, inlineFauce if inlineFaucet.UserNeedsWallet { inlineFaucet.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineFaucet.LanguageCode, "inlineFaucetCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } - bot.tryEditStack(c.Message, inlineFaucet.ID, inlineFaucet.Message, &tb.ReplyMarkup{}) + bot.tryEditStack(c, inlineFaucet.ID, inlineFaucet.Message, &tb.ReplyMarkup{}) log.Debugf("[faucet] Faucet finished %s", inlineFaucet.ID) once.Remove(inlineFaucet.ID) @@ -411,6 +413,9 @@ func listFaucetTakers(inlineFaucet *InlineFaucet) string { return to_str } -func (bot *TipBot) cancelInlineFaucetHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { - return bot.cancelInlineFaucet(ctx, c, false) +func (bot *TipBot) cancelInlineFaucetHandler(ctx intercept.Context) (intercept.Context, error) { + var err error + ctx.Context, err = bot.cancelInlineFaucet(ctx, ctx.Callback(), false) + return ctx, err + } diff --git a/internal/telegram/files.go b/internal/telegram/files.go index 9fa6a067..852fe8d1 100644 --- a/internal/telegram/files.go +++ b/internal/telegram/files.go @@ -1,19 +1,20 @@ package telegram import ( - "context" "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/runtime" - tb "gopkg.in/lightningtipbot/telebot.v2" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + tb "gopkg.in/lightningtipbot/telebot.v3" ) -func (bot *TipBot) fileHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) fileHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() if m.Chat.Type != tb.ChatPrivate { return ctx, errors.Create(errors.NoPrivateChatError) } user := LoadUser(ctx) if c := stateCallbackMessage[user.StateKey]; c != nil { - // found handler for this state + // found ctx for this state // now looking for user state reset ticker ticker := runtime.GetTicker(user.ID) if !ticker.Started { @@ -27,7 +28,7 @@ func (bot *TipBot) fileHandler(ctx context.Context, m *tb.Message) (context.Cont ticker.ResetChan <- struct{}{} } - return c(ctx, m) + return c(ctx) } return ctx, errors.Create(errors.NoFileFoundError) } diff --git a/internal/telegram/groups.go b/internal/telegram/groups.go index 5a868949..76105a3d 100644 --- a/internal/telegram/groups.go +++ b/internal/telegram/groups.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strings" "github.com/LightningTipBot/LightningTipBot/internal" @@ -17,7 +18,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) type Ticket struct { @@ -77,7 +78,7 @@ func (ticketEvent TicketEvent) Key() string { } var ( - ticketPayConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + ticketPayConfirmationMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnPayTicket = paymentConfirmationMenu.Data("✅ Pay", "pay_ticket") ) @@ -95,16 +96,17 @@ var ( groupReceiveTicketInvoice = "🎟 You received *%d sat* for a ticket for group `%s` paid by user %s." ) -func (bot TipBot) groupHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot TipBot) groupHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() splits := strings.Split(m.Text, " ") if len(splits) == 1 { return ctx, nil } else if len(splits) > 1 { if splits[1] == "join" { - return bot.groupRequestJoinHandler(ctx, m) + return bot.groupRequestJoinHandler(ctx) } if splits[1] == "add" { - return bot.addGroupHandler(ctx, m) + return bot.addGroupHandler(ctx) } if splits[1] == "remove" { // todo -- implement this @@ -115,13 +117,13 @@ func (bot TipBot) groupHandler(ctx context.Context, m *tb.Message) (context.Cont } // groupRequestJoinHandler sends a payment request to the user who wants to join a group -func (bot TipBot) groupRequestJoinHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot TipBot) groupRequestJoinHandler(ctx intercept.Context) (intercept.Context, error) { user := LoadUser(ctx) // // reply only in private message - if m.Chat.Type != tb.ChatPrivate { + if ctx.Chat().Type != tb.ChatPrivate { return ctx, fmt.Errorf("not private chat") } - splits := strings.Split(m.Text, " ") + splits := strings.Split(ctx.Message().Text, " ") // if the command was /group join splitIdx := 1 // we also have the simpler command /join that can be used @@ -129,8 +131,8 @@ func (bot TipBot) groupRequestJoinHandler(ctx context.Context, m *tb.Message) (c if splits[0] == "/join" { splitIdx = 0 } - if len(splits) != splitIdx+2 || len(m.Text) > 100 { - bot.trySendMessage(m.Chat, grouJoinGroupHelpMessage) + if len(splits) != splitIdx+2 || len(ctx.Message().Text) > 100 { + bot.trySendMessage(ctx.Message().Chat, grouJoinGroupHelpMessage) return ctx, nil } groupName := strings.ToLower(splits[splitIdx+1]) @@ -138,7 +140,7 @@ func (bot TipBot) groupRequestJoinHandler(ctx context.Context, m *tb.Message) (c group := &Group{} tx := bot.GroupsDb.Where("name = ? COLLATE NOCASE", groupName).First(group) if tx.Error != nil { - bot.trySendMessage(m.Chat, groupNotFoundMessage) + bot.trySendMessage(ctx.Message().Chat, groupNotFoundMessage) return ctx, fmt.Errorf("group not found") } @@ -185,11 +187,11 @@ func (bot TipBot) groupRequestJoinHandler(ctx context.Context, m *tb.Message) (c if err != nil { errmsg := fmt.Sprintf("[/group] Error: Could not get user balance: %s", err.Error()) log.Errorln(errmsg) - bot.trySendMessage(m.Sender, Translate(ctx, "errorTryLaterMessage")) + bot.trySendMessage(ctx.Message().Sender, Translate(ctx, "errorTryLaterMessage")) return ctx, errors.New(errors.GetBalanceError, err) } if balance >= group.Ticket.Price { - return bot.groupSendPayButtonHandler(ctx, m, *ticketEvent) + return bot.groupSendPayButtonHandler(ctx, *ticketEvent) } // otherwise we send a payment request @@ -202,12 +204,12 @@ func (bot TipBot) groupRequestJoinHandler(ctx context.Context, m *tb.Message) (c log.Errorln(errmsg) return ctx, err } - ticketEvent.Message = bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoiceEvent.PaymentRequest)}) - bot.trySendMessage(m.Sender, fmt.Sprintf(groupPayInvoiceMessage, groupName)) + ticketEvent.Message = bot.trySendMessage(ctx.Message().Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoiceEvent.PaymentRequest)}) + bot.trySendMessage(ctx.Message().Sender, fmt.Sprintf(groupPayInvoiceMessage, groupName)) return ctx, nil } -func (bot *TipBot) groupSendPayButtonHandler(ctx context.Context, m *tb.Message, ticket TicketEvent) (context.Context, error) { +func (bot *TipBot) groupSendPayButtonHandler(ctx intercept.Context, ticket TicketEvent) (intercept.Context, error) { // object that holds all information about the send payment // // // create inline buttons btnPayTicket := ticketPayConfirmationMenu.Data(Translate(ctx, "payButtonMessage"), "pay_ticket", ticket.Base.ID) @@ -219,11 +221,12 @@ func (bot *TipBot) groupSendPayButtonHandler(ctx context.Context, m *tb.Message, if len(ticket.Group.Ticket.Memo) > 0 { confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmPayAppendMemo"), str.MarkdownEscape(ticket.Group.Ticket.Memo)) } - bot.trySendMessageEditable(m.Chat, confirmText, ticketPayConfirmationMenu) + bot.trySendMessageEditable(ctx.Message().Chat, confirmText, ticketPayConfirmationMenu) return ctx, nil } -func (bot *TipBot) groupConfirmPayButtonHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) groupConfirmPayButtonHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() tx := &TicketEvent{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -241,15 +244,15 @@ func (bot *TipBot) groupConfirmPayButtonHandler(ctx context.Context, c *tb.Callb } if !ticketEvent.Active { log.Errorf("[confirmPayHandler] send not active anymore") - bot.tryEditMessage(c.Message, i18n.Translate(ticketEvent.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) - bot.tryDeleteMessage(c.Message) + bot.tryEditMessage(c, i18n.Translate(ticketEvent.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) + bot.tryDeleteMessage(c) return ctx, errors.Create(errors.NotActiveError) } defer ticketEvent.Set(ticketEvent, bot.Bunt) user := LoadUser(ctx) if user.Wallet == nil { - bot.tryDeleteMessage(c.Message) + bot.tryDeleteMessage(c) return ctx, errors.Create(errors.UserNoWalletError) } @@ -259,13 +262,13 @@ func (bot *TipBot) groupConfirmPayButtonHandler(ctx context.Context, c *tb.Callb if err != nil { errmsg := fmt.Sprintf("[/pay] Could not pay invoice of %s: %s", GetUserStr(user.Telegram), err) err = fmt.Errorf(i18n.Translate(ticketEvent.LanguageCode, "invoiceUndefinedErrorMessage")) - bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(ticketEvent.LanguageCode, "invoicePaymentFailedMessage"), err.Error()), &tb.ReplyMarkup{}) + bot.tryEditMessage(c, fmt.Sprintf(i18n.Translate(ticketEvent.LanguageCode, "invoicePaymentFailedMessage"), err.Error()), &tb.ReplyMarkup{}) log.Errorln(errmsg) return ctx, err } // update the message and remove the button - bot.tryEditMessage(c.Message, i18n.Translate(ticketEvent.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c, i18n.Translate(ticketEvent.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) return ctx, nil } @@ -364,7 +367,8 @@ func (bot *TipBot) groupGetInviteLinkHandler(event Event) { return } -func (bot TipBot) addGroupHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot TipBot) addGroupHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() if m.Chat.Type == tb.ChatPrivate { return ctx, fmt.Errorf("not in group") } diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 813bc0a0..d2b26ebc 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -1,18 +1,17 @@ package telegram import ( - "context" "fmt" "strings" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) -type Handler struct { +type InterceptionWrapper struct { Endpoints []interface{} - Handler interface{} + Handler intercept.Func Interceptor *Interceptor } @@ -38,40 +37,22 @@ func getDefaultAfterInterceptor(bot TipBot) []intercept.Func { return []intercept.Func{} } -// registerHandlerWithInterceptor will register a handler with all the predefined interceptors, based on the interceptor type -func (bot TipBot) registerHandlerWithInterceptor(h Handler) { +// registerHandlerWithInterceptor will register a ctx with all the predefined interceptors, based on the interceptor type +func (bot TipBot) registerHandlerWithInterceptor(h InterceptionWrapper) { h.Interceptor.Before = append(getDefaultBeforeInterceptor(bot), h.Interceptor.Before...) //h.Interceptor.After = append(h.Interceptor.After, getDefaultAfterInterceptor(bot)...) //h.Interceptor.OnDefer = append(h.Interceptor.OnDefer, getDefaultDeferInterceptor(bot)...) - - switch h.Interceptor.Type { - case MessageInterceptor: - for _, endpoint := range h.Endpoints { - bot.handle(endpoint, intercept.HandlerWithMessage(h.Handler.(func(ctx context.Context, query *tb.Message) (context.Context, error)), - intercept.WithBeforeMessage(h.Interceptor.Before...), - intercept.WithAfterMessage(h.Interceptor.After...), - intercept.WithDeferMessage(h.Interceptor.OnDefer...))) - } - case QueryInterceptor: - for _, endpoint := range h.Endpoints { - bot.handle(endpoint, intercept.HandlerWithQuery(h.Handler.(func(ctx context.Context, query *tb.Query) (context.Context, error)), - intercept.WithBeforeQuery(h.Interceptor.Before...), - intercept.WithAfterQuery(h.Interceptor.After...), - intercept.WithDeferQuery(h.Interceptor.OnDefer...))) - } - case CallbackInterceptor: - for _, endpoint := range h.Endpoints { - bot.handle(endpoint, intercept.HandlerWithCallback(h.Handler.(func(ctx context.Context, callback *tb.Callback) (context.Context, error)), - intercept.WithBeforeCallback(h.Interceptor.Before...), - intercept.WithAfterCallback(h.Interceptor.After...), - intercept.WithDeferCallback(append(h.Interceptor.OnDefer, bot.answerCallbackInterceptor)...))) - } + for _, endpoint := range h.Endpoints { + bot.handle(endpoint, intercept.WithHandler(h.Handler, + intercept.WithBefore(h.Interceptor.Before...), + intercept.WithAfter(h.Interceptor.After...), + intercept.WithDefer(h.Interceptor.OnDefer...))) } } // handle accepts an endpoint and handler for Telegram handler registration. // function will automatically register string handlers as uppercase and first letter uppercase. -func (bot TipBot) handle(endpoint interface{}, handler interface{}) { +func (bot TipBot) handle(endpoint interface{}, handler tb.HandlerFunc) { // register the endpoint bot.Telegram.Handle(endpoint, handler) switch endpoint.(type) { @@ -90,24 +71,23 @@ func (bot TipBot) handle(endpoint interface{}, handler interface{}) { } // register registers a handler, so that Telegram can handle the endpoint correctly. -func (bot TipBot) register(h Handler) { +func (bot TipBot) register(h InterceptionWrapper) { if h.Interceptor != nil { bot.registerHandlerWithInterceptor(h) } else { for _, endpoint := range h.Endpoints { - bot.handle(endpoint, h.Handler) + bot.handle(endpoint, intercept.WithHandler(h.Handler)) } } } // getHandler returns a list of all handlers, that need to be registered with Telegram -func (bot TipBot) getHandler() []Handler { - return []Handler{ +func (bot TipBot) getHandler() []InterceptionWrapper { + return []InterceptionWrapper{ { Endpoints: []interface{}{"/start"}, Handler: bot.startHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -123,7 +103,6 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/tip", "/t", "/honk"}, Handler: bot.tipHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -140,7 +119,6 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/pay"}, Handler: bot.payHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -156,7 +134,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/invoice", &btnInvoiceMainMenu}, Handler: bot.invoiceHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -172,7 +150,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/shops"}, Handler: bot.shopsHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.logMessageInterceptor, bot.requireUserInterceptor, @@ -187,7 +165,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/shop"}, Handler: bot.shopHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -203,7 +181,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/balance", &btnBalanceMainMenu}, Handler: bot.balanceHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -219,7 +197,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/send", &btnSendMenuEnter}, Handler: bot.sendHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -236,7 +214,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnSendMainMenu}, Handler: bot.keyboardSendHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -253,7 +231,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan", "/grifo"}, Handler: bot.faucetHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -269,7 +247,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/tipjar", "/spendendose"}, Handler: bot.tipjarHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -285,7 +263,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/help", &btnHelpMainMenu}, Handler: bot.helpHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -301,7 +279,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/basics"}, Handler: bot.basicsHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -317,7 +295,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/donate"}, Handler: bot.donationHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -333,7 +311,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/advanced"}, Handler: bot.advancedHelpHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -349,7 +327,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/link"}, Handler: bot.lndhubHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -365,7 +343,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/lnurl"}, Handler: bot.lnurlHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -382,7 +360,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/group"}, Handler: bot.groupHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, @@ -398,7 +376,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{"/join"}, Handler: bot.groupRequestJoinHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.requirePrivateChatInterceptor, bot.localizerInterceptor, @@ -415,7 +393,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnPayTicket}, Handler: bot.groupConfirmPayButtonHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -430,7 +408,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{tb.OnPhoto}, Handler: bot.photoHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.requirePrivateChatInterceptor, bot.localizerInterceptor, @@ -447,7 +425,7 @@ 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, @@ -457,7 +435,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{tb.OnText}, Handler: bot.anyTextHandler, Interceptor: &Interceptor{ - Type: MessageInterceptor, + Before: []intercept.Func{ bot.requirePrivateChatInterceptor, // Respond to any text only in private chat bot.localizerInterceptor, @@ -474,7 +452,6 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{tb.OnQuery}, Handler: bot.anyQueryHandler, Interceptor: &Interceptor{ - Type: QueryInterceptor, Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -486,14 +463,14 @@ func (bot TipBot) getHandler() []Handler { }, }, { - Endpoints: []interface{}{tb.OnChosenInlineResult}, + Endpoints: []interface{}{tb.OnInlineResult}, Handler: bot.anyChosenInlineHandler, }, { Endpoints: []interface{}{&btnPay}, Handler: bot.confirmPayHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -508,7 +485,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnCancelPay}, Handler: bot.cancelPaymentHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -523,7 +500,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnSend}, Handler: bot.confirmSendHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -538,7 +515,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnCancelSend}, Handler: bot.cancelSendHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -553,7 +530,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnAcceptInlineSend}, Handler: bot.acceptInlineSendHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -568,7 +545,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnCancelInlineSend}, Handler: bot.cancelInlineSendHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -583,7 +560,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnAcceptInlineReceive}, Handler: bot.acceptInlineReceiveHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -598,7 +575,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnCancelInlineReceive}, Handler: bot.cancelInlineReceiveHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -613,7 +590,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnAcceptInlineFaucet}, Handler: bot.acceptInlineFaucetHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.singletonCallbackInterceptor, bot.localizerInterceptor, @@ -629,7 +606,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnCancelInlineFaucet}, Handler: bot.cancelInlineFaucetHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -644,7 +621,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnAcceptInlineTipjar}, Handler: bot.acceptInlineTipjarHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -659,7 +636,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnCancelInlineTipjar}, Handler: bot.cancelInlineTipjarHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -674,7 +651,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnWithdraw}, Handler: bot.confirmWithdrawHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -689,7 +666,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnCancelWithdraw}, Handler: bot.cancelWithdrawHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -704,7 +681,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnAuth}, Handler: bot.confirmLnurlAuthHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -719,7 +696,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&btnCancelAuth}, Handler: bot.cancelLnurlAuthHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.requireUserInterceptor, @@ -734,7 +711,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopNewShopButton}, Handler: bot.shopNewShopHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -748,7 +725,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopAddItemButton}, Handler: bot.shopNewItemHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -762,7 +739,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopBuyitemButton}, Handler: bot.shopGetItemFilesHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -776,7 +753,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopNextitemButton}, Handler: bot.shopNextItemButtonHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -790,7 +767,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&browseShopButton}, Handler: bot.shopsBrowser, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -804,7 +781,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopSelectButton}, Handler: bot.shopSelect, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -819,7 +796,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopDeleteShopButton}, Handler: bot.shopsDeleteShopBrowser, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -834,7 +811,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopDeleteSelectButton}, Handler: bot.shopSelectDelete, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -849,7 +826,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopLinkShopButton}, Handler: bot.shopsLinkShopBrowser, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -864,7 +841,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopLinkSelectButton}, Handler: bot.shopSelectLink, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -879,7 +856,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopRenameShopButton}, Handler: bot.shopsRenameShopBrowser, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -894,7 +871,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopRenameSelectButton}, Handler: bot.shopSelectRename, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -909,7 +886,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopSettingsButton}, Handler: bot.shopSettingsHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -924,7 +901,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopDescriptionShopButton}, Handler: bot.shopsDescriptionHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -939,7 +916,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopResetShopButton}, Handler: bot.shopsResetHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -953,7 +930,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopResetShopAskButton}, Handler: bot.shopsAskDeleteAllShopsHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -967,7 +944,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopPrevitemButton}, Handler: bot.shopPrevItemButtonHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -981,7 +958,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopShopsButton}, Handler: bot.shopsHandlerCallback, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -996,7 +973,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopItemSettingsButton}, Handler: bot.shopItemSettingsHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -1010,7 +987,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopItemSettingsBackButton}, Handler: bot.displayShopItemHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -1024,7 +1001,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopItemDeleteButton}, Handler: bot.shopItemDeleteHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -1038,7 +1015,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopItemPriceButton}, Handler: bot.shopItemPriceHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -1052,7 +1029,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopItemTitleButton}, Handler: bot.shopItemTitleHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -1066,7 +1043,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopItemAddFileButton}, Handler: bot.shopItemAddItemHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -1080,7 +1057,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopItemBuyButton}, Handler: bot.shopConfirmBuyHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, @@ -1094,7 +1071,7 @@ func (bot TipBot) getHandler() []Handler { Endpoints: []interface{}{&shopItemCancelBuyButton}, Handler: bot.displayShopItemHandler, Interceptor: &Interceptor{ - Type: CallbackInterceptor, + Before: []intercept.Func{ bot.localizerInterceptor, bot.loadUserInterceptor, diff --git a/internal/telegram/help.go b/internal/telegram/help.go index e947ec89..f468e70b 100644 --- a/internal/telegram/help.go +++ b/internal/telegram/help.go @@ -3,8 +3,9 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) func (bot TipBot) makeHelpMessage(ctx context.Context, m *tb.Message) string { @@ -26,25 +27,25 @@ func (bot TipBot) makeHelpMessage(ctx context.Context, m *tb.Message) string { return fmt.Sprintf(helpMessage, dynamicHelpMessage) } -func (bot TipBot) helpHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot TipBot) helpHandler(ctx intercept.Context) (intercept.Context, error) { // check and print all commands - bot.anyTextHandler(ctx, m) - if !m.Private() { + bot.anyTextHandler(ctx) + if !ctx.Message().Private() { // delete message - bot.tryDeleteMessage(m) + bot.tryDeleteMessage(ctx.Message()) } - bot.trySendMessage(m.Sender, bot.makeHelpMessage(ctx, m), tb.NoPreview) + bot.trySendMessage(ctx.Sender(), bot.makeHelpMessage(ctx, ctx.Message()), tb.NoPreview) return ctx, nil } -func (bot TipBot) basicsHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot TipBot) basicsHandler(ctx intercept.Context) (intercept.Context, error) { // check and print all commands - bot.anyTextHandler(ctx, m) - if !m.Private() { + bot.anyTextHandler(ctx) + if !ctx.Message().Private() { // delete message - bot.tryDeleteMessage(m) + bot.tryDeleteMessage(ctx.Message()) } - bot.trySendMessage(m.Sender, Translate(ctx, "basicsMessage"), tb.NoPreview) + bot.trySendMessage(ctx.Sender(), Translate(ctx, "basicsMessage"), tb.NoPreview) return ctx, nil } @@ -74,13 +75,13 @@ func (bot TipBot) makeAdvancedHelpMessage(ctx context.Context, m *tb.Message) st ) } -func (bot TipBot) advancedHelpHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot TipBot) advancedHelpHandler(ctx intercept.Context) (intercept.Context, error) { // check and print all commands - bot.anyTextHandler(ctx, m) - if !m.Private() { + bot.anyTextHandler(ctx) + if !ctx.Message().Private() { // delete message - bot.tryDeleteMessage(m) + bot.tryDeleteMessage(ctx.Message()) } - bot.trySendMessage(m.Sender, bot.makeAdvancedHelpMessage(ctx, m), tb.NoPreview) + bot.trySendMessage(ctx.Sender(), bot.makeAdvancedHelpMessage(ctx, ctx.Message()), tb.NoPreview) return ctx, nil } diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index a99ecb56..0c8ebc4f 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -7,18 +7,20 @@ import ( "strconv" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/internal/storage" "github.com/LightningTipBot/LightningTipBot/internal/i18n" i18n2 "github.com/nicksnyder/go-i18n/v2/i18n" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) const queryImage = "https://avatars.githubusercontent.com/u/88730856?v=4" -func (bot TipBot) inlineQueryInstructions(ctx context.Context, q *tb.Query) (context.Context, error) { +func (bot TipBot) inlineQueryInstructions(ctx intercept.Context) (intercept.Context, error) { instructions := []struct { url string title string @@ -60,11 +62,11 @@ func (bot TipBot) inlineQueryInstructions(ctx context.Context, q *tb.Query) (con results[i].SetResultID(strconv.Itoa(i)) } - err := bot.Telegram.Answer(q, &tb.QueryResponse{ + err := ctx.Answer(&tb.QueryResponse{ Results: results, CacheTime: 5, // a minute IsPersonal: true, - QueryID: q.ID, + QueryID: ctx.Query().ID, }) if err != nil { @@ -73,7 +75,7 @@ func (bot TipBot) inlineQueryInstructions(ctx context.Context, q *tb.Query) (con return ctx, err } -func (bot TipBot) inlineQueryReplyWithError(q *tb.Query, message string, help string) { +func (bot TipBot) inlineQueryReplyWithError(ctx intercept.Context, message string, help string) { results := make(tb.Results, 1) // []tb.Result result := &tb.ArticleResult{ // URL: url, @@ -83,11 +85,12 @@ func (bot TipBot) inlineQueryReplyWithError(q *tb.Query, message string, help st // required for photos ThumbURL: queryImage, } - id := fmt.Sprintf("inl-error-%d-%s", q.From.ID, RandStringRunes(5)) + id := fmt.Sprintf("inl-error-%d-%s", ctx.Query().Sender.ID, RandStringRunes(5)) result.SetResultID(id) results[0] = result - err := bot.Telegram.Answer(q, &tb.QueryResponse{ - Results: results, + err := ctx.Answer(&tb.QueryResponse{ + Results: results, + CacheTime: 1, // 60 == 1 minute, todo: make higher than 1 s in production }) @@ -98,21 +101,22 @@ func (bot TipBot) inlineQueryReplyWithError(q *tb.Query, message string, help st // anyChosenInlineHandler will load any inline object from cache and store into bunt. // this is used to decrease bunt db write ops. -func (bot TipBot) anyChosenInlineHandler(q *tb.ChosenInlineResult) { +func (bot TipBot) anyChosenInlineHandler(ctx intercept.Context) (intercept.Context, error) { // load inline object from cache - inlineObject, err := bot.Cache.Get(q.ResultID) + inlineObject, err := bot.Cache.Get(ctx.InlineResult().ResultID) // check error if err != nil { log.Errorf("[anyChosenInlineHandler] could not find inline object in cache. %v", err.Error()) - return + return ctx, err } switch inlineObject.(type) { case storage.Storable: // persist inline object in bunt runtime.IgnoreError(bot.Bunt.Set(inlineObject.(storage.Storable))) default: - log.Errorf("[anyChosenInlineHandler] invalid inline object type: %s, query: %s", reflect.TypeOf(inlineObject).String(), q.Query) + log.Errorf("[anyChosenInlineHandler] invalid inline object type: %s, query: %s", reflect.TypeOf(inlineObject).String(), ctx.InlineResult().Query) } + return ctx, nil } func (bot TipBot) commandTranslationMap(ctx context.Context, command string) context.Context { @@ -134,36 +138,37 @@ func (bot TipBot) commandTranslationMap(ctx context.Context, command string) con return ctx } -func (bot TipBot) anyQueryHandler(ctx context.Context, q *tb.Query) (context.Context, error) { - if q.Text == "" { - return bot.inlineQueryInstructions(ctx, q) +func (bot TipBot) anyQueryHandler(ctx intercept.Context) (intercept.Context, error) { + if ctx.Query().Text == "" { + return bot.inlineQueryInstructions(ctx) } // create the inline send result - if strings.HasPrefix(q.Text, "/") { - q.Text = strings.TrimPrefix(q.Text, "/") + var text = ctx.Query().Text + if strings.HasPrefix(text, "/") { + text = strings.TrimPrefix(text, "/") } - if strings.HasPrefix(q.Text, "send") || strings.HasPrefix(q.Text, "pay") { - return bot.handleInlineSendQuery(ctx, q) + if strings.HasPrefix(text, "send") || strings.HasPrefix(text, "pay") { + return bot.handleInlineSendQuery(ctx) } - if strings.HasPrefix(q.Text, "faucet") || strings.HasPrefix(q.Text, "zapfhahn") || strings.HasPrefix(q.Text, "kraan") || strings.HasPrefix(q.Text, "grifo") { - if len(strings.Split(q.Text, " ")) > 1 { - c := strings.Split(q.Text, " ")[0] - ctx = bot.commandTranslationMap(ctx, c) + if strings.HasPrefix(text, "faucet") || strings.HasPrefix(text, "zapfhahn") || strings.HasPrefix(text, "kraan") || strings.HasPrefix(text, "grifo") { + if len(strings.Split(text, " ")) > 1 { + c := strings.Split(text, " ")[0] + ctx.Context = bot.commandTranslationMap(ctx, c) } - return bot.handleInlineFaucetQuery(ctx, q) + return bot.handleInlineFaucetQuery(ctx) } - if strings.HasPrefix(q.Text, "tipjar") || strings.HasPrefix(q.Text, "spendendose") { - if len(strings.Split(q.Text, " ")) > 1 { - c := strings.Split(q.Text, " ")[0] - ctx = bot.commandTranslationMap(ctx, c) + if strings.HasPrefix(text, "tipjar") || strings.HasPrefix(text, "spendendose") { + if len(strings.Split(text, " ")) > 1 { + c := strings.Split(text, " ")[0] + ctx.Context = bot.commandTranslationMap(ctx, c) } - return bot.handleInlineTipjarQuery(ctx, q) + return bot.handleInlineTipjarQuery(ctx) } - if strings.HasPrefix(q.Text, "receive") || strings.HasPrefix(q.Text, "get") || strings.HasPrefix(q.Text, "payme") || strings.HasPrefix(q.Text, "request") { - return bot.handleInlineReceiveQuery(ctx, q) + if strings.HasPrefix(text, "receive") || strings.HasPrefix(text, "get") || strings.HasPrefix(text, "payme") || strings.HasPrefix(text, "request") { + return bot.handleInlineReceiveQuery(ctx) } return ctx, nil } diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index aa685336..7b65e5d7 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -4,10 +4,12 @@ import ( "bytes" "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -19,11 +21,11 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) var ( - inlineReceiveMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + inlineReceiveMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnCancelInlineReceive = inlineReceiveMenu.Data("🚫 Cancel", "cancel_receive_inline") btnAcceptInlineReceive = inlineReceiveMenu.Data("💸 Pay", "confirm_receive_inline") ) @@ -31,7 +33,7 @@ var ( type InlineReceive struct { *storage.Base MessageText string `json:"inline_receive_messagetext"` - Message *tb.Message `json:"inline_receive_message"` + Message tb.Editable `json:"inline_receive_message"` Amount int64 `json:"inline_receive_amount"` From *lnbits.User `json:"inline_receive_from"` To *lnbits.User `json:"inline_receive_to"` @@ -41,7 +43,7 @@ type InlineReceive struct { } func (bot TipBot) makeReceiveKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { - inlineReceiveMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} + inlineReceiveMenu := &tb.ReplyMarkup{ResizeKeyboard: true} acceptInlineReceiveButton := inlineReceiveMenu.Data(Translate(ctx, "payReceiveButtonMessage"), "confirm_receive_inline") cancelInlineReceiveButton := inlineReceiveMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_receive_inline") acceptInlineReceiveButton.Data = id @@ -55,18 +57,19 @@ func (bot TipBot) makeReceiveKeyboard(ctx context.Context, id string) *tb.ReplyM return inlineReceiveMenu } -func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) (context.Context, error) { +func (bot TipBot) handleInlineReceiveQuery(ctx intercept.Context) (intercept.Context, error) { + q := ctx.Query() to := LoadUser(ctx) amount, err := decodeAmountFromCommand(q.Text) if err != nil { - bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineQueryReceiveTitle"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, Translate(ctx, "inlineQueryReceiveTitle"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) return ctx, err } if amount < 1 { - bot.inlineQueryReplyWithError(q, Translate(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, Translate(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) return ctx, errors.Create(errors.InvalidAmountError) } - toUserStr := GetUserStr(&q.From) + toUserStr := GetUserStr(q.Sender) // check whether the 3rd argument is a username // command is "@LightningTipBot receive 123 @from_user This is the memo" @@ -80,7 +83,7 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) (co if err != nil { //bot.tryDeleteMessage(m) //bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), toUserStrMention)) - bot.inlineQueryReplyWithError(q, + bot.inlineQueryReplyWithError(ctx, fmt.Sprintf(TranslateUser(ctx, "sendUserHasNoWalletMessage"), from_username), fmt.Sprintf(TranslateUser(ctx, "inlineQueryReceiveDescription"), bot.Telegram.Me.Username)) @@ -116,8 +119,8 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) (co // required for photos ThumbURL: url, } - id := fmt.Sprintf("inl-receive-%d-%d-%s", q.From.ID, amount, RandStringRunes(5)) - result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: bot.makeReceiveKeyboard(ctx, id).InlineKeyboard} + id := fmt.Sprintf("inl-receive-%d-%d-%s", q.Sender.ID, amount, RandStringRunes(5)) + result.ReplyMarkup = &tb.ReplyMarkup{InlineKeyboard: bot.makeReceiveKeyboard(ctx, id).InlineKeyboard} results[i] = result // needed to set a unique string ID for each result results[i].SetResultID(id) @@ -147,7 +150,8 @@ func (bot TipBot) handleInlineReceiveQuery(ctx context.Context, q *tb.Query) (co return ctx, nil } -func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) acceptInlineReceiveHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls mutex.LockWithContext(ctx, tx.ID) @@ -177,7 +181,7 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac inlineReceive.From = from } - inlineReceive.Message = c.Message + inlineReceive.Message = c runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) to := inlineReceive.To @@ -196,16 +200,16 @@ func (bot *TipBot) acceptInlineReceiveHandler(ctx context.Context, c *tb.Callbac // if user has no wallet, show invoice bot.tryEditMessage(inlineReceive.Message, inlineReceive.MessageText, &tb.ReplyMarkup{}) // runtime.IgnoreError(inlineReceive.Set(inlineReceive, bot.Bunt)) - bot.inlineReceiveInvoice(ctx, c, inlineReceive) + bot.inlineReceiveInvoice(ctx, inlineReceive) return ctx, errors.Create(errors.BalanceToLowError) } else { // else, do an internal transaction - return bot.sendInlineReceiveHandler(ctx, c) - + return bot.sendInlineReceiveHandler(ctx) } } -func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) sendInlineReceiveHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -254,17 +258,18 @@ func (bot *TipBot) sendInlineReceiveHandler(ctx context.Context, c *tb.Callback) if !success { errMsg := fmt.Sprintf("[acceptInlineReceiveHandler] Transaction failed: %s", err.Error()) log.Errorln(errMsg) - bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveFailedMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveFailedMessage"), &tb.ReplyMarkup{}) return ctx, errors.Create(errors.UnknownError) } log.Infof("[💸 inlineReceive] Send from %s to %s (%d sat).", fromUserStr, toUserStr, inlineReceive.Amount) inlineReceive.Set(inlineReceive, bot.Bunt) - return bot.finishInlineReceiveHandler(ctx, c) + ctx.Context, err = bot.finishInlineReceiveHandler(ctx, ctx.Callback()) + return ctx, err } -func (bot *TipBot) inlineReceiveInvoice(ctx context.Context, c *tb.Callback, inlineReceive *InlineReceive) { +func (bot *TipBot) inlineReceiveInvoice(ctx intercept.Context, inlineReceive *InlineReceive) { if !inlineReceive.Active { log.Errorf("[acceptInlineReceiveHandler] inline receive not active anymore") return @@ -287,13 +292,8 @@ func (bot *TipBot) inlineReceiveInvoice(ctx context.Context, c *tb.Callback, inl } // send the invoice data to user - var msg *tb.Message - if inlineReceive.Message.Chat != nil { - msg = bot.trySendMessage(inlineReceive.Message.Chat, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) - } else { - msg = bot.trySendMessage(c.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) - bot.tryEditMessage(inlineReceive.Message, fmt.Sprintf("%s\n\nPay this invoice:\n```%s```", inlineReceive.MessageText, invoice.PaymentRequest)) - } + msg := bot.trySendMessage(ctx.Callback().Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) + bot.tryEditMessage(inlineReceive.Message, fmt.Sprintf("%s\n\nPay this invoice:\n```%s```", inlineReceive.MessageText, invoice.PaymentRequest)) invoice.InvoiceMessage = msg runtime.IgnoreError(bot.Bunt.Set(invoice)) log.Printf("[/invoice] Incvoice created. User: %s, amount: %d sat.", GetUserStr(inlineReceive.To.Telegram), inlineReceive.Amount) @@ -309,8 +309,10 @@ func (bot *TipBot) inlineReceiveEvent(event Event) { func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} // immediatelly set intransaction to block duplicate calls - mutex.LockWithContext(ctx, tx.ID) - defer mutex.UnlockWithContext(ctx, tx.ID) + if ctx != nil { + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) + } rn, err := tx.Get(tx, bot.Bunt) if err != nil { log.Errorf("[getInlineReceive] %s", err.Error()) @@ -323,7 +325,7 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac toUserStrMd := GetUserStrMd(to.Telegram) fromUserStrMd := GetUserStrMd(from.Telegram) toUserStr := GetUserStr(to.Telegram) - inlineReceive.MessageText = fmt.Sprintf("%s", fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendUpdateMessageAccept"), inlineReceive.Amount, fromUserStrMd, toUserStrMd)) + inlineReceive.MessageText = fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineSendUpdateMessageAccept"), inlineReceive.Amount, fromUserStrMd, toUserStrMd) memo := inlineReceive.Memo if len(memo) > 0 { inlineReceive.MessageText += fmt.Sprintf(i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveAppendMemo"), memo) @@ -346,7 +348,8 @@ func (bot *TipBot) finishInlineReceiveHandler(ctx context.Context, c *tb.Callbac // inlineReceive.Release(inlineReceive, bot.Bunt) } -func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) cancelInlineReceiveHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() tx := &InlineReceive{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -360,7 +363,7 @@ func (bot *TipBot) cancelInlineReceiveHandler(ctx context.Context, c *tb.Callbac if c.Sender.ID != inlineReceive.To.Telegram.ID { return ctx, errors.Create(errors.UnknownError) } - bot.tryEditMessage(c.Message, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c, i18n.Translate(inlineReceive.LanguageCode, "inlineReceiveCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineReceive inactive inlineReceive.Active = false return ctx, inlineReceive.Set(inlineReceive, bot.Bunt) diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index a66b1e33..eb28a984 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -3,10 +3,12 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -17,11 +19,11 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) var ( - inlineSendMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + inlineSendMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnCancelInlineSend = inlineSendMenu.Data("🚫 Cancel", "cancel_send_inline") btnAcceptInlineSend = inlineSendMenu.Data("✅ Receive", "confirm_send_inline") ) @@ -38,7 +40,7 @@ type InlineSend struct { } func (bot TipBot) makeSendKeyboard(ctx context.Context, id string) *tb.ReplyMarkup { - inlineSendMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} + inlineSendMenu := &tb.ReplyMarkup{ResizeKeyboard: true} acceptInlineSendButton := inlineSendMenu.Data(Translate(ctx, "receiveButtonMessage"), "confirm_send_inline") cancelInlineSendButton := inlineSendMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_send_inline") acceptInlineSendButton.Data = id @@ -52,20 +54,21 @@ func (bot TipBot) makeSendKeyboard(ctx context.Context, id string) *tb.ReplyMark return inlineSendMenu } -func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) (context.Context, error) { +func (bot TipBot) handleInlineSendQuery(ctx intercept.Context) (intercept.Context, error) { + q := ctx.Query() // inlineSend := NewInlineSend() // var err error amount, err := decodeAmountFromCommand(q.Text) if err != nil { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQuerySendTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineQuerySendTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) return ctx, err } if amount < 1 { - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(Translate(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) return ctx, errors.Create(errors.InvalidAmountError) } fromUser := LoadUser(ctx) - fromUserStr := GetUserStr(&q.From) + fromUserStr := GetUserStr(q.Sender) balance, err := bot.GetUserBalanceCached(fromUser) if err != nil { errmsg := fmt.Sprintf("could not get balance of user %s", fromUserStr) @@ -75,7 +78,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) (conte // check if fromUser has balance if balance < amount { log.Errorf("Balance of user %s too low", fromUserStr) - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) return ctx, errors.Create(errors.InvalidAmountError) } @@ -91,7 +94,7 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) (conte if err != nil { //bot.tryDeleteMessage(m) //bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), toUserStrMention)) - bot.inlineQueryReplyWithError(q, + bot.inlineQueryReplyWithError(ctx, fmt.Sprintf(TranslateUser(ctx, "sendUserHasNoWalletMessage"), to_username), fmt.Sprintf(TranslateUser(ctx, "inlineQuerySendDescription"), bot.Telegram.Me.Username)) @@ -127,8 +130,8 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) (conte // required for photos ThumbURL: url, } - id := fmt.Sprintf("inl-send-%d-%d-%s", q.From.ID, amount, RandStringRunes(5)) - result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: bot.makeSendKeyboard(ctx, id).InlineKeyboard} + id := fmt.Sprintf("inl-send-%d-%d-%s", q.Sender.ID, amount, RandStringRunes(5)) + result.ReplyMarkup = &tb.ReplyMarkup{InlineKeyboard: bot.makeSendKeyboard(ctx, id).InlineKeyboard} results[i] = result // needed to set a unique string ID for each result results[i].SetResultID(id) @@ -161,7 +164,8 @@ func (bot TipBot) handleInlineSendQuery(ctx context.Context, q *tb.Query) (conte return ctx, nil } -func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) acceptInlineSendHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() to := LoadUser(ctx) tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) @@ -227,7 +231,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) if !success { errMsg := fmt.Sprintf("[sendInline] Transaction failed: %s", err.Error()) log.Errorln(errMsg) - bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "inlineSendFailedMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c, i18n.Translate(inlineSend.LanguageCode, "inlineSendFailedMessage"), &tb.ReplyMarkup{}) return ctx, errors.Create(errors.UnknownError) } @@ -241,7 +245,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) if !to.Initialized { inlineSend.Message += "\n\n" + fmt.Sprintf(i18n.Translate(inlineSend.LanguageCode, "inlineSendCreateWalletMessage"), GetUserStrMd(bot.Telegram.Me)) } - bot.tryEditMessage(c.Message, inlineSend.Message, &tb.ReplyMarkup{}) + bot.tryEditMessage(c, inlineSend.Message, &tb.ReplyMarkup{}) // notify users bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) bot.trySendMessage(fromUser.Telegram, fmt.Sprintf(i18n.Translate(fromUser.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) @@ -252,7 +256,8 @@ func (bot *TipBot) acceptInlineSendHandler(ctx context.Context, c *tb.Callback) return ctx, nil } -func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) cancelInlineSendHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() tx := &InlineSend{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -266,7 +271,7 @@ func (bot *TipBot) cancelInlineSendHandler(ctx context.Context, c *tb.Callback) if c.Sender.ID != inlineSend.From.Telegram.ID { return ctx, errors.Create(errors.UnknownError) } - bot.tryEditMessage(c.Message, i18n.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c, i18n.Translate(inlineSend.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{}) // set the inlineSend inactive inlineSend.Active = false return ctx, inlineSend.Set(inlineSend, bot.Bunt) diff --git a/internal/telegram/intercept/callback.go b/internal/telegram/intercept/callback.go deleted file mode 100644 index ea00e097..00000000 --- a/internal/telegram/intercept/callback.go +++ /dev/null @@ -1,77 +0,0 @@ -package intercept - -import ( - "context" - - log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" -) - -type CallbackFuncHandler func(ctx context.Context, message *tb.Callback) (context.Context, error) -type Func func(ctx context.Context, message interface{}) (context.Context, error) - -type handlerCallbackInterceptor struct { - handler CallbackFuncHandler - before CallbackChain - after CallbackChain - onDefer CallbackChain -} -type CallbackChain []Func -type CallbackInterceptOption func(*handlerCallbackInterceptor) - -func WithBeforeCallback(chain ...Func) CallbackInterceptOption { - return func(a *handlerCallbackInterceptor) { - a.before = chain - } -} -func WithAfterCallback(chain ...Func) CallbackInterceptOption { - return func(a *handlerCallbackInterceptor) { - a.after = chain - } -} -func WithDeferCallback(chain ...Func) CallbackInterceptOption { - return func(a *handlerCallbackInterceptor) { - a.onDefer = chain - } -} - -func interceptCallback(ctx context.Context, message *tb.Callback, hm CallbackChain) (context.Context, error) { - if ctx == nil { - ctx = context.Background() - } - if hm != nil { - var err error - for _, m := range hm { - ctx, err = m(ctx, message) - if err != nil { - return ctx, err - } - } - } - return ctx, nil -} - -func HandlerWithCallback(handler CallbackFuncHandler, option ...CallbackInterceptOption) func(Callback *tb.Callback) { - hm := &handlerCallbackInterceptor{handler: handler} - for _, opt := range option { - opt(hm) - } - return func(c *tb.Callback) { - ctx := context.Background() - ctx, err := interceptCallback(ctx, c, hm.before) - if err != nil { - log.Traceln(err) - return - } - defer interceptCallback(ctx, c, hm.onDefer) - ctx, err = hm.handler(ctx, c) - if err != nil { - log.Traceln(err) - return - } - _, err = interceptCallback(ctx, c, hm.after) - if err != nil { - log.Traceln(err) - } - } -} diff --git a/internal/telegram/intercept/context.go b/internal/telegram/intercept/context.go new file mode 100644 index 00000000..39e3bb0d --- /dev/null +++ b/internal/telegram/intercept/context.go @@ -0,0 +1,84 @@ +package intercept + +import ( + "context" + + log "github.com/sirupsen/logrus" + tb "gopkg.in/lightningtipbot/telebot.v3" +) + +type Context struct { + context.Context + TeleContext +} +type TeleContext struct { + tb.Context +} + +type Func func(ctx Context) (Context, error) + +type handlerInterceptor struct { + handler Func + before Chain + after Chain + onDefer Chain +} +type Chain []Func +type Option func(*handlerInterceptor) + +func WithBefore(chain ...Func) Option { + return func(a *handlerInterceptor) { + a.before = chain + } +} +func WithAfter(chain ...Func) Option { + return func(a *handlerInterceptor) { + a.after = chain + } +} +func WithDefer(chain ...Func) Option { + return func(a *handlerInterceptor) { + a.onDefer = chain + } +} + +func intercept(h Context, hm Chain) (Context, error) { + + if hm != nil { + var err error + for _, m := range hm { + h, err = m(h) + if err != nil { + return h, err + } + } + } + return h, nil +} + +func WithHandler(handler Func, option ...Option) tb.HandlerFunc { + hm := &handlerInterceptor{handler: handler} + for _, opt := range option { + opt(hm) + } + return func(c tb.Context) error { + h := Context{TeleContext: TeleContext{Context: c}, Context: context.Background()} + h, err := intercept(h, hm.before) + if err != nil { + log.Traceln(err) + return err + } + defer intercept(h, hm.onDefer) + h, err = hm.handler(h) + if err != nil { + log.Traceln(err) + return err + } + _, err = intercept(h, hm.after) + if err != nil { + log.Traceln(err) + return err + } + return nil + } +} diff --git a/internal/telegram/intercept/message.go b/internal/telegram/intercept/message.go deleted file mode 100644 index c6c3f3cc..00000000 --- a/internal/telegram/intercept/message.go +++ /dev/null @@ -1,77 +0,0 @@ -package intercept - -import ( - "context" - - log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" -) - -type MessageFuncHandler func(ctx context.Context, message *tb.Message) (context.Context, error) - -type handlerMessageInterceptor struct { - handler MessageFuncHandler - before MessageChain - after MessageChain - onDefer MessageChain -} -type MessageChain []Func -type MessageInterceptOption func(*handlerMessageInterceptor) - -func WithBeforeMessage(chain ...Func) MessageInterceptOption { - return func(a *handlerMessageInterceptor) { - a.before = chain - } -} -func WithAfterMessage(chain ...Func) MessageInterceptOption { - return func(a *handlerMessageInterceptor) { - a.after = chain - } -} -func WithDeferMessage(chain ...Func) MessageInterceptOption { - return func(a *handlerMessageInterceptor) { - a.onDefer = chain - } -} - -func interceptMessage(ctx context.Context, message *tb.Message, hm MessageChain) (context.Context, error) { - if ctx == nil { - ctx = context.Background() - } - if hm != nil { - var err error - for _, m := range hm { - ctx, err = m(ctx, message) - if err != nil { - return ctx, err - } - } - } - return ctx, nil -} - -func HandlerWithMessage(handler MessageFuncHandler, option ...MessageInterceptOption) func(message *tb.Message) { - hm := &handlerMessageInterceptor{handler: handler} - for _, opt := range option { - opt(hm) - } - return func(message *tb.Message) { - ctx := context.Background() - ctx, err := interceptMessage(ctx, message, hm.before) - if err != nil { - log.Traceln(err) - return - } - defer interceptMessage(ctx, message, hm.onDefer) - ctx, err = hm.handler(ctx, message) - if err != nil { - log.Traceln(err) - return - } - _, err = interceptMessage(ctx, message, hm.after) - if err != nil { - log.Traceln(err) - return - } - } -} diff --git a/internal/telegram/intercept/query.go b/internal/telegram/intercept/query.go deleted file mode 100644 index 6aa8a6d2..00000000 --- a/internal/telegram/intercept/query.go +++ /dev/null @@ -1,77 +0,0 @@ -package intercept - -import ( - "context" - - log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" -) - -type QueryFuncHandler func(ctx context.Context, message *tb.Query) (context.Context, error) - -type handlerQueryInterceptor struct { - handler QueryFuncHandler - before QueryChain - after QueryChain - onDefer QueryChain -} -type QueryChain []Func -type QueryInterceptOption func(*handlerQueryInterceptor) - -func WithBeforeQuery(chain ...Func) QueryInterceptOption { - return func(a *handlerQueryInterceptor) { - a.before = chain - } -} -func WithAfterQuery(chain ...Func) QueryInterceptOption { - return func(a *handlerQueryInterceptor) { - a.after = chain - } -} -func WithDeferQuery(chain ...Func) QueryInterceptOption { - return func(a *handlerQueryInterceptor) { - a.onDefer = chain - } -} - -func interceptQuery(ctx context.Context, message *tb.Query, hm QueryChain) (context.Context, error) { - if ctx == nil { - ctx = context.Background() - } - if hm != nil { - var err error - for _, m := range hm { - ctx, err = m(ctx, message) - if err != nil { - return ctx, err - } - } - } - return ctx, nil -} - -func HandlerWithQuery(handler QueryFuncHandler, option ...QueryInterceptOption) func(message *tb.Query) { - hm := &handlerQueryInterceptor{handler: handler} - for _, opt := range option { - opt(hm) - } - return func(query *tb.Query) { - ctx := context.Background() - ctx, err := interceptQuery(context.Background(), query, hm.before) - if err != nil { - log.Traceln(err) - return - } - defer interceptQuery(ctx, query, hm.onDefer) - ctx, err = hm.handler(ctx, query) - if err != nil { - log.Traceln(err) - return - } - _, err = interceptQuery(ctx, query, hm.after) - if err != nil { - log.Traceln(err) - return - } - } -} diff --git a/internal/telegram/interceptor.go b/internal/telegram/interceptor.go index 62ce3a1e..3aeb9f86 100644 --- a/internal/telegram/interceptor.go +++ b/internal/telegram/interceptor.go @@ -3,7 +3,6 @@ package telegram import ( "context" "fmt" - "reflect" "strconv" "github.com/LightningTipBot/LightningTipBot/internal/errors" @@ -17,19 +16,10 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" -) - -type InterceptorType int - -const ( - MessageInterceptor InterceptorType = iota - CallbackInterceptor - QueryInterceptor + tb "gopkg.in/lightningtipbot/telebot.v3" ) type Interceptor struct { - Type InterceptorType Before []intercept.Func After []intercept.Func OnDefer []intercept.Func @@ -37,36 +27,35 @@ type Interceptor struct { // 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)) +func (bot TipBot) singletonCallbackInterceptor(ctx intercept.Context) (intercept.Context, error) { + if ctx.Callback() != nil { + return ctx, once.Once(ctx.Callback().Data, strconv.FormatInt(ctx.Callback().Sender.ID, 10)) } return ctx, errors.Create(errors.InvalidTypeError) } // lockInterceptor invoked as first before interceptor -func (bot TipBot) lockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - user := getTelegramUserFromInterface(i) +func (bot TipBot) lockInterceptor(ctx intercept.Context) (intercept.Context, error) { + user := ctx.Sender() if user != nil { mutex.Lock(strconv.FormatInt(user.ID, 10)) return ctx, nil } - return nil, errors.Create(errors.InvalidTypeError) + return ctx, errors.Create(errors.InvalidTypeError) } // unlockInterceptor invoked as onDefer interceptor -func (bot TipBot) unlockInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - user := getTelegramUserFromInterface(i) +func (bot TipBot) unlockInterceptor(ctx intercept.Context) (intercept.Context, error) { + user := ctx.Sender() if user != nil { mutex.Unlock(strconv.FormatInt(user.ID, 10)) return ctx, nil } return ctx, errors.Create(errors.InvalidTypeError) } -func (bot TipBot) idInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - return context.WithValue(ctx, "uid", RandStringRunes(64)), nil +func (bot TipBot) idInterceptor(ctx intercept.Context) (intercept.Context, error) { + ctx.Context = context.WithValue(ctx, "uid", RandStringRunes(64)) + return ctx, nil } // answerCallbackInterceptor will answer the callback with the given text in the context @@ -92,30 +81,32 @@ func (bot TipBot) answerCallbackInterceptor(ctx context.Context, i interface{}) // requireUserInterceptor will return an error if user is not found // user is here an lnbits.User -func (bot TipBot) requireUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { +func (bot TipBot) requireUserInterceptor(ctx intercept.Context) (intercept.Context, error) { var user *lnbits.User var err error - u := getTelegramUserFromInterface(i) + u := ctx.Sender() if u != nil { user, err = GetUser(u, bot) // do not respond to banned users if bot.UserIsBanned(user) { - ctx = context.WithValue(ctx, "banned", true) - return context.WithValue(ctx, "user", user), errors.Create(errors.InvalidTypeError) + ctx.Context = context.WithValue(ctx, "banned", true) + ctx.Context = context.WithValue(ctx, "user", user) + return ctx, errors.Create(errors.InvalidTypeError) } if user != nil { - return context.WithValue(ctx, "user", user), err + ctx.Context = context.WithValue(ctx, "user", user) + return ctx, err } } - return nil, errors.Create(errors.InvalidTypeError) + return ctx, errors.Create(errors.InvalidTypeError) } // startUserInterceptor will invoke /start if user not exists. -func (bot TipBot) startUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - ctx, err := bot.loadUserInterceptor(ctx, i) +func (bot TipBot) startUserInterceptor(ctx intercept.Context) (intercept.Context, error) { + handler, err := bot.loadUserInterceptor(ctx) if err != nil { // user banned - return ctx, err + return handler, err } // load user u := ctx.Value("user") @@ -124,49 +115,34 @@ func (bot TipBot) startUserInterceptor(ctx context.Context, i interface{}) (cont user := u.(*lnbits.User) // check wallet nil or !initialized if user.Wallet == nil || !user.Initialized { - ctx, err = bot.startHandler(ctx, i.(*tb.Message)) + handler, err = bot.startHandler(handler) if err != nil { - return ctx, err + return handler, err } - return ctx, nil + return handler, nil } } - return ctx, nil + return handler, nil } -func (bot TipBot) loadUserInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - ctx, _ = bot.requireUserInterceptor(ctx, i) +func (bot TipBot) loadUserInterceptor(ctx intercept.Context) (intercept.Context, error) { + ctx, _ = bot.requireUserInterceptor(ctx) // if user is banned, also loadUserInterceptor will return an error if ctx.Value("banned") != nil && ctx.Value("banned").(bool) { - return nil, errors.Create(errors.InvalidTypeError) + return ctx, errors.Create(errors.InvalidTypeError) } return ctx, nil } -// getTelegramUserFromInterface returns the tb user based in interface type -func getTelegramUserFromInterface(i interface{}) (user *tb.User) { - switch i.(type) { - case *tb.Query: - user = &i.(*tb.Query).From - case *tb.Callback: - user = i.(*tb.Callback).Sender - case *tb.Message: - user = i.(*tb.Message).Sender - default: - log.Tracef("[getTelegramUserFromInterface] invalid type %s", reflect.TypeOf(i).String()) - } - return -} - // loadReplyToInterceptor Loading the Telegram user with message intercept -func (bot TipBot) loadReplyToInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - switch i.(type) { - case *tb.Message: - m := i.(*tb.Message) - if m.ReplyTo != nil { - if m.ReplyTo.Sender != nil { - user, _ := GetUser(m.ReplyTo.Sender, bot) - user.Telegram = m.ReplyTo.Sender - return context.WithValue(ctx, "reply_to_user", user), nil +func (bot TipBot) loadReplyToInterceptor(ctx intercept.Context) (intercept.Context, error) { + if ctx.Message() != nil { + if ctx.Message().ReplyTo != nil { + if ctx.Message().ReplyTo.Sender != nil { + user, _ := GetUser(ctx.Message().ReplyTo.Sender, bot) + user.Telegram = ctx.Message().ReplyTo.Sender + ctx.Context = context.WithValue(ctx, "reply_to_user", user) + return ctx, nil + } } return ctx, nil @@ -174,75 +150,71 @@ func (bot TipBot) loadReplyToInterceptor(ctx context.Context, i interface{}) (co return ctx, errors.Create(errors.InvalidTypeError) } -func (bot TipBot) localizerInterceptor(ctx context.Context, i interface{}) (context.Context, error) { +func (bot TipBot) localizerInterceptor(ctx intercept.Context) (intercept.Context, error) { var userLocalizer *i18n2.Localizer var publicLocalizer *i18n2.Localizer // default language is english publicLocalizer = i18n2.NewLocalizer(i18n.Bundle, "en") - ctx = context.WithValue(ctx, "publicLanguageCode", "en") - ctx = context.WithValue(ctx, "publicLocalizer", publicLocalizer) - - switch i.(type) { - case *tb.Message: - m := i.(*tb.Message) - userLocalizer = i18n2.NewLocalizer(i18n.Bundle, m.Sender.LanguageCode) - ctx = context.WithValue(ctx, "userLanguageCode", m.Sender.LanguageCode) - ctx = context.WithValue(ctx, "userLocalizer", userLocalizer) - if m.Private() { + ctx.Context = context.WithValue(ctx, "publicLanguageCode", "en") + ctx.Context = context.WithValue(ctx, "publicLocalizer", publicLocalizer) + + if ctx.Message() != nil { + userLocalizer = i18n2.NewLocalizer(i18n.Bundle, ctx.Message().Sender.LanguageCode) + ctx.Context = context.WithValue(ctx, "userLanguageCode", ctx.Message().Sender.LanguageCode) + ctx.Context = context.WithValue(ctx, "userLocalizer", userLocalizer) + if ctx.Message().Private() { // in pm overwrite public localizer with user localizer - ctx = context.WithValue(ctx, "publicLanguageCode", m.Sender.LanguageCode) - ctx = context.WithValue(ctx, "publicLocalizer", userLocalizer) + ctx.Context = context.WithValue(ctx, "publicLanguageCode", ctx.Message().Sender.LanguageCode) + ctx.Context = context.WithValue(ctx, "publicLocalizer", userLocalizer) } return ctx, nil - case *tb.Callback: - m := i.(*tb.Callback) - userLocalizer = i18n2.NewLocalizer(i18n.Bundle, m.Sender.LanguageCode) - ctx = context.WithValue(ctx, "userLanguageCode", m.Sender.LanguageCode) - return context.WithValue(ctx, "userLocalizer", userLocalizer), nil - case *tb.Query: - m := i.(*tb.Query) - userLocalizer = i18n2.NewLocalizer(i18n.Bundle, m.From.LanguageCode) - ctx = context.WithValue(ctx, "userLanguageCode", m.From.LanguageCode) - return context.WithValue(ctx, "userLocalizer", userLocalizer), nil + } else if ctx.Callback() != nil { + userLocalizer = i18n2.NewLocalizer(i18n.Bundle, ctx.Callback().Sender.LanguageCode) + ctx.Context = context.WithValue(ctx, "userLanguageCode", ctx.Callback().Sender.LanguageCode) + ctx.Context = context.WithValue(ctx, "userLocalizer", userLocalizer) + return ctx, nil + } else if ctx.Query() != nil { + userLocalizer = i18n2.NewLocalizer(i18n.Bundle, ctx.Query().Sender.LanguageCode) + ctx.Context = context.WithValue(ctx, "userLanguageCode", ctx.Query().Sender.LanguageCode) + ctx.Context = context.WithValue(ctx, "userLocalizer", userLocalizer) + return ctx, nil } + return ctx, nil } -func (bot TipBot) requirePrivateChatInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - switch i.(type) { - case *tb.Message: - m := i.(*tb.Message) - if m.Chat.Type != tb.ChatPrivate { - return nil, fmt.Errorf("[requirePrivateChatInterceptor] no private chat") +func (bot TipBot) requirePrivateChatInterceptor(ctx intercept.Context) (intercept.Context, error) { + if ctx.Message() != nil { + if ctx.Message().Chat.Type != tb.ChatPrivate { + return ctx, fmt.Errorf("[requirePrivateChatInterceptor] no private chat") } return ctx, nil } - return nil, errors.Create(errors.InvalidTypeError) + return ctx, errors.Create(errors.InvalidTypeError) } const photoTag = "" -func (bot TipBot) logMessageInterceptor(ctx context.Context, i interface{}) (context.Context, error) { - switch i.(type) { - case *tb.Message: - m := i.(*tb.Message) - if m.Text != "" { - log_string := fmt.Sprintf("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, m.Text) - if m.IsReply() { - log_string = fmt.Sprintf("%s -> %s", log_string, GetUserStr(m.ReplyTo.Sender)) +func (bot TipBot) logMessageInterceptor(ctx intercept.Context) (intercept.Context, error) { + if ctx.Message() != nil { + + if ctx.Message().Text != "" { + log_string := fmt.Sprintf("[%s:%d %s:%d] %s", ctx.Message().Chat.Title, ctx.Message().Chat.ID, GetUserStr(ctx.Message().Sender), ctx.Message().Sender.ID, ctx.Message().Text) + if ctx.Message().IsReply() { + log_string = fmt.Sprintf("%s -> %s", log_string, GetUserStr(ctx.Message().ReplyTo.Sender)) } log.Infof(log_string) - } else if m.Photo != nil { - log.Infof("[%s:%d %s:%d] %s", m.Chat.Title, m.Chat.ID, GetUserStr(m.Sender), m.Sender.ID, photoTag) + } else if ctx.Message().Photo != nil { + log.Infof("[%s:%d %s:%d] %s", ctx.Message().Chat.Title, ctx.Message().Chat.ID, GetUserStr(ctx.Message().Sender), ctx.Message().Sender.ID, photoTag) } return ctx, nil - case *tb.Callback: - m := i.(*tb.Callback) - log.Infof("[Callback %s:%d] Data: %s", GetUserStr(m.Sender), m.Sender.ID, m.Data) + } else if ctx.Callback() != nil { + log.Infof("[Callback %s:%d] Data: %s", GetUserStr(ctx.Callback().Sender), ctx.Callback().Sender.ID, ctx.Callback().Data) return ctx, nil + } - return nil, errors.Create(errors.InvalidTypeError) + return ctx, errors.Create(errors.InvalidTypeError) } // LoadUser from context diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 28e72c10..77a79bd3 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strings" "time" @@ -19,7 +20,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/skip2/go-qrcode" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) type InvoiceEventCallback map[int]EventHandler @@ -102,9 +103,10 @@ func helpInvoiceUsage(ctx context.Context, errormsg string) string { } } -func (bot *TipBot) invoiceHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) invoiceHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() // check and print all commands - bot.anyTextHandler(ctx, m) + bot.anyTextHandler(ctx) user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) diff --git a/internal/telegram/link.go b/internal/telegram/link.go index 4647a72a..0f6647f6 100644 --- a/internal/telegram/link.go +++ b/internal/telegram/link.go @@ -2,24 +2,25 @@ package telegram import ( "bytes" - "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "time" "github.com/LightningTipBot/LightningTipBot/internal" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) -func (bot *TipBot) lndhubHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) lndhubHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() if internal.Configuration.Lnbits.LnbitsPublicUrl == "" { bot.trySendMessage(m.Sender, Translate(ctx, "couldNotLinkMessage")) return ctx, fmt.Errorf("invalid configuration") } // check and print all commands - bot.anyTextHandler(ctx, m) + bot.anyTextHandler(ctx) // reply only in private message if m.Chat.Type != tb.ChatPrivate { // delete message diff --git a/internal/telegram/lnurl-auth.go b/internal/telegram/lnurl-auth.go index 30422cb8..2f408a65 100644 --- a/internal/telegram/lnurl-auth.go +++ b/internal/telegram/lnurl-auth.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "net/url" "github.com/LightningTipBot/LightningTipBot/internal/errors" @@ -17,11 +18,11 @@ import ( lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) var ( - authConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + authConfirmationMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnCancelAuth = paymentConfirmationMenu.Data("🚫 Cancel", "cancel_login") btnAuth = paymentConfirmationMenu.Data("✅ Login", "confirm_login") ) @@ -69,7 +70,8 @@ func (bot *TipBot) lnurlAuthHandler(ctx context.Context, m *tb.Message, authPara return ctx, nil } -func (bot *TipBot) confirmLnurlAuthHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) confirmLnurlAuthHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() tx := &LnurlAuthState{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -118,7 +120,7 @@ func (bot *TipBot) confirmLnurlAuthHandler(ctx context.Context, c *tb.Callback) return ctx, err } if sentsigres.Status == "ERROR" { - bot.tryEditMessage(c.Message, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), sentsigres.Reason)) + bot.tryEditMessage(c, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), sentsigres.Reason)) return ctx, err } bot.editSingleButton(ctx, c.Message, lnurlAuthState.Message.Text, Translate(ctx, "lnurlSuccessfulLogin")) @@ -126,7 +128,8 @@ func (bot *TipBot) confirmLnurlAuthHandler(ctx context.Context, c *tb.Callback) } // cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot *TipBot) cancelLnurlAuthHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) cancelLnurlAuthHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() tx := &LnurlAuthState{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -143,7 +146,7 @@ func (bot *TipBot) cancelLnurlAuthHandler(ctx context.Context, c *tb.Callback) ( return ctx, errors.Create(errors.UnknownError) } // delete and send instead of edit for the keyboard to pop up after sending - bot.tryEditMessage(c.Message, i18n.Translate(lnurlAuthState.LanguageCode, "loginCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c, i18n.Translate(lnurlAuthState.LanguageCode, "loginCancelledMessage"), &tb.ReplyMarkup{}) // bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) return ctx, lnurlAuthState.Inactivate(lnurlAuthState, bot.Bunt) } diff --git a/internal/telegram/lnurl-pay.go b/internal/telegram/lnurl-pay.go index f85e4ca1..2c78cd94 100644 --- a/internal/telegram/lnurl-pay.go +++ b/internal/telegram/lnurl-pay.go @@ -1,9 +1,9 @@ package telegram import ( - "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "io/ioutil" "net/url" "strconv" @@ -18,7 +18,6 @@ import ( lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" ) // LnurlPayState saves the state of the user for an LNURL payment @@ -34,7 +33,8 @@ type LnurlPayState struct { // lnurlPayHandler1 is invoked when the first lnurl response was a lnurlpay response // at this point, the user hans't necessarily entered an amount yet -func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams *LnurlPayState) { +func (bot *TipBot) lnurlPayHandler(ctx intercept.Context, payParams *LnurlPayState) { + m := ctx.Message() user := LoadUser(ctx) if user.Wallet == nil { return @@ -92,7 +92,7 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams return } - // We need to save the pay state in the user state so we can load the payment in the next handler + // We need to save the pay state in the user state so we can load the payment in the next ctx paramsJson, err := json.Marshal(payParams) if err != nil { log.Errorf("[lnurlPayHandler] Error: %s", err.Error()) @@ -101,12 +101,13 @@ func (bot *TipBot) lnurlPayHandler(ctx context.Context, m *tb.Message, payParams } SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(paramsJson)) // directly go to confirm - bot.lnurlPayHandlerSend(ctx, m) + bot.lnurlPayHandlerSend(ctx) return } // lnurlPayHandlerSend is invoked when the user has delivered an amount and is ready to pay -func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) lnurlPayHandlerSend(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -194,10 +195,11 @@ func (bot *TipBot) lnurlPayHandlerSend(ctx context.Context, m *tb.Message) (cont runtime.IgnoreError(lnurlPayState.Set(lnurlPayState, bot.Bunt)) bot.Telegram.Delete(statusMsg) m.Text = fmt.Sprintf("/pay %s", response2.PR) - return bot.payHandler(ctx, m) + return bot.payHandler(ctx) } -func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, address string, amount int64) (context.Context, error) { +func (bot *TipBot) sendToLightningAddress(ctx intercept.Context, address string, amount int64) (intercept.Context, error) { + m := ctx.Message() split := strings.Split(address, "@") if len(split) != 2 { return ctx, fmt.Errorf("lightning address format wrong") @@ -232,6 +234,5 @@ func (bot *TipBot) sendToLightningAddress(ctx context.Context, m *tb.Message, ad // this will invoke the "enter amount" dialog in the lnurl handler m.Text = fmt.Sprintf("/lnurl %s", lnurl) } - bot.lnurlHandler(ctx, m) - return ctx, nil + return bot.lnurlHandler(ctx) } diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index 3bc5e96f..6c5f31e7 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "io/ioutil" "net/url" @@ -20,11 +21,11 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) var ( - withdrawConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + withdrawConfirmationMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnCancelWithdraw = paymentConfirmationMenu.Data("🚫 Cancel", "cancel_withdraw") btnWithdraw = paymentConfirmationMenu.Data("✅ Withdraw", "confirm_withdraw") ) @@ -58,7 +59,8 @@ func (bot *TipBot) editSingleButton(ctx context.Context, m *tb.Message, message // lnurlWithdrawHandler is invoked when the first lnurl response was a lnurl-withdraw response // at this point, the user hans't necessarily entered an amount yet -func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, withdrawParams *LnurlWithdrawState) { +func (bot *TipBot) lnurlWithdrawHandler(ctx intercept.Context, withdrawParams *LnurlWithdrawState) { + m := ctx.Message() user := LoadUser(ctx) if user.Wallet == nil { return @@ -104,7 +106,7 @@ func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, with return } - // We need to save the pay state in the user state so we can load the payment in the next handler + // We need to save the pay state in the user state so we can load the payment in the next ctx paramsJson, err := json.Marshal(withdrawParams) if err != nil { log.Errorf("[lnurlWithdrawHandler] Error: %s", err.Error()) @@ -113,12 +115,13 @@ func (bot *TipBot) lnurlWithdrawHandler(ctx context.Context, m *tb.Message, with } SetUserState(user, bot, lnbits.UserHasEnteredAmount, string(paramsJson)) // directly go to confirm - bot.lnurlWithdrawHandlerWithdraw(ctx, m) + bot.lnurlWithdrawHandlerWithdraw(ctx) return } // lnurlWithdrawHandlerWithdraw is invoked when the user has delivered an amount and is ready to pay -func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -186,7 +189,8 @@ func (bot *TipBot) lnurlWithdrawHandlerWithdraw(ctx context.Context, m *tb.Messa } // confirmPayHandler when user clicked pay on payment confirmation -func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) confirmWithdrawHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() tx := &LnurlWithdrawState{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -210,15 +214,15 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) ( } if !lnurlWithdrawState.Active { log.Errorf("[confirmPayHandler] send not active anymore") - bot.tryEditMessage(c.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) - bot.tryDeleteMessage(c.Message) + bot.tryEditMessage(c, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) + bot.tryDeleteMessage(c) return ctx, errors.Create(errors.NotActiveError) } defer lnurlWithdrawState.Set(lnurlWithdrawState, bot.Bunt) user := LoadUser(ctx) if user.Wallet == nil { - bot.tryDeleteMessage(c.Message) + bot.tryDeleteMessage(c) return ctx, errors.Create(errors.UserNoWalletError) } @@ -231,7 +235,7 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) ( callbackUrl, err := url.Parse(lnurlWithdrawState.LNURLWithdrawResponse.Callback) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) - // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + // bot.trySendMessage(c.Sender, Translate(handler.Ctx, "errorTryLaterMessage")) bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) return ctx, err } @@ -270,14 +274,14 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) ( res, err := client.Get(callbackUrl.String()) if err != nil || res.StatusCode >= 300 { log.Errorf("[lnurlWithdrawHandlerWithdraw] Failed.") - // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + // bot.trySendMessage(c.Sender, Translate(handler.Ctx, "errorTryLaterMessage")) bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) return ctx, errors.New(errors.UnknownError, err) } body, err := ioutil.ReadAll(res.Body) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) - // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) + // bot.trySendMessage(c.Sender, Translate(handler.Ctx, "errorTryLaterMessage")) bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) return ctx, err } @@ -303,7 +307,8 @@ func (bot *TipBot) confirmWithdrawHandler(ctx context.Context, c *tb.Callback) ( } // cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) cancelWithdrawHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() // reset state immediately user := LoadUser(ctx) ResetUserState(user, bot) @@ -326,6 +331,6 @@ func (bot *TipBot) cancelWithdrawHandler(ctx context.Context, c *tb.Callback) (c if lnurlWithdrawState.From.Telegram.ID != c.Sender.ID { return ctx, errors.Create(errors.UnknownError) } - bot.tryEditMessage(c.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawCancelled"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawCancelled"), &tb.ReplyMarkup{}) return ctx, lnurlWithdrawState.Inactivate(lnurlWithdrawState, bot.Bunt) } diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 15069b88..8b0bb375 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -2,8 +2,8 @@ package telegram import ( "bytes" - "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "io/ioutil" "net/http" "net/url" @@ -18,7 +18,7 @@ import ( lnurl "github.com/fiatjaf/go-lnurl" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) func (bot *TipBot) GetHttpClient() (*http.Client, error) { @@ -37,11 +37,12 @@ func (bot TipBot) cancelLnUrlHandler(c *tb.Callback) { } // lnurlHandler is invoked on /lnurl command -func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) lnurlHandler(ctx intercept.Context) (intercept.Context, error) { // commands: // /lnurl // /lnurl // or /lnurl + m := ctx.Message() if m.Chat.Type != tb.ChatPrivate { return ctx, errors.Create(errors.NoPrivateChatError) } @@ -53,7 +54,7 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con // if only /lnurl is entered, show the lnurl of the user if m.Text == "/lnurl" { - return bot.lnurlReceiveHandler(ctx, m) + return bot.lnurlReceiveHandler(ctx) } statusMsg := bot.trySendMessageEditable(m.Sender, Translate(ctx, "lnurlResolvingUrlMessage")) @@ -89,7 +90,8 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con authParams := &LnurlAuthState{LNURLAuthParams: params.(lnurl.LNURLAuthParams)} log.Infof("[LNURL-auth] %s", authParams.LNURLAuthParams.Callback) bot.tryDeleteMessage(statusMsg) - return bot.lnurlAuthHandler(ctx, m, authParams) + ctx.Context, err = bot.lnurlAuthHandler(ctx, m, authParams) + return ctx, err case lnurl.LNURLPayParams: payParams := &LnurlPayState{LNURLPayParams: params.(lnurl.LNURLPayParams)} @@ -107,13 +109,13 @@ func (bot *TipBot) lnurlHandler(ctx context.Context, m *tb.Message) (context.Con bot.trySendMessage(m.Sender, fmt.Sprintf("`%s`", payParams.LNURLPayParams.Metadata.Description)) } // ask whether to make payment - bot.lnurlPayHandler(ctx, m, payParams) + bot.lnurlPayHandler(ctx, payParams) case lnurl.LNURLWithdrawResponse: withdrawParams := &LnurlWithdrawState{LNURLWithdrawResponse: params.(lnurl.LNURLWithdrawResponse)} log.Infof("[LNURL-w] %s", withdrawParams.LNURLWithdrawResponse.Callback) bot.tryDeleteMessage(statusMsg) - bot.lnurlWithdrawHandler(ctx, m, withdrawParams) + bot.lnurlWithdrawHandler(ctx, withdrawParams) default: if err == nil { err = fmt.Errorf("invalid LNURL type") @@ -152,7 +154,8 @@ func UserGetLNURL(user *lnbits.User) (string, error) { } // lnurlReceiveHandler outputs the LNURL of the user -func (bot TipBot) lnurlReceiveHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot TipBot) lnurlReceiveHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() fromUser := LoadUser(ctx) lnurlEncode, err := UserGetLNURL(fromUser) if err != nil { diff --git a/internal/telegram/message.go b/internal/telegram/message.go index b1341cef..fac16bca 100644 --- a/internal/telegram/message.go +++ b/internal/telegram/message.go @@ -4,7 +4,7 @@ import ( "strconv" "time" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) type Message struct { diff --git a/internal/telegram/pay.go b/internal/telegram/pay.go index 92af9196..0cc4dd1e 100644 --- a/internal/telegram/pay.go +++ b/internal/telegram/pay.go @@ -3,6 +3,7 @@ package telegram import ( "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strings" "github.com/LightningTipBot/LightningTipBot/internal/errors" @@ -17,11 +18,11 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" decodepay "github.com/fiatjaf/ln-decodepay" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) var ( - paymentConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + paymentConfirmationMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnCancelPay = paymentConfirmationMenu.Data("🚫 Cancel", "cancel_pay") btnPay = paymentConfirmationMenu.Data("✅ Pay", "confirm_pay") ) @@ -48,23 +49,23 @@ type PayData struct { } // payHandler invoked on "/pay lnbc..." command -func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) payHandler(ctx intercept.Context) (intercept.Context, error) { // check and print all commands - bot.anyTextHandler(ctx, m) + bot.anyTextHandler(ctx) user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) } - if len(strings.Split(m.Text, " ")) < 2 { - NewMessage(m, WithDuration(0, bot)) - bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, "")) + if len(strings.Split(ctx.Message().Text, " ")) < 2 { + NewMessage(ctx.Message(), WithDuration(0, bot)) + bot.trySendMessage(ctx.Sender(), helpPayInvoiceUsage(ctx, "")) return ctx, errors.Create(errors.InvalidSyntaxError) } - userStr := GetUserStr(m.Sender) - paymentRequest, err := getArgumentFromCommand(m.Text, 1) + userStr := GetUserStr(ctx.Sender()) + paymentRequest, err := getArgumentFromCommand(ctx.Message().Text, 1) if err != nil { - NewMessage(m, WithDuration(0, bot)) - bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) + NewMessage(ctx.Message(), WithDuration(0, bot)) + bot.trySendMessage(ctx.Sender(), helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) errmsg := fmt.Sprintf("[/pay] Error: Could not getArgumentFromCommand: %s", err.Error()) log.Errorln(errmsg) return ctx, errors.New(errors.InvalidSyntaxError, err) @@ -76,7 +77,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte // decode invoice bolt11, err := decodepay.Decodepay(paymentRequest) if err != nil { - bot.trySendMessage(m.Sender, helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) + bot.trySendMessage(ctx.Sender(), helpPayInvoiceUsage(ctx, Translate(ctx, "invalidInvoiceHelpMessage"))) errmsg := fmt.Sprintf("[/pay] Error: Could not decode invoice: %s", err.Error()) log.Errorln(errmsg) return ctx, errors.New(errors.InvalidSyntaxError, err) @@ -84,7 +85,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte amount := int64(bolt11.MSatoshi / 1000) if amount <= 0 { - bot.trySendMessage(m.Sender, Translate(ctx, "invoiceNoAmountMessage")) + bot.trySendMessage(ctx.Sender(), Translate(ctx, "invoiceNoAmountMessage")) errmsg := fmt.Sprint("[/pay] Error: invoice without amount") log.Warnln(errmsg) return ctx, errors.Create(errors.InvalidAmountError) @@ -93,21 +94,21 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte // check user balance first balance, err := bot.GetUserBalance(user) if err != nil { - NewMessage(m, WithDuration(0, bot)) + NewMessage(ctx.Message(), WithDuration(0, bot)) errmsg := fmt.Sprintf("[/pay] Error: Could not get user balance: %s", err.Error()) log.Errorln(errmsg) - bot.trySendMessage(m.Sender, Translate(ctx, "errorTryLaterMessage")) + bot.trySendMessage(ctx.Sender(), Translate(ctx, "errorTryLaterMessage")) return ctx, errors.New(errors.GetBalanceError, err) } if amount > balance { - NewMessage(m, WithDuration(0, bot)) - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "insufficientFundsMessage"), balance, amount)) + NewMessage(ctx.Message(), WithDuration(0, bot)) + bot.trySendMessage(ctx.Sender(), fmt.Sprintf(Translate(ctx, "insufficientFundsMessage"), balance, amount)) return ctx, errors.Create(errors.InvalidSyntaxError) } // send warning that the invoice might fail due to missing fee reserve if float64(amount) > float64(balance)*0.98 { - bot.trySendMessage(m.Sender, Translate(ctx, "feeReserveMessage")) + bot.trySendMessage(ctx.Sender(), Translate(ctx, "feeReserveMessage")) } confirmText := fmt.Sprintf(Translate(ctx, "confirmPayInvoiceMessage"), amount) @@ -118,7 +119,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte log.Infof("[/pay] Invoice entered. User: %s, amount: %d sat.", userStr, amount) // object that holds all information about the send payment - id := fmt.Sprintf("pay:%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) + id := fmt.Sprintf("pay:%d-%d-%s", ctx.Sender().ID, amount, RandStringRunes(5)) // // // create inline buttons payButton := paymentConfirmationMenu.Data(Translate(ctx, "payButtonMessage"), "confirm_pay", id) @@ -129,7 +130,7 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte payButton, cancelButton), ) - payMessage := bot.trySendMessageEditable(m.Chat, confirmText, paymentConfirmationMenu) + payMessage := bot.trySendMessageEditable(ctx.Chat(), confirmText, paymentConfirmationMenu) payData := &PayData{ Base: storage.New(storage.ID(id)), From: user, @@ -148,8 +149,8 @@ func (bot *TipBot) payHandler(ctx context.Context, m *tb.Message) (context.Conte } // confirmPayHandler when user clicked pay on payment confirmation -func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { - tx := &PayData{Base: storage.New(storage.ID(c.Data))} +func (bot *TipBot) confirmPayHandler(ctx intercept.Context) (intercept.Context, error) { + tx := &PayData{Base: storage.New(storage.ID(ctx.Data()))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) @@ -161,23 +162,23 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) (conte payData := sn.(*PayData) // onnly the correct user can press - if payData.From.Telegram.ID != c.Sender.ID { + if payData.From.Telegram.ID != ctx.Sender().ID { return ctx, errors.Create(errors.UnknownError) } if !payData.Active { log.Errorf("[confirmPayHandler] send not active anymore") - bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) - bot.tryDeleteMessage(c.Message) + bot.tryEditMessage(ctx.Message(), i18n.Translate(payData.LanguageCode, "errorTryLaterMessage"), &tb.ReplyMarkup{}) + bot.tryDeleteMessage(ctx.Message()) return ctx, errors.Create(errors.NotActiveError) } defer payData.Set(payData, bot.Bunt) // remove buttons from confirmation message - // bot.tryEditMessage(c.Message, MarkdownEscape(payData.Message), &tb.ReplyMarkup{}) + // bot.tryEditMessage(handler.Message(), MarkdownEscape(payData.Message), &tb.ReplyMarkup{}) user := LoadUser(ctx) if user.Wallet == nil { - bot.tryDeleteMessage(c.Message) + bot.tryDeleteMessage(ctx.Message()) return ctx, errors.Create(errors.UserNoWalletError) } @@ -186,11 +187,11 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) (conte // reset state immediately ResetUserState(user, bot) - userStr := GetUserStr(c.Sender) + userStr := GetUserStr(ctx.Sender()) // update button text bot.tryEditMessage( - c.Message, + ctx.Message(), payData.Message, &tb.ReplyMarkup{ InlineKeyboard: [][]tb.InlineButton{ @@ -205,7 +206,7 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) (conte if err != nil { errmsg := fmt.Sprintf("[/pay] Could not pay invoice of %s: %s", userStr, err) err = fmt.Errorf(i18n.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) - bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), err.Error()), &tb.ReplyMarkup{}) + bot.tryEditMessage(ctx.Message(), fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), err.Error()), &tb.ReplyMarkup{}) // verbose error message, turned off for now // if len(err.Error()) == 0 { // err = fmt.Errorf(i18n.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) @@ -223,27 +224,27 @@ func (bot *TipBot) confirmPayHandler(ctx context.Context, c *tb.Callback) (conte log.Errorln(errmsg) } - if c.Message.Private() { + if ctx.Message().Private() { // if the command was invoked in private chat // the edit below was cool, but we need to pop up the keyboard again // bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "invoicePaidMessage"), &tb.ReplyMarkup{}) - bot.tryDeleteMessage(c.Message) - bot.trySendMessage(c.Sender, i18n.Translate(payData.LanguageCode, "invoicePaidMessage")) + bot.tryDeleteMessage(ctx.Message()) + bot.trySendMessage(ctx.Sender(), i18n.Translate(payData.LanguageCode, "invoicePaidMessage")) } else { // if the command was invoked in group chat - bot.trySendMessage(c.Sender, i18n.Translate(payData.LanguageCode, "invoicePaidMessage")) - bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePublicPaidMessage"), userStr), &tb.ReplyMarkup{}) + bot.trySendMessage(ctx.Sender(), i18n.Translate(payData.LanguageCode, "invoicePaidMessage")) + bot.tryEditMessage(ctx.Message(), fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePublicPaidMessage"), userStr), &tb.ReplyMarkup{}) } log.Infof("[⚡️ pay] User %s paid invoice %s (%d sat)", userStr, payData.ID, payData.Amount) return ctx, nil } // cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) cancelPaymentHandler(ctx intercept.Context) (intercept.Context, error) { // reset state immediately user := LoadUser(ctx) ResetUserState(user, bot) - tx := &PayData{Base: storage.New(storage.ID(c.Data))} + tx := &PayData{Base: storage.New(storage.ID(ctx.Data()))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) // immediatelly set intransaction to block duplicate calls @@ -254,12 +255,12 @@ func (bot *TipBot) cancelPaymentHandler(ctx context.Context, c *tb.Callback) (co } payData := sn.(*PayData) // onnly the correct user can press - if payData.From.Telegram.ID != c.Sender.ID { + if payData.From.Telegram.ID != ctx.Callback().Sender.ID { return ctx, errors.Create(errors.UnknownError) } // delete and send instead of edit for the keyboard to pop up after sending - bot.tryDeleteMessage(c.Message) - bot.trySendMessage(c.Message.Chat, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage")) + bot.tryDeleteMessage(ctx.Message()) + bot.trySendMessage(ctx.Message().Chat, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage")) // bot.tryEditMessage(c.Message, i18n.Translate(payData.LanguageCode, "paymentCancelledMessage"), &tb.ReplyMarkup{}) return ctx, payData.Inactivate(payData, bot.Bunt) diff --git a/internal/telegram/photo.go b/internal/telegram/photo.go index b35b4092..fd914750 100644 --- a/internal/telegram/photo.go +++ b/internal/telegram/photo.go @@ -2,9 +2,9 @@ package telegram import ( "bytes" - "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "image" "image/jpeg" "strings" @@ -17,10 +17,10 @@ import ( "github.com/nfnt/resize" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) -// TryRecognizeInvoiceFromQrCode will try to read an invoice string from a qr code and invoke the payment handler. +// TryRecognizeInvoiceFromQrCode will try to read an invoice string from a qr code and invoke the payment ctx. func TryRecognizeQrCode(img image.Image) (*gozxing.Result, error) { // check for qr code bmp, _ := gozxing.NewBinaryBitmapFromImage(img) @@ -33,14 +33,15 @@ func TryRecognizeQrCode(img image.Image) (*gozxing.Result, error) { payload := strings.ToLower(result.String()) if lightning.IsInvoice(payload) || lightning.IsLnurl(payload) { // create payment command payload - // invoke payment confirmation handler + // invoke payment confirmation ctx return result, nil } return nil, fmt.Errorf("no codes found") } // 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) (context.Context, error) { +func (bot *TipBot) photoHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() if m.Chat.Type != tb.ChatPrivate { return ctx, errors.Create(errors.NoPrivateChatError) } @@ -49,13 +50,13 @@ func (bot *TipBot) photoHandler(ctx context.Context, m *tb.Message) (context.Con } user := LoadUser(ctx) if c := stateCallbackMessage[user.StateKey]; c != nil { - ctx, err := c(ctx, m) + ctx, err := c(ctx) ResetUserState(user, bot) return ctx, err } // get file reader closer from Telegram api - reader, err := bot.Telegram.GetFile(m.Photo.MediaFile()) + reader, err := bot.Telegram.File(m.Photo.MediaFile()) if err != nil { log.Errorf("[photoHandler] getfile error: %v\n", err.Error()) return ctx, err @@ -77,10 +78,10 @@ func (bot *TipBot) photoHandler(ctx context.Context, m *tb.Message) (context.Con // invoke payment handler if lightning.IsInvoice(data.String()) { m.Text = fmt.Sprintf("/pay %s", data.String()) - return bot.payHandler(ctx, m) + return bot.payHandler(ctx) } else if lightning.IsLnurl(data.String()) { m.Text = fmt.Sprintf("/lnurl %s", data.String()) - return bot.lnurlHandler(ctx, m) + return bot.lnurlHandler(ctx) } return ctx, nil } @@ -98,7 +99,7 @@ func DownloadProfilePicture(telegram *tb.Bot, user *tb.User) ([]byte, error) { return nil, err } buf := new(bytes.Buffer) - reader, err := telegram.GetFile(&photo[0].File) + reader, err := telegram.File(&photo[0].File) if err != nil { log.Errorf("[DownloadProfilePicture] %v", err) return nil, err diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 695d258b..63bf3e4f 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strings" "github.com/LightningTipBot/LightningTipBot/internal/errors" @@ -18,11 +19,11 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) var ( - sendConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true} + sendConfirmationMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnCancelSend = sendConfirmationMenu.Data("🚫 Cancel", "cancel_send") btnSend = sendConfirmationMenu.Data("✅ Send", "confirm_send") ) @@ -55,8 +56,8 @@ type SendData struct { } // sendHandler invoked on "/send 123 @user" command -func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Context, error) { - bot.anyTextHandler(ctx, m) +func (bot *TipBot) sendHandler(ctx intercept.Context) (intercept.Context, error) { + bot.anyTextHandler(ctx) user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -68,8 +69,8 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Cont // check and print all commands // If the send is a reply, then trigger /tip handler - if m.IsReply() && m.Chat.Type != tb.ChatPrivate { - return bot.tipHandler(ctx, m) + if ctx.Message().IsReply() && ctx.Message().Chat.Type != tb.ChatPrivate { + return bot.tipHandler(ctx) } @@ -80,21 +81,21 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Cont // } // get send amount, returns 0 if no amount is given - amount, err := decodeAmountFromCommand(m.Text) + amount, err := decodeAmountFromCommand(ctx.Text()) // info: /send 10 DEMANDS an amount, while /send also works without // todo: /send should also invoke amount input dialog if no amount is given // CHECK whether first or second argument is a LIGHTNING ADDRESS arg := "" - if len(strings.Split(m.Text, " ")) > 2 { - arg, err = getArgumentFromCommand(m.Text, 2) - } else if len(strings.Split(m.Text, " ")) == 2 { - arg, err = getArgumentFromCommand(m.Text, 1) + if len(strings.Split(ctx.Message().Text, " ")) > 2 { + arg, err = getArgumentFromCommand(ctx.Message().Text, 2) + } else if len(strings.Split(ctx.Message().Text, " ")) == 2 { + arg, err = getArgumentFromCommand(ctx.Message().Text, 1) } if err == nil { if lightning.IsLightningAddress(arg) { // lightning address, send to that address - ctx, err = bot.sendToLightningAddress(ctx, m, arg, amount) + ctx, err = bot.sendToLightningAddress(ctx, arg, amount) if err != nil { log.Errorln(err.Error()) return ctx, err @@ -104,16 +105,16 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Cont } // is a user given? - arg, err = getArgumentFromCommand(m.Text, 1) - if err != nil && m.Chat.Type == tb.ChatPrivate { - _, err = bot.askForUser(ctx, "", "CreateSendState", m.Text) + arg, err = getArgumentFromCommand(ctx.Message().Text, 1) + if err != nil && ctx.Message().Chat.Type == tb.ChatPrivate { + _, err = bot.askForUser(ctx, "", "CreateSendState", ctx.Message().Text) return ctx, err } // is an amount given? - amount, err = decodeAmountFromCommand(m.Text) - if (err != nil || amount < 1) && m.Chat.Type == tb.ChatPrivate { - _, err = bot.askForAmount(ctx, "", "CreateSendState", 0, 0, m.Text) + amount, err = decodeAmountFromCommand(ctx.Message().Text) + if (err != nil || amount < 1) && ctx.Message().Chat.Type == tb.ChatPrivate { + _, err = bot.askForAmount(ctx, "", "CreateSendState", 0, 0, ctx.Message().Text) return ctx, err } @@ -122,24 +123,24 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Cont errmsg := fmt.Sprintf("[/send] Error: Send amount not valid.") log.Warnln(errmsg) // immediately delete if the amount is bullshit - NewMessage(m, WithDuration(0, bot)) - bot.trySendMessage(m.Sender, helpSendUsage(ctx, Translate(ctx, "sendValidAmountMessage"))) + NewMessage(ctx.Message(), WithDuration(0, bot)) + bot.trySendMessage(ctx.Sender(), helpSendUsage(ctx, Translate(ctx, "sendValidAmountMessage"))) return ctx, err } // SEND COMMAND IS VALID // check for memo in command - sendMemo := GetMemoFromCommand(m.Text, 3) + sendMemo := GetMemoFromCommand(ctx.Message().Text, 3) toUserStrMention := "" toUserStrWithoutAt := "" // check for user in command, accepts user mention or plain username without @ - if len(m.Entities) > 1 && m.Entities[1].Type == "mention" { - toUserStrMention = m.Text[m.Entities[1].Offset : m.Entities[1].Offset+m.Entities[1].Length] + if len(ctx.Message().Entities) > 1 && ctx.Message().Entities[1].Type == "mention" { + toUserStrMention = ctx.Message().Text[ctx.Message().Entities[1].Offset : ctx.Message().Entities[1].Offset+ctx.Message().Entities[1].Length] toUserStrWithoutAt = strings.TrimPrefix(toUserStrMention, "@") } else { - toUserStrWithoutAt, err = getArgumentFromCommand(m.Text, 2) + toUserStrWithoutAt, err = getArgumentFromCommand(ctx.Message().Text, 2) if err != nil { log.Errorln(err.Error()) return ctx, err @@ -148,24 +149,24 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Cont toUserStrMention = "@" + toUserStrWithoutAt } - err = bot.parseCmdDonHandler(ctx, m) + err = bot.parseCmdDonHandler(ctx) if err == nil { return ctx, errors.Create(errors.InvalidSyntaxError) } toUserDb, err := GetUserByTelegramUsername(toUserStrWithoutAt, *bot) if err != nil { - NewMessage(m, WithDuration(0, bot)) + NewMessage(ctx.Message(), WithDuration(0, bot)) // cut username if it's too long if len(toUserStrMention) > 100 { toUserStrMention = toUserStrMention[:100] } - bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), str.MarkdownEscape(toUserStrMention))) + bot.trySendMessage(ctx.Message().Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), str.MarkdownEscape(toUserStrMention))) return ctx, err } if user.ID == toUserDb.ID { - bot.trySendMessage(m.Sender, Translate(ctx, "sendYourselfMessage")) + bot.trySendMessage(ctx.Message().Sender, Translate(ctx, "sendYourselfMessage")) return ctx, errors.Create(errors.SelfPaymentError) } @@ -175,7 +176,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Cont confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmSendAppendMemo"), str.MarkdownEscape(sendMemo)) } // object that holds all information about the send payment - id := fmt.Sprintf("send-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5)) + id := fmt.Sprintf("send-%d-%d-%s", ctx.Message().Sender.ID, amount, RandStringRunes(5)) sendData := &SendData{ From: user, Base: storage.New(storage.ID(id)), @@ -191,9 +192,9 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Cont sendDataJson, err := json.Marshal(sendData) if err != nil { - NewMessage(m, WithDuration(0, bot)) + NewMessage(ctx.Message(), WithDuration(0, bot)) log.Printf("[/send] Error: %s\n", err.Error()) - bot.trySendMessage(m.Sender, fmt.Sprint(Translate(ctx, "errorTryLaterMessage"))) + bot.trySendMessage(ctx.Message().Sender, fmt.Sprint(Translate(ctx, "errorTryLaterMessage"))) return ctx, err } // save the send data to the Database @@ -209,10 +210,10 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Cont sendButton, cancelButton), ) - if m.Private() { - bot.trySendMessage(m.Chat, confirmText, sendConfirmationMenu) + if ctx.Message().Private() { + bot.trySendMessage(ctx.Chat(), confirmText, sendConfirmationMenu) } else { - bot.tryReplyMessage(m, confirmText, sendConfirmationMenu) + bot.tryReplyMessage(ctx.Message(), confirmText, sendConfirmationMenu) } return ctx, nil } @@ -221,7 +222,7 @@ func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) (context.Cont // it will pop up a new keyboard with the last interacted contacts to send funds to // then, the flow is handled as if the user entered /send (then ask for contacts from keyboard or entry, // then ask for an amount). -func (bot *TipBot) keyboardSendHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) keyboardSendHandler(ctx intercept.Context) (intercept.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -243,8 +244,8 @@ func (bot *TipBot) keyboardSendHandler(ctx context.Context, m *tb.Message) (cont // if no contact is found (one entry will always be inside, it's the enter user button) // immediatelly go to the send handler if len(sendToButtons) == 1 { - m.Text = "/send" - return bot.sendHandler(ctx, m) + ctx.Message().Text = "/send" + return bot.sendHandler(ctx) } // Attention! We need to ues the original Telegram.Send command here! @@ -258,8 +259,8 @@ func (bot *TipBot) keyboardSendHandler(ctx context.Context, m *tb.Message) (cont } // sendHandler invoked when user clicked send on payment confirmation -func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { - tx := &SendData{Base: storage.New(storage.ID(c.Data))} +func (bot *TipBot) confirmSendHandler(ctx intercept.Context) (intercept.Context, error) { + tx := &SendData{Base: storage.New(storage.ID(ctx.Data()))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) sn, err := tx.Get(tx, bot.Bunt) @@ -269,7 +270,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) (cont } sendData := sn.(*SendData) // onnly the correct user can press - if sendData.From.Telegram.ID != c.Sender.ID { + if sendData.From.Telegram.ID != ctx.Callback().Sender.ID { return ctx, errors.Create(errors.UnknownError) } if !sendData.Active { @@ -297,7 +298,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) (cont to, err := GetLnbitsUser(&tb.User{ID: toId, Username: toUserStrWithoutAt}, *bot) if err != nil { log.Errorln(err.Error()) - bot.tryDeleteMessage(c.Message) + bot.tryDeleteMessage(ctx.Callback().Message) return ctx, err } toUserStrMd := GetUserStrMd(to.Telegram) @@ -314,7 +315,7 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) (cont // bot.trySendMessage(c.Sender, sendErrorMessage) errmsg := fmt.Sprintf("[/send] Error: Transaction failed. %s", err.Error()) log.Errorln(errmsg) - bot.tryEditMessage(c.Message, i18n.Translate(sendData.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(ctx.Callback().Message, i18n.Translate(sendData.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{}) return ctx, errors.Create(errors.UnknownError) } sendData.Inactivate(sendData, bot.Bunt) @@ -324,16 +325,16 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) (cont // notify to user bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount)) // bot.trySendMessage(from.Telegram, fmt.Sprintf(Translate(ctx, "sendSentMessage"), amount, toUserStrMd)) - if c.Message.Private() { + if ctx.Callback().Message.Private() { // if the command was invoked in private chat // the edit below was cool, but we need to get rid of the replymarkup inline keyboard thingy for the main menu to pop up // bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd), &tb.ReplyMarkup{}) - bot.tryDeleteMessage(c.Message) - bot.trySendMessage(c.Sender, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) + bot.tryDeleteMessage(ctx.Callback().Message) + bot.trySendMessage(ctx.Callback().Sender, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) } else { // if the command was invoked in group chat - bot.trySendMessage(c.Sender, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) - bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendPublicSentMessage"), amount, fromUserStrMd, toUserStrMd), &tb.ReplyMarkup{}) + bot.trySendMessage(ctx.Callback().Sender, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd)) + bot.tryEditMessage(ctx.Callback().Message, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendPublicSentMessage"), amount, fromUserStrMd, toUserStrMd), &tb.ReplyMarkup{}) } // send memo if it was present if len(sendMemo) > 0 { @@ -344,8 +345,9 @@ func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) (cont } // cancelPaymentHandler invoked when user clicked cancel on payment confirmation -func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) cancelSendHandler(ctx intercept.Context) (intercept.Context, error) { // reset state immediately + c := ctx.Callback() user := LoadUser(ctx) ResetUserState(user, bot) tx := &SendData{Base: storage.New(storage.ID(c.Data))} @@ -363,7 +365,7 @@ func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) (conte return ctx, errors.Create(errors.UnknownError) } // delete and send instead of edit for the keyboard to pop up after sending - bot.tryDeleteMessage(c.Message) + bot.tryDeleteMessage(c) bot.trySendMessage(c.Message.Chat, i18n.Translate(sendData.LanguageCode, "sendCancelledMessage")) sendData.Inactivate(sendData, bot.Bunt) return ctx, nil diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 01ebcef9..115e199e 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/i18n" @@ -16,7 +18,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) type ShopView struct { @@ -26,7 +28,6 @@ type ShopView struct { Page int Message *tb.Message StatusMessages []*tb.Message - Chat *tb.Chat } type ShopItem struct { @@ -81,7 +82,7 @@ func (shop *Shop) getItem(itemId string) (item ShopItem, ok bool) { } var ( - shopKeyboard = &tb.ReplyMarkup{ResizeReplyKeyboard: false} + shopKeyboard = &tb.ReplyMarkup{ResizeKeyboard: false} browseShopButton = shopKeyboard.Data("Browse shops", "shops_browse") shopNewShopButton = shopKeyboard.Data("New Shop", "shops_newshop") shopDeleteShopButton = shopKeyboard.Data("Delete Shops", "shops_deleteshop") @@ -114,7 +115,8 @@ var ( ) // shopItemPriceHandler is invoked when the user presses the item settings button to set a price -func (bot *TipBot) shopItemPriceHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopItemPriceHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopItemPriceHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -138,7 +140,8 @@ func (bot *TipBot) shopItemPriceHandler(ctx context.Context, c *tb.Callback) (co } // enterShopItemPriceHandler is invoked when the user enters a price amount -func (bot *TipBot) enterShopItemPriceHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) enterShopItemPriceHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() log.Debugf("[enterShopItemPriceHandler] %s", m.Text) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -172,9 +175,9 @@ func (bot *TipBot) enterShopItemPriceHandler(ctx context.Context, m *tb.Message) } } - if amount > 200 { - bot.sendStatusMessageAndDelete(ctx, m.Sender, fmt.Sprintf("ℹ️ During alpha testing, price can be max 200 sat.")) - amount = 200 + if amount > 2000 { + bot.sendStatusMessageAndDelete(ctx, m.Sender, "ℹ️ During testing, price can be max 2000 sat.") + amount = 2000 } item.Price = amount shop.Items[item.ID] = item @@ -191,7 +194,8 @@ func (bot *TipBot) enterShopItemPriceHandler(ctx context.Context, m *tb.Message) } // shopItemPriceHandler is invoked when the user presses the item settings button to set a item title -func (bot *TipBot) shopItemTitleHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopItemTitleHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopItemTitleHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -215,7 +219,8 @@ func (bot *TipBot) shopItemTitleHandler(ctx context.Context, c *tb.Callback) (co } // enterShopItemTitleHandler is invoked when the user enters a title of the item -func (bot *TipBot) enterShopItemTitleHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) enterShopItemTitleHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() log.Debugf("[enterShopItemTitleHandler] %s", m.Text) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -264,7 +269,8 @@ func (bot *TipBot) enterShopItemTitleHandler(ctx context.Context, m *tb.Message) } // shopItemSettingsHandler is invoked when the user presses the item settings button -func (bot *TipBot) shopItemSettingsHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopItemSettingsHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopItemSettingsHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -286,7 +292,8 @@ func (bot *TipBot) shopItemSettingsHandler(ctx context.Context, c *tb.Callback) } // shopItemPriceHandler is invoked when the user presses the item settings button to set a item title -func (bot *TipBot) shopItemDeleteHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopItemDeleteHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopItemDeleteHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -335,7 +342,8 @@ func (bot *TipBot) shopItemDeleteHandler(ctx context.Context, c *tb.Callback) (c } // displayShopItemHandler is invoked when the user presses the back button in the item settings -func (bot *TipBot) displayShopItemHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) displayShopItemHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[displayShopItemHandler] c.Data: %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -357,7 +365,8 @@ func (bot *TipBot) displayShopItemHandler(ctx context.Context, c *tb.Callback) ( } // shopNextItemHandler is invoked when the user presses the next item button -func (bot *TipBot) shopNextItemButtonHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopNextItemButtonHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopNextItemButtonHandler] c.Data: %s", c.Data) user := LoadUser(ctx) // shopView, err := bot.Cache.Get(fmt.Sprintf("shopview-%d", user.Telegram.ID)) @@ -379,7 +388,8 @@ func (bot *TipBot) shopNextItemButtonHandler(ctx context.Context, c *tb.Callback } // shopPrevItemButtonHandler is invoked when the user presses the previous item button -func (bot *TipBot) shopPrevItemButtonHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopPrevItemButtonHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopPrevItemButtonHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -388,7 +398,7 @@ func (bot *TipBot) shopPrevItemButtonHandler(ctx context.Context, c *tb.Callback } if shopView.Page == 0 { c.Message.Text = "/shops " + shopView.ShopOwner.Telegram.Username - return bot.shopsHandler(ctx, c.Message) + return bot.shopsHandler(ctx) } if shopView.Page > 0 { @@ -421,9 +431,9 @@ func (bot *TipBot) getItemTitle(ctx context.Context, item *ShopItem) string { // displayShopItem renders the current item in the shopView // requires that the shopview page is already set accordingly // m is the message that will be edited -func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Shop) *tb.Message { +func (bot *TipBot) displayShopItem(ctx intercept.Context, m *tb.Message, shop *Shop) *tb.Message { user := LoadUser(ctx) - log.Debugf("[displayShopItem] User: %d shop: %s", GetUserStr(user.Telegram), shop.ID) + log.Debugf("[displayShopItem] User: %s shop: %s", GetUserStr(user.Telegram), shop.ID) shopView, err := bot.getUserShopview(ctx, user) if err != nil { log.Errorf("[displayShopItem] %s", err.Error()) @@ -445,7 +455,7 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho if shopView.Message != nil { bot.tryDeleteMessage(shopView.Message) } - shopView.Message = bot.trySendMessage(shopView.Chat, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) + shopView.Message = bot.trySendMessage(shopView.Message.Chat, no_items_message, bot.shopMenu(ctx, shop, &ShopItem{})) } shopView.Page = 0 return shopView.Message @@ -487,7 +497,8 @@ func (bot *TipBot) displayShopItem(ctx context.Context, m *tb.Message, shop *Sho } // shopHandler is invoked when the user enters /shop -func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) shopHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() log.Debugf("[shopHandler] %s", m.Text) if !m.Private() { return ctx, errors.Create(errors.NoPrivateChatError) @@ -498,7 +509,7 @@ func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) (context.Cont // when no argument is given, i.e. command is only /shop, load /shops shop := &Shop{} if len(strings.Split(m.Text, " ")) < 2 || !strings.HasPrefix(strings.Split(m.Text, " ")[1], "shop-") { - return bot.shopsHandler(ctx, m) + return bot.shopsHandler(ctx) } else { // else: get shop by shop ID shopID := strings.Split(m.Text, " ")[1] @@ -515,7 +526,6 @@ func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) (context.Cont ShopID: shop.ID, Page: 0, ShopOwner: shopOwner, - Chat: m.Chat, } bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) shopView.Message = bot.displayShopItem(ctx, m, shop) @@ -533,7 +543,8 @@ func (bot *TipBot) shopHandler(ctx context.Context, m *tb.Message) (context.Cont } // shopNewItemHandler is invoked when the user presses the new item button -func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopNewItemHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopNewItemHandler] %s", c.Data) user := LoadUser(ctx) shop, err := bot.getShop(ctx, c.Data) @@ -562,7 +573,7 @@ func (bot *TipBot) shopNewItemHandler(ctx context.Context, c *tb.Callback) (cont } // addShopItem is a helper function for creating a shop item in the database -func (bot *TipBot) addShopItem(ctx context.Context, shopId string) (*Shop, ShopItem, error) { +func (bot *TipBot) addShopItem(ctx intercept.Context, shopId string) (*Shop, ShopItem, error) { log.Debugf("[addShopItem] shopId: %s", shopId) shop, err := bot.getShop(ctx, shopId) if err != nil { @@ -593,7 +604,8 @@ func (bot *TipBot) addShopItem(ctx context.Context, shopId string) (*Shop, ShopI } // addShopItemPhoto is invoked when the users sends a photo as a new item -func (bot *TipBot) addShopItemPhoto(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) addShopItemPhoto(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() log.Debugf("[addShopItemPhoto] ") user := LoadUser(ctx) if user.Wallet == nil { @@ -644,7 +656,8 @@ func (bot *TipBot) addShopItemPhoto(ctx context.Context, m *tb.Message) (context // ------------------- item files ---------- // shopItemAddItemHandler is invoked when the user presses the new item button -func (bot *TipBot) shopItemAddItemHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopItemAddItemHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopItemAddItemHandler] %s", c.Data) user := LoadUser(ctx) if user.Wallet == nil { @@ -676,7 +689,8 @@ func (bot *TipBot) shopItemAddItemHandler(ctx context.Context, c *tb.Callback) ( } // addItemFileHandler is invoked when the users sends a new file for the item -func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) addItemFileHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() log.Debugf("[addItemFileHandler] ") user := LoadUser(ctx) if user.Wallet == nil { @@ -769,7 +783,8 @@ func (bot *TipBot) addItemFileHandler(ctx context.Context, m *tb.Message) (conte return ctx, nil } -func (bot *TipBot) shopGetItemFilesHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopGetItemFilesHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopGetItemFilesHandler] %s", c.Data) user := LoadUser(ctx) if user.Wallet == nil { @@ -808,7 +823,8 @@ func (bot *TipBot) shopGetItemFilesHandler(ctx context.Context, c *tb.Callback) } // shopConfirmBuyHandler is invoked when the user has confirmed to pay for an item -func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopConfirmBuyHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopConfirmBuyHandler] %s", c.Data) user := LoadUser(ctx) if user.Wallet == nil { @@ -851,7 +867,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) (c // bot.trySendMessage(c.Sender, sendErrorMessage) errmsg := fmt.Sprintf("[shop] Error: Transaction failed. %s", err.Error()) log.Errorln(errmsg) - ctx = context.WithValue(ctx, "callback_response", i18n.Translate(user.Telegram.LanguageCode, "sendErrorMessage")) + ctx.Context = context.WithValue(ctx, "callback_response", i18n.Translate(user.Telegram.LanguageCode, "sendErrorMessage")) // bot.trySendMessage(user.Telegram, i18n.Translate(user.Telegram.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{}) return ctx, errors.New(errors.UnknownError, err) } @@ -860,7 +876,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) (c if len(item.Title) > 0 { shopItemTitle = fmt.Sprintf("%s", item.Title) } - ctx = context.WithValue(ctx, "callback_response", "🛍 Purchase successful.") + ctx.Context = context.WithValue(ctx, "callback_response", "🛍 Purchase successful.") bot.trySendMessage(to.Telegram, fmt.Sprintf("🛍 Someone bought `%s` from your shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), str.MarkdownEscape(shop.Title), amount)) bot.trySendMessage(from.Telegram, fmt.Sprintf("🛍 You bought `%s` from %s's shop `%s` for `%d sat`.", str.MarkdownEscape(shopItemTitle), toUserStrMd, str.MarkdownEscape(shop.Title), amount)) log.Infof("[🛍 shop] %s bought from %s shop: %s item: %s for %d sat.", toUserStr, GetUserStr(to.Telegram), shop.Title, shopItemTitle, amount) @@ -869,7 +885,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx context.Context, c *tb.Callback) (c } // shopSendItemFilesToUser is a handler function to send itemID's files to the user -func (bot *TipBot) shopSendItemFilesToUser(ctx context.Context, toUser *lnbits.User, itemID string) { +func (bot *TipBot) shopSendItemFilesToUser(ctx intercept.Context, toUser *lnbits.User, itemID string) { log.Debugf("[shopSendItemFilesToUser] %s -> %s", GetUserStr(toUser.Telegram), itemID) user := LoadUser(ctx) if user.Wallet == nil { @@ -939,12 +955,13 @@ var ShopsTextHelp = "⚠️ Shops are still in beta. Expect bugs." var ShopsNoShopsText = "*There are no shops here yet.*" // shopsHandlerCallback is a warpper for shopsHandler for callbacks -func (bot *TipBot) shopsHandlerCallback(ctx context.Context, c *tb.Callback) (context.Context, error) { - return bot.shopsHandler(ctx, c.Message) +func (bot *TipBot) shopsHandlerCallback(ctx intercept.Context) (intercept.Context, error) { + return bot.shopsHandler(ctx) } // shopsHandler is invoked when the user enters /shops -func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) shopsHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() log.Debugf("[shopsHandler] %s", GetUserStr(m.Sender)) if !m.Private() { return ctx, errors.Create(errors.NoPrivateChatError) @@ -1085,7 +1102,8 @@ func (bot *TipBot) shopsHandler(ctx context.Context, m *tb.Message) (context.Con } // shopsDeleteShopBrowser is invoked when the user clicks on "delete shops" and makes a list of all shops -func (bot *TipBot) shopsDeleteShopBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopsDeleteShopBrowser(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopsDeleteShopBrowser] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) @@ -1108,7 +1126,8 @@ func (bot *TipBot) shopsDeleteShopBrowser(ctx context.Context, c *tb.Callback) ( return ctx, err } -func (bot *TipBot) shopsAskDeleteAllShopsHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopsAskDeleteAllShopsHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopsAskDeleteAllShopsHandler] %s", c.Data) shopResetShopButton := shopKeyboard.Data("⚠️ Delete all shops", "shops_reset", c.Data) buttons := []tb.Row{ @@ -1123,7 +1142,8 @@ func (bot *TipBot) shopsAskDeleteAllShopsHandler(ctx context.Context, c *tb.Call } // shopsLinkShopBrowser is invoked when the user clicks on "shop links" and makes a list of all shops -func (bot *TipBot) shopsLinkShopBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopsLinkShopBrowser(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopsLinkShopBrowser] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) @@ -1145,7 +1165,8 @@ func (bot *TipBot) shopsLinkShopBrowser(ctx context.Context, c *tb.Callback) (co } // shopSelectLink is invoked when the user has chosen a shop to get the link of -func (bot *TipBot) shopSelectLink(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopSelectLink(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopSelectLink] %s", c.Data) shop, _ := bot.getShop(ctx, c.Data) if shop.Owner.Telegram.ID != c.Sender.ID { @@ -1156,7 +1177,8 @@ func (bot *TipBot) shopSelectLink(ctx context.Context, c *tb.Callback) (context. } // shopsLinkShopBrowser is invoked when the user clicks on "shop links" and makes a list of all shops -func (bot *TipBot) shopsRenameShopBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopsRenameShopBrowser(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopsRenameShopBrowser] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) @@ -1178,7 +1200,8 @@ func (bot *TipBot) shopsRenameShopBrowser(ctx context.Context, c *tb.Callback) ( } // shopSelectLink is invoked when the user has chosen a shop to get the link of -func (bot *TipBot) shopSelectRename(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopSelectRename(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopSelectRename] %s", c.Data) user := LoadUser(ctx) shop, _ := bot.getShop(ctx, c.Data) @@ -1192,7 +1215,8 @@ func (bot *TipBot) shopSelectRename(ctx context.Context, c *tb.Callback) (contex } // shopsDescriptionHandler is invoked when the user clicks on "description" to set a shop description -func (bot *TipBot) shopsDescriptionHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopsDescriptionHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopsDescriptionHandler] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) @@ -1206,7 +1230,8 @@ func (bot *TipBot) shopsDescriptionHandler(ctx context.Context, c *tb.Callback) } // enterShopsDescriptionHandler is invoked when the user enters the shop title -func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) enterShopsDescriptionHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() log.Debugf("[enterShopsDescriptionHandler] %s", m.Text) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) @@ -1239,13 +1264,14 @@ func (bot *TipBot) enterShopsDescriptionHandler(ctx context.Context, m *tb.Messa // time.Sleep(time.Duration(5) * time.Second) // bot.shopViewDeleteAllStatusMsgs(ctx, user) // }() - bot.shopsHandler(ctx, m) + bot.shopsHandler(ctx) bot.tryDeleteMessage(m) return ctx, nil } // shopsResetHandler is invoked when the user clicks button to reset shops completely -func (bot *TipBot) shopsResetHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopsResetHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopsResetHandler] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) @@ -1262,11 +1288,12 @@ func (bot *TipBot) shopsResetHandler(ctx context.Context, c *tb.Callback) (conte // time.Sleep(time.Duration(5) * time.Second) // bot.shopViewDeleteAllStatusMsgs(ctx, user) // }() - return bot.shopsHandlerCallback(ctx, c) + return bot.shopsHandlerCallback(ctx) } // shopSelect is invoked when the user has selected a shop to browse -func (bot *TipBot) shopSelect(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopSelect(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopSelect] %s", c.Data) shop, _ := bot.getShop(ctx, c.Data) user := LoadUser(ctx) @@ -1292,12 +1319,13 @@ func (bot *TipBot) shopSelect(ctx context.Context, c *tb.Callback) (context.Cont // } shopView.Message = shopMessage log.Infof("[🛍 shop] %s erntering shop %s.", GetUserStr(user.Telegram), shop.ID) - ctx = context.WithValue(ctx, "callback_response", fmt.Sprintf("🛍 You are browsing %s", shop.Title)) + ctx.Context = context.WithValue(ctx, "callback_response", fmt.Sprintf("🛍 You are browsing %s", shop.Title)) return ctx, bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) } // shopSelectDelete is invoked when the user has chosen a shop to delete -func (bot *TipBot) shopSelectDelete(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopSelectDelete(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopSelectDelete] %s", c.Data) shop, _ := bot.getShop(ctx, c.Data) user := LoadUser(ctx) @@ -1323,11 +1351,12 @@ func (bot *TipBot) shopSelectDelete(ctx context.Context, c *tb.Callback) (contex log.Infof("[🛍 shop] %s deleted shop %s.", GetUserStr(user.Telegram), shop.ID) // then update buttons - return bot.shopsDeleteShopBrowser(ctx, c) + return bot.shopsDeleteShopBrowser(ctx) } // shopsBrowser makes a button list of all shops the user can browse -func (bot *TipBot) shopsBrowser(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopsBrowser(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopsBrowser] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -1356,7 +1385,8 @@ func (bot *TipBot) shopsBrowser(ctx context.Context, c *tb.Callback) (context.Co } // shopItemSettingsHandler is invoked when the user presses the shop settings button -func (bot *TipBot) shopSettingsHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopSettingsHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopSettingsHandler] %s", c.Data) user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) @@ -1376,7 +1406,8 @@ func (bot *TipBot) shopSettingsHandler(ctx context.Context, c *tb.Callback) (con } // shopNewShopHandler is invoked when the user presses the new shop button -func (bot *TipBot) shopNewShopHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) shopNewShopHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() log.Debugf("[shopNewShopHandler] %s", c.Data) user := LoadUser(ctx) shops, err := bot.getUserShops(ctx, user) @@ -1396,7 +1427,8 @@ func (bot *TipBot) shopNewShopHandler(ctx context.Context, c *tb.Callback) (cont } // enterShopTitleHandler is invoked when the user enters the shop title -func (bot *TipBot) enterShopTitleHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) enterShopTitleHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() log.Debugf("[enterShopTitleHandler] %s", m.Text) user := LoadUser(ctx) // read item from user.StateData @@ -1429,7 +1461,10 @@ func (bot *TipBot) enterShopTitleHandler(ctx context.Context, m *tb.Message) (co // time.Sleep(time.Duration(5) * time.Second) // bot.shopViewDeleteAllStatusMsgs(ctx, user) // }() - bot.shopsHandler(ctx, m) + ctx, err = bot.shopsHandler(ctx) + if err != nil { + log.Errorf("[shop] failed shopshandler") + } bot.tryDeleteMessage(m) log.Infof("[🛍 shop] %s added new shop %s.", GetUserStr(user.Telegram), shop.ID) return ctx, nil diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index c04c2479..3cb52edf 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -1,22 +1,19 @@ package telegram import ( - "context" "fmt" - "time" - - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" - "github.com/LightningTipBot/LightningTipBot/internal/storage" - "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" - + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" + "time" ) -func (bot TipBot) shopsMainMenu(ctx context.Context, shops *Shops) *tb.ReplyMarkup { +func (bot TipBot) shopsMainMenu(ctx intercept.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) @@ -35,7 +32,7 @@ func (bot TipBot) shopsMainMenu(ctx context.Context, shops *Shops) *tb.ReplyMark return shopKeyboard } -func (bot TipBot) shopsSettingsMenu(ctx context.Context, shops *Shops) *tb.ReplyMarkup { +func (bot TipBot) shopsSettingsMenu(ctx intercept.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) @@ -66,7 +63,7 @@ func (bot TipBot) shopsSettingsMenu(ctx context.Context, shops *Shops) *tb.Reply } // shopItemSettingsMenu builds the buttons of the item settings -func (bot TipBot) shopItemSettingsMenu(ctx context.Context, shop *Shop, item *ShopItem) *tb.ReplyMarkup { +func (bot TipBot) shopItemSettingsMenu(ctx intercept.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) @@ -86,7 +83,7 @@ func (bot TipBot) shopItemSettingsMenu(ctx context.Context, shop *Shop, item *Sh } // shopItemConfirmBuyMenu builds the buttons to confirm a purchase -func (bot TipBot) shopItemConfirmBuyMenu(ctx context.Context, shop *Shop, item *ShopItem) *tb.ReplyMarkup { +func (bot TipBot) shopItemConfirmBuyMenu(ctx intercept.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{} @@ -99,7 +96,7 @@ func (bot TipBot) shopItemConfirmBuyMenu(ctx context.Context, shop *Shop, item * } // shopMenu builds the buttons in the item browser -func (bot TipBot) shopMenu(ctx context.Context, shop *Shop, item *ShopItem) *tb.ReplyMarkup { +func (bot TipBot) shopMenu(ctx intercept.Context, shop *Shop, item *ShopItem) *tb.ReplyMarkup { user := LoadUser(ctx) shopView, err := bot.getUserShopview(ctx, user) if err != nil { @@ -152,7 +149,7 @@ func (bot *TipBot) makseShopSelectionButtons(shops []*Shop, uniqueString string) // -------------- 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) { +func (bot *TipBot) getUserShopview(ctx intercept.Context, user *lnbits.User) (shopView ShopView, err error) { sv, err := bot.Cache.Get(fmt.Sprintf("shopview-%d", user.Telegram.ID)) if err != nil { return @@ -160,7 +157,7 @@ func (bot *TipBot) getUserShopview(ctx context.Context, user *lnbits.User) (shop shopView = sv.(ShopView) return } -func (bot *TipBot) shopViewDeleteAllStatusMsgs(ctx context.Context, user *lnbits.User) (shopView ShopView, err error) { +func (bot *TipBot) shopViewDeleteAllStatusMsgs(ctx intercept.Context, user *lnbits.User) (shopView ShopView, err error) { mutex.Lock(fmt.Sprintf("shopview-delete-%d", user.Telegram.ID)) defer mutex.Unlock(fmt.Sprintf("shopview-delete-%d", user.Telegram.ID)) shopView, err = bot.getUserShopview(ctx, user) @@ -182,7 +179,7 @@ func deleteStatusMessages(messages []*tb.Message, bot *TipBot) { // 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) { +func (bot *TipBot) sendStatusMessage(ctx intercept.Context, to tb.Recipient, what interface{}, options ...interface{}) (msg *tb.Message) { user := LoadUser(ctx) id := fmt.Sprintf("shopview-delete-%d", user.Telegram.ID) @@ -201,7 +198,7 @@ func (bot *TipBot) sendStatusMessage(ctx context.Context, to tb.Recipient, what // 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) { +func (bot *TipBot) sendStatusMessageAndDelete(ctx intercept.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...) @@ -222,7 +219,7 @@ func (bot *TipBot) sendStatusMessageAndDelete(ctx context.Context, to tb.Recipie // --------------- 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) { +func (bot *TipBot) initUserShops(ctx intercept.Context, user *lnbits.User) (*Shops, error) { id := fmt.Sprintf("shops-%d", user.Telegram.ID) shops := &Shops{ Base: storage.New(storage.ID(id)), @@ -235,7 +232,7 @@ func (bot *TipBot) initUserShops(ctx context.Context, user *lnbits.User) (*Shops } // getUserShops returns the Shops for the user -func (bot *TipBot) getUserShops(ctx context.Context, user *lnbits.User) (*Shops, error) { +func (bot *TipBot) getUserShops(ctx intercept.Context, user *lnbits.User) (*Shops, error) { tx := &Shops{Base: storage.New(storage.ID(fmt.Sprintf("shops-%d", user.Telegram.ID)))} sn, err := tx.Get(tx, bot.ShopBunt) if err != nil { @@ -247,7 +244,7 @@ func (bot *TipBot) getUserShops(ctx context.Context, user *lnbits.User) (*Shops, } // addUserShop adds a new Shop to the Shops of a user -func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, error) { +func (bot *TipBot) addUserShop(ctx intercept.Context, user *lnbits.User) (*Shop, error) { shops, err := bot.getUserShops(ctx, user) if err != nil { return &Shop{}, err @@ -270,7 +267,7 @@ func (bot *TipBot) addUserShop(ctx context.Context, user *lnbits.User) (*Shop, e } // getShop returns the Shop of a given ID -func (bot *TipBot) getShop(ctx context.Context, shopId string) (*Shop, error) { +func (bot *TipBot) getShop(ctx intercept.Context, shopId string) (*Shop, error) { tx := &Shop{Base: storage.New(storage.ID(shopId))} // immediatelly set intransaction to block duplicate calls sn, err := tx.Get(tx, bot.ShopBunt) diff --git a/internal/telegram/start.go b/internal/telegram/start.go index 5e0de372..f31a67e1 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -4,6 +4,7 @@ import ( "context" stderrors "errors" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strconv" "time" @@ -15,34 +16,34 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/str" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" "gorm.io/gorm" ) -func (bot TipBot) startHandler(ctx context.Context, m *tb.Message) (context.Context, error) { - if !m.Private() { +func (bot TipBot) startHandler(ctx intercept.Context) (intercept.Context, error) { + if !ctx.Message().Private() { return ctx, errors.Create(errors.NoPrivateChatError) } // ATTENTION: DO NOT CALL ANY HANDLER BEFORE THE WALLET IS CREATED // WILL RESULT IN AN ENDLESS LOOP OTHERWISE // bot.helpHandler(m) - log.Printf("[⭐️ /start] New user: %s (%d)\n", GetUserStr(m.Sender), m.Sender.ID) - walletCreationMsg := bot.trySendMessageEditable(m.Sender, Translate(ctx, "startSettingWalletMessage")) - user, err := bot.initWallet(m.Sender) + log.Printf("[⭐️ /start] New user: %s (%d)\n", GetUserStr(ctx.Sender()), ctx.Sender().ID) + walletCreationMsg := bot.trySendMessageEditable(ctx.Sender(), Translate(ctx, "startSettingWalletMessage")) + user, err := bot.initWallet(ctx.Sender()) if err != nil { log.Errorln(fmt.Sprintf("[startHandler] Error with initWallet: %s", err.Error())) bot.tryEditMessage(walletCreationMsg, Translate(ctx, "startWalletErrorMessage")) return ctx, err } bot.tryDeleteMessage(walletCreationMsg) - ctx = context.WithValue(ctx, "user", user) - bot.helpHandler(ctx, m) - bot.trySendMessage(m.Sender, Translate(ctx, "startWalletReadyMessage")) - bot.balanceHandler(ctx, m) + ctx.Context = context.WithValue(ctx, "user", user) + bot.helpHandler(ctx) + bot.trySendMessage(ctx.Sender(), Translate(ctx, "startWalletReadyMessage")) + bot.balanceHandler(ctx) // send the user a warning about the fact that they need to set a username - if len(m.Sender.Username) == 0 { - bot.trySendMessage(m.Sender, Translate(ctx, "startNoUsernameMessage"), tb.NoPreview) + if len(ctx.Sender().Username) == 0 { + bot.trySendMessage(ctx.Sender(), Translate(ctx, "startNoUsernameMessage"), tb.NoPreview) } return ctx, nil } diff --git a/internal/telegram/state.go b/internal/telegram/state.go index fd6ee125..f3135c2a 100644 --- a/internal/telegram/state.go +++ b/internal/telegram/state.go @@ -1,12 +1,11 @@ package telegram import ( - "context" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - tb "gopkg.in/lightningtipbot/telebot.v2" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" ) -type StateCallbackMessage map[lnbits.UserStateKey]func(ctx context.Context, m *tb.Message) (context.Context, error) +type StateCallbackMessage map[lnbits.UserStateKey]func(ctx intercept.Context) (intercept.Context, error) var stateCallbackMessage StateCallbackMessage diff --git a/internal/telegram/telegram.go b/internal/telegram/telegram.go index 5b1d95d0..fc25b2f0 100644 --- a/internal/telegram/telegram.go +++ b/internal/telegram/telegram.go @@ -8,7 +8,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/rate" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) // getChatIdFromRecipient will parse the recipient to int64 diff --git a/internal/telegram/text.go b/internal/telegram/text.go index 2d2c2098..37929fc9 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -5,15 +5,17 @@ import ( "encoding/json" "fmt" "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strings" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) -func (bot *TipBot) anyTextHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) anyTextHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() if m.Chat.Type != tb.ChatPrivate { return ctx, errors.Create(errors.NoPrivateChatError) } @@ -21,7 +23,7 @@ func (bot *TipBot) anyTextHandler(ctx context.Context, m *tb.Message) (context.C // check if user is in Database, if not, initialize wallet user := LoadUser(ctx) if user.Wallet == nil || !user.Initialized { - return bot.startHandler(ctx, m) + return bot.startHandler(ctx) } // check if the user clicked on the balance button @@ -30,21 +32,21 @@ func (bot *TipBot) anyTextHandler(ctx context.Context, m *tb.Message) (context.C // overwrite the message text so it doesn't cause an infinite loop // because balanceHandler calls anyTextHAndler... m.Text = "" - return bot.balanceHandler(ctx, m) + return bot.balanceHandler(ctx) } // could be an invoice anyText := strings.ToLower(m.Text) if lightning.IsInvoice(anyText) { m.Text = "/pay " + anyText - return bot.payHandler(ctx, m) + return bot.payHandler(ctx) } if lightning.IsLnurl(anyText) { m.Text = "/lnurl " + anyText - return bot.lnurlHandler(ctx, m) + return bot.lnurlHandler(ctx) } if c := stateCallbackMessage[user.StateKey]; c != nil { - return c(ctx, m) + return c(ctx) //ResetUserState(user, bot) } return ctx, nil @@ -83,8 +85,9 @@ func (bot *TipBot) askForUser(ctx context.Context, id string, eventType string, // enterAmountHandler is invoked in anyTextHandler when the user needs to enter an amount // the amount is then stored as an entry in the user's stateKey in the user database -// any other handler that relies on this, needs to load the resulting amount from the database -func (bot *TipBot) enterUserHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +// any other ctx that relies on this, needs to load the resulting amount from the database +func (bot *TipBot) enterUserHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() user := LoadUser(ctx) if user.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -114,7 +117,7 @@ func (bot *TipBot) enterUserHandler(ctx context.Context, m *tb.Message) (context switch EnterUserStateData.Type { case "CreateSendState": m.Text = fmt.Sprintf("/send %s", userstr) - return bot.sendHandler(ctx, m) + return bot.sendHandler(ctx) default: ResetUserState(user, bot) return ctx, errors.Create(errors.InvalidSyntaxError) diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index 21e19fd4..c79e9c80 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strings" "time" @@ -12,7 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/i18n" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) func helpTipUsage(ctx context.Context, errormsg string) string { @@ -31,9 +32,10 @@ func TipCheckSyntax(ctx context.Context, m *tb.Message) (bool, string) { return true, "" } -func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) (context.Context, error) { +func (bot *TipBot) tipHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() // check and print all commands - bot.anyTextHandler(ctx, m) + bot.anyTextHandler(ctx) user := LoadUser(ctx) if user.Wallet == nil { return ctx, fmt.Errorf("user has no wallet") @@ -64,7 +66,7 @@ func (bot *TipBot) tipHandler(ctx context.Context, m *tb.Message) (context.Conte return ctx, errors.Create(errors.InvalidAmountError) } - err = bot.parseCmdDonHandler(ctx, m) + err = bot.parseCmdDonHandler(ctx) if err == nil { return ctx, fmt.Errorf("invalid parseCmdDonHandler") } diff --git a/internal/telegram/tipjar.go b/internal/telegram/tipjar.go index 7abae3b6..a9b13ddd 100644 --- a/internal/telegram/tipjar.go +++ b/internal/telegram/tipjar.go @@ -6,6 +6,8 @@ import ( "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -16,11 +18,11 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) var ( - inlineTipjarMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: false} + inlineTipjarMenu = &tb.ReplyMarkup{ResizeKeyboard: false} btnCancelInlineTipjar = inlineTipjarMenu.Data("🚫", "cancel_tipjar_inline") btnAcceptInlineTipjar = inlineTipjarMenu.Data("💸 Pay", "confirm_tipjar_inline") ) @@ -131,28 +133,28 @@ func (bot TipBot) makeTipjar(ctx context.Context, m *tb.Message, query bool) (*I return tipjar, err } -func (bot TipBot) makeQueryTipjar(ctx context.Context, q *tb.Query, query bool) (*InlineTipjar, error) { - tipjar, err := bot.createTipjar(ctx, q.Text, &q.From) +func (bot TipBot) makeQueryTipjar(ctx intercept.Context) (*InlineTipjar, error) { + tipjar, err := bot.createTipjar(ctx, ctx.Query().Text, ctx.Query().Sender) if err != nil { switch err.(errors.TipBotError).Code { case errors.DecodeAmountError: - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryTipjarTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineQueryTipjarTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) return nil, err case errors.DecodePerUserAmountError: - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryTipjarTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineQueryTipjarTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) return nil, err case errors.InvalidAmountError: - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineSendInvalidAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) return nil, err case errors.InvalidAmountPerUserError: - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineTipjarInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineTipjarInvalidPeruserAmountMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) return nil, err case errors.GetBalanceError: - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineQueryTipjarTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineQueryTipjarTitle"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) return nil, err case errors.BalanceToLowError: log.Errorf(err.Error()) - bot.inlineQueryReplyWithError(q, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) + bot.inlineQueryReplyWithError(ctx, TranslateUser(ctx, "inlineSendBalanceLowMessage"), fmt.Sprintf(TranslateUser(ctx, "inlineQueryTipjarDescription"), bot.Telegram.Me.Username)) return nil, err } } @@ -160,7 +162,7 @@ func (bot TipBot) makeQueryTipjar(ctx context.Context, q *tb.Query, query bool) } func (bot TipBot) makeTipjarKeyboard(ctx context.Context, inlineTipjar *InlineTipjar) *tb.ReplyMarkup { - inlineTipjarMenu := &tb.ReplyMarkup{ResizeReplyKeyboard: true} + inlineTipjarMenu := &tb.ReplyMarkup{ResizeKeyboard: true} // slice of buttons buttons := make([]tb.Btn, 0) cancelInlineTipjarButton := inlineTipjarMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_tipjar_inline", inlineTipjar.ID) @@ -173,13 +175,14 @@ func (bot TipBot) makeTipjarKeyboard(ctx context.Context, inlineTipjar *InlineTi return inlineTipjarMenu } -func (bot TipBot) tipjarHandler(ctx context.Context, m *tb.Message) (context.Context, error) { - bot.anyTextHandler(ctx, m) +func (bot TipBot) tipjarHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() + bot.anyTextHandler(ctx) if m.Private() { bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "inlineTipjarHelpText"), Translate(ctx, "inlineTipjarHelpTipjarInGroup"))) return ctx, errors.Create(errors.NoPrivateChatError) } - ctx = bot.mapTipjarLanguage(ctx, m.Text) + ctx.Context = bot.mapTipjarLanguage(ctx, m.Text) inlineTipjar, err := bot.makeTipjar(ctx, m, false) if err != nil { log.Errorf("[tipjar] %s", err.Error()) @@ -191,8 +194,9 @@ func (bot TipBot) tipjarHandler(ctx context.Context, m *tb.Message) (context.Con return ctx, inlineTipjar.Set(inlineTipjar, bot.Bunt) } -func (bot TipBot) handleInlineTipjarQuery(ctx context.Context, q *tb.Query) (context.Context, error) { - inlineTipjar, err := bot.makeQueryTipjar(ctx, q, false) +func (bot TipBot) handleInlineTipjarQuery(ctx intercept.Context) (intercept.Context, error) { + q := ctx.Query() + inlineTipjar, err := bot.makeQueryTipjar(ctx) if err != nil { // log.Errorf("[tipjar] %s", err.Error()) return ctx, err @@ -210,7 +214,7 @@ func (bot TipBot) handleInlineTipjarQuery(ctx context.Context, q *tb.Query) (con // required for photos ThumbURL: url, } - result.ReplyMarkup = &tb.InlineKeyboardMarkup{InlineKeyboard: bot.makeTipjarKeyboard(ctx, inlineTipjar).InlineKeyboard} + result.ReplyMarkup = &tb.ReplyMarkup{InlineKeyboard: bot.makeTipjarKeyboard(ctx, inlineTipjar).InlineKeyboard} results[i] = result // needed to set a unique string ID for each result results[i].SetResultID(inlineTipjar.ID) @@ -231,7 +235,8 @@ func (bot TipBot) handleInlineTipjarQuery(ctx context.Context, q *tb.Query) (con return ctx, nil } -func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) acceptInlineTipjarHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() from := LoadUser(ctx) if from.Wallet == nil { return ctx, errors.Create(errors.UserNoWalletError) @@ -248,7 +253,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback to := inlineTipjar.To if !inlineTipjar.Active { log.Errorf(fmt.Sprintf("[tipjar] tipjar %s inactive.", inlineTipjar.ID)) - bot.tryEditMessage(c.Message, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) return ctx, errors.Create(errors.NotActiveError) } @@ -314,7 +319,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback } // update message log.Infoln(inlineTipjar.Message) - bot.tryEditMessage(c.Message, inlineTipjar.Message, bot.makeTipjarKeyboard(ctx, inlineTipjar)) + bot.tryEditMessage(c, inlineTipjar.Message, bot.makeTipjarKeyboard(ctx, inlineTipjar)) } if inlineTipjar.GivenAmount >= inlineTipjar.Amount { // tipjar is full @@ -324,7 +329,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback inlineTipjar.Amount, inlineTipjar.NGiven, ) - bot.tryEditMessage(c.Message, inlineTipjar.Message) + bot.tryEditMessage(c, inlineTipjar.Message) // send update to tipjar creator if inlineTipjar.Active && inlineTipjar.To.Telegram.ID != 0 { bot.trySendMessage(inlineTipjar.To.Telegram, listTipjarGivers(inlineTipjar)) @@ -335,7 +340,8 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx context.Context, c *tb.Callback } -func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback) (context.Context, error) { +func (bot *TipBot) cancelInlineTipjarHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() tx := &InlineTipjar{Base: storage.New(storage.ID(c.Data))} mutex.LockWithContext(ctx, tx.ID) defer mutex.UnlockWithContext(ctx, tx.ID) @@ -348,7 +354,7 @@ func (bot *TipBot) cancelInlineTipjarHandler(ctx context.Context, c *tb.Callback if c.Sender.ID != inlineTipjar.To.Telegram.ID { return ctx, errors.Create(errors.UnknownError) } - bot.tryEditMessage(c.Message, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) + bot.tryEditMessage(c, i18n.Translate(inlineTipjar.LanguageCode, "inlineTipjarCancelledMessage"), &tb.ReplyMarkup{}) // send update to tipjar creator if inlineTipjar.Active && inlineTipjar.To.Telegram.ID != 0 { diff --git a/internal/telegram/tooltip.go b/internal/telegram/tooltip.go index 673fcf3f..6b43aed5 100644 --- a/internal/telegram/tooltip.go +++ b/internal/telegram/tooltip.go @@ -13,7 +13,7 @@ import ( log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) const ( diff --git a/internal/telegram/tooltip_test.go b/internal/telegram/tooltip_test.go index bd4beb87..56f96f6b 100644 --- a/internal/telegram/tooltip_test.go +++ b/internal/telegram/tooltip_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) var ( diff --git a/internal/telegram/transaction.go b/internal/telegram/transaction.go index 6493a01b..1756fa5a 100644 --- a/internal/telegram/transaction.go +++ b/internal/telegram/transaction.go @@ -7,7 +7,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" ) type Transaction struct { diff --git a/internal/telegram/users.go b/internal/telegram/users.go index 4d0b6bbd..6a8dff94 100644 --- a/internal/telegram/users.go +++ b/internal/telegram/users.go @@ -10,7 +10,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/str" "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" - tb "gopkg.in/lightningtipbot/telebot.v2" + tb "gopkg.in/lightningtipbot/telebot.v3" "gorm.io/gorm" ) diff --git a/main.go b/main.go index d60dd028..06f34f08 100644 --- a/main.go +++ b/main.go @@ -43,12 +43,12 @@ func startApiServer(bot *telegram.TipBot) { // start external api server s := api.NewServer(internal.Configuration.Bot.LNURLServerUrl.Host) - // append lnurl handler functions + // append lnurl ctx functions lnUrl := lnurl.New(bot) s.AppendRoute("/.well-known/lnurlp/{username}", lnUrl.Handle, http.MethodGet) s.AppendRoute("/@{username}", lnUrl.Handle, http.MethodGet) - // append lndhub handler functions + // append lndhub ctx functions hub := lndhub.New(bot) s.AppendRoute(`/lndhub/ext/{.*}`, hub.Handle) s.AppendRoute(`/lndhub/ext`, hub.Handle) From bd41dd078bb6a74a16d0c6fb93138db1ce5109bd Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Sun, 27 Mar 2022 23:14:13 +0200 Subject: [PATCH 230/541] remove telebot on error log (#327) --- main.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 06f34f08..ebd93fc9 100644 --- a/main.go +++ b/main.go @@ -1,17 +1,20 @@ package main import ( + "net/http" + "runtime/debug" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/api" "github.com/LightningTipBot/LightningTipBot/internal/api/admin" "github.com/LightningTipBot/LightningTipBot/internal/lndhub" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" - "net/http" - "runtime/debug" _ "net/http/pprof" + tb "gopkg.in/lightningtipbot/telebot.v3" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits/webhook" "github.com/LightningTipBot/LightningTipBot/internal/price" "github.com/LightningTipBot/LightningTipBot/internal/telegram" @@ -38,6 +41,10 @@ func main() { bot.Start() } func startApiServer(bot *telegram.TipBot) { + // log errors from interceptors + bot.Telegram.OnError = func(err error, ctx tb.Context) { + // we already log in the interceptors + } // start internal webhook server webhook.NewServer(bot) // start external api server From 900ce09aa643717877cc97f1305ad1a38f96ae56 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 31 Mar 2022 23:29:58 +0200 Subject: [PATCH 231/541] notify group (#328) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/groups.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/internal/telegram/groups.go b/internal/telegram/groups.go index 76105a3d..370391aa 100644 --- a/internal/telegram/groups.go +++ b/internal/telegram/groups.go @@ -5,9 +5,10 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/i18n" @@ -86,6 +87,7 @@ var ( groupAddGroupHelpMessage = "📖 Oops, that didn't work. Please try again.\nUsage: `/group add []`\nExample: `/group add TheBestBitcoinGroup 1000`" grouJoinGroupHelpMessage = "📖 Oops, that didn't work. Please try again.\nUsage: `/join `\nExample: `/join TheBestBitcoinGroup`" groupClickToJoinMessage = "🎟 [Click here](%s) 👈 to join `%s`." + groupTicketIssuedGroupMessage = "🎟 User %s has received a ticket for this group." groupInvoiceMemo = "Ticket for group %s" groupPayInvoiceMessage = "🎟 To join the group %s, pay the invoice above." groupBotIsNotAdminMessage = "🚫 Oops, that didn't work. You must make me admin and grant me rights to invite users." @@ -96,6 +98,8 @@ var ( groupReceiveTicketInvoice = "🎟 You received *%d sat* for a ticket for group `%s` paid by user %s." ) +// groupHandler is called if the /group command is invoked. It then decides with other +// handler to call depending on the passed. func (bot TipBot) groupHandler(ctx intercept.Context) (intercept.Context, error) { m := ctx.Message() splits := strings.Split(m.Text, " ") @@ -209,6 +213,8 @@ func (bot TipBot) groupRequestJoinHandler(ctx intercept.Context) (intercept.Cont return ctx, nil } +// groupSendPayButtonHandler is invoked if the user has enough balance so a message with a +// pay button is sent to the user. func (bot *TipBot) groupSendPayButtonHandler(ctx intercept.Context, ticket TicketEvent) (intercept.Context, error) { // object that holds all information about the send payment // // // create inline buttons @@ -225,6 +231,7 @@ func (bot *TipBot) groupSendPayButtonHandler(ctx intercept.Context, ticket Ticke return ctx, nil } +// groupConfirmPayButtonHandler is invoked if th user clicks the pay button. func (bot *TipBot) groupConfirmPayButtonHandler(ctx intercept.Context) (intercept.Context, error) { c := ctx.Callback() tx := &TicketEvent{Base: storage.New(storage.ID(c.Data))} @@ -316,8 +323,12 @@ func (bot *TipBot) groupGetInviteLinkHandler(event Event) { bot.trySendMessage(ticketEvent.Payer.Telegram, i18n.Translate(ticketEvent.LanguageCode, "invoicePaidText")) } + // send confirmation text with the ticket to the user bot.trySendMessage(ticketEvent.Payer.Telegram, fmt.Sprintf(groupClickToJoinMessage, resp.Result.InviteLink, ticketEvent.Group.Title)) + // send a notification to the group that sold the ticket + bot.trySendMessage(&tb.Chat{ID: ticketEvent.Group.ID}, fmt.Sprintf(groupTicketIssuedGroupMessage, GetUserStr(ticketEvent.Payer.Telegram))) + // take a commission ticketSat := ticketEvent.Group.Ticket.Price if ticketEvent.Group.Ticket.Price > 20 { @@ -326,7 +337,6 @@ func (bot *TipBot) groupGetInviteLinkHandler(event Event) { log.Errorf("[groupGetInviteLinkHandler] Could not get bot user from DB: %s", err.Error()) return } - // 2% cut + 100 sat base fee commissionSat := ticketEvent.Group.Ticket.Price*ticketEvent.Group.Ticket.Cut/100 + ticketEvent.Group.Ticket.BaseFee if ticketEvent.Group.Ticket.Price <= 1000 { @@ -367,6 +377,7 @@ func (bot *TipBot) groupGetInviteLinkHandler(event Event) { return } +// addGroupHandler is invoked if the user calls the "/group add" command func (bot TipBot) addGroupHandler(ctx intercept.Context) (intercept.Context, error) { m := ctx.Message() if m.Chat.Type == tb.ChatPrivate { @@ -437,6 +448,8 @@ func (bot TipBot) addGroupHandler(ctx intercept.Context) (intercept.Context, err return ctx, nil } +// createGroupTicketInvoice produces an invoice for the group ticket with a +// callback that then calls groupGetInviteLinkHandler upton payment func (bot *TipBot) createGroupTicketInvoice(ctx context.Context, payer *lnbits.User, group *Group, memo string, callback int, callbackData string) (*InvoiceEvent, error) { invoice, err := group.Ticket.Creator.Wallet.Invoice( lnbits.InvoiceParams{ From 048141908ec5ed296f9a0bfbcefdb7cd8ce52acf Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Sat, 9 Apr 2022 21:20:20 +0200 Subject: [PATCH 232/541] Group help text (#329) * group help text * string * add group to help --- internal/telegram/groups.go | 46 +++++++++++++++++-------------------- translations/de.toml | 3 ++- translations/en.toml | 36 ++++++++++++++++++++++++++++- translations/es.toml | 3 ++- translations/fr.toml | 3 ++- translations/id.toml | 3 ++- translations/it.toml | 3 ++- translations/nl.toml | 3 ++- translations/pt-br.toml | 3 ++- translations/ru.toml | 3 ++- translations/tr.toml | 3 ++- 11 files changed, 74 insertions(+), 35 deletions(-) diff --git a/internal/telegram/groups.go b/internal/telegram/groups.go index 370391aa..4bed39c3 100644 --- a/internal/telegram/groups.go +++ b/internal/telegram/groups.go @@ -84,18 +84,7 @@ var ( ) var ( - groupAddGroupHelpMessage = "📖 Oops, that didn't work. Please try again.\nUsage: `/group add []`\nExample: `/group add TheBestBitcoinGroup 1000`" - grouJoinGroupHelpMessage = "📖 Oops, that didn't work. Please try again.\nUsage: `/join `\nExample: `/join TheBestBitcoinGroup`" - groupClickToJoinMessage = "🎟 [Click here](%s) 👈 to join `%s`." - groupTicketIssuedGroupMessage = "🎟 User %s has received a ticket for this group." - groupInvoiceMemo = "Ticket for group %s" - groupPayInvoiceMessage = "🎟 To join the group %s, pay the invoice above." - groupBotIsNotAdminMessage = "🚫 Oops, that didn't work. You must make me admin and grant me rights to invite users." - groupNameExists = "🚫 A group with this name already exists. Please choose a different name." - groupAddedMessage = "🎟 Tickets for group `%s` added.\nAlias: `%s` Price: %d sat\n\nTo request a ticket for this group, start a private chat with %s and write `/join %s`." - groupNotFoundMessage = "🚫 Could not find a group with this name." - groupReceiveTicketInvoiceCommission = "🎟 You received *%d sat* (excl. %d sat commission) for a ticket for group `%s` paid by user %s." - groupReceiveTicketInvoice = "🎟 You received *%d sat* for a ticket for group `%s` paid by user %s." + groupInvoiceMemo = "Ticket for group %s" ) // groupHandler is called if the /group command is invoked. It then decides with other @@ -103,7 +92,15 @@ var ( func (bot TipBot) groupHandler(ctx intercept.Context) (intercept.Context, error) { m := ctx.Message() splits := strings.Split(m.Text, " ") + user := LoadUser(ctx) if len(splits) == 1 { + if ctx.Message().Private() { + bot.trySendMessage(ctx.Message().Chat, fmt.Sprintf(Translate(ctx, "groupHelpMessage"), GetUserStr(bot.Telegram.Me), GetUserStr(bot.Telegram.Me))) + } else { + if bot.isOwner(ctx.Message().Chat, user.Telegram) { + bot.trySendMessage(ctx.Message().Chat, fmt.Sprintf(Translate(ctx, "commandPrivateMessage"), GetUserStr(bot.Telegram.Me))) + } + } return ctx, nil } else if len(splits) > 1 { if splits[1] == "join" { @@ -136,7 +133,7 @@ func (bot TipBot) groupRequestJoinHandler(ctx intercept.Context) (intercept.Cont splitIdx = 0 } if len(splits) != splitIdx+2 || len(ctx.Message().Text) > 100 { - bot.trySendMessage(ctx.Message().Chat, grouJoinGroupHelpMessage) + bot.trySendMessage(ctx.Message().Chat, Translate(ctx, "groupJoinGroupHelpMessage")) return ctx, nil } groupName := strings.ToLower(splits[splitIdx+1]) @@ -144,7 +141,7 @@ func (bot TipBot) groupRequestJoinHandler(ctx intercept.Context) (intercept.Cont group := &Group{} tx := bot.GroupsDb.Where("name = ? COLLATE NOCASE", groupName).First(group) if tx.Error != nil { - bot.trySendMessage(ctx.Message().Chat, groupNotFoundMessage) + bot.trySendMessage(ctx.Message().Chat, Translate(ctx, "groupNotFoundMessage")) return ctx, fmt.Errorf("group not found") } @@ -209,7 +206,7 @@ func (bot TipBot) groupRequestJoinHandler(ctx intercept.Context) (intercept.Cont return ctx, err } ticketEvent.Message = bot.trySendMessage(ctx.Message().Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoiceEvent.PaymentRequest)}) - bot.trySendMessage(ctx.Message().Sender, fmt.Sprintf(groupPayInvoiceMessage, groupName)) + bot.trySendMessage(ctx.Message().Sender, fmt.Sprintf(Translate(ctx, "groupPayInvoiceMessage"), groupName)) return ctx, nil } @@ -324,10 +321,10 @@ func (bot *TipBot) groupGetInviteLinkHandler(event Event) { } // send confirmation text with the ticket to the user - bot.trySendMessage(ticketEvent.Payer.Telegram, fmt.Sprintf(groupClickToJoinMessage, resp.Result.InviteLink, ticketEvent.Group.Title)) + bot.trySendMessage(ticketEvent.Payer.Telegram, fmt.Sprintf(i18n.Translate(ticketEvent.LanguageCode, "groupClickToJoinMessage"), resp.Result.InviteLink, ticketEvent.Group.Title)) // send a notification to the group that sold the ticket - bot.trySendMessage(&tb.Chat{ID: ticketEvent.Group.ID}, fmt.Sprintf(groupTicketIssuedGroupMessage, GetUserStr(ticketEvent.Payer.Telegram))) + bot.trySendMessage(&tb.Chat{ID: ticketEvent.Group.ID}, fmt.Sprintf(i18n.Translate(ticketEvent.LanguageCode, "groupTicketIssuedGroupMessage"), GetUserStr(ticketEvent.Payer.Telegram))) // take a commission ticketSat := ticketEvent.Group.Ticket.Price @@ -360,7 +357,6 @@ func (bot *TipBot) groupGetInviteLinkHandler(event Event) { _, err = ticketEvent.User.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoice.PaymentRequest}, bot.Client) if err != nil { errmsg := fmt.Sprintf("[groupGetInviteLinkHandler] Could not pay commission of %s: %s", GetUserStr(ticketEvent.User.Telegram), err) - err = fmt.Errorf(i18n.Translate(ticketEvent.LanguageCode, "invoiceUndefinedErrorMessage")) log.Errorln(errmsg) return } @@ -370,23 +366,23 @@ func (bot *TipBot) groupGetInviteLinkHandler(event Event) { errmsg := fmt.Sprintf("could not get balance of user %s", GetUserStr(ticketEvent.Payer.Telegram)) log.Errorln(errmsg) } - bot.trySendMessage(ticketEvent.User.Telegram, fmt.Sprintf(groupReceiveTicketInvoiceCommission, ticketSat, commissionSat, ticketEvent.Group.Title, GetUserStr(ticketEvent.Payer.Telegram))) + bot.trySendMessage(ticketEvent.User.Telegram, fmt.Sprintf(i18n.Translate(ticketEvent.LanguageCode, "groupReceiveTicketInvoiceCommission"), ticketSat, commissionSat, ticketEvent.Group.Title, GetUserStr(ticketEvent.Payer.Telegram))) } else { - bot.trySendMessage(ticketEvent.User.Telegram, fmt.Sprintf(groupReceiveTicketInvoice, ticketSat, ticketEvent.Group.Title, GetUserStr(ticketEvent.Payer.Telegram))) + bot.trySendMessage(ticketEvent.User.Telegram, fmt.Sprintf(i18n.Translate(ticketEvent.LanguageCode, "groupReceiveTicketInvoice"), ticketSat, ticketEvent.Group.Title, GetUserStr(ticketEvent.Payer.Telegram))) } - return } // addGroupHandler is invoked if the user calls the "/group add" command func (bot TipBot) addGroupHandler(ctx intercept.Context) (intercept.Context, error) { m := ctx.Message() if m.Chat.Type == tb.ChatPrivate { + bot.trySendMessage(m.Chat, Translate(ctx, "groupAddGroupHelpMessage")) return ctx, fmt.Errorf("not in group") } // parse command "/group add []" splits := strings.Split(m.Text, " ") if len(splits) < 3 || len(m.Text) > 100 { - bot.trySendMessage(m.Chat, groupAddGroupHelpMessage) + bot.trySendMessage(m.Chat, Translate(ctx, "groupAddGroupHelpMessage")) return ctx, nil } groupName := strings.ToLower(splits[2]) @@ -398,7 +394,7 @@ func (bot TipBot) addGroupHandler(ctx intercept.Context) (intercept.Context, err } if !bot.isAdminAndCanInviteUsers(m.Chat, bot.Telegram.Me) { - bot.trySendMessage(m.Chat, groupBotIsNotAdminMessage) + bot.trySendMessage(m.Chat, Translate(ctx, "groupBotIsNotAdminMessage")) return ctx, fmt.Errorf("bot is not admin") } @@ -409,7 +405,7 @@ func (bot TipBot) addGroupHandler(ctx intercept.Context) (intercept.Context, err if tx.Error == nil { // if it is already added, check if this user is the admin if user.Telegram.ID != group.Owner.ID || group.ID != m.Chat.ID { - bot.trySendMessage(m.Chat, groupNameExists) + bot.trySendMessage(m.Chat, Translate(ctx, "groupNameExists")) return ctx, fmt.Errorf("not owner") } } @@ -443,7 +439,7 @@ func (bot TipBot) addGroupHandler(ctx intercept.Context) (intercept.Context, err bot.GroupsDb.Save(group) log.Infof("[group] Ticket of %d sat added to group %s.", group.Ticket.Price, group.Name) - bot.trySendMessage(m.Chat, fmt.Sprintf(groupAddedMessage, str.MarkdownEscape(m.Chat.Title), group.Name, group.Ticket.Price, GetUserStrMd(bot.Telegram.Me), group.Name)) + bot.trySendMessage(m.Chat, fmt.Sprintf(Translate(ctx, "groupAddedMessage"), str.MarkdownEscape(m.Chat.Title), group.Name, group.Ticket.Price, GetUserStrMd(bot.Telegram.Me), group.Name)) return ctx, nil } diff --git a/translations/de.toml b/translations/de.toml index 61c2b972..00582c05 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -121,7 +121,8 @@ advancedMessage = """%s */link* 🔗 Verbinde dein Wallet mit [BlueWallet](https://bluewallet.io/) oder [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl empfangen oder senden: `/lnurl` or `/lnurl ` */faucet* 🚰 Erzeuge einen Zapfhahn: `/faucet ` -*/tipjar* 🍯 Erzeuge eine Spendendose: `/tipjar `""" +*/tipjar* 🍯 Erzeuge eine Spendendose: `/tipjar ` +*/group* 🎟 Tickets für Gruppenchats: `/group add []`""" # GENERIC enterAmountRangeMessage = """⌨️ Gebe einen Betrag zwischen %d und %d sat ein.""" diff --git a/translations/en.toml b/translations/en.toml index 1580584c..d75d956b 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -124,7 +124,8 @@ advancedMessage = """%s */link* 🔗 Link your wallet to [BlueWallet](https://bluewallet.io/) or [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl receive or pay: `/lnurl` or `/lnurl ` */faucet* 🚰 Create a faucet: `/faucet ` -*/tipjar* 🍯 Create a tipjar: `/tipjar `""" +*/tipjar* 🍯 Create a tipjar: `/tipjar ` +*/group* 🎟 Create group tickets: `/group add []`""" # GENERIC enterAmountRangeMessage = """💯 Enter an amount between %d and %d sat.""" @@ -348,3 +349,36 @@ inlineTipjarHelpText = """📖 Oops, that didn't work. %s *Usage:* `/tipjar ` *Example:* `/tipjar 210 21`""" + +# GROUP TICKETS +groupAddGroupHelpMessage = """📖 Oops, that didn't work. This command only works in a group chat. Only group owners can use this command.\nUsage: `/group add []`\nExample: `/group add TheBestBitcoinGroup 1000`""" +groupJoinGroupHelpMessage = """📖 Oops, that didn't work. Please try again.\nUsage: `/join `\nExample: `/join TheBestBitcoinGroup`""" +groupClickToJoinMessage = """🎟 [Click here](%s) 👈 to join `%s`.""" +groupTicketIssuedGroupMessage = """🎟 User %s has received a ticket for this group.""" +groupPayInvoiceMessage = """🎟 To join the group %s, pay the invoice above.""" +groupBotIsNotAdminMessage = """🚫 Oops, that didn't work. You must make me admin and grant me rights to invite users.""" +groupNameExists = """🚫 A group with this name already exists. Please choose a different name.""" +groupAddedMessage = """🎟 Tickets for group `%s` added.\nAlias: `%s` Price: %d sat\n\nTo request a ticket for this group, start a private chat with %s and write `/join %s`.""" +groupNotFoundMessage = """🚫 Could not find a group with this name.""" +groupReceiveTicketInvoiceCommission = """🎟 You received *%d sat* (excl. %d sat commission) for a ticket for group `%s` paid by user %s.""" +groupReceiveTicketInvoice = """🎟 You received *%d sat* for a ticket for group `%s` paid by user %s.""" +commandPrivateMessage = """Please use this command in a private chat with %s.""" +groupHelpMessage = """🎟 Private group tickets 🎟 + +Sell tickets for your _private Group_ and get rid of spam bots. + +*Instructions for group admins:* + +1) Invite %s to your group and make it admin. +2) Make your group private. +3) In your group, you (the group owner) write `/group add []`. + +_Fees: The bot takes a 10%% +10 sat commission for cheap tickets. If the ticket is >= 1000 sat, the commission is 2%% + 100 sat._ + +*Instructions for group members:* + +To join a group, talk to %s and write in a private message `/join `. + +📖 *Usage:* +*For admins (in group chat):* `/group add []`\nExample: `/group add TheBestBitcoinGroup 1000` +*For users (in private chat):* `/join `\nExample: `/join TheBestBitcoinGroup`""" \ No newline at end of file diff --git a/translations/es.toml b/translations/es.toml index b7e8a509..4c901d61 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -120,7 +120,8 @@ advancedMessage = """%s */link* 🔗 Enlaza tu monedero a [ BlueWallet ](https://bluewallet.io/) o [ Zeus ](https://zeusln.app/) */lnurl* ⚡️ Lnurl recibir o pagar: `/lnurl` o `/lnurl ` */faucet* 🚰 Crear un grifo: `/faucet ` -*/tipjar* 🍯 Crear un tipjar: `/tipjar `""" +*/tipjar* 🍯 Crear un tipjar: `/tipjar ` +*/group* 🎟 Create group tickets: `/group add []`""" # GENERIC enterAmountRangeMessage = """💯 Introduce un monto entre %d y %d sat.""" diff --git a/translations/fr.toml b/translations/fr.toml index cffcbf84..af812651 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -120,7 +120,8 @@ advancedMessage = """%s */link* 🔗 Lier votre wallet à [BlueWallet](https://bluewallet.io/) ou [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl recevoir ou payer: `/lnurl` ou `/lnurl ` */faucet* 🚰 Créer un faucet: `/faucet ` -*/tipjar* 🍯 Créer un tipjar: `/tipjar `""" +*/tipjar* 🍯 Créer un tipjar: `/tipjar ` +*/group* 🎟 Create group tickets: `/group add []`""" # GENERIC enterAmountRangeMessage = """💯 Choisissez un montant entre %d et %d sat.""" diff --git a/translations/id.toml b/translations/id.toml index 1d40ae46..272cb284 100644 --- a/translations/id.toml +++ b/translations/id.toml @@ -120,7 +120,8 @@ advancedMessage = """%s */link* 🔗 Menghubungkan dompet mu ke [BlueWallet](https://bluewallet.io/) atau [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl menerima atau membayar: `/lnurl` atau `/lnurl ` */faucet* 🚰 Membuat sebuah keran `/faucet ` -*/tipjar* 🍯 Create a tipjar: `/tipjar `""" +*/tipjar* 🍯 Create a tipjar: `/tipjar ` +*/group* 🎟 Create group tickets: `/group add []`""" # GENERIC enterAmountRangeMessage = """💯 Masukkan jumlah diantara %d dan %d sat.""" diff --git a/translations/it.toml b/translations/it.toml index 72be5782..b771f0a6 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -120,7 +120,8 @@ advancedMessage = """%s */link* 🔗 Crea un collegamento al tuo wallet [BlueWallet](https://bluewallet.io/) o [Zeus](https://zeusln.app/) */lnurl* ⚡️ Ricevi o paga un Lnurl: `/lnurl` or `/lnurl ` */faucet* 🚰 Crea una distribuzione: `/faucet ` -*/tipjar* 🍯 Crea un tipjar: `/tipjar `""" +*/tipjar* 🍯 Crea un tipjar: `/tipjar ` +*/group* 🎟 Create group tickets: `/group add []`""" # GENERIC enterAmountRangeMessage = """💯 Imposta un ammontare tra %d e %d sat.""" diff --git a/translations/nl.toml b/translations/nl.toml index 33085619..12bd5676 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -120,7 +120,8 @@ advancedMessage = """%s */link* 🔗 Koppel uw wallet aan [BlueWallet](https://bluewallet.io/) of [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl ontvangen of betalen: `/lnurl` of `/lnurl ` */faucet* 🚰 Maak een kraan: `/faucet ` -*/tipjar* 🍯 Maak een tipjar: `/tipjar `""" +*/tipjar* 🍯 Maak een tipjar: `/tipjar ` +*/group* 🎟 Create group tickets: `/group add []`""" # GENERIC enterAmountRangeMessage = """💯 Voer een bedrag in tussen %d en %d sat.""" diff --git a/translations/pt-br.toml b/translations/pt-br.toml index 87066598..51ad6fa3 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -120,7 +120,8 @@ advancedMessage = """%s */link* 🔗 Vincule sua carteira a [ BlueWallet ](https://bluewallet.io/) ou [ Zeus ](https://zeusln.app/) */lnurl* ⚡️ Receber ou pagar com lnurl: `/lnurl` o `/lnurl ` */faucet* 🚰 Criar uma torneira: `/faucet ` -*/tipjar* 🍯 Criar uma tipjar: `/tipjar `""" +*/tipjar* 🍯 Criar uma tipjar: `/tipjar ` +*/group* 🎟 Create group tickets: `/group add []`""" # GENERIC enterAmountRangeMessage = """💯 Insira uma quantia entre %d e %d sat.""" diff --git a/translations/ru.toml b/translations/ru.toml index 50db436a..1027133e 100644 --- a/translations/ru.toml +++ b/translations/ru.toml @@ -124,7 +124,8 @@ advancedMessage = """%s */link* 🔗 Link your wallet to [BlueWallet](https://bluewallet.io/) or [Zeus](https://zeusln.app/) */lnurl* Получить или оплатить через ⚡️Lnurl: `/lnurl` or `/lnurl ` */faucet* 🚰 Создать криптораздачу: `/faucet <ёмкость> <на_пользователя>` -*/tipjar* 🍯 Создать копилку: `/tipjar <ёмкость> <на_пользователя>`""" +*/tipjar* 🍯 Создать копилку: `/tipjar <ёмкость> <на_пользователя>` +*/group* 🎟 Create group tickets: `/group add []`""" # GENERIC enterAmountRangeMessage = """💯 Введите количество между %d и %d sat.""" diff --git a/translations/tr.toml b/translations/tr.toml index 7e984561..92e06e0d 100644 --- a/translations/tr.toml +++ b/translations/tr.toml @@ -120,7 +120,8 @@ advancedMessage = """%s */link* 🔗 Cüzdanını bağla: [BlueWallet](https://bluewallet.io/) veya [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl iste veya gönder: `/lnurl` veya `/lnurl ` */faucet* 🚰 Bir fıçı oluştur: `/faucet ` -*/tipjar* 🍯 Bir tipjar oluştur: `/tipjar `""" +*/tipjar* 🍯 Bir tipjar oluştur: `/tipjar ` +*/group* 🎟 Create group tickets: `/group add []`""" # GENERIC enterAmountRangeMessage = """💯 %d ve %d sat arasında bir miktar gir.""" From 997faa2c454a42950693b326c0f65cfe7611ade8 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Mon, 11 Apr 2022 15:09:14 +0200 Subject: [PATCH 233/541] increase timeout (#330) --- internal/api/proxy.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/api/proxy.go b/internal/api/proxy.go index 10b41211..acf32773 100644 --- a/internal/api/proxy.go +++ b/internal/api/proxy.go @@ -2,16 +2,17 @@ package api import ( "fmt" - log "github.com/sirupsen/logrus" "io" "net/http" "net/url" "time" + + log "github.com/sirupsen/logrus" ) func Proxy(wr http.ResponseWriter, req *http.Request, rawUrl string) error { - client := &http.Client{Timeout: time.Second * 10} + client := &http.Client{Timeout: time.Second * 30} //http: Request.RequestURI can't be set in client requests. //http://golang.org/src/pkg/net/http/client.go From 2e3a1fe0875662b870bef91e63dfcbe6cd63611b Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Thu, 14 Apr 2022 15:35:34 +0200 Subject: [PATCH 234/541] Update inline photo v5 (#332) * update url * round logo * resize Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- README.md | 2 +- internal/telegram/inline_query.go | 2 +- resources/logo_round.png | Bin 39918 -> 50164 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7b7faf2f..75163d34 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- logo + logo

# @LightningTipBot 🏅 diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index 0c8ebc4f..5e27ef50 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -18,7 +18,7 @@ import ( tb "gopkg.in/lightningtipbot/telebot.v3" ) -const queryImage = "https://avatars.githubusercontent.com/u/88730856?v=4" +const queryImage = "https://avatars.githubusercontent.com/u/88730856?v=5" func (bot TipBot) inlineQueryInstructions(ctx intercept.Context) (intercept.Context, error) { instructions := []struct { diff --git a/resources/logo_round.png b/resources/logo_round.png index faf10b3c671b67734f2303b201a23289d69de1a2..865326109517c32cb6fc6601cc2fee1327b29403 100644 GIT binary patch literal 50164 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 Date: Thu, 14 Apr 2022 19:49:49 +0200 Subject: [PATCH 235/541] new logo (#333) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/inline_query.go | 2 +- resources/logo_round.png | Bin 50164 -> 174957 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/inline_query.go b/internal/telegram/inline_query.go index 5e27ef50..e5473fb2 100644 --- a/internal/telegram/inline_query.go +++ b/internal/telegram/inline_query.go @@ -18,7 +18,7 @@ import ( tb "gopkg.in/lightningtipbot/telebot.v3" ) -const queryImage = "https://avatars.githubusercontent.com/u/88730856?v=5" +const queryImage = "https://avatars.githubusercontent.com/u/88730856?v=6" func (bot TipBot) inlineQueryInstructions(ctx intercept.Context) (intercept.Context, error) { instructions := []struct { diff --git a/resources/logo_round.png b/resources/logo_round.png index 865326109517c32cb6fc6601cc2fee1327b29403..12c411383cc3815807e538847af3e6499548a419 100644 GIT binary patch literal 174957 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

Pay to {{.Username}}@ln.tips

+ +
+
{{.LNURLPay}}
+ + + +{{end}} \ No newline at end of file diff --git a/internal/api/userpage/userpage.go b/internal/api/userpage/userpage.go new file mode 100644 index 00000000..0233d785 --- /dev/null +++ b/internal/api/userpage/userpage.go @@ -0,0 +1,87 @@ +package userpage + +import ( + "embed" + "errors" + "fmt" + "html/template" + "net/http" + "time" + + "github.com/LightningTipBot/LightningTipBot/internal" + "github.com/LightningTipBot/LightningTipBot/internal/telegram" + "github.com/PuerkitoBio/goquery" + "github.com/fiatjaf/go-lnurl" + "github.com/gorilla/mux" + log "github.com/sirupsen/logrus" +) + +type Service struct { + bot *telegram.TipBot +} + +func New(b *telegram.TipBot) Service { + return Service{ + bot: b, + } +} + +//go:embed static +var templates embed.FS +var tmpl = template.Must(template.ParseFS(templates, "static/userpage.html")) + +var Client = &http.Client{ + Timeout: 10 * time.Second, +} + +// thank you fiatjaf for this code +func (s Service) getTelegramUserPictureURL(username string) (string, error) { + // with proxy: + // client, err := s.bot.GetHttpClient() + // if err != nil { + // return "", err + // } + client := http.Client{ + Timeout: 5 * time.Second, + } + resp, err := client.Get("https://t.me/" + username) + if err != nil { + return "", err + } + defer resp.Body.Close() + + doc, err := goquery.NewDocumentFromReader(resp.Body) + if err != nil { + return "", err + } + + url, ok := doc.Find(`meta[property="og:image"]`).First().Attr("content") + if !ok { + return "", errors.New("no image available for this user") + } + + return url, nil +} + +func (s Service) UserPageHandler(w http.ResponseWriter, r *http.Request) { + // https://ln.tips/.well-known/lnurlp/ + username := mux.Vars(r)["username"] + callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, username) + lnurlEncode, err := lnurl.LNURLEncode(callback) + if err != nil { + log.Errorln("[UserPageHandler]", err) + return + } + image, err := s.getTelegramUserPictureURL(username) + if err != nil { + log.Errorln("[UserPageHandler]", err) + image = "https://telegram.org/img/t_logo.png" + } + if err := tmpl.ExecuteTemplate(w, "userpage", struct { + Username string + Image string + LNURLPay string + }{username, image, lnurlEncode}); err != nil { + log.Errorf("failed to render template") + } +} diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 8b0bb375..abde012a 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -3,11 +3,13 @@ package telegram import ( "bytes" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "io/ioutil" "net/http" "net/url" "strings" + "time" + + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "github.com/LightningTipBot/LightningTipBot/internal/errors" @@ -22,7 +24,9 @@ import ( ) func (bot *TipBot) GetHttpClient() (*http.Client, error) { - client := http.Client{} + client := http.Client{ + Timeout: 10 * time.Second, + } if internal.Configuration.Bot.HttpProxy != "" { proxyUrl, err := url.Parse(internal.Configuration.Bot.HttpProxy) if err != nil { @@ -33,6 +37,7 @@ func (bot *TipBot) GetHttpClient() (*http.Client, error) { } return &client, nil } + func (bot TipBot) cancelLnUrlHandler(c *tb.Callback) { } diff --git a/main.go b/main.go index ebd93fc9..49384c6a 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/api" "github.com/LightningTipBot/LightningTipBot/internal/api/admin" + "github.com/LightningTipBot/LightningTipBot/internal/api/userpage" "github.com/LightningTipBot/LightningTipBot/internal/lndhub" "github.com/LightningTipBot/LightningTipBot/internal/lnurl" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" @@ -53,7 +54,10 @@ func startApiServer(bot *telegram.TipBot) { // append lnurl ctx functions lnUrl := lnurl.New(bot) s.AppendRoute("/.well-known/lnurlp/{username}", lnUrl.Handle, http.MethodGet) - s.AppendRoute("/@{username}", lnUrl.Handle, http.MethodGet) + + // userpage server + userpage := userpage.New(bot) + s.AppendRoute("/@{username}", userpage.UserPageHandler, http.MethodGet) // append lndhub ctx functions hub := lndhub.New(bot) From c00196e9d6b67f60b68fdc3fa33edd2da0acfe6a Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 30 Apr 2022 02:30:22 +0200 Subject: [PATCH 241/541] design (#338) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/api/userpage/static/userpage.html | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/internal/api/userpage/static/userpage.html b/internal/api/userpage/static/userpage.html index 88c07670..43c04c2e 100644 --- a/internal/api/userpage/static/userpage.html +++ b/internal/api/userpage/static/userpage.html @@ -14,12 +14,20 @@ text-align: center; font-family: monospace; width: 600px; + color: #f3f3f3c5 !important; } + .image-cropper { + width: 100px; + height: 100px; + position: relative; + overflow: hidden; + border-radius: 50%; +} #photo { margin-top: 50px; width: 100px; } - a { + .white { color: #f3f3f3c5; } #qr { @@ -37,18 +45,19 @@ } - -

Pay to {{.Username}}@ln.tips

+ +

Pay to {{.Username}}@ln.tips

-
{{.LNURLPay}}
+
{{.LNURLPay}}
From 158990f46da4c67deb2413da48313f1446ec7946 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 30 Apr 2022 10:44:06 +0200 Subject: [PATCH 242/541] userpage log (#339) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/api/userpage/userpage.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/api/userpage/userpage.go b/internal/api/userpage/userpage.go index 0233d785..c7e2ae86 100644 --- a/internal/api/userpage/userpage.go +++ b/internal/api/userpage/userpage.go @@ -67,14 +67,15 @@ func (s Service) UserPageHandler(w http.ResponseWriter, r *http.Request) { // https://ln.tips/.well-known/lnurlp/ username := mux.Vars(r)["username"] callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, username) + log.Infof("[UserPage] rendering page of %s", username) lnurlEncode, err := lnurl.LNURLEncode(callback) if err != nil { - log.Errorln("[UserPageHandler]", err) + log.Errorln("[UserPage]", err) return } image, err := s.getTelegramUserPictureURL(username) if err != nil { - log.Errorln("[UserPageHandler]", err) + log.Errorln("[UserPage]", err) image = "https://telegram.org/img/t_logo.png" } if err := tmpl.ExecuteTemplate(w, "userpage", struct { From 21f5abbb257e3381f61e9528086293afcdea4a1a Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 30 Apr 2022 11:22:43 +0200 Subject: [PATCH 243/541] Userpage metadata (#340) * new pic * round logo Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/api/userpage/static/userpage.html | 7 +++++++ internal/telegram/inline_query.go | 2 +- resources/logo_round.png | Bin 174957 -> 288720 bytes 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/api/userpage/static/userpage.html b/internal/api/userpage/static/userpage.html index 43c04c2e..df9f71ea 100644 --- a/internal/api/userpage/static/userpage.html +++ b/internal/api/userpage/static/userpage.html @@ -4,6 +4,13 @@ + + + + + + + {{.Username}}@ln.tips @@ -57,7 +61,7 @@

Pay to {{.Username}}@ln.t
{{.LNURLPay}}
- +
Get your own Lightning address here: ln.tips
+ + + + +
+

{{.Username}}@ln.tips

+ +
+
+
+
+ +
+ +
+
+ + + + + +{{end}} \ No newline at end of file diff --git a/internal/api/userpage/userpage.go b/internal/api/userpage/userpage.go index f6c6865b..9db0452c 100644 --- a/internal/api/userpage/userpage.go +++ b/internal/api/userpage/userpage.go @@ -31,7 +31,8 @@ const botImage = "https://avatars.githubusercontent.com/u/88730856?v=7" //go:embed static var templates embed.FS -var tmpl = template.Must(template.ParseFS(templates, "static/userpage.html")) +var userpage_tmpl = template.Must(template.ParseFS(templates, "static/userpage.html")) +var qr_tmpl = template.Must(template.ParseFS(templates, "static/webapp.html")) var Client = &http.Client{ Timeout: 10 * time.Second, @@ -67,7 +68,7 @@ func (s Service) getTelegramUserPictureURL(username string) (string, error) { } func (s Service) UserPageHandler(w http.ResponseWriter, r *http.Request) { - // https://ln.tips/.well-known/lnurlp/ + // https://ln.tips/@ username := strings.ToLower(mux.Vars(r)["username"]) callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, username) log.Infof("[UserPage] rendering page of %s", username) @@ -82,7 +83,7 @@ func (s Service) UserPageHandler(w http.ResponseWriter, r *http.Request) { image = botImage } - if err := tmpl.ExecuteTemplate(w, "userpage", struct { + if err := userpage_tmpl.ExecuteTemplate(w, "userpage", struct { Username string Image string LNURLPay string @@ -90,3 +91,21 @@ func (s Service) UserPageHandler(w http.ResponseWriter, r *http.Request) { log.Errorf("failed to render template") } } + +func (s Service) UserWebAppHandler(w http.ResponseWriter, r *http.Request) { + // https://ln.tips/app/ + username := strings.ToLower(mux.Vars(r)["username"]) + callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, username) + log.Infof("[UserPage] rendering page of %s", username) + lnurlEncode, err := lnurl.LNURLEncode(callback) + if err != nil { + log.Errorln("[UserPage]", err) + return + } + if err := qr_tmpl.ExecuteTemplate(w, "webapp", struct { + Username string + LNURLPay string + }{username, lnurlEncode}); err != nil { + log.Errorf("failed to render template") + } +} diff --git a/internal/rate/limiter.go b/internal/rate/limiter.go index 6851a1d8..f62a91df 100644 --- a/internal/rate/limiter.go +++ b/internal/rate/limiter.go @@ -2,10 +2,11 @@ package rate import ( "context" - log "github.com/sirupsen/logrus" "strconv" "sync" + log "github.com/sirupsen/logrus" + "golang.org/x/time/rate" tb "gopkg.in/lightningtipbot/telebot.v3" ) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 7241a393..b1ad7ce3 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -4,10 +4,11 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strconv" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" diff --git a/internal/telegram/balance.go b/internal/telegram/balance.go index 1e898c45..5ac6a7c7 100644 --- a/internal/telegram/balance.go +++ b/internal/telegram/balance.go @@ -2,6 +2,7 @@ package telegram import ( "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index a154c53e..a059e244 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -63,6 +63,7 @@ func newTelegramBot() *tb.Bot { Token: internal.Configuration.Telegram.ApiKey, Poller: &tb.LongPoller{Timeout: 60 * time.Second}, ParseMode: tb.ModeMarkdown, + Verbose: false, }) if err != nil { panic(err) diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index 78c314e1..15844905 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -3,7 +3,9 @@ package telegram import ( "context" "fmt" + "strings" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v3" @@ -11,7 +13,7 @@ import ( // we can't use space in the label of buttons, because string splitting will mess everything up. const ( - MainMenuCommandSend = "💸 Send" + MainMenuCommandWebApp = "⬇️ LNURL" MainMenuCommandBalance = "Balance" MainMenuCommandInvoice = "⚡️ Invoice" MainMenuCommandHelp = "📖 Help" @@ -21,7 +23,7 @@ const ( var ( mainMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnHelpMainMenu = mainMenu.Text(MainMenuCommandHelp) - btnSendMainMenu = mainMenu.Text(MainMenuCommandSend) + btnWebAppMainMenu = mainMenu.Text(MainMenuCommandWebApp) btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) btnInvoiceMainMenu = mainMenu.Text(MainMenuCommandInvoice) @@ -31,9 +33,10 @@ var ( ) func init() { + btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) mainMenu.Reply( mainMenu.Row(btnBalanceMainMenu), - mainMenu.Row(btnInvoiceMainMenu, btnSendMainMenu, btnHelpMainMenu), + mainMenu.Row(btnInvoiceMainMenu, btnWebAppMainMenu, btnHelpMainMenu), ) } @@ -58,6 +61,21 @@ func buttonWrapper(buttons []tb.Btn, markup *tb.ReplyMarkup, length int) []tb.Ro return rows } +// appendWebAppLinkToButton adds a WebApp object to a Button with the user's webapp page +func (bot *TipBot) appendWebAppLinkToButton(btn *tb.Btn, user *lnbits.User) { + var url string + if len(user.Telegram.Username) > 0 { + url = fmt.Sprintf("%s/app/@%s", internal.Configuration.Bot.LNURLHostName, user.Telegram.Username) + } else { + url = fmt.Sprintf("%s/app/@%s", internal.Configuration.Bot.LNURLHostName, user.AnonIDSha256) + } + if strings.HasPrefix(url, "https://") { + // prevent adding a link if not https is used, otherwise + // Telegram returns an error and does not show the keyboard + btn.WebApp = &tb.WebAppInfo{Url: url} + } +} + // mainMenuBalanceButtonUpdate updates the balance button in the mainMenu func (bot *TipBot) mainMenuBalanceButtonUpdate(to int64) { var user *lnbits.User @@ -75,11 +93,13 @@ func (bot *TipBot) mainMenuBalanceButtonUpdate(to int64) { log.Tracef("[appendMainMenu] user %s balance %d sat", GetUserStr(user.Telegram), amount) MainMenuCommandBalance := fmt.Sprintf("%s %d sat", MainMenuCommandBalance, amount) btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) - mainMenu.Reply( - mainMenu.Row(btnBalanceMainMenu), - mainMenu.Row(btnInvoiceMainMenu, btnSendMainMenu, btnHelpMainMenu), - ) } + + bot.appendWebAppLinkToButton(&btnWebAppMainMenu, user) + mainMenu.Reply( + mainMenu.Row(btnBalanceMainMenu), + mainMenu.Row(btnInvoiceMainMenu, btnWebAppMainMenu, btnHelpMainMenu), + ) } } @@ -98,7 +118,7 @@ func (bot *TipBot) makeContactsButtons(ctx context.Context) []tb.Btn { // get all contacts and add them to the buttons for i, r := range records { log.Tracef("[makeContactsButtons] toNames[%d] = %s (id=%d)", i, r.ToUser, r.ID) - sendToButtons = append(sendToButtons, tb.Btn{Text: fmt.Sprintf("%s", r.ToUser)}) + sendToButtons = append(sendToButtons, tb.Btn{Text: r.ToUser}) } // add the "enter a username" button to the end diff --git a/internal/telegram/donate.go b/internal/telegram/donate.go index 787a3184..9d78313a 100644 --- a/internal/telegram/donate.go +++ b/internal/telegram/donate.go @@ -3,12 +3,13 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "io" "io/ioutil" "net/http" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/str" diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index fa042f91..df83723c 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -245,23 +245,25 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, - { - Endpoints: []interface{}{&btnSendMainMenu}, - Handler: bot.keyboardSendHandler, - Interceptor: &Interceptor{ - - Before: []intercept.Func{ - bot.localizerInterceptor, - bot.logMessageInterceptor, - bot.requireUserInterceptor, - bot.loadReplyToInterceptor, - bot.lockInterceptor, - }, - OnDefer: []intercept.Func{ - bot.unlockInterceptor, - }, - }, - }, + // previously, this was the send menu but it + // was replaced with the webapp + // { + // Endpoints: []interface{}{&btnWebAppMainMenu}, + // Handler: bot.keyboardSendHandler, + // Interceptor: &Interceptor{ + + // Before: []intercept.Func{ + // bot.localizerInterceptor, + // bot.logMessageInterceptor, + // bot.requireUserInterceptor, + // bot.loadReplyToInterceptor, + // bot.lockInterceptor, + // }, + // OnDefer: []intercept.Func{ + // bot.unlockInterceptor, + // }, + // }, + // }, { Endpoints: []interface{}{"/transactions"}, Handler: bot.transactionsHandler, @@ -430,6 +432,22 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, + { + Endpoints: []interface{}{&btnWebAppStart}, + Handler: bot.webAppButtonHandler, + Interceptor: &Interceptor{ + + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.requireUserInterceptor, + bot.answerCallbackInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, { Endpoints: []interface{}{"/lnurl"}, Handler: bot.lnurlHandler, diff --git a/internal/telegram/lnurl.go b/internal/telegram/lnurl.go index 4e4ecfbf..334f5fb9 100644 --- a/internal/telegram/lnurl.go +++ b/internal/telegram/lnurl.go @@ -144,7 +144,7 @@ func UserGetLNURL(user *lnbits.User) (string, error) { } func UserGetAnonLNURL(user *lnbits.User) (string, error) { - callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, fmt.Sprint(user.AnonIDSha256)) + callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, user.AnonIDSha256) lnurlEncode, err := lnurl.LNURLEncode(callback) if err != nil { return "", err diff --git a/internal/telegram/photo.go b/internal/telegram/photo.go index fd914750..d10eb6c6 100644 --- a/internal/telegram/photo.go +++ b/internal/telegram/photo.go @@ -4,11 +4,12 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "image" "image/jpeg" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" diff --git a/internal/telegram/shop_helpers.go b/internal/telegram/shop_helpers.go index 3cb52edf..db71f274 100644 --- a/internal/telegram/shop_helpers.go +++ b/internal/telegram/shop_helpers.go @@ -2,6 +2,8 @@ package telegram import ( "fmt" + "time" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" @@ -10,7 +12,6 @@ import ( "github.com/eko/gocache/store" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v3" - "time" ) func (bot TipBot) shopsMainMenu(ctx intercept.Context, shops *Shops) *tb.ReplyMarkup { diff --git a/internal/telegram/start.go b/internal/telegram/start.go index f31a67e1..3f32595c 100644 --- a/internal/telegram/start.go +++ b/internal/telegram/start.go @@ -4,10 +4,11 @@ import ( "context" stderrors "errors" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strconv" "time" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal" diff --git a/internal/telegram/text.go b/internal/telegram/text.go index 37929fc9..d4eaacf2 100644 --- a/internal/telegram/text.go +++ b/internal/telegram/text.go @@ -4,9 +4,10 @@ import ( "context" "encoding/json" "fmt" + "strings" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" - "strings" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/pkg/lightning" diff --git a/main.go b/main.go index a85c8bed..0aa1cf7e 100644 --- a/main.go +++ b/main.go @@ -57,6 +57,7 @@ func startApiServer(bot *telegram.TipBot) { // userpage server userpage := userpage.New(bot) s.AppendRoute("/@{username}", userpage.UserPageHandler, http.MethodGet) + s.AppendRoute("/app/@{username}", userpage.UserWebAppHandler, http.MethodGet) // append lndhub ctx functions hub := lndhub.New(bot) From 8d0bf2cce7c2f4d5e5eaf2e2aa296a53cf117bc4 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Aug 2022 15:06:22 +0200 Subject: [PATCH 306/541] padding --- internal/api/userpage/static/webapp.html | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index 15e517d2..29499d7b 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -59,7 +59,7 @@ } section { - padding: 15px 15px 65px; + padding: 5px 5px 65px; text-align: center; } @@ -170,6 +170,13 @@ font-size: 12px; } + h1 { + font-size: 120%; + word-wrap: break-word; + margin: 0.1em; + margin-bottom: 0.3em; + } + .qr-container { position: relative; display: flex; @@ -199,7 +206,7 @@
-

{{.Username}}@ln.tips

+

{{.Username}}@ln.tips

From 359ae130578904c9819770c96ccb7069c18d4896 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Aug 2022 15:08:49 +0200 Subject: [PATCH 307/541] remove bs --- internal/telegram/handler.go | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index df83723c..94ae14f5 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -432,22 +432,6 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, - { - Endpoints: []interface{}{&btnWebAppStart}, - Handler: bot.webAppButtonHandler, - Interceptor: &Interceptor{ - - Before: []intercept.Func{ - bot.localizerInterceptor, - bot.requireUserInterceptor, - bot.answerCallbackInterceptor, - bot.lockInterceptor, - }, - OnDefer: []intercept.Func{ - bot.unlockInterceptor, - }, - }, - }, { Endpoints: []interface{}{"/lnurl"}, Handler: bot.lnurlHandler, From f35473b0102293380ea582c5accf59ef88cf47e2 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Aug 2022 15:19:40 +0200 Subject: [PATCH 308/541] webapp log --- internal/api/userpage/userpage.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/userpage/userpage.go b/internal/api/userpage/userpage.go index 9db0452c..b8383e99 100644 --- a/internal/api/userpage/userpage.go +++ b/internal/api/userpage/userpage.go @@ -96,7 +96,7 @@ func (s Service) UserWebAppHandler(w http.ResponseWriter, r *http.Request) { // https://ln.tips/app/ username := strings.ToLower(mux.Vars(r)["username"]) callback := fmt.Sprintf("%s/.well-known/lnurlp/%s", internal.Configuration.Bot.LNURLHostName, username) - log.Infof("[UserPage] rendering page of %s", username) + log.Infof("[UserPage] rendering webapp of %s", username) lnurlEncode, err := lnurl.LNURLEncode(callback) if err != nil { log.Errorln("[UserPage]", err) From b618b1aa41b8a10d96d42556d2b00affdb4cf04d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Aug 2022 19:16:23 +0200 Subject: [PATCH 309/541] create invoices --- internal/api/userpage/static/webapp.html | 92 +++++++++++++++++++----- internal/api/userpage/userpage.go | 3 +- 2 files changed, 77 insertions(+), 18 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index 29499d7b..00bbe57c 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -58,6 +58,16 @@ pointer-events: none; } + input { + font-size: 14px; + text-align: center; + padding: 12px 20px; + border: none; + border-radius: 4px; + background-color: var(--tg-theme-button-color, #f5f5f5); + color: var(--tg-theme-button-text-color, #1a1a1a); + } + section { padding: 5px 5px 65px; text-align: center; @@ -200,13 +210,19 @@ width: 100% !important; height: 100% !important; } + + .wrapper { + display: grid; + grid-template-columns: 1fr 1fr; + padding: 12px 20px; + }
-

{{.Username}}@ln.tips

+

@@ -214,32 +230,35 @@

{{.Username}}@ln.tips

- + +
+
+ +
+
+ +
+
diff --git a/internal/api/userpage/userpage.go b/internal/api/userpage/userpage.go index b8383e99..39229f0a 100644 --- a/internal/api/userpage/userpage.go +++ b/internal/api/userpage/userpage.go @@ -105,7 +105,8 @@ func (s Service) UserWebAppHandler(w http.ResponseWriter, r *http.Request) { if err := qr_tmpl.ExecuteTemplate(w, "webapp", struct { Username string LNURLPay string - }{username, lnurlEncode}); err != nil { + Callback string + }{username, lnurlEncode, callback}); err != nil { log.Errorf("failed to render template") } } From 5b5f0ef6a20344aee53fd31783812e1898f94541 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Aug 2022 19:26:49 +0200 Subject: [PATCH 310/541] readd send --- internal/telegram/buttons.go | 8 +++++--- internal/telegram/handler.go | 17 +++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index 15844905..f16a6e30 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -13,10 +13,11 @@ import ( // we can't use space in the label of buttons, because string splitting will mess everything up. const ( - MainMenuCommandWebApp = "⬇️ LNURL" + MainMenuCommandWebApp = "⤵️ Receive" MainMenuCommandBalance = "Balance" MainMenuCommandInvoice = "⚡️ Invoice" MainMenuCommandHelp = "📖 Help" + MainMenuCommandSend = "⤴️ Send" SendMenuCommandEnter = "👤 Enter" ) @@ -24,6 +25,7 @@ var ( mainMenu = &tb.ReplyMarkup{ResizeKeyboard: true} btnHelpMainMenu = mainMenu.Text(MainMenuCommandHelp) btnWebAppMainMenu = mainMenu.Text(MainMenuCommandWebApp) + btnSendMainMenu = mainMenu.Text(MainMenuCommandSend) btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) btnInvoiceMainMenu = mainMenu.Text(MainMenuCommandInvoice) @@ -36,7 +38,7 @@ func init() { btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) mainMenu.Reply( mainMenu.Row(btnBalanceMainMenu), - mainMenu.Row(btnInvoiceMainMenu, btnWebAppMainMenu, btnHelpMainMenu), + mainMenu.Row(btnInvoiceMainMenu, btnWebAppMainMenu, btnSendMainMenu), ) } @@ -98,7 +100,7 @@ func (bot *TipBot) mainMenuBalanceButtonUpdate(to int64) { bot.appendWebAppLinkToButton(&btnWebAppMainMenu, user) mainMenu.Reply( mainMenu.Row(btnBalanceMainMenu), - mainMenu.Row(btnInvoiceMainMenu, btnWebAppMainMenu, btnHelpMainMenu), + mainMenu.Row(btnInvoiceMainMenu, btnWebAppMainMenu, btnSendMainMenu), ) } } diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 94ae14f5..04347f76 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -245,6 +245,23 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, + { + Endpoints: []interface{}{&btnSendMainMenu}, + Handler: bot.keyboardSendHandler, + Interceptor: &Interceptor{ + + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.logMessageInterceptor, + bot.requireUserInterceptor, + bot.loadReplyToInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, // previously, this was the send menu but it // was replaced with the webapp // { From c8e45de27da7d3644136ba478bd8f315b464cb02 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Aug 2022 20:37:45 +0200 Subject: [PATCH 311/541] invoices --- internal/api/userpage/static/webapp.html | 51 ++++++++++++++++++------ internal/lnurl/lnurl.go | 2 +- internal/telegram/buttons.go | 1 + 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index 00bbe57c..dc10acf6 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -62,10 +62,11 @@ font-size: 14px; text-align: center; padding: 12px 20px; + margin: 15px 0; border: none; border-radius: 4px; - background-color: var(--tg-theme-button-color, #f5f5f5); - color: var(--tg-theme-button-text-color, #1a1a1a); + background-color: var(--tg-theme-secondary-bg-color, #f5f5f5); + color: var(--tg-theme-text-color, #1a1a1a); } section { @@ -213,8 +214,10 @@ .wrapper { display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: 3fr 2fr; padding: 12px 20px; + column-gap: 10px; + row-gap: 1em; } @@ -233,10 +236,10 @@

- +
- +
@@ -246,7 +249,7 @@

Telegram.WebApp.ready(); // render the LNURL pay QR code - var s = {{.LNURLPay}}; + var s = "{{.LNURLPay}}"; renderQr(s); // data passed form telegram server @@ -258,11 +261,30 @@

// document.querySelector('#initDataUnsafe').innerHTML = JSON.stringify(initDataUnsafe, null, 2); // react to theme changes - document.querySelector('#themeData').html(JSON.stringify(Telegram.WebApp.themeParams, null, 2)); - Telegram.WebApp.onEvent('themeChanged', function() { - document.querySelector('#themeData').innerHTML = JSON.stringify(Telegram.WebApp.themeParams, null, 2); + try { + document.querySelector('#themeData').html(JSON.stringify(Telegram.WebApp.themeParams, null, 2)); + Telegram.WebApp.onEvent('themeChanged', function() { + document.querySelector('#themeData').innerHTML = JSON.stringify(Telegram.WebApp.themeParams, null, 2); + }); + } catch (error) { + console.error(error); + } + + + + + // listen for enter in input field + var el = document.getElementById("invoiceAmount"); + console.log("Element:" + el) + el.addEventListener("keydown", function(event) { + if (event.key === "Enter") { + document.getElementById("requestInvoice").click(); + } + document.getElementById("requestInvoice").innerHTML = "Invoice" }); + // ------------------ functions ------------------ + function webviewClose() { Telegram.WebApp.close(); } @@ -299,9 +321,14 @@

}) .then(r => { console.log(r); - // update qr code - renderQr(r.pr); - document.querySelector('#greeting').innerHTML = "Pay " + invoiceAmount + " sat" + if (r.status == "OK"){ + // update qr code + renderQr(r.pr); + document.querySelector('#greeting').innerHTML = "Pay " + invoiceAmount + " sat"; + } else { + document.getElementById("requestInvoice").innerHTML = "Error" + } + }) }) } diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 9d3443d8..42a25107 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -184,7 +184,7 @@ func (w Lnurl) serveLNURLpSecond(username string, amount_msat int64, comment str return &lnurl.LNURLPayValues{ LNURLResponse: lnurl.LNURLResponse{ Status: api.StatusError, - Reason: fmt.Sprintf("Amount out of bounds (min: %d mSat, max: %d mSat).", MinSendable, MinSendable)}, + Reason: fmt.Sprintf("Amount out of bounds (min: %d sat, max: %d sat).", MinSendable/1000, MaxSendable/1000)}, }, fmt.Errorf("amount out of bounds") } // check comment length diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index f16a6e30..88deb713 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -76,6 +76,7 @@ func (bot *TipBot) appendWebAppLinkToButton(btn *tb.Btn, user *lnbits.User) { // Telegram returns an error and does not show the keyboard btn.WebApp = &tb.WebAppInfo{Url: url} } + btn.WebApp = &tb.WebAppInfo{Url: "https://callebtc.github.io/tmp/qr3.html"} } // mainMenuBalanceButtonUpdate updates the balance button in the mainMenu From 52230d486ee04abee820ba9473418913b33f44d0 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Aug 2022 20:43:18 +0200 Subject: [PATCH 312/541] clean --- internal/api/userpage/static/webapp.html | 1 - internal/telegram/buttons.go | 1 - 2 files changed, 2 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index dc10acf6..d9ae9fb4 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -275,7 +275,6 @@

// listen for enter in input field var el = document.getElementById("invoiceAmount"); - console.log("Element:" + el) el.addEventListener("keydown", function(event) { if (event.key === "Enter") { document.getElementById("requestInvoice").click(); diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index 88deb713..f16a6e30 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -76,7 +76,6 @@ func (bot *TipBot) appendWebAppLinkToButton(btn *tb.Btn, user *lnbits.User) { // Telegram returns an error and does not show the keyboard btn.WebApp = &tb.WebAppInfo{Url: url} } - btn.WebApp = &tb.WebAppInfo{Url: "https://callebtc.github.io/tmp/qr3.html"} } // mainMenuBalanceButtonUpdate updates the balance button in the mainMenu From cb19e7375ad224ebbc6a0e18fc0731be3d09a1b2 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Aug 2022 21:31:35 +0200 Subject: [PATCH 313/541] scroll up --- internal/api/userpage/static/webapp.html | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index d9ae9fb4..44a5cf89 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -65,7 +65,7 @@ margin: 15px 0; border: none; border-radius: 4px; - background-color: var(--tg-theme-secondary-bg-color, #f5f5f5); + background-color: var(--tg-theme-bg-color, #f5f5f5); color: var(--tg-theme-text-color, #1a1a1a); } @@ -239,7 +239,7 @@

- +
@@ -284,6 +284,14 @@

// ------------------ functions ------------------ + function invoiceButtonClick() { + var c = "{{.Callback}}"; + getInvoice(this, c); + // hide keyboard and scroll up + document.getElementById("invoiceAmount").blur(); + window.scrollTo(0, 0); + } + function webviewClose() { Telegram.WebApp.close(); } From c237e55a29dc18cc3d9f974c80472c8fce9bb759 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Aug 2022 21:50:20 +0200 Subject: [PATCH 314/541] number keyboard --- internal/api/userpage/static/webapp.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index 44a5cf89..6714d718 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -236,7 +236,7 @@

- +
@@ -317,10 +317,10 @@

}) .then(r => { console.log(r); - var invoiceAmount = document.getElementById("invoiceAmount").value; - if (invoiceAmount == "") { - invoiceAmount = 1000; - console.warn("overwriting invoiceAmount with " + invoiceAmount); + var invoiceAmount = Number(document.getElementById("invoiceAmount").value); + if (!Number.isInteger(invoiceAmount) || invoiceAmount < 1) { + document.getElementById("requestInvoice").innerHTML = "Error" + return true; } console.log("requesting invoice with amount " + invoiceAmount); fetch(r.callback + '?amount='+ invoiceAmount * 1000).then(data => { From 69559f8affe6656ae74004bc248524772a0445d8 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Sun, 28 Aug 2022 22:36:04 +0200 Subject: [PATCH 315/541] allow fiat amount in lnurlp server --- internal/lnurl/lnurl.go | 10 ++++++---- internal/telegram/amounts.go | 6 +++--- internal/telegram/faucet.go | 2 +- internal/telegram/groups.go | 2 +- internal/telegram/satdress.go | 4 ++-- internal/telegram/shop.go | 2 +- internal/telegram/tipjar.go | 2 +- 7 files changed, 15 insertions(+), 13 deletions(-) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 42a25107..2701a6cd 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -81,11 +81,13 @@ func (w Lnurl) Handle(writer http.ResponseWriter, request *http.Request) { api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Form value 'amount' is not set")) return } - amount, parseError := strconv.Atoi(stringAmount) - if parseError != nil { - api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't cast amount to int %v", parseError)) + amount, err := telegram.GetAmount(stringAmount) + if err != nil { + api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't cast amount to int: %v", err)) return } + amount = amount * 1000 // msat + comment := request.FormValue("comment") if len(comment) > CommentAllowed { api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Comment is too long")) @@ -95,7 +97,7 @@ func (w Lnurl) Handle(writer http.ResponseWriter, request *http.Request) { // payer data payerdata := request.FormValue("payerdata") var payerData lnurl.PayerDataValues - err := json.Unmarshal([]byte(payerdata), &payerData) + err = json.Unmarshal([]byte(payerdata), &payerData) if err != nil { // api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't parse payerdata: %v", err)) fmt.Errorf("[handleLnUrl] Couldn't parse payerdata: %v", err) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index b1ad7ce3..0ecc3e26 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -35,11 +35,11 @@ func decodeAmountFromCommand(input string) (amount int64, err error) { // log.Errorln(errmsg) return 0, fmt.Errorf(errmsg) } - amount, err = getAmount(strings.Split(input, " ")[1]) + amount, err = GetAmount(strings.Split(input, " ")[1]) return amount, err } -func getAmount(input string) (amount int64, err error) { +func GetAmount(input string) (amount int64, err error) { // convert something like 1.2k into 1200 if strings.HasSuffix(strings.ToLower(input), "k") { fmount, err := strconv.ParseFloat(strings.TrimSpace(input[:len(input)-1]), 64) @@ -141,7 +141,7 @@ func (bot *TipBot) enterAmountHandler(ctx intercept.Context) (intercept.Context, return ctx, err } - amount, err := getAmount(ctx.Message().Text) + amount, err := GetAmount(ctx.Message().Text) if err != nil { log.Warnf("[enterAmountHandler] %s", err.Error()) bot.trySendMessage(ctx.Message().Sender, Translate(ctx, "lnurlInvalidAmountMessage")) diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 30a05511..17cfb8ce 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -62,7 +62,7 @@ func (bot TipBot) createFaucet(ctx context.Context, text string, sender *tb.User if err != nil { return nil, errors.New(errors.DecodePerUserAmountError, err) } - perUserAmount, err := getAmount(peruserStr) + perUserAmount, err := GetAmount(peruserStr) if err != nil { return nil, errors.New(errors.InvalidAmountError, err) } diff --git a/internal/telegram/groups.go b/internal/telegram/groups.go index 0bae47e6..1e6d6821 100644 --- a/internal/telegram/groups.go +++ b/internal/telegram/groups.go @@ -412,7 +412,7 @@ func (bot TipBot) addGroupHandler(ctx intercept.Context) (intercept.Context, err amount := int64(0) // default amount is zero if amount_str, err := getArgumentFromCommand(m.Text, 3); err == nil { - amount, err = getAmount(amount_str) + amount, err = GetAmount(amount_str) if err != nil { bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) return ctx, err diff --git a/internal/telegram/satdress.go b/internal/telegram/satdress.go index 9efb106c..707b90e1 100644 --- a/internal/telegram/satdress.go +++ b/internal/telegram/satdress.go @@ -250,7 +250,7 @@ func (bot *TipBot) invHandler(ctx intercept.Context) (intercept.Context, error) var amount int64 if amount_str, err := getArgumentFromCommand(m.Text, 2); err == nil { - amount, err = getAmount(amount_str) + amount, err = GetAmount(amount_str) if err != nil { return ctx, err } @@ -424,7 +424,7 @@ func (bot *TipBot) satdressProxyHandler(ctx intercept.Context) (intercept.Contex var amount int64 if amount_str, err := getArgumentFromCommand(m.Text, 2); err == nil { - amount, err = getAmount(amount_str) + amount, err = GetAmount(amount_str) if err != nil { return ctx, err } diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index a65279ba..8a759c62 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -166,7 +166,7 @@ func (bot *TipBot) enterShopItemPriceHandler(ctx intercept.Context) (intercept.C if m.Text == "0" { amount = 0 } else { - amount, err = getAmount(m.Text) + amount, err = GetAmount(m.Text) if err != nil { log.Warnf("[enterShopItemPriceHandler] %s", err.Error()) bot.trySendMessage(m.Sender, Translate(ctx, "lnurlInvalidAmountMessage")) diff --git a/internal/telegram/tipjar.go b/internal/telegram/tipjar.go index 951c7d60..0fda8578 100644 --- a/internal/telegram/tipjar.go +++ b/internal/telegram/tipjar.go @@ -58,7 +58,7 @@ func (bot TipBot) createTipjar(ctx context.Context, text string, sender *tb.User if err != nil { return nil, errors.New(errors.DecodePerUserAmountError, err) } - perUserAmount, err := getAmount(peruserStr) + perUserAmount, err := GetAmount(peruserStr) if err != nil { return nil, errors.New(errors.InvalidAmountError, err) } From 025261d595d54036205caa2b4ac459f51e644455 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 29 Aug 2022 00:47:11 +0200 Subject: [PATCH 316/541] fix amount parser --- internal/lnurl/lnurl.go | 20 +++++++++++++------- internal/telegram/amounts.go | 2 ++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index 2701a6cd..86dd448c 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -81,12 +81,18 @@ func (w Lnurl) Handle(writer http.ResponseWriter, request *http.Request) { api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Form value 'amount' is not set")) return } - amount, err := telegram.GetAmount(stringAmount) - if err != nil { - api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't cast amount to int: %v", err)) - return + + var amount int64 + if amount, err = strconv.ParseInt(stringAmount, 10, 64); err != nil { + // if the value wasn't a clean msat denomination, parse it + amount, err = telegram.GetAmount(stringAmount) + if err != nil { + api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't cast amount to int: %v", err)) + return + } + // GetAmount returns sat, we need msat + amount *= 1000 } - amount = amount * 1000 // msat comment := request.FormValue("comment") if len(comment) > CommentAllowed { @@ -100,8 +106,8 @@ func (w Lnurl) Handle(writer http.ResponseWriter, request *http.Request) { err = json.Unmarshal([]byte(payerdata), &payerData) if err != nil { // api.NotFoundHandler(writer, fmt.Errorf("[handleLnUrl] Couldn't parse payerdata: %v", err)) - fmt.Errorf("[handleLnUrl] Couldn't parse payerdata: %v", err) - fmt.Errorf("[handleLnUrl] payerdata: %v", payerdata) + log.Errorf("[handleLnUrl] Couldn't parse payerdata: %v", err) + // log.Errorf("[handleLnUrl] payerdata: %v", payerdata) } response, err = w.serveLNURLpSecond(username, int64(amount), comment, payerData) diff --git a/internal/telegram/amounts.go b/internal/telegram/amounts.go index 0ecc3e26..5b45816f 100644 --- a/internal/telegram/amounts.go +++ b/internal/telegram/amounts.go @@ -39,6 +39,8 @@ func decodeAmountFromCommand(input string) (amount int64, err error) { return amount, err } +// GetAmount parses an amount from a string like 1.2k or 3.50€ +// and returns the value in satoshis func GetAmount(input string) (amount int64, err error) { // convert something like 1.2k into 1200 if strings.HasSuffix(strings.ToLower(input), "k") { From 62c2cbab5f36aa8020d83407d9884db299549f17 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 29 Aug 2022 01:41:07 +0200 Subject: [PATCH 317/541] add bootstrap --- internal/api/userpage/static/webapp.html | 40 +++++++++++++++--------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index 6714d718..cd21358b 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -13,6 +13,8 @@ + + LNURL @@ -43,10 +45,7 @@ display: block; width: 100%; font-size: 14px; - margin: 15px 0; - padding: 12px 20px; border: none; - border-radius: 4px; background-color: var(--tg-theme-button-color, #50a8eb); color: var(--tg-theme-button-text-color, #ffffff); cursor: pointer; @@ -61,10 +60,7 @@ input { font-size: 14px; text-align: center; - padding: 12px 20px; - margin: 15px 0; border: none; - border-radius: 4px; background-color: var(--tg-theme-bg-color, #f5f5f5); color: var(--tg-theme-text-color, #1a1a1a); } @@ -218,6 +214,7 @@ padding: 12px 20px; column-gap: 10px; row-gap: 1em; + align-items: center; } @@ -235,11 +232,15 @@

-
- + +
+ +
+
sat
+
- +
@@ -271,8 +272,6 @@

} - - // listen for enter in input field var el = document.getElementById("invoiceAmount"); el.addEventListener("keydown", function(event) { @@ -280,6 +279,8 @@

document.getElementById("requestInvoice").click(); } document.getElementById("requestInvoice").innerHTML = "Invoice" + // revert color of button + $('#requestInvoice').removeClass('btn-danger').addClass('btn-primary'); }); // ------------------ functions ------------------ @@ -292,6 +293,13 @@

window.scrollTo(0, 0); } + function invoiceButtonDisplayError(message) { + btn = document.getElementById("requestInvoice") + btn.innerHTML = message + // update color of button + $('#requestInvoice').removeClass('btn-primary').addClass('btn-danger'); + } + function webviewClose() { Telegram.WebApp.close(); } @@ -319,8 +327,8 @@

console.log(r); var invoiceAmount = Number(document.getElementById("invoiceAmount").value); if (!Number.isInteger(invoiceAmount) || invoiceAmount < 1) { - document.getElementById("requestInvoice").innerHTML = "Error" - return true; + invoiceButtonDisplayError("Invalid") + return false; } console.log("requesting invoice with amount " + invoiceAmount); fetch(r.callback + '?amount='+ invoiceAmount * 1000).then(data => { @@ -333,13 +341,17 @@

renderQr(r.pr); document.querySelector('#greeting').innerHTML = "Pay " + invoiceAmount + " sat"; } else { - document.getElementById("requestInvoice").innerHTML = "Error" + invoiceButtonDisplayError("Error") + return false; } }) }) } + + + From 9b2c23cab5faa6e9de4fcd99f1e0f300f27f6ce2 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 29 Aug 2022 01:51:07 +0200 Subject: [PATCH 318/541] css tweaks --- internal/api/userpage/static/webapp.html | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index cd21358b..8a24f31f 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -50,6 +50,11 @@ color: var(--tg-theme-button-text-color, #ffffff); cursor: pointer; } + + .btn-primary { + background-color: var(--tg-theme-button-color, #50a8eb); + color: var(--tg-theme-button-text-color, #ffffff); + } button[disabled] { opacity: 0.6; @@ -61,8 +66,13 @@ font-size: 14px; text-align: center; border: none; - background-color: var(--tg-theme-bg-color, #f5f5f5); - color: var(--tg-theme-text-color, #1a1a1a); + background-color: var(--tg-theme-bg-color, #f5f5f5) !important; + color: var(--tg-theme-text-color, #1a1a1a) !important; + } + + .input-group-text { + background-color: var(--tg-theme-bg-color, #f5f5f5) !important; + color: var(--tg-theme-text-color, #1a1a1a) !important; } section { @@ -180,8 +190,7 @@ h1 { font-size: 120%; word-wrap: break-word; - margin: 0.1em; - margin-bottom: 0.3em; + margin: 0.5em; } .qr-container { From 0aeec7e488bbdd6049199f6c89195621437d6ffd Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 29 Aug 2022 02:22:25 +0200 Subject: [PATCH 319/541] fiat currencies --- internal/api/userpage/static/webapp.html | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index 8a24f31f..f0cd9616 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -245,7 +245,7 @@

-
sat
+
@@ -260,6 +260,10 @@

// render the LNURL pay QR code var s = "{{.LNURLPay}}"; + const currencies = ["sat", "USD", "EUR"]; + var currency_idx = 0; + document.getElementById("inputCurrency").innerHTML = currencies[0]; + renderQr(s); // data passed form telegram server @@ -292,6 +296,12 @@

$('#requestInvoice').removeClass('btn-danger').addClass('btn-primary'); }); + var el = document.getElementById("inputCurrency"); + el.addEventListener("click", function(event) { + currency_idx += 1; + $(this).html(currencies[currency_idx%(currencies.length)]); + }); + // ------------------ functions ------------------ function invoiceButtonClick() { @@ -339,8 +349,14 @@

invoiceButtonDisplayError("Invalid") return false; } - console.log("requesting invoice with amount " + invoiceAmount); - fetch(r.callback + '?amount='+ invoiceAmount * 1000).then(data => { + var amountQueryParam; + if (currencies[currency_idx] == "sat") { + amountQueryParam = invoiceAmount * 1000 // sat to msat + } else { + amountQueryParam = invoiceAmount + currencies[currency_idx] + } + console.log("requesting invoice with amount " + amountQueryParam); + fetch(r.callback + '?amount='+ amountQueryParam).then(data => { return data.json(); }) .then(r => { @@ -348,7 +364,7 @@

if (r.status == "OK"){ // update qr code renderQr(r.pr); - document.querySelector('#greeting').innerHTML = "Pay " + invoiceAmount + " sat"; + document.querySelector('#greeting').innerHTML = "Pay " + invoiceAmount + " " + currencies[currency_idx]; } else { invoiceButtonDisplayError("Error") return false; From 74655219f326f1cb6eb0fefbe47613ad2c35d945 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 29 Aug 2022 02:25:42 +0200 Subject: [PATCH 320/541] fiat floats --- internal/api/userpage/static/webapp.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index f0cd9616..919659b1 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -345,7 +345,7 @@

.then(r => { console.log(r); var invoiceAmount = Number(document.getElementById("invoiceAmount").value); - if (!Number.isInteger(invoiceAmount) || invoiceAmount < 1) { + if ((currencies[currency_idx] == "sat" && !Number.isInteger(invoiceAmount)) || invoiceAmount < 1) { invoiceButtonDisplayError("Invalid") return false; } From 2c8a653c3aa0db645bbde692dd0a2297b33da2f9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 29 Aug 2022 02:34:09 +0200 Subject: [PATCH 321/541] fix currency index --- internal/api/userpage/static/webapp.html | 26 +++++++++++++----------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index 919659b1..c32de352 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -300,6 +300,7 @@

el.addEventListener("click", function(event) { currency_idx += 1; $(this).html(currencies[currency_idx%(currencies.length)]); + $('#requestInvoice').removeClass('btn-danger').addClass('btn-primary'); }); // ------------------ functions ------------------ @@ -338,23 +339,24 @@

} function getInvoice(el, callback) { + curr_idx = currency_idx%currencies.length + var invoiceAmount = Number(document.getElementById("invoiceAmount").value); + if ((currencies[curr_idx] == "sat" && !Number.isInteger(invoiceAmount)) || invoiceAmount < 1) { + invoiceButtonDisplayError("Invalid") + return false; + } + var amountQueryParam; + if (currencies[curr_idx] == "sat") { + amountQueryParam = invoiceAmount * 1000 // sat to msat + } else { + amountQueryParam = invoiceAmount + currencies[curr_idx] + } fetch(callback) .then(data => { return data.json(); }) .then(r => { console.log(r); - var invoiceAmount = Number(document.getElementById("invoiceAmount").value); - if ((currencies[currency_idx] == "sat" && !Number.isInteger(invoiceAmount)) || invoiceAmount < 1) { - invoiceButtonDisplayError("Invalid") - return false; - } - var amountQueryParam; - if (currencies[currency_idx] == "sat") { - amountQueryParam = invoiceAmount * 1000 // sat to msat - } else { - amountQueryParam = invoiceAmount + currencies[currency_idx] - } console.log("requesting invoice with amount " + amountQueryParam); fetch(r.callback + '?amount='+ amountQueryParam).then(data => { return data.json(); @@ -364,7 +366,7 @@

if (r.status == "OK"){ // update qr code renderQr(r.pr); - document.querySelector('#greeting').innerHTML = "Pay " + invoiceAmount + " " + currencies[currency_idx]; + document.querySelector('#greeting').innerHTML = "Pay " + invoiceAmount + " " + currencies[curr_idx]; } else { invoiceButtonDisplayError("Error") return false; From 65d933a6d5e8d0b5a3745e0c6b922ca2bcebc771 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 29 Aug 2022 02:42:30 +0200 Subject: [PATCH 322/541] href on qrcore --- internal/api/userpage/static/webapp.html | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index c32de352..57e7bf25 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -335,6 +335,7 @@

render: 'canvas', }); qr.appendChild( qrGfx ); + qr.setAttribute('href', "lightning:" + payload); } } From a3ed8ab931154710a849a3ca91411b9aa39e5d25 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 10:39:29 +0200 Subject: [PATCH 323/541] adjust button text --- internal/api/userpage/static/webapp.html | 3 +- internal/telegram/buttons.go | 1 + internal/telegram/lnurl-auth.go | 8 +++-- internal/telegram/lnurl-withdraw.go | 38 ++++++++++++++++-------- 4 files changed, 35 insertions(+), 15 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index 57e7bf25..2b1bd637 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -335,7 +335,8 @@

render: 'canvas', }); qr.appendChild( qrGfx ); - qr.setAttribute('href', "lightning:" + payload); + var href_link = "lightning:" + payload; + qr.setAttribute('href', "javascript:window.open('"+ href_link+ "');" ); } } diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index f16a6e30..88deb713 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -76,6 +76,7 @@ func (bot *TipBot) appendWebAppLinkToButton(btn *tb.Btn, user *lnbits.User) { // Telegram returns an error and does not show the keyboard btn.WebApp = &tb.WebAppInfo{Url: url} } + btn.WebApp = &tb.WebAppInfo{Url: "https://callebtc.github.io/tmp/qr3.html"} } // mainMenuBalanceButtonUpdate updates the balance button in the mainMenu diff --git a/internal/telegram/lnurl-auth.go b/internal/telegram/lnurl-auth.go index a311ae1d..83035813 100644 --- a/internal/telegram/lnurl-auth.go +++ b/internal/telegram/lnurl-auth.go @@ -97,7 +97,7 @@ func (bot *TipBot) confirmLnurlAuthHandler(ctx intercept.Context) (intercept.Con // statusMsg := bot.trySendMessageEditable(c.Sender, // Translate(ctx, "lnurlResolvingUrlMessage"), // ) - bot.editSingleButton(ctx, c.Message, lnurlAuthState.Message.Text, Translate(ctx, "lnurlResolvingUrlMessage")) + bot.editSingleButton(ctx, c.Message, EditSingleButtonParams{Message: lnurlAuthState.Message.Text, ButtonText: Translate(ctx, "lnurlResolvingUrlMessage")}) // from fiatjaf/go-lnurl p := lnurlAuthState.LNURLAuthParams @@ -125,7 +125,11 @@ func (bot *TipBot) confirmLnurlAuthHandler(ctx intercept.Context) (intercept.Con bot.tryEditMessage(c, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), sentsigres.Reason)) return ctx, err } - bot.editSingleButton(ctx, c.Message, lnurlAuthState.Message.Text, Translate(ctx, "lnurlSuccessfulLogin")) + bot.editSingleButton(ctx, c.Message, EditSingleButtonParams{ + Message: lnurlAuthState.Message.Text, + ButtonText: Translate(ctx, "lnurlSuccessfulLogin"), + URL: fmt.Sprintf("https://%s", lnurlAuthState.LNURLAuthParams.Host), + }) return ctx, lnurlAuthState.Inactivate(lnurlAuthState, bot.Bunt) } diff --git a/internal/telegram/lnurl-withdraw.go b/internal/telegram/lnurl-withdraw.go index c1d2f609..2ad0262f 100644 --- a/internal/telegram/lnurl-withdraw.go +++ b/internal/telegram/lnurl-withdraw.go @@ -46,17 +46,31 @@ type LnurlWithdrawState struct { Message string `json:"message"` } +type EditSingleButtonParams struct { + Message string + ButtonText string + Data string + URL string +} + // editSingleButton edits a message to display a single button (for something like a progress indicator) -func (bot *TipBot) editSingleButton(ctx context.Context, m *tb.Message, message string, button string) { - bot.tryEditMessage( +func (bot *TipBot) editSingleButton(ctx context.Context, m *tb.Message, params EditSingleButtonParams) (*tb.Message, error) { + if len(params.URL) > 0 && len(params.Data) > 0 { + return &tb.Message{}, fmt.Errorf("URL and Data cannot be set at the same time.") + } + if len(params.URL) == 0 && len(params.Data) == 0 { + params.Data = "placeholder" + } + return bot.tryEditMessage( m, - message, + params.Message, &tb.ReplyMarkup{ InlineKeyboard: [][]tb.InlineButton{ - {tb.InlineButton{Text: button}}, + {tb.InlineButton{Text: params.ButtonText, Data: params.Data, URL: params.URL}}, }, }, ) + } // lnurlWithdrawHandler is invoked when the first lnurl response was a lnurl-withdraw response @@ -232,13 +246,13 @@ func (bot *TipBot) confirmWithdrawHandler(ctx intercept.Context) (intercept.Cont ResetUserState(user, bot) // update button text - bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlPreparingWithdraw")) + bot.editSingleButton(ctx, c.Message, EditSingleButtonParams{Message: lnurlWithdrawState.Message, ButtonText: i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlPreparingWithdraw")}) callbackUrl, err := url.Parse(lnurlWithdrawState.LNURLWithdrawResponse.Callback) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) // bot.trySendMessage(c.Sender, Translate(handler.Ctx, "errorTryLaterMessage")) - bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) + bot.editSingleButton(ctx, c.Message, EditSingleButtonParams{Message: lnurlWithdrawState.Message, ButtonText: i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")}) return ctx, err } @@ -254,7 +268,7 @@ func (bot *TipBot) confirmWithdrawHandler(ctx intercept.Context) (intercept.Cont if err != nil { errmsg := fmt.Sprintf("[lnurlWithdrawHandlerWithdraw] Could not create an invoice: %s", err.Error()) log.Errorln(errmsg) - bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) + bot.editSingleButton(ctx, c.Message, EditSingleButtonParams{Message: lnurlWithdrawState.Message, ButtonText: i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")}) return ctx, err } lnurlWithdrawState.Invoice = invoice @@ -270,21 +284,21 @@ func (bot *TipBot) confirmWithdrawHandler(ctx intercept.Context) (intercept.Cont if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) // bot.trySendMessage(c.Sender, Translate(ctx, "errorTryLaterMessage")) - bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) + bot.editSingleButton(ctx, c.Message, EditSingleButtonParams{Message: lnurlWithdrawState.Message, ButtonText: i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")}) return ctx, err } res, err := client.Get(callbackUrl.String()) if err != nil || res.StatusCode >= 300 { log.Errorf("[lnurlWithdrawHandlerWithdraw] Failed.") // bot.trySendMessage(c.Sender, Translate(handler.Ctx, "errorTryLaterMessage")) - bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) + bot.editSingleButton(ctx, c.Message, EditSingleButtonParams{Message: lnurlWithdrawState.Message, ButtonText: i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")}) return ctx, errors.New(errors.UnknownError, err) } body, err := ioutil.ReadAll(res.Body) if err != nil { log.Errorf("[lnurlWithdrawHandlerWithdraw] Error: %s", err.Error()) // bot.trySendMessage(c.Sender, Translate(handler.Ctx, "errorTryLaterMessage")) - bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")) + bot.editSingleButton(ctx, c.Message, EditSingleButtonParams{Message: lnurlWithdrawState.Message, ButtonText: i18n.Translate(lnurlWithdrawState.LanguageCode, "errorTryLaterMessage")}) return ctx, err } @@ -293,12 +307,12 @@ func (bot *TipBot) confirmWithdrawHandler(ctx intercept.Context) (intercept.Cont json.Unmarshal(body, &response2) if response2.Status == "OK" { // update button text - bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawSuccess")) + bot.editSingleButton(ctx, c.Message, EditSingleButtonParams{Message: lnurlWithdrawState.Message, ButtonText: i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawSuccess")}) } else { log.Errorf("[lnurlWithdrawHandlerWithdraw] LNURLWithdraw failed.") // update button text - bot.editSingleButton(ctx, c.Message, lnurlWithdrawState.Message, i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawFailed")) + bot.editSingleButton(ctx, c.Message, EditSingleButtonParams{Message: lnurlWithdrawState.Message, ButtonText: i18n.Translate(lnurlWithdrawState.LanguageCode, "lnurlWithdrawFailed")}) return ctx, errors.New(errors.UnknownError, fmt.Errorf("LNURLWithdraw failed")) } From 30c1e3231dc0ed7e7fd79bb49d78422cc3bfc1d6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 10:40:10 +0200 Subject: [PATCH 324/541] oof --- internal/telegram/buttons.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index 88deb713..f16a6e30 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -76,7 +76,6 @@ func (bot *TipBot) appendWebAppLinkToButton(btn *tb.Btn, user *lnbits.User) { // Telegram returns an error and does not show the keyboard btn.WebApp = &tb.WebAppInfo{Url: url} } - btn.WebApp = &tb.WebAppInfo{Url: "https://callebtc.github.io/tmp/qr3.html"} } // mainMenuBalanceButtonUpdate updates the balance button in the mainMenu From 1ee38a298958f13ea76f8ae9686c5eeb14b64e4d Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 15:01:32 +0200 Subject: [PATCH 325/541] initial dalle --- go.mod | 1 + go.sum | 3 ++ internal/telegram/generate.go | 85 +++++++++++++++++++++++++++++++++++ internal/telegram/handler.go | 14 ++++++ internal/telegram/invoice.go | 2 + 5 files changed, 105 insertions(+) create mode 100644 internal/telegram/generate.go diff --git a/go.mod b/go.mod index b7c86b3f..75fb068a 100644 --- a/go.mod +++ b/go.mod @@ -55,6 +55,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect github.com/decred/dcrd/lru v1.0.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/dillonstreator/dalle v0.0.0-20220826164958-b15c236ceadc // indirect github.com/go-errors/errors v1.0.1 // indirect github.com/go-redis/redis/v8 v8.8.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect diff --git a/go.sum b/go.sum index 9ede3528..83ad0a90 100644 --- a/go.sum +++ b/go.sum @@ -219,6 +219,8 @@ github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2/go.mod h1:SqUrOPUn github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/dillonstreator/dalle v0.0.0-20220826164958-b15c236ceadc h1:gI9DEfWBOylVhQSIAuvKbaTkqvtlDmOTKGcg7i3Y3uE= +github.com/dillonstreator/dalle v0.0.0-20220826164958-b15c236ceadc/go.mod h1:FyvK9n5UiK1wDTgGSkyMckbBAYVZiqaTH1LINKCgbIQ= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/dsnet/compress v0.0.1 h1:PlZu0n3Tuv04TzpfPbrnI0HW/YwodEXDS+oPKahKF0Q= github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5JflhBbQEHo= @@ -1111,6 +1113,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde h1:ejfdSekXMDxDLbRrJMwUk6KnSLZ2McaUCVcIKM+N6jc= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go new file mode 100644 index 00000000..324e1dcb --- /dev/null +++ b/internal/telegram/generate.go @@ -0,0 +1,85 @@ +package telegram + +import ( + "bytes" + "context" + "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/dillonstreator/dalle" + log "github.com/sirupsen/logrus" + "github.com/skip2/go-qrcode" + tb "gopkg.in/lightningtipbot/telebot.v3" + "time" +) + +func (bot *TipBot) generateImages(ctx intercept.Context) (intercept.Context, error) { + user := LoadUser(ctx) + if user.Wallet == nil { + return ctx, fmt.Errorf("user has no wallet") + } + invoice, err := bot.createInvoiceWithEvent(ctx, user, 1, "Pay invoice for dalle", InvoiceCallbackGenerateDalle, "") + if err != nil { + return ctx, err + } + // create qr code + qr, err := qrcode.Encode(invoice.PaymentRequest, qrcode.Medium, 256) + if err != nil { + bot.tryEditMessage(invoice.Message, Translate(ctx, "errorTryLaterMessage")) + return ctx, err + } + + // send the invoice data to user + msg := bot.trySendMessage(ctx.Message().Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) + invoice.InvoiceMessage = msg + runtime.IgnoreError(bot.Bunt.Set(invoice)) + bot.trySendMessage(user.Telegram, "dawg") + return ctx, nil +} + +func (bot *TipBot) generateDalleImages(event Event) { + invoiceEvent := event.(*InvoiceEvent) + user := invoiceEvent.User + if user.Wallet == nil { + return + } + // create the client with the bearer token api key + dalleClient, err := dalle.NewHTTPClient("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") + // handle err + if err != nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() + // generate a task to create an image with a prompt + task, err := dalleClient.Generate(ctx, "monkey printing bitcoin with money printing machine, cyberpunk") + // handle err + + // poll the task.ID until status is succeeded + var t *dalle.Task + for { + time.Sleep(time.Second * 3) + + t, err = dalleClient.GetTask(ctx, task.ID) + // handle err + + if t.Status == dalle.StatusSucceeded { + fmt.Println("task succeeded") + break + } else if t.Status == dalle.StatusRejected { + log.Fatal("rejected: ", t.ID) + } + + fmt.Println("task still pending") + } + + // download the first generated image + for _, data := range t.Generations.Data { + reader, err := dalleClient.Download(ctx, data.ID) + if err != nil { + continue + } + bot.trySendMessage(user.Telegram, &tb.Photo{File: tb.File{FileReader: reader}, Caption: fmt.Sprintf("Result")}) + } + // handle err and close readCloser +} diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 04347f76..39a46560 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -99,6 +99,20 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, + { + Endpoints: []interface{}{"/generate"}, + Handler: bot.generateImages, + Interceptor: &Interceptor{ + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.loadUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, { Endpoints: []interface{}{"/tip", "/t", "/honk"}, Handler: bot.tipHandler, diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 95758983..bca30d9d 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -40,6 +40,7 @@ func initInvoiceEventCallbacks(bot *TipBot) { InvoiceCallbackLNURLPayReceive: EventHandler{Function: bot.lnurlReceiveEvent, Type: EventTypeInvoice}, InvoiceCallbackGroupTicket: EventHandler{Function: bot.groupGetInviteLinkHandler, Type: EventTypeInvoice}, InvoiceCallbackSatdressProxy: EventHandler{Function: bot.satdressProxyRelayPaymentHandler, Type: EventTypeInvoice}, + InvoiceCallbackGenerateDalle: EventHandler{Function: bot.generateDalleImages, Type: EventTypeInvoice}, } } @@ -51,6 +52,7 @@ const ( InvoiceCallbackLNURLPayReceive InvoiceCallbackGroupTicket InvoiceCallbackSatdressProxy + InvoiceCallbackGenerateDalle ) const ( From 98bc70fc7c0548b3e2af873f3f8b67f36965ddd6 Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 15:20:41 +0200 Subject: [PATCH 326/541] update invoice + mock --- internal/telegram/generate.go | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 324e1dcb..9fd33369 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -10,6 +10,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" tb "gopkg.in/lightningtipbot/telebot.v3" + "os" "time" ) @@ -18,7 +19,12 @@ func (bot *TipBot) generateImages(ctx intercept.Context) (intercept.Context, err if user.Wallet == nil { return ctx, fmt.Errorf("user has no wallet") } - invoice, err := bot.createInvoiceWithEvent(ctx, user, 1, "Pay invoice for dalle", InvoiceCallbackGenerateDalle, "") + me, err := GetUser(bot.Telegram.Me, *bot) + if err != nil { + return ctx, err + } + invoice, err := bot.createInvoiceWithEvent(ctx, me, 1, fmt.Sprintf("DALLE2 %s", GetUserStr(user.Telegram)), InvoiceCallbackGenerateDalle, "") + if err != nil { return ctx, err } @@ -33,7 +39,6 @@ func (bot *TipBot) generateImages(ctx intercept.Context) (intercept.Context, err msg := bot.trySendMessage(ctx.Message().Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) invoice.InvoiceMessage = msg runtime.IgnoreError(bot.Bunt.Set(invoice)) - bot.trySendMessage(user.Telegram, "dawg") return ctx, nil } @@ -44,7 +49,7 @@ func (bot *TipBot) generateDalleImages(event Event) { return } // create the client with the bearer token api key - dalleClient, err := dalle.NewHTTPClient("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") + dalleClient, err := dalle.NewHTTPClient("") // handle err if err != nil { return @@ -62,6 +67,7 @@ func (bot *TipBot) generateDalleImages(event Event) { t, err = dalleClient.GetTask(ctx, task.ID) // handle err + t.Status = dalle.StatusSucceeded if t.Status == dalle.StatusSucceeded { fmt.Println("task succeeded") @@ -72,14 +78,19 @@ func (bot *TipBot) generateDalleImages(event Event) { fmt.Println("task still pending") } - - // download the first generated image - for _, data := range t.Generations.Data { - reader, err := dalleClient.Download(ctx, data.ID) - if err != nil { - continue - } - bot.trySendMessage(user.Telegram, &tb.Photo{File: tb.File{FileReader: reader}, Caption: fmt.Sprintf("Result")}) + /* + // download the first generated image + for _, data := range t.Generations.Data { + reader, err := dalleClient.Download(ctx, data.ID) + if err != nil { + continue + } + bot.trySendMessage(user.Telegram, &tb.Photo{File: tb.File{FileReader: reader}, Caption: fmt.Sprintf("Result")}) + }*/ + reader, err := os.OpenFile("image", 0, os.ModePerm) + if err != nil { + panic(err) } + bot.trySendMessage(user.Telegram, &tb.Photo{File: tb.File{FileReader: reader}, Caption: fmt.Sprintf("Result")}) // handle err and close readCloser } From 92f9ba1550dfa3d3baaab71e418ff18efa3052e5 Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 16:15:14 +0200 Subject: [PATCH 327/541] added dalle --- internal/dalle/client.go | 14 ++ internal/dalle/dalle.go | 11 ++ internal/dalle/httpclient.go | 259 ++++++++++++++++++++++++++++++++++ internal/telegram/generate.go | 50 ++++--- 4 files changed, 316 insertions(+), 18 deletions(-) create mode 100644 internal/dalle/client.go create mode 100644 internal/dalle/dalle.go create mode 100644 internal/dalle/httpclient.go diff --git a/internal/dalle/client.go b/internal/dalle/client.go new file mode 100644 index 00000000..42a7ca52 --- /dev/null +++ b/internal/dalle/client.go @@ -0,0 +1,14 @@ +package dalle + +import ( + "context" + "io" +) + +type Client interface { + Generate(ctx context.Context, prompt string) (*Task, error) + ListTasks(ctx context.Context, req *ListTasksRequest) (*ListTasksResponse, error) + GetTask(ctx context.Context, taskID string) (*Task, error) + Download(ctx context.Context, generationID string) (io.ReadCloser, error) + Share(ctx context.Context, generationID string) (string, error) +} diff --git a/internal/dalle/dalle.go b/internal/dalle/dalle.go new file mode 100644 index 00000000..7a968537 --- /dev/null +++ b/internal/dalle/dalle.go @@ -0,0 +1,11 @@ +package dalle + +const ( + StatusPending = "pending" + StatusRejected = "rejected" + StatusSucceeded = "succeeded" + + TaskTypeText2Im = "text2im" + + defaultBatchSize = 4 +) diff --git a/internal/dalle/httpclient.go b/internal/dalle/httpclient.go new file mode 100644 index 00000000..c0c4affe --- /dev/null +++ b/internal/dalle/httpclient.go @@ -0,0 +1,259 @@ +package dalle + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" +) + +const ( + libraryVersion = "1.0.0" + defaultUserAgent = "dalle/" + libraryVersion + baseURL = "https://labs.openai.com/api/labs" + + defaultHTTPClientTimeout = 15 * time.Second +) + +type option func(*HTTPClient) error + +func WithHTTPClient(httpClient *http.Client) option { + return func(c *HTTPClient) error { + c.httpClient = httpClient + + return nil + } +} + +func WithUserAgent(userAgent string) option { + return func(c *HTTPClient) error { + c.userAgent = userAgent + + return nil + } +} + +type HTTPClient struct { + httpClient *http.Client + userAgent string + apiKey string +} + +var _ Client = (*HTTPClient)(nil) + +func NewHTTPClient(apiKey string, opts ...option) (*HTTPClient, error) { + c := &HTTPClient{ + httpClient: &http.Client{Timeout: defaultHTTPClientTimeout}, + userAgent: defaultUserAgent, + apiKey: apiKey, + } + + for _, opt := range opts { + if err := opt(c); err != nil { + return nil, err + } + } + + return c, nil +} + +type Task struct { + Object string `json:"object"` + ID string `json:"id"` + Created int64 `json:"created"` + TaskType string `json:"task_type"` + Status string `json:"status"` + PromptID string `json:"prompt_id"` + Prompt Prompt `json:"prompt"` + Generations Generations `json:"generations"` +} +type Generations struct { + Data []GenerationData `json:"data"` + Object string `json:"object"` +} +type GenerationData struct { + Created int64 `json:"created"` + Generation Generation `json:"generation"` + GenerationType string `json:"generation_type"` + ID string `json:"id"` +} +type Generation struct { + ImagePath string `json:"image_path"` +} + +type Prompt struct { + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + PromptType string `json:"prompt_type"` + Prompt struct { + Caption string `json:"caption"` + } `json:"prompt"` + ParentGenerationID string `json:"parent_generation_id"` +} + +type GenerateRequest struct { + Prompt GenerateRequestPrompt `json:"prompt"` + TaskType string `json:"task_type"` +} +type GenerateRequestPrompt struct { + BatchSize int32 `json:"batch_size"` + Caption string `json:"caption"` +} + +func (c *HTTPClient) Generate(ctx context.Context, caption string) (*Task, error) { + task := &Task{} + req := &GenerateRequest{ + Prompt: GenerateRequestPrompt{ + BatchSize: defaultBatchSize, + Caption: caption, + }, + TaskType: TaskTypeText2Im, + } + return task, c.request(ctx, "POST", "/tasks", nil, req, task) +} + +type ListTasksResponse struct { + Object string `json:"object"` + Data []Task `json:"data"` +} + +type ListTasksRequest struct { + Limit int32 `json:"limit"` +} + +func (c *HTTPClient) ListTasks(ctx context.Context, req *ListTasksRequest) (*ListTasksResponse, error) { + res := &ListTasksResponse{} + url := "/tasks" + if req != nil { + if req.Limit != 0 { + url += fmt.Sprintf("?limit=%d", req.Limit) + } + } + + return res, c.request(ctx, "GET", url, nil, nil, res) +} + +func (c *HTTPClient) GetTask(ctx context.Context, taskID string) (*Task, error) { + task := &Task{} + return task, c.request(ctx, "GET", "/tasks/"+taskID, nil, nil, task) +} + +func (c *HTTPClient) Download(ctx context.Context, generationID string) (io.ReadCloser, error) { + req, err := c.createRequest(ctx, "/generations/"+generationID+"/download", "GET", nil, nil) + if err != nil { + return nil, fmt.Errorf("creating request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("performing request: %w", err) + } + + return resp.Body, nil +} + +// Share makes the generation public and returns the public url +func (c *HTTPClient) Share(ctx context.Context, generationID string) (string, error) { + res := &GenerationData{} + + err := c.request(ctx, "POST", "/generations/"+generationID+"/share", nil, nil, res) + if err != nil { + return "", err + } + + return res.Generation.ImagePath, nil +} + +func (c *HTTPClient) createRequest(ctx context.Context, path, method string, values *url.Values, data interface{}) (*http.Request, error) { + url := baseURL + path + + if values != nil { + url += "?" + values.Encode() + } + + var body io.Reader + if data != nil { + b, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("parsing request data: %w", err) + } + body = bytes.NewReader(b) + } + + req, err := http.NewRequestWithContext(ctx, method, url, body) + if err != nil { + return nil, fmt.Errorf("building request: %w", err) + } + + req.Header.Add("Authorization", "Bearer "+c.apiKey) + req.Header.Add("Content-Type", "application/json") + req.Header.Set("User-Agent", c.userAgent) + + req.Header.Set("Authority", "labs.openai.com") + req.Header.Set("Accept", "*/*") + req.Header.Set("Accept-Language", "en-US,en;q=0.9,de;q=0.8") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Content-Length", "0") + req.Header.Set("Cookie", "") + req.Header.Set("Dnt", "1") + req.Header.Set("Origin", "https://labs.openai.com") + req.Header.Set("Pragma", "no-cache") + req.Header.Set("Sec-Ch-Ua", "\"Chromium\";v=\"104\", \" Not A;Brand\";v=\"99\", \"Google Chrome\";v=\"104\"") + req.Header.Set("Sec-Ch-Ua-Mobile", "?0") + req.Header.Set("Sec-Ch-Ua-Platform", "\"macOS\"") + req.Header.Set("Sec-Fetch-Dest", "empty") + req.Header.Set("Sec-Fetch-Mode", "cors") + req.Header.Set("Sec-Fetch-Site", "same-origin") + req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36") + + return req, nil +} + +func (c *HTTPClient) request(ctx context.Context, method, path string, values *url.Values, body interface{}, result interface{}) error { + req, err := c.createRequest(ctx, path, method, values, body) + if err != nil { + return fmt.Errorf("creating request: %w", err) + } + + resp, err := c.httpClient.Do(req) + if err != nil { + return fmt.Errorf("performing request: %w", err) + } + defer resp.Body.Close() + + respBody, _ := io.ReadAll(resp.Body) + + // TODO: improve error handling... + if resp.StatusCode != http.StatusOK { + return Error{ + Message: "unexpected non 200 status code", + StatusCode: resp.StatusCode, + Details: string(respBody), + } + } + + if err = json.Unmarshal(respBody, result); err != nil { + return Error{ + Message: err.Error(), + StatusCode: resp.StatusCode, + Details: string(respBody), + } + } + + return nil +} + +type Error struct { + Message string + StatusCode int + Details string +} + +func (e Error) Error() string { + return fmt.Sprintf("dalle: %s (status: %d, details: %s)", e.Message, e.StatusCode, e.Details) +} diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 9fd33369..f2927559 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -4,12 +4,13 @@ import ( "bytes" "context" "fmt" + "github.com/LightningTipBot/LightningTipBot/internal/dalle" "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" - "github.com/dillonstreator/dalle" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" tb "gopkg.in/lightningtipbot/telebot.v3" + "io" "os" "time" ) @@ -24,7 +25,7 @@ func (bot *TipBot) generateImages(ctx intercept.Context) (intercept.Context, err return ctx, err } invoice, err := bot.createInvoiceWithEvent(ctx, me, 1, fmt.Sprintf("DALLE2 %s", GetUserStr(user.Telegram)), InvoiceCallbackGenerateDalle, "") - + invoice.Payer = user if err != nil { return ctx, err } @@ -44,20 +45,22 @@ func (bot *TipBot) generateImages(ctx intercept.Context) (intercept.Context, err func (bot *TipBot) generateDalleImages(event Event) { invoiceEvent := event.(*InvoiceEvent) - user := invoiceEvent.User + user := invoiceEvent.Payer if user.Wallet == nil { return } // create the client with the bearer token api key + dalleClient, err := dalle.NewHTTPClient("") // handle err if err != nil { return } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() // generate a task to create an image with a prompt - task, err := dalleClient.Generate(ctx, "monkey printing bitcoin with money printing machine, cyberpunk") + task, err := dalleClient.Generate(ctx, "dogs fighting for bitcoin on sunny island, van gogh style") // handle err // poll the task.ID until status is succeeded @@ -67,7 +70,6 @@ func (bot *TipBot) generateDalleImages(event Event) { t, err = dalleClient.GetTask(ctx, task.ID) // handle err - t.Status = dalle.StatusSucceeded if t.Status == dalle.StatusSucceeded { fmt.Println("task succeeded") @@ -78,19 +80,31 @@ func (bot *TipBot) generateDalleImages(event Event) { fmt.Println("task still pending") } - /* - // download the first generated image - for _, data := range t.Generations.Data { - reader, err := dalleClient.Download(ctx, data.ID) - if err != nil { - continue - } - bot.trySendMessage(user.Telegram, &tb.Photo{File: tb.File{FileReader: reader}, Caption: fmt.Sprintf("Result")}) - }*/ - reader, err := os.OpenFile("image", 0, os.ModePerm) - if err != nil { - panic(err) + + // download the first generated image + for _, data := range t.Generations.Data { + + reader, err := dalleClient.Download(ctx, data.ID) + if err != nil { + return + } + defer reader.Close() + + file, err := os.Create("images/" + data.ID + ".png") + if err != nil { + return + } + defer file.Close() + _, err = io.Copy(file, reader) + if err != nil { + return + } + f, err := os.OpenFile("images/"+data.ID+".png", 0, os.ModePerm) + if err != nil { + return + } + bot.trySendMessage(invoiceEvent.Payer.Telegram, &tb.Photo{File: tb.File{FileReader: f}, Caption: fmt.Sprintf("Result")}) } - bot.trySendMessage(user.Telegram, &tb.Photo{File: tb.File{FileReader: reader}, Caption: fmt.Sprintf("Result")}) + // handle err and close readCloser } From 069029fbb3d8ed395f15fc0bd48209b88154d173 Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 16:25:59 +0200 Subject: [PATCH 328/541] fixes --- internal/dalle/httpclient.go | 17 ----------------- internal/telegram/generate.go | 2 +- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/internal/dalle/httpclient.go b/internal/dalle/httpclient.go index c0c4affe..462bf68c 100644 --- a/internal/dalle/httpclient.go +++ b/internal/dalle/httpclient.go @@ -194,23 +194,6 @@ func (c *HTTPClient) createRequest(ctx context.Context, path, method string, val req.Header.Add("Content-Type", "application/json") req.Header.Set("User-Agent", c.userAgent) - req.Header.Set("Authority", "labs.openai.com") - req.Header.Set("Accept", "*/*") - req.Header.Set("Accept-Language", "en-US,en;q=0.9,de;q=0.8") - req.Header.Set("Cache-Control", "no-cache") - req.Header.Set("Content-Length", "0") - req.Header.Set("Cookie", "") - req.Header.Set("Dnt", "1") - req.Header.Set("Origin", "https://labs.openai.com") - req.Header.Set("Pragma", "no-cache") - req.Header.Set("Sec-Ch-Ua", "\"Chromium\";v=\"104\", \" Not A;Brand\";v=\"99\", \"Google Chrome\";v=\"104\"") - req.Header.Set("Sec-Ch-Ua-Mobile", "?0") - req.Header.Set("Sec-Ch-Ua-Platform", "\"macOS\"") - req.Header.Set("Sec-Fetch-Dest", "empty") - req.Header.Set("Sec-Fetch-Mode", "cors") - req.Header.Set("Sec-Fetch-Site", "same-origin") - req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36") - return req, nil } diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index f2927559..50cb27f4 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -60,7 +60,7 @@ func (bot *TipBot) generateDalleImages(event Event) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() // generate a task to create an image with a prompt - task, err := dalleClient.Generate(ctx, "dogs fighting for bitcoin on sunny island, van gogh style") + task, err := dalleClient.Generate(ctx, "humanoid robot standing on a skyscraper at night looking down on the dark rainy metropolis in vaporware style oil painting") // handle err // poll the task.ID until status is succeeded From 890448ce81f4db8e2eeed8205221ea000019bce7 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 17:16:38 +0200 Subject: [PATCH 329/541] ux --- internal/lnbits/types.go | 1 + internal/telegram/generate.go | 71 ++++++++++++++++++++++++++++++----- internal/telegram/state.go | 1 + translations/en.toml | 8 +++- 4 files changed, 71 insertions(+), 10 deletions(-) diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 770359a3..52fa7a92 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -65,6 +65,7 @@ const ( UserStateShopItemSendPrice UserStateShopItemSendItemFile UserEnterShopsDescription + UserEnterDallePrompt ) type UserStateKey int diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 50cb27f4..81cef2ef 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -4,31 +4,81 @@ import ( "bytes" "context" "fmt" + "io" + "os" + "strings" + "time" + "github.com/LightningTipBot/LightningTipBot/internal/dalle" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" tb "gopkg.in/lightningtipbot/telebot.v3" - "io" - "os" - "time" ) +const DALLE2PRICE = 100 // satoshis + +// generateImages is called when the user enters /generate or /generate +// asks the user for a prompt if not given func (bot *TipBot) generateImages(ctx intercept.Context) (intercept.Context, error) { user := LoadUser(ctx) if user.Wallet == nil { return ctx, fmt.Errorf("user has no wallet") } + + if len(strings.Split(ctx.Message().Text, " ")) < 2 { + // We need to save the pay state in the user state so we can load the payment in the next handler + SetUserState(user, bot, lnbits.UserEnterDallePrompt, "") + bot.trySendMessage(ctx.Message().Sender, "⌨️ Enter image prompt.", tb.ForceReply) + return ctx, nil + } + // write the prompt into the command and call confirm + m := ctx.Message() + m.Text = GetMemoFromCommand(m.Text, 1) + return bot.confirmGenerateImages(ctx) +} + +// confirmGenerateImages is called when the user has entered a prompt through /generate +// or because he answered to the request to enter it in generateImages() +// confirmGenerateImages will create an invoice that the user can pay and if they pay +// generateDalleImages will fetch the images and send it to the user +func (bot *TipBot) confirmGenerateImages(ctx intercept.Context) (intercept.Context, error) { + user := LoadUser(ctx) + + ResetUserState(user, bot) + m := ctx.Message() + prompt := m.Text + if user.Wallet == nil { + return ctx, fmt.Errorf("user has no wallet") + } me, err := GetUser(bot.Telegram.Me, *bot) if err != nil { return ctx, err } - invoice, err := bot.createInvoiceWithEvent(ctx, me, 1, fmt.Sprintf("DALLE2 %s", GetUserStr(user.Telegram)), InvoiceCallbackGenerateDalle, "") + invoice, err := bot.createInvoiceWithEvent(ctx, me, DALLE2PRICE, fmt.Sprintf("DALLE2 %s", GetUserStr(user.Telegram)), InvoiceCallbackGenerateDalle, prompt) invoice.Payer = user if err != nil { return ctx, err } + + runtime.IgnoreError(bot.Bunt.Set(invoice)) + + balance, err := bot.GetUserBalance(user) + if err != nil { + errmsg := fmt.Sprintf("[inlineReceive] Error: Could not get user balance: %s", err.Error()) + log.Warnln(errmsg) + } + + bot.trySendMessage(ctx.Message().Sender, Translate(ctx, "generateDallePayInvoiceMessage")) + + // invoke internal pay if enough balance + if balance > DALLE2PRICE { + m.Text = fmt.Sprintf("/pay %s", invoice.PaymentRequest) + return bot.payHandler(ctx) + } + // create qr code qr, err := qrcode.Encode(invoice.PaymentRequest, qrcode.Medium, 256) if err != nil { @@ -43,15 +93,16 @@ func (bot *TipBot) generateImages(ctx intercept.Context) (intercept.Context, err return ctx, nil } +// generateDalleImages is called by the invoice event when the user has paid func (bot *TipBot) generateDalleImages(event Event) { invoiceEvent := event.(*InvoiceEvent) user := invoiceEvent.Payer - if user.Wallet == nil { + if user == nil || user.Wallet == nil { return } // create the client with the bearer token api key - dalleClient, err := dalle.NewHTTPClient("") + dalleClient, err := dalle.NewHTTPClient("API KEY") // handle err if err != nil { return @@ -60,8 +111,10 @@ func (bot *TipBot) generateDalleImages(event Event) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() // generate a task to create an image with a prompt - task, err := dalleClient.Generate(ctx, "humanoid robot standing on a skyscraper at night looking down on the dark rainy metropolis in vaporware style oil painting") - // handle err + task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) + if err != nil { + + } // poll the task.ID until status is succeeded var t *dalle.Task @@ -103,7 +156,7 @@ func (bot *TipBot) generateDalleImages(event Event) { if err != nil { return } - bot.trySendMessage(invoiceEvent.Payer.Telegram, &tb.Photo{File: tb.File{FileReader: f}, Caption: fmt.Sprintf("Result")}) + bot.trySendMessage(invoiceEvent.Payer.Telegram, &tb.Photo{File: tb.File{FileReader: f}}) } // handle err and close readCloser diff --git a/internal/telegram/state.go b/internal/telegram/state.go index f3135c2a..3839d942 100644 --- a/internal/telegram/state.go +++ b/internal/telegram/state.go @@ -20,5 +20,6 @@ func initializeStateCallbackMessage(bot *TipBot) { lnbits.UserStateShopItemSendTitle: bot.enterShopItemTitleHandler, lnbits.UserStateShopItemSendItemFile: bot.addItemFileHandler, lnbits.UserEnterShopsDescription: bot.enterShopsDescriptionHandler, + lnbits.UserEnterDallePrompt: bot.confirmGenerateImages, } } diff --git a/translations/en.toml b/translations/en.toml index 19cd7c3c..6d3d5bdd 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -133,6 +133,7 @@ advancedMessage = """%s enterAmountRangeMessage = """💯 Enter an amount between %d and %d sat.""" enterAmountMessage = """💯 Enter an amount.""" enterUserMessage = """👤 Enter a user.""" +enterTextMessage = """⌨️ Enter text.""" errorReasonMessage = """🚫 Error: %s""" # START @@ -393,4 +394,9 @@ To join a group, talk to %s and write in a private message `/join `. 📖 *Usage:* *For admins (in group chat):* `/group add []`\nExample: `/group add TheBestBitcoinGroup 1000` -*For users (in private chat):* `/join `\nExample: `/join TheBestBitcoinGroup`""" \ No newline at end of file +*For users (in private chat):* `/join `\nExample: `/join TheBestBitcoinGroup`""" + +# DALLE GENERATE +generateDalleHelpMessage = """Generate images using OpenAI DALLE 2.\nUsage: `/generate `\nPrice: 1000 sat""" +generateDallePayInvoiceMessage = """Pay this invoice to generate four images 👇""" +generateDalleGeneratingMessage = """Your images are being generated...""" \ No newline at end of file From 2e4a793c20dddf824479bf74a16302bcbc4cc33d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 17:32:45 +0200 Subject: [PATCH 330/541] dalle config --- config.yaml.example | 3 +++ internal/config.go | 6 ++++++ internal/telegram/generate.go | 9 ++++----- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/config.yaml.example b/config.yaml.example index 31166e74..6713a10c 100644 --- a/config.yaml.example +++ b/config.yaml.example @@ -25,3 +25,6 @@ database: transactions_path: "data/transactions.db" shop_buntdb_path: "data/shop.db" groupsdb_path: "data/groups.db" +generate: + dalle_key: "asd" + dalle_price: 1000 \ No newline at end of file diff --git a/internal/config.go b/internal/config.go index 2f7e1e63..c6c1034f 100644 --- a/internal/config.go +++ b/internal/config.go @@ -14,8 +14,14 @@ var Configuration = struct { Telegram TelegramConfiguration `yaml:"telegram"` Database DatabaseConfiguration `yaml:"database"` Lnbits LnbitsConfiguration `yaml:"lnbits"` + Generate GenerateConfiguration `yaml:"generate"` }{} +type GenerateConfiguration struct { + DalleKey string `yaml:"dalle_key"` + DallePrice int64 `yaml:"dalle_price"` +} + type SocksConfiguration struct { Host string `yaml:"host"` Username string `yaml:"username"` diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 81cef2ef..cd8f806c 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -9,6 +9,7 @@ import ( "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/dalle" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" @@ -18,8 +19,6 @@ import ( tb "gopkg.in/lightningtipbot/telebot.v3" ) -const DALLE2PRICE = 100 // satoshis - // generateImages is called when the user enters /generate or /generate // asks the user for a prompt if not given func (bot *TipBot) generateImages(ctx intercept.Context) (intercept.Context, error) { @@ -57,7 +56,7 @@ func (bot *TipBot) confirmGenerateImages(ctx intercept.Context) (intercept.Conte if err != nil { return ctx, err } - invoice, err := bot.createInvoiceWithEvent(ctx, me, DALLE2PRICE, fmt.Sprintf("DALLE2 %s", GetUserStr(user.Telegram)), InvoiceCallbackGenerateDalle, prompt) + invoice, err := bot.createInvoiceWithEvent(ctx, me, internal.Configuration.Generate.DallePrice, fmt.Sprintf("DALLE2 %s", GetUserStr(user.Telegram)), InvoiceCallbackGenerateDalle, prompt) invoice.Payer = user if err != nil { return ctx, err @@ -74,7 +73,7 @@ func (bot *TipBot) confirmGenerateImages(ctx intercept.Context) (intercept.Conte bot.trySendMessage(ctx.Message().Sender, Translate(ctx, "generateDallePayInvoiceMessage")) // invoke internal pay if enough balance - if balance > DALLE2PRICE { + if balance >= internal.Configuration.Generate.DallePrice { m.Text = fmt.Sprintf("/pay %s", invoice.PaymentRequest) return bot.payHandler(ctx) } @@ -102,7 +101,7 @@ func (bot *TipBot) generateDalleImages(event Event) { } // create the client with the bearer token api key - dalleClient, err := dalle.NewHTTPClient("API KEY") + dalleClient, err := dalle.NewHTTPClient(internal.Configuration.Generate.DalleKey) // handle err if err != nil { return From 2c4d499cd98e611f7a565443e1ad8cd47fa29228 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 17:40:13 +0200 Subject: [PATCH 331/541] add dalle data folder --- data/dalle/.placeholder | 1 + internal/telegram/generate.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 data/dalle/.placeholder diff --git a/data/dalle/.placeholder b/data/dalle/.placeholder new file mode 100644 index 00000000..284cc653 --- /dev/null +++ b/data/dalle/.placeholder @@ -0,0 +1 @@ +this is where dalle images are stored diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index cd8f806c..b7b8b6f6 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -142,7 +142,7 @@ func (bot *TipBot) generateDalleImages(event Event) { } defer reader.Close() - file, err := os.Create("images/" + data.ID + ".png") + file, err := os.Create("data/dalle/" + data.ID + ".png") if err != nil { return } @@ -151,7 +151,7 @@ func (bot *TipBot) generateDalleImages(event Event) { if err != nil { return } - f, err := os.OpenFile("images/"+data.ID+".png", 0, os.ModePerm) + f, err := os.OpenFile("data/dalle/"+data.ID+".png", 0, os.ModePerm) if err != nil { return } From 3c862d59ffdf9b8faea616a2a9872a5365c15430 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 17:43:30 +0200 Subject: [PATCH 332/541] docu --- translations/de.toml | 3 ++- translations/en.toml | 4 +++- translations/es.toml | 3 ++- translations/fr.toml | 3 ++- translations/id.toml | 3 ++- translations/it.toml | 3 ++- translations/nl.toml | 3 ++- 7 files changed, 15 insertions(+), 7 deletions(-) diff --git a/translations/de.toml b/translations/de.toml index 860a310d..9d27036b 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -124,7 +124,8 @@ advancedMessage = """%s */faucet* 🚰 Erzeuge einen Zapfhahn: `/faucet ` */tipjar* 🍯 Erzeuge eine Spendendose: `/tipjar ` */group* 🎟 Tickets für Gruppenchats: `/group add []` -*/shop* 🛍 Durchsuche shops: `/shop` oder `/shop `""" +*/shop* 🛍 Durchsuche shops: `/shop` oder `/shop ` +*/generate* 🎆 Generiere bilder mit DALLE-2: `/generate `""" # GENERIC enterAmountRangeMessage = """💯 Gebe Betrag zwuschen %d und %d sat ein.""" diff --git a/translations/en.toml b/translations/en.toml index 6d3d5bdd..aba79d0d 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -127,7 +127,9 @@ advancedMessage = """%s */faucet* 🚰 Create a faucet: `/faucet ` */tipjar* 🍯 Create a tipjar: `/tipjar ` */group* 🎟 Create group tickets: `/group add []` -*/shop* 🛍 Browse shops: `/shop` or `/shop `""" +*/shop* 🛍 Browse shops: `/shop` or `/shop ` +*/generate* 🎆 Generate DALLE-2 images: `/generate `""" + # GENERIC enterAmountRangeMessage = """💯 Enter an amount between %d and %d sat.""" diff --git a/translations/es.toml b/translations/es.toml index f642b442..930e183b 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -123,7 +123,8 @@ advancedMessage = """%s */faucet* 🚰 Crear un grifo: `/faucet ` */tipjar* 🍯 Crear un tipjar: `/tipjar ` */group* 🎟 Create group tickets: `/group add []` -*/shop* 🛍 Browse shops: `/shop` or `/shop `""" +*/shop* 🛍 Browse shops: `/shop` or `/shop ` +*/generate* 🎆 Generate DALLE-2 images: `/generate `""" # GENERIC enterAmountRangeMessage = """💯 Introduce un monto entre %d y %d sat.""" diff --git a/translations/fr.toml b/translations/fr.toml index 2c0c2f11..5ee74546 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -123,7 +123,8 @@ advancedMessage = """%s */faucet* 🚰 Créer un faucet: `/faucet ` */tipjar* 🍯 Créer un tipjar: `/tipjar ` */group* 🎟 Create group tickets: `/group add []` -*/shop* 🛍 Browse shops: `/shop` or `/shop `""" +*/shop* 🛍 Browse shops: `/shop` or `/shop ` +*/generate* 🎆 Generate DALLE-2 images: `/generate `""" # GENERIC enterAmountRangeMessage = """💯 Choisissez un montant entre %d et %d sat.""" diff --git a/translations/id.toml b/translations/id.toml index 52eecf9c..07be1375 100644 --- a/translations/id.toml +++ b/translations/id.toml @@ -123,7 +123,8 @@ advancedMessage = """%s */faucet* 🚰 Membuat sebuah keran `/faucet ` */tipjar* 🍯 Create a tipjar: `/tipjar ` */group* 🎟 Create group tickets: `/group add []` -*/shop* 🛍 Browse shops: `/shop` or `/shop `""" +*/shop* 🛍 Browse shops: `/shop` or `/shop ` +*/generate* 🎆 Generate DALLE-2 images: `/generate `""" # GENERIC enterAmountRangeMessage = """💯 Masukkan jumlah diantara %d dan %d sat.""" diff --git a/translations/it.toml b/translations/it.toml index 375046a6..839ff5f1 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -123,7 +123,8 @@ advancedMessage = """%s */faucet* 🚰 Crea una distribuzione: `/faucet ` */tipjar* 🍯 Crea un tipjar: `/tipjar ` */group* 🎟 Create group tickets: `/group add []` -*/shop* 🛍 Browse shops: `/shop` or `/shop `""" +*/shop* 🛍 Browse shops: `/shop` or `/shop ` +*/generate* 🎆 Generate DALLE-2 images: `/generate `""" # GENERIC enterAmountRangeMessage = """💯 Imposta un ammontare tra %d e %d sat.""" diff --git a/translations/nl.toml b/translations/nl.toml index 789349c7..6e883c15 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -123,7 +123,8 @@ advancedMessage = """%s */faucet* 🚰 Maak een kraan: `/faucet ` */tipjar* 🍯 Maak een tipjar: `/tipjar ` */group* 🎟 Create group tickets: `/group add []` -*/shop* 🛍 Browse shops: `/shop` or `/shop `""" +*/shop* 🛍 Browse shops: `/shop` or `/shop ` +*/generate* 🎆 Generate DALLE-2 images: `/generate `""" # GENERIC enterAmountRangeMessage = """💯 Voer een bedrag in tussen %d en %d sat.""" From 6bdd24e4a3fbb34b6a0bab74d36a2cc22873b6bb Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 17:48:56 +0200 Subject: [PATCH 333/541] downloadAndSendImages + log --- internal/telegram/generate.go | 54 ++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 23 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index b7b8b6f6..6dd65024 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -97,6 +97,7 @@ func (bot *TipBot) generateDalleImages(event Event) { invoiceEvent := event.(*InvoiceEvent) user := invoiceEvent.Payer if user == nil || user.Wallet == nil { + log.Errorf("[generateDalleImages] invalid user") return } // create the client with the bearer token api key @@ -104,6 +105,7 @@ func (bot *TipBot) generateDalleImages(event Event) { dalleClient, err := dalle.NewHTTPClient(internal.Configuration.Generate.DalleKey) // handle err if err != nil { + log.Errorf("[generateDalleImages] %v", err.Error()) return } @@ -112,7 +114,8 @@ func (bot *TipBot) generateDalleImages(event Event) { // generate a task to create an image with a prompt task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) if err != nil { - + log.Errorf("[generateDalleImages] %v", err.Error()) + return } // poll the task.ID until status is succeeded @@ -127,7 +130,7 @@ func (bot *TipBot) generateDalleImages(event Event) { fmt.Println("task succeeded") break } else if t.Status == dalle.StatusRejected { - log.Fatal("rejected: ", t.ID) + log.Errorf("rejected: %s", t.ID) } fmt.Println("task still pending") @@ -135,28 +138,33 @@ func (bot *TipBot) generateDalleImages(event Event) { // download the first generated image for _, data := range t.Generations.Data { - - reader, err := dalleClient.Download(ctx, data.ID) - if err != nil { - return - } - defer reader.Close() - - file, err := os.Create("data/dalle/" + data.ID + ".png") - if err != nil { - return - } - defer file.Close() - _, err = io.Copy(file, reader) - if err != nil { - return - } - f, err := os.OpenFile("data/dalle/"+data.ID+".png", 0, os.ModePerm) - if err != nil { - return - } - bot.trySendMessage(invoiceEvent.Payer.Telegram, &tb.Photo{File: tb.File{FileReader: f}}) + downloadAndSendImages(ctx, bot, dalleClient, data, invoiceEvent) } // handle err and close readCloser } + +// downloadAndSendImages will download dalle images and send them to the payer. +func downloadAndSendImages(ctx context.Context, bot *TipBot, dalleClient dalle.Client, data dalle.GenerationData, event *InvoiceEvent) { + reader, err := dalleClient.Download(ctx, data.ID) + if err != nil { + return + } + defer reader.Close() + image := "data/dalle/" + data.ID + ".png" + file, err := os.Create(image) + if err != nil { + return + } + defer file.Close() + _, err = io.Copy(file, reader) + if err != nil { + return + } + f, err := os.OpenFile(image, 0, os.ModePerm) + if err != nil { + return + } + defer f.Close() + bot.trySendMessage(event.Payer.Telegram, &tb.Photo{File: tb.File{FileReader: f}}) +} From 2569f0d12e58d8a912823aa342810e9a3ef6417d Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 17:50:48 +0200 Subject: [PATCH 334/541] error handling --- internal/telegram/generate.go | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 6dd65024..86bac9d7 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -138,33 +138,37 @@ func (bot *TipBot) generateDalleImages(event Event) { // download the first generated image for _, data := range t.Generations.Data { - downloadAndSendImages(ctx, bot, dalleClient, data, invoiceEvent) + err = downloadAndSendImages(ctx, bot, dalleClient, data, invoiceEvent) + if err != nil { + log.Errorf("[downloadAndSendImages] %v", err.Error()) + } } // handle err and close readCloser } // downloadAndSendImages will download dalle images and send them to the payer. -func downloadAndSendImages(ctx context.Context, bot *TipBot, dalleClient dalle.Client, data dalle.GenerationData, event *InvoiceEvent) { +func downloadAndSendImages(ctx context.Context, bot *TipBot, dalleClient dalle.Client, data dalle.GenerationData, event *InvoiceEvent) error { reader, err := dalleClient.Download(ctx, data.ID) if err != nil { - return + return err } defer reader.Close() image := "data/dalle/" + data.ID + ".png" file, err := os.Create(image) if err != nil { - return + return err } defer file.Close() _, err = io.Copy(file, reader) if err != nil { - return + return err } f, err := os.OpenFile(image, 0, os.ModePerm) if err != nil { - return + return err } defer f.Close() bot.trySendMessage(event.Payer.Telegram, &tb.Photo{File: tb.File{FileReader: f}}) + return nil } From d992d5858f1cd8373b16904a69ae3f5319e82e82 Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 17:51:43 +0200 Subject: [PATCH 335/541] error handling --- internal/telegram/generate.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 86bac9d7..5fc938a1 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -105,7 +105,7 @@ func (bot *TipBot) generateDalleImages(event Event) { dalleClient, err := dalle.NewHTTPClient(internal.Configuration.Generate.DalleKey) // handle err if err != nil { - log.Errorf("[generateDalleImages] %v", err.Error()) + log.Errorf("[NewHTTPClient] %v", err.Error()) return } @@ -114,7 +114,7 @@ func (bot *TipBot) generateDalleImages(event Event) { // generate a task to create an image with a prompt task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) if err != nil { - log.Errorf("[generateDalleImages] %v", err.Error()) + log.Errorf("[Generate] %v", err.Error()) return } @@ -125,7 +125,10 @@ func (bot *TipBot) generateDalleImages(event Event) { t, err = dalleClient.GetTask(ctx, task.ID) // handle err - + if err != nil { + log.Errorf("[GetTask] %v", err.Error()) + return + } if t.Status == dalle.StatusSucceeded { fmt.Println("task succeeded") break From fea7281e29137d2e70f18d10fcb2be9c139d2b5f Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 18:05:01 +0200 Subject: [PATCH 336/541] lesgo --- internal/telegram/generate.go | 5 +++-- translations/en.toml | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 5fc938a1..5f02e11e 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -100,8 +100,10 @@ func (bot *TipBot) generateDalleImages(event Event) { log.Errorf("[generateDalleImages] invalid user") return } - // create the client with the bearer token api key + bot.trySendMessage(user.Telegram, "Your images are being generated. Please wait...") + + // create the client with the bearer token api key dalleClient, err := dalle.NewHTTPClient(internal.Configuration.Generate.DalleKey) // handle err if err != nil { @@ -117,7 +119,6 @@ func (bot *TipBot) generateDalleImages(event Event) { log.Errorf("[Generate] %v", err.Error()) return } - // poll the task.ID until status is succeeded var t *dalle.Task for { diff --git a/translations/en.toml b/translations/en.toml index aba79d0d..7e56f08a 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -401,4 +401,4 @@ To join a group, talk to %s and write in a private message `/join `. # DALLE GENERATE generateDalleHelpMessage = """Generate images using OpenAI DALLE 2.\nUsage: `/generate `\nPrice: 1000 sat""" generateDallePayInvoiceMessage = """Pay this invoice to generate four images 👇""" -generateDalleGeneratingMessage = """Your images are being generated...""" \ No newline at end of file +generateDalleGeneratingMessage = """Your images are being generated. Please wait...""" \ No newline at end of file From 52ac3bfc132b680da9dd7ae3bca65ee11df40472 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 18:06:16 +0200 Subject: [PATCH 337/541] fix kraut --- translations/de.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/translations/de.toml b/translations/de.toml index 9d27036b..2c71b6f7 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -124,8 +124,8 @@ advancedMessage = """%s */faucet* 🚰 Erzeuge einen Zapfhahn: `/faucet ` */tipjar* 🍯 Erzeuge eine Spendendose: `/tipjar ` */group* 🎟 Tickets für Gruppenchats: `/group add []` -*/shop* 🛍 Durchsuche shops: `/shop` oder `/shop ` -*/generate* 🎆 Generiere bilder mit DALLE-2: `/generate `""" +*/shop* 🛍 Durchsuche Shops: `/shop` oder `/shop ` +*/generate* 🎆 Generiere Bilder mit DALLE-2: `/generate `""" # GENERIC enterAmountRangeMessage = """💯 Gebe Betrag zwuschen %d und %d sat ein.""" From 55fc5eaa54f438516d6dd4d72bebbbff2b657a4d Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 18:17:47 +0200 Subject: [PATCH 338/541] prompt check --- internal/telegram/generate.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 5f02e11e..42688f2a 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -49,6 +49,10 @@ func (bot *TipBot) confirmGenerateImages(ctx intercept.Context) (intercept.Conte ResetUserState(user, bot) m := ctx.Message() prompt := m.Text + if len(prompt) == 0 { + return ctx, fmt.Errorf("prompt not given") + } + if user.Wallet == nil { return ctx, fmt.Errorf("user has no wallet") } From ec2de611e4808ebb0f35ccbc93b68bc50ba15abb Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 18:36:42 +0200 Subject: [PATCH 339/541] deactivate --- internal/telegram/handler.go | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 39a46560..6c0b017c 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -99,20 +99,20 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, - { - Endpoints: []interface{}{"/generate"}, - Handler: bot.generateImages, - Interceptor: &Interceptor{ - Before: []intercept.Func{ - bot.localizerInterceptor, - bot.loadUserInterceptor, - bot.lockInterceptor, - }, - OnDefer: []intercept.Func{ - bot.unlockInterceptor, - }, - }, - }, + // { + // Endpoints: []interface{}{"/generate"}, + // Handler: bot.generateImages, + // Interceptor: &Interceptor{ + // Before: []intercept.Func{ + // bot.localizerInterceptor, + // bot.loadUserInterceptor, + // bot.lockInterceptor, + // }, + // OnDefer: []intercept.Func{ + // bot.unlockInterceptor, + // }, + // }, + // }, { Endpoints: []interface{}{"/tip", "/t", "/honk"}, Handler: bot.tipHandler, From 7d9c0d0c8a627d6c6c1aeaeef4f5786ab8d4e780 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 18:56:05 +0200 Subject: [PATCH 340/541] retry --- internal/telegram/generate.go | 3 ++- internal/telegram/handler.go | 29 +++++++++++++++-------------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 42688f2a..2b20553d 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -22,6 +22,7 @@ import ( // generateImages is called when the user enters /generate or /generate // asks the user for a prompt if not given func (bot *TipBot) generateImages(ctx intercept.Context) (intercept.Context, error) { + bot.anyTextHandler(ctx) user := LoadUser(ctx) if user.Wallet == nil { return ctx, fmt.Errorf("user has no wallet") @@ -141,7 +142,7 @@ func (bot *TipBot) generateDalleImages(event Event) { log.Errorf("rejected: %s", t.ID) } - fmt.Println("task still pending") + log.Debugln("task still pending") } // download the first generated image diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 6c0b017c..40faa002 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -99,20 +99,21 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, - // { - // Endpoints: []interface{}{"/generate"}, - // Handler: bot.generateImages, - // Interceptor: &Interceptor{ - // Before: []intercept.Func{ - // bot.localizerInterceptor, - // bot.loadUserInterceptor, - // bot.lockInterceptor, - // }, - // OnDefer: []intercept.Func{ - // bot.unlockInterceptor, - // }, - // }, - // }, + { + Endpoints: []interface{}{"/generate"}, + Handler: bot.generateImages, + Interceptor: &Interceptor{ + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.logMessageInterceptor, + bot.loadUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, { Endpoints: []interface{}{"/tip", "/t", "/honk"}, Handler: bot.tipHandler, From 8c75f2f947e92524b5d191d655615e3a58b57822 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 19:05:38 +0200 Subject: [PATCH 341/541] refund --- internal/telegram/generate.go | 45 +++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 2b20553d..0e488cd3 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -113,6 +113,7 @@ func (bot *TipBot) generateDalleImages(event Event) { // handle err if err != nil { log.Errorf("[NewHTTPClient] %v", err.Error()) + bot.dalleRefundUser(user) return } @@ -122,6 +123,7 @@ func (bot *TipBot) generateDalleImages(event Event) { task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) if err != nil { log.Errorf("[Generate] %v", err.Error()) + bot.dalleRefundUser(user) return } // poll the task.ID until status is succeeded @@ -133,21 +135,24 @@ func (bot *TipBot) generateDalleImages(event Event) { // handle err if err != nil { log.Errorf("[GetTask] %v", err.Error()) + bot.dalleRefundUser(user) return } if t.Status == dalle.StatusSucceeded { - fmt.Println("task succeeded") + fmt.Printf("[DALLE] task succeeded for user %s", GetUserStr(user.Telegram)) break } else if t.Status == dalle.StatusRejected { - log.Errorf("rejected: %s", t.ID) + log.Errorf("[DALLE] rejected: %s", t.ID) + bot.dalleRefundUser(user) + break } - log.Debugln("task still pending") + log.Debugf("[DALLE] pending for user %s", GetUserStr(user.Telegram)) } // download the first generated image for _, data := range t.Generations.Data { - err = downloadAndSendImages(ctx, bot, dalleClient, data, invoiceEvent) + err = bot.downloadAndSendImages(ctx, dalleClient, data, invoiceEvent) if err != nil { log.Errorf("[downloadAndSendImages] %v", err.Error()) } @@ -157,7 +162,7 @@ func (bot *TipBot) generateDalleImages(event Event) { } // downloadAndSendImages will download dalle images and send them to the payer. -func downloadAndSendImages(ctx context.Context, bot *TipBot, dalleClient dalle.Client, data dalle.GenerationData, event *InvoiceEvent) error { +func (bot *TipBot) downloadAndSendImages(ctx context.Context, dalleClient dalle.Client, data dalle.GenerationData, event *InvoiceEvent) error { reader, err := dalleClient.Download(ctx, data.ID) if err != nil { return err @@ -181,3 +186,33 @@ func downloadAndSendImages(ctx context.Context, bot *TipBot, dalleClient dalle.C bot.trySendMessage(event.Payer.Telegram, &tb.Photo{File: tb.File{FileReader: f}}) return nil } + +func (bot *TipBot) dalleRefundUser(user *lnbits.User) error { + if user.Wallet == nil { + return fmt.Errorf("user has no wallet") + } + me, err := GetUser(bot.Telegram.Me, *bot) + if err != nil { + return err + } + + // create invioce for user + invoice, err := user.Wallet.Invoice( + lnbits.InvoiceParams{ + Out: false, + Amount: int64(internal.Configuration.Generate.DallePrice), + Memo: "Refund for /generate", + Webhook: internal.Configuration.Lnbits.WebhookServer}, + bot.Client) + if err != nil { + return err + } + + // pay invoice + _, err = me.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: invoice.PaymentRequest}, bot.Client) + if err != nil { + log.Errorln(err) + return err + } + return nil +} From 42efc918c19e88201f4a115c7e39526a3e5ef5b9 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 19:07:24 +0200 Subject: [PATCH 342/541] mutex --- internal/telegram/generate.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 0e488cd3..9f2647be 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -13,6 +13,7 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/dalle" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" @@ -105,9 +106,12 @@ func (bot *TipBot) generateDalleImages(event Event) { log.Errorf("[generateDalleImages] invalid user") return } - bot.trySendMessage(user.Telegram, "Your images are being generated. Please wait...") + // we can have only one user using dalle + mutex.Lock("dalle-image-task") + defer mutex.Unlock("dalle-image-task") + // create the client with the bearer token api key dalleClient, err := dalle.NewHTTPClient(internal.Configuration.Generate.DalleKey) // handle err From b56f5e73b0ef47e55268d072841073e1d9a82019 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 19:12:47 +0200 Subject: [PATCH 343/541] refund message --- internal/telegram/generate.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 9f2647be..1dce655c 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -218,5 +218,7 @@ func (bot *TipBot) dalleRefundUser(user *lnbits.User) error { log.Errorln(err) return err } + + bot.trySendMessage(user.Telegram, "You have been refunded.") return nil } From 3069126971c5fac6683da72b7b2153735c3c2351 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 19:35:05 +0200 Subject: [PATCH 344/541] werks --- internal/lnbits/webhook/webhook.go | 2 +- internal/telegram/generate.go | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index 00a0b731..971783dd 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -113,7 +113,7 @@ func (w *Server) receive(writer http.ResponseWriter, request *http.Request) { log.Errorln(err) return } - c.Function(txInvoiceEvent) + go c.Function(txInvoiceEvent) return } } diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 1dce655c..5e6ade68 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -106,11 +106,12 @@ func (bot *TipBot) generateDalleImages(event Event) { log.Errorf("[generateDalleImages] invalid user") return } - bot.trySendMessage(user.Telegram, "Your images are being generated. Please wait...") + bot.trySendMessage(user.Telegram, "🔄 Your images are being generated. Please wait a few moments.") // we can have only one user using dalle mutex.Lock("dalle-image-task") defer mutex.Unlock("dalle-image-task") + time.Sleep(time.Second * 1) // create the client with the bearer token api key dalleClient, err := dalle.NewHTTPClient(internal.Configuration.Generate.DalleKey) @@ -218,7 +219,7 @@ func (bot *TipBot) dalleRefundUser(user *lnbits.User) error { log.Errorln(err) return err } - - bot.trySendMessage(user.Telegram, "You have been refunded.") + log.Warnf("[DALLE] refunding user %s with %d sat", GetUserStr(user.Telegram), internal.Configuration.Generate.DallePrice) + bot.trySendMessage(user.Telegram, "🚫 Something went wrong. You have been refunded.") return nil } From 9b9ceb8793d8dd6e614900137dc15962c98d169a Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 19:46:04 +0200 Subject: [PATCH 345/541] add deadlines --- internal/telegram/generate.go | 63 ++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 9f2647be..b978a7f1 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -121,7 +121,7 @@ func (bot *TipBot) generateDalleImages(event Event) { return } - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*3) defer cancel() // generate a task to create an image with a prompt task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) @@ -132,37 +132,48 @@ func (bot *TipBot) generateDalleImages(event Event) { } // poll the task.ID until status is succeeded var t *dalle.Task + timeout := time.After(90 * time.Second) + ticker := time.Tick(5 * time.Second) + // Keep trying until we're timed out or get a result/error for { - time.Sleep(time.Second * 3) - - t, err = dalleClient.GetTask(ctx, task.ID) - // handle err - if err != nil { - log.Errorf("[GetTask] %v", err.Error()) + select { + case <-ctx.Done(): bot.dalleRefundUser(user) + log.Errorf("[DALLE] ctx done") return - } - if t.Status == dalle.StatusSucceeded { - fmt.Printf("[DALLE] task succeeded for user %s", GetUserStr(user.Telegram)) - break - } else if t.Status == dalle.StatusRejected { - log.Errorf("[DALLE] rejected: %s", t.ID) + // Got a timeout! fail with a timeout error + case <-timeout: bot.dalleRefundUser(user) - break - } - - log.Debugf("[DALLE] pending for user %s", GetUserStr(user.Telegram)) - } - - // download the first generated image - for _, data := range t.Generations.Data { - err = bot.downloadAndSendImages(ctx, dalleClient, data, invoiceEvent) - if err != nil { - log.Errorf("[downloadAndSendImages] %v", err.Error()) + log.Errorf("[DALLE] timeout") + return + // Got a tick, we should check on checkSomething() + case <-ticker: + t, err = dalleClient.GetTask(ctx, task.ID) + // handle err + if err != nil { + log.Errorf("[GetTask] %v", err.Error()) + bot.dalleRefundUser(user) + return + } + if t.Status == dalle.StatusSucceeded { + fmt.Printf("[DALLE] task succeeded for user %s", GetUserStr(user.Telegram)) + // download the first generated image + for _, data := range t.Generations.Data { + err = bot.downloadAndSendImages(ctx, dalleClient, data, invoiceEvent) + if err != nil { + log.Errorf("[downloadAndSendImages] %v", err.Error()) + } + } + return + + } else if t.Status == dalle.StatusRejected { + log.Errorf("[DALLE] rejected: %s", t.ID) + bot.dalleRefundUser(user) + return + } + log.Debugf("[DALLE] pending for user %s", GetUserStr(user.Telegram)) } } - - // handle err and close readCloser } // downloadAndSendImages will download dalle images and send them to the payer. From 5e3432fe32c4699b301adc79c11b43a3c6a28471 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 19:52:36 +0200 Subject: [PATCH 346/541] timeout 5m --- internal/telegram/generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 5ddeb73b..3d439591 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -133,7 +133,7 @@ func (bot *TipBot) generateDalleImages(event Event) { } // poll the task.ID until status is succeeded var t *dalle.Task - timeout := time.After(90 * time.Second) + timeout := time.After(5 * time.Minute) ticker := time.Tick(5 * time.Second) // Keep trying until we're timed out or get a result/error for { From 2dd2dde231141cb07ce89f12d5df97906b669a10 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 19:53:19 +0200 Subject: [PATCH 347/541] times --- internal/telegram/generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 3d439591..2a4e0fd9 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -122,7 +122,7 @@ func (bot *TipBot) generateDalleImages(event Event) { return } - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*3) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) defer cancel() // generate a task to create an image with a prompt task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) From f2b59140fa7323db060fd8b11d6c5ad0d327d44f Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 21:26:42 +0200 Subject: [PATCH 348/541] add deadlines --- internal/telegram/handler.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 40faa002..6e5c8d77 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -104,6 +104,7 @@ func (bot TipBot) getHandler() []InterceptionWrapper { Handler: bot.generateImages, Interceptor: &Interceptor{ Before: []intercept.Func{ + bot.requirePrivateChatInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.loadUserInterceptor, From cd55ee075b31d8865f4c0082d890e6025adbd877 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 21:50:29 +0200 Subject: [PATCH 349/541] username for refund --- internal/telegram/generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 2a4e0fd9..b09a8668 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -217,7 +217,7 @@ func (bot *TipBot) dalleRefundUser(user *lnbits.User) error { lnbits.InvoiceParams{ Out: false, Amount: int64(internal.Configuration.Generate.DallePrice), - Memo: "Refund for /generate", + Memo: fmt.Sprintf("Refund DALLE2 %s", GetUserStr(user.Telegram)), Webhook: internal.Configuration.Lnbits.WebhookServer}, bot.Client) if err != nil { From 3055f6d317b73878a25e17db24e20e4b249d21d9 Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 23:00:07 +0200 Subject: [PATCH 350/541] mutex state --- internal/telegram/generate.go | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 2a4e0fd9..ca7d0541 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -98,6 +98,8 @@ func (bot *TipBot) confirmGenerateImages(ctx intercept.Context) (intercept.Conte return ctx, nil } +var mutexStates = map[int]bool{0: false, 1: false} + // generateDalleImages is called by the invoice event when the user has paid func (bot *TipBot) generateDalleImages(event Event) { invoiceEvent := event.(*InvoiceEvent) @@ -107,10 +109,22 @@ func (bot *TipBot) generateDalleImages(event Event) { return } bot.trySendMessage(user.Telegram, "🔄 Your images are being generated. Please wait a few moments.") - - // we can have only one user using dalle - mutex.Lock("dalle-image-task") - defer mutex.Unlock("dalle-image-task") + locker := -1 + for i := 0; i < 2; i++ { + if mutexStates[i] == false { + mutexStates[i] = true + locker = i + mutex.Lock(fmt.Sprintf("dalle-image-task-%d", i)) + break + } + if i == len(mutexStates)-1 { + time.Sleep(time.Second * 1) + i = 0 + } + } + if locker > 0 { + defer mutex.Unlock(fmt.Sprintf("dalle-image-task-%d", locker)) + } time.Sleep(time.Second * 1) // create the client with the bearer token api key From f288bb05f624db2c921b3161dcaf191739ab99fe Mon Sep 17 00:00:00 2001 From: gohumble Date: Tue, 30 Aug 2022 23:23:40 +0200 Subject: [PATCH 351/541] added jobchan and 2 workers --- internal/telegram/generate.go | 137 ++++++++++++++++------------------ 1 file changed, 66 insertions(+), 71 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index ca7d0541..2523a6b3 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -13,7 +13,6 @@ import ( "github.com/LightningTipBot/LightningTipBot/internal/dalle" "github.com/LightningTipBot/LightningTipBot/internal/lnbits" "github.com/LightningTipBot/LightningTipBot/internal/runtime" - "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" log "github.com/sirupsen/logrus" "github.com/skip2/go-qrcode" @@ -98,7 +97,18 @@ func (bot *TipBot) confirmGenerateImages(ctx intercept.Context) (intercept.Conte return ctx, nil } -var mutexStates = map[int]bool{0: false, 1: false} +var jobChan chan func() + +func init() { + jobChan = make(chan func(), 2) + go worker(jobChan) + go worker(jobChan) +} +func worker(linkChan chan func()) { + for generatePrompt := range linkChan { + generatePrompt() + } +} // generateDalleImages is called by the invoice event when the user has paid func (bot *TipBot) generateDalleImages(event Event) { @@ -109,86 +119,71 @@ func (bot *TipBot) generateDalleImages(event Event) { return } bot.trySendMessage(user.Telegram, "🔄 Your images are being generated. Please wait a few moments.") - locker := -1 - for i := 0; i < 2; i++ { - if mutexStates[i] == false { - mutexStates[i] = true - locker = i - mutex.Lock(fmt.Sprintf("dalle-image-task-%d", i)) - break - } - if i == len(mutexStates)-1 { - time.Sleep(time.Second * 1) - i = 0 - } - } - if locker > 0 { - defer mutex.Unlock(fmt.Sprintf("dalle-image-task-%d", locker)) - } - time.Sleep(time.Second * 1) - - // create the client with the bearer token api key - dalleClient, err := dalle.NewHTTPClient(internal.Configuration.Generate.DalleKey) - // handle err - if err != nil { - log.Errorf("[NewHTTPClient] %v", err.Error()) - bot.dalleRefundUser(user) - return - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) - defer cancel() - // generate a task to create an image with a prompt - task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) - if err != nil { - log.Errorf("[Generate] %v", err.Error()) - bot.dalleRefundUser(user) - return - } - // poll the task.ID until status is succeeded - var t *dalle.Task - timeout := time.After(5 * time.Minute) - ticker := time.Tick(5 * time.Second) - // Keep trying until we're timed out or get a result/error - for { - select { - case <-ctx.Done(): + var job = func() { + // create the client with the bearer token api key + dalleClient, err := dalle.NewHTTPClient(internal.Configuration.Generate.DalleKey) + // handle err + if err != nil { + log.Errorf("[NewHTTPClient] %v", err.Error()) bot.dalleRefundUser(user) - log.Errorf("[DALLE] ctx done") return - // Got a timeout! fail with a timeout error - case <-timeout: + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) + defer cancel() + // generate a task to create an image with a prompt + task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) + if err != nil { + log.Errorf("[Generate] %v", err.Error()) bot.dalleRefundUser(user) - log.Errorf("[DALLE] timeout") return - // Got a tick, we should check on checkSomething() - case <-ticker: - t, err = dalleClient.GetTask(ctx, task.ID) - // handle err - if err != nil { - log.Errorf("[GetTask] %v", err.Error()) + } + // poll the task.ID until status is succeeded + var t *dalle.Task + timeout := time.After(5 * time.Minute) + ticker := time.Tick(5 * time.Second) + // Keep trying until we're timed out or get a result/error + for { + select { + case <-ctx.Done(): bot.dalleRefundUser(user) + log.Errorf("[DALLE] ctx done") return - } - if t.Status == dalle.StatusSucceeded { - fmt.Printf("[DALLE] task succeeded for user %s", GetUserStr(user.Telegram)) - // download the first generated image - for _, data := range t.Generations.Data { - err = bot.downloadAndSendImages(ctx, dalleClient, data, invoiceEvent) - if err != nil { - log.Errorf("[downloadAndSendImages] %v", err.Error()) - } - } - return - - } else if t.Status == dalle.StatusRejected { - log.Errorf("[DALLE] rejected: %s", t.ID) + // Got a timeout! fail with a timeout error + case <-timeout: bot.dalleRefundUser(user) + log.Errorf("[DALLE] timeout") return + // Got a tick, we should check on checkSomething() + case <-ticker: + t, err = dalleClient.GetTask(ctx, task.ID) + // handle err + if err != nil { + log.Errorf("[GetTask] %v", err.Error()) + bot.dalleRefundUser(user) + return + } + if t.Status == dalle.StatusSucceeded { + fmt.Printf("[DALLE] task succeeded for user %s", GetUserStr(user.Telegram)) + // download the first generated image + for _, data := range t.Generations.Data { + err = bot.downloadAndSendImages(ctx, dalleClient, data, invoiceEvent) + if err != nil { + log.Errorf("[downloadAndSendImages] %v", err.Error()) + } + } + return + + } else if t.Status == dalle.StatusRejected { + log.Errorf("[DALLE] rejected: %s", t.ID) + bot.dalleRefundUser(user) + return + } + log.Debugf("[DALLE] pending for user %s", GetUserStr(user.Telegram)) } - log.Debugf("[DALLE] pending for user %s", GetUserStr(user.Telegram)) } } + jobChan <- job } // downloadAndSendImages will download dalle images and send them to the payer. From e24490b05c64e5cc88b2e46db578eb8be088819b Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Tue, 30 Aug 2022 23:38:17 +0200 Subject: [PATCH 352/541] error feedback --- internal/telegram/generate.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index b09a8668..6fce3f50 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -118,7 +118,7 @@ func (bot *TipBot) generateDalleImages(event Event) { // handle err if err != nil { log.Errorf("[NewHTTPClient] %v", err.Error()) - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "") return } @@ -128,7 +128,7 @@ func (bot *TipBot) generateDalleImages(event Event) { task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) if err != nil { log.Errorf("[Generate] %v", err.Error()) - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "") return } // poll the task.ID until status is succeeded @@ -139,12 +139,12 @@ func (bot *TipBot) generateDalleImages(event Event) { for { select { case <-ctx.Done(): - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "") log.Errorf("[DALLE] ctx done") return // Got a timeout! fail with a timeout error case <-timeout: - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "Timeout. Please try again later.") log.Errorf("[DALLE] timeout") return // Got a tick, we should check on checkSomething() @@ -153,11 +153,11 @@ func (bot *TipBot) generateDalleImages(event Event) { // handle err if err != nil { log.Errorf("[GetTask] %v", err.Error()) - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "") return } if t.Status == dalle.StatusSucceeded { - fmt.Printf("[DALLE] task succeeded for user %s", GetUserStr(user.Telegram)) + log.Infof("[DALLE] task succeeded for user %s", GetUserStr(user.Telegram)) // download the first generated image for _, data := range t.Generations.Data { err = bot.downloadAndSendImages(ctx, dalleClient, data, invoiceEvent) @@ -169,7 +169,7 @@ func (bot *TipBot) generateDalleImages(event Event) { } else if t.Status == dalle.StatusRejected { log.Errorf("[DALLE] rejected: %s", t.ID) - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "Your prompt has been rejected by OpenAI. Do not use celebrity names, sexual expressions, or any other harmful content as prompt.") return } log.Debugf("[DALLE] pending for user %s", GetUserStr(user.Telegram)) @@ -203,7 +203,7 @@ func (bot *TipBot) downloadAndSendImages(ctx context.Context, dalleClient dalle. return nil } -func (bot *TipBot) dalleRefundUser(user *lnbits.User) error { +func (bot *TipBot) dalleRefundUser(user *lnbits.User, message string) error { if user.Wallet == nil { return fmt.Errorf("user has no wallet") } @@ -231,6 +231,13 @@ func (bot *TipBot) dalleRefundUser(user *lnbits.User) error { return err } log.Warnf("[DALLE] refunding user %s with %d sat", GetUserStr(user.Telegram), internal.Configuration.Generate.DallePrice) - bot.trySendMessage(user.Telegram, "🚫 Something went wrong. You have been refunded.") + + var err_reason string + if len(message) > 0 { + err_reason = message + } else { + err_reason = "Something went wrong." + } + bot.trySendMessage(user.Telegram, fmt.Sprintf("🚫 %s You have been refunded.", err_reason)) return nil } From 89552a73b7642f90da93c8f61bc7f2e2a2c0665e Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 31 Aug 2022 10:58:50 +0200 Subject: [PATCH 353/541] changes from main --- internal/telegram/generate.go | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 2523a6b3..68719f66 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -125,7 +125,7 @@ func (bot *TipBot) generateDalleImages(event Event) { // handle err if err != nil { log.Errorf("[NewHTTPClient] %v", err.Error()) - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "") return } @@ -135,7 +135,7 @@ func (bot *TipBot) generateDalleImages(event Event) { task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) if err != nil { log.Errorf("[Generate] %v", err.Error()) - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "") return } // poll the task.ID until status is succeeded @@ -146,12 +146,12 @@ func (bot *TipBot) generateDalleImages(event Event) { for { select { case <-ctx.Done(): - bot.dalleRefundUser(user) - log.Errorf("[DALLE] ctx done") + bot.dalleRefundUser(user, "") + log.Errorf("[DALLE] ctx done", "") return // Got a timeout! fail with a timeout error case <-timeout: - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "Timeout. Please try again later.") log.Errorf("[DALLE] timeout") return // Got a tick, we should check on checkSomething() @@ -160,7 +160,7 @@ func (bot *TipBot) generateDalleImages(event Event) { // handle err if err != nil { log.Errorf("[GetTask] %v", err.Error()) - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "") return } if t.Status == dalle.StatusSucceeded { @@ -176,7 +176,7 @@ func (bot *TipBot) generateDalleImages(event Event) { } else if t.Status == dalle.StatusRejected { log.Errorf("[DALLE] rejected: %s", t.ID) - bot.dalleRefundUser(user) + bot.dalleRefundUser(user, "Your prompt has been rejected by OpenAI. Do not use celebrity names, sexual expressions, or any other harmful content as prompt.") return } log.Debugf("[DALLE] pending for user %s", GetUserStr(user.Telegram)) @@ -212,7 +212,7 @@ func (bot *TipBot) downloadAndSendImages(ctx context.Context, dalleClient dalle. return nil } -func (bot *TipBot) dalleRefundUser(user *lnbits.User) error { +func (bot *TipBot) dalleRefundUser(user *lnbits.User, message string) error { if user.Wallet == nil { return fmt.Errorf("user has no wallet") } @@ -240,6 +240,13 @@ func (bot *TipBot) dalleRefundUser(user *lnbits.User) error { return err } log.Warnf("[DALLE] refunding user %s with %d sat", GetUserStr(user.Telegram), internal.Configuration.Generate.DallePrice) - bot.trySendMessage(user.Telegram, "🚫 Something went wrong. You have been refunded.") + + var err_reason string + if len(message) > 0 { + err_reason = message + } else { + err_reason = "Something went wrong." + } + bot.trySendMessage(user.Telegram, fmt.Sprintf("🚫 %s You have been refunded.", err_reason)) return nil } From 979d3c58a2bdd6b006e7d032b8d7c1a11a4ad231 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 31 Aug 2022 15:35:47 +0200 Subject: [PATCH 354/541] readd help button --- internal/telegram/buttons.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index f16a6e30..324eacae 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -13,7 +13,7 @@ import ( // we can't use space in the label of buttons, because string splitting will mess everything up. const ( - MainMenuCommandWebApp = "⤵️ Receive" + MainMenuCommandWebApp = "⤵️ Recv" MainMenuCommandBalance = "Balance" MainMenuCommandInvoice = "⚡️ Invoice" MainMenuCommandHelp = "📖 Help" @@ -38,7 +38,7 @@ func init() { btnBalanceMainMenu = mainMenu.Text(MainMenuCommandBalance) mainMenu.Reply( mainMenu.Row(btnBalanceMainMenu), - mainMenu.Row(btnInvoiceMainMenu, btnWebAppMainMenu, btnSendMainMenu), + mainMenu.Row(btnInvoiceMainMenu, btnWebAppMainMenu, btnSendMainMenu, btnHelpMainMenu), ) } @@ -100,7 +100,7 @@ func (bot *TipBot) mainMenuBalanceButtonUpdate(to int64) { bot.appendWebAppLinkToButton(&btnWebAppMainMenu, user) mainMenu.Reply( mainMenu.Row(btnBalanceMainMenu), - mainMenu.Row(btnInvoiceMainMenu, btnWebAppMainMenu, btnSendMainMenu), + mainMenu.Row(btnInvoiceMainMenu, btnWebAppMainMenu, btnSendMainMenu, btnHelpMainMenu), ) } } From e170394eb8c6fdf1a67f2aff791184a98053792a Mon Sep 17 00:00:00 2001 From: gohumble Date: Wed, 31 Aug 2022 15:44:29 +0200 Subject: [PATCH 355/541] add workerId --- internal/telegram/generate.go | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 2523a6b3..4893cdc7 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -97,16 +97,18 @@ func (bot *TipBot) confirmGenerateImages(ctx intercept.Context) (intercept.Conte return ctx, nil } -var jobChan chan func() +var jobChan chan func(workerId int) +var workers = 2 func init() { - jobChan = make(chan func(), 2) - go worker(jobChan) - go worker(jobChan) + jobChan = make(chan func(workerId int), workers) + for i := 0; i < workers; i++ { + go worker(jobChan, i) + } } -func worker(linkChan chan func()) { +func worker(linkChan chan func(workerId int), workerId int) { for generatePrompt := range linkChan { - generatePrompt() + generatePrompt(workerId) } } @@ -119,12 +121,12 @@ func (bot *TipBot) generateDalleImages(event Event) { return } bot.trySendMessage(user.Telegram, "🔄 Your images are being generated. Please wait a few moments.") - var job = func() { + var job = func(worker int) { // create the client with the bearer token api key dalleClient, err := dalle.NewHTTPClient(internal.Configuration.Generate.DalleKey) // handle err if err != nil { - log.Errorf("[NewHTTPClient] %v", err.Error()) + log.Errorf("[NewHTTPClient-%d] %v", worker, err.Error()) bot.dalleRefundUser(user) return } @@ -134,7 +136,7 @@ func (bot *TipBot) generateDalleImages(event Event) { // generate a task to create an image with a prompt task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) if err != nil { - log.Errorf("[Generate] %v", err.Error()) + log.Errorf("[Generate-%d] %v", worker, err.Error()) bot.dalleRefundUser(user) return } @@ -147,12 +149,12 @@ func (bot *TipBot) generateDalleImages(event Event) { select { case <-ctx.Done(): bot.dalleRefundUser(user) - log.Errorf("[DALLE] ctx done") + log.Errorf("[DALLE-%d] ctx done", worker) return // Got a timeout! fail with a timeout error case <-timeout: bot.dalleRefundUser(user) - log.Errorf("[DALLE] timeout") + log.Errorf("[DALLE-%d] timeout", worker) return // Got a tick, we should check on checkSomething() case <-ticker: @@ -164,22 +166,22 @@ func (bot *TipBot) generateDalleImages(event Event) { return } if t.Status == dalle.StatusSucceeded { - fmt.Printf("[DALLE] task succeeded for user %s", GetUserStr(user.Telegram)) + fmt.Printf("[DALLE-%d] task succeeded for user %s", worker, GetUserStr(user.Telegram)) // download the first generated image for _, data := range t.Generations.Data { err = bot.downloadAndSendImages(ctx, dalleClient, data, invoiceEvent) if err != nil { - log.Errorf("[downloadAndSendImages] %v", err.Error()) + log.Errorf("[downloadAndSendImages-%d] %v", worker, err.Error()) } } return } else if t.Status == dalle.StatusRejected { - log.Errorf("[DALLE] rejected: %s", t.ID) + log.Errorf("[DALLE-%d] rejected: %s", worker, t.ID) bot.dalleRefundUser(user) return } - log.Debugf("[DALLE] pending for user %s", GetUserStr(user.Telegram)) + log.Debugf("[DALLE-%d] pending for user %s", worker, GetUserStr(user.Telegram)) } } } From 857649150d0696cf8c9e73425307a87ac6332e3f Mon Sep 17 00:00:00 2001 From: gohumble <55599638+gohumble@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:07:04 +0200 Subject: [PATCH 356/541] fix format (#407) --- internal/telegram/generate.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/telegram/generate.go b/internal/telegram/generate.go index 2bf3aa57..abe9ded5 100644 --- a/internal/telegram/generate.go +++ b/internal/telegram/generate.go @@ -127,7 +127,7 @@ func (bot *TipBot) generateDalleImages(event Event) { // handle err if err != nil { log.Errorf("[NewHTTPClient-%d] %v", workerId, err.Error()) - bot.dalleRefundUser(user,"") + bot.dalleRefundUser(user, "") return } @@ -137,7 +137,7 @@ func (bot *TipBot) generateDalleImages(event Event) { task, err := dalleClient.Generate(ctx, invoiceEvent.CallbackData) if err != nil { log.Errorf("[Generate-%d] %v", workerId, err.Error()) - bot.dalleRefundUser(user,"") + bot.dalleRefundUser(user, "") return } // poll the task.ID until status is succeeded @@ -148,12 +148,12 @@ func (bot *TipBot) generateDalleImages(event Event) { for { select { case <-ctx.Done(): - bot.dalleRefundUser(user,"") + bot.dalleRefundUser(user, "") log.Errorf("[DALLE-%d] ctx done", workerId) return // Got a timeout! fail with a timeout error case <-timeout: - bot.dalleRefundUser(user,"Timeout. Please try again later.") + bot.dalleRefundUser(user, "Timeout. Please try again later.") log.Errorf("[DALLE-%d] timeout", workerId) return // Got a tick, we should check on checkSomething() @@ -161,27 +161,27 @@ func (bot *TipBot) generateDalleImages(event Event) { t, err = dalleClient.GetTask(ctx, task.ID) // handle err if err != nil { - log.Errorf("[GetTask] %v", err.Error()) - bot.dalleRefundUser(user,"") + log.Errorf("[GetTask-%d] %v", workerId, err.Error()) + bot.dalleRefundUser(user, "") return } if t.Status == dalle.StatusSucceeded { - fmt.Printf("[DALLE-%d] task succeeded for user %s", worker, GetUserStr(user.Telegram)) + log.Printf("[DALLE-%d] task succeeded for user %s", workerId, GetUserStr(user.Telegram)) // download the first generated image for _, data := range t.Generations.Data { err = bot.downloadAndSendImages(ctx, dalleClient, data, invoiceEvent) if err != nil { - log.Errorf("[downloadAndSendImages-%d] %v", worker, err.Error()) + log.Errorf("[downloadAndSendImages-%d] %v", workerId, err.Error()) } } return } else if t.Status == dalle.StatusRejected { log.Errorf("[DALLE-%d] rejected: %s", workerId, t.ID) - bot.dalleRefundUser(user,"Your prompt has been rejected by OpenAI. Do not use celebrity names, sexual expressions, or any other harmful content as prompt.") + bot.dalleRefundUser(user, "Your prompt has been rejected by OpenAI. Do not use celebrity names, sexual expressions, or any other harmful content as prompt.") return } - log.Debugf("[DALLE-%d] pending for user %s", worker, GetUserStr(user.Telegram)) + log.Debugf("[DALLE-%d] pending for user %s", workerId, GetUserStr(user.Telegram)) } } } From 51ac159072b39050e519fb96a17c186911ddccf6 Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Wed, 31 Aug 2022 16:54:19 +0200 Subject: [PATCH 357/541] remove href --- internal/api/userpage/static/webapp.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/api/userpage/static/webapp.html b/internal/api/userpage/static/webapp.html index 2b1bd637..e5d51fab 100644 --- a/internal/api/userpage/static/webapp.html +++ b/internal/api/userpage/static/webapp.html @@ -15,7 +15,7 @@ - LNURL + @LightningTipBot

k?$|Jc0LU+# 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 Date: Sat, 16 Apr 2022 00:20:52 +0200 Subject: [PATCH 236/541] help (#334) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- botfather-setcommands.txt | 2 +- internal/lnbits/lnbits.go | 27 ++++++++++++++++- internal/lnbits/types.go | 17 +++++++++++ internal/telegram/faucet.go | 2 +- internal/telegram/groups.go | 4 +-- internal/telegram/handler.go | 45 ++++++++++++++++++++++++++++- internal/telegram/inline_receive.go | 2 +- internal/telegram/inline_send.go | 2 +- internal/telegram/send.go | 5 ++-- internal/telegram/shop.go | 2 +- internal/telegram/tip.go | 7 +++-- internal/telegram/tipjar.go | 2 +- translations/de.toml | 6 ++-- translations/en.toml | 4 ++- translations/es.toml | 4 ++- translations/fr.toml | 4 ++- translations/id.toml | 4 ++- translations/it.toml | 4 ++- translations/nl.toml | 4 ++- translations/pt-br.toml | 4 ++- translations/ru.toml | 4 ++- translations/tr.toml | 4 ++- 22 files changed, 133 insertions(+), 26 deletions(-) diff --git a/botfather-setcommands.txt b/botfather-setcommands.txt index 1a50f82b..5e61ae0f 100644 --- a/botfather-setcommands.txt +++ b/botfather-setcommands.txt @@ -7,11 +7,11 @@ help - Read the help. balance - Check balance. +transactions - List transactions tip - Reply to a message to tip: /tip 50 send - Send funds to a user: /send 100 @LightningTipBot invoice - Receive with Lightning: /invoice 1000 pay - Pay with Lightning: /pay lnbc10n1ps... donate - Donate: /donate 1000 faucet - Create a faucet: /faucet 2100 21 -tipjar - Create a tipjar: /tipjar 100 10 advanced - Advanced help \ No newline at end of file diff --git a/internal/lnbits/lnbits.go b/internal/lnbits/lnbits.go index 8c4edde2..c446b9ae 100644 --- a/internal/lnbits/lnbits.go +++ b/internal/lnbits/lnbits.go @@ -1,8 +1,9 @@ package lnbits import ( - "github.com/imroc/req" "time" + + "github.com/imroc/req" ) // NewClient returns a new lnbits api client. Pass your API key and url here. @@ -130,6 +131,30 @@ func (c Client) Info(w Wallet) (wtx Wallet, err error) { return } +// Info returns wallet payments +func (c Client) Payments(w Wallet) (wtx Payments, err error) { + // custom header with invoice key + invoiceHeader := req.Header{ + "Content-Type": "application/json", + "Accept": "application/json", + "X-Api-Key": w.Inkey, + } + resp, err := req.Get(c.url+"/api/v1/payments?limit=60", invoiceHeader, nil) + if err != nil { + return + } + + if resp.Response().StatusCode >= 300 { + var reqErr Error + resp.ToJSON(&reqErr) + err = reqErr + return + } + + err = resp.ToJSON(&wtx) + return +} + // Wallets returns all wallets belonging to an user func (c Client) Wallets(w User) (wtx []Wallet, err error) { resp, err := req.Get(c.url+"/usermanager/api/v1/wallets/"+w.ID, c.header, nil) diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 78959526..6c5dd4a8 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -105,6 +105,23 @@ type Wallet struct { Name string `json:"name"` User string `json:"user"` } + +type Payments []struct { + CheckingID string `json:"checking_id"` + Pending bool `json:"pending"` + Amount int `json:"amount"` + Fee int `json:"fee"` + Memo string `json:"memo"` + Time int `json:"time"` + Bolt11 string `json:"bolt11"` + Preimage string `json:"preimage"` + PaymentHash string `json:"payment_hash"` + Extra struct{} `json:"extra"` + WalletID string `json:"wallet_id"` + Webhook interface{} `json:"webhook"` + WebhookStatus interface{} `json:"webhook_status"` +} + type BitInvoice struct { PaymentHash string `json:"payment_hash"` PaymentRequest string `json:"payment_request"` diff --git a/internal/telegram/faucet.go b/internal/telegram/faucet.go index 86103138..30a05511 100644 --- a/internal/telegram/faucet.go +++ b/internal/telegram/faucet.go @@ -304,7 +304,7 @@ func (bot *TipBot) acceptInlineFaucetHandler(ctx intercept.Context) (intercept.C } // todo: user new get username function to get userStrings - transactionMemo := fmt.Sprintf("Faucet from %s to %s (%d sat).", fromUserStr, toUserStr, inlineFaucet.PerUserAmount) + transactionMemo := fmt.Sprintf("🚰 Faucet from %s to %s.", fromUserStr, toUserStr) t := NewTransaction(bot, from, to, inlineFaucet.PerUserAmount, TransactionType("faucet")) t.Memo = transactionMemo diff --git a/internal/telegram/groups.go b/internal/telegram/groups.go index 4bed39c3..72619342 100644 --- a/internal/telegram/groups.go +++ b/internal/telegram/groups.go @@ -84,7 +84,7 @@ var ( ) var ( - groupInvoiceMemo = "Ticket for group %s" + groupInvoiceMemo = "🎟 Ticket for group %s" ) // groupHandler is called if the /group command is invoked. It then decides with other @@ -346,7 +346,7 @@ func (bot *TipBot) groupGetInviteLinkHandler(event Event) { lnbits.InvoiceParams{ Out: false, Amount: commissionSat, - Memo: "Ticket commission for group " + ticketEvent.Group.Title, + Memo: "🎟 Ticket commission for group " + ticketEvent.Group.Title, Webhook: internal.Configuration.Lnbits.WebhookServer}, bot.Client) if err != nil { diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index d2b26ebc..9a0a3e23 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -120,6 +120,7 @@ func (bot TipBot) getHandler() []InterceptionWrapper { Handler: bot.payHandler, Interceptor: &Interceptor{ Before: []intercept.Func{ + bot.requirePrivateChatInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, @@ -136,6 +137,7 @@ func (bot TipBot) getHandler() []InterceptionWrapper { Interceptor: &Interceptor{ Before: []intercept.Func{ + bot.requirePrivateChatInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, @@ -183,6 +185,7 @@ func (bot TipBot) getHandler() []InterceptionWrapper { Interceptor: &Interceptor{ Before: []intercept.Func{ + bot.requirePrivateChatInterceptor, bot.localizerInterceptor, bot.logMessageInterceptor, bot.requireUserInterceptor, @@ -227,11 +230,51 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, + { + Endpoints: []interface{}{"/transactions"}, + Handler: bot.transactionsHandler, + Interceptor: &Interceptor{ + Before: []intercept.Func{ + bot.requirePrivateChatInterceptor, + bot.localizerInterceptor, + bot.logMessageInterceptor, + bot.requireUserInterceptor, + }}, + }, + { + Endpoints: []interface{}{&btnLeftTransactionsButton}, + Handler: bot.transactionsScrollLeftHandler, + Interceptor: &Interceptor{ + + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.loadUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, + { + Endpoints: []interface{}{&btnRightTransactionsButton}, + Handler: bot.transactionsScrollRightHandler, + Interceptor: &Interceptor{ + + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.loadUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, { Endpoints: []interface{}{"/faucet", "/zapfhahn", "/kraan", "/grifo"}, Handler: bot.faucetHandler, Interceptor: &Interceptor{ - Before: []intercept.Func{ bot.localizerInterceptor, bot.logMessageInterceptor, diff --git a/internal/telegram/inline_receive.go b/internal/telegram/inline_receive.go index 7b65e5d7..1bad4cb0 100644 --- a/internal/telegram/inline_receive.go +++ b/internal/telegram/inline_receive.go @@ -251,7 +251,7 @@ func (bot *TipBot) sendInlineReceiveHandler(ctx intercept.Context) (intercept.Co inlineReceive.Inactivate(inlineReceive, bot.Bunt) // todo: user new get username function to get userStrings - transactionMemo := fmt.Sprintf("InlineReceive from %s to %s (%d sat).", fromUserStr, toUserStr, inlineReceive.Amount) + transactionMemo := fmt.Sprintf("💸 Receive from %s to %s.", fromUserStr, toUserStr) t := NewTransaction(bot, from, to, inlineReceive.Amount, TransactionType("inline receive")) t.Memo = transactionMemo success, err := t.Send() diff --git a/internal/telegram/inline_send.go b/internal/telegram/inline_send.go index eb28a984..90289f1a 100644 --- a/internal/telegram/inline_send.go +++ b/internal/telegram/inline_send.go @@ -224,7 +224,7 @@ func (bot *TipBot) acceptInlineSendHandler(ctx intercept.Context) (intercept.Con inlineSend.Inactivate(inlineSend, bot.Bunt) // todo: user new get username function to get userStrings - transactionMemo := fmt.Sprintf("InlineSend from %s to %s (%d sat).", fromUserStr, toUserStr, amount) + transactionMemo := fmt.Sprintf("💸 Send from %s to %s.", fromUserStr, toUserStr) t := NewTransaction(bot, fromUser, to, amount, TransactionType("inline send")) t.Memo = transactionMemo success, err := t.Send() diff --git a/internal/telegram/send.go b/internal/telegram/send.go index 63bf3e4f..3f343945 100644 --- a/internal/telegram/send.go +++ b/internal/telegram/send.go @@ -4,9 +4,10 @@ import ( "context" "encoding/json" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strings" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" @@ -306,7 +307,7 @@ func (bot *TipBot) confirmSendHandler(ctx intercept.Context) (intercept.Context, toUserStr := GetUserStr(to.Telegram) fromUserStr := GetUserStr(from.Telegram) - transactionMemo := fmt.Sprintf("Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount) + transactionMemo := fmt.Sprintf("💸 Send from %s to %s.", fromUserStr, toUserStr) t := NewTransaction(bot, from, to, amount, TransactionType("send")) t.Memo = transactionMemo diff --git a/internal/telegram/shop.go b/internal/telegram/shop.go index 115e199e..a65279ba 100644 --- a/internal/telegram/shop.go +++ b/internal/telegram/shop.go @@ -858,7 +858,7 @@ func (bot *TipBot) shopConfirmBuyHandler(ctx intercept.Context) (intercept.Conte log.Errorf("[shopConfirmBuyHandler] item has no price.") return ctx, errors.Create(errors.InvalidAmountError) } - transactionMemo := fmt.Sprintf("Buy item %s (%d sat).", toUserStr, amount) + transactionMemo := fmt.Sprintf("🛍 Shop from %s.", toUserStr) t := NewTransaction(bot, from, to, amount, TransactionType("shop")) t.Memo = transactionMemo diff --git a/internal/telegram/tip.go b/internal/telegram/tip.go index c79e9c80..f65c4883 100644 --- a/internal/telegram/tip.go +++ b/internal/telegram/tip.go @@ -3,11 +3,12 @@ package telegram import ( "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/errors" - "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal" "github.com/LightningTipBot/LightningTipBot/internal/str" @@ -106,7 +107,7 @@ func (bot *TipBot) tipHandler(ctx intercept.Context) (intercept.Context, error) } // todo: user new get username function to get userStrings - transactionMemo := fmt.Sprintf("Tip from %s to %s (%d sat).", fromUserStr, toUserStr, amount) + transactionMemo := fmt.Sprintf("🏅 Tip from %s to %s.", fromUserStr, toUserStr) t := NewTransaction(bot, from, to, amount, TransactionType("tip"), TransactionChat(m.Chat)) t.Memo = transactionMemo success, err := t.Send() diff --git a/internal/telegram/tipjar.go b/internal/telegram/tipjar.go index a9b13ddd..951c7d60 100644 --- a/internal/telegram/tipjar.go +++ b/internal/telegram/tipjar.go @@ -279,7 +279,7 @@ func (bot *TipBot) acceptInlineTipjarHandler(ctx intercept.Context) (intercept.C fromUserStr := GetUserStr(from.Telegram) // todo: user new get username function to get userStrings - transactionMemo := fmt.Sprintf("Tipjar from %s to %s (%d sat).", fromUserStr, toUserStr, inlineTipjar.PerUserAmount) + transactionMemo := fmt.Sprintf("🍯 Tipjar from %s to %s.", fromUserStr, toUserStr) t := NewTransaction(bot, from, to, inlineTipjar.PerUserAmount, TransactionType("tipjar")) t.Memo = transactionMemo diff --git a/translations/de.toml b/translations/de.toml index 00582c05..b81ce38a 100644 --- a/translations/de.toml +++ b/translations/de.toml @@ -118,11 +118,13 @@ advancedMessage = """%s 📖 Du kannst Inline Befehle in jedem Chat verwenden, sogar in privaten Nachrichten. Warte eine Sekunde, nachdem du den Befehl eingegeben hast und *klicke* auf das Ergebnis, statt Enter einzugeben. ⚙️ *Fortgeschrittene Befehle* +*/transactions* 📊 Liste der Transaktionen */link* 🔗 Verbinde dein Wallet mit [BlueWallet](https://bluewallet.io/) oder [Zeus](https://zeusln.app/) -*/lnurl* ⚡️ Lnurl empfangen oder senden: `/lnurl` or `/lnurl ` +*/lnurl* ⚡️ Lnurl empfangen oder senden: `/lnurl` oder `/lnurl ` */faucet* 🚰 Erzeuge einen Zapfhahn: `/faucet ` */tipjar* 🍯 Erzeuge eine Spendendose: `/tipjar ` -*/group* 🎟 Tickets für Gruppenchats: `/group add []`""" +*/group* 🎟 Tickets für Gruppenchats: `/group add []` +*/shop* 🛍 Durchsuche shops: `/shop` oder `/shop `""" # GENERIC enterAmountRangeMessage = """⌨️ Gebe einen Betrag zwischen %d und %d sat ein.""" diff --git a/translations/en.toml b/translations/en.toml index d75d956b..1d517fb2 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -121,11 +121,13 @@ advancedMessage = """%s 📖 You can use inline commands in every chat, even in private conversations. Wait a second after entering an inline command and *click* the result, don't press enter. ⚙️ *Advanced commands* +*/transactions* 📊 List transactions */link* 🔗 Link your wallet to [BlueWallet](https://bluewallet.io/) or [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl receive or pay: `/lnurl` or `/lnurl ` */faucet* 🚰 Create a faucet: `/faucet ` */tipjar* 🍯 Create a tipjar: `/tipjar ` -*/group* 🎟 Create group tickets: `/group add []`""" +*/group* 🎟 Create group tickets: `/group add []` +*/shop* 🛍 Browse shops: `/shop` or `/shop `""" # GENERIC enterAmountRangeMessage = """💯 Enter an amount between %d and %d sat.""" diff --git a/translations/es.toml b/translations/es.toml index 4c901d61..f642b442 100644 --- a/translations/es.toml +++ b/translations/es.toml @@ -117,11 +117,13 @@ advancedMessage = """%s 📖 Puedes usar comandos _inline_ en todos los chats, incluso en las conversaciones privadas. Espera un segundo después de introducir un comando _inline_ y *haz clic* en el resultado, no pulses enter. ⚙️ *Comandos avanzados* +*/transactions* 📊 List transactions */link* 🔗 Enlaza tu monedero a [ BlueWallet ](https://bluewallet.io/) o [ Zeus ](https://zeusln.app/) */lnurl* ⚡️ Lnurl recibir o pagar: `/lnurl` o `/lnurl ` */faucet* 🚰 Crear un grifo: `/faucet ` */tipjar* 🍯 Crear un tipjar: `/tipjar ` -*/group* 🎟 Create group tickets: `/group add []`""" +*/group* 🎟 Create group tickets: `/group add []` +*/shop* 🛍 Browse shops: `/shop` or `/shop `""" # GENERIC enterAmountRangeMessage = """💯 Introduce un monto entre %d y %d sat.""" diff --git a/translations/fr.toml b/translations/fr.toml index af812651..2c0c2f11 100644 --- a/translations/fr.toml +++ b/translations/fr.toml @@ -117,11 +117,13 @@ advancedMessage = """%s 📖 Vous pouvez utiliser ces commandes dans tous les chats et même dans les conversations privées. Attendez une seconde après avoir tapé une commandé puis *click* sur le résultat, n'appuyez pas sur entrée. ⚙️ *Commandes avancées* +*/transactions* 📊 List transactions */link* 🔗 Lier votre wallet à [BlueWallet](https://bluewallet.io/) ou [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl recevoir ou payer: `/lnurl` ou `/lnurl ` */faucet* 🚰 Créer un faucet: `/faucet ` */tipjar* 🍯 Créer un tipjar: `/tipjar ` -*/group* 🎟 Create group tickets: `/group add []`""" +*/group* 🎟 Create group tickets: `/group add []` +*/shop* 🛍 Browse shops: `/shop` or `/shop `""" # GENERIC enterAmountRangeMessage = """💯 Choisissez un montant entre %d et %d sat.""" diff --git a/translations/id.toml b/translations/id.toml index 272cb284..52eecf9c 100644 --- a/translations/id.toml +++ b/translations/id.toml @@ -117,11 +117,13 @@ advancedMessage = """%s 📖 Kamu dapat menggunakan sebaris perintah di tiap percakapan, bahkan di percakapan privat. Tunggu sejenak setelah memasukkan sebaris perintah lalu *pencet* hasilnya, jangan tekan enter. ⚙️ *Perintah lanjutan* +*/transactions* 📊 List transactions */link* 🔗 Menghubungkan dompet mu ke [BlueWallet](https://bluewallet.io/) atau [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl menerima atau membayar: `/lnurl` atau `/lnurl ` */faucet* 🚰 Membuat sebuah keran `/faucet ` */tipjar* 🍯 Create a tipjar: `/tipjar ` -*/group* 🎟 Create group tickets: `/group add []`""" +*/group* 🎟 Create group tickets: `/group add []` +*/shop* 🛍 Browse shops: `/shop` or `/shop `""" # GENERIC enterAmountRangeMessage = """💯 Masukkan jumlah diantara %d dan %d sat.""" diff --git a/translations/it.toml b/translations/it.toml index b771f0a6..375046a6 100644 --- a/translations/it.toml +++ b/translations/it.toml @@ -117,11 +117,13 @@ advancedMessage = """%s 📖 Puoi usare i comandi in linea in ogni chat, anche nelle conversazioni private. Attendi un secondo dopo aver inviato un comando in linea e *clicca* sull'azione desiderata, non premere invio. ⚙️ *Comandi avanzati* +*/transactions* 📊 List transactions */link* 🔗 Crea un collegamento al tuo wallet [BlueWallet](https://bluewallet.io/) o [Zeus](https://zeusln.app/) */lnurl* ⚡️ Ricevi o paga un Lnurl: `/lnurl` or `/lnurl ` */faucet* 🚰 Crea una distribuzione: `/faucet ` */tipjar* 🍯 Crea un tipjar: `/tipjar ` -*/group* 🎟 Create group tickets: `/group add []`""" +*/group* 🎟 Create group tickets: `/group add []` +*/shop* 🛍 Browse shops: `/shop` or `/shop `""" # GENERIC enterAmountRangeMessage = """💯 Imposta un ammontare tra %d e %d sat.""" diff --git a/translations/nl.toml b/translations/nl.toml index 12bd5676..789349c7 100644 --- a/translations/nl.toml +++ b/translations/nl.toml @@ -117,11 +117,13 @@ advancedMessage = """%s 📖 Je kunt inline commando's in elke chat gebruiken, zelfs in privé gesprekken. Wacht een seconde na het invoeren van een inline commando en *klik* op het resultaat, druk niet op enter. ⚙️ *Geavanceerde opdrachten* +*/transactions* 📊 List transactions */link* 🔗 Koppel uw wallet aan [BlueWallet](https://bluewallet.io/) of [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl ontvangen of betalen: `/lnurl` of `/lnurl ` */faucet* 🚰 Maak een kraan: `/faucet ` */tipjar* 🍯 Maak een tipjar: `/tipjar ` -*/group* 🎟 Create group tickets: `/group add []`""" +*/group* 🎟 Create group tickets: `/group add []` +*/shop* 🛍 Browse shops: `/shop` or `/shop `""" # GENERIC enterAmountRangeMessage = """💯 Voer een bedrag in tussen %d en %d sat.""" diff --git a/translations/pt-br.toml b/translations/pt-br.toml index 51ad6fa3..8f1250d1 100644 --- a/translations/pt-br.toml +++ b/translations/pt-br.toml @@ -117,11 +117,13 @@ advancedMessage = """%s 📖 Você pode usar comandos _inline_ em todas as conversas, mesmo em conversas privadas. Espere um segundo após inserir um comando _inline_ e *clique* no resultado, não pressione enter. ⚙️ *Comandos avançados* +*/transactions* 📊 List transactions */link* 🔗 Vincule sua carteira a [ BlueWallet ](https://bluewallet.io/) ou [ Zeus ](https://zeusln.app/) */lnurl* ⚡️ Receber ou pagar com lnurl: `/lnurl` o `/lnurl ` */faucet* 🚰 Criar uma torneira: `/faucet ` */tipjar* 🍯 Criar uma tipjar: `/tipjar ` -*/group* 🎟 Create group tickets: `/group add []`""" +*/group* 🎟 Create group tickets: `/group add []` +*/shop* 🛍 Browse shops: `/shop` or `/shop `""" # GENERIC enterAmountRangeMessage = """💯 Insira uma quantia entre %d e %d sat.""" diff --git a/translations/ru.toml b/translations/ru.toml index 1027133e..8ae30378 100644 --- a/translations/ru.toml +++ b/translations/ru.toml @@ -121,11 +121,13 @@ advancedMessage = """%s 📖 Вы можете использовать команды в любом чате, даже в личных беседах. Подождите секунду после ввода команды и *щелкните* результат, не нажимайте Enter.. ⚙️ *Продвинутые команды* +*/transactions* 📊 List transactions */link* 🔗 Link your wallet to [BlueWallet](https://bluewallet.io/) or [Zeus](https://zeusln.app/) */lnurl* Получить или оплатить через ⚡️Lnurl: `/lnurl` or `/lnurl ` */faucet* 🚰 Создать криптораздачу: `/faucet <ёмкость> <на_пользователя>` */tipjar* 🍯 Создать копилку: `/tipjar <ёмкость> <на_пользователя>` -*/group* 🎟 Create group tickets: `/group add []`""" +*/group* 🎟 Create group tickets: `/group add []` +*/shop* 🛍 Browse shops: `/shop` or `/shop `""" # GENERIC enterAmountRangeMessage = """💯 Введите количество между %d и %d sat.""" diff --git a/translations/tr.toml b/translations/tr.toml index 92e06e0d..0545c98a 100644 --- a/translations/tr.toml +++ b/translations/tr.toml @@ -117,11 +117,13 @@ advancedMessage = """%s 📖 İnline komutları her sohbette ve hatta özel mesajlarda kullanabilirsin. Komutu yazdıktan sonra bir saniye bekle ve Enter yazmak yerine sonuca *tıkla*. ⚙️ *Gelişmiş komutlar* +*/transactions* 📊 List transactions */link* 🔗 Cüzdanını bağla: [BlueWallet](https://bluewallet.io/) veya [Zeus](https://zeusln.app/) */lnurl* ⚡️ Lnurl iste veya gönder: `/lnurl` veya `/lnurl ` */faucet* 🚰 Bir fıçı oluştur: `/faucet ` */tipjar* 🍯 Bir tipjar oluştur: `/tipjar ` -*/group* 🎟 Create group tickets: `/group add []`""" +*/group* 🎟 Create group tickets: `/group add []` +*/shop* 🛍 Browse shops: `/shop` or `/shop `""" # GENERIC enterAmountRangeMessage = """💯 %d ve %d sat arasında bir miktar gir.""" From 5aaf9452b59cb0845b34bfc8a04245db552aa8c1 Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 16 Apr 2022 00:35:17 +0200 Subject: [PATCH 237/541] add file (#335) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/transactions.go | 173 ++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 internal/telegram/transactions.go diff --git a/internal/telegram/transactions.go b/internal/telegram/transactions.go new file mode 100644 index 00000000..899689bd --- /dev/null +++ b/internal/telegram/transactions.go @@ -0,0 +1,173 @@ +package telegram + +import ( + "fmt" + "time" + + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/str" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/eko/gocache/store" + log "github.com/sirupsen/logrus" + tb "gopkg.in/lightningtipbot/telebot.v3" +) + +type TransactionsList struct { + ID string `json:"id"` + User *lnbits.User `json:"from"` + Payments lnbits.Payments `json:"payments"` + LanguageCode string `json:"languagecode"` + CurrentPage int `json:"currentpage"` + MaxPages int `json:"maxpages"` + TxPerPage int `json:"txperpage"` +} + +func (txlist *TransactionsList) printTransactions(ctx intercept.Context) string { + txstr := "" + // for _, p := range payments { + payments := txlist.Payments + pagenr := txlist.CurrentPage + tx_per_page := txlist.TxPerPage + if pagenr > (len(payments)+1)/tx_per_page { + pagenr = 0 + } + if len(payments) < tx_per_page { + tx_per_page = len(payments) + } + start := pagenr * (tx_per_page - 1) + end := start + tx_per_page + if end >= len(payments) { + end = len(payments) - 1 + } + for i := start; i <= end; i++ { + p := payments[i] + if p.Pending { + txstr += "🔄" + } else { + if p.Amount < 0 { + txstr += "🔴" + } else { + txstr += "🟢" + } + } + timestr := time.Unix(int64(p.Time), 0).UTC().Format("2 Jan 06 15:04") + txstr += fmt.Sprintf("` %s`", timestr) + txstr += fmt.Sprintf("` %+d sat`", p.Amount/1000) + if p.Fee > 0 { + fee := p.Fee + if fee < 1000 { + fee = 1000 + } + txstr += fmt.Sprintf(" _(fee: %d sat)_", fee/1000) + } + memo := p.Memo + memo_maxlen := 50 + if len(memo) > memo_maxlen { + memo = memo[:memo_maxlen] + "..." + } + if len(memo) > 0 { + txstr += fmt.Sprintf("\n✉️ _%s_", str.MarkdownEscape(memo)) + } + txstr += "\n" + } + txstr += fmt.Sprintf("\nShowing %d transactions. Page %d of %d.", len(payments), txlist.CurrentPage+1, txlist.MaxPages) + return txstr +} + +var ( + transactionsMeno = &tb.ReplyMarkup{ResizeKeyboard: true} + btnLeftTransactionsButton = inlineTipjarMenu.Data("◀️", "left_transactions") + btnRightTransactionsButton = inlineTipjarMenu.Data("▶️", "right_transactions") +) + +func (bot *TipBot) makeTransactionsKeyboard(ctx intercept.Context, txlist TransactionsList) *tb.ReplyMarkup { + leftTransactionsButton := transactionsMeno.Data("←", "left_transactions", txlist.ID) + rightTransactionsButton := transactionsMeno.Data("→", "right_transactions", txlist.ID) + + if txlist.CurrentPage == 0 { + transactionsMeno.Inline( + transactionsMeno.Row( + leftTransactionsButton), + ) + } else if txlist.CurrentPage == txlist.MaxPages-1 { + transactionsMeno.Inline( + transactionsMeno.Row( + rightTransactionsButton), + ) + } else { + transactionsMeno.Inline( + transactionsMeno.Row( + leftTransactionsButton, + rightTransactionsButton), + ) + } + return transactionsMeno +} + +func (bot *TipBot) transactionsHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() + user := LoadUser(ctx) + payments, err := bot.Client.Payments(*user.Wallet) + if err != nil { + log.Errorf("[transactions] Error: %s", err.Error()) + return ctx, err + } + tx_per_page := 10 + transactionsList := TransactionsList{ + ID: fmt.Sprintf("txlist:%d:%s", user.Telegram.ID, RandStringRunes(5)), + User: user, + Payments: payments, + LanguageCode: ctx.Value("userLanguageCode").(string), + CurrentPage: 0, + TxPerPage: tx_per_page, + MaxPages: (len(payments)+1)/tx_per_page + 1, + } + bot.Cache.Set(fmt.Sprintf("%s_transactions", user.Name), transactionsList, &store.Options{Expiration: 1 * time.Minute}) + txstr := transactionsList.printTransactions(ctx) + bot.trySendMessage(m.Sender, txstr, bot.makeTransactionsKeyboard(ctx, transactionsList)) + return ctx, nil +} + +func (bot *TipBot) transactionsScrollLeftHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() + user := LoadUser(ctx) + transactionsListInterface, err := bot.Cache.Get(fmt.Sprintf("%s_transactions", user.Name)) + if err != nil { + log.Info("Transactions not in cache anymore") + return ctx, err + } + transactionsList := transactionsListInterface.(TransactionsList) + + if c.Sender.ID == transactionsList.User.Telegram.ID { + if transactionsList.CurrentPage < transactionsList.MaxPages-1 { + transactionsList.CurrentPage++ + } else { + return ctx, err + } + bot.Cache.Set(fmt.Sprintf("%s_transactions", user.Name), transactionsList, &store.Options{Expiration: 1 * time.Minute}) + bot.tryEditMessage(c.Message, transactionsList.printTransactions(ctx), bot.makeTransactionsKeyboard(ctx, transactionsList)) + } + return ctx, nil +} + +func (bot *TipBot) transactionsScrollRightHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() + user := LoadUser(ctx) + transactionsListInterface, err := bot.Cache.Get(fmt.Sprintf("%s_transactions", user.Name)) + if err != nil { + log.Info("Transactions not in cache anymore") + return ctx, err + } + transactionsList := transactionsListInterface.(TransactionsList) + + if c.Sender.ID == transactionsList.User.Telegram.ID { + if transactionsList.CurrentPage > 0 { + transactionsList.CurrentPage-- + } else { + return ctx, nil + } + bot.Cache.Set(fmt.Sprintf("%s_transactions", user.Name), transactionsList, &store.Options{Expiration: 1 * time.Minute}) + bot.tryEditMessage(c.Message, transactionsList.printTransactions(ctx), bot.makeTransactionsKeyboard(ctx, transactionsList)) + } + return ctx, nil +} From afcd530d0667d584c1b09037c8c9195bda7e8a0f Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Sat, 16 Apr 2022 00:39:12 +0200 Subject: [PATCH 238/541] markdown (#336) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- internal/telegram/transactions.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/telegram/transactions.go b/internal/telegram/transactions.go index 899689bd..341b29d0 100644 --- a/internal/telegram/transactions.go +++ b/internal/telegram/transactions.go @@ -66,7 +66,7 @@ func (txlist *TransactionsList) printTransactions(ctx intercept.Context) string memo = memo[:memo_maxlen] + "..." } if len(memo) > 0 { - txstr += fmt.Sprintf("\n✉️ _%s_", str.MarkdownEscape(memo)) + txstr += fmt.Sprintf("\n✉️ %s", str.MarkdownEscape(memo)) } txstr += "\n" } From 0631fd53eb3006e343432596f3fec33eb80182b3 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Fri, 29 Apr 2022 14:28:12 +0200 Subject: [PATCH 239/541] WIP: Satdress (#331) * satdress rebase * check config * satdress rebase * check config * check for invoices * return error * Update inline photo v5 (#332) * update url * round logo * resize Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * new logo (#333) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * help (#334) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * add file (#335) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * markdown (#336) Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> * satdress rebase * check config * satdress rebase * check for invoices * return error * satdress works * add lnbits * timeout * error checking * small lnd * errors * button fix Co-authored-by: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> --- go.mod | 9 +- go.sum | 4 + internal/api/admin/ban.go | 2 +- internal/lnbits/types.go | 13 + internal/lnbits/webhook/webhook.go | 2 +- internal/lndhub/lndhub.go | 2 +- internal/lnurl/lnurl.go | 2 +- internal/runtime/retry.go | 63 ++++ internal/satdress/satdress.go | 333 ++++++++++++++++++ internal/telegram/bot.go | 11 +- internal/telegram/buttons.go | 6 +- internal/telegram/database.go | 40 ++- internal/telegram/groups.go | 6 +- internal/telegram/handler.go | 31 ++ internal/telegram/invoice.go | 5 +- internal/telegram/satdress.go | 527 +++++++++++++++++++++++++++++ internal/telegram/transaction.go | 2 +- 17 files changed, 1027 insertions(+), 31 deletions(-) create mode 100644 internal/runtime/retry.go create mode 100644 internal/satdress/satdress.go create mode 100644 internal/telegram/satdress.go diff --git a/go.mod b/go.mod index e3bc764b..1f5016ed 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.15 require ( github.com/BurntSushi/toml v0.3.1 - github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 // indirect + github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 github.com/eko/gocache v1.2.0 github.com/fiatjaf/go-lnurl v1.8.4 github.com/fiatjaf/ln-decodepay v1.1.0 @@ -12,7 +12,7 @@ require ( github.com/imroc/req v0.3.0 github.com/jinzhu/configor v1.2.1 github.com/makiuchi-d/gozxing v0.0.2 - github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect + github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 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 @@ -20,10 +20,11 @@ require ( github.com/sirupsen/logrus v1.6.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/tidwall/buntdb v1.2.7 - github.com/tidwall/gjson v1.10.2 + github.com/tidwall/gjson v1.12.1 + github.com/tidwall/sjson v1.2.4 golang.org/x/text v0.3.5 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 - gopkg.in/lightningtipbot/telebot.v3 v3.0.0-20220326213923-f323bb71ac8e // indirect + gopkg.in/lightningtipbot/telebot.v3 v3.0.0-20220326213923-f323bb71ac8e gorm.io/driver/sqlite v1.1.4 gorm.io/gorm v1.21.12 ) diff --git a/go.sum b/go.sum index fbf01931..e8bb4dca 100644 --- a/go.sum +++ b/go.sum @@ -521,6 +521,8 @@ github.com/tidwall/gjson v1.6.1 h1:LRbvNuNuvAiISWg6gxLEFuCe72UKy5hDqhxW/8183ws= github.com/tidwall/gjson v1.6.1/go.mod h1:BaHyNc5bjzYkPqgLq7mdVzeiRtULKULXLgZFKsxEHI0= github.com/tidwall/gjson v1.10.2 h1:APbLGOM0rrEkd8WBw9C24nllro4ajFuJu0Sc9hRz8Bo= github.com/tidwall/gjson v1.10.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.12.1 h1:ikuZsLdhr8Ws0IdROXUS1Gi4v9Z4pGqpX/CvJkxvfpo= +github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/grect v0.1.3 h1:z9YwQAMUxVSBde3b7Sl8Da37rffgNfZ6Fq6h9t6KdXE= github.com/tidwall/grect v0.1.3/go.mod h1:8GMjwh3gPZVpLBI/jDz9uslCe0dpxRpWDdtN0lWAS/E= github.com/tidwall/lotsa v1.0.2 h1:dNVBH5MErdaQ/xd9s769R31/n2dXavsQ0Yf4TMEHHw8= @@ -536,6 +538,8 @@ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/rtred v0.1.2 h1:exmoQtOLvDoO8ud++6LwVsAMTu0KPzLTUrMln8u1yu8= github.com/tidwall/rtred v0.1.2/go.mod h1:hd69WNXQ5RP9vHd7dqekAz+RIdtfBogmglkZSRxCHFQ= +github.com/tidwall/sjson v1.2.4 h1:cuiLzLnaMeBhRmEv00Lpk3tkYrcxpmbU81tAY4Dw0tc= +github.com/tidwall/sjson v1.2.4/go.mod h1:098SZ494YoMWPmMO6ct4dcFnqxwj9r/gF0Etp19pSNM= github.com/tidwall/tinyqueue v0.1.1 h1:SpNEvEggbpyN5DIReaJ2/1ndroY8iyEGxPYxoSaymYE= github.com/tidwall/tinyqueue v0.1.1/go.mod h1:O/QNHwrnjqr6IHItYrzoHAKYhBkLI67Q096fQP5zMYw= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= diff --git a/internal/api/admin/ban.go b/internal/api/admin/ban.go index a5b24dff..80d41f38 100644 --- a/internal/api/admin/ban.go +++ b/internal/api/admin/ban.go @@ -68,7 +68,7 @@ func (s Service) getUserByTelegramId(r *http.Request) (*lnbits.User, error) { if v["id"] == "" { return nil, fmt.Errorf("invalid id") } - tx := s.bot.Database.Where("telegram_id = ? COLLATE NOCASE", v["id"]).First(user) + tx := s.bot.DB.Users.Where("telegram_id = ? COLLATE NOCASE", v["id"]).First(user) if tx.Error != nil { return nil, tx.Error } diff --git a/internal/lnbits/types.go b/internal/lnbits/types.go index 6c5dd4a8..1ce23b76 100644 --- a/internal/lnbits/types.go +++ b/internal/lnbits/types.go @@ -6,6 +6,7 @@ import ( "fmt" "time" + "github.com/LightningTipBot/LightningTipBot/internal/satdress" "github.com/btcsuite/btcd/btcec" "github.com/imroc/req" @@ -33,6 +34,18 @@ type User struct { AnonIDSha256 string `json:"anon_id_sha256"` UUID string `json:"uuid"` Banned bool `json:"banned"` + Settings *Settings `json:"settings" gorm:"foreignKey:id"` +} + +type Settings struct { + ID string `json:"id" gorm:"primarykey"` + Node NodeSettings `gorm:"embedded;embeddedPrefix:node_"` +} + +type NodeSettings struct { + NodeType string `json:"nodetype"` + LNDParams *satdress.LNDParams `gorm:"embedded;embeddedPrefix:lndparams_"` + LNbitsParams *satdress.LNBitsParams `gorm:"embedded;embeddedPrefix:lnbitsparams_"` } const ( diff --git a/internal/lnbits/webhook/webhook.go b/internal/lnbits/webhook/webhook.go index b17e2b92..00a0b731 100644 --- a/internal/lnbits/webhook/webhook.go +++ b/internal/lnbits/webhook/webhook.go @@ -54,7 +54,7 @@ func NewServer(bot *telegram.TipBot) *Server { } apiServer := &Server{ c: bot.Client, - database: bot.Database, + database: bot.DB.Users, bot: bot.Telegram, httpServer: srv, buntdb: bot.Bunt, diff --git a/internal/lndhub/lndhub.go b/internal/lndhub/lndhub.go index fce284fd..cc3342bf 100644 --- a/internal/lndhub/lndhub.go +++ b/internal/lndhub/lndhub.go @@ -18,7 +18,7 @@ type LndHub struct { } func New(bot *telegram.TipBot) LndHub { - return LndHub{database: bot.Database} + return LndHub{database: bot.DB.Users} } func (w LndHub) Handle(writer http.ResponseWriter, request *http.Request) { auth := request.Header.Get("Authorization") diff --git a/internal/lnurl/lnurl.go b/internal/lnurl/lnurl.go index a92e8aa8..ac6f79a9 100644 --- a/internal/lnurl/lnurl.go +++ b/internal/lnurl/lnurl.go @@ -57,7 +57,7 @@ type Lnurl struct { func New(bot *telegram.TipBot) Lnurl { return Lnurl{ c: bot.Client, - database: bot.Database, + database: bot.DB.Users, callbackHostname: internal.Configuration.Bot.LNURLHostUrl, WebhookServer: internal.Configuration.Lnbits.WebhookServer, buntdb: bot.Bunt, diff --git a/internal/runtime/retry.go b/internal/runtime/retry.go new file mode 100644 index 00000000..e23366de --- /dev/null +++ b/internal/runtime/retry.go @@ -0,0 +1,63 @@ +package runtime + +import ( + "context" + "time" +) + +// var retryMap cmap.ConcurrentMap + +// func init() { +// retryMap = cmap.New() +// } + +// ResettableFunctionTicker will reset the user state as soon as tick is delivered. +type FunctionRetry struct { + Ticker *time.Ticker + duration time.Duration + ctx context.Context + name string +} + +type FunctionRetryOption func(*FunctionRetry) + +func WithRetryDuration(d time.Duration) FunctionRetryOption { + return func(a *FunctionRetry) { + a.duration = d + } +} +func NewRetryTicker(ctx context.Context, name string, option ...FunctionRetryOption) *FunctionRetry { + t := &FunctionRetry{ + name: name, + ctx: ctx, + } + for _, opt := range option { + opt(t) + } + if t.duration == 0 { + t.duration = defaultTickerCoolDown + } + t.Ticker = time.NewTicker(t.duration) + return t +} + +func (t *FunctionRetry) Do(f func(), cancel_f func(), deadline_f func()) { + tickerMap.Set(t.name, t) + go func() { + for { + select { + case <-t.Ticker.C: + // ticker delivered signal. do function f + f() + case <-t.ctx.Done(): + if t.ctx.Err() == context.DeadlineExceeded { + deadline_f() + } + if t.ctx.Err() == context.Canceled { + cancel_f() + } + return + } + } + }() +} diff --git a/internal/satdress/satdress.go b/internal/satdress/satdress.go new file mode 100644 index 00000000..7f2d4279 --- /dev/null +++ b/internal/satdress/satdress.go @@ -0,0 +1,333 @@ +package satdress + +import ( + "bytes" + "crypto/tls" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + "github.com/LightningTipBot/LightningTipBot/internal" + log "github.com/sirupsen/logrus" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" +) + +// Much of this is from github.com/fiatjaf/makeinvoice +// but with added "checkInvoice" and http proxy support + +var HttpProxyURL = internal.Configuration.Bot.HttpProxy + +var Client = &http.Client{ + Timeout: 10 * time.Second, +} + +type LNDParams struct { + Cert []byte `json:"cert" gorm:"-"` + CertString string `json:"certstring"` + Host string `json:"host"` + Macaroon string `json:"macaroon"` +} + +func (l LNDParams) getCert() []byte { return l.Cert } +func (l LNDParams) isLocal() bool { return strings.HasPrefix(l.Host, "https://127.0.0.1") } +func (l LNDParams) isTor() bool { return strings.Index(l.Host, ".onion") != -1 } + +type LNBitsParams struct { + Cert string `json:"certstring"` + Host string `json:"host"` + Key string `json:"key"` +} + +func (l LNBitsParams) getCert() []byte { return []byte(l.Cert) } +func (l LNBitsParams) isTor() bool { return strings.Index(l.Host, ".onion") != -1 } +func (l LNBitsParams) isLocal() bool { return strings.HasPrefix(l.Host, "https://127.0.0.1") } + +type BackendParams interface { + getCert() []byte + isTor() bool + isLocal() bool +} + +type Params struct { + Backend BackendParams + Msatoshi int64 + Description string + DescriptionHash []byte + + Label string // only used for c-lightning +} + +type CheckInvoiceParams struct { + Backend BackendParams + PR string + Hash []byte + Status string +} + +func MakeInvoice(params Params) (CheckInvoiceParams, error) { + defer func(prevTransport http.RoundTripper) { + Client.Transport = prevTransport + }(Client.Transport) + + if params.Backend == nil { + return CheckInvoiceParams{}, errors.New("no backend specified") + } + + specialTransport := &http.Transport{} + + // use a cert or skip TLS verification? + if len(params.Backend.getCert()) > 0 { + caCertPool := x509.NewCertPool() + ok := caCertPool.AppendCertsFromPEM([]byte(params.Backend.getCert())) + if !ok { + return CheckInvoiceParams{}, fmt.Errorf("invalid root certificate") + } + specialTransport.TLSClientConfig = &tls.Config{RootCAs: caCertPool} + } else { + specialTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + // use a proxy? + if !params.Backend.isLocal() && len(HttpProxyURL) > 0 { + proxyURL, _ := url.Parse(HttpProxyURL) + specialTransport.Proxy = http.ProxyURL(proxyURL) + } + + Client.Transport = specialTransport + + // description hash? + var hexh, b64h string + if params.DescriptionHash != nil { + hexh = hex.EncodeToString(params.DescriptionHash) + b64h = base64.StdEncoding.EncodeToString(params.DescriptionHash) + } + + switch backend := params.Backend.(type) { + case LNDParams: + log.Debugf("[MakeInvoice] LND invoice at %s", backend.Host) + body, _ := sjson.Set("{}", "value_msat", params.Msatoshi) + + if params.DescriptionHash == nil { + body, _ = sjson.Set(body, "memo", params.Description) + } else { + body, _ = sjson.Set(body, "description_hash", b64h) + } + + req, err := http.NewRequest("POST", + backend.Host+"/v1/invoices", + bytes.NewBufferString(body), + ) + if err != nil { + return CheckInvoiceParams{}, err + } + + // macaroon must be hex, so if it is on base64 we adjust that + if b, err := base64.StdEncoding.DecodeString(backend.Macaroon); err == nil { + backend.Macaroon = hex.EncodeToString(b) + } + + req.Header.Set("Grpc-Metadata-macaroon", backend.Macaroon) + resp, err := Client.Do(req) + if err != nil { + return CheckInvoiceParams{}, err + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + body, _ := ioutil.ReadAll(resp.Body) + text := string(body) + if len(text) > 300 { + text = text[:300] + } + return CheckInvoiceParams{}, fmt.Errorf("call to lnd failed (%d): %s", resp.StatusCode, text) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return CheckInvoiceParams{}, err + } + + // bot.Cache.Set(shopView.ID, shopView, &store.Options{Expiration: 24 * time.Hour}) + checkInvoiceParams := CheckInvoiceParams{ + Backend: params.Backend, + PR: gjson.ParseBytes(b).Get("payment_request").String(), + Hash: []byte(gjson.ParseBytes(b).Get("r_hash").String()), + Status: "OPEN", + } + return checkInvoiceParams, nil + + case LNBitsParams: + log.Debugf("[MakeInvoice] LNBits invoice at %s", backend.Host) + body, _ := sjson.Set("{}", "amount", params.Msatoshi/1000) + body, _ = sjson.Set(body, "out", false) + + if params.DescriptionHash == nil { + if params.Description == "" { + body, _ = sjson.Set(body, "memo", "invoice") + } else { + body, _ = sjson.Set(body, "memo", params.Description) + } + } else { + body, _ = sjson.Set(body, "description_hash", hexh) + } + + req, err := http.NewRequest("POST", + backend.Host+"/api/v1/payments", + bytes.NewBufferString(body), + ) + if err != nil { + return CheckInvoiceParams{}, err + } + + req.Header.Set("X-Api-Key", backend.Key) + req.Header.Set("Content-Type", "application/json") + resp, err := Client.Do(req) + if err != nil { + return CheckInvoiceParams{}, err + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + body, _ := ioutil.ReadAll(resp.Body) + text := string(body) + if len(text) > 300 { + text = text[:300] + } + return CheckInvoiceParams{}, fmt.Errorf("call to lnbits failed (%d): %s", resp.StatusCode, text) + } + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return CheckInvoiceParams{}, err + } + checkInvoiceParams := CheckInvoiceParams{ + Backend: params.Backend, + PR: gjson.ParseBytes(b).Get("payment_request").String(), + Hash: []byte(gjson.ParseBytes(b).Get("payment_hash").String()), + Status: "OPEN", + } + return checkInvoiceParams, nil + default: + return CheckInvoiceParams{}, errors.New("wrong backend type") + } +} + +func CheckInvoice(params CheckInvoiceParams) (CheckInvoiceParams, error) { + defer func(prevTransport http.RoundTripper) { + Client.Transport = prevTransport + }(Client.Transport) + + if params.Backend == nil { + return CheckInvoiceParams{}, errors.New("no backend specified") + } + specialTransport := &http.Transport{} + + // use a cert or skip TLS verification? + if len(params.Backend.getCert()) > 0 { + caCertPool := x509.NewCertPool() + ok := caCertPool.AppendCertsFromPEM(params.Backend.getCert()) + if !ok { + return CheckInvoiceParams{}, fmt.Errorf("invalid root certificate") + } + specialTransport.TLSClientConfig = &tls.Config{RootCAs: caCertPool} + } else { + specialTransport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + } + + // use a proxy? + if !params.Backend.isLocal() && len(HttpProxyURL) > 0 { + proxyURL, _ := url.Parse(HttpProxyURL) + specialTransport.Proxy = http.ProxyURL(proxyURL) + } + + Client.Transport = specialTransport + + switch backend := params.Backend.(type) { + case LNDParams: + log.Debugf("[CheckInvoice] LND invoice %s at %s", base64.StdEncoding.EncodeToString(params.Hash), backend.Host) + p, err := base64.StdEncoding.DecodeString(string(params.Hash)) + if err != nil { + return CheckInvoiceParams{}, fmt.Errorf("invalid hash") + } + hexHash := hex.EncodeToString(p) + requestUrl, err := url.Parse(fmt.Sprintf("%s/v1/invoice/%s?r_hash=%s", backend.Host, hexHash, base64.StdEncoding.EncodeToString(params.Hash))) + if err != nil { + return CheckInvoiceParams{}, err + } + requestUrl.Scheme = "https" + req, err := http.NewRequest("GET", + requestUrl.String(), nil) + if err != nil { + return CheckInvoiceParams{}, err + } + // macaroon must be hex, so if it is on base64 we adjust that + if b, err := base64.StdEncoding.DecodeString(backend.Macaroon); err == nil { + backend.Macaroon = hex.EncodeToString(b) + } + + req.Header.Set("Grpc-Metadata-macaroon", backend.Macaroon) + resp, err := Client.Do(req) + if err != nil { + return CheckInvoiceParams{}, err + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + body, _ := ioutil.ReadAll(resp.Body) + text := string(body) + if len(text) > 300 { + text = text[:300] + } + return CheckInvoiceParams{}, fmt.Errorf("call to lnd failed (%d): %s", resp.StatusCode, text) + } + + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return CheckInvoiceParams{}, err + } + params.Status = gjson.ParseBytes(b).Get("state").String() + return params, nil + + case LNBitsParams: + log.Debugf("[CheckInvoice] LNBits invoice %s at %s", base64.StdEncoding.EncodeToString(params.Hash), backend.Host) + log.Debug("Getting ", backend.Host+"/api/v1/payments/"+string(params.Hash)) + req, err := http.NewRequest("GET", backend.Host+"/api/v1/payments/"+string(params.Hash), nil) + if err != nil { + return CheckInvoiceParams{}, err + } + + req.Header.Set("X-Api-Key", backend.Key) + req.Header.Set("Content-Type", "application/json") + resp, err := Client.Do(req) + if err != nil { + return CheckInvoiceParams{}, err + } + defer resp.Body.Close() + if resp.StatusCode >= 300 { + body, _ := ioutil.ReadAll(resp.Body) + text := string(body) + if len(text) > 300 { + text = text[:300] + } + return CheckInvoiceParams{}, fmt.Errorf("call to lnbits failed (%d): %s", resp.StatusCode, text) + } + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return CheckInvoiceParams{}, err + } + status := strings.ToLower(gjson.ParseBytes(b).Get("paid").String()) + if status == "true" { + params.Status = "SETTLED" + } else { + params.Status = "OPEN" + } + return params, nil + default: + return CheckInvoiceParams{}, errors.New("missing backend params") + } + return CheckInvoiceParams{}, errors.New("missing backend params") +} diff --git a/internal/telegram/bot.go b/internal/telegram/bot.go index e324d612..a154c53e 100644 --- a/internal/telegram/bot.go +++ b/internal/telegram/bot.go @@ -20,15 +20,12 @@ import ( gocache "github.com/patrickmn/go-cache" log "github.com/sirupsen/logrus" tb "gopkg.in/lightningtipbot/telebot.v3" - "gorm.io/gorm" ) type TipBot struct { - Database *gorm.DB + DB *Databases Bunt *storage.DB ShopBunt *storage.DB - GroupsDb *gorm.DB - logger *gorm.DB Telegram *tb.Bot Client *lnbits.Client limiter map[string]limiter.Limiter @@ -48,17 +45,15 @@ func NewBot() TipBot { gocacheClient := gocache.New(5*time.Minute, 10*time.Minute) gocacheStore := store.NewGoCache(gocacheClient, nil) // create sqlite databases - db, txLogger, groupsDb := AutoMigration() + dbs := AutoMigration() limiter.Start() return TipBot{ - Database: db, + DB: dbs, Client: lnbits.NewClient(internal.Configuration.Lnbits.AdminKey, internal.Configuration.Lnbits.Url), - logger: txLogger, Bunt: createBunt(internal.Configuration.Database.BuntDbPath), ShopBunt: createBunt(internal.Configuration.Database.ShopBuntDbPath), Telegram: newTelegramBot(), Cache: Cache{GoCacheStore: gocacheStore}, - GroupsDb: groupsDb, } } diff --git a/internal/telegram/buttons.go b/internal/telegram/buttons.go index c842d3e4..78c314e1 100644 --- a/internal/telegram/buttons.go +++ b/internal/telegram/buttons.go @@ -32,8 +32,8 @@ var ( func init() { mainMenu.Reply( - mainMenu.Row(btnBalanceMainMenu, btnHelpMainMenu), - mainMenu.Row(btnInvoiceMainMenu, btnSendMainMenu), + mainMenu.Row(btnBalanceMainMenu), + mainMenu.Row(btnInvoiceMainMenu, btnSendMainMenu, btnHelpMainMenu), ) } @@ -92,7 +92,7 @@ func (bot *TipBot) makeContactsButtons(ctx context.Context) []tb.Btn { user := LoadUser(ctx) // get 5 most recent transactions by from_id with distint to_user // where to_user starts with an @ and is not the user itself - bot.logger.Where("from_id = ? AND to_user LIKE ? AND to_user <> ?", user.Telegram.ID, "@%", GetUserStr(user.Telegram)).Distinct("to_user").Order("id desc").Limit(5).Find(&records) + bot.DB.Transactions.Where("from_id = ? AND to_user LIKE ? AND to_user <> ?", user.Telegram.ID, "@%", GetUserStr(user.Telegram)).Distinct("to_user").Order("id desc").Limit(5).Find(&records) log.Debugf("[makeContactsButtons] found %d records", len(records)) // get all contacts and add them to the buttons diff --git a/internal/telegram/database.go b/internal/telegram/database.go index 4d78619e..0b335710 100644 --- a/internal/telegram/database.go +++ b/internal/telegram/database.go @@ -28,6 +28,12 @@ import ( "gorm.io/gorm" ) +type Databases struct { + Users *gorm.DB + Transactions *gorm.DB + Groups *gorm.DB +} + const ( MessageOrderedByReplyToFrom = "message.reply_to_message.from.id" TipTooltipKeyPattern = "tip-tool-tip:*" @@ -86,7 +92,7 @@ func ColumnMigrationTasks(db *gorm.DB) error { return err } -func AutoMigration() (db *gorm.DB, txLogger *gorm.DB, groupsDb *gorm.DB) { +func AutoMigration() *Databases { orm, err := gorm.Open(sqlite.Open(internal.Configuration.Database.DbPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) if err != nil { panic("Initialize orm failed.") @@ -100,7 +106,7 @@ func AutoMigration() (db *gorm.DB, txLogger *gorm.DB, groupsDb *gorm.DB) { panic(err) } - txLogger, err = gorm.Open(sqlite.Open(internal.Configuration.Database.TransactionsPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) + txLogger, err := gorm.Open(sqlite.Open(internal.Configuration.Database.TransactionsPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) if err != nil { panic("Initialize orm failed.") } @@ -109,7 +115,7 @@ func AutoMigration() (db *gorm.DB, txLogger *gorm.DB, groupsDb *gorm.DB) { panic(err) } - groupsDb, err = gorm.Open(sqlite.Open(internal.Configuration.Database.GroupsDbPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) + groupsDb, err := gorm.Open(sqlite.Open(internal.Configuration.Database.GroupsDbPath), &gorm.Config{DisableForeignKeyConstraintWhenMigrating: true, FullSaveAssociations: true}) if err != nil { panic("Initialize orm failed.") } @@ -117,7 +123,12 @@ func AutoMigration() (db *gorm.DB, txLogger *gorm.DB, groupsDb *gorm.DB) { if err != nil { panic(err) } - return orm, txLogger, groupsDb + + return &Databases{ + Users: orm, + Transactions: txLogger, + Groups: groupsDb, + } } func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.User, error) { @@ -126,7 +137,7 @@ func GetUserByTelegramUsername(toUserStrWithoutAt string, bot TipBot) (*lnbits.U if len(toUserStrWithoutAt) > 100 { return nil, fmt.Errorf("[GetUserByTelegramUsername] Telegram username is too long: %s..", toUserStrWithoutAt[:100]) } - tx := bot.Database.Where("telegram_username = ? COLLATE NOCASE", toUserStrWithoutAt).First(toUserDb) + tx := bot.DB.Users.Where("telegram_username = ? COLLATE NOCASE", toUserStrWithoutAt).First(toUserDb) if tx.Error != nil || toUserDb.Wallet == nil { err := tx.Error if toUserDb.Wallet == nil { @@ -153,7 +164,7 @@ func getCachedUser(u *tb.User, bot TipBot) (*lnbits.User, error) { // without updating the user in storage. func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { user := &lnbits.User{Name: strconv.FormatInt(u.ID, 10)} - tx := bot.Database.First(user) + tx := bot.DB.Users.First(user) if tx.Error != nil { errmsg := fmt.Sprintf("[GetUser] Couldn't fetch %s from Database: %s", GetUserStr(u), tx.Error.Error()) log.Warnln(errmsg) @@ -164,6 +175,21 @@ func GetLnbitsUser(u *tb.User, bot TipBot) (*lnbits.User, error) { return user, nil } +func GetLnbitsUserWithSettings(u *tb.User, bot TipBot) (*lnbits.User, error) { + user := &lnbits.User{Name: strconv.FormatInt(u.ID, 10)} + tx := bot.DB.Users.Preload("Settings").First(user) + if tx.Error != nil { + errmsg := fmt.Sprintf("[GetLnbitsUserWithSettings] Couldn't fetch %s from Database: %s", GetUserStr(u), tx.Error.Error()) + log.Warnln(errmsg) + user.Telegram = u + return user, tx.Error + } + if user.Settings == nil { + user.Settings = &lnbits.Settings{ID: user.ID} + } + return user, nil +} + // GetUser from Telegram user. Update the user if user information changed. func GetUser(u *tb.User, bot TipBot) (*lnbits.User, error) { var user *lnbits.User @@ -245,7 +271,7 @@ func UpdateUserRecord(user *lnbits.User, bot TipBot) error { log.Errorf("[UpdateUserRecord] UUID empty! Setting to: %s", user.UUID) } - tx := bot.Database.Save(user) + tx := bot.DB.Users.Save(user) if tx.Error != nil { errmsg := fmt.Sprintf("[UpdateUserRecord] Error: Couldn't update %s's info in Database.", GetUserStr(user.Telegram)) log.Errorln(errmsg) diff --git a/internal/telegram/groups.go b/internal/telegram/groups.go index 72619342..174e0a24 100644 --- a/internal/telegram/groups.go +++ b/internal/telegram/groups.go @@ -139,7 +139,7 @@ func (bot TipBot) groupRequestJoinHandler(ctx intercept.Context) (intercept.Cont groupName := strings.ToLower(splits[splitIdx+1]) group := &Group{} - tx := bot.GroupsDb.Where("name = ? COLLATE NOCASE", groupName).First(group) + tx := bot.DB.Groups.Where("name = ? COLLATE NOCASE", groupName).First(group) if tx.Error != nil { bot.trySendMessage(ctx.Message().Chat, Translate(ctx, "groupNotFoundMessage")) return ctx, fmt.Errorf("group not found") @@ -401,7 +401,7 @@ func (bot TipBot) addGroupHandler(ctx intercept.Context) (intercept.Context, err // check if the group with this name is already in db // only if a group with this name is owned by this user, it can be overwritten group := &Group{} - tx := bot.GroupsDb.Where("name = ? COLLATE NOCASE", groupName).First(group) + tx := bot.DB.Groups.Where("name = ? COLLATE NOCASE", groupName).First(group) if tx.Error == nil { // if it is already added, check if this user is the admin if user.Telegram.ID != group.Owner.ID || group.ID != m.Chat.ID { @@ -437,7 +437,7 @@ func (bot TipBot) addGroupHandler(ctx intercept.Context) (intercept.Context, err Ticket: ticket, } - bot.GroupsDb.Save(group) + bot.DB.Groups.Save(group) log.Infof("[group] Ticket of %d sat added to group %s.", group.Ticket.Price, group.Name) bot.trySendMessage(m.Chat, fmt.Sprintf(Translate(ctx, "groupAddedMessage"), str.MarkdownEscape(m.Chat.Title), group.Name, group.Ticket.Price, GetUserStrMd(bot.Telegram.Me), group.Name)) diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index 9a0a3e23..f14a3f59 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -148,6 +148,37 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, + { + Endpoints: []interface{}{"/node"}, + Handler: bot.nodeHandler, + Interceptor: &Interceptor{ + Before: []intercept.Func{ + bot.requirePrivateChatInterceptor, + bot.localizerInterceptor, + bot.logMessageInterceptor, + bot.requireUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, + { + Endpoints: []interface{}{&btnSatdressCheckInvoice}, + Handler: bot.satdressCheckInvoiceHandler, + Interceptor: &Interceptor{ + + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.requireUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, { Endpoints: []interface{}{"/shops"}, Handler: bot.shopsHandler, diff --git a/internal/telegram/invoice.go b/internal/telegram/invoice.go index 77a79bd3..95758983 100644 --- a/internal/telegram/invoice.go +++ b/internal/telegram/invoice.go @@ -4,10 +4,11 @@ import ( "bytes" "context" "fmt" - "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" "strings" "time" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/errors" "github.com/LightningTipBot/LightningTipBot/internal/storage" @@ -38,6 +39,7 @@ func initInvoiceEventCallbacks(bot *TipBot) { InvoiceCallbackInlineReceive: EventHandler{Function: bot.inlineReceiveEvent, Type: EventTypeInvoice}, InvoiceCallbackLNURLPayReceive: EventHandler{Function: bot.lnurlReceiveEvent, Type: EventTypeInvoice}, InvoiceCallbackGroupTicket: EventHandler{Function: bot.groupGetInviteLinkHandler, Type: EventTypeInvoice}, + InvoiceCallbackSatdressProxy: EventHandler{Function: bot.satdressProxyRelayPaymentHandler, Type: EventTypeInvoice}, } } @@ -48,6 +50,7 @@ const ( InvoiceCallbackInlineReceive InvoiceCallbackLNURLPayReceive InvoiceCallbackGroupTicket + InvoiceCallbackSatdressProxy ) const ( diff --git a/internal/telegram/satdress.go b/internal/telegram/satdress.go new file mode 100644 index 00000000..4a72f9e7 --- /dev/null +++ b/internal/telegram/satdress.go @@ -0,0 +1,527 @@ +package telegram + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "fmt" + "strings" + "time" + + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + tb "gopkg.in/lightningtipbot/telebot.v3" + + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/satdress" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/eko/gocache/store" + log "github.com/sirupsen/logrus" + "github.com/skip2/go-qrcode" +) + +var ( + registerNodeMessage = "📖 You did not register a node yet.\n\nCurrently supported backends: `lnd` and `lnbits`\nTo register a node, type: `/node add `\n*LND:* `/node add lnd `\n*LNbits:* `/node add lnbits `\n\nFor security reasons, you should *only use an invoice macaroon* for LND and an *invoice key* for LNbits." + checkingInvoiceMessage = "⏳ Checking invoice on your node..." + invoiceNotSettledMessage = "❌ Invoice has not settled yet." + checkInvoiceButtonMessage = "🔄 Check invoice" + routingInvoiceMessage = "🔄 Getting invoice from your node..." + checkingNodeMessage = "🔄 Checking your node..." + errorCouldNotAddNodeMessage = "❌ Could not add node. Please check your node details." + gettingInvoiceOnlyErrorMessage = "❌ Error getting invoice from your node." + gettingInvoiceErrorMessage = "❌ Error getting invoice from your node. Your funds are still available." + payingInvoiceErrorMessage = "❌ Could not route payment. Your funds are still available." + invoiceRoutedMessage = "✅ *Payment routed to your node.*" + invoiceSettledMessage = "✅ *Invoice settled.*" + nodeAddedMessage = "✅ *Node added.*" + satdressCheckInvoicenMenu = &tb.ReplyMarkup{ResizeKeyboard: true} + btnSatdressCheckInvoice = satdressCheckInvoicenMenu.Data(checkInvoiceButtonMessage, "satdress_check_invoice") +) + +// todo -- rename to something better like parse node settings or something +func parseUserSettingInput(ctx intercept.Context, m *tb.Message) (satdress.BackendParams, error) { + // input is "/node add " + params := satdress.LNDParams{} + splits := strings.Split(m.Text, " ") + splitlen := len(splits) + if splitlen < 4 { + return params, fmt.Errorf("not enough arguments") + } + if strings.ToLower(splits[2]) == "lnd" { + if splitlen < 6 || splitlen > 7 { + return params, fmt.Errorf("wrong format. Use ") + } + host := splits[3] + macaroon := splits[4] + cert := splits[5] + + hostsplit := strings.Split(host, ".") + if len(hostsplit) == 0 { + return params, fmt.Errorf("host has wrong format") + } + pem := parseCertificateToPem(cert) + if len(pem) < 1 { + return params, fmt.Errorf("certificate has invalid format") + } + return satdress.LNDParams{ + Cert: pem, + Host: host, + Macaroon: macaroon, + CertString: string(pem), + }, nil + } else if strings.ToLower(splits[2]) == "lnbits" { + if splitlen < 5 || splitlen > 6 { + return params, fmt.Errorf("wrong format. Use ") + } + host := splits[3] + key := splits[4] + + host = strings.TrimSuffix(host, "/") + hostsplit := strings.Split(host, ".") + if len(hostsplit) == 0 { + return params, fmt.Errorf("host has wrong format") + } + return satdress.LNBitsParams{ + Host: host, + Key: key, + }, nil + } + return params, fmt.Errorf("unknown backend type. Supported types: `lnd`, `lnbits`") +} + +func nodeInfoString(node *lnbits.NodeSettings) (string, error) { + if len(node.NodeType) == 0 { + return "", fmt.Errorf("node type is empty") + } + var node_info_str_filled string + var node_info_str string + switch strings.ToLower(node.NodeType) { + case "lnd": + node_info_str = "*Type:* `%s`\n\n*Host:*\n\n`%s`\n\n*Macaroon:*\n\n`%s`\n\n*Cert:*\n\n`%s`" + node_info_str_filled = fmt.Sprintf(node_info_str, node.NodeType, node.LNDParams.Host, node.LNDParams.Macaroon, node.LNDParams.CertString) + case "lnbits": + node_info_str = "*Type:* `%s`\n\n*Host:*\n\n`%s`\n\n*Key:*\n\n`%s`" + node_info_str_filled = fmt.Sprintf(node_info_str, node.NodeType, node.LNbitsParams.Host, node.LNbitsParams.Key) + default: + return "", fmt.Errorf("unknown node type") + } + return fmt.Sprintf("ℹ️ *Your node information.*\n\n%s", node_info_str_filled), nil +} + +func (bot *TipBot) getNodeHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() + user, err := GetLnbitsUserWithSettings(m.Sender, *bot) + if err != nil { + log.Infof("Could not get user settings for user %s", GetUserStr(user.Telegram)) + return ctx, err + } + + if user.Settings == nil { + bot.trySendMessage(m.Sender, registerNodeMessage) + return ctx, fmt.Errorf("no node registered") + } + + node_info_str, err := nodeInfoString(&user.Settings.Node) + if err != nil { + log.Infof("Could not get node info for user %s", GetUserStr(user.Telegram)) + bot.trySendMessage(m.Sender, registerNodeMessage) + return ctx, err + } + bot.trySendMessage(m.Sender, node_info_str) + + return ctx, nil +} + +func (bot *TipBot) nodeHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() + splits := strings.Split(m.Text, " ") + if len(splits) == 1 { + return bot.getNodeHandler(ctx) + } else if len(splits) > 1 { + if splits[1] == "invoice" { + return bot.invHandler(ctx) + } + if splits[1] == "add" { + return bot.registerNodeHandler(ctx) + } + if splits[1] == "check" { + return bot.satdressCheckInvoiceHandler(ctx) + } + if splits[1] == "proxy" { + return bot.satdressProxyHandler(ctx) + } + } + return ctx, nil +} + +func (bot *TipBot) registerNodeHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() + user, err := GetLnbitsUserWithSettings(m.Sender, *bot) + if err != nil { + return ctx, err + } + check_message := bot.trySendMessageEditable(user.Telegram, checkingNodeMessage) + + backendParams, err := parseUserSettingInput(ctx, m) + if err != nil { + bot.tryEditMessage(check_message, fmt.Sprintf(Translate(ctx, "errorReasonMessage"), err.Error())) + return ctx, err + } + + switch backend := backendParams.(type) { + case satdress.LNDParams: + // get test invoice from user's node + getInvoiceParams, err := satdress.MakeInvoice( + satdress.Params{ + Backend: backend, + Msatoshi: 1000, + Description: "Test invoice", + }, + ) + if err != nil { + log.Errorf("[registerNodeHandler] Could not add user %s's %s node: %s", GetUserStr(user.Telegram), getInvoiceParams.Status, err.Error()) + bot.tryEditMessage(check_message, errorCouldNotAddNodeMessage) + return ctx, err + } + + // save node in db + user.Settings.Node.LNDParams = &backend + user.Settings.Node.NodeType = "lnd" + case satdress.LNBitsParams: + // get test invoice from user's node + getInvoiceParams, err := satdress.MakeInvoice( + satdress.Params{ + Backend: backend, + Msatoshi: 1000, + Description: "Test invoice", + }, + ) + if err != nil { + log.Errorf("[registerNodeHandler] Could not add user %s's %s node: %s", GetUserStr(user.Telegram), getInvoiceParams.Status, err.Error()) + bot.tryEditMessage(check_message, errorCouldNotAddNodeMessage) + return ctx, err + } + // save node in db + user.Settings.Node.LNbitsParams = &backend + user.Settings.Node.NodeType = "lnbits" + + } + err = UpdateUserRecord(user, *bot) + if err != nil { + log.Errorf("[registerNodeHandler] could not update record of user %s: %v", GetUserStr(user.Telegram), err) + return ctx, err + } + node_info_str, err := nodeInfoString(&user.Settings.Node) + if err != nil { + log.Infof("Could not get node info for user %s", GetUserStr(user.Telegram)) + bot.trySendMessage(m.Sender, registerNodeMessage) + return ctx, err + } + bot.tryEditMessage(check_message, fmt.Sprintf("%s\n\n%s", node_info_str, nodeAddedMessage)) + return ctx, nil +} + +func (bot *TipBot) invHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() + user, err := GetLnbitsUserWithSettings(m.Sender, *bot) + if err != nil { + return ctx, err + } + if user.Settings == nil || user.Settings.Node.NodeType == "" { + bot.trySendMessage(m.Sender, "You did not register a node yet.") + return ctx, fmt.Errorf("node of user %s not registered", GetUserStr(user.Telegram)) + } + + var amount int64 + if amount_str, err := getArgumentFromCommand(m.Text, 2); err == nil { + amount, err = getAmount(amount_str) + if err != nil { + return ctx, err + } + } + + check_message := bot.trySendMessageEditable(user.Telegram, routingInvoiceMessage) + var getInvoiceParams satdress.CheckInvoiceParams + + if user.Settings.Node.NodeType == "lnd" { + // get invoice from user's node + getInvoiceParams, err = satdress.MakeInvoice( + satdress.Params{ + Backend: satdress.LNDParams{ + Cert: []byte(user.Settings.Node.LNDParams.CertString), + Host: user.Settings.Node.LNDParams.Host, + Macaroon: user.Settings.Node.LNDParams.Macaroon, + }, + Msatoshi: amount * 1000, + Description: fmt.Sprintf("Invoice by %s", GetUserStr(bot.Telegram.Me)), + }, + ) + } else if user.Settings.Node.NodeType == "lnbits" { + // get invoice from user's node + getInvoiceParams, err = satdress.MakeInvoice( + satdress.Params{ + Backend: satdress.LNBitsParams{ + Key: user.Settings.Node.LNbitsParams.Key, + Host: user.Settings.Node.LNbitsParams.Host, + }, + Msatoshi: amount * 1000, + Description: fmt.Sprintf("Invoice by %s", GetUserStr(bot.Telegram.Me)), + }, + ) + } + if err != nil { + log.Errorln(err.Error()) + bot.tryEditMessage(check_message, gettingInvoiceOnlyErrorMessage) + return ctx, err + } + + // bot.trySendMessage(m.Sender, fmt.Sprintf("PR: `%s`\n\nHash: `%s`\n\nStatus: `%s`", getInvoiceParams.PR, string(getInvoiceParams.Hash), getInvoiceParams.Status)) + + // create qr code + qr, err := qrcode.Encode(getInvoiceParams.PR, qrcode.Medium, 256) + if err != nil { + errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err.Error()) + bot.trySendMessage(user.Telegram, Translate(ctx, "errorTryLaterMessage")) + log.Errorln(errmsg) + return ctx, err + } + bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", getInvoiceParams.PR)}) + + // add the getInvoiceParams to cache to check it later + bot.Cache.Set(fmt.Sprintf("invoice:%d", user.Telegram.ID), getInvoiceParams, &store.Options{Expiration: 24 * time.Hour}) + + // check if invoice settles + return bot.satdressCheckInvoiceHandler(ctx) +} + +func (bot *TipBot) satdressCheckInvoiceHandler(ctx intercept.Context) (intercept.Context, error) { + tgUser := LoadUser(ctx).Telegram + user, err := GetLnbitsUserWithSettings(tgUser, *bot) + if err != nil { + return ctx, err + } + + // get the getInvoiceParams from cache + log.Debugf("[Cache] Getting key: %s", fmt.Sprintf("invoice:%d", user.Telegram.ID)) + getInvoiceParamsInterface, err := bot.Cache.Get(fmt.Sprintf("invoice:%d", user.Telegram.ID)) + if err != nil { + log.Errorf("[satdressCheckInvoiceHandler] UserID: %d, %s", user.Telegram.ID, err.Error()) + return ctx, err + } + getInvoiceParams := getInvoiceParamsInterface.(satdress.CheckInvoiceParams) + + // check the invoice + + // check if there is an invoice check message in cache already + check_message_interface, err := bot.Cache.Get(fmt.Sprintf("invoice:msg:%s", getInvoiceParams.Hash)) + var check_message *tb.Message + if err != nil { + // send a new message if there isn't one in the cache + check_message = bot.trySendMessageEditable(tgUser, checkingInvoiceMessage) + } else { + check_message = check_message_interface.(*tb.Message) + check_message, err = bot.tryEditMessage(check_message, checkingInvoiceMessage) + if err != nil { + log.Errorf("[satdressCheckInvoiceHandler] UserID: %d, %s", user.Telegram.ID, err.Error()) + } + } + + // save it in the cache for another call later + bot.Cache.Set(fmt.Sprintf("invoice:msg:%s", getInvoiceParams.Hash), check_message, &store.Options{Expiration: 24 * time.Hour}) + + deadLineCtx, cancel := context.WithDeadline(ctx, time.Now().Add(time.Second*30)) + runtime.NewRetryTicker(deadLineCtx, "node_invoice_check", runtime.WithRetryDuration(5*time.Second)).Do(func() { + // get invoice from user's node + log.Debugf("[satdressCheckInvoiceHandler] Checking invoice: %s", getInvoiceParams.Hash) + getInvoiceParams, err = satdress.CheckInvoice(getInvoiceParams) + if err != nil { + log.Errorln(err.Error()) + return + } + if getInvoiceParams.Status == "SETTLED" { + log.Debugf("[satdressCheckInvoiceHandler] Invoice settled: %s", getInvoiceParams.Hash) + bot.tryEditMessage(check_message, invoiceSettledMessage) + cancel() + } + + }, func() { + // cancel + }, + func() { + // deadline + log.Debugf("[satdressCheckInvoiceHandler] Invoice check expired: %s", getInvoiceParams.Hash) + bot.tryEditMessage(check_message, invoiceNotSettledMessage, + &tb.ReplyMarkup{ + InlineKeyboard: [][]tb.InlineButton{ + {tb.InlineButton{Text: checkInvoiceButtonMessage, Unique: "satdress_check_invoice"}}, + }, + }) + }, + ) + + return ctx, nil +} + +func parseCertificateToPem(cert string) []byte { + block, _ := pem.Decode([]byte(cert)) + if block != nil { + // already PEM + return []byte(cert) + } else { + var dec []byte + + dec, err := hex.DecodeString(cert) + if err != nil { + // not HEX + dec, err = base64.StdEncoding.DecodeString(cert) + if err != nil { + // not base54, we have a problem huston + return nil + } + } + if block, _ := pem.Decode(dec); block != nil { + return dec + } + // decoding went wrong + return nil + } +} + +func (bot *TipBot) satdressProxyHandler(ctx intercept.Context) (intercept.Context, error) { + m := ctx.Message() + user, err := GetLnbitsUserWithSettings(m.Sender, *bot) + if err != nil { + return ctx, err + } + if user.Settings == nil || user.Settings.Node.LNDParams == nil { + bot.trySendMessage(user.Telegram, "You did not register a node yet.") + log.Errorf("node of user %s not registered", GetUserStr(user.Telegram)) + return ctx, fmt.Errorf("no node settings.") + } + + var amount int64 + if amount_str, err := getArgumentFromCommand(m.Text, 2); err == nil { + amount, err = getAmount(amount_str) + if err != nil { + return ctx, err + } + } + + memo := "🔀 Payment proxy in." + invoice, err := bot.createInvoiceWithEvent(ctx, user, amount, memo, InvoiceCallbackSatdressProxy, "") + if err != nil { + errmsg := fmt.Sprintf("[/invoice] Could not create an invoice: %s", err.Error()) + bot.trySendMessage(user.Telegram, Translate(ctx, "errorTryLaterMessage")) + log.Errorln(errmsg) + return ctx, err + } + + // create qr code + qr, err := qrcode.Encode(invoice.PaymentRequest, qrcode.Medium, 256) + if err != nil { + errmsg := fmt.Sprintf("[/invoice] Failed to create QR code for invoice: %s", err.Error()) + bot.trySendMessage(user.Telegram, Translate(ctx, "errorTryLaterMessage")) + log.Errorln(errmsg) + return ctx, err + } + bot.trySendMessage(m.Sender, &tb.Photo{File: tb.File{FileReader: bytes.NewReader(qr)}, Caption: fmt.Sprintf("`%s`", invoice.PaymentRequest)}) + return ctx, nil +} + +func (bot *TipBot) satdressProxyRelayPaymentHandler(event Event) { + invoiceEvent := event.(*InvoiceEvent) + user := invoiceEvent.User + if user.Settings == nil || user.Settings.Node.LNDParams == nil { + bot.trySendMessage(user.Telegram, "You did not register a node yet.") + log.Errorf("node of user %s not registered", GetUserStr(user.Telegram)) + return + } + + bot.notifyInvoiceReceivedEvent(invoiceEvent) + + // now relay the payment to the user's node + var amount int64 = invoiceEvent.Amount + + check_message := bot.trySendMessageEditable(user.Telegram, routingInvoiceMessage) + var getInvoiceParams satdress.CheckInvoiceParams + var err error + if user.Settings.Node.NodeType == "lnd" { + // get invoice from user's node + getInvoiceParams, err = satdress.MakeInvoice( + satdress.Params{ + Backend: satdress.LNDParams{ + Cert: []byte(user.Settings.Node.LNDParams.CertString), + Host: user.Settings.Node.LNDParams.Host, + Macaroon: user.Settings.Node.LNDParams.Macaroon, + }, + Msatoshi: amount * 1000, + Description: fmt.Sprintf("🔀 Payment proxy out from %s.", GetUserStr(bot.Telegram.Me)), + }, + ) + } else if user.Settings.Node.NodeType == "lnbits" { + // get invoice from user's node + getInvoiceParams, err = satdress.MakeInvoice( + satdress.Params{ + Backend: satdress.LNBitsParams{ + Key: user.Settings.Node.LNbitsParams.Key, + Host: user.Settings.Node.LNbitsParams.Host, + }, + Msatoshi: amount * 1000, + Description: fmt.Sprintf("🔀 Payment proxy out from %s.", GetUserStr(bot.Telegram.Me)), + }, + ) + } + if err != nil { + log.Errorln(err.Error()) + bot.tryEditMessage(check_message, gettingInvoiceErrorMessage) + return + } + + // bot.trySendMessage(user.Telegram, fmt.Sprintf("PR: `%s`\n\nHash: `%s`\n\nStatus: `%s`", getInvoiceParams.PR, string(getInvoiceParams.Hash), getInvoiceParams.Status)) + + // pay invoice + invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: getInvoiceParams.PR}, bot.Client) + if err != nil { + errmsg := fmt.Sprintf("[/pay] Could not pay invoice of %s: %s", GetUserStr(user.Telegram), err) + // err = fmt.Errorf(i18n.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) + // bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), err.Error()), &tb.ReplyMarkup{}) + // verbose error message, turned off for now + // if len(err.Error()) == 0 { + // err = fmt.Errorf(i18n.Translate(payData.LanguageCode, "invoiceUndefinedErrorMessage")) + // } + // bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(payData.LanguageCode, "invoicePaymentFailedMessage"), str.MarkdownEscape(err.Error())), &tb.ReplyMarkup{}) + log.Errorln(errmsg) + bot.tryEditMessage(check_message, payingInvoiceErrorMessage) + return + } + + // object that holds all information about the send payment + id := fmt.Sprintf("proxypay:%d:%d:%s", user.Telegram.ID, amount, RandStringRunes(8)) + + payData := &PayData{ + Base: storage.New(storage.ID(id)), + From: user, + Invoice: invoice.PaymentRequest, + Hash: invoice.PaymentHash, + Amount: amount, + } + // add result to persistent struct + runtime.IgnoreError(payData.Set(payData, bot.Bunt)) + + // add the getInvoiceParams to cache to check it later + bot.Cache.Set(fmt.Sprintf("invoice:%d", user.Telegram.ID), getInvoiceParams, &store.Options{Expiration: 24 * time.Hour}) + + time.Sleep(time.Second) + + getInvoiceParams, err = satdress.CheckInvoice(getInvoiceParams) + if err != nil { + log.Errorln(err.Error()) + return + } + bot.tryEditMessage(check_message, invoiceRoutedMessage) + // bot.trySendMessage(user.Telegram, fmt.Sprintf("PR: `%s`\n\nHash: `%s`\n\nStatus: `%s`", getInvoiceParams.PR, string(getInvoiceParams.Hash), getInvoiceParams.Status)) + + return +} diff --git a/internal/telegram/transaction.go b/internal/telegram/transaction.go index 1756fa5a..fe2d0322 100644 --- a/internal/telegram/transaction.go +++ b/internal/telegram/transaction.go @@ -76,7 +76,7 @@ func (t *Transaction) Send() (success bool, err error) { } // save transaction to db - tx := t.Bot.logger.Save(t) + tx := t.Bot.DB.Transactions.Save(t) if tx.Error != nil { errMsg := fmt.Sprintf("Error: Could not log transaction: %s", err.Error()) log.Errorln(errMsg) From 410f56aaf59c7fbceb906b55a8de6a829f110f1f Mon Sep 17 00:00:00 2001 From: LightningTipBot <88730856+LightningTipBot@users.noreply.github.com> Date: Fri, 29 Apr 2022 22:05:28 +0200 Subject: [PATCH 240/541] Userpage (#337) * userpage init * page * files Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com> --- go.mod | 5 +- go.sum | 11 +++ internal/api/server.go | 3 +- internal/api/userpage/static/userpage.html | 56 ++++++++++++++ internal/api/userpage/userpage.go | 87 ++++++++++++++++++++++ internal/telegram/lnurl.go | 9 ++- main.go | 6 +- 7 files changed, 170 insertions(+), 7 deletions(-) create mode 100644 internal/api/userpage/static/userpage.html create mode 100644 internal/api/userpage/userpage.go diff --git a/go.mod b/go.mod index 1f5016ed..7f3ef820 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ module github.com/LightningTipBot/LightningTipBot -go 1.15 +go 1.17 require ( github.com/BurntSushi/toml v0.3.1 + github.com/PuerkitoBio/goquery v1.8.0 github.com/btcsuite/btcd v0.20.1-beta.0.20200515232429-9f0179fd2c46 github.com/eko/gocache v1.2.0 github.com/fiatjaf/go-lnurl v1.8.4 @@ -22,7 +23,7 @@ require ( github.com/tidwall/buntdb v1.2.7 github.com/tidwall/gjson v1.12.1 github.com/tidwall/sjson v1.2.4 - golang.org/x/text v0.3.5 + golang.org/x/text v0.3.6 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 gopkg.in/lightningtipbot/telebot.v3 v3.0.0-20220326213923-f323bb71ac8e gorm.io/driver/sqlite v1.1.4 diff --git a/go.sum b/go.sum index e8bb4dca..ebbd5760 100644 --- a/go.sum +++ b/go.sum @@ -9,6 +9,8 @@ github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e/go.mod h1:Bd github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc= github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= @@ -28,6 +30,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/allegro/bigcache/v2 v2.2.5 h1:mRc8r6GQjuJsmSKQNPsR5jQVXc8IJ1xsW5YXUYMLfqI= github.com/allegro/bigcache/v2 v2.2.5/go.mod h1:FppZsIO+IZk7gCuj5FiIDHGygD9xvWQcqg1uIPMb6tY= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= @@ -623,6 +627,8 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -663,14 +669,17 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 h1:XfKQ4OlFl8okEOr5UvAqFRVj8pY/4yfcXrddB8qAbU0= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2 h1:z99zHgr7hKfrUcX/KsoJk5FJfjTceCKIp96+biqP4To= @@ -679,6 +688,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2 h1:+DCIGbF/swA92ohVg0//6X2IVY3KZs6p9mix0ziNYJM= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= diff --git a/internal/api/server.go b/internal/api/server.go index f9a38ab9..da35696e 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -5,7 +5,6 @@ import ( "net/http" "time" - "github.com/LightningTipBot/LightningTipBot/internal" "github.com/gorilla/mux" log "github.com/sirupsen/logrus" ) @@ -33,7 +32,7 @@ func NewServer(address string) *Server { apiServer.router = mux.NewRouter() apiServer.httpServer.Handler = apiServer.router go apiServer.httpServer.ListenAndServe() - log.Infof("[LNURL] Server started at %s", internal.Configuration.Bot.LNURLServerUrl.Host) + log.Infof("[API] Server started at %s", address) return apiServer } diff --git a/internal/api/userpage/static/userpage.html b/internal/api/userpage/static/userpage.html new file mode 100644 index 00000000..88c07670 --- /dev/null +++ b/internal/api/userpage/static/userpage.html @@ -0,0 +1,56 @@ + + +{{define "userpage"}} + + + +{{.Username}}@ln.tips + + + + +