github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/helper.go (about)

     1  package chat
     2  
     3  import (
     4  	"encoding/hex"
     5  	"errors"
     6  	"fmt"
     7  	"math"
     8  	"sort"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/keybase/client/go/chat/globals"
    13  	"github.com/keybase/client/go/chat/storage"
    14  	"github.com/keybase/client/go/chat/types"
    15  	"github.com/keybase/client/go/chat/utils"
    16  	"github.com/keybase/client/go/libkb"
    17  	"github.com/keybase/client/go/protocol/chat1"
    18  	"github.com/keybase/client/go/protocol/gregor1"
    19  	"github.com/keybase/client/go/protocol/keybase1"
    20  	"github.com/keybase/client/go/teams"
    21  	"golang.org/x/net/context"
    22  )
    23  
    24  type Helper struct {
    25  	globals.Contextified
    26  	utils.DebugLabeler
    27  
    28  	ri func() chat1.RemoteInterface
    29  }
    30  
    31  var _ (libkb.ChatHelper) = (*Helper)(nil)
    32  
    33  func NewHelper(g *globals.Context, ri func() chat1.RemoteInterface) *Helper {
    34  	return &Helper{
    35  		Contextified: globals.NewContextified(g),
    36  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "Helper", false),
    37  		ri:           ri,
    38  	}
    39  }
    40  
    41  func (h *Helper) NewConversation(ctx context.Context, uid gregor1.UID, tlfName string,
    42  	topicName *string, topicType chat1.TopicType, membersType chat1.ConversationMembersType,
    43  	vis keybase1.TLFVisibility) (chat1.ConversationLocal, bool, error) {
    44  	return NewConversation(ctx, h.G(), uid, tlfName, topicName,
    45  		topicType, membersType, vis, nil, h.ri, NewConvFindExistingNormal)
    46  }
    47  
    48  func (h *Helper) NewConversationSkipFindExisting(ctx context.Context, uid gregor1.UID, tlfName string,
    49  	topicName *string, topicType chat1.TopicType, membersType chat1.ConversationMembersType,
    50  	vis keybase1.TLFVisibility) (chat1.ConversationLocal, bool, error) {
    51  	return NewConversation(ctx, h.G(), uid, tlfName, topicName,
    52  		topicType, membersType, vis, nil, h.ri, NewConvFindExistingSkip)
    53  }
    54  
    55  func (h *Helper) NewConversationWithMemberSourceConv(ctx context.Context, uid gregor1.UID, tlfName string,
    56  	topicName *string, topicType chat1.TopicType, membersType chat1.ConversationMembersType,
    57  	vis keybase1.TLFVisibility, retentionPolicy *chat1.RetentionPolicy,
    58  	memberSourceConv *chat1.ConversationID) (chat1.ConversationLocal, bool, error) {
    59  	return NewConversationWithMemberSourceConv(ctx, h.G(), uid, tlfName, topicName,
    60  		topicType, membersType, vis, nil, h.ri, NewConvFindExistingNormal, retentionPolicy, memberSourceConv)
    61  }
    62  
    63  func (h *Helper) SendTextByID(ctx context.Context, convID chat1.ConversationID,
    64  	tlfName string, text string, vis keybase1.TLFVisibility) error {
    65  	return h.SendMsgByID(ctx, convID, tlfName, chat1.NewMessageBodyWithText(chat1.MessageText{
    66  		Body: text,
    67  	}), chat1.MessageType_TEXT, vis)
    68  }
    69  
    70  func (h *Helper) SendMsgByID(ctx context.Context, convID chat1.ConversationID, tlfName string,
    71  	body chat1.MessageBody, msgType chat1.MessageType, vis keybase1.TLFVisibility) error {
    72  	boxer := NewBoxer(h.G())
    73  	sender := NewBlockingSender(h.G(), boxer, h.ri)
    74  	public := vis == keybase1.TLFVisibility_PUBLIC
    75  	msg := chat1.MessagePlaintext{
    76  		ClientHeader: chat1.MessageClientHeader{
    77  			TlfName:     tlfName,
    78  			TlfPublic:   public,
    79  			MessageType: msgType,
    80  		},
    81  		MessageBody: body,
    82  	}
    83  	_, _, err := sender.Send(ctx, convID, msg, 0, nil, nil, nil)
    84  	return err
    85  }
    86  
    87  func (h *Helper) SendTextByIDNonblock(ctx context.Context, convID chat1.ConversationID,
    88  	tlfName string, text string, outboxID *chat1.OutboxID, replyTo *chat1.MessageID) (chat1.OutboxID, error) {
    89  	return h.SendMsgByIDNonblock(ctx, convID, tlfName, chat1.NewMessageBodyWithText(chat1.MessageText{
    90  		Body: text,
    91  	}), chat1.MessageType_TEXT, outboxID, replyTo)
    92  }
    93  
    94  func (h *Helper) SendMsgByIDNonblock(ctx context.Context, convID chat1.ConversationID,
    95  	tlfName string, body chat1.MessageBody, msgType chat1.MessageType, inOutboxID *chat1.OutboxID,
    96  	replyTo *chat1.MessageID) (chat1.OutboxID, error) {
    97  	boxer := NewBoxer(h.G())
    98  	baseSender := NewBlockingSender(h.G(), boxer, h.ri)
    99  	sender := NewNonblockingSender(h.G(), baseSender)
   100  	msg := chat1.MessagePlaintext{
   101  		ClientHeader: chat1.MessageClientHeader{
   102  			TlfName:     tlfName,
   103  			MessageType: msgType,
   104  		},
   105  		MessageBody: body,
   106  	}
   107  	prepareOpts := chat1.SenderPrepareOptions{
   108  		ReplyTo: replyTo,
   109  	}
   110  	outboxID, _, err := sender.Send(ctx, convID, msg, 0, inOutboxID, nil, &prepareOpts)
   111  	return outboxID, err
   112  }
   113  
   114  func (h *Helper) DeleteMsg(ctx context.Context, convID chat1.ConversationID, tlfName string,
   115  	msgID chat1.MessageID) error {
   116  	boxer := NewBoxer(h.G())
   117  	sender := NewBlockingSender(h.G(), boxer, h.ri)
   118  	msg := chat1.MessagePlaintext{
   119  		ClientHeader: chat1.MessageClientHeader{
   120  			TlfName:     tlfName,
   121  			MessageType: chat1.MessageType_DELETE,
   122  			Supersedes:  msgID,
   123  		},
   124  	}
   125  	_, _, err := sender.Send(ctx, convID, msg, 0, nil, nil, nil)
   126  	return err
   127  }
   128  
   129  func (h *Helper) DeleteMsgNonblock(ctx context.Context, convID chat1.ConversationID, tlfName string,
   130  	msgID chat1.MessageID) error {
   131  	boxer := NewBoxer(h.G())
   132  	sender := NewNonblockingSender(h.G(), NewBlockingSender(h.G(), boxer, h.ri))
   133  	msg := chat1.MessagePlaintext{
   134  		ClientHeader: chat1.MessageClientHeader{
   135  			TlfName:     tlfName,
   136  			MessageType: chat1.MessageType_DELETE,
   137  			Supersedes:  msgID,
   138  		},
   139  	}
   140  	_, _, err := sender.Send(ctx, convID, msg, 0, nil, nil, nil)
   141  	return err
   142  }
   143  
   144  func (h *Helper) SendTextByName(ctx context.Context, name string, topicName *string,
   145  	membersType chat1.ConversationMembersType, ident keybase1.TLFIdentifyBehavior, text string) error {
   146  	boxer := NewBoxer(h.G())
   147  	sender := NewBlockingSender(h.G(), boxer, h.ri)
   148  	helper := newSendHelper(h.G(), name, topicName, membersType, ident, sender, h.ri)
   149  	_, _, err := helper.SendText(ctx, text, nil)
   150  	return err
   151  }
   152  
   153  func (h *Helper) SendMsgByName(ctx context.Context, name string, topicName *string,
   154  	membersType chat1.ConversationMembersType, ident keybase1.TLFIdentifyBehavior, body chat1.MessageBody,
   155  	msgType chat1.MessageType) error {
   156  	boxer := NewBoxer(h.G())
   157  	sender := NewBlockingSender(h.G(), boxer, h.ri)
   158  	helper := newSendHelper(h.G(), name, topicName, membersType, ident, sender, h.ri)
   159  	_, _, err := helper.SendBody(ctx, body, msgType, nil)
   160  	return err
   161  }
   162  
   163  func (h *Helper) SendTextByNameNonblock(ctx context.Context, name string, topicName *string,
   164  	membersType chat1.ConversationMembersType, ident keybase1.TLFIdentifyBehavior, text string,
   165  	inOutboxID *chat1.OutboxID) (chat1.OutboxID, error) {
   166  	boxer := NewBoxer(h.G())
   167  	baseSender := NewBlockingSender(h.G(), boxer, h.ri)
   168  	sender := NewNonblockingSender(h.G(), baseSender)
   169  	helper := newSendHelper(h.G(), name, topicName, membersType, ident, sender, h.ri)
   170  	outboxID, _, err := helper.SendText(ctx, text, inOutboxID)
   171  	return outboxID, err
   172  }
   173  
   174  func (h *Helper) SendMsgByNameNonblock(ctx context.Context, name string, topicName *string,
   175  	membersType chat1.ConversationMembersType, ident keybase1.TLFIdentifyBehavior, body chat1.MessageBody,
   176  	msgType chat1.MessageType, inOutboxID *chat1.OutboxID) (chat1.OutboxID, error) {
   177  	boxer := NewBoxer(h.G())
   178  	baseSender := NewBlockingSender(h.G(), boxer, h.ri)
   179  	sender := NewNonblockingSender(h.G(), baseSender)
   180  	helper := newSendHelper(h.G(), name, topicName, membersType, ident, sender, h.ri)
   181  	outboxID, _, err := helper.SendBody(ctx, body, msgType, inOutboxID)
   182  	return outboxID, err
   183  }
   184  
   185  func (h *Helper) FindConversations(ctx context.Context,
   186  	name string, topicName *string,
   187  	topicType chat1.TopicType, membersType chat1.ConversationMembersType, vis keybase1.TLFVisibility) ([]chat1.ConversationLocal, error) {
   188  	kuid, err := CurrentUID(h.G())
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  	uid := gregor1.UID(kuid.ToBytes())
   193  
   194  	oneChat := true
   195  	var tname string
   196  	if topicName != nil {
   197  		tname = utils.SanitizeTopicName(*topicName)
   198  	}
   199  	convs, err := FindConversations(ctx, h.G(), h.DebugLabeler, types.InboxSourceDataSourceAll, h.ri, uid,
   200  		name, topicType, membersType, vis, tname, &oneChat)
   201  	return convs, err
   202  }
   203  
   204  func (h *Helper) FindConversationsByID(ctx context.Context, convIDs []chat1.ConversationID) ([]chat1.ConversationLocal, error) {
   205  	kuid, err := CurrentUID(h.G())
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  	uid := gregor1.UID(kuid.ToBytes())
   210  	query := &chat1.GetInboxLocalQuery{
   211  		ConvIDs: convIDs,
   212  	}
   213  	inbox, _, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking,
   214  		types.InboxSourceDataSourceAll, nil, query)
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	return inbox.Convs, nil
   219  }
   220  
   221  // GetChannelTopicName gets the name of a team channel even if it's not in the inbox.
   222  func (h *Helper) GetChannelTopicName(ctx context.Context, teamID keybase1.TeamID,
   223  	topicType chat1.TopicType, convID chat1.ConversationID) (topicName string, err error) {
   224  	defer h.Trace(ctx, &err, "ChatHelper.GetChannelTopicName")()
   225  	h.Debug(ctx, "for teamID:%v convID:%v", teamID.String(), convID.String())
   226  	kuid, err := CurrentUID(h.G())
   227  	if err != nil {
   228  		return topicName, err
   229  	}
   230  	uid := gregor1.UID(kuid.ToBytes())
   231  	tlfID, err := chat1.TeamIDToTLFID(teamID)
   232  	if err != nil {
   233  		return topicName, err
   234  	}
   235  	query := &chat1.GetInboxLocalQuery{
   236  		ConvIDs: []chat1.ConversationID{convID},
   237  	}
   238  	inbox, _, err := h.G().InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking,
   239  		types.InboxSourceDataSourceAll, nil, query)
   240  	if err != nil {
   241  		return topicName, err
   242  	}
   243  	h.Debug(ctx, "found inbox convs: %v", len(inbox.Convs))
   244  	for _, conv := range inbox.Convs {
   245  		if conv.GetConvID().Eq(convID) && conv.GetMembersType() == chat1.ConversationMembersType_TEAM {
   246  			return conv.Info.TopicName, nil
   247  		}
   248  	}
   249  	// Fallback to TeamChannelSource
   250  	h.Debug(ctx, "using TeamChannelSource")
   251  	topicName, err = h.G().TeamChannelSource.GetChannelTopicName(ctx, uid, tlfID, topicType, convID)
   252  	return topicName, err
   253  }
   254  
   255  func (h *Helper) UpgradeKBFSToImpteam(ctx context.Context, tlfName string, tlfID chat1.TLFID, public bool) (err error) {
   256  	ctx = globals.ChatCtx(ctx, h.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, NewCachingIdentifyNotifier(h.G()))
   257  	defer h.Trace(ctx, &err, "ChatHelper.UpgradeKBFSToImpteam(%s,%s,%v)",
   258  		tlfID, tlfName, public)()
   259  	var cryptKeys []keybase1.CryptKey
   260  	nis := NewKBFSNameInfoSource(h.G())
   261  	keys, err := nis.AllCryptKeys(ctx, tlfName, public)
   262  	if err != nil {
   263  		return err
   264  	}
   265  	for _, key := range keys[chat1.ConversationMembersType_KBFS] {
   266  		cryptKeys = append(cryptKeys, keybase1.CryptKey{
   267  			KeyGeneration: key.Generation(),
   268  			Key:           key.Material(),
   269  		})
   270  	}
   271  	ni, err := nis.LookupID(ctx, tlfName, public)
   272  	if err != nil {
   273  		return err
   274  	}
   275  
   276  	tlfName = ni.CanonicalName
   277  	h.Debug(ctx, "UpgradeKBFSToImpteam: upgrading: TlfName: %s TLFID: %s public: %v keys: %d",
   278  		tlfName, tlfID, public, len(cryptKeys))
   279  	return teams.UpgradeTLFIDToImpteam(ctx, h.G().ExternalG(), tlfName, keybase1.TLFID(tlfID.String()),
   280  		public, keybase1.TeamApplication_CHAT, cryptKeys)
   281  }
   282  
   283  func (h *Helper) GetMessages(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   284  	msgIDs []chat1.MessageID, resolveSupersedes bool, reason *chat1.GetThreadReason) ([]chat1.MessageUnboxed, error) {
   285  	return h.G().ConvSource.GetMessages(ctx, convID, uid, msgIDs, reason, nil, resolveSupersedes)
   286  }
   287  
   288  func (h *Helper) GetMessage(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID,
   289  	msgID chat1.MessageID, resolveSupersedes bool, reason *chat1.GetThreadReason) (chat1.MessageUnboxed, error) {
   290  	return h.G().ConvSource.GetMessage(ctx, convID, uid, msgID, reason, nil, resolveSupersedes)
   291  }
   292  
   293  func (h *Helper) UserReacjis(ctx context.Context, uid gregor1.UID) keybase1.UserReacjis {
   294  	return storage.NewReacjiStore(h.G()).UserReacjis(ctx, uid)
   295  }
   296  
   297  func (h *Helper) JourneycardTimeTravel(ctx context.Context, uid gregor1.UID, duration time.Duration) (int, int, error) {
   298  	j, ok := h.G().JourneyCardManager.(*JourneyCardManager)
   299  	if !ok {
   300  		return 0, 0, fmt.Errorf("could not get JourneyCardManager")
   301  	}
   302  	return j.TimeTravel(ctx, uid, duration)
   303  }
   304  
   305  func (h *Helper) JourneycardResetAllConvs(ctx context.Context, uid gregor1.UID) error {
   306  	j, ok := h.G().JourneyCardManager.(*JourneyCardManager)
   307  	if !ok {
   308  		return fmt.Errorf("could not get JourneyCardManager")
   309  	}
   310  	return j.ResetAllConvs(ctx, uid)
   311  }
   312  
   313  func (h *Helper) JourneycardDebugState(ctx context.Context, uid gregor1.UID, teamID keybase1.TeamID) (string, error) {
   314  	j, ok := h.G().JourneyCardManager.(*JourneyCardManager)
   315  	if !ok {
   316  		return "", fmt.Errorf("could not get JourneyCardManager")
   317  	}
   318  	return j.DebugState(ctx, uid, teamID)
   319  }
   320  
   321  // InTeam gives a best effort to answer team membership based on the current state of the inbox cache
   322  func (h *Helper) InTeam(ctx context.Context, uid gregor1.UID, teamID keybase1.TeamID) (bool, error) {
   323  	tlfID := chat1.TLFID(teamID.ToBytes())
   324  	ibox, err := h.G().InboxSource.ReadUnverified(ctx, uid, types.InboxSourceDataSourceLocalOnly,
   325  		&chat1.GetInboxQuery{
   326  			TlfID:            &tlfID,
   327  			MemberStatus:     []chat1.ConversationMemberStatus{chat1.ConversationMemberStatus_ACTIVE},
   328  			AllowUnseenQuery: true,
   329  		})
   330  	if err != nil {
   331  		return false, err
   332  	}
   333  	return len(ibox.ConvsUnverified) > 0, nil
   334  }
   335  
   336  type sendHelper struct {
   337  	utils.DebugLabeler
   338  
   339  	name        string
   340  	membersType chat1.ConversationMembersType
   341  	ident       keybase1.TLFIdentifyBehavior
   342  	sender      types.Sender
   343  	ri          func() chat1.RemoteInterface
   344  
   345  	topicName *string
   346  	convID    chat1.ConversationID
   347  	triple    chat1.ConversationIDTriple
   348  
   349  	globals.Contextified
   350  }
   351  
   352  func newSendHelper(g *globals.Context, name string, topicName *string,
   353  	membersType chat1.ConversationMembersType, ident keybase1.TLFIdentifyBehavior, sender types.Sender,
   354  	ri func() chat1.RemoteInterface) *sendHelper {
   355  	return &sendHelper{
   356  		Contextified: globals.NewContextified(g),
   357  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "sendHelper", false),
   358  		name:         name,
   359  		topicName:    topicName,
   360  		membersType:  membersType,
   361  		ident:        ident,
   362  		sender:       sender,
   363  		ri:           ri,
   364  	}
   365  }
   366  
   367  func (s *sendHelper) SendText(ctx context.Context, text string, outboxID *chat1.OutboxID) (chat1.OutboxID, *chat1.MessageBoxed, error) {
   368  	body := chat1.NewMessageBodyWithText(chat1.MessageText{Body: text})
   369  	return s.SendBody(ctx, body, chat1.MessageType_TEXT, outboxID)
   370  }
   371  
   372  func (s *sendHelper) SendBody(ctx context.Context, body chat1.MessageBody, mtype chat1.MessageType,
   373  	outboxID *chat1.OutboxID) (chat1.OutboxID, *chat1.MessageBoxed, error) {
   374  	ctx = globals.ChatCtx(ctx, s.G(), s.ident, nil, NewCachingIdentifyNotifier(s.G()))
   375  	if err := s.conversation(ctx); err != nil {
   376  		return chat1.OutboxID{}, nil, err
   377  	}
   378  	return s.deliver(ctx, body, mtype, outboxID)
   379  }
   380  
   381  func (s *sendHelper) conversation(ctx context.Context) error {
   382  	kuid, err := CurrentUID(s.G())
   383  	if err != nil {
   384  		return err
   385  	}
   386  	uid := gregor1.UID(kuid.ToBytes())
   387  	conv, _, err := NewConversation(ctx, s.G(), uid, s.name, s.topicName,
   388  		chat1.TopicType_CHAT, s.membersType, keybase1.TLFVisibility_PRIVATE, nil, s.remoteInterface,
   389  		NewConvFindExistingNormal)
   390  	if err != nil {
   391  		return err
   392  	}
   393  	s.convID = conv.GetConvID()
   394  	s.triple = conv.Info.Triple
   395  	s.name = conv.Info.TlfName
   396  	return nil
   397  }
   398  
   399  func (s *sendHelper) deliver(ctx context.Context, body chat1.MessageBody, mtype chat1.MessageType,
   400  	outboxID *chat1.OutboxID) (chat1.OutboxID, *chat1.MessageBoxed, error) {
   401  	msg := chat1.MessagePlaintext{
   402  		ClientHeader: chat1.MessageClientHeader{
   403  			Conv:        s.triple,
   404  			TlfName:     s.name,
   405  			MessageType: mtype,
   406  		},
   407  		MessageBody: body,
   408  	}
   409  	return s.sender.Send(ctx, s.convID, msg, 0, outboxID, nil, nil)
   410  }
   411  
   412  func (s *sendHelper) remoteInterface() chat1.RemoteInterface {
   413  	return s.ri()
   414  }
   415  
   416  func CurrentUID(g *globals.Context) (keybase1.UID, error) {
   417  	uid := g.Env.GetUID()
   418  	if uid.IsNil() {
   419  		return "", libkb.LoginRequiredError{}
   420  	}
   421  	return uid, nil
   422  }
   423  
   424  type recentConversationParticipants struct {
   425  	globals.Contextified
   426  	utils.DebugLabeler
   427  }
   428  
   429  func newRecentConversationParticipants(g *globals.Context) *recentConversationParticipants {
   430  	return &recentConversationParticipants{
   431  		Contextified: globals.NewContextified(g),
   432  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "recentConversationParticipants", false),
   433  	}
   434  }
   435  
   436  func (r *recentConversationParticipants) getActiveScore(ctx context.Context, conv chat1.Conversation) float64 {
   437  	mtime := conv.GetMtime()
   438  	diff := time.Since(mtime.Time())
   439  	weeksAgo := diff.Seconds() / (time.Hour.Seconds() * 24 * 7)
   440  	val := 10.0 - math.Pow(1.6, weeksAgo)
   441  	if val < 1.0 {
   442  		val = 1.0
   443  	}
   444  	return val
   445  }
   446  
   447  func (r *recentConversationParticipants) get(ctx context.Context, myUID gregor1.UID) (res []gregor1.UID, err error) {
   448  	_, convs, err := storage.NewInbox(r.G()).ReadAll(ctx, myUID, true)
   449  	if err != nil {
   450  		if _, ok := err.(storage.MissError); ok {
   451  			r.Debug(ctx, "get: no inbox, returning blank results")
   452  			return nil, nil
   453  		}
   454  		return nil, err
   455  	}
   456  
   457  	r.Debug(ctx, "get: convs: %d", len(convs))
   458  	m := make(map[string]float64, len(convs))
   459  	for _, conv := range convs {
   460  		if conv.Conv.Metadata.Status == chat1.ConversationStatus_BLOCKED ||
   461  			conv.Conv.Metadata.Status == chat1.ConversationStatus_REPORTED {
   462  			continue
   463  		}
   464  		for _, uid := range conv.Conv.Metadata.ActiveList {
   465  			if uid.Eq(myUID) {
   466  				continue
   467  			}
   468  			m[uid.String()] += r.getActiveScore(ctx, conv.Conv)
   469  		}
   470  	}
   471  	for suid := range m {
   472  		uid, _ := hex.DecodeString(suid)
   473  		res = append(res, gregor1.UID(uid))
   474  	}
   475  
   476  	// Sort by the most appearances in the active lists
   477  	sort.Slice(res, func(i, j int) bool {
   478  		return m[res[i].String()] > m[res[j].String()]
   479  	})
   480  	return res, nil
   481  }
   482  
   483  func RecentConversationParticipants(ctx context.Context, g *globals.Context, myUID gregor1.UID) ([]gregor1.UID, error) {
   484  	ctx = globals.ChatCtx(ctx, g, keybase1.TLFIdentifyBehavior_CHAT_GUI, nil, NewCachingIdentifyNotifier(g))
   485  	return newRecentConversationParticipants(g).get(ctx, myUID)
   486  }
   487  
   488  func PresentConversationLocalWithFetchRetry(ctx context.Context, g *globals.Context,
   489  	uid gregor1.UID, conv chat1.ConversationLocal, partMode utils.PresentParticipantsMode) (pc *chat1.InboxUIItem) {
   490  	shouldPresent := true
   491  	if conv.Error != nil {
   492  		// If we get a transient failure, add this to the retrier queue
   493  		if conv.Error.Typ == chat1.ConversationErrorType_TRANSIENT {
   494  			g.FetchRetrier.Failure(ctx, uid,
   495  				NewConversationRetry(g, conv.GetConvID(), &conv.Info.Triple.Tlfid, InboxLoad))
   496  		} else {
   497  			// If this is a permanent error, then we don't send anything to the frontend yet.
   498  			shouldPresent = false
   499  		}
   500  	}
   501  	if shouldPresent {
   502  		pc = new(chat1.InboxUIItem)
   503  		*pc = utils.PresentConversationLocal(ctx, g, uid, conv, partMode)
   504  	}
   505  	return pc
   506  }
   507  
   508  func GetTopicNameState(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
   509  	convs []chat1.ConversationLocal,
   510  	uid gregor1.UID, tlfID chat1.TLFID, topicType chat1.TopicType,
   511  	membersType chat1.ConversationMembersType) (res chat1.TopicNameState, err error) {
   512  
   513  	var pairs chat1.ConversationIDMessageIDPairs
   514  	sort.Sort(utils.ConvLocalByConvID(convs))
   515  	for _, conv := range convs {
   516  		msg, err := conv.GetMaxMessage(chat1.MessageType_METADATA)
   517  		if err != nil {
   518  			debugger.Debug(ctx, "GetTopicNameState: unable to get maxmessage: convID: %v, %v", conv.GetConvID(), err)
   519  			continue
   520  		}
   521  		pairs.Pairs = append(pairs.Pairs, chat1.ConversationIDMessageIDPair{
   522  			ConvID: conv.GetConvID(),
   523  			MsgID:  msg.GetMessageID(),
   524  		})
   525  	}
   526  
   527  	if res, err = utils.CreateTopicNameState(pairs); err != nil {
   528  		debugger.Debug(ctx, "GetTopicNameState: failed to create topic name state: %v", err)
   529  		return res, err
   530  	}
   531  
   532  	return res, nil
   533  }
   534  
   535  func FindConversations(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
   536  	dataSource types.InboxSourceDataSourceTyp, ri func() chat1.RemoteInterface, uid gregor1.UID,
   537  	tlfName string, topicType chat1.TopicType,
   538  	membersTypeIn chat1.ConversationMembersType, vis keybase1.TLFVisibility, topicName string,
   539  	oneChatPerTLF *bool) (res []chat1.ConversationLocal, err error) {
   540  
   541  	findConvosWithMembersType := func(membersType chat1.ConversationMembersType) (res []chat1.ConversationLocal, err error) {
   542  		// Don't look for KBFS conversations anymore, they have mostly been converted, and it is better
   543  		// to just not search for them than to create a double conversation. Make an exception for
   544  		// public conversations.
   545  		if g.GetEnv().GetChatMemberType() != "kbfs" && membersType == chat1.ConversationMembersType_KBFS &&
   546  			vis == keybase1.TLFVisibility_PRIVATE {
   547  			return nil, nil
   548  		}
   549  		// Make sure team topic name makes sense
   550  		if topicName == "" && membersType == chat1.ConversationMembersType_TEAM {
   551  			topicName = globals.DefaultTeamTopic
   552  		}
   553  
   554  		// Attempt to resolve any sbs convs incase the team already exists.
   555  		var nameInfo *types.NameInfo
   556  		if strings.Contains(tlfName, "@") || strings.Contains(tlfName, ":") {
   557  			// Fetch the TLF ID from specified name
   558  			if info, err := CreateNameInfoSource(ctx, g, membersType).LookupID(ctx, tlfName, vis == keybase1.TLFVisibility_PUBLIC); err == nil {
   559  				nameInfo = &info
   560  				tlfName = nameInfo.CanonicalName
   561  			}
   562  		}
   563  
   564  		query := &chat1.GetInboxLocalQuery{
   565  			Name: &chat1.NameQuery{
   566  				Name:        tlfName,
   567  				MembersType: membersType,
   568  			},
   569  			TlfVisibility:     &vis,
   570  			TopicName:         &topicName,
   571  			TopicType:         &topicType,
   572  			OneChatTypePerTLF: oneChatPerTLF,
   573  		}
   574  
   575  		inbox, _, err := g.InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, dataSource, nil,
   576  			query)
   577  		if err != nil {
   578  			acceptableErr := false
   579  			// if we fail to load the team for some kind of rekey reason, treat as a complete miss
   580  			if _, ok := IsRekeyError(err); ok {
   581  				acceptableErr = true
   582  			}
   583  			// don't error out if the TLF name is just unknown, treat it as a complete miss
   584  			if _, ok := err.(UnknownTLFNameError); ok {
   585  				acceptableErr = true
   586  			}
   587  			if !acceptableErr {
   588  				return res, err
   589  			}
   590  			inbox.Convs = nil
   591  		}
   592  
   593  		// If we have inbox hits, return those
   594  		if len(inbox.Convs) > 0 {
   595  			debugger.Debug(ctx, "FindConversations: found conversations in inbox: tlfName: %s num: %d",
   596  				tlfName, len(inbox.Convs))
   597  			res = inbox.Convs
   598  		} else if membersType == chat1.ConversationMembersType_TEAM {
   599  			// If this is a team chat that we are looking for, then let's try searching all
   600  			// chats on the team to see if any match the arguments before giving up.
   601  			// No need to worry (yet) about conflicting with public code path, since there
   602  			// are not any public team chats.
   603  
   604  			// Fetch the TLF ID from specified name
   605  			if nameInfo == nil {
   606  				info, err := CreateNameInfoSource(ctx, g, membersType).LookupID(ctx, tlfName, false)
   607  				if err != nil {
   608  					debugger.Debug(ctx, "FindConversations: failed to get TLFID from name: %s", err.Error())
   609  					return res, err
   610  				}
   611  				nameInfo = &info
   612  			}
   613  			tlfConvs, err := g.TeamChannelSource.GetChannelsFull(ctx, uid, nameInfo.ID, topicType)
   614  			if err != nil {
   615  				debugger.Debug(ctx, "FindConversations: failed to list TLF conversations: %s", err.Error())
   616  				return res, err
   617  			}
   618  
   619  			for _, tlfConv := range tlfConvs {
   620  				if tlfConv.Info.TopicName == topicName {
   621  					res = append(res, tlfConv)
   622  				}
   623  			}
   624  			if len(res) > 0 {
   625  				debugger.Debug(ctx, "FindConversations: found team channels: num: %d", len(res))
   626  			}
   627  		} else if vis == keybase1.TLFVisibility_PUBLIC {
   628  			debugger.Debug(ctx, "FindConversations: no conversations found in inbox, trying public chats")
   629  
   630  			// Check for offline and return an error
   631  			if g.InboxSource.IsOffline(ctx) {
   632  				return res, OfflineError{}
   633  			}
   634  
   635  			// If we miss the inbox, and we are looking for a public TLF, let's try and find
   636  			// any conversation that matches
   637  			nameInfo, err := GetInboxQueryNameInfo(ctx, g, query)
   638  			if err != nil {
   639  				return res, err
   640  			}
   641  
   642  			// Call into gregor to try and find some public convs
   643  			pubConvs, err := ri().GetPublicConversations(ctx, chat1.GetPublicConversationsArg{
   644  				TlfID:            nameInfo.ID,
   645  				TopicType:        topicType,
   646  				SummarizeMaxMsgs: true,
   647  			})
   648  			if err != nil {
   649  				return res, err
   650  			}
   651  
   652  			// Localize the convs (if any)
   653  			if len(pubConvs.Conversations) > 0 {
   654  				convsLocal, _, err := g.InboxSource.Localize(ctx, uid,
   655  					utils.RemoteConvs(pubConvs.Conversations), types.ConversationLocalizerBlocking)
   656  				if err != nil {
   657  					return res, err
   658  				}
   659  
   660  				// Search for conversations that match the topic name
   661  				for _, convLocal := range convsLocal {
   662  					if convLocal.Error != nil {
   663  						debugger.Debug(ctx, "FindConversations: skipping convID: %s localization failure: %s",
   664  							convLocal.GetConvID(), convLocal.Error.Message)
   665  						continue
   666  					}
   667  					if convLocal.Info.TopicName == topicName &&
   668  						convLocal.Info.TLFNameExpanded() == nameInfo.CanonicalName {
   669  						debugger.Debug(ctx, "FindConversations: found matching public conv: id: %s topicName: %s",
   670  							convLocal.GetConvID(), topicName)
   671  						res = append(res, convLocal)
   672  					}
   673  				}
   674  			}
   675  
   676  		}
   677  		return res, nil
   678  	}
   679  
   680  	attempts := make(map[chat1.ConversationMembersType]bool)
   681  	mt := membersTypeIn
   682  L:
   683  	for {
   684  		var ierr error
   685  		attempts[mt] = true
   686  		res, ierr = findConvosWithMembersType(mt)
   687  		if ierr != nil || len(res) == 0 {
   688  			if ierr != nil {
   689  				debugger.Debug(ctx, "FindConversations: fail reason: %s mt: %v", ierr, mt)
   690  			} else {
   691  				debugger.Debug(ctx, "FindConversations: fail reason: no convs mt: %v", mt)
   692  			}
   693  			var newMT chat1.ConversationMembersType
   694  			switch mt {
   695  			case chat1.ConversationMembersType_TEAM:
   696  				err = ierr
   697  				debugger.Debug(ctx, "FindConversations: failed with team, aborting")
   698  				break L
   699  			case chat1.ConversationMembersType_IMPTEAMUPGRADE:
   700  				if !attempts[chat1.ConversationMembersType_IMPTEAMNATIVE] {
   701  					newMT = chat1.ConversationMembersType_IMPTEAMNATIVE
   702  					// Only set the error if the members type is the same as what was passed in
   703  					err = ierr
   704  				} else {
   705  					newMT = chat1.ConversationMembersType_KBFS
   706  				}
   707  			case chat1.ConversationMembersType_IMPTEAMNATIVE:
   708  				if !attempts[chat1.ConversationMembersType_IMPTEAMUPGRADE] {
   709  					newMT = chat1.ConversationMembersType_IMPTEAMUPGRADE
   710  					// Only set the error if the members type is the same as what was passed in
   711  					err = ierr
   712  				} else {
   713  					newMT = chat1.ConversationMembersType_KBFS
   714  				}
   715  			case chat1.ConversationMembersType_KBFS:
   716  				debugger.Debug(ctx, "FindConversations: failed with KBFS, aborting")
   717  				// We don't want to return random errors from KBFS if we are falling back to it,
   718  				// just return no conversations and call it a day
   719  				if membersTypeIn == chat1.ConversationMembersType_KBFS {
   720  					err = ierr
   721  				}
   722  				break L
   723  			}
   724  			debugger.Debug(ctx,
   725  				"FindConversations: failing to find anything for %v, trying again for %v", mt, newMT)
   726  			mt = newMT
   727  		} else {
   728  			debugger.Debug(ctx, "FindConversations: success with mt: %v", mt)
   729  			break L
   730  		}
   731  	}
   732  	return res, err
   733  }
   734  
   735  // Post a join or leave message. Must be called when the user is in the conv.
   736  // Uses a blocking sender.
   737  func postJoinLeave(ctx context.Context, g *globals.Context, ri func() chat1.RemoteInterface, uid gregor1.UID,
   738  	convID chat1.ConversationID, body chat1.MessageBody) (err error) {
   739  	typ, err := body.MessageType()
   740  	if err != nil {
   741  		return fmt.Errorf("message type for postJoinLeave: %v", err)
   742  	}
   743  	switch typ {
   744  	case chat1.MessageType_JOIN, chat1.MessageType_LEAVE:
   745  	// good
   746  	default:
   747  		return fmt.Errorf("invalid message type for postJoinLeave: %v", typ)
   748  	}
   749  
   750  	// Get the conversation from the inbox.
   751  	query := chat1.GetInboxLocalQuery{
   752  		ConvIDs: []chat1.ConversationID{convID},
   753  	}
   754  	ib, _, err := g.InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking,
   755  		types.InboxSourceDataSourceAll, nil, &query)
   756  	if err != nil {
   757  		return fmt.Errorf("inbox read error: %s", err)
   758  	}
   759  	if len(ib.Convs) != 1 {
   760  		return fmt.Errorf("post join/leave: found %d conversations", len(ib.Convs))
   761  	}
   762  
   763  	conv := ib.Convs[0]
   764  	if conv.GetTopicType() != chat1.TopicType_CHAT {
   765  		// only post these in chat convs
   766  		return nil
   767  	}
   768  	plaintext := chat1.MessagePlaintext{
   769  		ClientHeader: chat1.MessageClientHeader{
   770  			Conv:         conv.Info.Triple,
   771  			TlfName:      conv.Info.TlfName,
   772  			TlfPublic:    conv.Info.Visibility == keybase1.TLFVisibility_PUBLIC,
   773  			MessageType:  typ,
   774  			Supersedes:   chat1.MessageID(0),
   775  			Deletes:      nil,
   776  			Prev:         nil, // Filled by Sender
   777  			Sender:       nil, // Filled by Sender
   778  			SenderDevice: nil, // Filled by Sender
   779  			MerkleRoot:   nil, // Filled by Boxer
   780  			OutboxID:     nil,
   781  			OutboxInfo:   nil,
   782  		},
   783  		MessageBody: body,
   784  	}
   785  
   786  	// Send with a blocking sender
   787  	sender := NewBlockingSender(g, NewBoxer(g), ri)
   788  	_, _, err = sender.Send(ctx, convID, plaintext, 0, nil, nil, nil)
   789  	return err
   790  }
   791  
   792  func (h *Helper) JoinConversationByID(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (err error) {
   793  	defer h.Trace(ctx, &err, "ChatHelper.JoinConversationByID")()
   794  	return JoinConversation(ctx, h.G(), h.DebugLabeler, h.ri, uid, convID)
   795  }
   796  
   797  func JoinConversation(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
   798  	ri func() chat1.RemoteInterface, uid gregor1.UID, convID chat1.ConversationID) (err error) {
   799  	if err := g.ConvSource.AcquireConversationLock(ctx, uid, convID); err != nil {
   800  		return err
   801  	}
   802  	defer g.ConvSource.ReleaseConversationLock(ctx, uid, convID)
   803  
   804  	if alreadyIn, err := g.InboxSource.IsMember(ctx, uid, convID); err != nil {
   805  		// charge forward anyway
   806  		debugger.Debug(ctx, "JoinConversation: IsMember err: %v", err)
   807  	} else if alreadyIn {
   808  		return nil
   809  	}
   810  
   811  	if _, err = ri().JoinConversation(ctx, convID); err != nil {
   812  		debugger.Debug(ctx, "JoinConversation: failed to join conversation: %v", err)
   813  		return err
   814  	}
   815  
   816  	if _, err = g.InboxSource.MembershipUpdate(ctx, uid, 0, []chat1.ConversationMember{
   817  		{
   818  			Uid:    uid,
   819  			ConvID: convID,
   820  		},
   821  	}, nil, nil, nil, nil); err != nil {
   822  		debugger.Debug(ctx, "JoinConversation: failed to apply membership update: %v", err)
   823  	}
   824  	// Send a message to the channel after joining
   825  	joinMessageBody := chat1.NewMessageBodyWithJoin(chat1.MessageJoin{})
   826  	debugger.Debug(ctx, "JoinConversation: sending join message to: %s", convID)
   827  	if err := postJoinLeave(ctx, g, ri, uid, convID, joinMessageBody); err != nil {
   828  		debugger.Debug(ctx, "JoinConversation: posting join-conv message failed: %v", err)
   829  		// ignore the error
   830  	}
   831  	return nil
   832  }
   833  
   834  func (h *Helper) JoinConversationByName(ctx context.Context, uid gregor1.UID, tlfName, topicName string,
   835  	topicType chat1.TopicType, vis keybase1.TLFVisibility) (err error) {
   836  	defer h.Trace(ctx, &err, "ChatHelper.JoinConversationByName")()
   837  	return JoinConversationByName(ctx, h.G(), h.DebugLabeler, h.ri, uid, tlfName, topicName, topicType, vis)
   838  }
   839  
   840  func JoinConversationByName(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
   841  	ri func() chat1.RemoteInterface, uid gregor1.UID, tlfName, topicName string, topicType chat1.TopicType,
   842  	vis keybase1.TLFVisibility) (err error) {
   843  	// Fetch the TLF ID from specified name
   844  	nameInfo, err := CreateNameInfoSource(ctx, g, chat1.ConversationMembersType_TEAM).LookupID(ctx,
   845  		tlfName, vis == keybase1.TLFVisibility_PUBLIC)
   846  	if err != nil {
   847  		debugger.Debug(ctx, "JoinConversationByName: failed to get TLFID from name: %s", err.Error())
   848  		return err
   849  	}
   850  
   851  	// List all the conversations on the team
   852  	convs, err := g.TeamChannelSource.GetChannelsFull(ctx, uid, nameInfo.ID, topicType)
   853  	if err != nil {
   854  		return err
   855  	}
   856  	var convID chat1.ConversationID
   857  	for _, conv := range convs {
   858  		convTopicName := conv.Info.TopicName
   859  		if convTopicName != "" && convTopicName == topicName {
   860  			convID = conv.GetConvID()
   861  		}
   862  	}
   863  	if convID.IsNil() {
   864  		return fmt.Errorf("no topic name %s exists on specified team", topicName)
   865  	}
   866  	if err = JoinConversation(ctx, g, debugger, ri, uid, convID); err != nil {
   867  		return err
   868  	}
   869  	return nil
   870  }
   871  
   872  func (h *Helper) LeaveConversation(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID) (err error) {
   873  	defer h.Trace(ctx, &err, "ChatHelper.LeaveConversation")()
   874  	return LeaveConversation(ctx, h.G(), h.DebugLabeler, h.ri, uid, convID)
   875  }
   876  
   877  func LeaveConversation(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
   878  	ri func() chat1.RemoteInterface, uid gregor1.UID, convID chat1.ConversationID) (err error) {
   879  	alreadyIn, err := g.InboxSource.IsMember(ctx, uid, convID)
   880  	if err != nil {
   881  		debugger.Debug(ctx, "LeaveConversation: IsMember err: %s", err.Error())
   882  		// Pretend we're in.
   883  		alreadyIn = true
   884  	}
   885  
   886  	// Send a message to the channel to leave the conversation
   887  	if alreadyIn {
   888  		leaveMessageBody := chat1.NewMessageBodyWithLeave(chat1.MessageLeave{})
   889  		err := postJoinLeave(ctx, g, ri, uid, convID, leaveMessageBody)
   890  		if err != nil {
   891  			debugger.Debug(ctx, "LeaveConversation: posting leave-conv message failed: %v", err)
   892  			return err
   893  		}
   894  	} else {
   895  		_, err = ri().LeaveConversation(ctx, convID)
   896  		if err != nil {
   897  			debugger.Debug(ctx, "LeaveConversation: failed to leave conversation as a non-member: %s", err)
   898  			return err
   899  		}
   900  	}
   901  
   902  	return nil
   903  }
   904  
   905  func PreviewConversation(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
   906  	ri func() chat1.RemoteInterface, uid gregor1.UID, convID chat1.ConversationID) (res chat1.ConversationLocal, err error) {
   907  	alreadyIn, err := g.InboxSource.IsMember(ctx, uid, convID)
   908  	if err != nil {
   909  		debugger.Debug(ctx, "PreviewConversation: IsMember err: %s", err.Error())
   910  		// Assume we aren't in, server will reject us otherwise.
   911  		alreadyIn = false
   912  	}
   913  	if alreadyIn {
   914  		debugger.Debug(ctx, "PreviewConversation: already in the conversation, no need to preview")
   915  		return utils.GetVerifiedConv(ctx, g, uid, convID, types.InboxSourceDataSourceAll)
   916  	}
   917  
   918  	if _, err = ri().PreviewConversation(ctx, convID); err != nil {
   919  		debugger.Debug(ctx, "PreviewConversation: failed to preview conversation: %s", err.Error())
   920  		return res, err
   921  	}
   922  	return utils.GetVerifiedConv(ctx, g, uid, convID, types.InboxSourceDataSourceRemoteOnly)
   923  }
   924  
   925  func RemoveFromConversation(ctx context.Context, g *globals.Context, debugger utils.DebugLabeler,
   926  	ri func() chat1.RemoteInterface, convID chat1.ConversationID, usernames []string) (err error) {
   927  	users := make([]gregor1.UID, len(usernames))
   928  	for i, username := range usernames {
   929  		uid, err := g.GetUPAKLoader().LookupUID(ctx, libkb.NewNormalizedUsername(username))
   930  		if err != nil {
   931  			return fmt.Errorf("error resolving user %s: %s", username, err)
   932  		}
   933  		users[i] = uid.ToBytes()
   934  	}
   935  
   936  	_, err = ri().RemoveFromConversation(ctx, chat1.RemoveFromConversationArg{
   937  		ConvID: convID,
   938  		Users:  users,
   939  	})
   940  	return err
   941  }
   942  
   943  type NewConvFindExistingMode int
   944  
   945  const (
   946  	NewConvFindExistingNormal NewConvFindExistingMode = iota
   947  	NewConvFindExistingSkip
   948  )
   949  
   950  func NewConversation(ctx context.Context, g *globals.Context, uid gregor1.UID, tlfName string,
   951  	topicName *string, topicType chat1.TopicType, membersType chat1.ConversationMembersType,
   952  	vis keybase1.TLFVisibility, knownTopicID *chat1.TopicID, ri func() chat1.RemoteInterface,
   953  	findExistingMode NewConvFindExistingMode) (chat1.ConversationLocal, bool, error) {
   954  	return NewConversationWithMemberSourceConv(ctx, g, uid, tlfName, topicName, topicType, membersType, vis,
   955  		knownTopicID, ri, findExistingMode, nil, nil)
   956  }
   957  
   958  func NewConversationWithMemberSourceConv(ctx context.Context, g *globals.Context, uid gregor1.UID,
   959  	tlfName string, topicName *string, topicType chat1.TopicType, membersType chat1.ConversationMembersType,
   960  	vis keybase1.TLFVisibility, knownTopicID *chat1.TopicID, ri func() chat1.RemoteInterface,
   961  	findExistingMode NewConvFindExistingMode, retentionPolicy *chat1.RetentionPolicy,
   962  	memberSourceConv *chat1.ConversationID) (chat1.ConversationLocal, bool, error) {
   963  	defer utils.SuspendComponent(ctx, g, g.ConvLoader)()
   964  	helper := newNewConversationHelper(g, uid, tlfName, topicName, topicType, membersType, vis,
   965  		ri, findExistingMode, retentionPolicy, memberSourceConv, knownTopicID)
   966  	return helper.create(ctx)
   967  }
   968  
   969  type newConversationHelper struct {
   970  	globals.Contextified
   971  	utils.DebugLabeler
   972  
   973  	uid              gregor1.UID
   974  	tlfName          string
   975  	topicName        *string
   976  	topicType        chat1.TopicType
   977  	topicID          *chat1.TopicID
   978  	membersType      chat1.ConversationMembersType
   979  	memberSourceConv *chat1.ConversationID
   980  	vis              keybase1.TLFVisibility
   981  	ri               func() chat1.RemoteInterface
   982  	findExistingMode NewConvFindExistingMode
   983  	retentionPolicy  *chat1.RetentionPolicy
   984  }
   985  
   986  func newNewConversationHelper(g *globals.Context, uid gregor1.UID, tlfName string, topicName *string,
   987  	topicType chat1.TopicType, membersType chat1.ConversationMembersType, vis keybase1.TLFVisibility,
   988  	ri func() chat1.RemoteInterface, findExistingMode NewConvFindExistingMode,
   989  	retentionPolicy *chat1.RetentionPolicy, memberSourceConv *chat1.ConversationID,
   990  	knownTopicID *chat1.TopicID) *newConversationHelper {
   991  	return &newConversationHelper{
   992  		Contextified:     globals.NewContextified(g),
   993  		DebugLabeler:     utils.NewDebugLabeler(g.ExternalG(), "newConversationHelper", false),
   994  		uid:              uid,
   995  		tlfName:          utils.AddUserToTLFName(g, tlfName, vis, membersType),
   996  		topicName:        topicName,
   997  		topicType:        topicType,
   998  		membersType:      membersType,
   999  		memberSourceConv: memberSourceConv,
  1000  		vis:              vis,
  1001  		ri:               ri,
  1002  		findExistingMode: findExistingMode,
  1003  		retentionPolicy:  retentionPolicy,
  1004  		topicID:          knownTopicID,
  1005  	}
  1006  }
  1007  
  1008  func (n *newConversationHelper) findExisting(ctx context.Context, tlfID chat1.TLFID, topicName string,
  1009  	dataSource types.InboxSourceDataSourceTyp) (res []chat1.ConversationLocal, err error) {
  1010  	switch n.findExistingMode {
  1011  	case NewConvFindExistingNormal:
  1012  		ib, _, err := n.G().InboxSource.Read(ctx, n.uid, types.ConversationLocalizerBlocking,
  1013  			dataSource, nil, &chat1.GetInboxLocalQuery{
  1014  				Name: &chat1.NameQuery{
  1015  					Name:        n.tlfName,
  1016  					TlfID:       &tlfID,
  1017  					MembersType: n.membersType,
  1018  				},
  1019  				MemberStatus:  chat1.AllConversationMemberStatuses(),
  1020  				TlfVisibility: &n.vis,
  1021  				TopicName:     &topicName,
  1022  				TopicType:     &n.topicType,
  1023  			})
  1024  		if err != nil {
  1025  			return res, err
  1026  		}
  1027  		return ib.Convs, nil
  1028  	case NewConvFindExistingSkip:
  1029  		return nil, nil
  1030  	}
  1031  	return nil, nil
  1032  }
  1033  
  1034  func (n *newConversationHelper) getNameInfo(ctx context.Context) (res types.NameInfo, err error) {
  1035  	isPublic := n.vis == keybase1.TLFVisibility_PUBLIC
  1036  	switch n.membersType {
  1037  	case chat1.ConversationMembersType_KBFS, chat1.ConversationMembersType_TEAM,
  1038  		chat1.ConversationMembersType_IMPTEAMUPGRADE:
  1039  		return CreateNameInfoSource(ctx, n.G(), n.membersType).LookupID(ctx, n.tlfName, isPublic)
  1040  	case chat1.ConversationMembersType_IMPTEAMNATIVE:
  1041  		// NameInfoSource interface doesn't allow us to quickly lookup and create at the same time,
  1042  		// so let's just do this manually here. Note: this will allow a user to dup impteamupgrade
  1043  		// convs with unresolved assertions in them, the server can catch any normal convs being duped.
  1044  		if override, _ := globals.CtxOverrideNameInfoSource(ctx); override != nil {
  1045  			return override.LookupID(ctx, n.tlfName, isPublic)
  1046  		}
  1047  		team, _, impTeamName, err := teams.LookupOrCreateImplicitTeam(ctx, n.G().ExternalG(), n.tlfName,
  1048  			isPublic)
  1049  		if err != nil {
  1050  			return res, err
  1051  		}
  1052  		return types.NameInfo{
  1053  			ID:            chat1.TLFID(team.ID.ToBytes()),
  1054  			CanonicalName: impTeamName.String(),
  1055  		}, nil
  1056  	}
  1057  	return res, errors.New("unknown members type")
  1058  }
  1059  
  1060  func (n *newConversationHelper) findExistingViaInboxSearch(ctx context.Context, searchTopicName string) *chat1.ConversationLocal {
  1061  	query := utils.StripUsernameFromConvName(n.tlfName, n.G().GetEnv().GetUsername().String())
  1062  	n.Debug(ctx, "findExistingViaInboxSearch: looking for: %s", query)
  1063  	convs, err := n.G().InboxSource.Search(ctx, n.uid, query, 0, types.InboxSourceSearchEmptyModeAll)
  1064  	if err != nil {
  1065  		n.Debug(ctx, "findExistingViaInboxSearch: failed to perform inbox search: %s", err)
  1066  		return nil
  1067  	}
  1068  	if len(convs) == 0 {
  1069  		n.Debug(ctx, "findExistingViaInboxSearch: no convs found from search")
  1070  		return nil
  1071  	}
  1072  
  1073  	convsLocal, _, err := n.G().InboxSource.Localize(ctx, n.uid, convs, types.ConversationLocalizerBlocking)
  1074  	if err != nil {
  1075  		n.Debug(ctx, "findExistingViaInboxSearch: failed to localize: %s", err)
  1076  		return nil
  1077  	}
  1078  	searchIsPublic := n.vis == keybase1.TLFVisibility_PUBLIC
  1079  	for _, conv := range convsLocal {
  1080  		convName := conv.Info.TlfName
  1081  		if conv.Error != nil {
  1082  			convName = conv.Error.UnverifiedTLFName
  1083  		}
  1084  		convName = utils.StripUsernameFromConvName(convName, n.G().GetEnv().GetUsername().String())
  1085  		n.Debug(ctx, "findExistingViaInboxSearch: candidate: %s", convName)
  1086  		if convName == query && conv.GetTopicType() == n.topicType &&
  1087  			conv.GetTopicName() == searchTopicName && conv.GetMembersType() == n.membersType &&
  1088  			conv.IsPublic() == searchIsPublic {
  1089  			n.Debug(ctx, "findExistingViaInboxSearch: found conv match: %s id: %s", conv.Info.TlfName,
  1090  				conv.GetConvID())
  1091  			return &conv
  1092  		}
  1093  	}
  1094  	n.Debug(ctx, "findExistingViaInboxSearch: no convs found with exact match")
  1095  	return nil
  1096  }
  1097  
  1098  func (n *newConversationHelper) create(ctx context.Context) (res chat1.ConversationLocal, created bool, reserr error) {
  1099  	defer n.Trace(ctx, &reserr, "newConversationHelper")()
  1100  	// Handle a nil topic name with default values for the members type specified
  1101  	if n.topicName == nil {
  1102  		// We never want a blank topic name in team chats, always default to the default team name
  1103  		switch n.membersType {
  1104  		case chat1.ConversationMembersType_TEAM:
  1105  			n.topicName = &globals.DefaultTeamTopic
  1106  		default:
  1107  			// Nothing to do for other member types.
  1108  		}
  1109  	}
  1110  
  1111  	var findConvsTopicName string
  1112  	if n.topicName != nil {
  1113  		findConvsTopicName = utils.SanitizeTopicName(*n.topicName)
  1114  	}
  1115  	info, err := n.getNameInfo(ctx)
  1116  	if err != nil {
  1117  		// If we failed this, just do a quick inbox search to see if we can find one with the same name.
  1118  		// This can happen if a user tries to create a conversation with the same person as a conversation
  1119  		// in which they are currently locked out due to reset.
  1120  		if conv := n.findExistingViaInboxSearch(ctx, findConvsTopicName); conv != nil {
  1121  			return *conv, false, nil
  1122  		}
  1123  		return res, false, err
  1124  	}
  1125  	n.tlfName = info.CanonicalName
  1126  
  1127  	// Find any existing conversations that match this argument specifically. We need to do this check
  1128  	// here in the client since we can't see the topic name on the server.
  1129  
  1130  	// NOTE: The CLI already does this. It is hard to move that code completely into the service, since
  1131  	// there is a ton of logic in there to try and present a nice looking menu to help out the
  1132  	// user and such. For the most part, the CLI just uses FindConversationsLocal though, so it
  1133  	// should hopefully just result in a bunch of cache hits on the second invocation.
  1134  	convs, err := n.findExisting(ctx, info.ID, findConvsTopicName, types.InboxSourceDataSourceAll)
  1135  	if err != nil {
  1136  		n.Debug(ctx, "error running findExisting: %s", err)
  1137  		convs = nil
  1138  	}
  1139  	// If we find one conversation, then just return it as if we created it.
  1140  	if len(convs) == 1 {
  1141  		// if we have a known topic ID, make sure we hit it
  1142  		if n.topicID == nil || n.topicID.Eq(convs[0].Info.Triple.TopicID) {
  1143  			n.Debug(ctx, "found previous conversation that matches, returning")
  1144  			return convs[0], false, nil
  1145  		}
  1146  	}
  1147  
  1148  	if n.G().ExternalG().Env.GetChatMemberType() == "impteam" {
  1149  		// if KBFS, return an error. Need to use IMPTEAM now.
  1150  		if n.membersType == chat1.ConversationMembersType_KBFS {
  1151  			// let it slide in devel for tests
  1152  			if n.G().ExternalG().Env.GetRunMode() != libkb.DevelRunMode {
  1153  				n.Debug(ctx, "KBFS conversations deprecated; switching membersType from KBFS to IMPTEAM")
  1154  				n.membersType = chat1.ConversationMembersType_IMPTEAMNATIVE
  1155  			}
  1156  		}
  1157  	}
  1158  
  1159  	n.Debug(ctx, "no matching previous conversation, proceeding to create new conv")
  1160  	triple := chat1.ConversationIDTriple{
  1161  		Tlfid:     info.ID,
  1162  		TopicType: n.topicType,
  1163  		TopicID:   make(chat1.TopicID, 16),
  1164  	}
  1165  
  1166  	// If we get a ChatStalePreviousStateError we blow away in the box cache
  1167  	// once to allow the retry to get fresh data.
  1168  	clearedCache := false
  1169  	isPublic := n.vis == keybase1.TLFVisibility_PUBLIC
  1170  	for i := 0; i < 5; i++ {
  1171  		if n.topicID != nil {
  1172  			triple.TopicID = *n.topicID
  1173  		} else {
  1174  			triple.TopicID, err = utils.NewChatTopicID()
  1175  			if err != nil {
  1176  				return res, false, fmt.Errorf("error creating topic ID: %s", err)
  1177  			}
  1178  		}
  1179  		n.Debug(ctx, "attempt: %v [tlfID: %s topicType: %d topicID: %s name: %s public: %v mt: %v]",
  1180  			i, triple.Tlfid, triple.TopicType, triple.TopicID, info.CanonicalName, isPublic,
  1181  			n.membersType)
  1182  		firstMessageBoxed, topicNameState, err := n.makeFirstMessage(ctx, triple, info.CanonicalName,
  1183  			n.membersType, n.vis, n.topicName)
  1184  		switch err := err.(type) {
  1185  		case nil:
  1186  		case DuplicateTopicNameError:
  1187  			return err.Conv, false, nil
  1188  		default:
  1189  			return res, false, err
  1190  		}
  1191  
  1192  		var ncrres chat1.NewConversationRemoteRes
  1193  		ncrres, reserr = n.ri().NewConversationRemote2(ctx, chat1.NewConversationRemote2Arg{
  1194  			IdTriple:         triple,
  1195  			TLFMessage:       *firstMessageBoxed,
  1196  			MembersType:      n.membersType,
  1197  			TopicNameState:   topicNameState,
  1198  			MemberSourceConv: n.memberSourceConv,
  1199  			RetentionPolicy:  n.retentionPolicy,
  1200  		})
  1201  		convID := ncrres.ConvID
  1202  		if reserr != nil {
  1203  			switch cerr := reserr.(type) {
  1204  			case libkb.ChatStalePreviousStateError:
  1205  				n.Debug(ctx, "stale topic name state, trying again")
  1206  				if !clearedCache {
  1207  					n.Debug(ctx, "Send: clearing inbox cache to retry stale previous state")
  1208  					err := n.G().InboxSource.Clear(ctx, n.uid, &types.ClearOpts{
  1209  						SendLocalAdminNotification: true,
  1210  						Reason:                     "received ChatStalePreviousStateError",
  1211  					})
  1212  					if err != nil {
  1213  						n.Debug(ctx, "Send: error clearing inbox: %+v", err)
  1214  					}
  1215  					clearedCache = true
  1216  				}
  1217  				continue
  1218  			case libkb.ChatConvExistsError:
  1219  				// This triple already exists.
  1220  				n.Debug(ctx, "conv exists: %v", cerr.ConvID)
  1221  				if n.topicID != nil {
  1222  					// if the topicID is hardcoded, just fail right away
  1223  					return res, false, reserr
  1224  				}
  1225  				if triple.TopicType != chat1.TopicType_CHAT ||
  1226  					n.membersType == chat1.ConversationMembersType_TEAM {
  1227  					// THIS CHECK IS FOR WHEN THE SERVER RETURNS THIS ERROR WHEN PREVENTING
  1228  					// MULTIPLE CHANNELS ON NON-TEAM CHATS. IT TRIES TO REDIRECT YOU TO THE CONV
  1229  					// THAT IS ALREADY THERE.
  1230  					//
  1231  					// Not a chat (or is a team) conversation. Multiples are fine. Just retry with a
  1232  					// different topic ID.
  1233  					continue
  1234  				}
  1235  				// A chat conversation already exists; just reuse it. See above comment.
  1236  				// Note that from this point on, TopicID is entirely the wrong value.
  1237  				convID = cerr.ConvID
  1238  			case libkb.ChatCollisionError:
  1239  				// The triple did not exist, but a collision occurred on convID. Retry with a different topic ID.
  1240  				n.Debug(ctx, "collision: %v", reserr)
  1241  				if n.topicID != nil {
  1242  					// if the topicID is hardcoded, just fail right away
  1243  					return res, false, reserr
  1244  				}
  1245  				continue
  1246  			case libkb.ChatClientError:
  1247  				// just make sure we can't find anything with FindConversations if we get this back
  1248  				topicName := ""
  1249  				if n.topicName != nil {
  1250  					topicName = *n.topicName
  1251  				}
  1252  				fcRes, err := FindConversations(ctx, n.G(), n.DebugLabeler, types.InboxSourceDataSourceAll,
  1253  					n.ri, n.uid, n.tlfName, n.topicType, n.membersType, n.vis, topicName, nil)
  1254  				if err != nil {
  1255  					n.Debug(ctx, "failed trying FindConversations after client error: %s", err)
  1256  					return res, false, reserr
  1257  				} else if len(fcRes) > 0 {
  1258  					convID = fcRes[0].GetConvID()
  1259  				} else {
  1260  					return res, false, reserr
  1261  				}
  1262  			case libkb.ChatNotInTeamError:
  1263  				if n.membersType == chat1.ConversationMembersType_TEAM {
  1264  					teamID, tmpErr := TLFIDToTeamID(triple.Tlfid)
  1265  					if tmpErr == nil && teamID.IsSubTeam() {
  1266  						n.Debug(ctx, "For tlf ID %s, inferring NotExplicitMemberOfSubteamError, from error: %s", triple.Tlfid, reserr.Error())
  1267  						return res, false, teams.NewNotExplicitMemberOfSubteamError()
  1268  					}
  1269  				}
  1270  				return res, false, fmt.Errorf("error creating conversation: %s", reserr)
  1271  			default:
  1272  				return res, false, fmt.Errorf("error creating conversation: %s", reserr)
  1273  			}
  1274  		}
  1275  
  1276  		n.Debug(ctx, "established conv: %v", convID)
  1277  
  1278  		// create succeeded; grabbing the conversation and returning
  1279  		ib, _, err := n.G().InboxSource.Read(ctx, n.uid, types.ConversationLocalizerBlocking,
  1280  			types.InboxSourceDataSourceRemoteOnly, nil,
  1281  			&chat1.GetInboxLocalQuery{
  1282  				ConvIDs: []chat1.ConversationID{convID},
  1283  			})
  1284  		if err != nil {
  1285  			return res, false, err
  1286  		}
  1287  
  1288  		if len(ib.Convs) != 1 {
  1289  			return res, false,
  1290  				fmt.Errorf("newly created conversation fetch error: found %d conversations", len(ib.Convs))
  1291  		}
  1292  		res = ib.Convs[0]
  1293  		n.Debug(ctx, "fetched conv: %v mt: %v public: %v", res.GetConvID(), res.GetMembersType(),
  1294  			res.IsPublic())
  1295  
  1296  		// Update inbox cache
  1297  		updateConv := ib.ConvsUnverified[0]
  1298  		if err = n.G().InboxSource.NewConversation(ctx, n.uid, 0, updateConv.Conv); err != nil {
  1299  			return res, false, err
  1300  		}
  1301  
  1302  		if res.Error != nil {
  1303  			return res, false, errors.New(res.Error.Message)
  1304  		}
  1305  
  1306  		// Send a message to the channel after joining.
  1307  		switch n.membersType {
  1308  		case chat1.ConversationMembersType_TEAM:
  1309  			// don't send join messages to #general
  1310  			if findConvsTopicName != globals.DefaultTeamTopic {
  1311  				joinMessageBody := chat1.NewMessageBodyWithJoin(chat1.MessageJoin{})
  1312  				if err := postJoinLeave(ctx, n.G(), n.ri, n.uid, convID, joinMessageBody); err != nil {
  1313  					n.Debug(ctx, "posting join-conv message failed: %v", err)
  1314  					// ignore the error
  1315  				}
  1316  			}
  1317  		default:
  1318  			// pass
  1319  		}
  1320  
  1321  		// If we created a complex team in the process of creating this conversation, send a special
  1322  		// message into the general channel letting everyone know about the change.
  1323  		if ncrres.CreatedComplexTeam {
  1324  			subBody := chat1.NewMessageSystemWithComplexteam(chat1.MessageSystemComplexTeam{
  1325  				Team: n.tlfName,
  1326  			})
  1327  			body := chat1.NewMessageBodyWithSystem(subBody)
  1328  			if _, err := n.G().ChatHelper.SendMsgByNameNonblock(ctx, n.tlfName, &globals.DefaultTeamTopic,
  1329  				chat1.ConversationMembersType_TEAM, keybase1.TLFIdentifyBehavior_CHAT_GUI,
  1330  				body, chat1.MessageType_SYSTEM, nil); err != nil {
  1331  				n.Debug(ctx, "failed to send complex team intro message: %s", err)
  1332  			}
  1333  		}
  1334  		return res, true, nil
  1335  	}
  1336  	return res, false, reserr
  1337  }
  1338  
  1339  func (n *newConversationHelper) makeFirstMessage(ctx context.Context, triple chat1.ConversationIDTriple,
  1340  	tlfName string, membersType chat1.ConversationMembersType, tlfVisibility keybase1.TLFVisibility,
  1341  	topicName *string) (*chat1.MessageBoxed, *chat1.TopicNameState, error) {
  1342  	var msg chat1.MessagePlaintext
  1343  	if topicName != nil {
  1344  		msg = chat1.MessagePlaintext{
  1345  			ClientHeader: chat1.MessageClientHeader{
  1346  				Conv:        triple,
  1347  				TlfName:     tlfName,
  1348  				TlfPublic:   tlfVisibility == keybase1.TLFVisibility_PUBLIC,
  1349  				MessageType: chat1.MessageType_METADATA,
  1350  				Prev:        nil, // TODO
  1351  				// Sender and SenderDevice filled by prepareMessageForRemote
  1352  			},
  1353  			MessageBody: chat1.NewMessageBodyWithMetadata(
  1354  				chat1.MessageConversationMetadata{
  1355  					ConversationTitle: *topicName,
  1356  				}),
  1357  		}
  1358  	} else {
  1359  		if membersType == chat1.ConversationMembersType_TEAM {
  1360  			return nil, nil, errors.New("team conversations require a topic name")
  1361  		}
  1362  		msg = chat1.MessagePlaintext{
  1363  			ClientHeader: chat1.MessageClientHeader{
  1364  				Conv:        triple,
  1365  				TlfName:     tlfName,
  1366  				TlfPublic:   tlfVisibility == keybase1.TLFVisibility_PUBLIC,
  1367  				MessageType: chat1.MessageType_TLFNAME,
  1368  				Prev:        nil, // TODO
  1369  				// Sender and SenderDevice filled by prepareMessageForRemote
  1370  			},
  1371  		}
  1372  	}
  1373  	opts := chat1.SenderPrepareOptions{
  1374  		SkipTopicNameState: n.findExistingMode == NewConvFindExistingSkip,
  1375  	}
  1376  	sender := NewBlockingSender(n.G(), NewBoxer(n.G()), n.ri)
  1377  	prepareRes, err := sender.Prepare(ctx, msg, membersType, nil, &opts)
  1378  	return &prepareRes.Boxed, prepareRes.TopicNameState, err
  1379  }
  1380  
  1381  func CreateNameInfoSource(ctx context.Context, g *globals.Context, membersType chat1.ConversationMembersType) types.NameInfoSource {
  1382  	if override, _ := globals.CtxOverrideNameInfoSource(ctx); override != nil {
  1383  		return override
  1384  	}
  1385  	switch membersType {
  1386  	case chat1.ConversationMembersType_KBFS:
  1387  		return NewKBFSNameInfoSource(g)
  1388  	case chat1.ConversationMembersType_TEAM:
  1389  		return NewTeamsNameInfoSource(g)
  1390  	case chat1.ConversationMembersType_IMPTEAMNATIVE:
  1391  		return NewImplicitTeamsNameInfoSource(g, membersType)
  1392  	case chat1.ConversationMembersType_IMPTEAMUPGRADE:
  1393  		return NewImplicitTeamsNameInfoSource(g, membersType)
  1394  	}
  1395  	g.GetLog().CDebugf(ctx, "createNameInfoSource: unknown members type, using KBFS: %v", membersType)
  1396  	return NewKBFSNameInfoSource(g)
  1397  }
  1398  
  1399  func (h *Helper) BulkAddToConv(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, usernames []string) error {
  1400  	if len(usernames) == 0 {
  1401  		return fmt.Errorf("Unable to BulkAddToConv, no users specified")
  1402  	}
  1403  
  1404  	rc, err := utils.GetUnverifiedConv(ctx, h.G(), uid, convID, types.InboxSourceDataSourceAll)
  1405  	if err != nil {
  1406  		return err
  1407  	}
  1408  	conv := rc.Conv
  1409  	mt := conv.Metadata.MembersType
  1410  	switch mt {
  1411  	case chat1.ConversationMembersType_TEAM:
  1412  	default:
  1413  		return fmt.Errorf("BulkAddToConv only available to TEAM conversations. Found %v conv", mt)
  1414  	}
  1415  
  1416  	boxer := NewBoxer(h.G())
  1417  	sender := NewBlockingSender(h.G(), boxer, h.ri)
  1418  	sendBulkAddToConv := func(ctx context.Context, sender *BlockingSender, usernames []string, convID chat1.ConversationID, info types.NameInfo) error {
  1419  		subBody := chat1.NewMessageSystemWithBulkaddtoconv(chat1.MessageSystemBulkAddToConv{
  1420  			Usernames: usernames,
  1421  		})
  1422  		body := chat1.NewMessageBodyWithSystem(subBody)
  1423  		msg := chat1.MessagePlaintext{
  1424  			ClientHeader: chat1.MessageClientHeader{
  1425  				TlfName:     info.CanonicalName,
  1426  				MessageType: chat1.MessageType_SYSTEM,
  1427  			},
  1428  			MessageBody: body,
  1429  		}
  1430  		status := chat1.ConversationMemberStatus_ACTIVE
  1431  		_, _, err = sender.Send(ctx, convID, msg, 0, nil, &chat1.SenderSendOptions{
  1432  			JoinMentionsAs: &status,
  1433  		}, nil)
  1434  		return err
  1435  	}
  1436  
  1437  	info, err := CreateNameInfoSource(ctx, h.G(), mt).LookupName(
  1438  		ctx, conv.Metadata.IdTriple.Tlfid, conv.Metadata.Visibility == keybase1.TLFVisibility_PUBLIC, "")
  1439  	if err != nil {
  1440  		return err
  1441  	}
  1442  	// retry the add a few times to prevent races. Each time we remove members
  1443  	// that are already part of the conversation.
  1444  	toExclude := make(map[keybase1.UID]bool)
  1445  	for i := 0; i < 4 && len(usernames) > 0; i++ {
  1446  		h.Debug(ctx, "BulkAddToConv: trying to add %v", usernames)
  1447  		err = sendBulkAddToConv(ctx, sender, usernames, convID, info)
  1448  		switch e := err.(type) {
  1449  		case nil:
  1450  			return nil
  1451  		case libkb.ChatUsersAlreadyInConversationError:
  1452  			// remove the usernames which are already part of the conversation and retry
  1453  			for _, uid := range e.Uids {
  1454  				toExclude[uid] = true
  1455  			}
  1456  			var usernamesToRetry []string
  1457  			for _, username := range usernames {
  1458  				if !toExclude[libkb.UsernameToUID(username)] {
  1459  					usernamesToRetry = append(usernamesToRetry, username)
  1460  				}
  1461  			}
  1462  			usernames = usernamesToRetry
  1463  			if len(usernamesToRetry) == 0 {
  1464  				// don't let this bubble up if everyone is already in the channel
  1465  				err = nil
  1466  			}
  1467  		default:
  1468  			return e
  1469  		}
  1470  	}
  1471  	return err
  1472  }