Merge pull request #374 from LightningTipBot/api

Api
This commit is contained in:
calle 2022-08-20 01:05:24 +02:00 committed by GitHub
commit 7009d3dedc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 317 additions and 32 deletions

27
internal/api/invoice.go Normal file
View 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
View 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)
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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",

View file

@ -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"`
}

View file

@ -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"`
}

View file

@ -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
View file

@ -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)