Files
mesh-catalog/modules/pacman/cmd/pacman-tools/news.go
T
jochen d9336d11d0 pacman: the package manager's configuration, mirrors and cache as a module
Mirrors were generated once and never again and caches never cleaned. The
module holds node-package-manager (hq ADR 0207), declares pacman itself, owns
/etc/pacman.conf whole — [options] cannot take an appended block — with the
union of the enabled repositories and improved options, proven by pacman-conf
in its test, and enables reflector.timer (its config owned) and
paccache.timer. Fifteen tools from a Go bundle; transactions run as transient
units so a call's timeout never kills pacman mid-transaction (to-be 42).
2026-10-04 12:50:20 +02:00

111 lines
3.1 KiB
Go

package main
// The distribution's news since the last upgrade (novox/hq research 026/05: "an upgrade with the
// news first"). Arch posts what an upgrade needs a person to do — a manual intervention, a replaced
// package — in its news feed, and an upgrade that ignores it is how a machine breaks. Fetched over
// https; no network is an answer, never a failure.
import (
"encoding/xml"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"time"
)
// NewsFeed is the distribution's news, as RSS.
const NewsFeed = "https://archlinux.org/feeds/news/"
// fetch is how the feed is read; a test replaces it.
var fetch = func(url string) ([]byte, error) {
client := http.Client{Timeout: 10 * time.Second}
res, err := client.Get(url)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s answered %s", url, res.Status)
}
return io.ReadAll(io.LimitReader(res.Body, 4<<20))
}
// NewsItem is one post.
type NewsItem struct {
Title string `json:"title"`
Link string `json:"link"`
Published string `json:"published"`
Summary string `json:"summary"`
}
type rss struct {
Items []struct {
Title string `xml:"title"`
Link string `xml:"link"`
PubDate string `xml:"pubDate"`
Description string `xml:"description"`
} `xml:"channel>item"`
}
var tags = regexp.MustCompile(`<[^>]*>`)
// ParseNews reads the feed's posts published after a time, newest first as the feed has them.
func ParseNews(body []byte, since time.Time) ([]NewsItem, error) {
var feed rss
if err := xml.Unmarshal(body, &feed); err != nil {
return nil, err
}
items := []NewsItem{}
for _, it := range feed.Items {
t, err := time.Parse(time.RFC1123Z, strings.TrimSpace(it.PubDate))
if err != nil {
t, err = time.Parse(time.RFC1123, strings.TrimSpace(it.PubDate))
}
if err != nil || !t.After(since) {
continue
}
summary := strings.Join(strings.Fields(tags.ReplaceAllString(it.Description, " ")), " ")
if len(summary) > 600 {
summary = summary[:600] + "…"
}
items = append(items, NewsItem{Title: it.Title, Link: it.Link, Published: t.Format(time.RFC3339), Summary: summary})
}
return items, nil
}
// News is the posts since a day (YYYY-MM-DD), or since the last full upgrade the log records.
func (m *Machine) News(since string) map[string]any {
out := map[string]any{"feed": NewsFeed, "items": []NewsItem{}}
var from time.Time
if since != "" {
t, err := time.ParseInLocation("2006-01-02", since, time.Local)
if err != nil {
out["error"] = fmt.Sprintf("since %q is not a day as YYYY-MM-DD", since)
return out
}
from = t
} else if last, err := m.LastUpgrade(); err == nil && !last.IsZero() {
from = last
out["since_last_full_upgrade"] = true
} else {
from = m.Now().AddDate(0, 0, -90)
}
out["since"] = from.Format(time.RFC3339)
body, err := fetch(NewsFeed)
if err != nil {
out["reachable"] = false
out["error"] = err.Error()
return out
}
out["reachable"] = true
items, err := ParseNews(body, from)
if err != nil {
out["error"] = "the feed could not be read: " + err.Error()
return out
}
out["items"] = items
return out
}