commit - 8e542a00827deacaea5a7dd721a2f09f18ebf9ac
commit + fcd43598d63fd3ecef6564bd836746fea5847b8b
blob - 7cd6f5a89adc50af9df979a5a614218e810038ea (mode 644)
blob + /dev/null
--- src/hn/hn.go
+++ /dev/null
-// Package hn provides a filesystem interface to items on Hacker News.
-package hn
-
-import (
- "bytes"
- "encoding/json"
- "fmt"
- "html"
- "io"
- "io/fs"
- "net/http"
- "os"
- "path"
- "strconv"
- "strings"
- "time"
-)
-
-const APIRoot = "https://hacker-news.firebaseio.com/v0"
-
-type Item struct {
- ID int
- Type string
- By string
- Time int
- Text string
- Parent int
- URL string
- Title string
-}
-
-func (it *Item) Name() string { return strconv.Itoa(it.ID) }
-func (it *Item) Size() int64 { r := toMessage(it); return r.Size() }
-func (it *Item) Mode() fs.FileMode { return 0o444 }
-func (it *Item) ModTime() time.Time { return time.Unix(int64(it.Time), 0) }
-func (it *Item) IsDir() bool { return false }
-func (it *Item) Sys() any { return nil }
-
-type FS struct {
- cache fs.FS
-}
-
-func CacheDirFS(name string) *FS {
- return &FS{cache: os.DirFS(name)}
-}
-
-func (fsys *FS) Open(name string) (fs.File, error) {
- if !fs.ValidPath(name) {
- return nil, &fs.PathError{"open", name, fs.ErrInvalid}
- }
- name = path.Clean(name)
- switch name {
- case ".":
- return nil, fmt.Errorf("TODO")
- default:
- if _, err := strconv.Atoi(name); err != nil {
- return nil, &fs.PathError{"open", name, fs.ErrNotExist}
- }
- }
- if fsys.cache != nil {
- if f, err := fsys.cache.Open(name); err == nil {
- return f, nil
- }
- }
-
- u := fmt.Sprintf("%s/item/%s.json", APIRoot, name)
- resp, err := http.Get(u)
- if err != nil {
- return nil, err
- }
- if resp.StatusCode != http.StatusOK {
- return nil, err
- }
- return &file{rc: resp.Body}, nil
-}
-
-type file struct {
- rc io.ReadCloser
- item *Item
- msg *bytes.Reader
-}
-
-func (f *file) Read(p []byte) (int, error) {
- var n int
- if f.item == nil {
- if err := json.NewDecoder(f.rc).Decode(&f.item); err != nil {
- return n, fmt.Errorf("decode item: %v", err)
- }
- }
- if f.msg == nil {
- f.msg = toMessage(f.item)
- }
- return f.msg.Read(p)
-}
-
-func (f *file) Stat() (fs.FileInfo, error) { return f.item, nil }
-
-func (f *file) Close() error {
- f.msg = nil
- return f.rc.Close()
-}
-
-func toMessage(item *Item) *bytes.Reader {
- buf := &bytes.Buffer{}
- fmt.Fprintf(buf, "From: %s\n", item.By)
- fmt.Fprintf(buf, "Message-ID: <%d@news.ycombinator.com>\n", item.ID)
- fmt.Fprintf(buf, "Date: %s\n", time.Unix(int64(item.Time), 0).Format(time.RFC1123Z))
- if item.Parent != 0 {
- fmt.Fprintf(buf, "References: <%d@news.ycombinator.com>\n", item.Parent)
- }
- if item.Title != "" {
- fmt.Fprintf(buf, "Subject: %s\n", item.Title)
- }
- fmt.Fprintln(buf)
- if item.URL != "" {
- fmt.Fprintln(buf, item.URL)
- }
- if item.Text != "" {
- fmt.Fprintln(buf, strings.ReplaceAll(html.UnescapeString(item.Text), "<p>", "\n\n"))
- }
- return bytes.NewReader(buf.Bytes())
-}
blob - cd77eb626540f20621904d81d32281846ba20376 (mode 644)
blob + /dev/null
--- src/hn/hn_test.go
+++ /dev/null
-package hn
-
-import (
- "io"
- "os"
- "testing"
-)
-
-func TestFS(t *testing.T) {
- fsys := &FS{}
- f, err := fsys.Open("39845126")
- if err != nil {
- t.Fatal(err)
- }
- defer f.Close()
- if _, err := io.Copy(os.Stderr, f); err != nil {
- t.Fatal(err)
- }
-}
blob - /dev/null
blob + 8f3e47df5df493afe67aa18b9b88b14ca1dcc4ad (mode 644)
--- /dev/null
+++ src/hnatom/hnatom.go
+// Command hnatom writes to the standard output a RFC 4287 Atom feed
+// of current top stories from the Hacker News front page.
+// The flags are:
+//
+// -n count
+// Include count items in the feed. The default is 30.
+//
+// See also the [Hacker News API].
+//
+// [Hacker News API]: https://github.com/HackerNews/API
+package main
+
+import (
+ "bytes"
+ "encoding/json"
+ "encoding/xml"
+ "flag"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "strconv"
+ "time"
+
+ "olowe.co/x/atom"
+)
+
+const apiRoot = "https://hacker-news.firebaseio.com/v0"
+
+type Item struct {
+ ID int
+ Type string
+ By string
+ Time int
+ Text string
+ Parent int
+ URL string
+ Title string
+ Score int
+ Descendants int
+}
+
+func Get(id int) (*Item, error) {
+ u := fmt.Sprintf("%s/item/%s.json", apiRoot, strconv.Itoa(id))
+ resp, err := http.Get(u)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ var item Item
+ err = json.NewDecoder(resp.Body).Decode(&item)
+ return &item, err
+}
+
+func Top() ([]int, error) {
+ resp, err := http.Get(apiRoot + "/topstories.json")
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ ids := make([]int, 500) // we know the API returns at most 500 items.
+ err = json.NewDecoder(resp.Body).Decode(&ids)
+ return ids, err
+}
+
+const maxEntries = 5
+
+func entryContent(item *Item) []byte {
+ buf := &bytes.Buffer{}
+ fmt.Fprintf(buf, "Score: %d\n", item.Score)
+ fmt.Fprintln(buf, "<br>")
+ comments := fmt.Sprintf("https://news.ycombinator.com/item?id=%d", item.ID)
+ fmt.Fprintf(buf, "<a href=%q>Comments: %d", comments, item.Descendants)
+ return buf.Bytes()
+}
+
+// The most number of items the top API endpoint will return.
+const maxItems = 500
+
+// 30 is the item count on the front page of Hacker News.
+var numItems = flag.Uint("n", 30, "number of items to fetch")
+
+func init() {
+ flag.Parse()
+ if *numItems > maxItems {
+ *numItems = maxItems
+ fmt.Fprintln(os.Stderr, "warning: maximum of 500 entries can be fetched")
+ }
+}
+
+func main() {
+ top, err := Top()
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ feed := &atom.Feed{
+ ID: "http://home.olowe.co/hnatom/feed.atom",
+ Title: "HN Atom",
+ Subtitle: "Top posts from Hacker News",
+ Link: []atom.Link{
+ {
+ Rel: "alternate",
+ Type: "html",
+ HRef: "https://news.ycombinator.com",
+ },
+ },
+ Updated: time.Now(),
+ Entries: make([]atom.Entry, *numItems),
+ }
+
+ for i := range top[:len(feed.Entries)] {
+ item, err := Get(top[i])
+ if err != nil {
+ log.Println(err)
+ continue
+ }
+ feed.Entries[i] = atom.Entry{
+ ID: fmt.Sprintf("%s/item/%d.json", apiRoot, top[i]),
+ Title: item.Title,
+ Updated: time.Unix(int64(item.Time), 0),
+ Author: &atom.Author{
+ Name: item.By,
+ URI: "https://news.ycombinator.com/user?id=" + item.By,
+ },
+ Content: []byte(entryContent(item)),
+ Links: []atom.Link{{HRef: item.URL}},
+ }
+ }
+
+ b, err := xml.MarshalIndent(feed, "", "\t")
+ if err != nil {
+ log.Fatal(err)
+ }
+ os.Stdout.Write(b)
+}