// 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 }