+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)
+}