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 }