commit d4b74d7a68368284eca18e00ef626542ae8bb20c from: Oliver Lowe date: Sun Feb 04 11:30:52 2024 UTC cmd/Lemmy: show real usernames, render comment threads To make this easier and smaller, we now use the lemmy package directly rather than the filesystem interface. Can't think of a nice way to use that filesystem (yet?). commit - 2a4edef3d088dbb3d4653ffa1edf6920955c7f79 commit + d4b74d7a68368284eca18e00ef626542ae8bb20c blob - 1bf408994b782ce9c379ac83c8ec227c149918e4 blob + 299037dd51bf3491f5aa63d17622b10169a39aa1 --- cmd/Lemmy/Lemmy.go +++ cmd/Lemmy/Lemmy.go @@ -2,18 +2,17 @@ package main import ( "bytes" - "errors" "flag" "fmt" - "io/fs" "log" "os" "path" + "strconv" "strings" + "time" "9fans.net/go/acme" "olowe.co/lemmy" - lemmyfs "olowe.co/lemmy/fs" ) type awin struct { @@ -26,50 +25,19 @@ func (win *awin) Look(text string) bool { } text = strings.TrimSpace(text) - f, err := fsys.Open(text) - if err == nil { - f.Close() - return open(text) - } else if errors.Is(err, fs.ErrNotExist) { - // maybe we're looking for a post in a community? - if win.community() != "" { - name := path.Join(win.community(), text) - f, err = fsys.Open(name) - if err == nil { - f.Close() - return open(name) - } - } + text = strings.TrimSuffix(text, "/") + postID, err := strconv.Atoi(text) + if err != nil { + return openCommunity(text) } - if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, fs.ErrInvalid) { - win.Err(err.Error()) - } + + community := path.Base(win.name()) + return openPost(postID, community) return false } func (win *awin) Execute(cmd string) bool { switch cmd { - case "Reply": - return open("reply") - case "Post": - body, err := win.ReadAll("body") - if err != nil { - win.Err(err.Error()) - return false - } - comment, err := parseReply(bytes.NewReader(body)) - if err != nil { - win.Err("parse comment reply: " + err.Error()) - return false - } - client := fsys.(*lemmyfs.FS).Client - if err := client.Reply(comment.PostID, 0, comment.Content); err != nil { - emsg := fmt.Sprintf("reply to %d: %v", comment.PostID, err) - win.Err(emsg) - return false - } - win.Ctl("clean") - return true case "Del": default: log.Println("unsupported execute", cmd) @@ -77,19 +45,6 @@ func (win *awin) Execute(cmd string) bool { return false } -func (w *awin) community() string { - elems := strings.Split(w.name(), "/") - switch len(elems) { - case 2: - // /lemmy/community - return elems[1] - case 3: - // /lemmy/community/post - return elems[2] - } - return "" -} - func (w *awin) name() string { buf, err := w.ReadAll("tag") if err != nil { @@ -100,114 +55,58 @@ func (w *awin) name() string { return path.Clean(name) } -var fsys fs.FS +var client *lemmy.Client -func loadPostList(dir string) ([]byte, error) { +func loadPostList(community string) ([]byte, error) { buf := &bytes.Buffer{} - dirents, err := fs.ReadDir(fsys, dir) + posts, err := client.Posts(community, lemmy.ListAll) if err != nil { return buf.Bytes(), err } - for _, d := range dirents { - title, err := fs.ReadFile(fsys, path.Join(dir, d.Name(), "title")) - if err != nil { - return nil, err - } - creator, err := fs.ReadFile(fsys, path.Join(dir, d.Name(), "creator")) - if err != nil { - return buf.Bytes(), err - } + for _, p := range posts { // 1234/ User // Hello world! // 5678/ Pengguna // Halo Dunia! - fmt.Fprintf(buf, "%s/\t%s\n\t%s\n", d.Name(), string(creator), string(title)) + fmt.Fprintf(buf, "%d/\t%s\n\t%s\n", p.ID, p.Creator, p.Title) } return buf.Bytes(), err } -func loadPost(pathname string) ([]byte, error) { +func loadPost(post lemmy.Post) ([]byte, error) { buf := &bytes.Buffer{} - for _, fname := range []string{"title", "creator", "url"} { - b, err := fs.ReadFile(fsys, path.Join(pathname, fname)) - if err != nil { - return buf.Bytes(), err - } - key := fmt.Sprintf("%s:", strings.Title(fname)) - fmt.Fprintln(buf, key, string(b)) - } - /* - stat, err := fs.Stat(fsys, pathname) - if err != nil { - return nil, err - } - fmt.Fprintln(buf, "Date:", stat.ModTime()) - */ - fmt.Fprintln(buf) + fmt.Fprintf(buf, "From: %s\n", post.Creator) + fmt.Fprintf(buf, "Date: %s\n", post.Published.Format(time.RFC822)) + fmt.Fprintf(buf, "Subject: %s\n", post.Title) - body, err := fs.ReadFile(fsys, path.Join(pathname, "body")) - if err != nil { - return buf.Bytes(), err + fmt.Fprintln(buf) + if post.URL != "" { + fmt.Fprintln(buf, post.URL) + fmt.Fprintln(buf) } - fmt.Fprintln(buf, string(body)) - fmt.Fprintln(buf, "") - dirents, err := fs.ReadDir(fsys, pathname) - if err != nil { - return buf.Bytes(), err + if post.Body != "" { + fmt.Fprintln(buf, post.Body) + fmt.Fprintln(buf) } - for _, ent := range dirents { - if !strings.ContainsAny(ent.Name(), "0123456789") { - continue - } - comment, err := fs.ReadFile(fsys, path.Join(pathname, ent.Name())) - if err != nil { - return buf.Bytes(), err - } - fmt.Fprintln(buf, string(comment)) - } return buf.Bytes(), nil } -func open(name string) bool { - win, err := acme.New() +func loadComments(id int) ([]byte, error) { + comments, err := client.Comments(id, lemmy.ListAll) if err != nil { - log.Fatal(err) + return nil, err } - win.Name(path.Join("/lemmy", name)) - if name == "." || name == "" { - win.Name("/lemmy/") - } - win.Ctl("dirty") - defer win.Ctl("clean") - - var body []byte - elems := strings.Split(name, "/") - switch len(elems) { - case 1: - if name == "." { - body, err = loadCommunityList() - break - } else if path.Base(name) == "reply" { - win.Write("tag", []byte("Post")) - body = loadNewReply("") - break + buf := &bytes.Buffer{} + for _, c := range comments { + refs := lemmy.ParseCommentPath(c.Path) + // do we have a root comment? + // A root comment only referenences itself and "0" + if len(refs) == 2 { + fprintComment(buf, "", c) + printThread(buf, "\t", c.ID, comments) } - body, err = loadPostList(name) - case 2: - win.Write("tag", []byte("Reply")) - body, err = loadPost(name) } - if errors.Is(err, fs.ErrNotExist) { - return false - } else if err != nil { - log.Print(err) - return false - } - - awin := &awin{win} - awin.Write("body", body) - go awin.EventLoop(awin) - return true + return buf.Bytes(), nil } const Usage string = "usage: Lemmy [host]" @@ -229,7 +128,7 @@ func main() { } else if len(flag.Args()) == 1 { addr = flag.Arg(0) } - client := &lemmy.Client{ + client = &lemmy.Client{ Address: addr, Debug: *debug, } @@ -248,11 +147,7 @@ func main() { } } - fsys = &lemmyfs.FS{ - Client: client, - } - - open(".") + openCommunityList() acme.AutoExit(true) select {} } blob - ca5c746091babfdbb6fa7f483f1cef7244233ca2 blob + 9b645de3870778f579e1f3eab608be62c1b56010 --- cmd/Lemmy/comment.go +++ cmd/Lemmy/comment.go @@ -33,3 +33,31 @@ func parseReply(r io.Reader) (*lemmy.Comment, error) { } return &comment, nil } + +func printThread(w io.Writer, prefix string, parent int, comments []lemmy.Comment) { + for _, child := range children(parent, comments) { + fprintComment(w, prefix, child) + if len(children(child.ID, comments)) > 0 { + printThread(w, prefix+"\t", child.ID, comments) + } + } +} + +func fprintComment(w io.Writer, prefix string, c lemmy.Comment) { + fmt.Fprintln(w, prefix, "From:", c.Creator) + fmt.Fprintln(w, prefix, "Archived-At:", c.ActivityURL) + fmt.Fprintln(w, prefix, c.Content) + println() +} + +func children(parent int, pool []lemmy.Comment) []lemmy.Comment { + var kids []lemmy.Comment + for _, c := range pool { + refs := lemmy.ParseCommentPath(c.Path) + pnt := refs[len(refs)-2] + if pnt == parent { + kids = append(kids, c) + } + } + return kids +} blob - e321a7039b95ea3a1ff5ee6d40ebdbc7dd1982e9 (mode 644) blob + /dev/null --- cmd/Lemmy/fmt.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "bytes" - "errors" - "fmt" - "io" - "io/fs" - "path" - "strings" -) - -func printDirEntries(w io.Writer, dirs []fs.DirEntry) { - for _, entry := range dirs { - name := entry.Name() - if entry.IsDir() { - name = name + "/" - } - fmt.Fprintln(w, name) - } -} - -func listDir(name string) (string, error) { - entries, err := fs.ReadDir(fsys, name) - if err != nil { - return "", err - } - var builder strings.Builder - for _, entry := range entries { - filename := path.Join(name, entry.Name(), "title") - title, err := fs.ReadFile(fsys, filename) - if err != nil && !errors.Is(err, io.EOF) { - return "", err - } - line := fmt.Sprintf("%s/\t%s\n", entry.Name(), string(title)) - builder.WriteString(line) - } - return builder.String(), nil -} - -func loadCommunityList() ([]byte, error) { - entries, err := fs.ReadDir(fsys, ".") - if err != nil { - return nil, err - } - buf := &bytes.Buffer{} - for _, dirent := range entries { - fmt.Fprintf(buf, "%s/\n", dirent.Name()) - } - return buf.Bytes(), nil -} blob - /dev/null blob + 87fab1bf1a0a072c7a0c0bf4063e7f3bfb96f130 (mode 644) --- /dev/null +++ cmd/Lemmy/open.go @@ -0,0 +1,102 @@ +package main + +import ( + "errors" + "log" + "path" + "strconv" + + "9fans.net/go/acme" + "olowe.co/lemmy" +) + +func openCommunityList() bool { + win, err := acme.New() + if err != nil { + log.Fatal(err) + } + win.Name("/lemmy/") + win.Ctl("dirty") + defer win.Ctl("clean") + + communities, err := client.Communities(lemmy.ListAll) + if err != nil { + log.Print(err) + return false + } + for _, c := range communities { + win.Fprintf("body", "%s/\n", c.Name()) + } + awin := &awin{win} + go awin.EventLoop(awin) + return true +} + +func openCommunity(name string) bool { + _, _, err := client.LookupCommunity(name) + if errors.Is(err, lemmy.ErrNotFound) { + return false + } else if err != nil { + log.Print(err) + return false + } + + win, err := acme.New() + if err != nil { + log.Fatal(err) + } + win.Ctl("dirty") + defer win.Ctl("clean") + + awin := &awin{win} + awin.Name(path.Join("/lemmy", name) + "/") + + body, err := loadPostList(name) + if err != nil { + win.Err(err.Error()) + return false + } + awin.Write("body", body) + win.Addr("#0") + win.Ctl("dot=addr") + win.Ctl("show") + go awin.EventLoop(awin) + return true +} + +func openPost(id int, community string) bool { + post, err := client.LookupPost(id) + if err != nil { + log.Print(err) + return false + } + + win, err := acme.New() + if err != nil { + log.Fatal(err) + } + awin := &awin{win} + awin.Name(path.Join("/lemmy", community, strconv.Itoa(id))) + win.Ctl("dirty") + defer win.Ctl("clean") + + body, err := loadPost(post) + if err != nil { + awin.Err(err.Error()) + return false + } + awin.Write("body", body) + + body, err = loadComments(post.ID) + if err != nil { + awin.Err(err.Error()) + return false + } + awin.Write("body", body) + + win.Addr("#0") + win.Ctl("dot=addr") + win.Ctl("show") + go awin.EventLoop(awin) + return true +}