diff --git a/go.mod b/go.mod index 2666e8e..48c6a4e 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 c06a9c1..d4eeb19 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 0445a22..2f2be68 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 96fef6e..1e33d2f 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 0000000..6747a0c --- /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 b2a8954..1afe577 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 b38dbbf..bd45249 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 203a02f..f018d88 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 17759d5..61c2b97 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 bcb0ef4..ca6f861 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 f9b974e..5077b7f 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"""