]> git.giacomolli.uk - onechat/blob - whatsapp_find.go
Send terminal images by local path
[onechat] / whatsapp_find.go
1 package main
2
3 import (
4         "cmp"
5         "context"
6         "errors"
7         "fmt"
8         "slices"
9         "strings"
10         "unicode"
11
12         "golang.org/x/text/runes"
13         "golang.org/x/text/transform"
14         "golang.org/x/text/unicode/norm"
15 )
16
17 const (
18         // whatsappFindMatches is the most matches of whatsapp_find.
19         whatsappFindMatches = 20
20
21         // whatsappNamesNotReady is the answer of whatsapp_find before the names
22         // arrive from the phone.
23         whatsappNamesNotReady = "The names of the contacts and groups have not arrived from the phone yet. They come some minutes after the link. Try again later."
24 )
25
26 // whatsappFind returns the contacts and the groups whose name contains name,
27 // with no regard to case and accents, as lines "ID | name | contact" or "ID |
28 // name | group", sorted by name. It gives at most whatsappFindMatches lines and
29 // the number of the others. The contacts come from the session database, and
30 // the groups from WhatsApp, read again after whatsappGroupsAge. When the groups
31 // cannot be read and none are known, the answer says so. Before the names
32 // arrive from the phone, it says that they are not ready. link gives the
33 // device, the client to read the groups, and the own IDs, which are not in the
34 // answer.
35 func whatsappFind(ctx context.Context, link WhatsAppLink, groups *WhatsAppGroups, name string) (string, error) {
36         stale := groups.stale()
37
38         if !link.linked {
39                 return whatsappNotLinked, nil
40         }
41
42         query := whatsappFold(name)
43         if query == "" {
44                 return "", errors.New("name is empty: give a part of the name of the contact or the group")
45         }
46
47         contacts, err := link.device.Contacts.GetAllContacts(ctx)
48         if err != nil {
49                 return "", fmt.Errorf("read the contacts: %s", whatsappRedact(link.own, err.Error()))
50         }
51
52         read := !stale || groups.read(ctx, link)
53         names := groups.all()
54
55         if len(contacts) == 0 && len(names) == 0 {
56                 if !read {
57                         return "The names are not known yet, and WhatsApp is disconnected, so the groups cannot be read. Try again later.", nil
58                 }
59
60                 return whatsappNamesNotReady, nil
61         }
62
63         var matches []WhatsAppMatch
64
65         for jid, contact := range contacts {
66                 names := []string{contact.FullName, contact.FirstName, contact.BusinessName, contact.PushName}
67                 shown := slices.IndexFunc(names, func(name string) bool {
68                         return name != ""
69                 })
70
71                 if slices.Contains(link.own, jid.User) || shown < 0 || !slices.ContainsFunc(names, func(name string) bool {
72                         return strings.Contains(whatsappFold(name), query)
73                 }) {
74                         continue
75                 }
76
77                 matches = append(matches, WhatsAppMatch{
78                         id:   jid.ToNonAD().String(),
79                         name: names[shown],
80                         kind: "contact",
81                 })
82         }
83
84         for id, group := range names {
85                 if strings.Contains(whatsappFold(group), query) {
86                         matches = append(matches, WhatsAppMatch{
87                                 id:   id,
88                                 name: group,
89                                 kind: "group",
90                         })
91                 }
92         }
93
94         return whatsappFindAnswer(name, matches, read || len(names) > 0), nil
95 }
96
97 // WhatsAppMatch is one contact or group of whatsappFind.
98 type WhatsAppMatch struct {
99         id   string
100         name string
101         kind string
102 }
103
104 // whatsappFold returns text in lower case with no accents and no space at its
105 // ends, so that names match with no regard to case and accents.
106 func whatsappFold(text string) string {
107         folded, _, err := transform.String(transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC), text)
108         if err != nil {
109                 folded = text
110         }
111
112         return strings.ToLower(strings.TrimSpace(folded))
113 }
114
115 // whatsappFindAnswer returns the answer of whatsappFind for name with matches. Without
116 // groups, the answer says that it has no groups.
117 func whatsappFindAnswer(name string, matches []WhatsAppMatch, groups bool) string {
118         slices.SortFunc(matches, func(a, b WhatsAppMatch) int {
119                 return cmp.Or(strings.Compare(whatsappFold(a.name), whatsappFold(b.name)), strings.Compare(a.id, b.id))
120         })
121
122         var lines []string
123
124         for _, match := range matches[:min(len(matches), whatsappFindMatches)] {
125                 lines = append(lines, match.id+" | "+match.name+" | "+match.kind)
126         }
127
128         if len(matches) == 0 {
129                 lines = append(lines, "No contact or group has a name with \""+name+"\".")
130         }
131
132         if len(matches) > whatsappFindMatches {
133                 lines = append(lines, fmt.Sprintf("%d more: give a longer part of the name.", len(matches)-whatsappFindMatches))
134         }
135
136         if !groups {
137                 lines = append(lines, "The groups are not in this answer, because WhatsApp is disconnected.")
138         }
139
140         return strings.Join(lines, "\n")
141 }