]> git.giacomolli.uk - onechat/blob - whatsapp_find.go
Send the compaction view in block messages
[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         for jid, contact := range contacts {
65                 names := []string{contact.FullName, contact.FirstName, contact.BusinessName, contact.PushName}
66                 shown := slices.IndexFunc(names, func(name string) bool {
67                         return name != ""
68                 })
69
70                 if slices.Contains(link.own, jid.User) || shown < 0 || !slices.ContainsFunc(names, func(name string) bool {
71                         return strings.Contains(whatsappFold(name), query)
72                 }) {
73                         continue
74                 }
75
76                 matches = append(matches, WhatsAppMatch{
77                         id:   jid.ToNonAD().String(),
78                         name: names[shown],
79                         kind: "contact",
80                 })
81         }
82
83         for id, group := range names {
84                 if strings.Contains(whatsappFold(group), query) {
85                         matches = append(matches, WhatsAppMatch{
86                                 id:   id,
87                                 name: group,
88                                 kind: "group",
89                         })
90                 }
91         }
92
93         return whatsappFindAnswer(name, matches, read || len(names) > 0), nil
94 }
95
96 // WhatsAppMatch is one contact or group of whatsappFind.
97 type WhatsAppMatch struct {
98         id   string
99         name string
100         kind string
101 }
102
103 // whatsappFold returns text in lower case with no accents and no space at its
104 // ends, so that names match with no regard to case and accents.
105 func whatsappFold(text string) string {
106         folded, _, err := transform.String(transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC), text)
107         if err != nil {
108                 folded = text
109         }
110
111         return strings.ToLower(strings.TrimSpace(folded))
112 }
113
114 // whatsappFindAnswer returns the answer of whatsappFind for name with matches. Without
115 // groups, the answer says that it has no groups.
116 func whatsappFindAnswer(name string, matches []WhatsAppMatch, groups bool) string {
117         slices.SortFunc(matches, func(a, b WhatsAppMatch) int {
118                 return cmp.Or(strings.Compare(whatsappFold(a.name), whatsappFold(b.name)), strings.Compare(a.id, b.id))
119         })
120
121         var lines []string
122         for _, match := range matches[:min(len(matches), whatsappFindMatches)] {
123                 lines = append(lines, match.id+" | "+match.name+" | "+match.kind)
124         }
125
126         if len(matches) == 0 {
127                 lines = append(lines, "No contact or group has a name with \""+name+"\".")
128         }
129
130         if len(matches) > whatsappFindMatches {
131                 lines = append(lines, fmt.Sprintf("%d more: give a longer part of the name.", len(matches)-whatsappFindMatches))
132         }
133
134         if !groups {
135                 lines = append(lines, "The groups are not in this answer, because WhatsApp is disconnected.")
136         }
137
138         return strings.Join(lines, "\n")
139 }