Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,11 @@ telegram:
lnbits:
url: "http://127.0.0.1:5000"
admin_key: "1234"
admin_id: "1234"
admin_id: "1234" # unused on lnbits 1.x, kept for older deployments
# ACL token scoped to /users/api/v1, required on lnbits 1.x to create users.
# Mint one under your lnbits account's access-control settings; a wallet api
# key does not grant account management there.
admin_token: "1234"
webhook_server: "http://0.0.0.0:5588" # Local webhook server address
webhook_public_url: "https://yourdomain.com/webhook" # Public URL for webhooks (for reverse proxy)
lnbits_public_url: "link.mylnurl.com"
Expand Down
4 changes: 4 additions & 0 deletions internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ type DatabaseConfiguration struct {
type LnbitsConfiguration struct {
AdminId string `yaml:"admin_id"`
AdminKey string `yaml:"admin_key"`
// AdminToken is an lnbits ACL token, scoped to /users/api/v1. Account
// management moved behind account-level auth in lnbits 1.x, where a wallet
// api key no longer grants it, so creating users needs a bearer token.
AdminToken string `yaml:"admin_token"`
Url string `yaml:"url"`
LnbitsPublicUrl string `yaml:"lnbits_public_url"`
WebhookServer string `yaml:"webhook_server"`
Expand Down
85 changes: 68 additions & 17 deletions internal/lnbits/lnbits.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ func parseLNbitsError(resp *req.Resp) Error {
return reqErr
}

// NewClient returns a new lnbits api client. Pass your API key and url here.
func NewClient(key, url string) *Client {
// NewClient returns a new lnbits api client. Pass your API key, the ACL token
// used for account management, and the url here.
func NewClient(key, adminToken, url string) *Client {
return &Client{
url: url,
// info: this header holds the ADMIN key for the entire API
Expand All @@ -34,12 +35,17 @@ func NewClient(key, url string) *Client {
"Accept": "application/json",
"X-Api-Key": key,
},
adminHeader: req.Header{
"Content-Type": "application/json",
"Accept": "application/json",
"Authorization": "Bearer " + adminToken,
},
}
}

// GetUser returns user information
func (c *Client) GetUser(userId string) (user User, err error) {
resp, err := req.Post(c.url+"/usermanager/api/v1/users/"+userId, c.header, nil)
resp, err := req.Get(c.url+"/users/api/v1/user/"+userId, c.adminHeader, nil)
if err != nil {
return
}
Expand All @@ -53,14 +59,21 @@ func (c *Client) GetUser(userId string) (user User, err error) {
return
}

// CreateUserWithInitialWallet creates new user with initial wallet
// CreateUserWithInitialWallet creates new user with initial wallet.
//
// lnbits 1.x creates the account's first wallet itself, named after
// lnbits_default_wallet_name, and its create-user response omits the wallets.
// So the wallet is fetched separately and renamed to walletName, keeping the
// "<telegram id> (<handle>)" convention the existing wallets use.
//
// adminId is unused: lnbits 1.x has no admin_id concept. It is kept so callers
// do not have to change.
func (c *Client) CreateUserWithInitialWallet(userName, walletName, adminId string, email string) (wal User, err error) {
resp, err := req.Post(c.url+"/usermanager/api/v1/users", c.header, req.BodyJSON(struct {
WalletName string `json:"wallet_name"`
AdminId string `json:"admin_id"`
UserName string `json:"user_name"`
Email string `json:"email"`
}{walletName, adminId, userName, email}))
// email is deliberately not sent: the bot passes a telegram handle here,
// which lnbits 1.x rejects via is_valid_email_address.
resp, err := req.Post(c.url+"/users/api/v1/user", c.adminHeader, req.BodyJSON(struct {
UserName string `json:"username"`
}{userName}))
if err != nil {
return
}
Expand All @@ -69,17 +82,55 @@ func (c *Client) CreateUserWithInitialWallet(userName, walletName, adminId strin
err = parseLNbitsError(resp)
return
}
err = resp.ToJSON(&wal)
if err = resp.ToJSON(&wal); err != nil {
return
}

wallets, err := c.Wallets(wal)
if err != nil {
return
}
if len(wallets) == 0 {
err = fmt.Errorf("lnbits created user %s without a wallet", wal.ID)
return
}
if err = c.renameWallet(wallets[0], walletName); err != nil {
return
}

// the create-user response echoes back only what it was given, so the name
// the bot stores is the username it asked for.
wal.Name = userName
return
}

// renameWallet sets a wallet's name. It authenticates with the wallet's own
// admin key rather than the ACL token, so it stays outside that token's scope.
func (c *Client) renameWallet(w Wallet, name string) error {
resp, err := req.Patch(c.url+"/api/v1/wallet", req.Header{
"Content-Type": "application/json",
"Accept": "application/json",
"X-Api-Key": w.Adminkey,
}, req.BodyJSON(struct {
Name string `json:"name"`
}{name}))
if err != nil {
return err
}
if resp.Response().StatusCode >= 300 {
return parseLNbitsError(resp)
}
return nil
}

// CreateWallet creates a new wallet.
//
// adminId is unused, see CreateUserWithInitialWallet.
func (c *Client) CreateWallet(userId, walletName, adminId string) (wal Wallet, err error) {
resp, err := req.Post(c.url+"/usermanager/api/v1/wallets", c.header, req.BodyJSON(struct {
UserId string `json:"user_id"`
WalletName string `json:"wallet_name"`
AdminId string `json:"admin_id"`
}{userId, walletName, adminId}))
resp, err := req.Post(c.url+"/users/api/v1/user/"+userId+"/wallet", c.adminHeader,
req.BodyJSON(struct {
Name string `json:"name"`
}{walletName}))
if err != nil {
return
}
Expand Down Expand Up @@ -188,7 +239,7 @@ func (c Client) Payment(w Wallet, payment_hash string) (payment LNbitsPayment, e

// 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)
resp, err := req.Get(c.url+"/users/api/v1/user/"+w.ID+"/wallet", c.adminHeader, nil)
if err != nil {
return
}
Expand Down
12 changes: 8 additions & 4 deletions internal/lnbits/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ import (
)

type Client struct {
header req.Header
url string
AdminKey string
InvoiceKey string
header req.Header
// adminHeader carries the ACL bearer token for /users/api/v1. It is kept
// separate from header so wallet calls cannot present it, and so the token
// cannot be used to spend: it is scoped to account management only.
adminHeader req.Header
url string
AdminKey string
InvoiceKey string
}

type User struct {
Expand Down
6 changes: 5 additions & 1 deletion internal/telegram/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ func NewBot() TipBot {

bot := TipBot{
DB: dbs,
Client: lnbits.NewClient(internal.Configuration.Lnbits.AdminKey, internal.Configuration.Lnbits.Url),
Client: lnbits.NewClient(
internal.Configuration.Lnbits.AdminKey,
internal.Configuration.Lnbits.AdminToken,
internal.Configuration.Lnbits.Url,
),
Bunt: createBunt(internal.Configuration.Database.BuntDbPath),
ShopBunt: createBunt(internal.Configuration.Database.ShopBuntDbPath),
Telegram: newTelegramBot(),
Expand Down
Loading