implement telegram bot
This commit is contained in:
parent
608561ab38
commit
bec3b7de5b
5 changed files with 613 additions and 6 deletions
|
|
@ -1 +1,559 @@
|
|||
package telegram
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gitea.computernetthings.ru/yash/crypto_alert_bot/internal/entities"
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
"github.com/shopspring/decimal"
|
||||
)
|
||||
|
||||
// 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)
|
||||
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)
|
||||
RemoveAlert(ctx context.Context, alertID entities.AlertID) error
|
||||
UpdateAlertPrice(ctx context.Context, alertID entities.AlertID, price decimal.Decimal) error
|
||||
}
|
||||
|
||||
// Alerter keeps the in-memory alert cache in sync with created/removed alerts.
|
||||
type Alerter interface {
|
||||
AddAlert(alert *entities.Alert)
|
||||
RemoveAlert(id entities.AlertID)
|
||||
}
|
||||
|
||||
// PriceProvider fetches the current market price for an instrument.
|
||||
type PriceProvider interface {
|
||||
Price(ctx context.Context, instrument entities.Instrument) (*entities.Price, error)
|
||||
}
|
||||
|
||||
// Reply keyboard button labels.
|
||||
const (
|
||||
btnAddAlert = "Add Alert"
|
||||
btnMyAlerts = "My Alerts"
|
||||
btnInstruments = "Instruments"
|
||||
)
|
||||
|
||||
type flowStep string
|
||||
|
||||
const (
|
||||
stepAddAlertPrice flowStep = "add_alert_price"
|
||||
stepEditAlertPrice flowStep = "edit_alert_price"
|
||||
)
|
||||
|
||||
type userState struct {
|
||||
step flowStep
|
||||
instrument entities.Instrument // set during add_alert flow
|
||||
currentPrice *entities.Price // fetched when instrument is selected
|
||||
alertID entities.AlertID // set during edit flow
|
||||
}
|
||||
|
||||
// Bot is the Telegram bot handling all user interactions.
|
||||
type Bot struct {
|
||||
api *tgbotapi.BotAPI
|
||||
log *slog.Logger
|
||||
usecase Usecase
|
||||
alerter Alerter
|
||||
provider PriceProvider
|
||||
states map[entities.TelegramID]*userState
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// New creates and initialises a new Bot. Call SetAlerter before Run to enable cache sync.
|
||||
func New(token string, log *slog.Logger, uc Usecase, pp PriceProvider) (*Bot, error) {
|
||||
api, err := tgbotapi.NewBotAPI(token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create bot: %w", err)
|
||||
}
|
||||
|
||||
return &Bot{
|
||||
api: api,
|
||||
log: log,
|
||||
usecase: uc,
|
||||
provider: pp,
|
||||
states: make(map[entities.TelegramID]*userState),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetAlerter injects the alerter after construction, resolving the circular dependency.
|
||||
func (b *Bot) SetAlerter(al Alerter) {
|
||||
b.alerter = al
|
||||
}
|
||||
|
||||
// Run starts the update loop and blocks until ctx is cancelled.
|
||||
func (b *Bot) Run(ctx context.Context) {
|
||||
u := tgbotapi.NewUpdate(0)
|
||||
u.Timeout = 60
|
||||
|
||||
updates := b.api.GetUpdatesChan(u)
|
||||
b.log.Info("bot started", "username", b.api.Self.UserName)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
b.api.StopReceivingUpdates()
|
||||
return
|
||||
case update, ok := <-updates:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
go b.handleUpdate(ctx, update)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyAlert implements alerter.Notifier. Called by the alerter service when a price threshold is hit.
|
||||
func (b *Bot) NotifyAlert(ctx context.Context, userID entities.UserID, alert *entities.Alert, currentPrice decimal.Decimal) error {
|
||||
user, err := b.usecase.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
text := fmt.Sprintf(
|
||||
"Alert triggered!\n\n%s/%s %s %s\nCurrent price: %s",
|
||||
alert.Instrument.BaseCurrency,
|
||||
alert.Instrument.QuoteCurrency,
|
||||
alert.Condition,
|
||||
alert.Price.String(),
|
||||
currentPrice.String(),
|
||||
)
|
||||
msg := tgbotapi.NewMessage(int64(user.TelegramID), text)
|
||||
kb := menuKeyboard()
|
||||
kb.ResizeKeyboard = true
|
||||
msg.ReplyMarkup = kb
|
||||
_, err = b.api.Send(msg)
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Routing ---
|
||||
|
||||
func (b *Bot) handleUpdate(ctx context.Context, update tgbotapi.Update) {
|
||||
switch {
|
||||
case update.Message != nil:
|
||||
b.handleMessage(ctx, update.Message)
|
||||
case update.CallbackQuery != nil:
|
||||
b.handleCallback(ctx, update.CallbackQuery)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) handleMessage(ctx context.Context, msg *tgbotapi.Message) {
|
||||
tgID := entities.TelegramID(msg.From.ID)
|
||||
chatID := msg.Chat.ID
|
||||
|
||||
if msg.IsCommand() {
|
||||
b.handleCommand(ctx, tgID, chatID, msg.Command())
|
||||
return
|
||||
}
|
||||
|
||||
// Map persistent reply-keyboard buttons to actions.
|
||||
switch msg.Text {
|
||||
case btnAddAlert:
|
||||
b.cmdAddAlert(ctx, tgID, chatID)
|
||||
return
|
||||
case btnMyAlerts:
|
||||
b.cmdMyAlerts(ctx, tgID, chatID)
|
||||
return
|
||||
case btnInstruments:
|
||||
b.cmdInstruments(ctx, tgID, chatID)
|
||||
return
|
||||
}
|
||||
|
||||
// Route plain-text input to the active multi-step flow.
|
||||
state := b.getState(tgID)
|
||||
switch state.step {
|
||||
case stepAddAlertPrice:
|
||||
b.handleAddAlertPrice(ctx, tgID, chatID, msg.Text, state)
|
||||
case stepEditAlertPrice:
|
||||
b.handleEditAlertPrice(ctx, tgID, chatID, msg.Text, state)
|
||||
default:
|
||||
b.sendMenu(chatID, "Use the menu below or /add_alert to set a price alert.")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) handleCommand(ctx context.Context, tgID entities.TelegramID, chatID int64, cmd string) {
|
||||
switch cmd {
|
||||
case "start":
|
||||
b.cmdStart(ctx, tgID, chatID)
|
||||
case "instruments":
|
||||
b.cmdInstruments(ctx, tgID, chatID)
|
||||
case "add_alert":
|
||||
b.cmdAddAlert(ctx, tgID, chatID)
|
||||
case "my_alerts":
|
||||
b.cmdMyAlerts(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")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bot) handleCallback(ctx context.Context, cb *tgbotapi.CallbackQuery) {
|
||||
tgID := entities.TelegramID(cb.From.ID)
|
||||
chatID := cb.Message.Chat.ID
|
||||
|
||||
// Acknowledge the callback immediately so the button stops spinning.
|
||||
b.api.Request(tgbotapi.NewCallback(cb.ID, "")) //nolint:errcheck
|
||||
|
||||
data := cb.Data
|
||||
switch {
|
||||
case strings.HasPrefix(data, "instrument:"):
|
||||
instrID := entities.InstrumentID(strings.TrimPrefix(data, "instrument:"))
|
||||
b.handleInstrumentSelected(ctx, tgID, chatID, instrID)
|
||||
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)
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Command handlers ---
|
||||
|
||||
func (b *Bot) cmdStart(ctx context.Context, tgID entities.TelegramID, chatID int64) {
|
||||
// If already registered, just greet them.
|
||||
if _, err := b.usecase.UserByTgID(ctx, tgID); err == nil {
|
||||
b.sendMenu(chatID, "Welcome back! Use the menu below to manage your alerts.")
|
||||
return
|
||||
}
|
||||
|
||||
user := &entities.User{TelegramID: tgID}
|
||||
if err := b.usecase.RegisterNewUser(ctx, user); err != nil {
|
||||
b.log.Error("failed to register user", "tg_id", tgID, "err", err)
|
||||
b.sendMenu(chatID, "Registration failed. Please try again.")
|
||||
return
|
||||
}
|
||||
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)
|
||||
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.")
|
||||
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())
|
||||
}
|
||||
|
||||
func (b *Bot) cmdAddAlert(ctx context.Context, tgID entities.TelegramID, chatID int64) {
|
||||
if _, err := b.requireUser(ctx, tgID, chatID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
instruments, err := b.usecase.InstrumentList(ctx, 0, 50)
|
||||
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.")
|
||||
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...)
|
||||
b.sendMsg(msg)
|
||||
}
|
||||
|
||||
func (b *Bot) cmdMyAlerts(ctx context.Context, tgID entities.TelegramID, chatID int64) {
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
alerts, err := b.usecase.Alerts(ctx, user.ID, 0, 20)
|
||||
if err != nil {
|
||||
b.log.Error("failed to get alerts", "err", err)
|
||||
b.sendMenu(chatID, "Failed to load alerts.")
|
||||
return
|
||||
}
|
||||
if len(alerts) == 0 {
|
||||
b.sendMenu(chatID, "You have no active alerts. Use \"Add Alert\" to create one.")
|
||||
return
|
||||
}
|
||||
|
||||
b.sendMenu(chatID, fmt.Sprintf("Your active alerts (%d):", len(alerts)))
|
||||
|
||||
for _, alert := range alerts {
|
||||
text := fmt.Sprintf("%s/%s — %s %s",
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
b.log.Error("failed to list instruments", "err", err)
|
||||
b.sendMenu(chatID, "Failed to load instruments.")
|
||||
return
|
||||
}
|
||||
|
||||
var selected entities.Instrument
|
||||
for _, instr := range instruments {
|
||||
if instr.ID == instrID {
|
||||
selected = instr
|
||||
break
|
||||
}
|
||||
}
|
||||
if selected.ID == "" {
|
||||
b.sendMenu(chatID, "Unknown instrument. Please try again.")
|
||||
return
|
||||
}
|
||||
|
||||
state := &userState{
|
||||
step: stepAddAlertPrice,
|
||||
instrument: selected,
|
||||
}
|
||||
|
||||
price, err := b.provider.Price(ctx, selected)
|
||||
if err != nil {
|
||||
b.log.Error("failed to fetch current price", "instrument", selected.ID, "err", err)
|
||||
b.setState(tgID, state)
|
||||
b.send(chatID, fmt.Sprintf("Selected: %s/%s\n\nCould not fetch current price. Enter your target price:", selected.BaseCurrency, selected.QuoteCurrency))
|
||||
return
|
||||
}
|
||||
|
||||
state.currentPrice = price
|
||||
b.setState(tgID, state)
|
||||
b.send(chatID, fmt.Sprintf(
|
||||
"Selected: %s/%s\n\nCurrent price:\n Ask: %s\n Bid: %s\n\nEnter your target price:",
|
||||
selected.BaseCurrency, selected.QuoteCurrency,
|
||||
price.Ask.String(), price.Bid.String(),
|
||||
))
|
||||
}
|
||||
|
||||
// handleAddAlertPrice parses the target price, auto-determines the condition from the current
|
||||
// market price (target >= ask → above, target < ask → below), and creates the alert.
|
||||
func (b *Bot) handleAddAlertPrice(ctx context.Context, tgID entities.TelegramID, chatID int64, text string, state *userState) {
|
||||
target, err := decimal.NewFromString(strings.TrimSpace(text))
|
||||
if err != nil || !target.IsPositive() {
|
||||
b.send(chatID, "Invalid price. Please enter a positive number (e.g. 50000.5):")
|
||||
return
|
||||
}
|
||||
|
||||
// Use the price already fetched at instrument-selection time; re-fetch only if missing.
|
||||
currentPrice := state.currentPrice
|
||||
if currentPrice == nil {
|
||||
currentPrice, err = b.provider.Price(ctx, state.instrument)
|
||||
if err != nil {
|
||||
b.log.Error("failed to fetch current price", "instrument", state.instrument.ID, "err", err)
|
||||
b.send(chatID, "Could not fetch current price. Please try again:")
|
||||
return
|
||||
}
|
||||
state.currentPrice = currentPrice
|
||||
b.setState(tgID, state)
|
||||
}
|
||||
|
||||
// Determine condition automatically: above ask or below ask.
|
||||
var condition entities.AlertCondition
|
||||
if target.GreaterThanOrEqual(currentPrice.Ask) {
|
||||
condition = entities.AlertConditionAbove
|
||||
} else {
|
||||
condition = entities.AlertConditionBelow
|
||||
}
|
||||
|
||||
user, err := b.requireUser(ctx, tgID, chatID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
alert := &entities.Alert{
|
||||
UserID: user.ID,
|
||||
Price: target,
|
||||
Condition: condition,
|
||||
Instrument: state.instrument,
|
||||
}
|
||||
|
||||
id, err := b.usecase.CreateAlert(ctx, alert)
|
||||
if err != nil {
|
||||
b.log.Error("failed to create alert", "err", err)
|
||||
b.sendMenu(chatID, "Failed to create alert. Please try again.")
|
||||
b.setState(tgID, &userState{})
|
||||
return
|
||||
}
|
||||
|
||||
alert.ID = id
|
||||
b.alerter.AddAlert(alert)
|
||||
b.setState(tgID, &userState{})
|
||||
b.sendMenu(chatID, fmt.Sprintf(
|
||||
"Alert created!\n\n%s/%s %s %s\n\nYou will be notified when the price reaches your target.",
|
||||
state.instrument.BaseCurrency, state.instrument.QuoteCurrency,
|
||||
condition, target.String(),
|
||||
))
|
||||
}
|
||||
|
||||
// handleEditAlertStart begins the edit flow for a specific alert.
|
||||
func (b *Bot) handleEditAlertStart(tgID entities.TelegramID, chatID int64, alertID entities.AlertID) {
|
||||
b.setState(tgID, &userState{
|
||||
step: stepEditAlertPrice,
|
||||
alertID: alertID,
|
||||
})
|
||||
b.send(chatID, "Enter the new target price:")
|
||||
}
|
||||
|
||||
// handleEditAlertPrice applies the new price entered by the user.
|
||||
func (b *Bot) handleEditAlertPrice(ctx context.Context, tgID entities.TelegramID, chatID int64, text string, state *userState) {
|
||||
price, err := decimal.NewFromString(strings.TrimSpace(text))
|
||||
if err != nil || !price.IsPositive() {
|
||||
b.send(chatID, "Invalid price. Please enter a positive number:")
|
||||
return
|
||||
}
|
||||
|
||||
if err := b.usecase.UpdateAlertPrice(ctx, state.alertID, price); err != nil {
|
||||
b.log.Error("failed to update alert price", "alert_id", state.alertID, "err", err)
|
||||
b.sendMenu(chatID, "Failed to update alert.")
|
||||
b.setState(tgID, &userState{})
|
||||
return
|
||||
}
|
||||
|
||||
// Refresh the alerter cache: remove stale entry and re-add with updated price.
|
||||
b.alerter.RemoveAlert(state.alertID)
|
||||
if updated, err := b.usecase.Alert(ctx, state.alertID); err == nil {
|
||||
b.alerter.AddAlert(updated)
|
||||
}
|
||||
|
||||
b.setState(tgID, &userState{})
|
||||
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)
|
||||
}
|
||||
|
||||
// handleRemoveAlertDo deletes the alert after confirmation.
|
||||
func (b *Bot) handleRemoveAlertDo(ctx context.Context, chatID int64, alertID entities.AlertID) {
|
||||
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.")
|
||||
return
|
||||
}
|
||||
b.alerter.RemoveAlert(alertID)
|
||||
b.sendMenu(chatID, "Alert removed.")
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func (b *Bot) requireUser(ctx context.Context, tgID entities.TelegramID, chatID int64) (*entities.User, error) {
|
||||
user, err := b.usecase.UserByTgID(ctx, tgID)
|
||||
if err != nil {
|
||||
b.sendMenu(chatID, "You are not registered. Please send /start first.")
|
||||
return nil, err
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (b *Bot) getState(tgID entities.TelegramID) *userState {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if s, ok := b.states[tgID]; ok {
|
||||
return s
|
||||
}
|
||||
s := &userState{}
|
||||
b.states[tgID] = s
|
||||
return s
|
||||
}
|
||||
|
||||
func (b *Bot) setState(tgID entities.TelegramID, state *userState) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
b.states[tgID] = state
|
||||
}
|
||||
|
||||
// menuKeyboard returns the persistent reply keyboard shown at the bottom of the chat.
|
||||
func menuKeyboard() tgbotapi.ReplyKeyboardMarkup {
|
||||
return tgbotapi.NewReplyKeyboard(
|
||||
tgbotapi.NewKeyboardButtonRow(
|
||||
tgbotapi.NewKeyboardButton(btnAddAlert),
|
||||
tgbotapi.NewKeyboardButton(btnMyAlerts),
|
||||
),
|
||||
tgbotapi.NewKeyboardButtonRow(
|
||||
tgbotapi.NewKeyboardButton(btnInstruments),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// sendMenu sends a text message and (re-)attaches the persistent menu keyboard.
|
||||
func (b *Bot) sendMenu(chatID int64, text string) {
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
kb := menuKeyboard()
|
||||
kb.ResizeKeyboard = true
|
||||
msg.ReplyMarkup = kb
|
||||
if _, err := b.api.Send(msg); err != nil {
|
||||
b.log.Error("failed to send message", "chat_id", chatID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// send sends a plain text message without altering the keyboard.
|
||||
func (b *Bot) send(chatID int64, text string) {
|
||||
if _, err := b.api.Send(tgbotapi.NewMessage(chatID, text)); err != nil {
|
||||
b.log.Error("failed to send message", "chat_id", chatID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// sendMsg sends a pre-built MessageConfig.
|
||||
func (b *Bot) sendMsg(msg tgbotapi.MessageConfig) {
|
||||
if _, err := b.api.Send(msg); err != nil {
|
||||
b.log.Error("failed to send message", "chat_id", msg.ChatID, "err", err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,16 @@ import (
|
|||
type Config struct {
|
||||
Logger Logger `yaml:"logger"`
|
||||
Postgresql Postgresql `yaml:"postgresql"`
|
||||
Telegram Telegram `yaml:"telegram"`
|
||||
Providers struct {
|
||||
Bybit Bybit `yaml:"bybit"`
|
||||
} `yaml:"providers"`
|
||||
}
|
||||
|
||||
type Telegram struct {
|
||||
Token string `yaml:"token" env-required:"true"`
|
||||
}
|
||||
|
||||
type Logger struct {
|
||||
ServiceName string `yaml:"service_name" env-required:"true"` // service name for printing in logs
|
||||
Encoding string `yaml:"encoding" env-default:"json"` // console/json
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue