github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/teamchannelsource.go (about) 1 package chat 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "sync" 8 "time" 9 10 "github.com/keybase/client/go/chat/globals" 11 "github.com/keybase/client/go/chat/types" 12 "github.com/keybase/client/go/chat/utils" 13 "github.com/keybase/client/go/libkb" 14 "github.com/keybase/client/go/protocol/chat1" 15 "github.com/keybase/client/go/protocol/gregor1" 16 "github.com/keybase/client/go/protocol/keybase1" 17 ) 18 19 type recentJoinsCacheItem struct { 20 numJoins int 21 mtime gregor1.Time 22 } 23 24 type recentJoinsMemCache struct { 25 sync.RWMutex 26 cache map[chat1.ConvIDStr]recentJoinsCacheItem 27 } 28 29 func newRecentJoinsMemCache() *recentJoinsMemCache { 30 return &recentJoinsMemCache{ 31 cache: make(map[chat1.ConvIDStr]recentJoinsCacheItem), 32 } 33 } 34 35 func (i *recentJoinsMemCache) Get(convID chat1.ConversationID) int { 36 i.RLock() 37 defer i.RUnlock() 38 if item, ok := i.cache[convID.ConvIDStr()]; ok { 39 if time.Since(item.mtime.Time()) > time.Hour { 40 delete(i.cache, convID.ConvIDStr()) 41 return -1 42 } 43 return item.numJoins 44 } 45 return -1 46 } 47 48 func (i *recentJoinsMemCache) Put(convID chat1.ConversationID, numJoins int) { 49 i.Lock() 50 defer i.Unlock() 51 i.cache[convID.ConvIDStr()] = recentJoinsCacheItem{ 52 numJoins: numJoins, 53 mtime: gregor1.ToTime(time.Now()), 54 } 55 } 56 57 func (i *recentJoinsMemCache) clearCache() { 58 i.Lock() 59 defer i.Unlock() 60 i.cache = make(map[chat1.ConvIDStr]recentJoinsCacheItem) 61 } 62 63 func (i *recentJoinsMemCache) OnLogout(mctx libkb.MetaContext) error { 64 i.clearCache() 65 return nil 66 } 67 68 func (i *recentJoinsMemCache) OnDbNuke(mctx libkb.MetaContext) error { 69 i.clearCache() 70 return nil 71 } 72 73 type lastActiveAtCacheItem struct { 74 lastActiveAt gregor1.Time 75 mtime gregor1.Time 76 } 77 78 type lastActiveAtMemCache struct { 79 sync.RWMutex 80 // key: teamID||uid 81 cache map[string]lastActiveAtCacheItem 82 } 83 84 func newLastActiveAtMemCache() *lastActiveAtMemCache { 85 return &lastActiveAtMemCache{ 86 cache: make(map[string]lastActiveAtCacheItem), 87 } 88 } 89 90 func (i *lastActiveAtMemCache) key(teamID keybase1.TeamID, uid gregor1.UID) string { 91 return fmt.Sprintf("%s:%s", teamID, uid) 92 } 93 94 func (i *lastActiveAtMemCache) Get(teamID keybase1.TeamID, uid gregor1.UID) (gregor1.Time, bool) { 95 i.RLock() 96 defer i.RUnlock() 97 if item, ok := i.cache[i.key(teamID, uid)]; ok { 98 if time.Since(item.mtime.Time()) > time.Hour { 99 delete(i.cache, i.key(teamID, uid)) 100 return 0, false 101 } 102 return item.lastActiveAt, true 103 } 104 return 0, false 105 } 106 107 func (i *lastActiveAtMemCache) Put(teamID keybase1.TeamID, uid gregor1.UID, lastActiveAt gregor1.Time) { 108 i.Lock() 109 defer i.Unlock() 110 i.cache[i.key(teamID, uid)] = lastActiveAtCacheItem{ 111 lastActiveAt: lastActiveAt, 112 mtime: gregor1.ToTime(time.Now()), 113 } 114 } 115 116 func (i *lastActiveAtMemCache) clearCache() { 117 i.Lock() 118 defer i.Unlock() 119 i.cache = make(map[string]lastActiveAtCacheItem) 120 } 121 122 func (i *lastActiveAtMemCache) OnLogout(mctx libkb.MetaContext) error { 123 i.clearCache() 124 return nil 125 } 126 127 func (i *lastActiveAtMemCache) OnDbNuke(mctx libkb.MetaContext) error { 128 i.clearCache() 129 return nil 130 } 131 132 type TeamChannelSource struct { 133 sync.Mutex 134 globals.Contextified 135 utils.DebugLabeler 136 recentJoinsCache *recentJoinsMemCache 137 lastActiveAtCache *lastActiveAtMemCache 138 } 139 140 var _ types.TeamChannelSource = (*TeamChannelSource)(nil) 141 142 func NewTeamChannelSource(g *globals.Context) *TeamChannelSource { 143 return &TeamChannelSource{ 144 Contextified: globals.NewContextified(g), 145 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "TeamChannelSource", false), 146 recentJoinsCache: newRecentJoinsMemCache(), 147 lastActiveAtCache: newLastActiveAtMemCache(), 148 } 149 } 150 151 func (c *TeamChannelSource) OnLogout(mctx libkb.MetaContext) error { 152 epick := libkb.FirstErrorPicker{} 153 epick.Push(c.recentJoinsCache.OnLogout(mctx)) 154 epick.Push(c.lastActiveAtCache.OnLogout(mctx)) 155 return epick.Error() 156 } 157 158 func (c *TeamChannelSource) OnDbNuke(mctx libkb.MetaContext) error { 159 epick := libkb.FirstErrorPicker{} 160 epick.Push(c.recentJoinsCache.OnDbNuke(mctx)) 161 epick.Push(c.lastActiveAtCache.OnDbNuke(mctx)) 162 return epick.Error() 163 } 164 165 func (c *TeamChannelSource) getTLFConversations(ctx context.Context, uid gregor1.UID, 166 tlfID chat1.TLFID, topicType chat1.TopicType) ([]types.RemoteConversation, error) { 167 inbox, err := c.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceAll, 168 &chat1.GetInboxQuery{ 169 TlfID: &tlfID, 170 TopicType: &topicType, 171 SummarizeMaxMsgs: false, 172 MemberStatus: chat1.AllConversationMemberStatuses(), 173 Existences: []chat1.ConversationExistence{chat1.ConversationExistence_ACTIVE}, 174 SkipBgLoads: true, 175 }) 176 return inbox.ConvsUnverified, err 177 } 178 179 func (c *TeamChannelSource) GetLastActiveForTLF(ctx context.Context, uid gregor1.UID, 180 tlfID chat1.TLFID, topicType chat1.TopicType) (res gregor1.Time, err error) { 181 defer c.Trace(ctx, &err, 182 fmt.Sprintf("GetLastActiveForTLF: tlfID: %v, topicType: %v", tlfID, topicType))() 183 184 rcs, err := c.getTLFConversations(ctx, uid, tlfID, topicType) 185 if err != nil { 186 return 0, err 187 } 188 sort.Sort(utils.RemoteConvByMtime(rcs)) 189 if len(rcs) > 0 { 190 return utils.GetConvMtime(rcs[0]), nil 191 } 192 return 0, nil 193 } 194 195 func (c *TeamChannelSource) GetLastActiveForTeams(ctx context.Context, uid gregor1.UID, topicType chat1.TopicType) ( 196 res chat1.LastActiveTimeAll, err error) { 197 ctx = globals.CtxModifyUnboxMode(ctx, types.UnboxModeQuick) 198 defer c.Trace(ctx, &err, 199 fmt.Sprintf("GetLastActiveForTeams: topicType: %v", topicType))() 200 201 inbox, err := c.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceAll, 202 &chat1.GetInboxQuery{ 203 TopicType: &topicType, 204 SummarizeMaxMsgs: false, 205 MemberStatus: chat1.AllConversationMemberStatuses(), 206 Existences: []chat1.ConversationExistence{chat1.ConversationExistence_ACTIVE}, 207 MembersTypes: []chat1.ConversationMembersType{chat1.ConversationMembersType_TEAM}, 208 SkipBgLoads: true, 209 }) 210 byTLFID := make(map[chat1.TLFIDStr][]types.RemoteConversation) 211 channels := make(map[chat1.ConvIDStr]gregor1.Time, len(inbox.ConvsUnverified)) 212 for _, conv := range inbox.ConvsUnverified { 213 rc := conv 214 tlfID := rc.Conv.Metadata.IdTriple.Tlfid.TLFIDStr() 215 byTLFID[tlfID] = append(byTLFID[tlfID], rc) 216 channels[rc.ConvIDStr] = utils.GetConvMtime(rc) 217 } 218 teams := make(map[chat1.TLFIDStr]gregor1.Time, len(byTLFID)) 219 for tlfID, rcs := range byTLFID { 220 sort.Sort(utils.RemoteConvByMtime(rcs)) 221 teams[tlfID] = channels[rcs[0].ConvIDStr] 222 } 223 res.Teams = teams 224 res.Channels = channels 225 return res, nil 226 } 227 228 func (c *TeamChannelSource) GetChannelsFull(ctx context.Context, uid gregor1.UID, 229 tlfID chat1.TLFID, topicType chat1.TopicType) (res []chat1.ConversationLocal, err error) { 230 ctx = globals.CtxModifyUnboxMode(ctx, types.UnboxModeQuick) 231 defer c.Trace(ctx, &err, 232 fmt.Sprintf("GetChannelsFull: tlfID: %v, topicType: %v", tlfID, topicType))() 233 234 rcs, err := c.getTLFConversations(ctx, uid, tlfID, topicType) 235 if err != nil { 236 return nil, err 237 } 238 for _, rc := range rcs { 239 c.G().ParticipantsSource.GetWithNotifyNonblock(ctx, uid, rc.GetConvID(), 240 types.InboxSourceDataSourceAll) 241 } 242 convs, _, err := c.G().InboxSource.Localize(ctx, uid, rcs, types.ConversationLocalizerBlocking) 243 if err != nil { 244 c.Debug(ctx, "GetChannelsFull: failed to localize conversations: %s", err.Error()) 245 return nil, err 246 } 247 sort.Sort(utils.ConvLocalByTopicName(convs)) 248 c.Debug(ctx, "GetChannelsFull: found %d convs", len(convs)) 249 return convs, nil 250 } 251 252 func (c *TeamChannelSource) GetChannelsTopicName(ctx context.Context, uid gregor1.UID, 253 tlfID chat1.TLFID, topicType chat1.TopicType) (res []chat1.ChannelNameMention, err error) { 254 ctx = globals.CtxModifyUnboxMode(ctx, types.UnboxModeQuick) 255 defer c.Trace(ctx, &err, 256 fmt.Sprintf("GetChannelsTopicName: tlfID: %v, topicType: %v", tlfID, topicType))() 257 258 addValidMetadataMsg := func(convID chat1.ConversationID, msg chat1.MessageUnboxed) { 259 if !msg.IsValid() { 260 c.Debug(ctx, "GetChannelsTopicName: metadata message invalid: convID, %s", convID) 261 return 262 } 263 body := msg.Valid().MessageBody 264 typ, err := body.MessageType() 265 if err != nil { 266 c.Debug(ctx, "GetChannelsTopicName: error getting message type: convID, %s", 267 convID, err) 268 return 269 } 270 if typ != chat1.MessageType_METADATA { 271 c.Debug(ctx, "GetChannelsTopicName: message not a real metadata message: convID, %s msgID: %d", 272 convID, msg.GetMessageID()) 273 return 274 } 275 res = append(res, chat1.ChannelNameMention{ 276 ConvID: convID, 277 TopicName: body.Metadata().ConversationTitle, 278 }) 279 } 280 281 convs, err := c.getTLFConversations(ctx, uid, tlfID, topicType) 282 if err != nil { 283 return nil, err 284 } 285 for _, rc := range convs { 286 conv := rc.Conv 287 msg, err := conv.GetMaxMessage(chat1.MessageType_METADATA) 288 if err != nil { 289 continue 290 } 291 unboxeds, err := c.G().ConvSource.GetMessages(ctx, conv.GetConvID(), uid, 292 []chat1.MessageID{msg.GetMessageID()}, nil, nil, false) 293 if err != nil { 294 c.Debug(ctx, "GetChannelsTopicName: failed to unbox metadata message for: convID: %s err: %s", 295 conv.GetConvID(), err) 296 continue 297 } 298 if len(unboxeds) != 1 { 299 c.Debug(ctx, "GetChannelsTopicName: empty result: convID: %s", conv.GetConvID()) 300 continue 301 } 302 addValidMetadataMsg(conv.GetConvID(), unboxeds[0]) 303 } 304 return res, nil 305 } 306 307 func (c *TeamChannelSource) GetChannelTopicName(ctx context.Context, uid gregor1.UID, 308 tlfID chat1.TLFID, topicType chat1.TopicType, convID chat1.ConversationID) (res string, err error) { 309 ctx = globals.CtxModifyUnboxMode(ctx, types.UnboxModeQuick) 310 defer c.Trace(ctx, &err, 311 fmt.Sprintf("GetChannelTopicName: tlfID: %v, topicType: %v, convID: %v", tlfID, topicType, convID))() 312 313 convs, err := c.GetChannelsTopicName(ctx, uid, tlfID, topicType) 314 if err != nil { 315 return "", err 316 } 317 if len(convs) == 0 { 318 return "", fmt.Errorf("no convs found") 319 } 320 for _, conv := range convs { 321 if conv.ConvID.Eq(convID) { 322 return conv.TopicName, nil 323 } 324 } 325 return "", fmt.Errorf("no convs found with convID") 326 } 327 328 func (c *TeamChannelSource) GetRecentJoins(ctx context.Context, convID chat1.ConversationID, remoteClient chat1.RemoteInterface) (res int, err error) { 329 defer c.Trace(ctx, &err, "GetRecentJoins")() 330 331 numJoins := c.recentJoinsCache.Get(convID) 332 if numJoins < 0 { 333 res, err := remoteClient.GetRecentJoins(ctx, convID) 334 if err != nil { 335 return 0, err 336 } 337 numJoins = res.NumJoins 338 c.recentJoinsCache.Put(convID, numJoins) 339 } 340 return numJoins, nil 341 } 342 343 func (c *TeamChannelSource) GetLastActiveAt(ctx context.Context, teamID keybase1.TeamID, uid gregor1.UID, 344 remoteClient chat1.RemoteInterface) (res gregor1.Time, err error) { 345 defer c.Trace(ctx, &err, "GetLastActiveAt")() 346 347 lastActiveAt, found := c.lastActiveAtCache.Get(teamID, uid) 348 if !found { 349 res, err := remoteClient.GetLastActiveAt(ctx, chat1.GetLastActiveAtArg{ 350 TeamID: teamID, 351 Uid: uid, 352 }) 353 if err != nil { 354 return 0, err 355 } 356 lastActiveAt = res.LastActiveAt 357 c.lastActiveAtCache.Put(teamID, uid, lastActiveAt) 358 } 359 return lastActiveAt, nil 360 }