]> git.giacomolli.uk - onechat/blob - whatsapp_groups.go
Reflow comments over the reflow width
[onechat] / whatsapp_groups.go
1 package main
2
3 import (
4 "context"
5 "log/slog"
6 "maps"
7 "sync"
8 "time"
9 )
10
11 const (
12 // whatsappGroupLookupTimeout is the longest time of a read of group
13 // names.
14 whatsappGroupLookupTimeout = 10 * time.Second
15
16 // whatsappGroupsRefresh is the shortest time between two reads of the
17 // groups from WhatsApp for a group with no name.
18 whatsappGroupsRefresh = time.Minute
19
20 // whatsappUnknownGroup is the name of a group whose name is not known.
21 whatsappUnknownGroup = "A group with no known name"
22
23 // whatsappGroupsAge is how long whatsapp_find uses the groups that it
24 // read from WhatsApp.
25 whatsappGroupsAge = time.Hour
26 )
27
28 // newWhatsAppGroups returns the names of the groups with no names yet. logger
29 // gets the failures of a read.
30 func newWhatsAppGroups(logger *slog.Logger) *WhatsAppGroups {
31 return &WhatsAppGroups{
32 logger: logger,
33 names: make(map[string]string),
34 }
35 }
36
37 // WhatsAppGroups holds the names of the groups of the account, by the ID of the
38 // group. They come from the events of the groups and from reads of the groups
39 // from WhatsApp, and they stay in memory only. triedAt is the time of the last
40 // read, and readAt the time of the last read that succeeded. The mutex
41 // protects all fields after logger.
42 type WhatsAppGroups struct {
43 logger *slog.Logger
44
45 mutex sync.Mutex
46 names map[string]string
47 triedAt time.Time
48 readAt time.Time
49 }
50
51 // name returns the name of the group id, or whatsappUnknownGroup. A group that
52 // is not in the names reads the groups from WhatsApp with the client of link
53 // again, at most once in whatsappGroupsRefresh.
54 func (g *WhatsAppGroups) name(ctx context.Context, link WhatsAppLink, id string) string {
55 g.mutex.Lock()
56
57 name, found := g.names[id]
58 fresh := time.Since(g.triedAt) < whatsappGroupsRefresh
59 g.mutex.Unlock()
60
61 if !found && !fresh {
62 g.read(ctx, link)
63
64 g.mutex.Lock()
65
66 name, found = g.names[id]
67 g.mutex.Unlock()
68 }
69
70 if !found || name == "" {
71 return whatsappUnknownGroup
72 }
73
74 return name
75 }
76
77 // read reads the names of the groups of the account from WhatsApp with the
78 // client of link, and tells if it could.
79 func (g *WhatsAppGroups) read(ctx context.Context, link WhatsAppLink) bool {
80 if link.client == nil {
81 return false
82 }
83
84 ctx, cancel := context.WithTimeout(ctx, whatsappGroupLookupTimeout)
85 defer cancel()
86
87 groups, err := link.client.GetJoinedGroups(ctx)
88 if err != nil {
89 g.logger.Warn("group list failed", "err", whatsappRedact(link.own, err.Error()))
90 }
91
92 g.mutex.Lock()
93 defer g.mutex.Unlock()
94
95 g.triedAt = time.Now()
96 if err != nil {
97 return false
98 }
99
100 g.readAt = g.triedAt
101 for _, group := range groups {
102 g.names[group.JID.String()] = group.Name
103 }
104
105 return true
106 }
107
108 // set makes name the name of the group id.
109 func (g *WhatsAppGroups) set(id, name string) {
110 g.mutex.Lock()
111 defer g.mutex.Unlock()
112
113 g.names[id] = name
114 }
115
116 // stale tells if the last read that succeeded is older than whatsappGroupsAge.
117 func (g *WhatsAppGroups) stale() bool {
118 g.mutex.Lock()
119 defer g.mutex.Unlock()
120
121 return time.Since(g.readAt) >= whatsappGroupsAge
122 }
123
124 // all returns a copy of the names of the groups.
125 func (g *WhatsAppGroups) all() map[string]string {
126 g.mutex.Lock()
127 defer g.mutex.Unlock()
128
129 return maps.Clone(g.names)
130 }