12 "golang.org/x/text/runes"
13 "golang.org/x/text/transform"
14 "golang.org/x/text/unicode/norm"
18 // whatsappFindMatches is the most matches of whatsapp_find.
19 whatsappFindMatches = 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."
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
35 func whatsappFind(ctx context.Context, link WhatsAppLink, groups *WhatsAppGroups, name string) (string, error) {
36 stale := groups.stale()
39 return whatsappNotLinked, nil
42 query := whatsappFold(name)
44 return "", errors.New("name is empty: give a part of the name of the contact or the group")
47 contacts, err := link.device.Contacts.GetAllContacts(ctx)
49 return "", fmt.Errorf("read the contacts: %s", whatsappRedact(link.own, err.Error()))
52 read := !stale || groups.read(ctx, link)
55 if len(contacts) == 0 && len(names) == 0 {
57 return "The names are not known yet, and WhatsApp is disconnected, so the groups cannot be read. Try again later.", nil
60 return whatsappNamesNotReady, nil
63 var matches []WhatsAppMatch
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 {
71 if slices.Contains(link.own, jid.User) || shown < 0 || !slices.ContainsFunc(names, func(name string) bool {
72 return strings.Contains(whatsappFold(name), query)
77 matches = append(matches, WhatsAppMatch{
78 id: jid.ToNonAD().String(),
84 for id, group := range names {
85 if strings.Contains(whatsappFold(group), query) {
86 matches = append(matches, WhatsAppMatch{
94 return whatsappFindAnswer(name, matches, read || len(names) > 0), nil
97 // WhatsAppMatch is one contact or group of whatsappFind.
98 type WhatsAppMatch struct {
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)
112 return strings.ToLower(strings.TrimSpace(folded))
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))
124 for _, match := range matches[:min(len(matches), whatsappFindMatches)] {
125 lines = append(lines, match.id+" | "+match.name+" | "+match.kind)
128 if len(matches) == 0 {
129 lines = append(lines, "No contact or group has a name with \""+name+"\".")
132 if len(matches) > whatsappFindMatches {
133 lines = append(lines, fmt.Sprintf("%d more: give a longer part of the name.", len(matches)-whatsappFindMatches))
137 lines = append(lines, "The groups are not in this answer, because WhatsApp is disconnected.")
140 return strings.Join(lines, "\n")