13 "go.mau.fi/whatsmeow/types"
16 // Routes owns the root's source map. Writes replace the private file before
17 // changing the map in memory. The mutex protects reads and writes from tools
18 // and status commands. Logs omit source IDs, which can hold phone numbers.
23 targets map[string]string
26 // openRoutes reads the source-to-child map. A missing file has no routes.
27 func openRoutes(path string, logger *slog.Logger) (*Routes, error) {
31 targets: make(map[string]string),
34 data, err := os.ReadFile(path)
35 if errors.Is(err, os.ErrNotExist) {
40 return nil, fmt.Errorf("read routes: %w", err)
43 err = json.Unmarshal(data, &r.targets)
45 return nil, fmt.Errorf("decode routes: %w", err)
48 for source, target := range r.targets {
49 err = routeSource(source)
51 return nil, fmt.Errorf("validate route: %w", err)
55 return nil, errors.New("route has an empty agent name")
60 r.targets = make(map[string]string)
66 // Set maps source to a child name, or removes it when target is empty. The
67 // caller checks that a nonempty target is a child of the root.
68 func (r *Routes) Set(source, target string) error {
69 err := routeSource(source)
71 return fmt.Errorf("validate source: %w", err)
75 defer r.mutex.Unlock()
77 targets := make(map[string]string, len(r.targets)+1)
78 for key, value := range r.targets {
83 delete(targets, source)
85 targets[source] = target
88 data, err := json.Marshal(targets)
90 return fmt.Errorf("encode routes: %w", err)
93 err = writeFileAtomic(r.path, append(data, '\n'))
95 return fmt.Errorf("save routes: %w", err)
100 r.logger.Info("route removed", "kind", whatsappSource)
102 r.logger.Info("route set", "kind", whatsappSource, "name", target)
108 // Target returns the configured name, or an empty string for an unrouted
110 func (r *Routes) Target(source string) string {
112 defer r.mutex.Unlock()
114 return r.targets[source]
117 // List returns the routes in source order, or a short empty result.
118 func (r *Routes) List() string {
120 defer r.mutex.Unlock()
124 for source := range r.targets {
125 sources = append(sources, source)
132 for _, source := range sources {
133 lines = append(lines, source+" -> "+r.targets[source])
140 return strings.Join(lines, "\n")
143 // routeSource accepts the supported source kind and a canonical private or
144 // group chat ID. The key keeps its kind so later sources can use the same map.
145 func routeSource(source string) error {
146 kind, id, _ := strings.Cut(source, ":")
147 if kind != whatsappSource {
148 return errors.New("unknown source kind: use whatsapp:CHAT-ID")
151 jid, err := types.ParseJID(id)
152 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") {
153 return errors.New("invalid WhatsApp source: use whatsapp:CHAT-ID from whatsapp_find or a [WhatsApp] message")