mirror of
https://github.com/ChuckNorrison/LightningTipBot.git
synced 2026-08-13 12:33:14 +02:00
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>
This commit is contained in:
parent
9522418272
commit
96292604c7
11 changed files with 259 additions and 19 deletions
1
go.mod
1
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
|
||||
|
|
|
|||
2
go.sum
2
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=
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
151
internal/telegram/lnurl-auth.go
Normal file
151
internal/telegram/lnurl-auth.go
Normal file
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -237,6 +237,13 @@ lnurlHelpText = """📖 Ups, das hat nicht geklappt. %s
|
|||
*Befehl:* `/lnurl [betrag] <lnurl>`
|
||||
*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"""
|
||||
|
|
|
|||
|
|
@ -241,6 +241,13 @@ lnurlHelpText = """📖 Oops, that didn't work. %s
|
|||
*Usage:* `/lnurl [amount] <lnurl>`
|
||||
*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"""
|
||||
|
|
|
|||
|
|
@ -237,6 +237,10 @@ lnurlHelpText = """📖 Oops, eso no funcionó. %s
|
|||
*Uso:* `/lnurl [monto] <lnurl>`
|
||||
*Ejemplo:* `/lnurl LNURL1DP68GUR...`"""
|
||||
|
||||
# LNURL LOGIN
|
||||
|
||||
lnurlSuccessfulLogin = """✅ Inicio de sesión exitoso."""
|
||||
|
||||
# LNURL WITHDRAW
|
||||
|
||||
confirmLnurlWithdrawMessage = """¿Desea realizar este retiro?\n\n💸 Monto: %d sat"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue