]> git.giacomolli.uk - onechat/blob - whatsapp_find.go
Order model and chat declarations
[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 // WhatsAppMatch is one contact or group of whatsappFind.
27 type WhatsAppMatch struct {
28 id string
29 name string
30 kind string
31 }
32
33 // whatsappFind returns the contacts and the groups whose name contains name,
34 // with no regard to case and accents, as lines "ID | name | contact" or "ID |
35 // name | group", sorted by name. It gives at most whatsappFindMatches lines and
36 // the number of the others. The contacts come from the session database, and
37 // the groups from WhatsApp, read again after whatsappGroupsAge. When the groups
38 // cannot be read and none are known, the answer says so. Before the names
39 // arrive from the phone, it says that they are not ready. link gives the
40 // device, the client to read the groups, and the own IDs, which are not in the
41 // answer.
42 func whatsappFind(ctx context.Context, link WhatsAppLink, groups *WhatsAppGroups, name string) (string, error) {
43 stale := groups.stale()
44
45 if !link.linked {
46 return whatsappNotLinked, nil
47 }
48
49 query := whatsappFold(name)
50 if query == "" {
51 return "", errors.New("name is empty: give a part of the name of the contact or the group")
52 }
53
54 contacts, err := link.device.Contacts.GetAllContacts(ctx)
55 if err != nil {
56 return "", fmt.Errorf("read the contacts: %s", whatsappRedact(link.own, err.Error()))
57 }
58
59 read := !stale || groups.read(ctx, link)
60 names := groups.all()
61
62 if len(contacts) == 0 && len(names) == 0 {
63 if !read {
64 return "The names are not known yet, and WhatsApp is disconnected, so the groups cannot be read. Try again later.", nil
65 }
66
67 return whatsappNamesNotReady, nil
68 }
69
70 var matches []WhatsAppMatch
71
72 for jid, contact := range contacts {
73 names := []string{contact.FullName, contact.FirstName, contact.BusinessName, contact.PushName}
74 shown := slices.IndexFunc(names, func(name string) bool {
75 return name != ""
76 })
77
78 if slices.Contains(link.own, jid.User) || shown < 0 || !slices.ContainsFunc(names, func(name string) bool {
79 return strings.Contains(whatsappFold(name), query)
80 }) {
81 continue
82 }
83
84 matches = append(matches, WhatsAppMatch{
85 id: jid.ToNonAD().String(),
86 name: names[shown],
87 kind: "contact",
88 })
89 }
90
91 for id, group := range names {
92 if strings.Contains(whatsappFold(group), query) {
93 matches = append(matches, WhatsAppMatch{
94 id: id,
95 name: group,
96 kind: "group",
97 })
98 }
99 }
100
101 return whatsappFindAnswer(name, matches, read || len(names) > 0), nil
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.
116 // Without 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 }