Files

9.8 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
17-telegram-scheduler 02 execute 1
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
true
SCHED-01
truths artifacts key_links
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
path provides exports
pkg/scheduler/scheduler.go Scheduler struct wrapping gocron with start/stop lifecycle
Scheduler
New
Start
Stop
path provides exports
pkg/scheduler/jobs.go Job struct and CRUD operations
Job
path provides exports
pkg/storage/scheduled_jobs.go SQLite CRUD for scheduled_jobs table
ScheduledJob
SaveScheduledJob
ListScheduledJobs
DeleteScheduledJob
UpdateJobLastRun
path provides exports
pkg/storage/subscribers.go SQLite CRUD for subscribers table
Subscriber
AddSubscriber
RemoveSubscriber
ListSubscribers
path provides contains
pkg/storage/schema.sql subscribers and scheduled_jobs CREATE TABLE statements CREATE TABLE IF NOT EXISTS subscribers
from to via pattern
pkg/scheduler/scheduler.go github.com/go-co-op/gocron/v2 gocron.NewScheduler + AddJob gocron.NewScheduler
from to via pattern
pkg/scheduler/scheduler.go pkg/storage DB.ListScheduledJobs for startup load db.ListScheduledJobs
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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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

From pkg/storage/db.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:

type ScanConfig struct { Workers int; Verify bool; Unmask bool }
func (e *Engine) Scan(ctx context.Context, src sources.Source, cfg ScanConfig) (<-chan Finding, error)
Task 1: Add gocron dependency, create storage tables, and subscriber/job CRUD go.mod, go.sum, pkg/storage/schema.sql, pkg/storage/subscribers.go, pkg/storage/scheduled_jobs.go 1. Run `go get github.com/go-co-op/gocron/v2@v2.19.1` to add gocron as a direct dependency.
  1. Append to pkg/storage/schema.sql (after existing custom_dorks table):
-- 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
);
  1. 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
  1. 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 cd /home/salva/Documents/apikey && go build ./pkg/storage/... schema.sql has subscribers and scheduled_jobs tables. Storage CRUD methods compile.
Task 2: Scheduler package with gocron wrapper and startup job loading pkg/scheduler/scheduler.go, pkg/scheduler/jobs.go, pkg/scheduler/scheduler_test.go - 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 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`
  1. 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
  1. 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 cd /home/salva/Documents/apikey && go test ./pkg/scheduler/... ./pkg/storage/... -v -count=1 -run "TestStorage|TestSubscriber|TestScheduler" Scheduler starts, loads jobs from DB, registers with gocron. AddJob/RemoveJob/RunJob work end-to-end. All tests pass.
- `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

<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>
After completion, create `.planning/phases/17-telegram-scheduler/17-02-SUMMARY.md`