238 lines
9.8 KiB
Markdown
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>
|