mirror of
https://github.com/ChuckNorrison/LightningTipBot.git
synced 2026-08-13 12:33:14 +02:00
* speed up tx lock * better logging and lnurl lightning: prefix Co-authored-by: callebtc <93376500+callebtc@users.noreply.github.com>
322 lines
11 KiB
Go
322 lines
11 KiB
Go
package telegram
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex"
|
|
"github.com/LightningTipBot/LightningTipBot/internal/storage"
|
|
|
|
"github.com/LightningTipBot/LightningTipBot/internal/i18n"
|
|
"github.com/LightningTipBot/LightningTipBot/internal/lnbits"
|
|
"github.com/LightningTipBot/LightningTipBot/internal/runtime"
|
|
|
|
"github.com/LightningTipBot/LightningTipBot/internal/str"
|
|
"github.com/LightningTipBot/LightningTipBot/pkg/lightning"
|
|
log "github.com/sirupsen/logrus"
|
|
tb "gopkg.in/lightningtipbot/telebot.v2"
|
|
)
|
|
|
|
var (
|
|
sendConfirmationMenu = &tb.ReplyMarkup{ResizeReplyKeyboard: true}
|
|
btnCancelSend = sendConfirmationMenu.Data("🚫 Cancel", "cancel_send")
|
|
btnSend = sendConfirmationMenu.Data("✅ Send", "confirm_send")
|
|
)
|
|
|
|
func helpSendUsage(ctx context.Context, errormsg string) string {
|
|
if len(errormsg) > 0 {
|
|
return fmt.Sprintf(Translate(ctx, "sendHelpText"), fmt.Sprintf("%s", errormsg))
|
|
} else {
|
|
return fmt.Sprintf(Translate(ctx, "sendHelpText"), "")
|
|
}
|
|
}
|
|
|
|
func (bot *TipBot) SendCheckSyntax(ctx context.Context, m *tb.Message) (bool, string) {
|
|
arguments := strings.Split(m.Text, " ")
|
|
if len(arguments) < 2 {
|
|
return false, fmt.Sprintf(Translate(ctx, "sendSyntaxErrorMessage"), GetUserStrMd(bot.Telegram.Me))
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
type SendData struct {
|
|
*storage.Base
|
|
From *lnbits.User `json:"from"`
|
|
ToTelegramId int64 `json:"to_telegram_id"`
|
|
ToTelegramUser string `json:"to_telegram_user"`
|
|
Memo string `json:"memo"`
|
|
Message string `json:"message"`
|
|
Amount int64 `json:"amount"`
|
|
LanguageCode string `json:"languagecode"`
|
|
}
|
|
|
|
// sendHandler invoked on "/send 123 @user" command
|
|
func (bot *TipBot) sendHandler(ctx context.Context, m *tb.Message) {
|
|
bot.anyTextHandler(ctx, m)
|
|
user := LoadUser(ctx)
|
|
if user.Wallet == nil {
|
|
return
|
|
}
|
|
|
|
// reset state immediately
|
|
ResetUserState(user, bot)
|
|
|
|
// check and print all commands
|
|
|
|
// If the send is a reply, then trigger /tip handler
|
|
if m.IsReply() && m.Chat.Type != tb.ChatPrivate {
|
|
bot.tipHandler(ctx, m)
|
|
return
|
|
}
|
|
|
|
// if ok, errstr := bot.SendCheckSyntax(ctx, m); !ok {
|
|
// bot.trySendMessage(m.Sender, helpSendUsage(ctx, errstr))
|
|
// NewMessage(m, WithDuration(0, bot))
|
|
// return
|
|
// }
|
|
|
|
// get send amount, returns 0 if no amount is given
|
|
amount, err := decodeAmountFromCommand(m.Text)
|
|
// info: /send 10 <user> DEMANDS an amount, while /send <ln@address.com> also works without
|
|
// todo: /send <user> should also invoke amount input dialog if no amount is given
|
|
|
|
// CHECK whether first or second argument is a LIGHTNING ADDRESS
|
|
arg := ""
|
|
if len(strings.Split(m.Text, " ")) > 2 {
|
|
arg, err = getArgumentFromCommand(m.Text, 2)
|
|
} else if len(strings.Split(m.Text, " ")) == 2 {
|
|
arg, err = getArgumentFromCommand(m.Text, 1)
|
|
}
|
|
if err == nil {
|
|
if lightning.IsLightningAddress(arg) {
|
|
// lightning address, send to that address
|
|
err = bot.sendToLightningAddress(ctx, m, arg, amount)
|
|
if err != nil {
|
|
log.Errorln(err.Error())
|
|
return
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// is a user given?
|
|
arg, err = getArgumentFromCommand(m.Text, 1)
|
|
if err != nil && m.Chat.Type == tb.ChatPrivate {
|
|
bot.askForUser(ctx, "", "CreateSendState", m.Text)
|
|
return
|
|
}
|
|
|
|
// is an amount given?
|
|
amount, err = decodeAmountFromCommand(m.Text)
|
|
if (err != nil || amount < 1) && m.Chat.Type == tb.ChatPrivate {
|
|
bot.askForAmount(ctx, "", "CreateSendState", 0, 0, m.Text)
|
|
return
|
|
}
|
|
|
|
// ASSUME INTERNAL SEND TO TELEGRAM USER
|
|
if err != nil || amount < 1 {
|
|
errmsg := fmt.Sprintf("[/send] Error: Send amount not valid.")
|
|
log.Warnln(errmsg)
|
|
// immediately delete if the amount is bullshit
|
|
NewMessage(m, WithDuration(0, bot))
|
|
bot.trySendMessage(m.Sender, helpSendUsage(ctx, Translate(ctx, "sendValidAmountMessage")))
|
|
return
|
|
}
|
|
|
|
// SEND COMMAND IS VALID
|
|
// check for memo in command
|
|
sendMemo := GetMemoFromCommand(m.Text, 3)
|
|
|
|
toUserStrMention := ""
|
|
toUserStrWithoutAt := ""
|
|
|
|
// check for user in command, accepts user mention or plain username without @
|
|
if len(m.Entities) > 1 && m.Entities[1].Type == "mention" {
|
|
toUserStrMention = m.Text[m.Entities[1].Offset : m.Entities[1].Offset+m.Entities[1].Length]
|
|
toUserStrWithoutAt = strings.TrimPrefix(toUserStrMention, "@")
|
|
} else {
|
|
toUserStrWithoutAt, err = getArgumentFromCommand(m.Text, 2)
|
|
if err != nil {
|
|
log.Errorln(err.Error())
|
|
return
|
|
}
|
|
toUserStrWithoutAt = strings.TrimPrefix(toUserStrWithoutAt, "@")
|
|
toUserStrMention = "@" + toUserStrWithoutAt
|
|
}
|
|
|
|
err = bot.parseCmdDonHandler(ctx, m)
|
|
if err == nil {
|
|
return
|
|
}
|
|
|
|
toUserDb, err := GetUserByTelegramUsername(toUserStrWithoutAt, *bot)
|
|
if err != nil {
|
|
NewMessage(m, WithDuration(0, bot))
|
|
// cut username if it's too long
|
|
if len(toUserStrMention) > 100 {
|
|
toUserStrMention = toUserStrMention[:100]
|
|
}
|
|
bot.trySendMessage(m.Sender, fmt.Sprintf(Translate(ctx, "sendUserHasNoWalletMessage"), str.MarkdownEscape(toUserStrMention)))
|
|
return
|
|
}
|
|
|
|
if user.ID == toUserDb.ID {
|
|
bot.trySendMessage(m.Sender, Translate(ctx, "sendYourselfMessage"))
|
|
return
|
|
}
|
|
|
|
// entire text of the inline object
|
|
confirmText := fmt.Sprintf(Translate(ctx, "confirmSendMessage"), str.MarkdownEscape(toUserStrMention), amount)
|
|
if len(sendMemo) > 0 {
|
|
confirmText = confirmText + fmt.Sprintf(Translate(ctx, "confirmSendAppendMemo"), str.MarkdownEscape(sendMemo))
|
|
}
|
|
// object that holds all information about the send payment
|
|
id := fmt.Sprintf("send-%d-%d-%s", m.Sender.ID, amount, RandStringRunes(5))
|
|
sendData := &SendData{
|
|
From: user,
|
|
Base: storage.New(storage.ID(id)),
|
|
Amount: int64(amount),
|
|
ToTelegramId: toUserDb.Telegram.ID,
|
|
ToTelegramUser: toUserStrWithoutAt,
|
|
Memo: sendMemo,
|
|
Message: confirmText,
|
|
LanguageCode: ctx.Value("publicLanguageCode").(string),
|
|
}
|
|
// save persistent struct
|
|
runtime.IgnoreError(sendData.Set(sendData, bot.Bunt))
|
|
|
|
sendDataJson, err := json.Marshal(sendData)
|
|
if err != nil {
|
|
NewMessage(m, WithDuration(0, bot))
|
|
log.Printf("[/send] Error: %s\n", err.Error())
|
|
bot.trySendMessage(m.Sender, fmt.Sprint(Translate(ctx, "errorTryLaterMessage")))
|
|
return
|
|
}
|
|
// save the send data to the Database
|
|
// log.Debug(sendData)
|
|
SetUserState(user, bot, lnbits.UserStateConfirmSend, string(sendDataJson))
|
|
sendButton := sendConfirmationMenu.Data(Translate(ctx, "sendButtonMessage"), "confirm_send")
|
|
cancelButton := sendConfirmationMenu.Data(Translate(ctx, "cancelButtonMessage"), "cancel_send")
|
|
sendButton.Data = id
|
|
cancelButton.Data = id
|
|
|
|
sendConfirmationMenu.Inline(
|
|
sendConfirmationMenu.Row(
|
|
sendButton,
|
|
cancelButton),
|
|
)
|
|
if m.Private() {
|
|
bot.trySendMessage(m.Chat, confirmText, sendConfirmationMenu)
|
|
} else {
|
|
bot.tryReplyMessage(m, confirmText, sendConfirmationMenu)
|
|
}
|
|
}
|
|
|
|
// sendHandler invoked when user clicked send on payment confirmation
|
|
func (bot *TipBot) confirmSendHandler(ctx context.Context, c *tb.Callback) {
|
|
tx := &SendData{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)
|
|
if err != nil {
|
|
log.Errorf("[acceptSendHandler] %s", err.Error())
|
|
return
|
|
}
|
|
sendData := sn.(*SendData)
|
|
// onnly the correct user can press
|
|
if sendData.From.Telegram.ID != c.Sender.ID {
|
|
return
|
|
}
|
|
if !sendData.Active {
|
|
log.Errorf("[acceptSendHandler] send not active anymore")
|
|
// bot.tryDeleteMessage(c.Message)
|
|
return
|
|
}
|
|
defer sendData.Set(sendData, bot.Bunt)
|
|
|
|
// // remove buttons from confirmation message
|
|
// bot.tryEditMessage(c.Message, MarkdownEscape(sendData.Message), &tb.ReplyMarkup{})
|
|
|
|
// decode callback data
|
|
// log.Debug("[send] Callback: %s", c.Data)
|
|
from := LoadUser(ctx)
|
|
ResetUserState(from, bot) // we don't need to check the statekey anymore like we did earlier
|
|
|
|
// information about the send
|
|
toId := sendData.ToTelegramId
|
|
toUserStrWithoutAt := sendData.ToTelegramUser
|
|
amount := sendData.Amount
|
|
sendMemo := sendData.Memo
|
|
|
|
// we can now get the wallets of both users
|
|
to, err := GetLnbitsUser(&tb.User{ID: toId, Username: toUserStrWithoutAt}, *bot)
|
|
if err != nil {
|
|
log.Errorln(err.Error())
|
|
bot.tryDeleteMessage(c.Message)
|
|
return
|
|
}
|
|
toUserStrMd := GetUserStrMd(to.Telegram)
|
|
fromUserStrMd := GetUserStrMd(from.Telegram)
|
|
toUserStr := GetUserStr(to.Telegram)
|
|
fromUserStr := GetUserStr(from.Telegram)
|
|
|
|
transactionMemo := fmt.Sprintf("Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount)
|
|
t := NewTransaction(bot, from, to, amount, TransactionType("send"))
|
|
t.Memo = transactionMemo
|
|
|
|
success, err := t.Send()
|
|
if !success || err != nil {
|
|
// bot.trySendMessage(c.Sender, sendErrorMessage)
|
|
errmsg := fmt.Sprintf("[/send] Error: Transaction failed. %s", err.Error())
|
|
log.Errorln(errmsg)
|
|
bot.tryEditMessage(c.Message, i18n.Translate(sendData.LanguageCode, "sendErrorMessage"), &tb.ReplyMarkup{})
|
|
return
|
|
}
|
|
sendData.Inactivate(sendData, bot.Bunt)
|
|
|
|
log.Infof("[💸 send] Send from %s to %s (%d sat).", fromUserStr, toUserStr, amount)
|
|
|
|
// notify to user
|
|
bot.trySendMessage(to.Telegram, fmt.Sprintf(i18n.Translate(to.Telegram.LanguageCode, "sendReceivedMessage"), fromUserStrMd, amount))
|
|
// bot.trySendMessage(from.Telegram, fmt.Sprintf(Translate(ctx, "sendSentMessage"), amount, toUserStrMd))
|
|
if c.Message.Private() {
|
|
// if the command was invoked in private chat
|
|
bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendSentMessage"), amount, toUserStrMd), &tb.ReplyMarkup{})
|
|
} else {
|
|
// if the command was invoked in group chat
|
|
bot.trySendMessage(c.Sender, fmt.Sprintf(i18n.Translate(from.Telegram.LanguageCode, "sendSentMessage"), amount, toUserStrMd))
|
|
bot.tryEditMessage(c.Message, fmt.Sprintf(i18n.Translate(sendData.LanguageCode, "sendPublicSentMessage"), amount, fromUserStrMd, toUserStrMd), &tb.ReplyMarkup{})
|
|
}
|
|
// send memo if it was present
|
|
if len(sendMemo) > 0 {
|
|
bot.trySendMessage(to.Telegram, fmt.Sprintf("✉️ %s", str.MarkdownEscape(sendMemo)))
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// cancelPaymentHandler invoked when user clicked cancel on payment confirmation
|
|
func (bot *TipBot) cancelSendHandler(ctx context.Context, c *tb.Callback) {
|
|
// reset state immediately
|
|
user := LoadUser(ctx)
|
|
ResetUserState(user, bot)
|
|
tx := &SendData{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)
|
|
if err != nil {
|
|
log.Errorf("[acceptSendHandler] %s", err.Error())
|
|
return
|
|
}
|
|
|
|
sendData := sn.(*SendData)
|
|
// onnly the correct user can press
|
|
if sendData.From.Telegram.ID != c.Sender.ID {
|
|
return
|
|
}
|
|
// remove buttons from confirmation message
|
|
bot.tryEditMessage(c.Message, i18n.Translate(sendData.LanguageCode, "sendCancelledMessage"), &tb.ReplyMarkup{})
|
|
sendData.Inactivate(sendData, bot.Bunt)
|
|
}
|