-// deliver logs the closed batches in l in order, each as a work message of
-// whatsappSource, and wakes the loop once with the batches that start a turn. A
-// batch leaves the state file after its message is durable in the log. A group
-// with no name gets its name from WhatsApp first. During a catch-up it logs
-// nothing, and at its end it closes all batches first. With all, it closes and
-// logs all batches also during a catch-up.
-func (w *WhatsApp) deliver(ctx context.Context, l *TurnLoop, all bool) error {
- w.mutex.Lock()
- catchingUp := w.catchingUp
- caughtUp := w.caughtUp
- w.caughtUp = false
- w.mutex.Unlock()
-
- var err error
- switch {
- case all || caughtUp:
- err = w.state.CloseAll()
- case catchingUp:
- return nil
- default:
- _, err = w.state.CloseDue(time.Now())
- }
-
- if err != nil {
- return err
- }
-
- var turns []Message
- for _, batch := range w.state.Closed() {
- if batch.Chat.Group && batch.Chat.Name == "" {
- batch.Chat.Name = w.groupName(ctx, batch.Chat.ID)
- }
-
- message, err := l.Work(whatsappSource, batch.Text(w.clock().Location()), "")
- if err != nil {
- return fmt.Errorf("log a WhatsApp batch: %w", err)
- }
-
- // A stop of the daemon during the flush is no failure: the
- // batch stays in the state file and repeats once at the next
- // start.
- err = l.Flush(ctx)
- if ctx.Err() != nil {
- return nil
- }
-
- if err != nil {
- return fmt.Errorf("log a WhatsApp batch: %w", err)
- }
-
- err = w.state.Remove(batch.Number)
- if err != nil {
- return err
- }
-
- if batch.Turn() {
- turns = append(turns, message)
- }
- }
-
- l.Wake(turns...)
-
- return nil
-}
-
-// groupName returns the name of the group id, or whatsappUnknownGroup. A group
-// that is not in the names of the groups reads the groups from WhatsApp again,
-// at most once in whatsappGroupsRefresh.
-func (w *WhatsApp) groupName(ctx context.Context, id string) string {
- w.mutex.Lock()
- name, found := w.groupNames[id]
- fresh := time.Since(w.groupsAt) < whatsappGroupsRefresh
- w.mutex.Unlock()
-
- if !found && !fresh {
- w.readGroups(ctx)
-
- w.mutex.Lock()
- name, found = w.groupNames[id]
- w.mutex.Unlock()
- }
-
- if !found || name == "" {
- return whatsappUnknownGroup
- }
-
- return name
-}
-
-// readGroups reads the names of the groups of the account from WhatsApp, and
-// tells if it could. They stay in memory only.
-func (w *WhatsApp) readGroups(ctx context.Context) bool {
- w.mutex.Lock()
- client := w.client
- w.mutex.Unlock()
-
- if client == nil {
- return false
- }
-
- ctx, cancel := context.WithTimeout(ctx, whatsappLookupTime)
- defer cancel()
-
- groups, err := client.GetJoinedGroups(ctx)
- if err != nil {
- w.logger.Printf("read the groups: %s", w.redact(err.Error()))
- }
-
- w.mutex.Lock()
- defer w.mutex.Unlock()
-
- w.groupsAt = time.Now()
- if err != nil {
- return false
- }
-
- w.groupsRead = w.groupsAt
- for _, group := range groups {
- w.groupNames[group.JID.String()] = group.Name
- }
-
- return true
-}
-
-// Find 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.
-func (w *WhatsApp) Find(ctx context.Context, name string) (string, error) {
- w.mutex.Lock()
- linked := w.linked
- device := w.device
- own := slices.Clone(w.own)
- stale := time.Since(w.groupsRead) >= whatsappGroupsAge
- w.mutex.Unlock()
-
- if !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 := device.Contacts.GetAllContacts(ctx)
- if err != nil {
- return "", fmt.Errorf("read the contacts: %s", w.redact(err.Error()))
- }
-
- read := !stale || w.readGroups(ctx)
- w.mutex.Lock()
- groups := maps.Clone(w.groupNames)
- w.mutex.Unlock()
-
- if len(contacts) == 0 && len(groups) == 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(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 groups {
- if strings.Contains(whatsappFold(group), query) {
- matches = append(matches, WhatsAppMatch{
- id: id,
- name: group,
- kind: "group",
- })
- }
- }
-
- return whatsappFindAnswer(name, matches, read || len(groups) > 0), nil
-}
-