diff --git a/feed.tmpl.xml b/feed.tmpl.xml
new file mode 100644
index 0000000..bedddb2
--- /dev/null
+++ b/feed.tmpl.xml
@@ -0,0 +1,19 @@
+
+
+
+ tilde.town blog
+ web log of tilde town
+ https://tilde.town/blog.html
+
+ {{ range .News }}
+ -
+ {{.Title}}
+ {{.Pubdate}}
+
+
+
+ {{.Pubdate}}-{{.Title}}
+
+ {{ end }}
+
+
diff --git a/generate_homepage b/generate_homepage
index 6344b92..6dec788 100644
--- a/generate_homepage
+++ b/generate_homepage
@@ -6,5 +6,6 @@ set -e
cd /town/src/tilde.town
/usr/bin/go run genblog.go > blog.html
+/usr/bin/go run genfeed.go > blog.xml
/usr/bin/go run genusers.go > users.html
-/bin/cp index.html blog.html users.html blog.css style.css /var/www/tilde.town/
+/bin/cp index.html blog.html blog.xml users.html blog.css style.css /var/www/tilde.town/
diff --git a/genfeed.go b/genfeed.go
new file mode 100644
index 0000000..1ed8e08
--- /dev/null
+++ b/genfeed.go
@@ -0,0 +1,121 @@
+package main
+
+import (
+ "bytes"
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "os"
+ "os/exec"
+ "sort"
+ "text/template"
+)
+
+const statsPath = "/usr/local/bin/stats"
+
+//go:embed feed.tmpl.xml
+var feedTmpl string
+
+type newsEntry struct {
+ Title string // Title of entry
+ Pubdate string // Human readable date
+ Content string // HTML of entry
+}
+
+type User struct {
+ Username string
+ Default bool
+}
+
+type tildeData struct {
+ News []newsEntry
+ Users []User
+ ActiveUsers []string `json:"active_users"`
+}
+
+type ByName []User
+
+func (n ByName) Len() int { return len(n) }
+func (n ByName) Swap(i, j int) { n[i], n[j] = n[j], n[i] }
+func (n ByName) Less(i, j int) bool { return n[i].Username < n[j].Username }
+
+func _main() error {
+ data, err := stats()
+ if err != nil {
+ return err
+ }
+
+ type tmplData struct {
+ News []newsEntry
+ Lights string
+ }
+
+ td := &tmplData{
+ News: data.News,
+ Lights: "",
+ }
+
+ sort.Sort(ByName(data.Users))
+
+ isActive := func(username string) bool {
+ for _, u := range data.ActiveUsers {
+ if u == username {
+ return true
+ }
+ }
+
+ return false
+ }
+
+ for _, u := range data.Users {
+ if isActive(u.Username) {
+ td.Lights += fmt.Sprintf("*", u.Username)
+ } else if !u.Default {
+ td.Lights += fmt.Sprintf("+", u.Username)
+ } else {
+ td.Lights += "."
+ }
+ }
+
+ t, err := template.New("feed").Parse(feedTmpl)
+ if err != nil {
+ return fmt.Errorf("failed to parse the feed template: %w", err)
+ }
+
+ out := bytes.Buffer{}
+ if err = t.Execute(&out, td); err != nil {
+ return fmt.Errorf("failed to render feed template: %w", err)
+ }
+
+ fmt.Println(out.String())
+
+ return nil
+}
+
+func stats() (*tildeData, error) {
+ sout := bytes.Buffer{}
+ cmd := exec.Command(statsPath)
+ cmd.Stdout = &sout
+
+ err := cmd.Run()
+ if err != nil {
+ return nil, err
+ }
+
+ var data tildeData
+
+ err = json.Unmarshal(sout.Bytes(), &data)
+ if err != nil {
+ return nil, err
+ }
+
+ return &data, nil
+}
+
+func main() {
+ err := _main()
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "error: %s\n", err)
+ os.Exit(1)
+ }
+}