github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/kbfs/libkbfs/chat_local.go (about) 1 // Copyright 2018 Keybase Inc. All rights reserved. 2 // Use of this source code is governed by a BSD 3 // license that can be found in the LICENSE file. 4 5 package libkbfs 6 7 import ( 8 "context" 9 "fmt" 10 "sort" 11 "sync" 12 "time" 13 14 "github.com/keybase/client/go/kbfs/kbfscrypto" 15 "github.com/keybase/client/go/kbfs/tlf" 16 "github.com/keybase/client/go/kbfs/tlfhandle" 17 "github.com/keybase/client/go/logger" 18 "github.com/keybase/client/go/protocol/chat1" 19 "github.com/pkg/errors" 20 ) 21 22 type convLocal struct { 23 convID chat1.ConversationID 24 chanName string 25 messages []string 26 cbs []ChatChannelNewMessageCB 27 mtime time.Time 28 } 29 30 type convLocalByIDMap map[chat1.ConvIDStr]*convLocal 31 32 type convLocalByNameMap map[tlf.CanonicalName]convLocalByIDMap 33 34 type convLocalByTypeMap map[tlf.Type]convLocalByNameMap 35 36 type newConvCB func( 37 context.Context, *tlfhandle.Handle, chat1.ConversationID, string) 38 39 type chatLocalSharedData struct { 40 lock sync.RWMutex 41 newChannelCBs map[Config]newConvCB 42 convs convLocalByTypeMap 43 convsByID convLocalByIDMap 44 } 45 46 type selfConvInfo struct { 47 convID chat1.ConversationID 48 tlfName tlf.CanonicalName 49 tlfType tlf.Type 50 } 51 52 // chatLocal is a local implementation for chat. 53 type chatLocal struct { 54 config Config 55 log logger.Logger 56 deferLog logger.Logger 57 data *chatLocalSharedData 58 59 lock sync.Mutex 60 selfConvInfos []selfConvInfo 61 } 62 63 func newChatLocalWithData(config Config, data *chatLocalSharedData) *chatLocal { 64 log := config.MakeLogger("") 65 deferLog := log.CloneWithAddedDepth(1) 66 return &chatLocal{ 67 log: log, 68 deferLog: deferLog, 69 config: config, 70 data: data, 71 } 72 } 73 74 // newChatLocal constructs a new local chat implementation. 75 func newChatLocal(config Config) *chatLocal { 76 return newChatLocalWithData(config, &chatLocalSharedData{ 77 convs: make(convLocalByTypeMap), 78 convsByID: make(convLocalByIDMap), 79 newChannelCBs: map[Config]newConvCB{ 80 config: nil, 81 }, 82 }) 83 } 84 85 var _ Chat = (*chatLocal)(nil) 86 87 // GetConversationID implements the Chat interface. 88 func (c *chatLocal) GetConversationID( 89 ctx context.Context, tlfName tlf.CanonicalName, tlfType tlf.Type, 90 channelName string, chatType chat1.TopicType) ( 91 chat1.ConversationID, error) { 92 if chatType != chat1.TopicType_KBFSFILEEDIT { 93 panic(fmt.Sprintf("Bad topic type: %d", chatType)) 94 } 95 96 c.data.lock.Lock() 97 defer c.data.lock.Unlock() 98 byID, ok := c.data.convs[tlfType][tlfName] 99 if !ok { 100 if _, ok := c.data.convs[tlfType]; !ok { 101 c.data.convs[tlfType] = make(convLocalByNameMap) 102 } 103 if _, ok := c.data.convs[tlfType][tlfName]; !ok { 104 byID = make(convLocalByIDMap) 105 c.data.convs[tlfType][tlfName] = byID 106 } 107 } 108 for _, conv := range byID { 109 if conv.chanName == channelName { 110 return conv.convID, nil 111 } 112 } 113 114 // Make a new conversation. 115 var idBytes [8]byte 116 err := kbfscrypto.RandRead(idBytes[:]) 117 if err != nil { 118 return nil, err 119 } 120 id := chat1.ConversationID(idBytes[:]) 121 c.log.CDebugf(ctx, "Making new conversation for %s, %s: %s", 122 tlfName, channelName, id) 123 conv := &convLocal{ 124 convID: id, 125 chanName: channelName, 126 } 127 c.data.convs[tlfType][tlfName][id.ConvIDStr()] = conv 128 c.data.convsByID[id.ConvIDStr()] = conv 129 130 h, err := GetHandleFromFolderNameAndType( 131 ctx, c.config.KBPKI(), c.config.MDOps(), c.config, 132 string(tlfName), tlfType) 133 if err != nil { 134 return nil, err 135 } 136 for config, cb := range c.data.newChannelCBs { 137 // Only send notifications to those that can read the TLF. 138 session, err := config.KBPKI().GetCurrentSession(ctx) 139 if err != nil { 140 return nil, err 141 } 142 isReader, err := isReaderFromHandle( 143 ctx, h, config.KBPKI(), config, session.UID) 144 if err != nil { 145 return nil, err 146 } 147 if !isReader { 148 continue 149 } 150 151 if cb == nil && config.KBFSOps() != nil { 152 cb = config.KBFSOps().NewNotificationChannel 153 } 154 155 cb(ctx, h, id, channelName) 156 } 157 158 return id, nil 159 } 160 161 // SendTextMessage implements the Chat interface. 162 func (c *chatLocal) SendTextMessage( 163 ctx context.Context, tlfName tlf.CanonicalName, tlfType tlf.Type, 164 convID chat1.ConversationID, body string) error { 165 c.data.lock.Lock() 166 defer c.data.lock.Unlock() 167 conv, ok := c.data.convs[tlfType][tlfName][convID.ConvIDStr()] 168 if !ok { 169 return errors.Errorf("Conversation %s doesn't exist", convID.String()) 170 } 171 conv.messages = append(conv.messages, body) 172 conv.mtime = c.config.Clock().Now() 173 174 c.lock.Lock() 175 // For testing purposes just keep a running tab of all 176 // self-written conversations. Reconsider if we run into memory 177 // or performance issues. TODO: if we ever run an edit history 178 // test with multiple devices from the same user, we'll need to 179 // save this data in the shared info. 180 c.selfConvInfos = append( 181 c.selfConvInfos, selfConvInfo{convID, tlfName, tlfType}) 182 c.lock.Unlock() 183 184 // TODO: if there are some users who can read this folder but who 185 // haven't yet subscribed to the conversation, we should send them 186 // a new channel notification. 187 for _, cb := range conv.cbs { 188 cb(convID, body) 189 } 190 191 return nil 192 } 193 194 type chatHandleAndTime struct { 195 h *tlfhandle.Handle 196 mtime time.Time 197 } 198 199 type chatHandleAndTimeByMtime []chatHandleAndTime 200 201 func (chatbm chatHandleAndTimeByMtime) Len() int { 202 return len(chatbm) 203 } 204 205 func (chatbm chatHandleAndTimeByMtime) Less(i, j int) bool { 206 // Reverse sort so newest conversation is at index 0. 207 return chatbm[i].mtime.After(chatbm[j].mtime) 208 } 209 210 func (chatbm chatHandleAndTimeByMtime) Swap(i, j int) { 211 chatbm[i], chatbm[j] = chatbm[j], chatbm[i] 212 } 213 214 // GetGroupedInbox implements the Chat interface. 215 func (c *chatLocal) GetGroupedInbox( 216 ctx context.Context, chatType chat1.TopicType, maxChats int) ( 217 results []*tlfhandle.Handle, err error) { 218 if chatType != chat1.TopicType_KBFSFILEEDIT { 219 panic(fmt.Sprintf("Bad topic type: %d", chatType)) 220 } 221 222 session, err := c.config.KBPKI().GetCurrentSession(ctx) 223 if err != nil { 224 return nil, err 225 } 226 227 var handlesAndTimes chatHandleAndTimeByMtime 228 229 seen := make(map[string]bool) 230 c.data.lock.Lock() 231 defer c.data.lock.Unlock() 232 for t, byName := range c.data.convs { 233 for name, byID := range byName { 234 if t == tlf.Public && string(name) != string(session.Name) { 235 // Skip public TLFs that aren't our own. 236 continue 237 } 238 239 h, err := GetHandleFromFolderNameAndType( 240 ctx, c.config.KBPKI(), c.config.MDOps(), c.config, 241 string(name), t) 242 if err != nil { 243 return nil, err 244 } 245 246 // Only include if the current user can read the folder. 247 isReader, err := isReaderFromHandle( 248 ctx, h, c.config.KBPKI(), c.config, session.UID) 249 if err != nil { 250 return nil, err 251 } 252 if !isReader { 253 continue 254 } 255 256 hAndT := chatHandleAndTime{h: h} 257 for _, conv := range byID { 258 if conv.mtime.After(hAndT.mtime) { 259 hAndT.mtime = conv.mtime 260 } 261 } 262 handlesAndTimes = append(handlesAndTimes, hAndT) 263 seen[h.GetCanonicalPath()] = true 264 } 265 } 266 267 sort.Sort(handlesAndTimes) 268 for i := 0; i < len(handlesAndTimes) && i < maxChats; i++ { 269 results = append(results, handlesAndTimes[i].h) 270 } 271 272 c.lock.Lock() 273 defer c.lock.Unlock() 274 var selfHandles []*tlfhandle.Handle 275 max := numSelfTlfs 276 for i := len(c.selfConvInfos) - 1; i >= 0 && len(selfHandles) < max; i-- { 277 info := c.selfConvInfos[i] 278 h, err := GetHandleFromFolderNameAndType( 279 ctx, c.config.KBPKI(), c.config.MDOps(), c.config, 280 string(info.tlfName), info.tlfType) 281 if err != nil { 282 return nil, err 283 } 284 285 p := h.GetCanonicalPath() 286 if seen[p] { 287 continue 288 } 289 seen[p] = true 290 selfHandles = append(selfHandles, h) 291 } 292 293 numOver := len(results) + len(selfHandles) - maxChats 294 if numOver < 0 { 295 numOver = 0 296 } 297 results = append(results[:len(results)-numOver], selfHandles...) 298 return results, nil 299 } 300 301 // GetChannels implements the Chat interface. 302 func (c *chatLocal) GetChannels( 303 ctx context.Context, tlfName tlf.CanonicalName, tlfType tlf.Type, 304 chatType chat1.TopicType) ( 305 convIDs []chat1.ConversationID, channelNames []string, err error) { 306 if chatType != chat1.TopicType_KBFSFILEEDIT { 307 panic(fmt.Sprintf("Bad topic type: %d", chatType)) 308 } 309 310 c.data.lock.RLock() 311 defer c.data.lock.RUnlock() 312 byID := c.data.convs[tlfType][tlfName] 313 for _, conv := range byID { 314 convIDs = append(convIDs, conv.convID) 315 channelNames = append(channelNames, conv.chanName) 316 } 317 return convIDs, channelNames, nil 318 } 319 320 // ReadChannel implements the Chat interface. 321 func (c *chatLocal) ReadChannel( 322 ctx context.Context, convID chat1.ConversationID, startPage []byte) ( 323 messages []string, nextPage []byte, err error) { 324 c.data.lock.RLock() 325 defer c.data.lock.RUnlock() 326 conv, ok := c.data.convsByID[convID.ConvIDStr()] 327 if !ok { 328 return nil, nil, errors.Errorf( 329 "Conversation %s doesn't exist", convID.String()) 330 } 331 // For now, no paging, just return the complete list. 332 return conv.messages, nil, nil 333 } 334 335 // RegisterForMessages implements the Chat interface. 336 func (c *chatLocal) RegisterForMessages( 337 convID chat1.ConversationID, cb ChatChannelNewMessageCB) { 338 c.data.lock.Lock() 339 defer c.data.lock.Unlock() 340 conv, ok := c.data.convsByID[convID.ConvIDStr()] 341 if !ok { 342 panic(fmt.Sprintf("Conversation %s doesn't exist", convID.String())) 343 } 344 conv.cbs = append(conv.cbs, cb) 345 } 346 347 func (c *chatLocal) copy(config Config) *chatLocal { 348 copy := newChatLocalWithData(config, c.data) 349 c.data.lock.Lock() 350 defer c.data.lock.Unlock() 351 c.data.newChannelCBs[config] = config.KBFSOps().NewNotificationChannel 352 return copy 353 } 354 355 // ClearCache implements the Chat interface. 356 func (c *chatLocal) ClearCache() { 357 c.lock.Lock() 358 defer c.lock.Unlock() 359 c.selfConvInfos = nil 360 }