fix visual
This commit is contained in:
parent
7eb4977b99
commit
30a7f1b68c
3 changed files with 171 additions and 33 deletions
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
|
|
@ -12,6 +13,8 @@ import (
|
|||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
const alertsPageSize = 5
|
||||
|
||||
// Usecase defines the business logic operations required by the bot.
|
||||
type Usecase interface {
|
||||
RegisterNewUser(ctx context.Context, user *entities.User) error
|
||||
|
|
@ -199,6 +202,7 @@ func (b *Bot) handleCommand(ctx context.Context, tgID entities.TelegramID, chatI
|
|||
func (b *Bot) handleCallback(ctx context.Context, cb *tgbotapi.CallbackQuery) {
|
||||
tgID := entities.TelegramID(cb.From.ID)
|
||||
chatID := cb.Message.Chat.ID
|
||||
messageID := cb.Message.MessageID
|
||||
|
||||
// Acknowledge the callback immediately so the button stops spinning.
|
||||
b.api.Request(tgbotapi.NewCallback(cb.ID, "")) //nolint:errcheck
|
||||
|
|
@ -208,17 +212,47 @@ func (b *Bot) handleCallback(ctx context.Context, cb *tgbotapi.CallbackQuery) {
|
|||
case strings.HasPrefix(data, "instrument:"):
|
||||
instrID := entities.InstrumentID(strings.TrimPrefix(data, "instrument:"))
|
||||
b.handleInstrumentSelected(ctx, tgID, chatID, instrID)
|
||||
|
||||
case strings.HasPrefix(data, "alerts_page:"), strings.HasPrefix(data, "alerts_back:"):
|
||||
rest := data[strings.Index(data, ":")+1:]
|
||||
page, _ := strconv.Atoi(rest)
|
||||
b.handleAlertsPage(ctx, tgID, chatID, messageID, page)
|
||||
|
||||
case strings.HasPrefix(data, "alert_select:"):
|
||||
// format: alert_select:<alertID>:<page>
|
||||
rest := strings.TrimPrefix(data, "alert_select:")
|
||||
idx := strings.LastIndex(rest, ":")
|
||||
alertID := entities.AlertID(rest[:idx])
|
||||
page, _ := strconv.Atoi(rest[idx+1:])
|
||||
b.handleAlertSelect(ctx, chatID, messageID, alertID, page)
|
||||
|
||||
case strings.HasPrefix(data, "edit_alert:"):
|
||||
alertID := entities.AlertID(strings.TrimPrefix(data, "edit_alert:"))
|
||||
b.handleEditAlertStart(tgID, chatID, alertID)
|
||||
|
||||
case strings.HasPrefix(data, "remove_alert:"):
|
||||
alertID := entities.AlertID(strings.TrimPrefix(data, "remove_alert:"))
|
||||
b.handleRemoveAlertConfirm(chatID, alertID)
|
||||
// format: remove_alert:<alertID>:<page>
|
||||
rest := strings.TrimPrefix(data, "remove_alert:")
|
||||
idx := strings.LastIndex(rest, ":")
|
||||
alertID := entities.AlertID(rest[:idx])
|
||||
page, _ := strconv.Atoi(rest[idx+1:])
|
||||
b.handleRemoveAlertConfirm(chatID, messageID, alertID, page)
|
||||
|
||||
case strings.HasPrefix(data, "confirm_remove:"):
|
||||
alertID := entities.AlertID(strings.TrimPrefix(data, "confirm_remove:"))
|
||||
b.handleRemoveAlertDo(ctx, chatID, alertID)
|
||||
case data == "cancel_remove":
|
||||
b.sendMenu(chatID, "Removal cancelled.")
|
||||
// format: confirm_remove:<alertID>:<page>
|
||||
rest := strings.TrimPrefix(data, "confirm_remove:")
|
||||
idx := strings.LastIndex(rest, ":")
|
||||
alertID := entities.AlertID(rest[:idx])
|
||||
page, _ := strconv.Atoi(rest[idx+1:])
|
||||
b.handleRemoveAlertDo(ctx, tgID, chatID, messageID, alertID, page)
|
||||
|
||||
case strings.HasPrefix(data, "cancel_remove:"):
|
||||
// format: cancel_remove:<alertID>:<page>
|
||||
rest := strings.TrimPrefix(data, "cancel_remove:")
|
||||
idx := strings.LastIndex(rest, ":")
|
||||
alertID := entities.AlertID(rest[:idx])
|
||||
page, _ := strconv.Atoi(rest[idx+1:])
|
||||
b.handleAlertSelect(ctx, chatID, messageID, alertID, page)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -296,7 +330,7 @@ func (b *Bot) cmdMyAlerts(ctx context.Context, tgID entities.TelegramID, chatID
|
|||
return
|
||||
}
|
||||
|
||||
alerts, err := b.usecase.Alerts(ctx, user.ID, 0, 20)
|
||||
alerts, err := b.usecase.Alerts(ctx, user.ID, 0, 100)
|
||||
if err != nil {
|
||||
b.log.Error("failed to get alerts", "err", err)
|
||||
b.sendMenu(chatID, "Failed to load alerts.")
|
||||
|
|
@ -307,22 +341,119 @@ func (b *Bot) cmdMyAlerts(ctx context.Context, tgID entities.TelegramID, chatID
|
|||
return
|
||||
}
|
||||
|
||||
b.sendMenu(chatID, fmt.Sprintf("Your active alerts (%d):", len(alerts)))
|
||||
text, kb := buildAlertsPage(alerts, 0)
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
msg.ReplyMarkup = kb
|
||||
b.sendMsg(msg)
|
||||
}
|
||||
|
||||
for _, alert := range alerts {
|
||||
text := fmt.Sprintf("%s/%s — %s %s",
|
||||
// buildAlertsPage constructs the message text and inline keyboard for a paginated alerts list.
|
||||
func buildAlertsPage(alerts []entities.Alert, page int) (string, tgbotapi.InlineKeyboardMarkup) {
|
||||
total := len(alerts)
|
||||
totalPages := (total + alertsPageSize - 1) / alertsPageSize
|
||||
|
||||
start := page * alertsPageSize
|
||||
end := start + alertsPageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
pageAlerts := alerts[start:end]
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "Your active alerts (page %d/%d):\n\n", page+1, totalPages)
|
||||
for i, alert := range pageAlerts {
|
||||
fmt.Fprintf(&sb, "%d. %s/%s %s %s\n",
|
||||
start+i+1,
|
||||
alert.Instrument.BaseCurrency,
|
||||
alert.Instrument.QuoteCurrency,
|
||||
alert.Condition,
|
||||
alert.Price.String(),
|
||||
)
|
||||
row := tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Edit price", fmt.Sprintf("edit_alert:%s", alert.ID)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Remove", fmt.Sprintf("remove_alert:%s", alert.ID)),
|
||||
)
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(row)
|
||||
b.sendMsg(msg)
|
||||
}
|
||||
|
||||
var rows [][]tgbotapi.InlineKeyboardButton
|
||||
|
||||
// One button per alert on this page.
|
||||
var alertRow []tgbotapi.InlineKeyboardButton
|
||||
for i, alert := range pageAlerts {
|
||||
alertRow = append(alertRow, tgbotapi.NewInlineKeyboardButtonData(
|
||||
strconv.Itoa(start+i+1),
|
||||
fmt.Sprintf("alert_select:%s:%d", alert.ID, page),
|
||||
))
|
||||
}
|
||||
if len(alertRow) > 0 {
|
||||
rows = append(rows, alertRow)
|
||||
}
|
||||
|
||||
// Prev / Next navigation.
|
||||
var navRow []tgbotapi.InlineKeyboardButton
|
||||
if page > 0 {
|
||||
navRow = append(navRow, tgbotapi.NewInlineKeyboardButtonData("◀", fmt.Sprintf("alerts_page:%d", page-1)))
|
||||
}
|
||||
if end < total {
|
||||
navRow = append(navRow, tgbotapi.NewInlineKeyboardButtonData("▶", fmt.Sprintf("alerts_page:%d", page+1)))
|
||||
}
|
||||
if len(navRow) > 0 {
|
||||
rows = append(rows, navRow)
|
||||
}
|
||||
|
||||
return sb.String(), tgbotapi.NewInlineKeyboardMarkup(rows...)
|
||||
}
|
||||
|
||||
// handleAlertsPage re-fetches alerts and edits the existing message to show the requested page.
|
||||
func (b *Bot) handleAlertsPage(ctx context.Context, tgID entities.TelegramID, chatID int64, messageID int, page int) {
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
alerts, err := b.usecase.Alerts(ctx, user.ID, 0, 100)
|
||||
if err != nil {
|
||||
b.log.Error("failed to get alerts", "err", err)
|
||||
return
|
||||
}
|
||||
if len(alerts) == 0 {
|
||||
b.editMsgText(chatID, messageID, "You have no active alerts.")
|
||||
return
|
||||
}
|
||||
|
||||
// Clamp page to valid range (e.g. after last alert on a page is removed).
|
||||
totalPages := (len(alerts) + alertsPageSize - 1) / alertsPageSize
|
||||
if page >= totalPages {
|
||||
page = totalPages - 1
|
||||
}
|
||||
|
||||
text, kb := buildAlertsPage(alerts, page)
|
||||
edit := tgbotapi.NewEditMessageTextAndMarkup(chatID, messageID, text, kb)
|
||||
if _, err := b.api.Send(edit); err != nil {
|
||||
b.log.Error("failed to edit message", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleAlertSelect edits the message to show the chosen alert with Edit / Remove / Back buttons.
|
||||
func (b *Bot) handleAlertSelect(ctx context.Context, chatID int64, messageID int, alertID entities.AlertID, page int) {
|
||||
alert, err := b.usecase.Alert(ctx, alertID)
|
||||
if err != nil {
|
||||
b.log.Error("failed to get alert", "alert_id", alertID, "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
text := fmt.Sprintf("%s/%s\nCondition: %s %s",
|
||||
alert.Instrument.BaseCurrency,
|
||||
alert.Instrument.QuoteCurrency,
|
||||
alert.Condition,
|
||||
alert.Price.String(),
|
||||
)
|
||||
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Edit price", fmt.Sprintf("edit_alert:%s", alertID)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Remove", fmt.Sprintf("remove_alert:%s:%d", alertID, page)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("◀ Back", fmt.Sprintf("alerts_back:%d", page)),
|
||||
))
|
||||
|
||||
edit := tgbotapi.NewEditMessageTextAndMarkup(chatID, messageID, text, kb)
|
||||
if _, err := b.api.Send(edit); err != nil {
|
||||
b.log.Error("failed to edit message", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -470,26 +601,27 @@ func (b *Bot) handleEditAlertPrice(ctx context.Context, tgID entities.TelegramID
|
|||
b.sendMenu(chatID, fmt.Sprintf("Alert price updated to %s.", price.String()))
|
||||
}
|
||||
|
||||
// handleRemoveAlertConfirm asks the user to confirm deletion.
|
||||
func (b *Bot) handleRemoveAlertConfirm(chatID int64, alertID entities.AlertID) {
|
||||
row := tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Yes, remove", fmt.Sprintf("confirm_remove:%s", alertID)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Cancel", "cancel_remove"),
|
||||
)
|
||||
msg := tgbotapi.NewMessage(chatID, "Are you sure you want to remove this alert?")
|
||||
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(row)
|
||||
b.sendMsg(msg)
|
||||
// handleRemoveAlertConfirm edits the message to show a confirmation prompt.
|
||||
func (b *Bot) handleRemoveAlertConfirm(chatID int64, messageID int, alertID entities.AlertID, page int) {
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Yes, remove", fmt.Sprintf("confirm_remove:%s:%d", alertID, page)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Cancel", fmt.Sprintf("cancel_remove:%s:%d", alertID, page)),
|
||||
))
|
||||
edit := tgbotapi.NewEditMessageTextAndMarkup(chatID, messageID, "Are you sure you want to remove this alert?", kb)
|
||||
if _, err := b.api.Send(edit); err != nil {
|
||||
b.log.Error("failed to edit message", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleRemoveAlertDo deletes the alert after confirmation.
|
||||
func (b *Bot) handleRemoveAlertDo(ctx context.Context, chatID int64, alertID entities.AlertID) {
|
||||
// handleRemoveAlertDo deletes the alert and refreshes the list in the same message.
|
||||
func (b *Bot) handleRemoveAlertDo(ctx context.Context, tgID entities.TelegramID, chatID int64, messageID int, alertID entities.AlertID, page int) {
|
||||
if err := b.usecase.RemoveAlert(ctx, alertID); err != nil {
|
||||
b.log.Error("failed to remove alert", "alert_id", alertID, "err", err)
|
||||
b.sendMenu(chatID, "Failed to remove alert.")
|
||||
b.editMsgText(chatID, messageID, "Failed to remove alert.")
|
||||
return
|
||||
}
|
||||
b.alerter.RemoveAlert(alertID)
|
||||
b.sendMenu(chatID, "Alert removed.")
|
||||
b.handleAlertsPage(ctx, tgID, chatID, messageID, page)
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
|
@ -557,3 +689,11 @@ func (b *Bot) sendMsg(msg tgbotapi.MessageConfig) {
|
|||
b.log.Error("failed to send message", "chat_id", msg.ChatID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// editMsgText edits only the text of an existing message (removes inline keyboard).
|
||||
func (b *Bot) editMsgText(chatID int64, messageID int, text string) {
|
||||
edit := tgbotapi.NewEditMessageText(chatID, messageID, text)
|
||||
if _, err := b.api.Send(edit); err != nil {
|
||||
b.log.Error("failed to edit message", "err", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,8 +79,6 @@ func (b *Bybit) getRequest(ctx context.Context, endPoint string, params any) ([]
|
|||
queryString = query.Encode()
|
||||
}
|
||||
|
||||
fmt.Println("req:", b.cfg.BaseURL+endPoint+"?"+queryString)
|
||||
|
||||
// make request
|
||||
request, err := http.NewRequest("GET", b.cfg.BaseURL+endPoint+"?"+queryString, nil)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ select i.id, c_base.symbol, c_quote.symbol
|
|||
from instrument i
|
||||
join currency c_base on c_base.id = i.base_currency_id
|
||||
join currency c_quote on c_quote.id = i.quoted_currency_id
|
||||
order by i.id
|
||||
order by i.id desc
|
||||
offset $1 limit $2`
|
||||
|
||||
func (p *Postgresql) InstrumentList(ctx context.Context, offset, limit int) ([]entities.Instrument, error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue