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
This commit is contained in:
140
pkg/bot/bot.go
Normal file
140
pkg/bot/bot.go
Normal file
@@ -0,0 +1,140 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user