"errors"
"fmt"
"log"
- "maps"
"net/url"
"os"
"path/filepath"
// session database or of the groups from WhatsApp.
whatsappLookupTime = 10 * time.Second
- // whatsappGroupsRefresh is the shortest time between two reads of the
- // groups from WhatsApp for a group with no name.
- whatsappGroupsRefresh = time.Minute
-
- // whatsappUnknownGroup is the name of a group whose name is not known.
- whatsappUnknownGroup = "A group with no known name"
-
// whatsappCatchupLimit is the longest wait for the end of the catch-up
// after a connection.
whatsappCatchupLimit = time.Minute
- // whatsappGroupsAge is how long whatsapp_find uses the groups that it
- // read from WhatsApp.
- whatsappGroupsAge = time.Hour
-
// whatsappFindMatches is the most matches of whatsapp_find.
whatsappFindMatches = 20
logouts: make(chan struct{}, 1),
wake: make(chan struct{}, 1),
failures: make(chan error, 1),
- groupNames: make(map[string]string),
+ groups: newWhatsAppGroups(logger),
}
var err error
wake chan struct{}
failures chan error
state *WhatsAppState
+ groups *WhatsAppGroups
mutex sync.Mutex
container *sqlstore.Container
noticed bool
timer *time.Timer
closed bool
- groupNames map[string]string
- groupsAt time.Time
- groupsRead time.Time
catchingUp bool
caughtUp bool
catchups int
return w.linked
}
+// link returns a copy of the link of the device: whether it is linked, the
+// device, the client, whether it is connected, and the own IDs.
+func (w *WhatsApp) link() WhatsAppLink {
+ w.mutex.Lock()
+ defer w.mutex.Unlock()
+
+ return WhatsAppLink{
+ linked: w.linked,
+ device: w.device,
+ client: w.client,
+ connected: w.connected,
+ own: slices.Clone(w.own),
+ }
+}
+
// connect makes the client of the linked device and connects it.
func (w *WhatsApp) connect(ctx context.Context) {
w.mutex.Lock()
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)
+ batch.Chat.Name = w.groups.name(ctx, w.link(), batch.Chat.ID)
}
message, err := l.Work(whatsappSource, batch.Text(w.clock().Location()), "")
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
linked := w.linked
device := w.device
own := slices.Clone(w.own)
- stale := time.Since(w.groupsRead) >= whatsappGroupsAge
w.mutex.Unlock()
+ stale := w.groups.stale()
+
if !linked {
return whatsappNotLinked, 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()
+ read := !stale || w.groups.read(ctx, w.link())
+ groups := w.groups.all()
if len(contacts) == 0 && len(groups) == 0 {
if !read {
// notice, and they get one more when the connection comes back. A disconnection
// that the library does not try again gives a notice at once.
func (w *WhatsApp) handle(event any) bool {
- message, isMessage := event.(*events.Message)
- if isMessage {
- return w.receive(message)
- }
-
- var notice string
- w.mutex.Lock()
switch event := event.(type) {
+ case *events.Message:
+ return w.receive(event)
case *events.GroupInfo:
if event.Name != nil {
- w.groupNames[event.JID.String()] = event.Name.Name
+ w.groups.set(event.JID.String(), event.Name.Name)
}
+
+ return true
case *events.JoinedGroup:
- w.groupNames[event.JID.String()] = event.Name
+ w.groups.set(event.JID.String(), event.Name)
+ return true
+ }
+
+ var notice string
+ w.mutex.Lock()
+ switch event := event.(type) {
case *events.PairSuccess:
w.own = whatsappOwnIDs(event.ID.User, event.LID.User)
case *events.Connected:
record.State = whatsappFailed
record.Answer = "The send failed: " + w.redact(err.Error()) + "."
} else {
- name := w.groupName(ctx, jid.String())
+ name := w.groups.name(ctx, w.link(), jid.String())
if jid.Server != types.GroupServer {
name = whatsappContactName(ctx, device, jid, "")
}
kind string
}
+// WhatsAppLink is a copy of the link of the device at one time, for the parts
+// of WhatsApp that do not own it: the groups, the batches, find, and send.
+type WhatsAppLink struct {
+ linked bool
+ device *store.Device
+ client WhatsAppClient
+ connected bool
+ own []string
+}
+
// WhatsAppRequest is a pair or unlink command for Run, with the function that
// gives its answers to the client that asked. A pairing has the phone number.
type WhatsAppRequest struct {
--- /dev/null
+package main
+
+import (
+ "context"
+ "log"
+ "maps"
+ "sync"
+ "time"
+)
+
+const (
+ // whatsappGroupsRefresh is the shortest time between two reads of the
+ // groups from WhatsApp for a group with no name.
+ whatsappGroupsRefresh = time.Minute
+
+ // whatsappUnknownGroup is the name of a group whose name is not known.
+ whatsappUnknownGroup = "A group with no known name"
+
+ // whatsappGroupsAge is how long whatsapp_find uses the groups that it
+ // read from WhatsApp.
+ whatsappGroupsAge = time.Hour
+)
+
+// newWhatsAppGroups returns the names of the groups with no names yet. logger
+// gets the failures of a read.
+func newWhatsAppGroups(logger *log.Logger) *WhatsAppGroups {
+ return &WhatsAppGroups{
+ logger: logger,
+ names: make(map[string]string),
+ }
+}
+
+// WhatsAppGroups holds the names of the groups of the account, by the ID of the
+// group. They come from the events of the groups and from reads of the groups
+// from WhatsApp, and they stay in memory only. triedAt is the time of the last
+// read, and readAt the time of the last read that succeeded. The mutex
+// protects all fields after logger.
+type WhatsAppGroups struct {
+ logger *log.Logger
+
+ mutex sync.Mutex
+ names map[string]string
+ triedAt time.Time
+ readAt time.Time
+}
+
+// name returns the name of the group id, or whatsappUnknownGroup. A group that
+// is not in the names reads the groups from WhatsApp with the client of link
+// again, at most once in whatsappGroupsRefresh.
+func (g *WhatsAppGroups) name(ctx context.Context, link WhatsAppLink, id string) string {
+ g.mutex.Lock()
+ name, found := g.names[id]
+ fresh := time.Since(g.triedAt) < whatsappGroupsRefresh
+ g.mutex.Unlock()
+
+ if !found && !fresh {
+ g.read(ctx, link)
+
+ g.mutex.Lock()
+ name, found = g.names[id]
+ g.mutex.Unlock()
+ }
+
+ if !found || name == "" {
+ return whatsappUnknownGroup
+ }
+
+ return name
+}
+
+// read reads the names of the groups of the account from WhatsApp with the
+// client of link, and tells if it could.
+func (g *WhatsAppGroups) read(ctx context.Context, link WhatsAppLink) bool {
+ if link.client == nil {
+ return false
+ }
+
+ ctx, cancel := context.WithTimeout(ctx, whatsappLookupTime)
+ defer cancel()
+
+ groups, err := link.client.GetJoinedGroups(ctx)
+ if err != nil {
+ g.logger.Printf("read the groups: %s", whatsappRedact(link.own, err.Error()))
+ }
+
+ g.mutex.Lock()
+ defer g.mutex.Unlock()
+
+ g.triedAt = time.Now()
+ if err != nil {
+ return false
+ }
+
+ g.readAt = g.triedAt
+ for _, group := range groups {
+ g.names[group.JID.String()] = group.Name
+ }
+
+ return true
+}
+
+// set makes name the name of the group id.
+func (g *WhatsAppGroups) set(id, name string) {
+ g.mutex.Lock()
+ defer g.mutex.Unlock()
+
+ g.names[id] = name
+}
+
+// stale tells if the last read that succeeded is older than whatsappGroupsAge.
+func (g *WhatsAppGroups) stale() bool {
+ g.mutex.Lock()
+ defer g.mutex.Unlock()
+
+ return time.Since(g.readAt) >= whatsappGroupsAge
+}
+
+// all returns a copy of the names of the groups.
+func (g *WhatsAppGroups) all() map[string]string {
+ g.mutex.Lock()
+ defer g.mutex.Unlock()
+
+ return maps.Clone(g.names)
+}