mirror of
https://github.com/ChuckNorrison/LightningTipBot.git
synced 2026-08-17 13:07:16 +02:00
commit
7009d3dedc
9 changed files with 317 additions and 32 deletions
27
internal/api/invoice.go
Normal file
27
internal/api/invoice.go
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package api
|
||||
|
||||
type BalanceResponse struct {
|
||||
Balance int64 `json:"balance"`
|
||||
}
|
||||
|
||||
type InvoiceStatusResponse struct {
|
||||
State string `json:"state,omitempty"`
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
Preimage int64 `json:"preimage"`
|
||||
}
|
||||
|
||||
type CreateInvoiceResponse struct {
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
PayRequest string `json:"pay_request"`
|
||||
Preimage string `json:"preimage,omitempty"`
|
||||
}
|
||||
type CreateInvoiceRequest struct {
|
||||
Memo string `json:"memo"`
|
||||
Amount int64 `json:"amount"`
|
||||
DescriptionHash string `json:"description_hash"`
|
||||
UnhashedDescription string `json:"unhashed_description"`
|
||||
}
|
||||
|
||||
type PayInvoiceRequest struct {
|
||||
PayRequest string `json:"pay_req"`
|
||||
}
|
||||
121
internal/api/lightning.go
Normal file
121
internal/api/lightning.go
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/LightningTipBot/LightningTipBot/internal"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/lnbits"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/telegram"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
Bot *telegram.TipBot
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Message string `json:"error"`
|
||||
}
|
||||
|
||||
func RespondError(w http.ResponseWriter, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(ErrorResponse{Message: message})
|
||||
}
|
||||
|
||||
func (s Service) Balance(w http.ResponseWriter, r *http.Request) {
|
||||
user := telegram.LoadUser(r.Context())
|
||||
balance, err := s.Bot.GetUserBalance(user)
|
||||
if err != nil {
|
||||
RespondError(w, "balance check failed")
|
||||
return
|
||||
}
|
||||
|
||||
balanceResponse := BalanceResponse{
|
||||
Balance: balance,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(balanceResponse)
|
||||
}
|
||||
|
||||
func (s Service) CreateInvoice(w http.ResponseWriter, r *http.Request) {
|
||||
user := telegram.LoadUser(r.Context())
|
||||
var createInvoiceRequest CreateInvoiceRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&createInvoiceRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
invoice, err := user.Wallet.Invoice(
|
||||
lnbits.InvoiceParams{
|
||||
Amount: createInvoiceRequest.Amount,
|
||||
Out: false,
|
||||
DescriptionHash: createInvoiceRequest.DescriptionHash,
|
||||
UnhashedDescription: createInvoiceRequest.UnhashedDescription,
|
||||
Webhook: internal.Configuration.Lnbits.WebhookServer},
|
||||
s.Bot.Client)
|
||||
if err != nil {
|
||||
RespondError(w, "could not create invoice")
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(invoice)
|
||||
}
|
||||
|
||||
func (s Service) PayInvoice(w http.ResponseWriter, r *http.Request) {
|
||||
user := telegram.LoadUser(r.Context())
|
||||
var payInvoiceRequest PayInvoiceRequest
|
||||
err := json.NewDecoder(r.Body).Decode(&payInvoiceRequest)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
invoice, err := user.Wallet.Pay(lnbits.PaymentParams{Out: true, Bolt11: payInvoiceRequest.PayRequest}, s.Bot.Client)
|
||||
if err != nil {
|
||||
RespondError(w, "could not pay invoice: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
payment, _ := s.Bot.Client.Payment(*user.Wallet, invoice.PaymentHash)
|
||||
if err != nil {
|
||||
// we assume that it's paid since thre was no error earlier
|
||||
payment.Paid = true
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(payment)
|
||||
}
|
||||
|
||||
func (s Service) PaymentStatus(w http.ResponseWriter, r *http.Request) {
|
||||
user := telegram.LoadUser(r.Context())
|
||||
payment_hash := mux.Vars(r)["payment_hash"]
|
||||
payment, err := s.Bot.Client.Payment(*user.Wallet, payment_hash)
|
||||
if err != nil {
|
||||
RespondError(w, "could not get payment")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(payment)
|
||||
}
|
||||
|
||||
// InvoiceStatus
|
||||
func (s Service) InvoiceStatus(w http.ResponseWriter, r *http.Request) {
|
||||
user := telegram.LoadUser(r.Context())
|
||||
payment_hash := mux.Vars(r)["payment_hash"]
|
||||
user.Wallet = &lnbits.Wallet{}
|
||||
payment, err := s.Bot.Client.Payment(*user.Wallet, payment_hash)
|
||||
if err != nil {
|
||||
RespondError(w, "could not get invoice")
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(payment)
|
||||
}
|
||||
|
|
@ -1,8 +1,16 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"strings"
|
||||
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/lnbits"
|
||||
"github.com/LightningTipBot/LightningTipBot/internal/telegram"
|
||||
"gorm.io/gorm"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
|
@ -16,6 +24,82 @@ func LoggingMiddleware(prefix string, next http.HandlerFunc) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
type AuthType struct {
|
||||
Type string
|
||||
Decoder func(s string) ([]byte, error)
|
||||
}
|
||||
|
||||
var AuthTypeBasic = AuthType{Type: "Basic"}
|
||||
var AuthTypeBearerBase64 = AuthType{Type: "Basic", Decoder: base64.StdEncoding.DecodeString}
|
||||
|
||||
func AuthorizationMiddleware(database *gorm.DB, authType AuthType, next http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
// check if the user is banned
|
||||
if auth == "" {
|
||||
w.WriteHeader(401)
|
||||
log.Warn("[AuthorizationMiddleware] no auth")
|
||||
return
|
||||
}
|
||||
_, password, ok := parseAuth(authType, auth)
|
||||
if !ok {
|
||||
w.WriteHeader(401)
|
||||
return
|
||||
}
|
||||
// first we make sure that the password is not already "banned_"
|
||||
if strings.Contains(password, "_") || strings.HasPrefix(password, "banned_") {
|
||||
w.WriteHeader(401)
|
||||
log.Warnf("[AuthorizationMiddleware] 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 := database.Where("wallet_adminkey = ? COLLATE NOCASE", password).First(user)
|
||||
if tx.Error != nil {
|
||||
tx = database.Where("wallet_inkey = ? COLLATE NOCASE", password).First(user)
|
||||
log.Warnf("[AuthorizationMiddleware] Could not get wallet admin key %s: %v", password, tx.Error)
|
||||
if tx.Error != nil {
|
||||
log.Warnf("[AuthorizationMiddleware] need admin key to pay invoice %s: %v", password, tx.Error)
|
||||
return
|
||||
}
|
||||
if r.URL.Path == "/api/v1/payinvoice" {
|
||||
log.Warnf("[AuthorizationMiddleware] need admin key to pay invoice %s: %v", password, tx.Error)
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Debugf("[AuthorizationMiddleware] User: %s", telegram.GetUserStr(user.Telegram))
|
||||
r = r.WithContext(context.WithValue(r.Context(), "user", user))
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// parseAuth parses an HTTP Basic Authentication string.
|
||||
// "Bearer QWxhZGRpbjpvcGVuIHNlc2FtZQ==" returns ("Aladdin", "open sesame", true).
|
||||
func parseAuth(authType AuthType, auth string) (username, password string, ok bool) {
|
||||
parse := func(prefix string) (username, password string, ok bool) {
|
||||
// Case insensitive prefix match. See Issue 22736.
|
||||
if len(auth) < len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
|
||||
return
|
||||
}
|
||||
if authType.Decoder != nil {
|
||||
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
|
||||
}
|
||||
return auth[len(prefix):], auth[len(prefix):], true
|
||||
|
||||
}
|
||||
return parse(fmt.Sprintf("%s ", authType.Type))
|
||||
|
||||
}
|
||||
|
||||
func dump(r *http.Request) string {
|
||||
x, err := httputil.DumpRequest(r, true)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package api
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"gorm.io/gorm"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
|
|
@ -42,6 +43,12 @@ func (w *Server) ListenAndServe() {
|
|||
func (w *Server) PathPrefix(path string, handler http.Handler) {
|
||||
w.router.PathPrefix(path).Handler(handler)
|
||||
}
|
||||
func (w *Server) AppendAuthorizedRoute(path string, authType AuthType, database *gorm.DB, handler func(http.ResponseWriter, *http.Request), methods ...string) {
|
||||
r := w.router.HandleFunc(path, LoggingMiddleware("API", AuthorizationMiddleware(database, authType, handler)))
|
||||
if len(methods) > 0 {
|
||||
r.Methods(methods...)
|
||||
}
|
||||
}
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package lnbits
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req"
|
||||
|
|
@ -84,7 +85,7 @@ 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) {
|
||||
func (w Wallet) Invoice(params InvoiceParams, c *Client) (lntx Invoice, err error) {
|
||||
// custom header with invoice key
|
||||
invoiceHeader := req.Header{
|
||||
"Content-Type": "application/json",
|
||||
|
|
@ -131,7 +132,7 @@ func (c Client) Info(w Wallet) (wtx Wallet, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// Info returns wallet payments
|
||||
// Payments returns wallet payments
|
||||
func (c Client) Payments(w Wallet) (wtx Payments, err error) {
|
||||
// custom header with invoice key
|
||||
invoiceHeader := req.Header{
|
||||
|
|
@ -155,6 +156,30 @@ func (c Client) Payments(w Wallet) (wtx Payments, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// Payment state of a payment
|
||||
func (c Client) Payment(w Wallet, payment_hash string) (payment LNbitsPayment, 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+fmt.Sprintf("/api/v1/payments/%s", payment_hash), invoiceHeader, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if resp.Response().StatusCode >= 300 {
|
||||
var reqErr Error
|
||||
resp.ToJSON(&reqErr)
|
||||
err = reqErr
|
||||
return
|
||||
}
|
||||
|
||||
err = resp.ToJSON(&payment)
|
||||
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)
|
||||
|
|
@ -174,7 +199,7 @@ 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) {
|
||||
func (w Wallet) Pay(params PaymentParams, c *Client) (wtx Invoice, err error) {
|
||||
// custom header with admin key
|
||||
adminHeader := req.Header{
|
||||
"Content-Type": "application/json",
|
||||
|
|
|
|||
|
|
@ -75,11 +75,12 @@ func (u *User) ResetState() {
|
|||
}
|
||||
|
||||
type InvoiceParams struct {
|
||||
Out bool `json:"out"` // must be True if invoice is payed, False if invoice is received
|
||||
Amount int64 `json:"amount"` // amount in Satoshi
|
||||
Memo string `json:"memo,omitempty"` // the invoice memo.
|
||||
Webhook string `json:"webhook,omitempty"` // the webhook to fire back to when payment is received.
|
||||
DescriptionHash string `json:"description_hash,omitempty"` // the invoice description hash.
|
||||
Out bool `json:"out"` // must be True if invoice is payed, False if invoice is received
|
||||
Amount int64 `json:"amount"` // amount in Satoshi
|
||||
Memo string `json:"memo,omitempty"` // the invoice memo.
|
||||
Webhook string `json:"webhook,omitempty"` // the webhook to fire back to when payment is received.
|
||||
DescriptionHash string `json:"description_hash,omitempty"` // the invoice description hash.
|
||||
UnhashedDescription string `json:"unhashed_description,omitempty"` // the unhashed invoice description.
|
||||
}
|
||||
|
||||
type PaymentParams struct {
|
||||
|
|
@ -117,7 +118,7 @@ type Wallet struct {
|
|||
User string `json:"user"`
|
||||
}
|
||||
|
||||
type Payments []struct {
|
||||
type Payment struct {
|
||||
CheckingID string `json:"checking_id"`
|
||||
Pending bool `json:"pending"`
|
||||
Amount int64 `json:"amount"`
|
||||
|
|
@ -133,7 +134,15 @@ type Payments []struct {
|
|||
WebhookStatus interface{} `json:"webhook_status"`
|
||||
}
|
||||
|
||||
type BitInvoice struct {
|
||||
type LNbitsPayment struct {
|
||||
Paid bool `json:"paid"`
|
||||
Preimage string `json:"preimage"`
|
||||
Details Payment `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
type Payments []Payment
|
||||
|
||||
type Invoice struct {
|
||||
PaymentHash string `json:"payment_hash"`
|
||||
PaymentRequest string `json:"payment_request"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ type LnurlWithdrawState struct {
|
|||
Comment string `json:"comment"`
|
||||
LanguageCode string `json:"languagecode"`
|
||||
Success bool `json:"success"`
|
||||
Invoice lnbits.BitInvoice `json:"invoice"`
|
||||
Invoice lnbits.Invoice `json:"invoice"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,26 +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 int64 `json:"from_id" `
|
||||
ToId int64 `json:"to_id" `
|
||||
FromUser string `json:"from_user"`
|
||||
ToUser string `json:"to_user"`
|
||||
Type string `json:"type"`
|
||||
Amount int64 `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_"`
|
||||
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 int64 `json:"from_id" `
|
||||
ToId int64 `json:"to_id" `
|
||||
FromUser string `json:"from_user"`
|
||||
ToUser string `json:"to_user"`
|
||||
Type string `json:"type"`
|
||||
Amount int64 `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.Invoice `gorm:"embedded;embeddedPrefix:invoice_"`
|
||||
}
|
||||
|
||||
type TransactionOption func(t *Transaction)
|
||||
|
|
|
|||
14
main.go
14
main.go
|
|
@ -54,7 +54,6 @@ func startApiServer(bot *telegram.TipBot) {
|
|||
// append lnurl ctx functions
|
||||
lnUrl := lnurl.New(bot)
|
||||
s.AppendRoute("/.well-known/lnurlp/{username}", lnUrl.Handle, http.MethodGet)
|
||||
|
||||
// userpage server
|
||||
userpage := userpage.New(bot)
|
||||
s.AppendRoute("/@{username}", userpage.UserPageHandler, http.MethodGet)
|
||||
|
|
@ -63,6 +62,16 @@ func startApiServer(bot *telegram.TipBot) {
|
|||
hub := lndhub.New(bot)
|
||||
s.AppendRoute(`/lndhub/ext/{.*}`, hub.Handle)
|
||||
s.AppendRoute(`/lndhub/ext`, hub.Handle)
|
||||
//s.AppendAuthorizedRoute(`/lndhub/ext/{.*}`, api.AuthTypeBearer, bot.DB.Users, hub.Handle)
|
||||
//s.AppendAuthorizedRoute(`/lndhub/ext`, api.AuthTypeBearer, bot.DB.Users, hub.Handle)
|
||||
|
||||
// starting api service
|
||||
apiService := api.Service{Bot: bot}
|
||||
s.AppendAuthorizedRoute(`/api/v1/paymentstatus/{payment_hash}`, api.AuthTypeBasic, bot.DB.Users, apiService.PaymentStatus, http.MethodPost)
|
||||
s.AppendAuthorizedRoute(`/api/v1/invoicestatus/{payment_hash}`, api.AuthTypeBasic, bot.DB.Users, apiService.InvoiceStatus, http.MethodPost)
|
||||
s.AppendAuthorizedRoute(`/api/v1/payinvoice`, api.AuthTypeBasic, bot.DB.Users, apiService.PayInvoice, http.MethodPost)
|
||||
s.AppendAuthorizedRoute(`/api/v1/createinvoice`, api.AuthTypeBasic, bot.DB.Users, apiService.CreateInvoice, http.MethodPost)
|
||||
s.AppendAuthorizedRoute(`/api/v1/balance`, api.AuthTypeBasic, bot.DB.Users, apiService.Balance, http.MethodGet)
|
||||
|
||||
// start internal admin server
|
||||
adminService := admin.New(bot)
|
||||
|
|
@ -75,6 +84,9 @@ func startApiServer(bot *telegram.TipBot) {
|
|||
|
||||
}
|
||||
|
||||
func withMiddleware(mw func(w http.ResponseWriter, r *http.Request)) http.HandlerFunc {
|
||||
return mw
|
||||
}
|
||||
func withRecovery() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorln("Recovered panic: ", r)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue