Commit Diff


commit - fcf319e83dbec9b824aae4d62e9a4d99cc473dd7
commit + 99ed05e33dafeb84968e397f636c0090d85f691d
blob - ab829bf31417d6c995617b0ad6a0b2fa01e421a9
blob + f17aae80d38fe908fefe427d4ce6513d2f0584a6
--- auth.go
+++ auth.go
@@ -34,4 +34,8 @@ func (c *Client) Login(name, password string) error {
 	}
 	c.authToken = response.JWT
 	return nil
-}
\ No newline at end of file
+}
+
+func (c *Client) Authenticated() bool {
+	return c.authToken != ""
+}
blob - 934c074d7f631ab82c3d20cbb7fed934b5a413f2
blob + 3a1bbd61539a774aec3760301b0e3a5fbbc12703
--- fs/file.go
+++ fs/file.go
@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"io"
 	"io/fs"
-	"os"
 	"strconv"
 	"strings"
 	"time"
blob - a2e673c958113926567be080fcc56290cd5dda17
blob + 635e8a6a94b392f31739c7fe63046b5eb64392f0
--- fs/fs.go
+++ fs/fs.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 	"io"
 	"io/fs"
-	"os"
 	"path"
 	"strconv"
 	"strings"
@@ -29,17 +28,15 @@ func (fsys *FS) start() error {
 
 	if fsys.Communities == nil {
 		fsys.Communities = make(map[string]lemmy.Community)
-		/*
-			if fsys.Client.authToken != "" {
-				subscribed, err := fsys.Client.Communities(lemmy.ListSubscribed)
-				if err != nil {
-					return fmt.Errorf("load subscriptions: %w", err)
-				}
-				for _, c := range subscribed {
-					fsys.Communities[c.Name()] = c
-				}
+		if fsys.Client.Authenticated() {
+			subscribed, err := fsys.Client.Communities(lemmy.ListSubscribed)
+			if err != nil {
+				return fmt.Errorf("load subscriptions: %w", err)
 			}
-		*/
+			for _, c := range subscribed {
+				fsys.Communities[c.Name()] = c
+			}
+		}
 	}
 
 	if fsys.Posts == nil {
@@ -144,6 +141,7 @@ func (fsys *FS) Open(name string) (fs.File, error) {
 func (fsys *FS) openRoot() (fs.File, error) {
 	dirinfo := new(dirInfo)
 	for _, c := range fsys.Communities {
+		c := c
 		de := fs.FileInfoToDirEntry(&c)
 		dirinfo.entries = append(dirinfo.entries, de)
 	}
blob - /dev/null
blob + 1bf408994b782ce9c379ac83c8ec227c149918e4 (mode 644)
--- /dev/null
+++ cmd/Lemmy/Lemmy.go
@@ -0,0 +1,266 @@
+package main
+
+import (
+	"bytes"
+	"errors"
+	"flag"
+	"fmt"
+	"io/fs"
+	"log"
+	"os"
+	"path"
+	"strings"
+
+	"9fans.net/go/acme"
+	"olowe.co/lemmy"
+	lemmyfs "olowe.co/lemmy/fs"
+)
+
+type awin struct {
+	*acme.Win
+}
+
+func (win *awin) Look(text string) bool {
+	if acme.Show(text) != nil {
+		return true
+	}
+
+	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)
+			}
+		}
+	}
+	if !errors.Is(err, fs.ErrNotExist) && !errors.Is(err, fs.ErrInvalid) {
+		win.Err(err.Error())
+	}
+	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)
+	}
+	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 {
+		w.Err(err.Error())
+		return ""
+	}
+	name := strings.Fields(string(buf))[0]
+	return path.Clean(name)
+}
+
+var fsys fs.FS
+
+func loadPostList(dir string) ([]byte, error) {
+	buf := &bytes.Buffer{}
+	dirents, err := fs.ReadDir(fsys, dir)
+	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
+		}
+		// 1234/	User
+		// 	Hello world!
+		// 5678/	Pengguna
+		// 	Halo Dunia!
+		fmt.Fprintf(buf, "%s/\t%s\n\t%s\n", d.Name(), string(creator), string(title))
+	}
+	return buf.Bytes(), err
+}
+
+func loadPost(pathname string) ([]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)
+
+	body, err := fs.ReadFile(fsys, path.Join(pathname, "body"))
+	if err != nil {
+		return buf.Bytes(), err
+	}
+	fmt.Fprintln(buf, string(body))
+	fmt.Fprintln(buf, "")
+	dirents, err := fs.ReadDir(fsys, pathname)
+	if err != nil {
+		return buf.Bytes(), err
+	}
+	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()
+	if err != nil {
+		log.Fatal(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
+		}
+		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
+}
+
+const Usage string = "usage: Lemmy [host]"
+
+func init() {
+	log.SetFlags(0)
+	log.SetPrefix("Lemmy: ")
+}
+
+func main() {
+	debug := flag.Bool("d", false, "enable debug output to stderr")
+	login := flag.Bool("l", false, "log in to Lemmy")
+	flag.Parse()
+
+	addr := "lemmy.sdf.org"
+	if len(flag.Args()) > 1 {
+		fmt.Fprintln(os.Stderr, Usage)
+		os.Exit(2)
+	} else if len(flag.Args()) == 1 {
+		addr = flag.Arg(0)
+	}
+	client := &lemmy.Client{
+		Address: addr,
+		Debug:   *debug,
+	}
+
+	if *login {
+		config, err := os.UserConfigDir()
+		if err != nil {
+			log.Fatalln(err)
+		}
+		username, password, err := readCreds(path.Join(config, "Lemmy"))
+		if err != nil {
+			log.Fatalln("read lemmy credentials:", err)
+		}
+		if err := client.Login(username, password); err != nil {
+			log.Fatalln("login:", err)
+		}
+	}
+
+	fsys = &lemmyfs.FS{
+		Client: client,
+	}
+
+	open(".")
+	acme.AutoExit(true)
+	select {}
+}
+
+func mustPathMatch(pattern, name string) bool {
+	match, err := path.Match(pattern, name)
+	if err != nil {
+		panic(err)
+	}
+	return match
+}
blob - /dev/null
blob + ca5c746091babfdbb6fa7f483f1cef7244233ca2 (mode 644)
--- /dev/null
+++ cmd/Lemmy/comment.go
@@ -0,0 +1,35 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"net/mail"
+	"path"
+	"strconv"
+
+	"olowe.co/lemmy"
+)
+
+func loadNewReply(pathname string) []byte {
+	if pathname == "" {
+		return []byte("To: ")
+	}
+	return []byte(fmt.Sprintf("To: %s\n\n", path.Base(pathname)))
+}
+
+func parseReply(r io.Reader) (*lemmy.Comment, error) {
+	msg, err := mail.ReadMessage(r)
+	if err != nil {
+		return nil, err
+	}
+	var comment lemmy.Comment
+	b, err := io.ReadAll(msg.Body)
+	if err != nil {
+		return nil, err
+	}
+	comment.Content = string(b)
+	if comment.PostID, err = strconv.Atoi(msg.Header.Get("To")); err != nil {
+		return nil, fmt.Errorf("parse post id: %w", err)
+	}
+	return &comment, nil
+}
blob - /dev/null
blob + 39c4b9ec297eb71e9ad3740c62dec96eb7e160c6 (mode 644)
--- /dev/null
+++ cmd/Lemmy/config.go
@@ -0,0 +1,20 @@
+package main
+
+import (
+	"bufio"
+	"os"
+)
+
+func readCreds(name string) (username, password string, err error) {
+	f, err := os.Open(name)
+	if err != nil {
+		return "", "", err
+	}
+	defer f.Close()
+	sc := bufio.NewScanner(f)
+	sc.Scan()
+	username = sc.Text()
+	sc.Scan()
+	password = sc.Text()
+	return username, password, sc.Err()
+}
blob - /dev/null
blob + e321a7039b95ea3a1ff5ee6d40ebdbc7dd1982e9 (mode 644)
--- /dev/null
+++ cmd/Lemmy/fmt.go
@@ -0,0 +1,51 @@
+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 - 18cc639701c8112104fc28817abac957b11744b8
blob + 5576477559ec98812d2c2a8539b45bf133252331
--- lemmy.go
+++ lemmy.go
@@ -11,18 +11,18 @@ import (
 )
 
 type Community struct {
-	ID      int    `json:"id"`
-	FName   string `json:"name"`
-	Title   string `json:"title"`
-	Local   bool
-	ActorID string `json:"actor_id"`
-	// Updated  time.Time `json:"updated"`
+	ID        int    `json:"id"`
+	FName     string `json:"name"`
+	Title     string `json:"title"`
+	Local     bool
+	ActorID   string    `json:"actor_id"`
+	Published time.Time `json:"TODO"`
 }
 
 func (c *Community) Name() string       { return c.String() }
 func (c *Community) Size() int64        { return 0 }
 func (c *Community) Mode() fs.FileMode  { return fs.ModeDir | 0o0555 }
-func (c *Community) ModTime() time.Time { return time.Unix(0, 0) }
+func (c *Community) ModTime() time.Time { return c.Published }
 func (c *Community) IsDir() bool        { return true }
 func (c *Community) Sys() interface{}   { return nil }
 
@@ -41,7 +41,7 @@ type Post struct {
 	Body      string
 	CreatorID int `json:"creator_id"`
 	URL       string
-	// Published time.Time
+	Published time.Time `json:"TODO"`
 	// Updated   time.Time
 }
 
@@ -52,7 +52,7 @@ func (p *Post) Size() int64 {
 }
 
 func (p *Post) Mode() fs.FileMode  { return fs.ModeDir | 0o0444 }
-func (p *Post) ModTime() time.Time { return time.Unix(0, 0) }
+func (p *Post) ModTime() time.Time { return p.Published }
 func (p *Post) IsDir() bool        { return true }
 func (p *Post) Sys() interface{}   { return nil }