diff --git a/errata.go b/errata.go index b320eaf..3d1eb60 100644 --- a/errata.go +++ b/errata.go @@ -1,31 +1,129 @@ package main import ( + "fmt" + "io" "net/http" + "sort" + "strconv" + "strings" + "time" "golang.org/x/net/html" ) -// ParseErrata grabs all of the OpenBSD errata from an html page -func ParseErrata(s string) ([]string, error) { - var data []string - resp, err := http.Get(s) +// Errata stores all of the erratas grouped by date +type Errata struct { + ID int + Type string + Date time.Time + Coverage string + Desc string + Link string +} + +// Errati is a collection of Errata +type Errati struct { + List []Errata + Length int +} + +// By is the type of a "less" function that defines the ordering of its Planet arguments. +type By func(p1, p2 *Errata) bool + +// Sort is a method on the function type, By, that sorts the argument slice according to the function. +func (by By) Sort(erratas []Errata) { + es := &errataSorter{ + erratas: erratas, + by: by, + } + sort.Sort(es) +} + +// errataSorter joins a By function and a slice of Errati to be sorted. +type errataSorter struct { + erratas []Errata + by func(p1, p2 *Errata) bool // Closure used in the Less method. +} + +// Len is part of sort.Interface. +func (s *errataSorter) Len() int { + return len(s.erratas) +} + +// Swap is part of sort.Interface. +func (s *errataSorter) Swap(i, j int) { + s.erratas[i], s.erratas[j] = s.erratas[j], s.erratas[i] +} + +// Less is part of sort.Interface. It is implemented by calling the "by" closure in the sorter. +func (s *errataSorter) Less(i, j int) bool { + return s.by(&s.erratas[i], &s.erratas[j]) +} + +// ParseErrata does the actual parsing of html (poorly!) +func ParseErrata(body io.Reader) (*Errati, error) { + var data []Errata + var errati = &Errati{} + doc, err := html.Parse(body) if err != nil { return nil, err } - - defer resp.Body.Close() - - doc, err := html.Parse(resp.Body) - if err != nil { - return nil, err - } - var f func(*html.Node) f = func(node *html.Node) { if node.Type == html.ElementNode && node.Data == "strong" { if node.FirstChild != nil { - data = append(data, node.FirstChild.Data) + var e Errata + var err error + parts := strings.Split(node.FirstChild.Data, ": ") + e.ID, err = strconv.Atoi(parts[0]) + if err != nil { + return + } + e.Type = parts[1] + e.Date, err = time.Parse("January 02, 2006", parts[2]) + if err != nil { + return + } + + // TODO: not this. + + if node.NextSibling != nil && + node.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.FirstChild.Data != "" { + e.Coverage = node.NextSibling.NextSibling.FirstChild.Data + } + + if node.NextSibling != nil && + node.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.Data != "" { + e.Desc = node.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.Data + e.Desc = strings.TrimRight(e.Desc, "\n") + e.Desc = strings.TrimLeft(e.Desc, "\n") + } + + if node.NextSibling != nil && + node.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling != nil && + node.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling != nil { + n := node.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling.NextSibling + for _, a := range n.Attr { + if a.Key == "href" { + e.Link = a.Val + break + } + } + } + + data = append(data, e) + errati.Length = errati.Length + 1 return } } @@ -35,5 +133,48 @@ func ParseErrata(s string) ([]string, error) { } f(doc) - return data, nil + byDate := func(p1, p2 *Errata) bool { + return p1.Date.Unix() < p2.Date.Unix() + } + + By(byDate).Sort(data) + errati.List = data + + return errati, nil } + +// ParseRemoteErrata grabs all of the OpenBSD errata from an html page +func ParseRemoteErrata(s string) (*Errati, error) { + resp, err := http.Get(s) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + return ParseErrata(resp.Body) +} + +// PrintErrata pretty prints an errata +func PrintErrata(e *Errata) string { + return fmt.Sprintf("%03d: %s: %s %s\n%s\n%s", + e.ID, + e.Type, + e.Date.String(), + e.Coverage, + e.Desc, + e.Link, + ) +} + +// PrintErrataMD pretty prints an errata in markdown +//func PrintErrataMD(e *Errata) string { +// return fmt.Sprintf("#%03d: %s: %s _%s_\n%s\n[A source code patch exists which remedies this problem.](%s)", +// e.ID, +// e.Type, +// e.Date.String(), +// e.Coverage, +// e.Desc, +// e.Link, +// ) +//} diff --git a/errata_test.go b/errata_test.go index f3379b0..10b8746 100644 --- a/errata_test.go +++ b/errata_test.go @@ -5,11 +5,11 @@ import ( ) func TestParseErrata(t *testing.T) { - got, err := ParseErrata("https://www.openbsd.org/errata66.html") + got, err := ParseRemoteErrata("https://www.openbsd.org/errata66.html") if err != nil { t.Error(err) } - l := len(got) + l := len(got.List) if l == 0 { t.Errorf("errata count %d; want > 0", l) } diff --git a/main.go b/main.go index fb6facb..cb5b9d0 100644 --- a/main.go +++ b/main.go @@ -6,6 +6,8 @@ import ( "log" "net/http" "os" + "strconv" + "strings" "time" "github.com/matrix-org/gomatrix" @@ -142,6 +144,41 @@ func main() { } }) + go func() { + errataCount := 0 + storeCount, _ := store.Get("errata_count") + openbsdRelease, _ := store.Get("openbsd_release") + errataCount, err = strconv.Atoi(storeCount) + for { + got, err := ParseRemoteErrata( + fmt.Sprintf("https://www.openbsd.org/errata%s.html", openbsdRelease), + ) + if err != nil { + fmt.Println(err) + } + l := got.Length + if l > errataCount { + log.Println("Found new errata") + alertRooms, _ := store.Get("errata_rooms") + c := 0 + for _, errata := range got.List { + if c+1 > errataCount { + log.Printf("%03d: %s - %s\n", errata.ID, errata.Type, errata.Desc) + for _, room := range strings.Split(alertRooms, ",") { + log.Printf("sending errata %03d alert to '%s'\n", errata.ID, room) + cli.SendNotice(room, PrintErrata(&errata)) + } + } + c = c + 1 + } + errataCount = l + } + fmt.Printf("Setting errata_count to %d\n", l) + store.Set("errata_count", string(l)) + time.Sleep(2 * time.Hour) + } + }() + syncer.OnEventType("m.room.message", func(ev *gomatrix.Event) { if ev.Sender == username { return