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"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
|
@ -12,6 +13,8 @@ import (
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const alertsPageSize = 5
|
||||||
|
|
||||||
// Usecase defines the business logic operations required by the bot.
|
// Usecase defines the business logic operations required by the bot.
|
||||||
type Usecase interface {
|
type Usecase interface {
|
||||||
RegisterNewUser(ctx context.Context, user *entities.User) error
|
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) {
|
func (b *Bot) handleCallback(ctx context.Context, cb *tgbotapi.CallbackQuery) {
|
||||||
tgID := entities.TelegramID(cb.From.ID)
|
tgID := entities.TelegramID(cb.From.ID)
|
||||||
chatID := cb.Message.Chat.ID
|
chatID := cb.Message.Chat.ID
|
||||||
|
messageID := cb.Message.MessageID
|
||||||
|
|
||||||
// Acknowledge the callback immediately so the button stops spinning.
|
// Acknowledge the callback immediately so the button stops spinning.
|
||||||
b.api.Request(tgbotapi.NewCallback(cb.ID, "")) //nolint:errcheck
|
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:"):
|
case strings.HasPrefix(data, "instrument:"):
|
||||||
instrID := entities.InstrumentID(strings.TrimPrefix(data, "instrument:"))
|
instrID := entities.InstrumentID(strings.TrimPrefix(data, "instrument:"))
|
||||||
b.handleInstrumentSelected(ctx, tgID, chatID, instrID)
|
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:"):
|
case strings.HasPrefix(data, "edit_alert:"):
|
||||||
alertID := entities.AlertID(strings.TrimPrefix(data, "edit_alert:"))
|
alertID := entities.AlertID(strings.TrimPrefix(data, "edit_alert:"))
|
||||||
b.handleEditAlertStart(tgID, chatID, alertID)
|
b.handleEditAlertStart(tgID, chatID, alertID)
|
||||||
|
|
||||||
case strings.HasPrefix(data, "remove_alert:"):
|
case strings.HasPrefix(data, "remove_alert:"):
|
||||||
alertID := entities.AlertID(strings.TrimPrefix(data, "remove_alert:"))
|
// format: remove_alert:<alertID>:<page>
|
||||||
b.handleRemoveAlertConfirm(chatID, alertID)
|
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:"):
|
case strings.HasPrefix(data, "confirm_remove:"):
|
||||||
alertID := entities.AlertID(strings.TrimPrefix(data, "confirm_remove:"))
|
// format: confirm_remove:<alertID>:<page>
|
||||||
b.handleRemoveAlertDo(ctx, chatID, alertID)
|
rest := strings.TrimPrefix(data, "confirm_remove:")
|
||||||
case data == "cancel_remove":
|
idx := strings.LastIndex(rest, ":")
|
||||||
b.sendMenu(chatID, "Removal cancelled.")
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
alerts, err := b.usecase.Alerts(ctx, user.ID, 0, 20)
|
alerts, err := b.usecase.Alerts(ctx, user.ID, 0, 100)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.log.Error("failed to get alerts", "err", err)
|
b.log.Error("failed to get alerts", "err", err)
|
||||||
b.sendMenu(chatID, "Failed to load alerts.")
|
b.sendMenu(chatID, "Failed to load alerts.")
|
||||||
|
|
@ -307,22 +341,119 @@ func (b *Bot) cmdMyAlerts(ctx context.Context, tgID entities.TelegramID, chatID
|
||||||
return
|
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 {
|
// buildAlertsPage constructs the message text and inline keyboard for a paginated alerts list.
|
||||||
text := fmt.Sprintf("%s/%s — %s %s",
|
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.BaseCurrency,
|
||||||
alert.Instrument.QuoteCurrency,
|
alert.Instrument.QuoteCurrency,
|
||||||
alert.Condition,
|
alert.Condition,
|
||||||
alert.Price.String(),
|
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)),
|
var rows [][]tgbotapi.InlineKeyboardButton
|
||||||
)
|
|
||||||
msg := tgbotapi.NewMessage(chatID, text)
|
// One button per alert on this page.
|
||||||
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(row)
|
var alertRow []tgbotapi.InlineKeyboardButton
|
||||||
b.sendMsg(msg)
|
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()))
|
b.sendMenu(chatID, fmt.Sprintf("Alert price updated to %s.", price.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleRemoveAlertConfirm asks the user to confirm deletion.
|
// handleRemoveAlertConfirm edits the message to show a confirmation prompt.
|
||||||
func (b *Bot) handleRemoveAlertConfirm(chatID int64, alertID entities.AlertID) {
|
func (b *Bot) handleRemoveAlertConfirm(chatID int64, messageID int, alertID entities.AlertID, page int) {
|
||||||
row := tgbotapi.NewInlineKeyboardRow(
|
kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Yes, remove", fmt.Sprintf("confirm_remove:%s", alertID)),
|
tgbotapi.NewInlineKeyboardButtonData("Yes, remove", fmt.Sprintf("confirm_remove:%s:%d", alertID, page)),
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Cancel", "cancel_remove"),
|
tgbotapi.NewInlineKeyboardButtonData("Cancel", fmt.Sprintf("cancel_remove:%s:%d", alertID, page)),
|
||||||
)
|
))
|
||||||
msg := tgbotapi.NewMessage(chatID, "Are you sure you want to remove this alert?")
|
edit := tgbotapi.NewEditMessageTextAndMarkup(chatID, messageID, "Are you sure you want to remove this alert?", kb)
|
||||||
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(row)
|
if _, err := b.api.Send(edit); err != nil {
|
||||||
b.sendMsg(msg)
|
b.log.Error("failed to edit message", "err", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleRemoveAlertDo deletes the alert after confirmation.
|
// handleRemoveAlertDo deletes the alert and refreshes the list in the same message.
|
||||||
func (b *Bot) handleRemoveAlertDo(ctx context.Context, chatID int64, alertID entities.AlertID) {
|
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 {
|
if err := b.usecase.RemoveAlert(ctx, alertID); err != nil {
|
||||||
b.log.Error("failed to remove alert", "alert_id", alertID, "err", err)
|
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
|
return
|
||||||
}
|
}
|
||||||
b.alerter.RemoveAlert(alertID)
|
b.alerter.RemoveAlert(alertID)
|
||||||
b.sendMenu(chatID, "Alert removed.")
|
b.handleAlertsPage(ctx, tgID, chatID, messageID, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Helpers ---
|
// --- 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)
|
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()
|
queryString = query.Encode()
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("req:", b.cfg.BaseURL+endPoint+"?"+queryString)
|
|
||||||
|
|
||||||
// make request
|
// make request
|
||||||
request, err := http.NewRequest("GET", b.cfg.BaseURL+endPoint+"?"+queryString, nil)
|
request, err := http.NewRequest("GET", b.cfg.BaseURL+endPoint+"?"+queryString, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ select i.id, c_base.symbol, c_quote.symbol
|
||||||
from instrument i
|
from instrument i
|
||||||
join currency c_base on c_base.id = i.base_currency_id
|
join currency c_base on c_base.id = i.base_currency_id
|
||||||
join currency c_quote on c_quote.id = i.quoted_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`
|
offset $1 limit $2`
|
||||||
|
|
||||||
func (p *Postgresql) InstrumentList(ctx context.Context, offset, limit int) ([]entities.Instrument, error) {
|
func (p *Postgresql) InstrumentList(ctx context.Context, offset, limit int) ([]entities.Instrument, error) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue