package main import ( "cmp" "context" "errors" "fmt" "slices" "strings" "unicode" "golang.org/x/text/runes" "golang.org/x/text/transform" "golang.org/x/text/unicode/norm" ) const ( // whatsappFindMatches is the most matches of whatsapp_find. whatsappFindMatches = 20 // whatsappNamesNotReady is the answer of whatsapp_find before the names // arrive from the phone. 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." ) // whatsappFind returns the contacts and the groups whose name contains name, // with no regard to case and accents, as lines "ID | name | contact" or "ID | // name | group", sorted by name. It gives at most whatsappFindMatches lines and // the number of the others. The contacts come from the session database, and // the groups from WhatsApp, read again after whatsappGroupsAge. When the groups // cannot be read and none are known, the answer says so. Before the names // arrive from the phone, it says that they are not ready. link gives the // device, the client to read the groups, and the own IDs, which are not in the // answer. func whatsappFind(ctx context.Context, link WhatsAppLink, groups *WhatsAppGroups, name string) (string, error) { stale := groups.stale() if !link.linked { return whatsappNotLinked, nil } query := whatsappFold(name) if query == "" { return "", errors.New("name is empty: give a part of the name of the contact or the group") } contacts, err := link.device.Contacts.GetAllContacts(ctx) if err != nil { return "", fmt.Errorf("read the contacts: %s", whatsappRedact(link.own, err.Error())) } read := !stale || groups.read(ctx, link) names := groups.all() if len(contacts) == 0 && len(names) == 0 { if !read { return "The names are not known yet, and WhatsApp is disconnected, so the groups cannot be read. Try again later.", nil } return whatsappNamesNotReady, nil } var matches []WhatsAppMatch for jid, contact := range contacts { names := []string{contact.FullName, contact.FirstName, contact.BusinessName, contact.PushName} shown := slices.IndexFunc(names, func(name string) bool { return name != "" }) if slices.Contains(link.own, jid.User) || shown < 0 || !slices.ContainsFunc(names, func(name string) bool { return strings.Contains(whatsappFold(name), query) }) { continue } matches = append(matches, WhatsAppMatch{ id: jid.ToNonAD().String(), name: names[shown], kind: "contact", }) } for id, group := range names { if strings.Contains(whatsappFold(group), query) { matches = append(matches, WhatsAppMatch{ id: id, name: group, kind: "group", }) } } return whatsappFindAnswer(name, matches, read || len(names) > 0), nil } // WhatsAppMatch is one contact or group of whatsappFind. type WhatsAppMatch struct { id string name string kind string } // whatsappFold returns text in lower case with no accents and no space at its // ends, so that names match with no regard to case and accents. func whatsappFold(text string) string { folded, _, err := transform.String(transform.Chain(norm.NFD, runes.Remove(runes.In(unicode.Mn)), norm.NFC), text) if err != nil { folded = text } return strings.ToLower(strings.TrimSpace(folded)) } // whatsappFindAnswer returns the answer of whatsappFind for name with matches. // Without groups, the answer says that it has no groups. func whatsappFindAnswer(name string, matches []WhatsAppMatch, groups bool) string { slices.SortFunc(matches, func(a, b WhatsAppMatch) int { return cmp.Or(strings.Compare(whatsappFold(a.name), whatsappFold(b.name)), strings.Compare(a.id, b.id)) }) var lines []string for _, match := range matches[:min(len(matches), whatsappFindMatches)] { lines = append(lines, match.id+" | "+match.name+" | "+match.kind) } if len(matches) == 0 { lines = append(lines, "No contact or group has a name with \""+name+"\".") } if len(matches) > whatsappFindMatches { lines = append(lines, fmt.Sprintf("%d more: give a longer part of the name.", len(matches)-whatsappFindMatches)) } if !groups { lines = append(lines, "The groups are not in this answer, because WhatsApp is disconnected.") } return strings.Join(lines, "\n") }