Files
nofx/nofxi/internal/interaction/telegram.go
shinchan-zhai cf7bf16c28 feat: init nofxi - AI Trading Agent module
NOFXi — Your AI Trading Agent, built on NOFX.

Architecture:
- Agent Core: intent routing, conversation memory, trade confirmation
- Memory: SQLite (trades, conversations, preferences, strategies)
- Thinking: LLM client (OpenAI/claw402/DeepSeek compatible)
- Execution: Bridge to NOFX trader engine (9 exchanges)
- Perception: Market monitor, price alerts, anomaly detection
- Interaction: Telegram bot + REST API + OpenAI-compatible endpoint

Features:
- Natural language trading (中英文)
- /buy /sell /analyze /watch /alert /positions /balance
- Daily P/L reports, portfolio risk checks
- Trade confirmation flow (safety first)
- Price polling and alert notifications
2026-03-22 17:48:14 +08:00

129 lines
3.2 KiB
Go

package interaction
import (
"context"
"fmt"
"log/slog"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// MessageHandler is called for each incoming message.
type MessageHandler func(ctx context.Context, userID int64, text string) (string, error)
// TelegramBot wraps the Telegram Bot API.
type TelegramBot struct {
bot *tgbotapi.BotAPI
handler MessageHandler
allowedIDs map[int64]bool // Empty = allow all
logger *slog.Logger
}
// NewTelegramBot creates a new Telegram bot.
func NewTelegramBot(token string, allowedIDs []int64, handler MessageHandler, logger *slog.Logger) (*TelegramBot, error) {
bot, err := tgbotapi.NewBotAPI(token)
if err != nil {
return nil, fmt.Errorf("create bot: %w", err)
}
allowed := make(map[int64]bool)
for _, id := range allowedIDs {
allowed[id] = true
}
logger.Info("telegram bot authorized", "username", bot.Self.UserName)
return &TelegramBot{
bot: bot,
handler: handler,
allowedIDs: allowed,
logger: logger,
}, nil
}
// Start begins polling for updates. Blocks until context is cancelled.
func (t *TelegramBot) Start(ctx context.Context) error {
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := t.bot.GetUpdatesChan(u)
t.logger.Info("telegram bot started, listening for messages...")
for {
select {
case <-ctx.Done():
t.bot.StopReceivingUpdates()
t.logger.Info("telegram bot stopped")
return nil
case update := <-updates:
if update.Message == nil {
continue
}
go t.handleUpdate(ctx, update)
}
}
}
func (t *TelegramBot) handleUpdate(ctx context.Context, update tgbotapi.Update) {
msg := update.Message
userID := msg.From.ID
// Access control
if len(t.allowedIDs) > 0 && !t.allowedIDs[userID] {
t.logger.Warn("unauthorized user", "user_id", userID, "username", msg.From.UserName)
t.sendText(msg.Chat.ID, "⛔ Unauthorized. Contact the admin to get access.")
return
}
text := msg.Text
if text == "" {
return
}
t.logger.Info("received message",
"user_id", userID,
"username", msg.From.UserName,
"text", text,
)
// Send "typing" indicator
typing := tgbotapi.NewChatAction(msg.Chat.ID, tgbotapi.ChatTyping)
t.bot.Send(typing)
// Process message
response, err := t.handler(ctx, userID, text)
if err != nil {
t.logger.Error("handle message", "error", err)
response = fmt.Sprintf("⚠️ Error: %v", err)
}
t.sendMarkdown(msg.Chat.ID, response)
}
func (t *TelegramBot) sendText(chatID int64, text string) {
msg := tgbotapi.NewMessage(chatID, text)
if _, err := t.bot.Send(msg); err != nil {
t.logger.Error("send message", "error", err)
}
}
func (t *TelegramBot) sendMarkdown(chatID int64, text string) {
msg := tgbotapi.NewMessage(chatID, text)
msg.ParseMode = tgbotapi.ModeMarkdown
if _, err := t.bot.Send(msg); err != nil {
// Retry without markdown if parsing fails
t.logger.Warn("markdown send failed, retrying plain", "error", err)
t.sendText(chatID, text)
}
}
// SendNotification sends a proactive message to a user.
func (t *TelegramBot) SendNotification(userID int64, text string) error {
msg := tgbotapi.NewMessage(userID, text)
msg.ParseMode = tgbotapi.ModeMarkdown
_, err := t.bot.Send(msg)
return err
}