Files
keyhunter/pkg/bot/bot.go
salvacybersec 9ad58534fc feat(17-03): implement Telegram bot command handlers
- Add telego v1.8.0 dependency for Telegram Bot API
- Create pkg/bot package with Bot struct holding engine, verifier, recon, storage, registry deps
- Implement 8 command handlers: /help, /scan, /verify, /recon, /status, /stats, /providers, /key
- /key enforced private-chat-only for security (never exposes unmasked keys in groups)
- All other commands use masked keys only
- Handler registration via telego's BotHandler with CommandEqual predicates
2026-04-06 17:34:44 +03:00

141 lines
3.9 KiB
Go

// Package bot implements the Telegram bot interface for KeyHunter.
// It wraps existing scan, verify, recon, and storage functionality,
// exposing them through Telegram command handlers via the telego library.
package bot
import (
"context"
"fmt"
"sync"
"time"
"github.com/mymmrac/telego"
th "github.com/mymmrac/telego/telegohandler"
"github.com/salvacybersec/keyhunter/pkg/engine"
"github.com/salvacybersec/keyhunter/pkg/providers"
"github.com/salvacybersec/keyhunter/pkg/recon"
"github.com/salvacybersec/keyhunter/pkg/storage"
"github.com/salvacybersec/keyhunter/pkg/verify"
)
// Bot holds the Telegram bot instance and all dependencies needed
// to process commands. It delegates to the existing KeyHunter engine,
// verifier, recon engine, and storage layer.
type Bot struct {
api *telego.Bot
handler *th.BotHandler
engine *engine.Engine
verifier *verify.HTTPVerifier
recon *recon.Engine
db *storage.DB
registry *providers.Registry
encKey []byte
mu sync.Mutex
startedAt time.Time
lastScan time.Time
}
// Deps bundles the dependencies required to construct a Bot.
type Deps struct {
Engine *engine.Engine
Verifier *verify.HTTPVerifier
Recon *recon.Engine
DB *storage.DB
Registry *providers.Registry
EncKey []byte
}
// New creates a Bot backed by the given telego API client and dependencies.
// Call RegisterHandlers to wire up command handlers before starting the update loop.
func New(api *telego.Bot, deps Deps) *Bot {
return &Bot{
api: api,
engine: deps.Engine,
verifier: deps.Verifier,
recon: deps.Recon,
db: deps.DB,
registry: deps.Registry,
encKey: deps.EncKey,
startedAt: time.Now(),
}
}
// RegisterHandlers wires all command handlers into a BotHandler that processes
// updates from the Telegram API. The caller must call Start() on the returned
// BotHandler to begin processing.
func (b *Bot) RegisterHandlers(updates <-chan telego.Update) *th.BotHandler {
bh, _ := th.NewBotHandler(b.api, updates)
bh.HandleMessage(b.handleHelp, th.CommandEqual("help"))
bh.HandleMessage(b.handleScan, th.CommandEqual("scan"))
bh.HandleMessage(b.handleVerify, th.CommandEqual("verify"))
bh.HandleMessage(b.handleRecon, th.CommandEqual("recon"))
bh.HandleMessage(b.handleStatus, th.CommandEqual("status"))
bh.HandleMessage(b.handleStats, th.CommandEqual("stats"))
bh.HandleMessage(b.handleProviders, th.CommandEqual("providers"))
bh.HandleMessage(b.handleKey, th.CommandEqual("key"))
b.handler = bh
return bh
}
// reply sends a text message back to the chat that originated msg.
func (b *Bot) reply(ctx context.Context, msg *telego.Message, text string) {
_, _ = b.api.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: msg.Chat.ID},
Text: text,
})
}
// isPrivateChat returns true if the message was sent in a private (1:1) chat.
func isPrivateChat(msg *telego.Message) bool {
return msg.Chat.Type == "private"
}
// runScan executes a scan against the given path and returns findings.
// Findings are collected synchronously; the caller formats the output.
func (b *Bot) runScan(ctx context.Context, path string) ([]engine.Finding, error) {
src, err := selectBotSource(path)
if err != nil {
return nil, err
}
cfg := engine.ScanConfig{
Workers: 0, // auto
Verify: false,
Unmask: false,
}
ch, err := b.engine.Scan(ctx, src, cfg)
if err != nil {
return nil, fmt.Errorf("starting scan: %w", err)
}
var findings []engine.Finding
for f := range ch {
findings = append(findings, f)
}
// Persist findings
for _, f := range findings {
sf := storage.Finding{
ProviderName: f.ProviderName,
KeyValue: f.KeyValue,
KeyMasked: f.KeyMasked,
Confidence: f.Confidence,
SourcePath: f.Source,
SourceType: f.SourceType,
LineNumber: f.LineNumber,
}
_, _ = b.db.SaveFinding(sf, b.encKey)
}
b.mu.Lock()
b.lastScan = time.Now()
b.mu.Unlock()
return findings, nil
}