Files
keyhunter/.planning/phases/17-telegram-scheduler/17-02-PLAN.md

238 lines
9.8 KiB
Markdown

---
phase: 17-telegram-scheduler
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
- pkg/scheduler/scheduler.go
- pkg/scheduler/jobs.go
- pkg/scheduler/scheduler_test.go
- pkg/storage/schema.sql
- pkg/storage/subscribers.go
- pkg/storage/scheduled_jobs.go
- go.mod
- go.sum
autonomous: true
requirements: [SCHED-01]
must_haves:
truths:
- "Scheduler loads enabled jobs from SQLite on startup and registers them with gocron"
- "Scheduled jobs persist across restarts (stored in scheduled_jobs table)"
- "Subscriber chat IDs persist in subscribers table"
- "Scheduler executes scan at cron intervals"
artifacts:
- path: "pkg/scheduler/scheduler.go"
provides: "Scheduler struct wrapping gocron with start/stop lifecycle"
exports: ["Scheduler", "New", "Start", "Stop"]
- path: "pkg/scheduler/jobs.go"
provides: "Job struct and CRUD operations"
exports: ["Job"]
- path: "pkg/storage/scheduled_jobs.go"
provides: "SQLite CRUD for scheduled_jobs table"
exports: ["ScheduledJob", "SaveScheduledJob", "ListScheduledJobs", "DeleteScheduledJob", "UpdateJobLastRun"]
- path: "pkg/storage/subscribers.go"
provides: "SQLite CRUD for subscribers table"
exports: ["Subscriber", "AddSubscriber", "RemoveSubscriber", "ListSubscribers"]
- path: "pkg/storage/schema.sql"
provides: "subscribers and scheduled_jobs CREATE TABLE statements"
contains: "CREATE TABLE IF NOT EXISTS subscribers"
key_links:
- from: "pkg/scheduler/scheduler.go"
to: "github.com/go-co-op/gocron/v2"
via: "gocron.NewScheduler + AddJob"
pattern: "gocron\\.NewScheduler"
- from: "pkg/scheduler/scheduler.go"
to: "pkg/storage"
via: "DB.ListScheduledJobs for startup load"
pattern: "db\\.ListScheduledJobs"
---
<objective>
Create the pkg/scheduler/ package and the SQLite storage tables (subscribers, scheduled_jobs) that both the bot and scheduler depend on.
Purpose: Establishes cron-based recurring scan infrastructure and the persistence layer for subscriptions and jobs. Independent of pkg/bot/ (Wave 1 parallel).
Output: pkg/scheduler/, pkg/storage/subscribers.go, pkg/storage/scheduled_jobs.go, updated schema.sql.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/17-telegram-scheduler/17-CONTEXT.md
@pkg/storage/db.go
@pkg/storage/schema.sql
@pkg/engine/engine.go
</context>
<interfaces>
<!-- Key types the executor needs from existing codebase -->
From pkg/storage/db.go:
```go
type DB struct { sql *sql.DB }
func Open(path string) (*DB, error)
func (db *DB) Close() error
func (db *DB) SQL() *sql.DB
```
From pkg/engine/engine.go:
```go
type ScanConfig struct { Workers int; Verify bool; Unmask bool }
func (e *Engine) Scan(ctx context.Context, src sources.Source, cfg ScanConfig) (<-chan Finding, error)
```
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Add gocron dependency, create storage tables, and subscriber/job CRUD</name>
<files>go.mod, go.sum, pkg/storage/schema.sql, pkg/storage/subscribers.go, pkg/storage/scheduled_jobs.go</files>
<action>
1. Run `go get github.com/go-co-op/gocron/v2@v2.19.1` to add gocron as a direct dependency.
2. Append to pkg/storage/schema.sql (after existing custom_dorks table):
```sql
-- Phase 17: Telegram bot subscribers for auto-notifications.
CREATE TABLE IF NOT EXISTS subscribers (
chat_id INTEGER PRIMARY KEY,
username TEXT,
subscribed_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- Phase 17: Cron-based scheduled scan jobs.
CREATE TABLE IF NOT EXISTS scheduled_jobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
cron_expr TEXT NOT NULL,
scan_command TEXT NOT NULL,
notify_telegram BOOLEAN DEFAULT FALSE,
enabled BOOLEAN DEFAULT TRUE,
last_run DATETIME,
next_run DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
3. Create pkg/storage/subscribers.go:
- `Subscriber` struct: `ChatID int64`, `Username string`, `SubscribedAt time.Time`
- `(db *DB) AddSubscriber(chatID int64, username string) error` — INSERT OR REPLACE
- `(db *DB) RemoveSubscriber(chatID int64) (int64, error)` — DELETE, return rows affected
- `(db *DB) ListSubscribers() ([]Subscriber, error)` — SELECT all
- `(db *DB) IsSubscribed(chatID int64) (bool, error)` — SELECT count
4. Create pkg/storage/scheduled_jobs.go:
- `ScheduledJob` struct: `ID int64`, `Name string`, `CronExpr string`, `ScanCommand string`, `NotifyTelegram bool`, `Enabled bool`, `LastRun *time.Time`, `NextRun *time.Time`, `CreatedAt time.Time`
- `(db *DB) SaveScheduledJob(j ScheduledJob) (int64, error)` — INSERT
- `(db *DB) ListScheduledJobs() ([]ScheduledJob, error)` — SELECT all
- `(db *DB) GetScheduledJob(name string) (*ScheduledJob, error)` — SELECT by name
- `(db *DB) DeleteScheduledJob(name string) (int64, error)` — DELETE by name, return rows affected
- `(db *DB) UpdateJobLastRun(name string, lastRun time.Time, nextRun *time.Time) error` — UPDATE last_run and next_run
- `(db *DB) SetJobEnabled(name string, enabled bool) error` — UPDATE enabled flag
</action>
<verify>
<automated>cd /home/salva/Documents/apikey && go build ./pkg/storage/...</automated>
</verify>
<done>schema.sql has subscribers and scheduled_jobs tables. Storage CRUD methods compile.</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Scheduler package with gocron wrapper and startup job loading</name>
<files>pkg/scheduler/scheduler.go, pkg/scheduler/jobs.go, pkg/scheduler/scheduler_test.go</files>
<behavior>
- Test 1: SaveScheduledJob + ListScheduledJobs round-trips correctly in :memory: DB
- Test 2: AddSubscriber + ListSubscribers round-trips correctly
- Test 3: Scheduler.Start loads jobs from DB and registers with gocron
- Test 4: Scheduler.AddJob persists to DB and registers cron job
- Test 5: Scheduler.RemoveJob removes from DB and gocron
</behavior>
<action>
1. Create pkg/scheduler/jobs.go:
- `Job` struct mirroring storage.ScheduledJob but with a `RunFunc func(context.Context) (int, error)` field (the scan function to call; returns finding count + error)
- `JobResult` struct: `JobName string`, `FindingCount int`, `Duration time.Duration`, `Error error`
2. Create pkg/scheduler/scheduler.go:
- `Config` struct:
- `DB *storage.DB`
- `ScanFunc func(ctx context.Context, scanCommand string) (int, error)` — abstracted scan executor (avoids tight coupling to engine)
- `OnComplete func(result JobResult)` — callback for notification bridge (Plan 17-04 wires this)
- `Scheduler` struct:
- `cfg Config`
- `sched gocron.Scheduler` (gocron scheduler instance)
- `jobs map[string]gocron.Job` (gocron job handles keyed by name)
- `mu sync.Mutex`
- `New(cfg Config) (*Scheduler, error)`:
- Create gocron scheduler via `gocron.NewScheduler()`
- Return Scheduler
- `Start(ctx context.Context) error`:
- Load all enabled jobs from DB via `cfg.DB.ListScheduledJobs()`
- For each, call internal `registerJob(job)` which creates a gocron.CronJob and stores handle
- Call `sched.Start()` to begin scheduling
- `Stop() error`:
- Call `sched.Shutdown()` to stop all jobs
- `AddJob(name, cronExpr, scanCommand string, notifyTelegram bool) error`:
- Save to DB via `cfg.DB.SaveScheduledJob`
- Register with gocron via `registerJob`
- `RemoveJob(name string) error`:
- Remove gocron job handle from `jobs` map and call `sched.RemoveJob`
- Delete from DB via `cfg.DB.DeleteScheduledJob`
- `ListJobs() ([]storage.ScheduledJob, error)`:
- Delegate to `cfg.DB.ListScheduledJobs()`
- `RunJob(ctx context.Context, name string) (JobResult, error)`:
- Manual trigger — look up job in DB, call ScanFunc directly, call OnComplete callback
- Internal `registerJob(sj storage.ScheduledJob)`:
- Create gocron job: `sched.NewJob(gocron.CronJob(sj.CronExpr, false), gocron.NewTask(func() { ... }))`
- The task function: call `cfg.ScanFunc(ctx, sj.ScanCommand)`, update last_run/next_run via DB, call `cfg.OnComplete` if sj.NotifyTelegram
3. Create pkg/scheduler/scheduler_test.go:
- Use storage.Open(":memory:") for all tests
- TestStorageRoundTrip: Save job, list, verify fields match
- TestSubscriberRoundTrip: Add subscriber, list, verify; remove, verify empty
- TestSchedulerStartLoadsJobs: Save 2 enabled jobs to DB, create Scheduler with mock ScanFunc, call Start, verify gocron has 2 jobs registered (check len(s.jobs)==2)
- TestSchedulerAddRemoveJob: Add via Scheduler.AddJob, verify in DB; Remove, verify gone from DB
- TestSchedulerRunJob: Manual trigger via RunJob, verify ScanFunc called with correct scanCommand, verify OnComplete called with result
</action>
<verify>
<automated>cd /home/salva/Documents/apikey && go test ./pkg/scheduler/... ./pkg/storage/... -v -count=1 -run "TestStorage|TestSubscriber|TestScheduler"</automated>
</verify>
<done>Scheduler starts, loads jobs from DB, registers with gocron. AddJob/RemoveJob/RunJob work end-to-end. All tests pass.</done>
</task>
</tasks>
<verification>
- `go build ./pkg/scheduler/...` compiles without errors
- `go test ./pkg/scheduler/... -v` passes all tests
- `go test ./pkg/storage/... -v -run Subscriber` passes subscriber CRUD tests
- `go test ./pkg/storage/... -v -run ScheduledJob` passes job CRUD tests
- `grep gocron go.mod` shows direct dependency at v2.19.1
</verification>
<success_criteria>
- pkg/scheduler/ exists with Scheduler struct, gocron wrapper, job loading from DB
- pkg/storage/subscribers.go and pkg/storage/scheduled_jobs.go exist with full CRUD
- schema.sql has both new tables
- gocron v2.19.1 is a direct dependency in go.mod
- All tests pass
</success_criteria>
<output>
After completion, create `.planning/phases/17-telegram-scheduler/17-02-SUMMARY.md`
</output>