fix visual

This commit is contained in:
yash 2026-04-27 19:57:20 +03:00
parent 7eb4977b99
commit 30a7f1b68c
3 changed files with 171 additions and 33 deletions

View file

@ -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)
}
}

View file

@ -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 {

View file

@ -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) {