From 52f64a47b662f42eefa575ead3f5d8d21338f560 Mon Sep 17 00:00:00 2001 From: Buddhi Thilakshana Date: Wed, 25 Mar 2026 22:43:22 +0530 Subject: [PATCH 1/7] feat: add /sendbatch command for multi-recipient batch sends Adds a new /sendbatch Telegram command that allows sending sats to multiple recipients in a single operation with confirmation. Supports two formats: - Per-line memo: /sendbatch\n @user \n... - Shared memo: /sendbatch \n @user\n... Includes balance pre-check and re-check at execution, sequential transfer execution with stop-on-failure, idempotency via Bunt storage, and per-recipient notifications. --- internal/telegram/handler.go | 46 ++++ internal/telegram/sendbatch.go | 376 +++++++++++++++++++++++++++++++++ 2 files changed, 422 insertions(+) create mode 100644 internal/telegram/sendbatch.go diff --git a/internal/telegram/handler.go b/internal/telegram/handler.go index d8f0c98c..44d37b1e 100644 --- a/internal/telegram/handler.go +++ b/internal/telegram/handler.go @@ -389,6 +389,22 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, + { + Endpoints: []interface{}{"/sendbatch"}, + Handler: bot.sendbatchHandler, + Interceptor: &Interceptor{ + Before: []intercept.Func{ + bot.requirePrivateChatInterceptor, + bot.localizerInterceptor, + bot.logMessageInterceptor, + bot.requireUserInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, { Endpoints: []interface{}{&btnSendMainMenu}, Handler: bot.keyboardSendHandler, @@ -822,6 +838,36 @@ func (bot TipBot) getHandler() []InterceptionWrapper { }, }, }, + { + Endpoints: []interface{}{&btnConfirmSendBatch}, + Handler: bot.confirmSendBatchHandler, + Interceptor: &Interceptor{ + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.requireUserInterceptor, + bot.answerCallbackInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, + { + Endpoints: []interface{}{&btnCancelSendBatch}, + Handler: bot.cancelSendBatchHandler, + Interceptor: &Interceptor{ + Before: []intercept.Func{ + bot.localizerInterceptor, + bot.requireUserInterceptor, + bot.answerCallbackInterceptor, + bot.lockInterceptor, + }, + OnDefer: []intercept.Func{ + bot.unlockInterceptor, + }, + }, + }, { Endpoints: []interface{}{&btnApproveAPITx}, Handler: bot.approveAPITransactionHandler, diff --git a/internal/telegram/sendbatch.go b/internal/telegram/sendbatch.go new file mode 100644 index 00000000..e1b79232 --- /dev/null +++ b/internal/telegram/sendbatch.go @@ -0,0 +1,376 @@ +package telegram + +import ( + "fmt" + "strconv" + "strings" + + "github.com/LightningTipBot/LightningTipBot/internal/errors" + "github.com/LightningTipBot/LightningTipBot/internal/lnbits" + "github.com/LightningTipBot/LightningTipBot/internal/runtime" + "github.com/LightningTipBot/LightningTipBot/internal/runtime/mutex" + "github.com/LightningTipBot/LightningTipBot/internal/storage" + "github.com/LightningTipBot/LightningTipBot/internal/str" + "github.com/LightningTipBot/LightningTipBot/internal/telegram/intercept" + "github.com/LightningTipBot/LightningTipBot/internal/thirdparty" + + log "github.com/sirupsen/logrus" + tb "gopkg.in/lightningtipbot/telebot.v3" +) + +var ( + sendBatchConfirmationMenu = &tb.ReplyMarkup{ResizeKeyboard: true} + btnCancelSendBatch = sendBatchConfirmationMenu.Data("🚫 Cancel", "cancel_send_batch") + btnConfirmSendBatch = sendBatchConfirmationMenu.Data("āœ… Send", "confirm_send_batch") +) + +type BatchEntry struct { + Amount int64 `json:"amount"` + ToUsername string `json:"to_username"` + ToTelegramId int64 `json:"to_telegram_id"` + Memo string `json:"memo"` +} + +type SendBatchData struct { + *storage.Base + From *lnbits.User `json:"from"` + Entries []BatchEntry `json:"entries"` + TotalAmount int64 `json:"total_amount"` + SharedMemo string `json:"shared_memo"` + LanguageCode string `json:"languagecode"` +} + +// parseBatchEntries parses multi-line batch send message. +// Format 1: /sendbatch\n @user [memo]\n... +// Format 2: /sendbatch \n @user\n... +func parseBatchEntries(text string) (entries []parsedEntry, sharedMemo string, err error) { + lines := strings.Split(strings.TrimSpace(text), "\n") + if len(lines) < 2 { + return nil, "", fmt.Errorf("need at least one recipient line after /sendbatch") + } + + // Parse first line for shared memo + firstLine := strings.TrimSpace(lines[0]) + parts := strings.SplitN(firstLine, " ", 2) + if len(parts) > 1 { + sharedMemo = strings.TrimSpace(parts[1]) + } + + // Parse recipient lines + for i, line := range lines[1:] { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + entry, err := parseRecipientLine(line, sharedMemo) + if err != nil { + return nil, "", fmt.Errorf("line %d: %s", i+2, err.Error()) + } + entries = append(entries, entry) + } + + if len(entries) == 0 { + return nil, "", fmt.Errorf("no valid recipient lines found") + } + + return entries, sharedMemo, nil +} + +type parsedEntry struct { + Amount int64 + Username string // without @ + Memo string +} + +func parseRecipientLine(line string, sharedMemo string) (parsedEntry, error) { + fields := strings.Fields(line) + if len(fields) < 2 { + return parsedEntry{}, fmt.Errorf("expected: @ [memo]") + } + + // Parse amount + amount, err := strconv.ParseInt(fields[0], 10, 64) + if err != nil || amount < 1 { + return parsedEntry{}, fmt.Errorf("invalid amount: %s", fields[0]) + } + + // Parse username + username := strings.TrimPrefix(fields[1], "@") + if username == "" { + return parsedEntry{}, fmt.Errorf("invalid username: %s", fields[1]) + } + + // Parse per-line memo (everything after amount and username) + memo := "" + if len(fields) > 2 { + memo = strings.Join(fields[2:], " ") + } + + // Fall back to shared memo if no per-line memo + if memo == "" { + memo = sharedMemo + } + + return parsedEntry{ + Amount: amount, + Username: username, + Memo: memo, + }, nil +} + +// sendbatchHandler invoked on "/sendbatch" command +func (bot *TipBot) sendbatchHandler(ctx intercept.Context) (intercept.Context, error) { + bot.anyTextHandler(ctx) + user := LoadUser(ctx) + if user.Wallet == nil { + return ctx, errors.Create(errors.UserNoWalletError) + } + + ResetUserState(user, bot) + + // Parse the batch entries + parsed, sharedMemo, err := parseBatchEntries(ctx.Message().Text) + if err != nil { + bot.trySendMessage(ctx.Message().Sender, fmt.Sprintf("āŒ *Batch Send Error*\n\n%s\n\n*Usage:*\n```\n/sendbatch [shared memo]\n @user [memo]\n @user [memo]\n```", str.MarkdownEscape(err.Error()))) + return ctx, errors.Create(errors.InvalidSyntaxError) + } + + // Resolve all recipients and build entries + var entries []BatchEntry + var totalAmount int64 + var confirmLines []string + + for i, p := range parsed { + // Look up recipient + toUser, err := GetUserByTelegramUsername(p.Username, *bot) + if err != nil { + bot.trySendMessage(ctx.Message().Sender, fmt.Sprintf("āŒ *Batch Send Error*\n\nLine %d: User @%s not found or has no wallet.", i+1, str.MarkdownEscape(p.Username))) + return ctx, errors.Create(errors.InvalidSyntaxError) + } + + // Prevent self-send + if user.Telegram.ID == toUser.Telegram.ID { + bot.trySendMessage(ctx.Message().Sender, fmt.Sprintf("āŒ *Batch Send Error*\n\nLine %d: Cannot send to yourself.", i+1)) + return ctx, errors.Create(errors.SelfPaymentError) + } + + entries = append(entries, BatchEntry{ + Amount: p.Amount, + ToUsername: p.Username, + ToTelegramId: toUser.Telegram.ID, + Memo: p.Memo, + }) + totalAmount += p.Amount + + // Build confirmation line + line := fmt.Sprintf("`%d` sats → @%s", p.Amount, str.MarkdownEscape(p.Username)) + if p.Memo != "" { + line += fmt.Sprintf(" _%s_", str.MarkdownEscape(p.Memo)) + } + confirmLines = append(confirmLines, line) + } + + // Check sender has enough balance for total + balance, err := bot.GetUserAvailableBalance(user) + if err != nil { + log.Errorln(err.Error()) + bot.trySendMessage(ctx.Message().Sender, "āŒ Could not check your balance. Please try again.") + return ctx, err + } + if balance < totalAmount { + bot.trySendMessage(ctx.Message().Sender, fmt.Sprintf("āŒ *Insufficient balance*\n\nRequired: %s\nAvailable: %s", + thirdparty.FormatSatsWithLKR(totalAmount), + thirdparty.FormatSatsWithLKR(balance))) + return ctx, fmt.Errorf("insufficient balance for batch send") + } + + // Build confirmation message + confirmText := fmt.Sprintf("šŸ“¦ *Batch Send Confirmation*\n\n%s\n\n━━━━━━━━━━━━━━━━━━\n*Total:* %s\n*Recipients:* %d", + strings.Join(confirmLines, "\n"), + thirdparty.FormatSatsWithLKR(totalAmount), + len(entries)) + + // Persist batch data + id := fmt.Sprintf("sendbatch-%d-%d-%s", ctx.Message().Sender.ID, totalAmount, RandStringRunes(5)) + batchData := &SendBatchData{ + Base: storage.New(storage.ID(id)), + From: user, + Entries: entries, + TotalAmount: totalAmount, + SharedMemo: sharedMemo, + LanguageCode: ctx.Value("publicLanguageCode").(string), + } + runtime.IgnoreError(batchData.Set(batchData, bot.Bunt)) + + // Set up confirmation buttons + confirmBtn := sendBatchConfirmationMenu.Data("āœ… Confirm Send", "confirm_send_batch") + cancelBtn := sendBatchConfirmationMenu.Data("🚫 Cancel", "cancel_send_batch") + confirmBtn.Data = id + cancelBtn.Data = id + + sendBatchConfirmationMenu.Inline( + sendBatchConfirmationMenu.Row(confirmBtn, cancelBtn), + ) + + bot.trySendMessage(ctx.Chat(), confirmText, sendBatchConfirmationMenu) + return ctx, nil +} + +// confirmSendBatchHandler executes the batch send after user confirms +func (bot *TipBot) confirmSendBatchHandler(ctx intercept.Context) (intercept.Context, error) { + tx := &SendBatchData{Base: storage.New(storage.ID(ctx.Data()))} + mutex.LockWithContext(ctx, tx.ID) + defer mutex.UnlockWithContext(ctx, tx.ID) + + sn, err := tx.Get(tx, bot.Bunt) + if err != nil { + log.Errorf("[confirmSendBatchHandler] %s", err.Error()) + return ctx, err + } + batchData := sn.(*SendBatchData) + + // Only the sender can confirm + if batchData.From.Telegram.ID != ctx.Callback().Sender.ID { + return ctx, errors.Create(errors.UnknownError) + } + if !batchData.Active { + log.Errorf("[confirmSendBatchHandler] batch not active anymore") + return ctx, errors.Create(errors.NotActiveError) + } + defer batchData.Set(batchData, bot.Bunt) + + from := LoadUser(ctx) + ResetUserState(from, bot) + + // Re-check balance before executing + balance, err := bot.GetUserAvailableBalance(from) + if err != nil { + log.Errorln(err.Error()) + bot.tryEditMessage(ctx.Callback().Message, "āŒ Could not verify balance. Batch cancelled.", &tb.ReplyMarkup{}) + batchData.Inactivate(batchData, bot.Bunt) + return ctx, err + } + if balance < batchData.TotalAmount { + bot.tryEditMessage(ctx.Callback().Message, fmt.Sprintf("āŒ *Insufficient balance*\n\nRequired: %s\nAvailable: %s\n\nBatch cancelled.", + thirdparty.FormatSatsWithLKR(batchData.TotalAmount), + thirdparty.FormatSatsWithLKR(balance)), &tb.ReplyMarkup{}) + batchData.Inactivate(batchData, bot.Bunt) + return ctx, fmt.Errorf("insufficient balance for batch send") + } + + // Update message to show processing + bot.tryEditMessage(ctx.Callback().Message, "ā³ *Processing batch send...*", &tb.ReplyMarkup{}) + + // Execute transfers sequentially + var succeeded []string + var failed []string + var totalSent int64 + + for _, entry := range batchData.Entries { + to, err := GetLnbitsUser(&tb.User{ID: entry.ToTelegramId, Username: entry.ToUsername}, *bot) + if err != nil { + log.Errorf("[sendbatch] failed to get user @%s: %s", entry.ToUsername, err.Error()) + failed = append(failed, fmt.Sprintf("@%s — user error", entry.ToUsername)) + // Stop on first failure to prevent partial state issues + for _, remaining := range batchData.Entries[len(succeeded)+len(failed):] { + failed = append(failed, fmt.Sprintf("@%s — skipped", remaining.ToUsername)) + } + break + } + + fromUserStr := GetUserStr(from.Telegram) + toUserStr := GetUserStr(to.Telegram) + transactionMemo := fmt.Sprintf("šŸ“¦ Batch send from %s to %s.", fromUserStr, toUserStr) + + t := NewTransaction(bot, from, to, entry.Amount, TransactionType("sendbatch")) + t.Memo = transactionMemo + + success, err := t.Send() + if !success || err != nil { + log.Errorf("[sendbatch] transfer to @%s failed: %s", entry.ToUsername, err.Error()) + if bot.ErrorLogger != nil { + bot.ErrorLogger.LogTransactionError(err, "sendbatch", entry.Amount, from.Telegram, to.Telegram) + } + failed = append(failed, fmt.Sprintf("@%s — transfer failed", entry.ToUsername)) + // Stop on failure — remaining are skipped + for _, remaining := range batchData.Entries[len(succeeded)+len(failed):] { + failed = append(failed, fmt.Sprintf("@%s — skipped", remaining.ToUsername)) + } + break + } + + totalSent += entry.Amount + succeeded = append(succeeded, fmt.Sprintf("@%s — %s", entry.ToUsername, thirdparty.FormatSatsWithLKR(entry.Amount))) + + // Notify recipient + fromUserStrMd := GetUserStrMd(from.Telegram) + notifyMsg := fmt.Sprintf("šŸ“¦ You received %s from %s", thirdparty.FormatSatsWithLKR(entry.Amount), fromUserStrMd) + bot.trySendMessage(to.Telegram, notifyMsg) + + // Send memo to recipient if present + if entry.Memo != "" { + bot.trySendMessage(to.Telegram, fmt.Sprintf("āœ‰ļø %s", str.MarkdownEscape(entry.Memo))) + } + + log.Infof("[šŸ“¦ sendbatch] Send from %s to %s (%d sat).", fromUserStr, toUserStr, entry.Amount) + } + + batchData.Inactivate(batchData, bot.Bunt) + + // Build result message + var resultLines []string + resultLines = append(resultLines, "šŸ“¦ *Batch Send Complete*\n") + + if len(succeeded) > 0 { + resultLines = append(resultLines, "*āœ… Succeeded:*") + for _, s := range succeeded { + resultLines = append(resultLines, s) + } + } + + if len(failed) > 0 { + resultLines = append(resultLines, "\n*āŒ Failed:*") + for _, f := range failed { + resultLines = append(resultLines, f) + } + } + + resultLines = append(resultLines, fmt.Sprintf("\n*Total sent:* %s", thirdparty.FormatSatsWithLKR(totalSent))) + + resultMsg := strings.Join(resultLines, "\n") + + bot.tryDeleteMessage(ctx.Callback().Message) + bot.trySendMessage(ctx.Callback().Sender, resultMsg) + + return ctx, nil +} + +// cancelSendBatchHandler cancels the batch send +func (bot *TipBot) cancelSendBatchHandler(ctx intercept.Context) (intercept.Context, error) { + c := ctx.Callback() + user := LoadUser(ctx) + ResetUserState(user, bot) + + tx := &SendBatchData{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("[cancelSendBatchHandler] %s", err.Error()) + return ctx, err + } + + batchData := sn.(*SendBatchData) + // Only the sender can cancel + if batchData.From.Telegram.ID != c.Sender.ID { + return ctx, errors.Create(errors.UnknownError) + } + + bot.tryDeleteMessage(c) + bot.trySendMessage(c.Message.Chat, "🚫 Batch send cancelled.") + batchData.Inactivate(batchData, bot.Bunt) + + return ctx, nil +} From 36c5acd1c16f929182f530261c5a6015249beae3 Mon Sep 17 00:00:00 2001 From: Buddhi Thilakshana Date: Wed, 25 Mar 2026 22:57:02 +0530 Subject: [PATCH 2/7] feat: update bot commands and translations to include /sendbatch functionality --- .gitignore | 8 +++++++- botfather-setcommands.txt | 3 ++- translations/en.toml | 5 +++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index c965465a..3d1df7ce 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,10 @@ data/* LightningTipBot LightningTipBot.exe BitcoinDeepaBot -test_pay_api.sh \ No newline at end of file +test_pay_api.sh +ANALYTICS_API.md +.gitignore +ANALYTICS_QUICKSTART.md +.claude/settings.local.json +analytics_requirements.txt +analytics_export.py diff --git a/botfather-setcommands.txt b/botfather-setcommands.txt index 8afb8420..5c401835 100644 --- a/botfather-setcommands.txt +++ b/botfather-setcommands.txt @@ -6,10 +6,11 @@ 4 - Paste this: help - Read the help. -balance - Check balance. +balance - Check balance. transactions - List transactions tip - Reply to a message to tip: /tip 50 send - Send funds to a user: /send 100 @LightningTipBot +sendbatch - Send to multiple users: /sendbatch invoice - Receive with Lightning: /invoice 1000 pay - Pay with Lightning: /pay lnbc10n1ps... donate - Donate: /donate 1000 diff --git a/translations/en.toml b/translations/en.toml index 109f74c1..45d2cff5 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -72,6 +72,7 @@ _This bot is a Bitcoin Lightning wallet that can sends tips on Telegram. To tip, */tip* šŸ… Reply to a message to tip: `/tip []` */balance* šŸ‘‘ Check your balance: `/balance` */send* šŸ’ø Send funds to a user: `/send @user or user@ln.tips []` +*/sendbatch* šŸ’ø Send to multiple users: `/sendbatch [memo]` */invoice* āš”ļø Receive with Lightning: `/invoice []` */pay* āš”ļø Pay with Lightning: `/pay ` */transactions* šŸ“Š List transactions @@ -203,7 +204,7 @@ invoiceHelpText = """šŸ“– Oops, that didn't work. %s *Usage:* `/invoice []` *Example:* `/invoice 1000 Thank you!`""" -invoicePaidText = """āœ… Invoice paid.""" +invoicePaidText = """āœ… Invoice paid.""" # PAY @@ -395,7 +396,7 @@ For admins (in group chat): `/group ticket `\nExample: `/group tic šŸŽŸ *Private tickets* -Sell tickets for your _private Group_ and get rid of spam bots. +Sell tickets for your _private Group_ and get rid of spam bots. *Instructions for group admins:* From 6f2fd8773d10d340e01e1e339b99fc55edec50d3 Mon Sep 17 00:00:00 2001 From: Buddhi Thilakshana Date: Wed, 25 Mar 2026 23:05:20 +0530 Subject: [PATCH 3/7] feat: enhance batch send functionality to support LKR amounts and display formatted amounts --- internal/telegram/sendbatch.go | 48 ++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/internal/telegram/sendbatch.go b/internal/telegram/sendbatch.go index e1b79232..c6adfa64 100644 --- a/internal/telegram/sendbatch.go +++ b/internal/telegram/sendbatch.go @@ -78,9 +78,10 @@ func parseBatchEntries(text string) (entries []parsedEntry, sharedMemo string, e } type parsedEntry struct { - Amount int64 - Username string // without @ - Memo string + Amount int64 + Username string // without @ + Memo string + DisplayAmount string } func parseRecipientLine(line string, sharedMemo string) (parsedEntry, error) { @@ -89,10 +90,34 @@ func parseRecipientLine(line string, sharedMemo string) (parsedEntry, error) { return parsedEntry{}, fmt.Errorf("expected: @ [memo]") } - // Parse amount - amount, err := strconv.ParseInt(fields[0], 10, 64) - if err != nil || amount < 1 { - return parsedEntry{}, fmt.Errorf("invalid amount: %s", fields[0]) + var amount int64 + var displayAmount string + amountStr := strings.ReplaceAll(fields[0], ",", "") + // Check for lkr suffix + if strings.HasSuffix(strings.ToLower(amountStr), "lkr") { + lkrPerSat, _, err := thirdparty.GetSatPrice() + if err != nil { + return parsedEntry{}, fmt.Errorf("failed to get LKR price: %v", err) + } + if lkrPerSat <= 0 { + return parsedEntry{}, fmt.Errorf("invalid LKR price") + } + + valStr := strings.TrimSuffix(strings.ToLower(amountStr), "lkr") + val, err := strconv.ParseFloat(valStr, 64) + if err != nil || val <= 0 { + return parsedEntry{}, fmt.Errorf("invalid amount: %s", fields[0]) + } + amount = int64(val / lkrPerSat) + displayAmount = fmt.Sprintf("`%s` (%d sats)", fields[0], amount) + } else { + // Parse amount as sats + parsedAmount, err := strconv.ParseInt(amountStr, 10, 64) + if err != nil || parsedAmount < 1 { + return parsedEntry{}, fmt.Errorf("invalid amount: %s", fields[0]) + } + amount = parsedAmount + displayAmount = fmt.Sprintf("`%d` sats", amount) } // Parse username @@ -113,9 +138,10 @@ func parseRecipientLine(line string, sharedMemo string) (parsedEntry, error) { } return parsedEntry{ - Amount: amount, - Username: username, - Memo: memo, + Amount: amount, + Username: username, + Memo: memo, + DisplayAmount: displayAmount, }, nil } @@ -164,7 +190,7 @@ func (bot *TipBot) sendbatchHandler(ctx intercept.Context) (intercept.Context, e totalAmount += p.Amount // Build confirmation line - line := fmt.Sprintf("`%d` sats → @%s", p.Amount, str.MarkdownEscape(p.Username)) + line := fmt.Sprintf("%s → @%s", p.DisplayAmount, str.MarkdownEscape(p.Username)) if p.Memo != "" { line += fmt.Sprintf(" _%s_", str.MarkdownEscape(p.Memo)) } From eb59f21a5ba034e7b65a3d2567ed743d62bfbd5f Mon Sep 17 00:00:00 2001 From: Buddhi Thilakshana Date: Wed, 25 Mar 2026 23:49:14 +0530 Subject: [PATCH 4/7] feat: enhance /sendbatch command error handling and usage examples in messages --- internal/telegram/sendbatch.go | 7 ++++++- translations/en.toml | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/telegram/sendbatch.go b/internal/telegram/sendbatch.go index c6adfa64..352f34cc 100644 --- a/internal/telegram/sendbatch.go +++ b/internal/telegram/sendbatch.go @@ -158,7 +158,12 @@ func (bot *TipBot) sendbatchHandler(ctx intercept.Context) (intercept.Context, e // Parse the batch entries parsed, sharedMemo, err := parseBatchEntries(ctx.Message().Text) if err != nil { - bot.trySendMessage(ctx.Message().Sender, fmt.Sprintf("āŒ *Batch Send Error*\n\n%s\n\n*Usage:*\n```\n/sendbatch [shared memo]\n @user [memo]\n @user [memo]\n```", str.MarkdownEscape(err.Error()))) + helpMsg := fmt.Sprintf("āŒ *Batch Send Error*\n\n%s\n\n", str.MarkdownEscape(err.Error())) + helpMsg += "*Usage:*\n`/sendbatch [shared memo]`\n` @user [memo]`\n` @user [memo]`\n\n" + helpMsg += "*Example 1 (Shared Memo):*\n`/sendbatch Salary 2026`\n`1000 @alice`\n`500 @bob`\n\n" + helpMsg += "*Example 2 (Individual Memos & LKR):*\n`/sendbatch`\n`1000 @alice Pizza`\n`500lkr @bob Coffee`" + + bot.trySendMessage(ctx.Message().Sender, helpMsg) return ctx, errors.Create(errors.InvalidSyntaxError) } diff --git a/translations/en.toml b/translations/en.toml index 45d2cff5..17c37862 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -72,7 +72,7 @@ _This bot is a Bitcoin Lightning wallet that can sends tips on Telegram. To tip, */tip* šŸ… Reply to a message to tip: `/tip []` */balance* šŸ‘‘ Check your balance: `/balance` */send* šŸ’ø Send funds to a user: `/send @user or user@ln.tips []` -*/sendbatch* šŸ’ø Send to multiple users: `/sendbatch [memo]` +*/sendbatch* šŸ’ø Send to multiple users: `/sendbatch [memo]` then list recipients (e.g. `100 @user`). */invoice* āš”ļø Receive with Lightning: `/invoice []` */pay* āš”ļø Pay with Lightning: `/pay ` */transactions* šŸ“Š List transactions From 9ac024f7db4cbff954f8de32435082e08a6a75e1 Mon Sep 17 00:00:00 2001 From: Buddhi Thilakshana Date: Wed, 25 Mar 2026 23:53:05 +0530 Subject: [PATCH 5/7] feat: update /sendbatch command usage instructions for clarity and formatting --- translations/en.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/translations/en.toml b/translations/en.toml index 17c37862..67d73ff6 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -72,7 +72,10 @@ _This bot is a Bitcoin Lightning wallet that can sends tips on Telegram. To tip, */tip* šŸ… Reply to a message to tip: `/tip []` */balance* šŸ‘‘ Check your balance: `/balance` */send* šŸ’ø Send funds to a user: `/send @user or user@ln.tips []` -*/sendbatch* šŸ’ø Send to multiple users: `/sendbatch [memo]` then list recipients (e.g. `100 @user`). +*/sendbatch* šŸ’ø Send to multiple users: +`/sendbatch [memo]` +` @user1 [memo]` +` @user2 [memo]` */invoice* āš”ļø Receive with Lightning: `/invoice []` */pay* āš”ļø Pay with Lightning: `/pay ` */transactions* šŸ“Š List transactions From 865cf9e751269c07b571b88f4bfb9d201d237d13 Mon Sep 17 00:00:00 2001 From: Buddhi Thilakshana Date: Wed, 25 Mar 2026 23:59:15 +0530 Subject: [PATCH 6/7] feat: update /sendbatch command to support LKR amounts in user messages --- translations/en.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/translations/en.toml b/translations/en.toml index 67d73ff6..8d03600f 100644 --- a/translations/en.toml +++ b/translations/en.toml @@ -75,7 +75,7 @@ _This bot is a Bitcoin Lightning wallet that can sends tips on Telegram. To tip, */sendbatch* šŸ’ø Send to multiple users: `/sendbatch [memo]` ` @user1 [memo]` -` @user2 [memo]` +`lkr @user2 [memo]` (converts LKR to sats) */invoice* āš”ļø Receive with Lightning: `/invoice []` */pay* āš”ļø Pay with Lightning: `/pay ` */transactions* šŸ“Š List transactions From 04a0d1f56f0c645fd6ea6eb1f37ccbc97764067f Mon Sep 17 00:00:00 2001 From: Buddhi Thilakshana Date: Thu, 26 Mar 2026 00:11:29 +0530 Subject: [PATCH 7/7] fix: preserve user memo on batch transaction records Use entry.Memo (shared or per-line) as the transaction memo when present, falling back to the synthetic batch label only when empty. This ensures memos appear in transaction history and invoice metadata. Addresses CodeRabbit review comment #4. --- internal/telegram/sendbatch.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/telegram/sendbatch.go b/internal/telegram/sendbatch.go index 352f34cc..3ce97387 100644 --- a/internal/telegram/sendbatch.go +++ b/internal/telegram/sendbatch.go @@ -312,10 +312,13 @@ func (bot *TipBot) confirmSendBatchHandler(ctx intercept.Context) (intercept.Con fromUserStr := GetUserStr(from.Telegram) toUserStr := GetUserStr(to.Telegram) - transactionMemo := fmt.Sprintf("šŸ“¦ Batch send from %s to %s.", fromUserStr, toUserStr) t := NewTransaction(bot, from, to, entry.Amount, TransactionType("sendbatch")) - t.Memo = transactionMemo + if entry.Memo != "" { + t.Memo = entry.Memo + } else { + t.Memo = fmt.Sprintf("šŸ“¦ Batch send from %s to %s.", fromUserStr, toUserStr) + } success, err := t.Send() if !success || err != nil {