package config // The two scheduled outbound readers: RSS/Atom feeds and watched web pages. // Both are off unless configured, both write notes and neither may speak — // nothing they fetch is dispatched, nudged or announced on arrival. That is the // "not a nag" constraint, and it is why there is no severity or channel field in // either block to reach for. // // Only the URL leaves the box. His notes, facts, persona block and history are // never part of a request; neither package can read the store. // FeedsConfig — the RSS/Atom reader (Vikunja #258, docs/plans/13-rss-news-feeds.md). // // Absent ⇒ off. Present with an empty `sources` list is also off — a poller with // nothing to poll is not wired, and normaliseFeeds folds that back to nil. type FeedsConfig struct { // Sources — the feeds to read. Empty ⇒ the reader stays down. Sources []FeedSourceConfig `json:"sources,omitempty"` // PollInterval — default per-feed cadence. 0 ⇒ rss.DefaultPollInterval (30m). PollInterval Duration `json:"poll_interval,omitempty"` // MaxItems — most items kept from one feed in one poll. 0 ⇒ // rss.DefaultMaxItems (5). This is the "не завали мне /dash" knob. MaxItems int `json:"max_items,omitempty"` // MaxAge — on a first poll (no saved mark), how far back to take items. // 0 ⇒ rss.DefaultMaxAge (24h), so switching a feed on imports today, not // the archive. MaxAge Duration `json:"max_age,omitempty"` // AllowHosts — when set, the reader may only connect to these hosts (and // their subdomains). The feed URLs' own hosts are added automatically, so // this is only needed to be stricter than that. AllowHosts []string `json:"allow_hosts,omitempty"` // Timeout — per-request budget. 0 ⇒ webfetch.DefaultTimeout. Timeout Duration `json:"timeout,omitempty"` // MaxBytes — response size cap. 0 ⇒ webfetch.DefaultMaxBytes (2 MiB). MaxBytes int64 `json:"max_bytes,omitempty"` } // FeedSourceConfig — one feed. type FeedSourceConfig struct { Name string `json:"name"` // note source is "rss:" URL string `json:"url"` // http(s) only Category string `json:"category,omitempty"` // "технологии" — what "что нового по X?" matches Interval Duration `json:"interval,omitempty"` // 0 ⇒ FeedsConfig.PollInterval Include []string `json:"include,omitempty"` // keep only items containing one of these Exclude []string `json:"exclude,omitempty"` // drop items containing any of these } // normaliseFeeds folds a block with no sources back to nil: it is the same as // no block, and keeping that "off" in one place is the point. func (c *Config) normaliseFeeds() { if c.Feeds != nil && len(c.Feeds.Sources) == 0 { c.Feeds = nil } } // CrawlConfig — the web crawler (Vikunja #259, docs/plans/14-web-crawler.md). // // Absent ⇒ off, and off means no page is ever fetched. Present with neither // `on_demand` nor a `watches` entry is also off: there would be nothing to do. // // The crawler is the LAST place an answer is looked for, behind the model, his // own memory, the live search and the local Kiwix ZIMs. That ordering lives in // the query-source chain (cmd/mavend/actions_query.go), not here, but it is the // reason this block is small: it is a fallback, not a search engine. type CrawlConfig struct { // OnDemand — may he ask her to read a page he names out loud // ("посмотри https://… — что там пишут?"). false ⇒ the on-demand answer // source stays off and only the watches below run. OnDemand bool `json:"on_demand,omitempty"` // Watches — pages re-read on a schedule. A page whose text changed is // written as a note (source "crawl:"); nothing is announced. Watches []CrawlWatchConfig `json:"watches,omitempty"` // Interval — default watch cadence. 0 ⇒ crawl.DefaultWatchInterval (6h). Interval Duration `json:"interval,omitempty"` // AllowHosts — when set, the ONLY hosts the crawler may reach (subdomains // included). Setting this is how "she may read the arch wiki and nothing // else" is expressed. // // A watched page's own host is reachable by the scheduled crawler whether // or not it is listed here, because configuring a watch is already saying // she may read it. That does NOT extend to on-demand reading: a watch is // not an allowlist entry for pages he pastes. AllowHosts []string `json:"allow_hosts,omitempty"` // DenyHosts — never reachable, checked first. Private addresses do not need // to be listed: they are refused unconditionally (see internal/webfetch). DenyHosts []string `json:"deny_hosts,omitempty"` // UserAgent — sent on every request AND matched against robots.txt groups. // Empty ⇒ webfetch.DefaultUserAgent. UserAgent string `json:"user_agent,omitempty"` // Timeout — per-request budget. 0 ⇒ webfetch.DefaultTimeout. Timeout Duration `json:"timeout,omitempty"` // MaxBytes — response size cap. 0 ⇒ webfetch.DefaultMaxBytes (2 MiB). MaxBytes int64 `json:"max_bytes,omitempty"` // MaxRunes — how much extracted text is kept. 0 ⇒ crawl.DefaultMaxRunes // (4000), which is what fits a 4096-token context alongside a prompt. MaxRunes int `json:"max_runes,omitempty"` } // CrawlWatchConfig — one page kept an eye on. type CrawlWatchConfig struct { Name string `json:"name"` // note source is "crawl:" URL string `json:"url"` Interval Duration `json:"interval,omitempty"` // 0 ⇒ CrawlConfig.Interval } // normaliseCrawl folds a block that neither answers on demand nor watches // anything back to nil: it has nothing to do. func (c *Config) normaliseCrawl() { if c.Crawl != nil && !c.Crawl.OnDemand && len(c.Crawl.Watches) == 0 { c.Crawl = nil } }