]> git.giacomolli.uk - onechat/blob - routes.go
Carry new images through model request chains
[onechat] / routes.go
1 package main
2
3 import (
4 "encoding/json"
5 "errors"
6 "fmt"
7 "log/slog"
8 "os"
9 "slices"
10 "strings"
11 "sync"
12
13 "go.mau.fi/whatsmeow/types"
14 )
15
16 // openRoutes reads the source-to-child map. A missing file has no routes.
17 func openRoutes(path string, logger *slog.Logger) (*Routes, error) {
18 r := &Routes{
19 path: path,
20 logger: logger,
21 targets: make(map[string]string),
22 }
23 data, err := os.ReadFile(path)
24 if errors.Is(err, os.ErrNotExist) {
25 return r, nil
26 }
27
28 if err != nil {
29 return nil, fmt.Errorf("read routes: %w", err)
30 }
31
32 err = json.Unmarshal(data, &r.targets)
33 if err != nil {
34 return nil, fmt.Errorf("decode routes: %w", err)
35 }
36
37 for source, target := range r.targets {
38 err = routeSource(source)
39 if err != nil {
40 return nil, fmt.Errorf("validate route: %w", err)
41 }
42
43 if target == "" {
44 return nil, errors.New("route has an empty agent name")
45 }
46 }
47
48 if r.targets == nil {
49 r.targets = make(map[string]string)
50 }
51
52 return r, nil
53 }
54
55 // Routes owns the root's source map. Writes replace the private file before
56 // changing the map in memory. The mutex protects reads and writes from tools
57 // and status commands. Logs omit source IDs, which can hold phone numbers.
58 type Routes struct {
59 mutex sync.Mutex
60 path string
61 logger *slog.Logger
62 targets map[string]string
63 }
64
65 // Set maps source to a child name, or removes it when target is empty. The
66 // caller checks that a nonempty target is a child of the root.
67 func (r *Routes) Set(source, target string) error {
68 err := routeSource(source)
69 if err != nil {
70 return fmt.Errorf("validate source: %w", err)
71 }
72
73 r.mutex.Lock()
74 defer r.mutex.Unlock()
75
76 targets := make(map[string]string, len(r.targets)+1)
77 for key, value := range r.targets {
78 targets[key] = value
79 }
80
81 if target == "" {
82 delete(targets, source)
83 } else {
84 targets[source] = target
85 }
86
87 data, err := json.Marshal(targets)
88 if err != nil {
89 return fmt.Errorf("encode routes: %w", err)
90 }
91
92 err = writeFileAtomic(r.path, append(data, '\n'))
93 if err != nil {
94 return fmt.Errorf("save routes: %w", err)
95 }
96
97 r.targets = targets
98 if target == "" {
99 r.logger.Info("route removed", "kind", whatsappSource)
100 } else {
101 r.logger.Info("route set", "kind", whatsappSource, "name", target)
102 }
103
104 return nil
105 }
106
107 // Target returns the configured name, or an empty string for an unrouted
108 // source.
109 func (r *Routes) Target(source string) string {
110 r.mutex.Lock()
111 defer r.mutex.Unlock()
112
113 return r.targets[source]
114 }
115
116 // List returns the routes in source order, or a short empty result.
117 func (r *Routes) List() string {
118 r.mutex.Lock()
119 defer r.mutex.Unlock()
120
121 var sources []string
122
123 for source := range r.targets {
124 sources = append(sources, source)
125 }
126
127 slices.Sort(sources)
128
129 var lines []string
130
131 for _, source := range sources {
132 lines = append(lines, source+" -> "+r.targets[source])
133 }
134
135 if len(lines) == 0 {
136 return "No routes."
137 }
138
139 return strings.Join(lines, "\n")
140 }
141
142 // routeSource accepts the supported source kind and a canonical private or
143 // group chat ID. The key keeps its kind so later sources can use the same map.
144 func routeSource(source string) error {
145 kind, id, _ := strings.Cut(source, ":")
146 if kind != whatsappSource {
147 return errors.New("unknown source kind: use whatsapp:CHAT-ID")
148 }
149
150 jid, err := types.ParseJID(id)
151 if err != nil || jid.User == "" || jid.String() != id || jid != jid.ToNonAD() || (jid.Server != types.DefaultUserServer && jid.Server != types.HiddenUserServer && jid.Server != types.GroupServer) || strings.ContainsAny(id, " \t\r\n") {
152 return errors.New("invalid WhatsApp source: use whatsapp:CHAT-ID from whatsapp_find or a [WhatsApp] message")
153 }
154
155 return nil
156 }