hayden

package module
v0.0.0-...-b7244fb Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 5, 2026 License: MIT Imports: 23 Imported by: 0

README

hayden

Go Report Card GoDoc Create and publish Docker image

Hayden scrapes the web for things and send you a webhook when it finds them. This was initially made to scrape for PS5s being on sale.

Documentation

Overview

Package hayden watches web pages for content and fires a webhook when a target matches.

Index

Constants

View Source
const (
	// Service is the service name used for logging and telemetry.
	Service = "hayden"
)

Variables

This section is empty.

Functions

func AutoMigrate

func AutoMigrate(ctx context.Context, db *gorm.DB) error

AutoMigrate syncs the schema with the current models; safe on every startup.

func Connect

func Connect(ctx context.Context, databaseURL string) (*gorm.DB, error)

Connect opens a Postgres connection at databaseURL and verifies it with PingContext.

func SeedConfig

func SeedConfig(ctx context.Context, store *Store, cf *ConfigFile) (int, error)

SeedConfig inserts the config file's targets into an empty store (no-op if it already has any), defaulting legacy targets to headless fetching.

func ShouldNotify

func ShouldNotify(t *Target, matched bool, contentHash string) bool

ShouldNotify decides whether to fire, per the target's NotifyMode:

  • once (default): fire only on a no-match → match transition.
  • change: fire whenever matched content's hash differs from the last.

Types

type Config

type Config struct {
	Log           *zap.SugaredLogger `json:"-"`
	DefaultHook   string             `json:"default-hook"`
	DefaultPeriod int                `json:"default-period"`
}

Config holds service-wide defaults shared across targets.

type ConfigFile

type ConfigFile struct {
	Config  *Config   `json:"config"`
	Targets []*Target `json:"targets"`
}

ConfigFile is the on-disk representation of the service config and its targets.

func ParseConfigFile

func ParseConfigFile(stream []byte) (*ConfigFile, error)

ParseConfigFile decodes a ConfigFile from JSON bytes.

type Event

type Event struct {
	Target    string    `json:"target"`
	URL       string    `json:"url"`
	Matched   bool      `json:"matched"`
	MatchedAt time.Time `json:"matched_at"`
	MatchType string    `json:"match_type"`
}

Event is the JSON payload POSTed to a target's hook when it matches.

type Fetcher

type Fetcher interface {
	Fetch(ctx context.Context, target *Target) ([]byte, error)
}

Fetcher retrieves the content of a target for matching.

func FetcherFor

func FetcherFor(fetchMode string) (Fetcher, error)

FetcherFor returns the Fetcher for a fetch mode.

type HTTPNotifier

type HTTPNotifier struct {
	Client      *http.Client
	DefaultHook string
}

HTTPNotifier POSTs the event JSON to the target hook (or DefaultHook).

func (HTTPNotifier) Notify

func (n HTTPNotifier) Notify(ctx context.Context, t *Target, ev Event) error

Notify posts ev to the target's effective hook.

type Matcher

type Matcher interface {
	Match(content []byte, value string) (bool, error)
	Validate(value string) error
}

Matcher decides whether fetched content matches a target's configured value, and validates that value up front.

func MatcherFor

func MatcherFor(matchType string) (Matcher, error)

MatcherFor returns the Matcher for a match type.

type Notifier

type Notifier interface {
	Notify(ctx context.Context, t *Target, ev Event) error
}

Notifier delivers a match event for a target.

type Scanner

type Scanner struct {
	Store    *Store
	Notifier Notifier

	// Fetcher overrides per-target resolution when set (tests); nil → FetcherFor.
	Fetcher Fetcher
	// Now overrides the clock (tests).
	Now func() time.Time
}

Scanner runs a single target through fetch → match → notify → persist.

func (*Scanner) Scan

func (sc *Scanner) Scan(ctx context.Context, t *Target) (err error)

Scan runs fetch → match → notify → persist. A failed notify leaves LastMatched unchanged so the next tick retries.

func (*Scanner) ScanAll

func (sc *Scanner) ScanAll(ctx context.Context) error

ScanAll scans every enabled target, continuing past per-target errors.

type Scheduler

type Scheduler struct {
	Scanner *Scanner
	Store   *Store
	Cfg     *Config
	Log     *zap.SugaredLogger

	// PeriodFor overrides per-target period (tests); nil → EffectivePeriod.
	PeriodFor func(*Target) time.Duration
	// contains filtered or unexported fields
}

Scheduler runs one ticker per enabled target, scanning each on its period.

func (*Scheduler) Reload

func (s *Scheduler) Reload(ctx context.Context) error

Reload restarts the tickers to reflect the current set of enabled targets.

func (*Scheduler) Start

func (s *Scheduler) Start(ctx context.Context) error

Start launches a ticker per enabled target; cancel ctx or call Stop to end.

func (*Scheduler) Stop

func (s *Scheduler) Stop()

Stop cancels every ticker and waits for the goroutines to exit.

type Store

type Store struct{ DB *gorm.DB }

Store persists targets and their run-state in Postgres.

func NewStore

func NewStore(db *gorm.DB) *Store

NewStore wraps a gorm DB.

func (*Store) Count

func (s *Store) Count(ctx context.Context) (int64, error)

Count returns the number of non-deleted targets.

func (*Store) Create

func (s *Store) Create(ctx context.Context, t *Target) error

Create inserts a new target.

func (*Store) Delete

func (s *Store) Delete(ctx context.Context, id uint) error

Delete soft-deletes a target by id.

func (*Store) Get

func (s *Store) Get(ctx context.Context, id uint) (*Target, error)

Get returns a target by id (gorm.ErrRecordNotFound if missing).

func (*Store) List

func (s *Store) List(ctx context.Context) ([]*Target, error)

List returns all targets in id order.

func (*Store) ListEnabled

func (s *Store) ListEnabled(ctx context.Context) ([]*Target, error)

ListEnabled returns only enabled targets, in id order.

func (*Store) SaveRunState

func (s *Store) SaveRunState(ctx context.Context, t *Target) error

SaveRunState persists only the run-state columns of t, including zero values.

type Target

type Target struct {
	ID        uint           `gorm:"primaryKey" json:"id"`
	CreatedAt time.Time      `json:"created_at"`
	UpdatedAt time.Time      `json:"updated_at"`
	DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`

	Name       string `gorm:"not null" json:"name"`
	URL        string `gorm:"not null" json:"url"`
	FetchMode  string `gorm:"not null;default:http" json:"fetch_mode"`      // http | headless
	MatchType  string `gorm:"not null;default:substring" json:"match_type"` // substring (css|regex|jsonpath in a later phase)
	MatchValue string `json:"match_value"`
	Invert     bool   `json:"invert"`
	NotifyMode string `gorm:"not null;default:once" json:"notify_mode"` // once (change in a later phase)
	Hook       string `json:"hook,omitempty"`
	Period     int    `json:"period,omitempty"`        // seconds; 0 → Config.DefaultPeriod
	Enabled    bool   `gorm:"not null" json:"enabled"` // defaulted in app code; gorm can't tell unset from false

	LastRunAt       *time.Time `json:"last_run_at,omitempty"`
	LastStatus      string     `json:"last_status,omitempty"` // ok | error
	LastMatchAt     *time.Time `json:"last_match_at,omitempty"`
	LastMatched     bool       `json:"last_matched"`
	LastError       string     `json:"last_error,omitempty"`
	LastContentHash string     `json:"-"`
}

Target is a single web page to watch, the match that triggers its hook, and the persisted state of its most recent scan.

func (*Target) EffectivePeriod

func (t *Target) EffectivePeriod(cfg *Config) time.Duration

EffectivePeriod returns the scan interval, falling back to the service default and finally to five minutes.

Directories

Path Synopsis
cmd
migrate command
Command migrate connects to Postgres and syncs the schema.
Command migrate connects to Postgres and syncs the schema.
Command server runs the hayden HTTP service: watch targets in Postgres, scanned on a schedule, firing a webhook on a match, managed via a small API.
Command server runs the hayden HTTP service: watch targets in Postgres, scanned on a schedule, firing a webhook on a match, managed via a small API.
static
Package static embeds the server's static assets and seed config.
Package static embeds the server's static assets and seed config.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL