package main import ( "context" "log/slog" "maps" "sync" "time" ) const ( // whatsappGroupLookupTimeout is the longest time of a read of group // names. whatsappGroupLookupTimeout = 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" // 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 *slog.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 *slog.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, whatsappGroupLookupTimeout) defer cancel() groups, err := link.client.GetJoinedGroups(ctx) if err != nil { g.logger.Warn("group list failed", "err", 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) }