]> git.giacomolli.uk - onechat/blob - whatsapp_find.go
Import WhatsApp history into a child
[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
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 {
68 return name != ""
69 })
70
71 if slices.Contains(link.own, jid.User) || shown < 0 || !slices.ContainsFunc(names, func(name string) bool {
72 return strings.Contains(whatsappFold(name), query)
73 }) {
74 continue
75 }
76
77 matches = append(matches, WhatsAppMatch{
78 id: jid.ToNonAD().String(),
79 name: names[shown],
80 kind: "contact",
81 })
82 }
83
84 for id, group := range names {
85 if strings.Contains(whatsappFold(group), query) {
86 matches = append(matches, WhatsAppMatch{
87 id: id,
88 name: group,
89 kind: "group",
90 })
91 }
92 }
93
94 return whatsappFindAnswer(name, matches, read || len(names) > 0), nil
95 }
96
97 // WhatsAppMatch is one contact or group of whatsappFind.
98 type WhatsAppMatch struct {
99 id string
100 name string
101 kind string
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. Without
116 // 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 }