1package chat
2
3import (
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
24type Helper struct {
25	globals.Contextified
26	utils.DebugLabeler
27
28	ri func() chat1.RemoteInterface
29}
30
31var _ (libkb.ChatHelper) = (*Helper)(nil)
32
33func 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
41func (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
48func (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
55func (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
63func (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
70func (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
87func (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
94func (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
114func (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
129func (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
144func (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
153func (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
163func (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
174func (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
185func (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
204func (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.
222func (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
255func (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
283func (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
288func (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
293func (h *Helper) UserReacjis(ctx context.Context, uid gregor1.UID) keybase1.UserReacjis {
294	return storage.NewReacjiStore(h.G()).UserReacjis(ctx, uid)
295}
296
297func (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
305func (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
313func (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
322func (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
336type 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
352func 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
367func (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
372func (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
381func (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
399func (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
412func (s *sendHelper) remoteInterface() chat1.RemoteInterface {
413	return s.ri()
414}
415
416func 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
424type recentConversationParticipants struct {
425	globals.Contextified
426	utils.DebugLabeler
427}
428
429func newRecentConversationParticipants(g *globals.Context) *recentConversationParticipants {
430	return &recentConversationParticipants{
431		Contextified: globals.NewContextified(g),
432		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "recentConversationParticipants", false),
433	}
434}
435
436func (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
447func (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
483func 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
488func 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
508func 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
535func 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
682L:
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.
737func 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
792func (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
797func 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
834func (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
840func 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
872func (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
877func 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
905func 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
925func 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
943type NewConvFindExistingMode int
944
945const (
946	NewConvFindExistingNormal NewConvFindExistingMode = iota
947	NewConvFindExistingSkip
948)
949
950func 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
958func 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
969type 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
986func 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
1008func (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
1034func (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
1060func (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
1098func (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
1339func (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
1381func 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
1399func (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}
1473