feat(17-02): add gocron dependency, subscribers and scheduled_jobs tables with CRUD

- Add gocron/v2 v2.19.1 as direct dependency
- Append subscribers and scheduled_jobs CREATE TABLE to schema.sql
- Implement full subscriber CRUD (Add/Remove/List/IsSubscribed)
- Implement full scheduled job CRUD (Save/List/Get/Delete/UpdateLastRun/SetEnabled)
This commit is contained in:
salvacybersec
2026-04-06 17:25:43 +03:00
parent 0e87618e32
commit c8f7592b73
5 changed files with 194 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
package storage
import (
"database/sql"
"time"
)
// ScheduledJob represents a cron-based scheduled scan job.
type ScheduledJob struct {
ID int64
Name string
CronExpr string
ScanCommand string
NotifyTelegram bool
Enabled bool
LastRun *time.Time
NextRun *time.Time
CreatedAt time.Time
}
// SaveScheduledJob inserts a new scheduled job. Returns the new row ID.
func (db *DB) SaveScheduledJob(j ScheduledJob) (int64, error) {
res, err := db.sql.Exec(
`INSERT INTO scheduled_jobs (name, cron_expr, scan_command, notify_telegram, enabled)
VALUES (?, ?, ?, ?, ?)`,
j.Name, j.CronExpr, j.ScanCommand, j.NotifyTelegram, j.Enabled,
)
if err != nil {
return 0, err
}
return res.LastInsertId()
}
// ListScheduledJobs returns all scheduled jobs.
func (db *DB) ListScheduledJobs() ([]ScheduledJob, error) {
rows, err := db.sql.Query(
`SELECT id, name, cron_expr, scan_command, notify_telegram, enabled, last_run, next_run, created_at
FROM scheduled_jobs ORDER BY id`,
)
if err != nil {
return nil, err
}
defer rows.Close()
var jobs []ScheduledJob
for rows.Next() {
var j ScheduledJob
var lastRun, nextRun sql.NullTime
if err := rows.Scan(&j.ID, &j.Name, &j.CronExpr, &j.ScanCommand,
&j.NotifyTelegram, &j.Enabled, &lastRun, &nextRun, &j.CreatedAt); err != nil {
return nil, err
}
if lastRun.Valid {
j.LastRun = &lastRun.Time
}
if nextRun.Valid {
j.NextRun = &nextRun.Time
}
jobs = append(jobs, j)
}
return jobs, rows.Err()
}
// GetScheduledJob returns a single scheduled job by name.
func (db *DB) GetScheduledJob(name string) (*ScheduledJob, error) {
var j ScheduledJob
var lastRun, nextRun sql.NullTime
err := db.sql.QueryRow(
`SELECT id, name, cron_expr, scan_command, notify_telegram, enabled, last_run, next_run, created_at
FROM scheduled_jobs WHERE name = ?`, name,
).Scan(&j.ID, &j.Name, &j.CronExpr, &j.ScanCommand,
&j.NotifyTelegram, &j.Enabled, &lastRun, &nextRun, &j.CreatedAt)
if err != nil {
return nil, err
}
if lastRun.Valid {
j.LastRun = &lastRun.Time
}
if nextRun.Valid {
j.NextRun = &nextRun.Time
}
return &j, nil
}
// DeleteScheduledJob removes a scheduled job by name. Returns rows affected.
func (db *DB) DeleteScheduledJob(name string) (int64, error) {
res, err := db.sql.Exec(`DELETE FROM scheduled_jobs WHERE name = ?`, name)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// UpdateJobLastRun updates the last_run and next_run timestamps for a job.
func (db *DB) UpdateJobLastRun(name string, lastRun time.Time, nextRun *time.Time) error {
var nr sql.NullTime
if nextRun != nil {
nr = sql.NullTime{Time: *nextRun, Valid: true}
}
_, err := db.sql.Exec(
`UPDATE scheduled_jobs SET last_run = ?, next_run = ? WHERE name = ?`,
lastRun, nr, name,
)
return err
}
// SetJobEnabled updates the enabled flag for a scheduled job.
func (db *DB) SetJobEnabled(name string, enabled bool) error {
_, err := db.sql.Exec(`UPDATE scheduled_jobs SET enabled = ? WHERE name = ?`, enabled, name)
return err
}

View File

@@ -55,3 +55,23 @@ CREATE TABLE IF NOT EXISTS custom_dorks (
CREATE INDEX IF NOT EXISTS idx_custom_dorks_source ON custom_dorks(source);
CREATE INDEX IF NOT EXISTS idx_custom_dorks_category ON custom_dorks(category);
-- 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
);

View File

@@ -0,0 +1,54 @@
package storage
import "time"
// Subscriber represents a Telegram chat subscribed to scan notifications.
type Subscriber struct {
ChatID int64
Username string
SubscribedAt time.Time
}
// AddSubscriber inserts or replaces a subscriber in the database.
func (db *DB) AddSubscriber(chatID int64, username string) error {
_, err := db.sql.Exec(
`INSERT OR REPLACE INTO subscribers (chat_id, username, subscribed_at) VALUES (?, ?, CURRENT_TIMESTAMP)`,
chatID, username,
)
return err
}
// RemoveSubscriber deletes a subscriber by chat ID. Returns rows affected.
func (db *DB) RemoveSubscriber(chatID int64) (int64, error) {
res, err := db.sql.Exec(`DELETE FROM subscribers WHERE chat_id = ?`, chatID)
if err != nil {
return 0, err
}
return res.RowsAffected()
}
// ListSubscribers returns all subscribers ordered by subscription time.
func (db *DB) ListSubscribers() ([]Subscriber, error) {
rows, err := db.sql.Query(`SELECT chat_id, username, subscribed_at FROM subscribers ORDER BY subscribed_at`)
if err != nil {
return nil, err
}
defer rows.Close()
var subs []Subscriber
for rows.Next() {
var s Subscriber
if err := rows.Scan(&s.ChatID, &s.Username, &s.SubscribedAt); err != nil {
return nil, err
}
subs = append(subs, s)
}
return subs, rows.Err()
}
// IsSubscribed returns true if the given chat ID is subscribed.
func (db *DB) IsSubscribed(chatID int64) (bool, error) {
var count int
err := db.sql.QueryRow(`SELECT COUNT(*) FROM subscribers WHERE chat_id = ?`, chatID).Scan(&count)
return count > 0, err
}