From 0d00215a261d5a17eeee39a4c3b68bb4be7bdf10 Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Mon, 6 Apr 2026 17:27:41 +0300 Subject: [PATCH] feat(17-01): add telego dependency and create Bot package skeleton - Add telego v1.8.0 as direct dependency for Telegram bot - Create pkg/bot/bot.go with Bot struct, Config, New, Start, Stop - Implement isAllowed chat authorization and per-user rate limiting - Add command dispatch with handler stubs for all 10 commands - Long polling lifecycle with context cancellation for graceful shutdown --- go.mod | 18 +++- go.sum | 40 ++++++++ pkg/bot/bot.go | 259 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 315 insertions(+), 2 deletions(-) create mode 100644 pkg/bot/bot.go diff --git a/go.mod b/go.mod index 3b95c7c..798b5fe 100644 --- a/go.mod +++ b/go.mod @@ -7,14 +7,17 @@ require ( github.com/charmbracelet/lipgloss v1.1.0 github.com/go-git/go-git/v5 v5.17.2 github.com/mattn/go-isatty v0.0.20 + github.com/mymmrac/telego v1.8.0 github.com/panjf2000/ants/v2 v2.12.0 github.com/petar-dambovaliev/aho-corasick v0.0.0-20250424160509-463d218d4745 github.com/spf13/cobra v1.10.2 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 + github.com/temoto/robotstxt v1.1.2 github.com/tidwall/gjson v1.18.0 golang.org/x/crypto v0.49.0 golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 + golang.org/x/net v0.52.0 golang.org/x/time v0.15.0 gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.48.1 @@ -24,12 +27,17 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.8.0 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/cloudflare/circl v1.6.3 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -40,9 +48,12 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/grbit/go-json v0.11.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.18.2 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/muesli/termenv v0.16.0 // indirect @@ -60,13 +71,16 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/temoto/robotstxt v1.1.2 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasthttp v1.69.0 // indirect + github.com/valyala/fastjson v1.6.10 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/net v0.52.0 // indirect + golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.35.0 // indirect diff --git a/go.sum b/go.sum index 96f8ad7..ca15ca4 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= @@ -13,6 +15,12 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= @@ -25,6 +33,8 @@ github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQ github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= @@ -61,6 +71,8 @@ github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17k github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc= +github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -69,6 +81,10 @@ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOl github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk= +github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -84,6 +100,8 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/mymmrac/telego v1.8.0 h1:EvIprWo9Cn0MHgumvvqNXPAXO1yJj3pu2cdCCeDxbow= +github.com/mymmrac/telego v1.8.0/go.mod h1:pdLV346EgVuq7Xrh3kMggeBiazeHhsdEoK0RTEOPXRM= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= @@ -129,9 +147,16 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -144,12 +169,26 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI= +github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw= +github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4= +github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= @@ -190,6 +229,7 @@ gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= diff --git a/pkg/bot/bot.go b/pkg/bot/bot.go new file mode 100644 index 0000000..b369276 --- /dev/null +++ b/pkg/bot/bot.go @@ -0,0 +1,259 @@ +// Package bot implements the Telegram bot interface for KeyHunter. +// It wraps telego v1.8.0 with long-polling updates, per-chat authorization, +// per-user rate limiting, and command dispatch to handler stubs. +package bot + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + "github.com/mymmrac/telego" + "github.com/mymmrac/telego/telegoutil" + "github.com/salvacybersec/keyhunter/pkg/engine" + "github.com/salvacybersec/keyhunter/pkg/providers" + "github.com/salvacybersec/keyhunter/pkg/recon" + "github.com/salvacybersec/keyhunter/pkg/storage" +) + +// Config holds all dependencies and settings for the Telegram bot. +type Config struct { + // Token is the Telegram bot token from BotFather. + Token string + + // AllowedChats restricts bot access to these chat IDs. + // Empty slice means allow all chats. + AllowedChats []int64 + + // DB is the SQLite database for subscriber queries and finding lookups. + DB *storage.DB + + // ScanEngine is the scanning engine for /scan commands. + ScanEngine *engine.Engine + + // ReconEngine is the recon engine for /recon commands. + ReconEngine *recon.Engine + + // ProviderRegistry is the provider registry for /providers and /verify. + ProviderRegistry *providers.Registry + + // EncKey is the encryption key for finding decryption. + EncKey []byte +} + +// Bot wraps a telego.Bot with KeyHunter command handling and authorization. +type Bot struct { + cfg Config + bot *telego.Bot + cancel context.CancelFunc + + rateMu sync.Mutex + rateLimits map[int64]time.Time +} + +// commands is the list of bot commands registered with Telegram. +var commands = []telego.BotCommand{ + {Command: "scan", Description: "Scan a target for API keys"}, + {Command: "verify", Description: "Verify a found API key"}, + {Command: "recon", Description: "Run OSINT recon for a keyword"}, + {Command: "status", Description: "Show bot and scan status"}, + {Command: "stats", Description: "Show finding statistics"}, + {Command: "providers", Description: "List supported providers"}, + {Command: "help", Description: "Show available commands"}, + {Command: "key", Description: "Show full details for a finding"}, + {Command: "subscribe", Description: "Subscribe to scan notifications"}, + {Command: "unsubscribe", Description: "Unsubscribe from notifications"}, +} + +// New creates a new Bot from the given config. Returns an error if the token +// is invalid or telego cannot initialize. +func New(cfg Config) (*Bot, error) { + tb, err := telego.NewBot(cfg.Token) + if err != nil { + return nil, fmt.Errorf("creating telego bot: %w", err) + } + return &Bot{ + cfg: cfg, + bot: tb, + rateLimits: make(map[int64]time.Time), + }, nil +} + +// Start begins long-polling for updates and dispatching commands. It blocks +// until the provided context is cancelled or an error occurs. +func (b *Bot) Start(ctx context.Context) error { + ctx, b.cancel = context.WithCancel(ctx) + + // Register command list with Telegram. + err := b.bot.SetMyCommands(ctx, &telego.SetMyCommandsParams{ + Commands: commands, + }) + if err != nil { + return fmt.Errorf("setting bot commands: %w", err) + } + + updates, err := b.bot.UpdatesViaLongPolling(ctx, nil) + if err != nil { + return fmt.Errorf("starting long polling: %w", err) + } + + for update := range updates { + if update.Message == nil { + continue + } + b.dispatch(ctx, update.Message) + } + + return nil +} + +// Stop cancels the bot context, which stops long polling and the update loop. +func (b *Bot) Stop() { + if b.cancel != nil { + b.cancel() + } +} + +// isAllowed returns true if the given chat ID is authorized to use the bot. +// If AllowedChats is empty, all chats are allowed. +func (b *Bot) isAllowed(chatID int64) bool { + if len(b.cfg.AllowedChats) == 0 { + return true + } + for _, id := range b.cfg.AllowedChats { + if id == chatID { + return true + } + } + return false +} + +// checkRateLimit returns true if the user is allowed to execute a command, +// false if they are still within the cooldown window. +func (b *Bot) checkRateLimit(userID int64, cooldown time.Duration) bool { + b.rateMu.Lock() + defer b.rateMu.Unlock() + + last, ok := b.rateLimits[userID] + if ok && time.Since(last) < cooldown { + return false + } + b.rateLimits[userID] = time.Now() + return true +} + +// dispatch routes an incoming message to the appropriate handler. +func (b *Bot) dispatch(ctx context.Context, msg *telego.Message) { + chatID := msg.Chat.ID + if !b.isAllowed(chatID) { + _ = b.replyPlain(ctx, chatID, "Unauthorized: your chat ID is not in the allowed list.") + return + } + + text := strings.TrimSpace(msg.Text) + if text == "" { + return + } + + // Extract command (first word, with optional @mention suffix removed). + cmd := strings.SplitN(text, " ", 2)[0] + if at := strings.Index(cmd, "@"); at > 0 { + cmd = cmd[:at] + } + + // Determine cooldown based on command type. + var cooldown time.Duration + switch cmd { + case "/scan", "/verify", "/recon": + cooldown = 60 * time.Second + default: + cooldown = 5 * time.Second + } + + if msg.From != nil && !b.checkRateLimit(msg.From.ID, cooldown) { + _ = b.replyPlain(ctx, chatID, "Rate limited. Please wait before sending another command.") + return + } + + switch cmd { + case "/scan": + b.handleScan(ctx, msg) + case "/verify": + b.handleVerify(ctx, msg) + case "/recon": + b.handleRecon(ctx, msg) + case "/status": + b.handleStatus(ctx, msg) + case "/stats": + b.handleStats(ctx, msg) + case "/providers": + b.handleProviders(ctx, msg) + case "/help", "/start": + b.handleHelp(ctx, msg) + case "/key": + b.handleKey(ctx, msg) + case "/subscribe": + b.handleSubscribe(ctx, msg) + case "/unsubscribe": + b.handleUnsubscribe(ctx, msg) + } +} + +// reply sends a MarkdownV2-formatted message to the given chat. +func (b *Bot) reply(ctx context.Context, chatID int64, text string) error { + params := telegoutil.Message(telego.ChatID{ID: chatID}, text). + WithParseMode("MarkdownV2") + _, err := b.bot.SendMessage(ctx, params) + return err +} + +// replyPlain sends a plain text message to the given chat. +func (b *Bot) replyPlain(ctx context.Context, chatID int64, text string) error { + params := telegoutil.Message(telego.ChatID{ID: chatID}, text) + _, err := b.bot.SendMessage(ctx, params) + return err +} + +// --- Handler stubs (implemented in Plan 17-03/17-04) --- + +func (b *Bot) handleScan(ctx context.Context, msg *telego.Message) { + _ = b.replyPlain(ctx, msg.Chat.ID, "Not yet implemented: /scan") +} + +func (b *Bot) handleVerify(ctx context.Context, msg *telego.Message) { + _ = b.replyPlain(ctx, msg.Chat.ID, "Not yet implemented: /verify") +} + +func (b *Bot) handleRecon(ctx context.Context, msg *telego.Message) { + _ = b.replyPlain(ctx, msg.Chat.ID, "Not yet implemented: /recon") +} + +func (b *Bot) handleStatus(ctx context.Context, msg *telego.Message) { + _ = b.replyPlain(ctx, msg.Chat.ID, "Not yet implemented: /status") +} + +func (b *Bot) handleStats(ctx context.Context, msg *telego.Message) { + _ = b.replyPlain(ctx, msg.Chat.ID, "Not yet implemented: /stats") +} + +func (b *Bot) handleProviders(ctx context.Context, msg *telego.Message) { + _ = b.replyPlain(ctx, msg.Chat.ID, "Not yet implemented: /providers") +} + +func (b *Bot) handleHelp(ctx context.Context, msg *telego.Message) { + _ = b.replyPlain(ctx, msg.Chat.ID, "Not yet implemented: /help") +} + +func (b *Bot) handleKey(ctx context.Context, msg *telego.Message) { + _ = b.replyPlain(ctx, msg.Chat.ID, "Not yet implemented: /key") +} + +func (b *Bot) handleSubscribe(ctx context.Context, msg *telego.Message) { + _ = b.replyPlain(ctx, msg.Chat.ID, "Not yet implemented: /subscribe") +} + +func (b *Bot) handleUnsubscribe(ctx context.Context, msg *telego.Message) { + _ = b.replyPlain(ctx, msg.Chat.ID, "Not yet implemented: /unsubscribe") +}