+// whatsappContactName returns the name of the person jid: the full name, the
+// first name, the business name, or the push name of the contact in the session
+// of device, then push, the push name of a message, then the phone number. A
+// LID gets its phone number from the LID map of the session.
+func whatsappContactName(ctx context.Context, device *store.Device, jid types.JID, push string) string {
+ jid = jid.ToNonAD()
+ candidates := []types.JID{jid}
+ phone := types.EmptyJID
+ switch jid.Server {
+ case types.DefaultUserServer:
+ phone = jid
+ case types.HiddenUserServer:
+ mapped, err := device.LIDs.GetPNForLID(ctx, jid)
+ if err == nil && !mapped.IsEmpty() {
+ phone = mapped
+ candidates = append(candidates, mapped)
+ }
+ }
+
+ for _, candidate := range candidates {
+ contact, err := device.Contacts.GetContact(ctx, candidate)
+ if err != nil || !contact.Found {
+ continue
+ }
+
+ for _, name := range []string{contact.FullName, contact.FirstName, contact.BusinessName, contact.PushName} {
+ if name != "" {
+ return name
+ }
+ }
+ }
+
+ switch {
+ case push != "":
+ return push
+ case !phone.IsEmpty():
+ return "+" + phone.User
+ default:
+ return jid.User
+ }
+}
+
+// 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 Find 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")
+}
+
+// whatsappSendCheck returns the JID of chat, and an error when id, chat, or
+// text cannot go to a send. The chat is a private chat or a group, and not the
+// chat of the user with itself.
+func whatsappSendCheck(id, chat, text string, own []string) (types.JID, error) {
+ jid, err := types.ParseJID(chat)
+ switch {
+ case strings.TrimSpace(id) == "" || len(id) > whatsappSendIDBytes:
+ return types.EmptyJID, fmt.Errorf("the id must have 1 to %d bytes", whatsappSendIDBytes)
+ case strings.TrimSpace(text) == "" || len(text) > whatsappSendBytes:
+ return types.EmptyJID, fmt.Errorf("the text must have 1 to %d bytes", whatsappSendBytes)
+ case err != nil || jid.User == "" || jid != jid.ToNonAD() || jid.String() != chat:
+ return types.EmptyJID, fmt.Errorf("%q is not the ID of a WhatsApp chat: find the ID with whatsapp_find", chat)
+ case jid.Server != types.DefaultUserServer && jid.Server != types.HiddenUserServer && jid.Server != types.GroupServer:
+ return types.EmptyJID, fmt.Errorf("%q is not a private chat or a group", chat)
+ case slices.Contains(own, jid.User):
+ return types.EmptyJID, errors.New("this is the chat of the user with itself: send to another chat")
+ }
+
+ return jid, nil
+}
+