user instruments
This commit is contained in:
parent
dd03cae0f3
commit
abb2411af7
9 changed files with 494 additions and 48 deletions
|
|
@ -14,14 +14,19 @@ import (
|
|||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
const alertsPageSize = 5
|
||||
const (
|
||||
alertsPageSize = 5
|
||||
instrPageSize = 4
|
||||
)
|
||||
|
||||
// Usecase defines the business logic operations required by the bot.
|
||||
type Usecase interface {
|
||||
RegisterNewUser(ctx context.Context, user *entities.User) error
|
||||
UserByID(ctx context.Context, userID entities.UserID) (*entities.User, error)
|
||||
UserByTgID(ctx context.Context, telegramID entities.TelegramID) (*entities.User, error)
|
||||
InstrumentList(ctx context.Context, offset, limit int) ([]entities.Instrument, error)
|
||||
InstrumentList(ctx context.Context, userID entities.UserID, offset, limit int) ([]entities.Instrument, error)
|
||||
AddUserInstrument(ctx context.Context, userID entities.UserID, base, quote string) (*entities.Instrument, error)
|
||||
RemoveUserInstrument(ctx context.Context, userID entities.UserID, instrumentID entities.InstrumentID) error
|
||||
CreateAlert(ctx context.Context, alert *entities.Alert) (entities.AlertID, error)
|
||||
Alert(ctx context.Context, alertID entities.AlertID) (*entities.Alert, error)
|
||||
Alerts(ctx context.Context, userID entities.UserID, offset, limit int) ([]entities.Alert, error)
|
||||
|
|
@ -35,9 +40,10 @@ type Alerter interface {
|
|||
RemoveAlert(id entities.AlertID)
|
||||
}
|
||||
|
||||
// PriceProvider fetches the current market price for an instrument.
|
||||
// PriceProvider fetches the current market price for an instrument and validates pairs.
|
||||
type PriceProvider interface {
|
||||
Price(ctx context.Context, instrument entities.Instrument) (*entities.Price, error)
|
||||
InstrumentExists(ctx context.Context, base, quote string) (bool, error)
|
||||
}
|
||||
|
||||
// Reply keyboard button labels.
|
||||
|
|
@ -45,6 +51,7 @@ const (
|
|||
btnAddAlert = "Add Alert"
|
||||
btnMyAlerts = "My Alerts"
|
||||
btnInstruments = "Instruments"
|
||||
btnAddPair = "Add Pair"
|
||||
)
|
||||
|
||||
type flowStep string
|
||||
|
|
@ -53,6 +60,7 @@ const (
|
|||
stepAddAlertPrice flowStep = "add_alert_price"
|
||||
stepAddAlertAwaitType flowStep = "add_alert_await_type" // price entered, waiting for type callback
|
||||
stepEditAlertPrice flowStep = "edit_alert_price"
|
||||
stepAddPair flowStep = "add_pair"
|
||||
)
|
||||
|
||||
type userState struct {
|
||||
|
|
@ -213,6 +221,9 @@ func (b *Bot) handleMessage(ctx context.Context, msg *tgbotapi.Message) {
|
|||
case btnInstruments:
|
||||
b.cmdInstruments(ctx, tgID, chatID)
|
||||
return
|
||||
case btnAddPair:
|
||||
b.cmdAddPair(ctx, tgID, chatID)
|
||||
return
|
||||
}
|
||||
|
||||
// Route plain-text input to the active multi-step flow.
|
||||
|
|
@ -224,6 +235,8 @@ func (b *Bot) handleMessage(ctx context.Context, msg *tgbotapi.Message) {
|
|||
b.send(chatID, "Please select the alert type using the buttons above.")
|
||||
case stepEditAlertPrice:
|
||||
b.handleEditAlertPrice(ctx, tgID, chatID, msg.Text, state)
|
||||
case stepAddPair:
|
||||
b.handleAddPairInput(ctx, tgID, chatID, msg.Text)
|
||||
default:
|
||||
b.sendMenu(chatID, "Use the menu below or /add_alert to set a price alert.")
|
||||
}
|
||||
|
|
@ -239,10 +252,12 @@ func (b *Bot) handleCommand(ctx context.Context, tgID entities.TelegramID, chatI
|
|||
b.cmdAddAlert(ctx, tgID, chatID)
|
||||
case "my_alerts":
|
||||
b.cmdMyAlerts(ctx, tgID, chatID)
|
||||
case "add_pair":
|
||||
b.cmdAddPair(ctx, tgID, chatID)
|
||||
case "cancel":
|
||||
b.cmdCancel(tgID, chatID)
|
||||
default:
|
||||
b.sendMenu(chatID, "Unknown command.\n\nAvailable commands:\n/start — register\n/instruments — list trading pairs\n/add_alert — create a price alert\n/my_alerts — view your alerts\n/cancel — cancel current operation")
|
||||
b.sendMenu(chatID, "Unknown command.\n\nAvailable commands:\n/start — register\n/instruments — list trading pairs\n/add_alert — create a price alert\n/my_alerts — view your alerts\n/add_pair — add a custom trading pair\n/cancel — cancel current operation")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -260,6 +275,46 @@ func (b *Bot) handleCallback(ctx context.Context, cb *tgbotapi.CallbackQuery) {
|
|||
instrID := entities.InstrumentID(strings.TrimPrefix(data, "instrument:"))
|
||||
b.handleInstrumentSelected(ctx, tgID, chatID, instrID)
|
||||
|
||||
case strings.HasPrefix(data, "add_alert_instr_page:"):
|
||||
page, _ := strconv.Atoi(strings.TrimPrefix(data, "add_alert_instr_page:"))
|
||||
b.handleAddAlertInstrPage(ctx, tgID, chatID, messageID, page)
|
||||
|
||||
case strings.HasPrefix(data, "instr_page:"):
|
||||
page, _ := strconv.Atoi(strings.TrimPrefix(data, "instr_page:"))
|
||||
b.handleInstrumentsPage(ctx, tgID, chatID, messageID, page)
|
||||
|
||||
case strings.HasPrefix(data, "instr_select:"):
|
||||
// format: instr_select:<instrID>:<page>
|
||||
rest := strings.TrimPrefix(data, "instr_select:")
|
||||
idx := strings.LastIndex(rest, ":")
|
||||
instrID := entities.InstrumentID(rest[:idx])
|
||||
page, _ := strconv.Atoi(rest[idx+1:])
|
||||
b.handleInstrumentSelect(ctx, tgID, chatID, messageID, instrID, page)
|
||||
|
||||
case strings.HasPrefix(data, "instr_remove:"):
|
||||
// format: instr_remove:<instrID>:<page>
|
||||
rest := strings.TrimPrefix(data, "instr_remove:")
|
||||
idx := strings.LastIndex(rest, ":")
|
||||
instrID := entities.InstrumentID(rest[:idx])
|
||||
page, _ := strconv.Atoi(rest[idx+1:])
|
||||
b.handleInstrumentRemoveConfirm(chatID, messageID, instrID, page)
|
||||
|
||||
case strings.HasPrefix(data, "confirm_remove_instr:"):
|
||||
// format: confirm_remove_instr:<instrID>:<page>
|
||||
rest := strings.TrimPrefix(data, "confirm_remove_instr:")
|
||||
idx := strings.LastIndex(rest, ":")
|
||||
instrID := entities.InstrumentID(rest[:idx])
|
||||
page, _ := strconv.Atoi(rest[idx+1:])
|
||||
b.handleInstrumentRemoveDo(ctx, tgID, chatID, messageID, instrID, page)
|
||||
|
||||
case strings.HasPrefix(data, "cancel_remove_instr:"):
|
||||
// format: cancel_remove_instr:<instrID>:<page>
|
||||
rest := strings.TrimPrefix(data, "cancel_remove_instr:")
|
||||
idx := strings.LastIndex(rest, ":")
|
||||
instrID := entities.InstrumentID(rest[:idx])
|
||||
page, _ := strconv.Atoi(rest[idx+1:])
|
||||
b.handleInstrumentSelect(ctx, tgID, chatID, messageID, instrID, page)
|
||||
|
||||
case strings.HasPrefix(data, "alerts_page:"), strings.HasPrefix(data, "alerts_back:"):
|
||||
rest := data[strings.Index(data, ":")+1:]
|
||||
page, _ := strconv.Atoi(rest)
|
||||
|
|
@ -331,8 +386,13 @@ func (b *Bot) cmdStart(ctx context.Context, tgID entities.TelegramID, chatID int
|
|||
b.sendMenu(chatID, "Welcome! You are now registered.\n\nUse the menu below to get started.")
|
||||
}
|
||||
|
||||
func (b *Bot) cmdInstruments(ctx context.Context, _ entities.TelegramID, chatID int64) {
|
||||
instruments, err := b.usecase.InstrumentList(ctx, 0, 50)
|
||||
func (b *Bot) cmdInstruments(ctx context.Context, tgID entities.TelegramID, chatID int64) {
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
instruments, err := b.usecase.InstrumentList(ctx, user.ID, 0, 200)
|
||||
if err != nil {
|
||||
b.log.Error("failed to list instruments", "err", err)
|
||||
b.sendMenu(chatID, "Failed to load instruments.")
|
||||
|
|
@ -343,41 +403,32 @@ func (b *Bot) cmdInstruments(ctx context.Context, _ entities.TelegramID, chatID
|
|||
return
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Available trading pairs:\n\n")
|
||||
for _, instr := range instruments {
|
||||
fmt.Fprintf(&sb, "- %s/%s\n", instr.BaseCurrency, instr.QuoteCurrency)
|
||||
}
|
||||
b.sendMenu(chatID, sb.String())
|
||||
text, kb := buildInstrumentsPage(instruments, 0)
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
msg.ReplyMarkup = kb
|
||||
b.sendMsg(msg)
|
||||
}
|
||||
|
||||
func (b *Bot) cmdAddAlert(ctx context.Context, tgID entities.TelegramID, chatID int64) {
|
||||
if _, err := b.requireUser(ctx, tgID, chatID); err != nil {
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
instruments, err := b.usecase.InstrumentList(ctx, 0, 50)
|
||||
instruments, err := b.usecase.InstrumentList(ctx, user.ID, 0, 200)
|
||||
if err != nil {
|
||||
b.log.Error("failed to list instruments", "err", err)
|
||||
b.sendMenu(chatID, "Failed to load instruments.")
|
||||
return
|
||||
}
|
||||
if len(instruments) == 0 {
|
||||
b.sendMenu(chatID, "No instruments available.")
|
||||
b.sendMenu(chatID, "No instruments available. Use \"Add Pair\" to add one.")
|
||||
return
|
||||
}
|
||||
|
||||
var rows [][]tgbotapi.InlineKeyboardButton
|
||||
for _, instr := range instruments {
|
||||
btn := tgbotapi.NewInlineKeyboardButtonData(
|
||||
fmt.Sprintf("%s/%s", instr.BaseCurrency, instr.QuoteCurrency),
|
||||
fmt.Sprintf("instrument:%s", instr.ID),
|
||||
)
|
||||
rows = append(rows, tgbotapi.NewInlineKeyboardRow(btn))
|
||||
}
|
||||
|
||||
msg := tgbotapi.NewMessage(chatID, "Select a trading pair:")
|
||||
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(rows...)
|
||||
text, kb := buildAddAlertInstrPage(instruments, 0)
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
msg.ReplyMarkup = kb
|
||||
b.sendMsg(msg)
|
||||
}
|
||||
|
||||
|
|
@ -404,6 +455,236 @@ func (b *Bot) cmdMyAlerts(ctx context.Context, tgID entities.TelegramID, chatID
|
|||
b.sendMsg(msg)
|
||||
}
|
||||
|
||||
func (b *Bot) cmdAddPair(ctx context.Context, tgID entities.TelegramID, chatID int64) {
|
||||
if _, err := b.requireUser(ctx, tgID, chatID); err != nil {
|
||||
return
|
||||
}
|
||||
b.setState(tgID, &userState{step: stepAddPair})
|
||||
b.send(chatID, "Enter the trading pair in BASE/QUOTE format (e.g. DOGE/USDT):")
|
||||
}
|
||||
|
||||
func (b *Bot) cmdCancel(tgID entities.TelegramID, chatID int64) {
|
||||
b.setState(tgID, &userState{})
|
||||
b.sendMenu(chatID, "Operation cancelled.")
|
||||
}
|
||||
|
||||
// --- Instruments pagination ---
|
||||
|
||||
// buildInstrumentsPage constructs a paginated instruments list.
|
||||
// Numbered buttons let users open a detail view; ◀ ▶ navigate pages.
|
||||
func buildInstrumentsPage(instruments []entities.Instrument, page int) (string, tgbotapi.InlineKeyboardMarkup) {
|
||||
total := len(instruments)
|
||||
totalPages := (total + instrPageSize - 1) / instrPageSize
|
||||
|
||||
start := page * instrPageSize
|
||||
end := start + instrPageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
pageItems := instruments[start:end]
|
||||
|
||||
var sb strings.Builder
|
||||
fmt.Fprintf(&sb, "Trading pairs (page %d/%d):\n\n", page+1, totalPages)
|
||||
for i, instr := range pageItems {
|
||||
marker := ""
|
||||
if !instr.IsGlobal {
|
||||
marker = " ✦" // marks user-added pairs
|
||||
}
|
||||
fmt.Fprintf(&sb, "%d. %s/%s%s\n", start+i+1, instr.BaseCurrency, instr.QuoteCurrency, marker)
|
||||
}
|
||||
|
||||
var rows [][]tgbotapi.InlineKeyboardButton
|
||||
|
||||
// One button per instrument on this page.
|
||||
var itemRow []tgbotapi.InlineKeyboardButton
|
||||
for i, instr := range pageItems {
|
||||
itemRow = append(itemRow, tgbotapi.NewInlineKeyboardButtonData(
|
||||
strconv.Itoa(start+i+1),
|
||||
fmt.Sprintf("instr_select:%s:%d", instr.ID, page),
|
||||
))
|
||||
}
|
||||
if len(itemRow) > 0 {
|
||||
rows = append(rows, itemRow)
|
||||
}
|
||||
|
||||
var navRow []tgbotapi.InlineKeyboardButton
|
||||
if page > 0 {
|
||||
navRow = append(navRow, tgbotapi.NewInlineKeyboardButtonData("◀", fmt.Sprintf("instr_page:%d", page-1)))
|
||||
}
|
||||
if end < total {
|
||||
navRow = append(navRow, tgbotapi.NewInlineKeyboardButtonData("▶", fmt.Sprintf("instr_page:%d", page+1)))
|
||||
}
|
||||
if len(navRow) > 0 {
|
||||
rows = append(rows, navRow)
|
||||
}
|
||||
|
||||
return sb.String(), tgbotapi.NewInlineKeyboardMarkup(rows...)
|
||||
}
|
||||
|
||||
// handleInstrumentsPage re-fetches instruments and edits the message to show the requested page.
|
||||
func (b *Bot) handleInstrumentsPage(ctx context.Context, tgID entities.TelegramID, chatID int64, messageID int, page int) {
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
instruments, err := b.usecase.InstrumentList(ctx, user.ID, 0, 200)
|
||||
if err != nil {
|
||||
b.log.Error("failed to list instruments", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
totalPages := (len(instruments) + instrPageSize - 1) / instrPageSize
|
||||
if page >= totalPages {
|
||||
page = totalPages - 1
|
||||
}
|
||||
|
||||
text, kb := buildInstrumentsPage(instruments, 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)
|
||||
}
|
||||
}
|
||||
|
||||
// handleInstrumentSelect shows instrument detail with a Remove button (for user-added pairs).
|
||||
func (b *Bot) handleInstrumentSelect(ctx context.Context, tgID entities.TelegramID, chatID int64, messageID int, instrID entities.InstrumentID, page int) {
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
instruments, err := b.usecase.InstrumentList(ctx, user.ID, 0, 200)
|
||||
if err != nil {
|
||||
b.log.Error("failed to list instruments", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
var selected entities.Instrument
|
||||
for _, instr := range instruments {
|
||||
if instr.ID == instrID {
|
||||
selected = instr
|
||||
break
|
||||
}
|
||||
}
|
||||
if selected.ID == "" {
|
||||
b.editMsgText(chatID, messageID, "Instrument not found.")
|
||||
return
|
||||
}
|
||||
|
||||
text := fmt.Sprintf("%s/%s", selected.BaseCurrency, selected.QuoteCurrency)
|
||||
if !selected.IsGlobal {
|
||||
text += "\n(user-added pair)"
|
||||
}
|
||||
|
||||
var btns []tgbotapi.InlineKeyboardButton
|
||||
if !selected.IsGlobal {
|
||||
btns = append(btns, tgbotapi.NewInlineKeyboardButtonData(
|
||||
"Remove", fmt.Sprintf("instr_remove:%s:%d", instrID, page),
|
||||
))
|
||||
}
|
||||
btns = append(btns, tgbotapi.NewInlineKeyboardButtonData(
|
||||
"◀ Back", fmt.Sprintf("instr_page:%d", page),
|
||||
))
|
||||
|
||||
edit := tgbotapi.NewEditMessageTextAndMarkup(chatID, messageID, text,
|
||||
tgbotapi.NewInlineKeyboardMarkup(btns))
|
||||
if _, err := b.api.Send(edit); err != nil {
|
||||
b.log.Error("failed to edit message", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleInstrumentRemoveConfirm asks the user to confirm removal.
|
||||
func (b *Bot) handleInstrumentRemoveConfirm(chatID int64, messageID int, instrID entities.InstrumentID, page int) {
|
||||
kb := tgbotapi.NewInlineKeyboardMarkup(tgbotapi.NewInlineKeyboardRow(
|
||||
tgbotapi.NewInlineKeyboardButtonData("Yes, remove", fmt.Sprintf("confirm_remove_instr:%s:%d", instrID, page)),
|
||||
tgbotapi.NewInlineKeyboardButtonData("Cancel", fmt.Sprintf("cancel_remove_instr:%s:%d", instrID, page)),
|
||||
))
|
||||
edit := tgbotapi.NewEditMessageTextAndMarkup(chatID, messageID, "Remove this pair from your list?", kb)
|
||||
if _, err := b.api.Send(edit); err != nil {
|
||||
b.log.Error("failed to edit message", "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleInstrumentRemoveDo removes the instrument from the user's list and refreshes the page.
|
||||
func (b *Bot) handleInstrumentRemoveDo(ctx context.Context, tgID entities.TelegramID, chatID int64, messageID int, instrID entities.InstrumentID, page int) {
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := b.usecase.RemoveUserInstrument(ctx, user.ID, instrID); err != nil {
|
||||
b.log.Error("failed to remove user instrument", "instrument_id", instrID, "err", err)
|
||||
b.editMsgText(chatID, messageID, "Failed to remove pair.")
|
||||
return
|
||||
}
|
||||
|
||||
b.handleInstrumentsPage(ctx, tgID, chatID, messageID, page)
|
||||
}
|
||||
|
||||
// buildAddAlertInstrPage builds the instrument selection keyboard for the add-alert flow.
|
||||
func buildAddAlertInstrPage(instruments []entities.Instrument, page int) (string, tgbotapi.InlineKeyboardMarkup) {
|
||||
total := len(instruments)
|
||||
totalPages := (total + instrPageSize - 1) / instrPageSize
|
||||
|
||||
start := page * instrPageSize
|
||||
end := start + instrPageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
pageItems := instruments[start:end]
|
||||
|
||||
text := fmt.Sprintf("Select a trading pair (page %d/%d):", page+1, totalPages)
|
||||
|
||||
var rows [][]tgbotapi.InlineKeyboardButton
|
||||
for _, instr := range pageItems {
|
||||
btn := tgbotapi.NewInlineKeyboardButtonData(
|
||||
fmt.Sprintf("%s/%s", instr.BaseCurrency, instr.QuoteCurrency),
|
||||
fmt.Sprintf("instrument:%s", instr.ID),
|
||||
)
|
||||
rows = append(rows, tgbotapi.NewInlineKeyboardRow(btn))
|
||||
}
|
||||
|
||||
var navRow []tgbotapi.InlineKeyboardButton
|
||||
if page > 0 {
|
||||
navRow = append(navRow, tgbotapi.NewInlineKeyboardButtonData("◀", fmt.Sprintf("add_alert_instr_page:%d", page-1)))
|
||||
}
|
||||
if end < total {
|
||||
navRow = append(navRow, tgbotapi.NewInlineKeyboardButtonData("▶", fmt.Sprintf("add_alert_instr_page:%d", page+1)))
|
||||
}
|
||||
if len(navRow) > 0 {
|
||||
rows = append(rows, navRow)
|
||||
}
|
||||
|
||||
return text, tgbotapi.NewInlineKeyboardMarkup(rows...)
|
||||
}
|
||||
|
||||
// handleAddAlertInstrPage edits the instrument selection message to show a different page.
|
||||
func (b *Bot) handleAddAlertInstrPage(ctx context.Context, tgID entities.TelegramID, chatID int64, messageID int, page int) {
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
instruments, err := b.usecase.InstrumentList(ctx, user.ID, 0, 200)
|
||||
if err != nil {
|
||||
b.log.Error("failed to list instruments", "err", err)
|
||||
return
|
||||
}
|
||||
|
||||
totalPages := (len(instruments) + instrPageSize - 1) / instrPageSize
|
||||
if page >= totalPages {
|
||||
page = totalPages - 1
|
||||
}
|
||||
|
||||
text, kb := buildAddAlertInstrPage(instruments, 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)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Alerts pagination ---
|
||||
|
||||
// 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)
|
||||
|
|
@ -516,16 +797,16 @@ func (b *Bot) handleAlertSelect(ctx context.Context, chatID int64, messageID int
|
|||
}
|
||||
}
|
||||
|
||||
func (b *Bot) cmdCancel(tgID entities.TelegramID, chatID int64) {
|
||||
b.setState(tgID, &userState{})
|
||||
b.sendMenu(chatID, "Operation cancelled.")
|
||||
}
|
||||
|
||||
// --- Multi-step flow handlers ---
|
||||
|
||||
// handleInstrumentSelected fetches the current price, stores it in state, and prompts for a target price.
|
||||
func (b *Bot) handleInstrumentSelected(ctx context.Context, tgID entities.TelegramID, chatID int64, instrID entities.InstrumentID) {
|
||||
instruments, err := b.usecase.InstrumentList(ctx, 0, 50)
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
instruments, err := b.usecase.InstrumentList(ctx, user.ID, 0, 200)
|
||||
if err != nil {
|
||||
b.log.Error("failed to list instruments", "err", err)
|
||||
b.sendMenu(chatID, "Failed to load instruments.")
|
||||
|
|
@ -701,6 +982,46 @@ func (b *Bot) handleAlertTimeframe(ctx context.Context, tgID entities.TelegramID
|
|||
))
|
||||
}
|
||||
|
||||
// handleAddPairInput processes the user-entered trading pair symbol.
|
||||
func (b *Bot) handleAddPairInput(ctx context.Context, tgID entities.TelegramID, chatID int64, text string) {
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(strings.TrimSpace(text), "/", 2)
|
||||
if len(parts) != 2 || strings.TrimSpace(parts[0]) == "" || strings.TrimSpace(parts[1]) == "" {
|
||||
b.send(chatID, "Invalid format. Please enter the pair as BASE/QUOTE (e.g. DOGE/USDT):")
|
||||
return
|
||||
}
|
||||
|
||||
base := strings.ToUpper(strings.TrimSpace(parts[0]))
|
||||
quote := strings.ToUpper(strings.TrimSpace(parts[1]))
|
||||
|
||||
exists, err := b.provider.InstrumentExists(ctx, base, quote)
|
||||
if err != nil {
|
||||
b.log.Error("failed to check instrument existence", "base", base, "quote", quote, "err", err)
|
||||
b.sendMenu(chatID, "Could not verify the trading pair. Please try again later.")
|
||||
b.setState(tgID, &userState{})
|
||||
return
|
||||
}
|
||||
if !exists {
|
||||
b.send(chatID, fmt.Sprintf("Pair %s/%s is not available on Bybit. Please enter a valid pair:", base, quote))
|
||||
return
|
||||
}
|
||||
|
||||
instr, err := b.usecase.AddUserInstrument(ctx, user.ID, base, quote)
|
||||
if err != nil {
|
||||
b.log.Error("failed to add user instrument", "err", err)
|
||||
b.sendMenu(chatID, "Failed to add the pair. Please try again.")
|
||||
b.setState(tgID, &userState{})
|
||||
return
|
||||
}
|
||||
|
||||
b.setState(tgID, &userState{})
|
||||
b.sendMenu(chatID, fmt.Sprintf("Pair %s/%s has been added to your list!", instr.BaseCurrency, instr.QuoteCurrency))
|
||||
}
|
||||
|
||||
// detectCondition returns above/below based on target vs current ask.
|
||||
func (b *Bot) detectCondition(state *userState) entities.AlertCondition {
|
||||
if state.currentPrice != nil && state.targetPrice.GreaterThanOrEqual(state.currentPrice.Ask) {
|
||||
|
|
@ -818,6 +1139,7 @@ func menuKeyboard() tgbotapi.ReplyKeyboardMarkup {
|
|||
),
|
||||
tgbotapi.NewKeyboardButtonRow(
|
||||
tgbotapi.NewKeyboardButton(btnInstruments),
|
||||
tgbotapi.NewKeyboardButton(btnAddPair),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue