Read RSS and Atom feeds, and speak about them only when asked (#258)
internal/rss parses RSS 2.0 and Atom, and polls each configured feed on its own interval; internal/webfetch is the one door either of them uses to touch the network. The poller writes items as notes with source "rss:<feed>" and nothing else: the answer path reads them back when he asks "что нового в лентах?", and nothing is announced on arrival. A feed that dispatched would be a nag, which is why the plan's breaking-news rule was left out rather than built. webfetch is where the limits live, as code rather than a paragraph: http(s) only, an allowlist (the configured feeds' hosts) and a denylist, a 2 MiB body cap, a 3-redirect cap, one request per host per second, and a refusal to connect to any private address — checked in the dialer's Control hook so it holds for every resolved address and every redirect hop, not just for a literal IP. Off unless configured: no "feeds" block, no poller, no outbound request. How far a feed was read is a config fact (rss:latest:<name>), so a restart does not re-note yesterday's headlines.
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
package router
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseFeedQuery(t *testing.T) {
|
||||
cases := []struct {
|
||||
text string
|
||||
ok bool
|
||||
category string
|
||||
}{
|
||||
{"что нового в лентах?", true, ""},
|
||||
{"что нового?", true, ""},
|
||||
{"какие новости?", true, ""},
|
||||
{"что нового по технологиям?", true, "технологиям"},
|
||||
{"расскажи новости про политику", true, "политику"},
|
||||
{"что нового по новостям", true, ""},
|
||||
{"what's new in the feeds?", true, ""},
|
||||
{"any news about kubernetes", true, "kubernetes"},
|
||||
// Statements, not requests.
|
||||
{"у меня новая лента в инстаграме", false, ""},
|
||||
{"новости меня утомили", false, ""},
|
||||
{"напомни полить цветы", false, ""},
|
||||
{"", false, ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
q, ok := ParseFeedQuery(c.text)
|
||||
if ok != c.ok {
|
||||
t.Errorf("ParseFeedQuery(%q) ok = %v, want %v", c.text, ok, c.ok)
|
||||
continue
|
||||
}
|
||||
if ok && q.Category != c.category {
|
||||
t.Errorf("ParseFeedQuery(%q) category = %q, want %q", c.text, q.Category, c.category)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCategoryMatches(t *testing.T) {
|
||||
// The inflected form he says must match the form the config spells.
|
||||
if !CategoryMatches("Новый релиз [технологии]", "технологиям") {
|
||||
t.Error("inflected category did not match")
|
||||
}
|
||||
if CategoryMatches("Новый релиз [технологии]", "политику") {
|
||||
t.Error("unrelated category matched")
|
||||
}
|
||||
if !CategoryMatches("anything", "") {
|
||||
t.Error("an empty category must match everything")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user