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:
111
pkg/storage/scheduled_jobs.go
Normal file
111
pkg/storage/scheduled_jobs.go
Normal 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
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
|
||||
54
pkg/storage/subscribers.go
Normal file
54
pkg/storage/subscribers.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user