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  }