eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "net/url"
7 "os"
8 "path/filepath"
9 "sort"
10 "strings"
11
12 "lchat/internal/client"
13)
14
15// The session store: ~/.eleven holds one session file per space plus the home
16// sign-in. Every file is a private key in JSON clothing, hence 0600/0700.
17//
18// ~/.eleven/spaces/<host>.json one client.Session per space
19// ~/.eleven/home.json the home vault session (token + blob key)
20//
21// A -session flag bypasses the store entirely (single-space mode, for scripts
22// pinning a specific identity file) — that path never touches this type.
23type store struct {
24 dir string
25}
26
27func openStore(dir string) (*store, error) {
28 st := &store{dir: dir}
29 if err := os.MkdirAll(filepath.Join(dir, "spaces"), 0o700); err != nil {
30 return nil, err
31 }
32 if err := st.migrateLegacy(); err != nil {
33 return nil, err
34 }
35 return st, nil
36}
37
38// migrateLegacy moves a pre-multi-space ~/.eleven/session.json into spaces/.
39func (st *store) migrateLegacy() error {
40 legacy := filepath.Join(st.dir, "session.json")
41 sess, err := client.LoadSession(legacy)
42 if err != nil || sess == nil {
43 return err
44 }
45 if sess.Base == "" {
46 return fmt.Errorf("%s has no server URL — re-join with `eleven join`", legacy)
47 }
48 if err := st.saveSpace(sess); err != nil {
49 return err
50 }
51 if err := os.Remove(legacy); err != nil {
52 return err
53 }
54 fmt.Fprintf(os.Stderr, "(Moved %s into %s.)\n", legacy, filepath.Join(st.dir, "spaces"))
55 return nil
56}
57
58func (st *store) spacePath(sess *client.Session) (string, error) {
59 u, err := url.Parse(sess.Base)
60 if err != nil || u.Host == "" {
61 return "", fmt.Errorf("bad space URL %q", sess.Base)
62 }
63 return filepath.Join(st.dir, "spaces", u.Host+".json"), nil
64}
65
66func (st *store) saveSpace(sess *client.Session) error {
67 path, err := st.spacePath(sess)
68 if err != nil {
69 return err
70 }
71 return sess.Save(path)
72}
73
74// spaces loads every saved space, hosts alphabetical (stable for display and
75// for "first match wins" resolution).
76func (st *store) spaces() ([]*client.Session, error) {
77 glob, err := filepath.Glob(filepath.Join(st.dir, "spaces", "*.json"))
78 if err != nil {
79 return nil, err
80 }
81 sort.Strings(glob)
82 var out []*client.Session
83 for _, p := range glob {
84 sess, err := client.LoadSession(p)
85 if err != nil {
86 fmt.Fprintf(os.Stderr, "eleven: skipping %s: %v\n", p, err)
87 continue
88 }
89 if sess != nil && sess.Base != "" {
90 out = append(out, sess)
91 }
92 }
93 return out, nil
94}
95
96func (st *store) homePath() string { return filepath.Join(st.dir, "home.json") }
97
98// homeSession loads the saved home sign-in, nil when there is none.
99func (st *store) homeSession() (*client.HomeSession, error) {
100 raw, err := os.ReadFile(st.homePath())
101 if os.IsNotExist(err) {
102 return nil, nil
103 }
104 if err != nil {
105 return nil, err
106 }
107 var hs client.HomeSession
108 if err := json.Unmarshal(raw, &hs); err != nil {
109 return nil, fmt.Errorf("bad home session file %s: %w", st.homePath(), err)
110 }
111 if hs.Token == "" || hs.DEK == "" {
112 return nil, fmt.Errorf("home session file %s is missing its session", st.homePath())
113 }
114 return &hs, nil
115}
116
117func (st *store) saveHomeSession(hs *client.HomeSession) error {
118 raw, err := json.MarshalIndent(hs, "", " ")
119 if err != nil {
120 return err
121 }
122 return os.WriteFile(st.homePath(), append(raw, '\n'), 0o600)
123}
124
125// spaceLabel names a space for display: its vault name when the backup had
126// one, else its host.
127func spaceLabel(sess *client.Session) string {
128 if sess.SpaceName != "" {
129 return sess.SpaceName
130 }
131 if u, err := url.Parse(sess.Base); err == nil && u.Host != "" {
132 return u.Host
133 }
134 return sess.Base
135}
136
137// spaceHost is the space's host, for the space/chat needle syntax.
138func spaceHost(sess *client.Session) string {
139 if u, err := url.Parse(sess.Base); err == nil {
140 return u.Host
141 }
142 return sess.Base
143}
144
145func matchesSpace(sess *client.Session, needle string) bool {
146 needle = strings.ToLower(needle)
147 return strings.Contains(strings.ToLower(spaceHost(sess)), needle) ||
148 strings.Contains(strings.ToLower(sess.SpaceName), needle)
149}
150