commit - fcf319e83dbec9b824aae4d62e9a4d99cc473dd7
commit + 99ed05e33dafeb84968e397f636c0090d85f691d
blob - ab829bf31417d6c995617b0ad6a0b2fa01e421a9
blob + f17aae80d38fe908fefe427d4ce6513d2f0584a6
--- auth.go
+++ auth.go
}
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
"fmt"
"io"
"io/fs"
- "os"
"strconv"
"strings"
"time"
blob - a2e673c958113926567be080fcc56290cd5dda17
blob + 635e8a6a94b392f31739c7fe63046b5eb64392f0
--- fs/fs.go
+++ fs/fs.go
"fmt"
"io"
"io/fs"
- "os"
"path"
"strconv"
"strings"
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 {
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
+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
+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
+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
+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
)
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 }
Body string
CreatorID int `json:"creator_id"`
URL string
- // Published time.Time
+ Published time.Time `json:"TODO"`
// Updated time.Time
}
}
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 }