-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathcache.go
More file actions
235 lines (199 loc) · 5.88 KB
/
Copy pathcache.go
File metadata and controls
235 lines (199 loc) · 5.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
package main
import (
"strings"
"sync"
"go.uber.org/zap"
)
// dialogCache is an in-memory snapshot of the user's channel and supergroup
// dialogs. It is seeded at startup (from persistent storage, or a one-time full
// fetch on first run) and then kept live by the Telegram updates stream.
//
// This replaces the previous approach of re-fetching the entire dialog list on
// every tool call, which fired one messages.getDialogs RPC per dialog and
// triggered FLOOD_WAIT. It mirrors how tdlib maintains per-dialog unread counts:
// load once, then mutate in place as updates arrive.
//
// Mutations are written through to the store so the cache survives restarts;
// the updates manager then reconciles it via getDifference.
type dialogCache struct {
mu sync.RWMutex
channels map[int64]UnreadChannel
store *dialogStore // optional; nil disables persistence.
lg *zap.Logger
}
func newDialogCache(store *dialogStore, lg *zap.Logger) *dialogCache {
return &dialogCache{
channels: make(map[int64]UnreadChannel),
store: store,
lg: lg,
}
}
// loadFromStore replaces the in-memory cache with the persisted dialogs and
// returns how many were loaded. Returns 0 when no store is configured or none
// are persisted yet.
func (c *dialogCache) loadFromStore() (int, error) {
if c.store == nil {
return 0, nil
}
chs, err := c.store.load()
if err != nil {
return 0, err
}
m := make(map[int64]UnreadChannel, len(chs))
for _, ch := range chs {
// Drop group chats (supergroups) persisted before tgmcp narrowed to
// broadcast channels only.
if !ch.Broadcast {
continue
}
m[ch.ID] = ch
}
c.mu.Lock()
c.channels = m
c.mu.Unlock()
return len(m), nil
}
// replaceAll swaps the entire cache content and persists it. Used by the
// one-time full fetch.
func (c *dialogCache) replaceAll(chs []UnreadChannel) {
m := make(map[int64]UnreadChannel, len(chs))
for _, ch := range chs {
m[ch.ID] = ch
}
c.mu.Lock()
c.channels = m
c.mu.Unlock()
if c.store != nil {
if err := c.store.putAll(chs); err != nil {
c.lg.Error("Persist dialogs", zap.Error(err))
}
}
}
// unread returns the cached channels that currently have unread messages or are
// manually marked as unread.
func (c *dialogCache) unread() []UnreadChannel {
c.mu.RLock()
defer c.mu.RUnlock()
var out []UnreadChannel
for _, ch := range c.channels {
if ch.UnreadCount > 0 || ch.UnreadMark {
out = append(out, ch)
}
}
return out
}
// find resolves a cached channel by numeric ID or @username.
func (c *dialogCache) find(target string) (UnreadChannel, bool) {
target = strings.TrimPrefix(strings.TrimSpace(target), "@")
wantID, isID := parseID(target)
c.mu.RLock()
defer c.mu.RUnlock()
for _, ch := range c.channels {
if isID && ch.ID == wantID {
return ch, true
}
if !isID && strings.EqualFold(ch.Username, target) {
return ch, true
}
}
return UnreadChannel{}, false
}
// get returns a cached channel by ID.
func (c *dialogCache) get(id int64) (UnreadChannel, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
ch, ok := c.channels[id]
return ch, ok
}
// set upserts a fully-resolved channel and persists it. Used to resync a single
// channel after a too-long difference.
func (c *dialogCache) set(ch UnreadChannel) {
c.mu.Lock()
c.channels[ch.ID] = ch
c.mu.Unlock()
c.persist(ch)
}
// remove drops a channel from the cache and the store. Used when the channel is
// no longer accessible (e.g. CHANNEL_PRIVATE: we were kicked, banned, or it went
// private).
func (c *dialogCache) remove(channelID int64) {
c.mu.Lock()
_, ok := c.channels[channelID]
delete(c.channels, channelID)
c.mu.Unlock()
if !ok || c.store == nil {
return
}
if err := c.store.delete(channelID); err != nil {
c.lg.Error("Delete dialog", zap.Int64("id", channelID), zap.Error(err))
}
}
// observeIncoming records an incoming message in a channel. If the channel is
// already cached its unread count is incremented; otherwise build is called to
// resolve channel metadata (from update entities) and the channel is inserted
// with a single unread message. build may return false when the channel cannot
// be resolved, in which case the message is dropped.
func (c *dialogCache) observeIncoming(channelID int64, build func() (UnreadChannel, bool)) {
c.mu.Lock()
ch, ok := c.channels[channelID]
if ok {
ch.UnreadCount++
c.channels[channelID] = ch
} else if nch, built := build(); built {
nch.UnreadCount = 1
ch, ok = nch, true
c.channels[channelID] = nch
}
c.mu.Unlock()
if ok {
c.persist(ch)
}
}
// setRead applies a read-inbox update: messages up to maxID are read and
// stillUnread messages remain. Unknown channels are ignored.
func (c *dialogCache) setRead(channelID int64, maxID, stillUnread int) {
c.update(channelID, func(ch *UnreadChannel) {
ch.readInboxMaxID = maxID
if stillUnread >= 0 {
ch.UnreadCount = stillUnread
}
ch.UnreadMark = false
})
}
// setUnreadMark applies a manual unread mark toggle. Unknown channels are
// ignored.
func (c *dialogCache) setUnreadMark(channelID int64, mark bool) {
c.update(channelID, func(ch *UnreadChannel) {
ch.UnreadMark = mark
})
}
// markRead clears the unread state of a channel after we mark it read locally.
func (c *dialogCache) markRead(channelID int64) {
c.update(channelID, func(ch *UnreadChannel) {
ch.UnreadCount = 0
ch.UnreadMark = false
})
}
// update applies mutate to a cached channel under lock and persists the result.
// Unknown channels are ignored.
func (c *dialogCache) update(channelID int64, mutate func(*UnreadChannel)) {
c.mu.Lock()
ch, ok := c.channels[channelID]
if ok {
mutate(&ch)
c.channels[channelID] = ch
}
c.mu.Unlock()
if ok {
c.persist(ch)
}
}
// persist write-throughs a single channel to the store, logging any error.
func (c *dialogCache) persist(ch UnreadChannel) {
if c.store == nil {
return
}
if err := c.store.put(ch); err != nil {
c.lg.Error("Persist dialog", zap.Int64("id", ch.ID), zap.Error(err))
}
}