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:
3
go.mod
3
go.mod
@@ -35,6 +35,7 @@ require (
|
|||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/go-co-op/gocron/v2 v2.19.1 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
github.com/go-git/go-billy/v5 v5.8.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
@@ -42,6 +43,7 @@ require (
|
|||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.5.0 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
@@ -52,6 +54,7 @@ require (
|
|||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -43,6 +43,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S
|
|||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.19.1 h1:B4iLeA0NB/2iO3EKQ7NfKn5KsQgZfjb2fkvoZJU3yBI=
|
||||||
|
github.com/go-co-op/gocron/v2 v2.19.1/go.mod h1:5lEiCKk1oVJV39Zg7/YG10OnaVrDAV5GGR6O0663k6U=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||||
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
github.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=
|
||||||
@@ -67,6 +69,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||||
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
@@ -105,6 +109,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
|
|||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
|||||||
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_source ON custom_dorks(source);
|
||||||
CREATE INDEX IF NOT EXISTS idx_custom_dorks_category ON custom_dorks(category);
|
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