13 "go.mau.fi/whatsmeow/types"
16 // openRoutes reads the source-to-child map. A missing file has no routes.
17 func openRoutes(path string, logger *slog.Logger) (*Routes, error) {
21 targets: make(map[string]string),
23 data, err := os.ReadFile(path)
24 if errors.Is(err, os.ErrNotExist) {
29 return nil, fmt.Errorf("read routes: %w", err)
32 err = json.Unmarshal(data, &r.targets)
34 return nil, fmt.Errorf("decode routes: %w", err)
37 for source, target := range r.targets {
38 err = routeSource(source)
40 return nil, fmt.Errorf("validate route: %w", err)
44 return nil, errors.New("route has an empty agent name")
49 r.targets = make(map[string]string)
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.
62 targets map[string]string
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)
70 return fmt.Errorf("validate source: %w", err)
74 defer r.mutex.Unlock()
76 targets := make(map[string]string, len(r.targets)+1)
77 for key, value := range r.targets {
82 delete(targets, source)
84 targets[source] = target
87 data, err := json.Marshal(targets)
89 return fmt.Errorf("encode routes: %w", err)
92 err = writeFileAtomic(r.path, append(data, '\n'))
94 return fmt.Errorf("save routes: %w", err)
99 r.logger.Info("route removed", "kind", whatsappSource)
101 r.logger.Info("route set", "kind", whatsappSource, "name", target)
107 // Target returns the configured name, or an empty string for an unrouted
109 func (r *Routes) Target(source string) string {
111 defer r.mutex.Unlock()
113 return r.targets[source]
116 // List returns the routes in source order, or a short empty result.
117 func (r *Routes) List() string {
119 defer r.mutex.Unlock()
123 for source := range r.targets {
124 sources = append(sources, source)
131 for _, source := range sources {
132 lines = append(lines, source+" -> "+r.targets[source])
139 return strings.Join(lines, "\n")
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")
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")