1package chat 2 3import ( 4 "encoding/hex" 5 "errors" 6 "fmt" 7 "time" 8 9 "github.com/keybase/client/go/chat/attachments" 10 "github.com/keybase/client/go/chat/bots" 11 "github.com/keybase/client/go/chat/globals" 12 "github.com/keybase/client/go/chat/msgchecker" 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/engine" 17 "github.com/keybase/client/go/libkb" 18 "github.com/keybase/client/go/protocol/chat1" 19 "github.com/keybase/client/go/protocol/gregor1" 20 "github.com/keybase/client/go/protocol/keybase1" 21 "github.com/keybase/client/go/teams" 22 "github.com/keybase/clockwork" 23 context "golang.org/x/net/context" 24) 25 26type BlockingSender struct { 27 globals.Contextified 28 utils.DebugLabeler 29 30 boxer *Boxer 31 store attachments.Store 32 getRi func() chat1.RemoteInterface 33 prevPtrPagination *chat1.Pagination 34 clock clockwork.Clock 35} 36 37var _ types.Sender = (*BlockingSender)(nil) 38 39func NewBlockingSender(g *globals.Context, boxer *Boxer, getRi func() chat1.RemoteInterface) *BlockingSender { 40 return &BlockingSender{ 41 Contextified: globals.NewContextified(g), 42 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "BlockingSender", false), 43 getRi: getRi, 44 boxer: boxer, 45 store: attachments.NewS3Store(g, g.GetRuntimeDir()), 46 clock: clockwork.NewRealClock(), 47 prevPtrPagination: &chat1.Pagination{Num: 50}, 48 } 49} 50 51func (s *BlockingSender) setPrevPagination(p *chat1.Pagination) { 52 s.prevPtrPagination = p 53} 54 55func (s *BlockingSender) SetClock(clock clockwork.Clock) { 56 s.clock = clock 57} 58 59func (s *BlockingSender) addSenderToMessage(msg chat1.MessagePlaintext) (chat1.MessagePlaintext, gregor1.UID, error) { 60 uid := s.G().Env.GetUID() 61 if uid.IsNil() { 62 return chat1.MessagePlaintext{}, nil, libkb.LoginRequiredError{} 63 } 64 did := s.G().Env.GetDeviceID() 65 if did.IsNil() { 66 return chat1.MessagePlaintext{}, nil, libkb.DeviceRequiredError{} 67 } 68 69 huid := uid.ToBytes() 70 if huid == nil { 71 return chat1.MessagePlaintext{}, nil, errors.New("invalid UID") 72 } 73 74 hdid := make([]byte, libkb.DeviceIDLen) 75 if err := did.ToBytes(hdid); err != nil { 76 return chat1.MessagePlaintext{}, nil, err 77 } 78 79 header := msg.ClientHeader 80 header.Sender = gregor1.UID(huid) 81 header.SenderDevice = gregor1.DeviceID(hdid) 82 updated := chat1.MessagePlaintext{ 83 ClientHeader: header, 84 MessageBody: msg.MessageBody, 85 SupersedesOutboxID: msg.SupersedesOutboxID, 86 } 87 return updated, gregor1.UID(huid), nil 88} 89 90func (s *BlockingSender) addPrevPointersAndCheckConvID(ctx context.Context, msg chat1.MessagePlaintext, 91 conv chat1.ConversationLocal) (resMsg chat1.MessagePlaintext, err error) { 92 93 // Make sure the caller hasn't already assembled this list. For now, this 94 // should never happen, and we'll return an error just in case we make a 95 // mistake in the future. But if there's some use case in the future where 96 // a caller wants to specify custom prevs, we can relax this. 97 if len(msg.ClientHeader.Prev) != 0 { 98 return resMsg, fmt.Errorf("addPrevPointersToMessage expects an empty prev list") 99 } 100 101 var thread chat1.ThreadView 102 var prevs []chat1.MessagePreviousPointer 103 pagination := &chat1.Pagination{ 104 Num: s.prevPtrPagination.Num, 105 } 106 // If we fail to find anything to prev against after maxAttempts, we allow 107 // the message to be send with an empty prev list. 108 maxAttempts := 5 109 attempt := 0 110 reachedLast := false 111 for { 112 thread, err = s.G().ConvSource.Pull(ctx, conv.GetConvID(), msg.ClientHeader.Sender, 113 chat1.GetThreadReason_PREPARE, nil, 114 &chat1.GetThreadQuery{ 115 DisableResolveSupersedes: true, 116 }, 117 pagination) 118 if err != nil { 119 return resMsg, err 120 } else if thread.Pagination == nil { 121 break 122 } 123 pagination.Next = thread.Pagination.Next 124 125 if len(thread.Messages) == 0 { 126 s.Debug(ctx, "no local messages found for prev pointers") 127 } 128 newPrevsForRegular, newPrevsForExploding, err := CheckPrevPointersAndGetUnpreved(&thread) 129 if err != nil { 130 return resMsg, err 131 } 132 133 var hasPrev bool 134 if msg.IsEphemeral() { 135 prevs = newPrevsForExploding 136 hasPrev = len(newPrevsForExploding) > 0 137 } else { 138 prevs = newPrevsForRegular 139 // If we have only sent ephemeralMessages and are now sending a regular 140 // message, we may have an empty list for newPrevsForRegular. In this 141 // case we allow the `Prev` to be empty, so we don't want to abort in 142 // the check on numPrev below. 143 hasPrev = len(newPrevsForRegular) > 0 || len(newPrevsForExploding) > 0 144 } 145 146 if hasPrev { 147 break 148 } else if thread.Pagination.Last && !reachedLast { 149 s.Debug(ctx, "Could not find previous messages for prev pointers (of %v). Nuking local storage and retrying.", len(thread.Messages)) 150 if err := s.G().ConvSource.Clear(ctx, conv.GetConvID(), msg.ClientHeader.Sender, &types.ClearOpts{ 151 SendLocalAdminNotification: true, 152 Reason: "missing prev pointer", 153 }); err != nil { 154 s.Debug(ctx, "Unable to clear conversation: %v, %v", conv.GetConvID(), err) 155 break 156 } 157 attempt = 0 158 pagination.Next = nil 159 // Make sure we only reset `attempt` once 160 reachedLast = true 161 continue 162 } else if attempt >= maxAttempts || reachedLast { 163 s.Debug(ctx, "Could not find previous messages for prev pointers (of %v), after %v attempts. Giving up.", len(thread.Messages), attempt) 164 break 165 } else { 166 s.Debug(ctx, "Could not find previous messages for prev pointers (of %v), attempt: %v of %v, retrying", len(thread.Messages), attempt, maxAttempts) 167 } 168 attempt++ 169 } 170 171 for _, msg2 := range thread.Messages { 172 if msg2.IsValid() { 173 if err = s.checkConvID(ctx, conv, msg, msg2); err != nil { 174 s.Debug(ctx, "Unable to checkConvID: %s", msg2.DebugString()) 175 return resMsg, err 176 } 177 break 178 } 179 } 180 181 // Make an attempt to avoid changing anything in the input message. There 182 // are a lot of shared pointers though, so this is 183 header := msg.ClientHeader 184 header.Prev = prevs 185 updated := chat1.MessagePlaintext{ 186 ClientHeader: header, 187 MessageBody: msg.MessageBody, 188 } 189 return updated, nil 190} 191 192// Check that the {ConvID,ConvTriple,TlfName} of msgToSend matches both the ConvID and an existing message from the questionable ConvID. 193// `convID` is the convID that `msgToSend` will be posted to. 194// `msgReference` is a validated message from `convID`. 195// The misstep that this method checks for is thus: The frontend may post a message while viewing an "untrusted inbox view". 196// That message (msgToSend) will have the header.{TlfName,TlfPublic} set to the user's intention. 197// But the header.Conv.{Tlfid,TopicType,TopicID} and the convID to post to may be erroneously set to a different conversation's values. 198// This method checks that all of those fields match. Using `msgReference` as the validated link from {TlfName,TlfPublic} <-> ConvTriple. 199func (s *BlockingSender) checkConvID(ctx context.Context, conv chat1.ConversationLocal, 200 msgToSend chat1.MessagePlaintext, msgReference chat1.MessageUnboxed) error { 201 202 headerQ := msgToSend.ClientHeader 203 headerRef := msgReference.Valid().ClientHeader 204 205 fmtConv := func(conv chat1.ConversationIDTriple) string { return hex.EncodeToString(conv.Hash()) } 206 207 if !headerQ.Conv.Derivable(conv.GetConvID()) { 208 s.Debug(ctx, "checkConvID: ConvID %s </- %s", fmtConv(headerQ.Conv), conv.GetConvID()) 209 return fmt.Errorf("ConversationID does not match reference message") 210 } 211 212 if !headerQ.Conv.Eq(headerRef.Conv) { 213 s.Debug(ctx, "checkConvID: Conv %s != %s", fmtConv(headerQ.Conv), fmtConv(headerRef.Conv)) 214 return fmt.Errorf("ConversationID does not match reference message") 215 } 216 217 if headerQ.TlfPublic != headerRef.TlfPublic { 218 s.Debug(ctx, "checkConvID: TlfPublic %s != %s", headerQ.TlfPublic, headerRef.TlfPublic) 219 return fmt.Errorf("Chat public-ness does not match reference message") 220 } 221 if headerQ.TlfName != headerRef.TlfName { 222 // If we're of type TEAM, we lookup the name info for the team and 223 // verify it matches what is on the message itself. If we rename a 224 // subteam the names between the current and reference message will 225 // differ so we cannot rely on that. 226 switch conv.GetMembersType() { 227 case chat1.ConversationMembersType_TEAM: 228 // Cannonicalize the given TlfName 229 teamNameParsed, err := keybase1.TeamNameFromString(headerQ.TlfName) 230 if err != nil { 231 return fmt.Errorf("invalid team name: %v", err) 232 } 233 if info, err := CreateNameInfoSource(ctx, s.G(), conv.GetMembersType()).LookupName(ctx, 234 conv.Info.Triple.Tlfid, 235 conv.Info.Visibility == keybase1.TLFVisibility_PUBLIC, 236 headerQ.TlfName); err != nil { 237 return err 238 } else if info.CanonicalName != teamNameParsed.String() { 239 return fmt.Errorf("TlfName does not match conversation tlf [%q vs ref %q]", teamNameParsed.String(), info.CanonicalName) 240 } 241 default: 242 // Try normalizing both tlfnames if simple comparison fails because they may have resolved. 243 if namesEq, err := s.boxer.CompareTlfNames(ctx, headerQ.TlfName, headerRef.TlfName, 244 conv.GetMembersType(), headerQ.TlfPublic); err != nil { 245 return err 246 } else if !namesEq { 247 s.Debug(ctx, "checkConvID: TlfName %s != %s", headerQ.TlfName, headerRef.TlfName) 248 return fmt.Errorf("TlfName does not match reference message [%q vs ref %q]", headerQ.TlfName, headerRef.TlfName) 249 } 250 } 251 } 252 253 return nil 254} 255 256// Get all messages to be deleted, and attachments to delete. 257// Returns (message, assetsToDelete, flipConvToDelete, error) 258// If the entire conversation is cached locally, this will find all messages that should be deleted. 259// If the conversation is not cached, this relies on the server to get old messages, so the server 260// could omit messages. Those messages would then not be signed into the `Deletes` list. And their 261// associated attachment assets would be left undeleted. 262func (s *BlockingSender) getAllDeletedEdits(ctx context.Context, uid gregor1.UID, 263 convID chat1.ConversationID, msg chat1.MessagePlaintext) (chat1.MessagePlaintext, []chat1.Asset, *chat1.ConversationID, error) { 264 265 var pendingAssetDeletes []chat1.Asset 266 var deleteFlipConvID *chat1.ConversationID 267 268 // Make sure this is a valid delete message 269 if msg.ClientHeader.MessageType != chat1.MessageType_DELETE { 270 return msg, nil, nil, nil 271 } 272 273 deleteTargetID := msg.ClientHeader.Supersedes 274 if deleteTargetID == 0 { 275 return msg, nil, nil, fmt.Errorf("getAllDeletedEdits: no supersedes specified") 276 } 277 278 // Get the one message to be deleted by ID. 279 deleteTarget, err := s.getMessage(ctx, uid, convID, deleteTargetID, false /* resolveSupersedes */) 280 if err != nil { 281 return msg, nil, nil, err 282 } 283 bodyTyp, err := deleteTarget.MessageBody.MessageType() 284 if err != nil { 285 return msg, nil, nil, err 286 } 287 switch bodyTyp { 288 case chat1.MessageType_REACTION: 289 // Don't do anything here for reactions/unfurls, they can't be edited 290 return msg, nil, nil, nil 291 case chat1.MessageType_SYSTEM: 292 msgSys := deleteTarget.MessageBody.System() 293 typ, err := msgSys.SystemType() 294 if err != nil { 295 return msg, nil, nil, err 296 } 297 if !chat1.IsSystemMsgDeletableByDelete(typ) { 298 return msg, nil, nil, fmt.Errorf("%v is not deletable", typ) 299 } 300 case chat1.MessageType_FLIP: 301 flipConvID := deleteTarget.MessageBody.Flip().FlipConvID 302 deleteFlipConvID = &flipConvID 303 } 304 305 // Delete all assets on the deleted message. 306 // assetsForMessage logs instead of failing. 307 pads2 := utils.AssetsForMessage(s.G(), deleteTarget.MessageBody) 308 pendingAssetDeletes = append(pendingAssetDeletes, pads2...) 309 310 // Time of the first message to be deleted. 311 timeOfFirst := gregor1.FromTime(deleteTarget.ServerHeader.Ctime) 312 // Time a couple seconds before that, because After querying is exclusive. 313 timeBeforeFirst := gregor1.ToTime(timeOfFirst.Add(-2 * time.Second)) 314 315 // Get all the affected edits/AUs since just before the delete target. 316 // Use ConvSource with an `After` which query. Fetches from a combination of local cache 317 // and the server. This is an opportunity for the server to retain messages that should 318 // have been deleted without getting caught. 319 var tv chat1.ThreadView 320 switch deleteTarget.ClientHeader.MessageType { 321 case chat1.MessageType_UNFURL: 322 // no edits/deletes possible here 323 default: 324 tv, err = s.G().ConvSource.Pull(ctx, convID, msg.ClientHeader.Sender, 325 chat1.GetThreadReason_PREPARE, nil, 326 &chat1.GetThreadQuery{ 327 MarkAsRead: false, 328 MessageTypes: []chat1.MessageType{chat1.MessageType_EDIT, chat1.MessageType_ATTACHMENTUPLOADED}, 329 After: &timeBeforeFirst, 330 }, nil) 331 if err != nil { 332 return msg, nil, nil, err 333 } 334 } 335 336 // Get all affected messages to be deleted 337 deletes := []chat1.MessageID{deleteTargetID} 338 // Add in any reaction/unfurl messages the deleteTargetID may have 339 deletes = append(deletes, 340 append(deleteTarget.ServerHeader.ReactionIDs, deleteTarget.ServerHeader.UnfurlIDs...)...) 341 for _, m := range tv.Messages { 342 if !m.IsValid() { 343 continue 344 } 345 body := m.Valid().MessageBody 346 typ, err := body.MessageType() 347 if err != nil { 348 s.Debug(ctx, "getAllDeletedEdits: error getting message type: convID: %s msgID: %d err: %s", 349 convID, m.GetMessageID(), err.Error()) 350 continue 351 } 352 switch typ { 353 case chat1.MessageType_EDIT: 354 if body.Edit().MessageID == deleteTargetID { 355 deletes = append(deletes, m.GetMessageID()) 356 } 357 case chat1.MessageType_ATTACHMENTUPLOADED: 358 if body.Attachmentuploaded().MessageID == deleteTargetID { 359 deletes = append(deletes, m.GetMessageID()) 360 361 // Delete all assets on AttachmentUploaded's for the deleted message. 362 // assetsForMessage logs instead of failing. 363 pads2 = utils.AssetsForMessage(s.G(), body) 364 pendingAssetDeletes = append(pendingAssetDeletes, pads2...) 365 } 366 default: 367 s.Debug(ctx, "getAllDeletedEdits: unexpected message type: convID: %s msgID: %d typ: %v", 368 convID, m.GetMessageID(), typ) 369 continue 370 } 371 } 372 373 // Modify original delete message 374 msg.ClientHeader.Deletes = deletes 375 // NOTE: If we ever add more fields to MessageDelete, we'll need to be 376 // careful to preserve them here. 377 msg.MessageBody = chat1.NewMessageBodyWithDelete(chat1.MessageDelete{MessageIDs: deletes}) 378 379 return msg, pendingAssetDeletes, deleteFlipConvID, nil 380} 381 382func (s *BlockingSender) getMessage(ctx context.Context, uid gregor1.UID, 383 convID chat1.ConversationID, msgID chat1.MessageID, resolveSupersedes bool) (mvalid chat1.MessageUnboxedValid, err error) { 384 reason := chat1.GetThreadReason_PREPARE 385 messages, err := s.G().ConvSource.GetMessages(ctx, convID, uid, []chat1.MessageID{msgID}, 386 &reason, nil, resolveSupersedes) 387 if err != nil { 388 return mvalid, err 389 } 390 if len(messages) == 0 { 391 return mvalid, fmt.Errorf("getMessage: message not found") 392 } 393 if !messages[0].IsValid() { 394 st, err := messages[0].State() 395 return mvalid, fmt.Errorf("getMessage returned invalid message: msgID: %v st: %v: err %v", 396 msgID, st, err) 397 } 398 return messages[0].Valid(), nil 399} 400 401// If we are superseding an ephemeral message, we have to set the 402// ephemeralMetadata on this superseder message. 403func (s *BlockingSender) getSupersederEphemeralMetadata(ctx context.Context, uid gregor1.UID, 404 convID chat1.ConversationID, msg chat1.MessagePlaintext) (metadata *chat1.MsgEphemeralMetadata, err error) { 405 406 if chat1.IsEphemeralNonSupersederType(msg.ClientHeader.MessageType) { 407 // Leave whatever was previously set 408 return msg.ClientHeader.EphemeralMetadata, nil 409 } else if !chat1.IsEphemeralSupersederType(msg.ClientHeader.MessageType) { 410 // clear out any defaults, this msg is a non-ephemeral type 411 return nil, nil 412 } 413 414 supersededMsg, err := s.getMessage(ctx, uid, convID, msg.ClientHeader.Supersedes, false /* resolveSupersedes */) 415 if err != nil { 416 return nil, err 417 } 418 if supersededMsg.IsEphemeral() { 419 metadata = supersededMsg.EphemeralMetadata() 420 metadata.Lifetime = gregor1.ToDurationSec(supersededMsg.RemainingEphemeralLifetime(s.clock.Now())) 421 } 422 return metadata, nil 423} 424 425// processReactionMessage determines if we are trying to post a duplicate 426// chat1.MessageType_REACTION, which is considered a chat1.MessageType_DELETE 427// and updates the send appropriately. 428func (s *BlockingSender) processReactionMessage(ctx context.Context, uid gregor1.UID, 429 convID chat1.ConversationID, msg chat1.MessagePlaintext) (clientHeader chat1.MessageClientHeader, body chat1.MessageBody, err error) { 430 if msg.ClientHeader.MessageType != chat1.MessageType_REACTION { 431 // nothing to do here 432 return msg.ClientHeader, msg.MessageBody, nil 433 } 434 435 // We could either be posting a reaction or removing one that we already posted. 436 supersededMsg, err := s.getMessage(ctx, uid, convID, msg.ClientHeader.Supersedes, 437 true /* resolveSupersedes */) 438 if err != nil { 439 return clientHeader, body, err 440 } 441 found, reactionMsgID := supersededMsg.Reactions.HasReactionFromUser(msg.MessageBody.Reaction().Body, 442 s.G().Env.GetUsername().String()) 443 if found { 444 msg.ClientHeader.Supersedes = reactionMsgID 445 msg.ClientHeader.MessageType = chat1.MessageType_DELETE 446 msg.ClientHeader.Deletes = []chat1.MessageID{reactionMsgID} 447 msg.MessageBody = chat1.NewMessageBodyWithDelete(chat1.MessageDelete{ 448 MessageIDs: []chat1.MessageID{reactionMsgID}, 449 }) 450 } else { 451 // bookkeep the reaction used so we can keep track of the user's 452 // popular reactions in the UI 453 if err := storage.NewReacjiStore(s.G()).PutReacji(ctx, uid, msg.MessageBody.Reaction().Body); err != nil { 454 s.Debug(ctx, "unable to put in ReacjiStore: %v", err) 455 } 456 // set an @ mention on the message body for the author of the message we are reacting to 457 s.Debug(ctx, "processReactionMessage: adding target: %s", supersededMsg.ClientHeader.Sender) 458 body := msg.MessageBody.Reaction().DeepCopy() 459 body.TargetUID = &supersededMsg.ClientHeader.Sender 460 msg.MessageBody = chat1.NewMessageBodyWithReaction(body) 461 } 462 463 return msg.ClientHeader, msg.MessageBody, nil 464} 465 466func (s *BlockingSender) checkTopicNameAndGetState(ctx context.Context, msg chat1.MessagePlaintext, 467 membersType chat1.ConversationMembersType) (topicNameState *chat1.TopicNameState, convIDs []chat1.ConversationID, err error) { 468 if msg.ClientHeader.MessageType != chat1.MessageType_METADATA { 469 return topicNameState, convIDs, nil 470 } 471 tlfID := msg.ClientHeader.Conv.Tlfid 472 topicType := msg.ClientHeader.Conv.TopicType 473 switch topicType { 474 case chat1.TopicType_EMOJICROSS: 475 // skip this for this topic type 476 return topicNameState, convIDs, nil 477 default: 478 } 479 newTopicName := msg.MessageBody.Metadata().ConversationTitle 480 convs, err := s.G().TeamChannelSource.GetChannelsFull(ctx, msg.ClientHeader.Sender, tlfID, topicType) 481 if err != nil { 482 return nil, nil, err 483 } 484 var validConvs []chat1.ConversationLocal 485 for _, conv := range convs { 486 // If we have a conv error consider the conv invalid. Exclude 487 // the conv from out TopicNameState forcing the client to retry. 488 if conv.Error == nil { 489 if conv.GetTopicName() == "" { 490 s.Debug(ctx, "checkTopicNameAndGetState: unnamed channel in play: %s", conv.GetConvID()) 491 } 492 validConvs = append(validConvs, conv) 493 convIDs = append(convIDs, conv.GetConvID()) 494 } else { 495 s.Debug(ctx, "checkTopicNameAndGetState: skipping conv: %s, will cause an error from server", 496 conv.GetConvID()) 497 } 498 if conv.GetTopicName() == newTopicName { 499 return nil, nil, DuplicateTopicNameError{Conv: conv} 500 } 501 } 502 503 ts, err := GetTopicNameState(ctx, s.G(), s.DebugLabeler, validConvs, 504 msg.ClientHeader.Sender, tlfID, topicType, membersType) 505 if err != nil { 506 return nil, nil, err 507 } 508 topicNameState = &ts 509 return topicNameState, convIDs, nil 510} 511 512func (s *BlockingSender) resolveOutboxIDEdit(ctx context.Context, uid gregor1.UID, 513 convID chat1.ConversationID, msg *chat1.MessagePlaintext) error { 514 if msg.SupersedesOutboxID == nil { 515 return nil 516 } 517 s.Debug(ctx, "resolveOutboxIDEdit: resolving edit: outboxID: %s", msg.SupersedesOutboxID) 518 typ, err := msg.MessageBody.MessageType() 519 if err != nil { 520 return err 521 } 522 if typ != chat1.MessageType_EDIT { 523 return errors.New("supersedes outboxID only valid for edit messages") 524 } 525 body := msg.MessageBody.Edit() 526 // try to find the message with the given outbox ID in the first 50 messages. 527 tv, err := s.G().ConvSource.Pull(ctx, convID, uid, chat1.GetThreadReason_PREPARE, nil, 528 &chat1.GetThreadQuery{ 529 MessageTypes: []chat1.MessageType{chat1.MessageType_TEXT}, 530 DisableResolveSupersedes: true, 531 }, &chat1.Pagination{Num: 50}) 532 if err != nil { 533 return err 534 } 535 for _, m := range tv.Messages { 536 if msg.SupersedesOutboxID.Eq(m.GetOutboxID()) { 537 s.Debug(ctx, "resolveOutboxIDEdit: resolved edit: outboxID: %s messageID: %v", 538 msg.SupersedesOutboxID, m.GetMessageID()) 539 msg.ClientHeader.Supersedes = m.GetMessageID() 540 msg.MessageBody = chat1.NewMessageBodyWithEdit(chat1.MessageEdit{ 541 MessageID: m.GetMessageID(), 542 Body: body.Body, 543 }) 544 return nil 545 } 546 } 547 return errors.New("failed to find message to edit") 548} 549 550func (s *BlockingSender) handleReplyTo(ctx context.Context, uid gregor1.UID, convID chat1.ConversationID, 551 msg chat1.MessagePlaintext, replyTo *chat1.MessageID) (chat1.MessagePlaintext, error) { 552 if replyTo == nil { 553 return msg, nil 554 } 555 typ, err := msg.MessageBody.MessageType() 556 if err != nil { 557 s.Debug(ctx, "handleReplyTo: failed to get body type: %s", err) 558 return msg, nil 559 } 560 switch typ { 561 case chat1.MessageType_TEXT: 562 s.Debug(ctx, "handleReplyTo: handling text message") 563 header := msg.ClientHeader 564 header.Supersedes = *replyTo 565 reply, err := s.G().ChatHelper.GetMessage(ctx, uid, convID, *replyTo, false, nil) 566 if err != nil { 567 s.Debug(ctx, "handleReplyTo: failed to get reply message: %s", err) 568 return msg, err 569 } 570 if !reply.IsValid() { 571 s.Debug(ctx, "handleReplyTo: reply message invalid: %v %v", replyTo, err) 572 return msg, nil 573 } 574 replyToUID := reply.Valid().ClientHeader.Sender 575 newBody := msg.MessageBody.Text().DeepCopy() 576 newBody.ReplyTo = replyTo 577 newBody.ReplyToUID = &replyToUID 578 return chat1.MessagePlaintext{ 579 ClientHeader: header, 580 MessageBody: chat1.NewMessageBodyWithText(newBody), 581 SupersedesOutboxID: msg.SupersedesOutboxID, 582 }, nil 583 default: 584 s.Debug(ctx, "handleReplyTo: skipping message of type: %v", typ) 585 } 586 return msg, nil 587} 588 589func (s *BlockingSender) handleEmojis(ctx context.Context, uid gregor1.UID, 590 convID chat1.ConversationID, msg chat1.MessagePlaintext, topicType chat1.TopicType) (chat1.MessagePlaintext, error) { 591 if topicType != chat1.TopicType_CHAT { 592 return msg, nil 593 } 594 typ, err := msg.MessageBody.MessageType() 595 if err != nil { 596 s.Debug(ctx, "handleEmojis: failed to get body type: %s", err) 597 return msg, nil 598 } 599 body := msg.MessageBody.TextForDecoration() 600 if len(body) == 0 { 601 return msg, nil 602 } 603 emojis, err := s.G().EmojiSource.Harvest(ctx, body, uid, convID, types.EmojiHarvestModeNormal) 604 if err != nil { 605 return msg, err 606 } 607 if len(emojis) == 0 { 608 return msg, nil 609 } 610 ct := make(map[string]chat1.HarvestedEmoji, len(emojis)) 611 for _, emoji := range emojis { 612 ct[emoji.Alias] = emoji 613 } 614 s.Debug(ctx, "handleEmojis: found %d emojis", len(ct)) 615 switch typ { 616 case chat1.MessageType_TEXT: 617 newBody := msg.MessageBody.Text().DeepCopy() 618 newBody.Emojis = ct 619 return chat1.MessagePlaintext{ 620 ClientHeader: msg.ClientHeader, 621 MessageBody: chat1.NewMessageBodyWithText(newBody), 622 SupersedesOutboxID: msg.SupersedesOutboxID, 623 }, nil 624 case chat1.MessageType_REACTION: 625 newBody := msg.MessageBody.Reaction().DeepCopy() 626 newBody.Emojis = ct 627 return chat1.MessagePlaintext{ 628 ClientHeader: msg.ClientHeader, 629 MessageBody: chat1.NewMessageBodyWithReaction(newBody), 630 SupersedesOutboxID: msg.SupersedesOutboxID, 631 }, nil 632 case chat1.MessageType_EDIT: 633 newBody := msg.MessageBody.Edit().DeepCopy() 634 newBody.Emojis = ct 635 return chat1.MessagePlaintext{ 636 ClientHeader: msg.ClientHeader, 637 MessageBody: chat1.NewMessageBodyWithEdit(newBody), 638 SupersedesOutboxID: msg.SupersedesOutboxID, 639 }, nil 640 case chat1.MessageType_ATTACHMENT: 641 newBody := msg.MessageBody.Attachment().DeepCopy() 642 newBody.Emojis = ct 643 return chat1.MessagePlaintext{ 644 ClientHeader: msg.ClientHeader, 645 MessageBody: chat1.NewMessageBodyWithAttachment(newBody), 646 SupersedesOutboxID: msg.SupersedesOutboxID, 647 }, nil 648 case chat1.MessageType_HEADLINE: 649 newBody := msg.MessageBody.Headline().DeepCopy() 650 newBody.Emojis = ct 651 return chat1.MessagePlaintext{ 652 ClientHeader: msg.ClientHeader, 653 MessageBody: chat1.NewMessageBodyWithHeadline(newBody), 654 SupersedesOutboxID: msg.SupersedesOutboxID, 655 }, nil 656 } 657 return msg, nil 658} 659 660func (s *BlockingSender) getUsernamesForMentions(ctx context.Context, uid gregor1.UID, 661 conv *chat1.ConversationLocal) (res []string, err error) { 662 if conv == nil { 663 return nil, nil 664 } 665 defer s.Trace(ctx, &err, "getParticipantsForMentions")() 666 // get the conv that we will look for @ mentions in 667 switch conv.GetMembersType() { 668 case chat1.ConversationMembersType_TEAM: 669 teamID, err := keybase1.TeamIDFromString(conv.Info.Triple.Tlfid.String()) 670 if err != nil { 671 return res, err 672 } 673 team, err := teams.Load(ctx, s.G().ExternalG(), keybase1.LoadTeamArg{ 674 ID: teamID, 675 }) 676 if err != nil { 677 return res, err 678 } 679 members, err := teams.MembersDetails(ctx, s.G().ExternalG(), team) 680 if err != nil { 681 return res, err 682 } 683 for _, memb := range members { 684 res = append(res, memb.Username) 685 } 686 return res, nil 687 default: 688 res = make([]string, 0, len(conv.Info.Participants)) 689 for _, p := range conv.Info.Participants { 690 res = append(res, p.Username) 691 } 692 return res, nil 693 } 694} 695func (s *BlockingSender) handleMentions(ctx context.Context, uid gregor1.UID, msg chat1.MessagePlaintext, 696 conv *chat1.ConversationLocal) (res chat1.MessagePlaintext, atMentions []gregor1.UID, chanMention chat1.ChannelMention, err error) { 697 if msg.ClientHeader.Conv.TopicType != chat1.TopicType_CHAT { 698 return msg, atMentions, chanMention, nil 699 } 700 // Function to check that the header and body types match. 701 // Call this before accessing the body. 702 // Do not call this for TLFNAME which has no body. 703 checkHeaderBodyTypeMatch := func() error { 704 bodyType, err := msg.MessageBody.MessageType() 705 if err != nil { 706 return err 707 } 708 if msg.ClientHeader.MessageType != bodyType { 709 return fmt.Errorf("cannot send message with mismatched header/body types: %v != %v", 710 msg.ClientHeader.MessageType, bodyType) 711 } 712 return nil 713 } 714 atFromKnown := func(knowns []chat1.KnownUserMention) (res []gregor1.UID) { 715 for _, known := range knowns { 716 res = append(res, known.Uid) 717 } 718 return res 719 } 720 maybeToTeam := func(maybeMentions []chat1.MaybeMention) (res []chat1.KnownTeamMention) { 721 for _, maybe := range maybeMentions { 722 if s.G().TeamMentionLoader.IsTeamMention(ctx, uid, maybe, nil) { 723 res = append(res, chat1.KnownTeamMention(maybe)) 724 } 725 } 726 return res 727 } 728 729 // find @ mentions 730 getConvUsernames := func() ([]string, error) { 731 return s.getUsernamesForMentions(ctx, uid, conv) 732 } 733 var knownUserMentions []chat1.KnownUserMention 734 var maybeMentions []chat1.MaybeMention 735 switch msg.ClientHeader.MessageType { 736 case chat1.MessageType_TEXT: 737 if err = checkHeaderBodyTypeMatch(); err != nil { 738 return res, atMentions, chanMention, err 739 } 740 knownUserMentions, maybeMentions, chanMention = utils.GetTextAtMentionedItems(ctx, s.G(), 741 uid, conv.GetConvID(), msg.MessageBody.Text(), getConvUsernames, &s.DebugLabeler) 742 atMentions = atFromKnown(knownUserMentions) 743 newBody := msg.MessageBody.Text().DeepCopy() 744 newBody.TeamMentions = maybeToTeam(maybeMentions) 745 newBody.UserMentions = knownUserMentions 746 res = chat1.MessagePlaintext{ 747 ClientHeader: msg.ClientHeader, 748 MessageBody: chat1.NewMessageBodyWithText(newBody), 749 SupersedesOutboxID: msg.SupersedesOutboxID, 750 } 751 case chat1.MessageType_ATTACHMENT: 752 if err = checkHeaderBodyTypeMatch(); err != nil { 753 return res, atMentions, chanMention, err 754 } 755 knownUserMentions, maybeMentions, chanMention = utils.ParseAtMentionedItems(ctx, s.G(), 756 msg.MessageBody.Attachment().GetTitle(), nil, getConvUsernames) 757 atMentions = atFromKnown(knownUserMentions) 758 newBody := msg.MessageBody.Attachment().DeepCopy() 759 newBody.TeamMentions = maybeToTeam(maybeMentions) 760 newBody.UserMentions = knownUserMentions 761 res = chat1.MessagePlaintext{ 762 ClientHeader: msg.ClientHeader, 763 MessageBody: chat1.NewMessageBodyWithAttachment(newBody), 764 SupersedesOutboxID: msg.SupersedesOutboxID, 765 } 766 case chat1.MessageType_FLIP: 767 if err = checkHeaderBodyTypeMatch(); err != nil { 768 return res, atMentions, chanMention, err 769 } 770 knownUserMentions, maybeMentions, chanMention = utils.ParseAtMentionedItems(ctx, s.G(), 771 msg.MessageBody.Flip().Text, nil, getConvUsernames) 772 atMentions = atFromKnown(knownUserMentions) 773 newBody := msg.MessageBody.Flip().DeepCopy() 774 newBody.TeamMentions = maybeToTeam(maybeMentions) 775 newBody.UserMentions = knownUserMentions 776 res = chat1.MessagePlaintext{ 777 ClientHeader: msg.ClientHeader, 778 MessageBody: chat1.NewMessageBodyWithFlip(newBody), 779 SupersedesOutboxID: msg.SupersedesOutboxID, 780 } 781 case chat1.MessageType_EDIT: 782 if err = checkHeaderBodyTypeMatch(); err != nil { 783 return res, atMentions, chanMention, err 784 } 785 knownUserMentions, maybeMentions, chanMention = utils.ParseAtMentionedItems(ctx, s.G(), 786 msg.MessageBody.Edit().Body, nil, getConvUsernames) 787 atMentions = atFromKnown(knownUserMentions) 788 newBody := msg.MessageBody.Edit().DeepCopy() 789 newBody.TeamMentions = maybeToTeam(maybeMentions) 790 newBody.UserMentions = knownUserMentions 791 res = chat1.MessagePlaintext{ 792 ClientHeader: msg.ClientHeader, 793 MessageBody: chat1.NewMessageBodyWithEdit(newBody), 794 SupersedesOutboxID: msg.SupersedesOutboxID, 795 } 796 case chat1.MessageType_REACTION: 797 targetUID := msg.MessageBody.Reaction().TargetUID 798 if targetUID != nil { 799 atMentions = []gregor1.UID{*targetUID} 800 } 801 res = msg 802 case chat1.MessageType_SYSTEM: 803 if err = checkHeaderBodyTypeMatch(); err != nil { 804 return res, atMentions, chanMention, err 805 } 806 res = msg 807 atMentions, chanMention, _ = utils.SystemMessageMentions(ctx, s.G(), uid, msg.MessageBody.System()) 808 default: 809 res = msg 810 } 811 return res, atMentions, chanMention, nil 812} 813 814// Prepare a message to be sent. 815// Returns (boxedMessage, pendingAssetDeletes, error) 816func (s *BlockingSender) Prepare(ctx context.Context, plaintext chat1.MessagePlaintext, 817 membersType chat1.ConversationMembersType, conv *chat1.ConversationLocal, 818 inopts *chat1.SenderPrepareOptions) (res types.SenderPrepareResult, err error) { 819 820 if plaintext.ClientHeader.MessageType == chat1.MessageType_NONE { 821 return res, fmt.Errorf("cannot send message without type") 822 } 823 // set default options unless some are given to us 824 var opts chat1.SenderPrepareOptions 825 if inopts != nil { 826 opts = *inopts 827 } 828 829 msg, uid, err := s.addSenderToMessage(plaintext) 830 if err != nil { 831 return res, err 832 } 833 834 // Make sure our delete message gets everything it should 835 var pendingAssetDeletes []chat1.Asset 836 var deleteFlipConvID *chat1.ConversationID 837 if conv != nil { 838 convID := conv.GetConvID() 839 msg.ClientHeader.Conv = conv.Info.Triple 840 if len(msg.ClientHeader.TlfName) == 0 { 841 msg.ClientHeader.TlfName = conv.Info.TlfName 842 msg.ClientHeader.TlfPublic = conv.Info.Visibility == keybase1.TLFVisibility_PUBLIC 843 } 844 s.Debug(ctx, "Prepare: performing convID based checks") 845 846 // Check for outboxID based edits 847 if err = s.resolveOutboxIDEdit(ctx, uid, convID, &msg); err != nil { 848 s.Debug(ctx, "Prepare: error resolving outboxID edit: %s", err) 849 return res, err 850 } 851 852 // Add and check prev pointers 853 msg, err = s.addPrevPointersAndCheckConvID(ctx, msg, *conv) 854 if err != nil { 855 s.Debug(ctx, "Prepare: error adding prev pointers: %s", err) 856 return res, err 857 } 858 859 // First process the reactionMessage in case we convert it to a delete 860 header, body, err := s.processReactionMessage(ctx, uid, convID, msg) 861 if err != nil { 862 s.Debug(ctx, "Prepare: error processing reactions: %s", err) 863 return res, err 864 } 865 msg.ClientHeader = header 866 msg.MessageBody = body 867 868 // Handle reply to 869 if msg, err = s.handleReplyTo(ctx, uid, convID, msg, opts.ReplyTo); err != nil { 870 s.Debug(ctx, "Prepare: error processing reply: %s", err) 871 return res, err 872 } 873 874 // Handle cross team emoji 875 if msg, err = s.handleEmojis(ctx, uid, convID, msg, conv.GetTopicType()); err != nil { 876 s.Debug(ctx, "Prepare: error processing cross team emoji: %s", err) 877 return res, err 878 } 879 880 // Be careful not to shadow (msg, pendingAssetDeletes, deleteFlipConvID) with this assignment. 881 msg, pendingAssetDeletes, deleteFlipConvID, err = s.getAllDeletedEdits(ctx, uid, convID, msg) 882 if err != nil { 883 s.Debug(ctx, "Prepare: error getting deleted edits: %s", err) 884 return res, err 885 } 886 887 if !conv.GetTopicType().EphemeralAllowed() { 888 if msg.EphemeralMetadata() != nil { 889 return res, fmt.Errorf("%v messages cannot be ephemeral", conv.GetTopicType()) 890 } 891 } else { 892 // If no ephemeral data set, then let's double check to make sure 893 // no exploding policy or Gregor state should set it if it's required. 894 if msg.EphemeralMetadata() == nil && 895 chat1.IsEphemeralNonSupersederType(msg.ClientHeader.MessageType) && 896 conv.GetTopicType().EphemeralRequired() { 897 s.Debug(ctx, "Prepare: attempting to set ephemeral policy from conversation") 898 elf, err := utils.EphemeralLifetimeFromConv(ctx, s.G(), *conv) 899 if err != nil { 900 s.Debug(ctx, "Prepare: failed to get ephemeral lifetime from conv: %s", err) 901 elf = nil 902 } 903 if elf != nil { 904 s.Debug(ctx, "Prepare: setting ephemeral lifetime from conv: %v", *elf) 905 msg.ClientHeader.EphemeralMetadata = &chat1.MsgEphemeralMetadata{ 906 Lifetime: *elf, 907 } 908 } 909 } 910 911 msg.ClientHeader.EphemeralMetadata, err = s.getSupersederEphemeralMetadata(ctx, uid, convID, msg) 912 if err != nil { 913 s.Debug(ctx, "Prepare: error getting superseder ephemeral metadata: %s", err) 914 return res, err 915 } 916 } 917 } 918 919 // Make sure it is a proper length 920 if err := msgchecker.CheckMessagePlaintext(msg); err != nil { 921 s.Debug(ctx, "Prepare: error checking message plaintext: %s", err) 922 return res, err 923 } 924 925 // Get topic name state if this is a METADATA message, so that we avoid any races to the 926 // server 927 var topicNameState *chat1.TopicNameState 928 var topicNameStateConvs []chat1.ConversationID 929 if !opts.SkipTopicNameState { 930 if topicNameState, topicNameStateConvs, err = s.checkTopicNameAndGetState(ctx, msg, membersType); err != nil { 931 s.Debug(ctx, "Prepare: error checking topic name state: %s", err) 932 return res, err 933 } 934 } 935 936 // handle mentions 937 var atMentions []gregor1.UID 938 var chanMention chat1.ChannelMention 939 if msg, atMentions, chanMention, err = s.handleMentions(ctx, uid, msg, conv); err != nil { 940 s.Debug(ctx, "Prepare: error handling mentions: %s", err) 941 return res, err 942 } 943 944 // encrypt the message 945 skp, err := s.getSigningKeyPair(ctx) 946 if err != nil { 947 s.Debug(ctx, "Prepare: error getting signing key pair: %s", err) 948 return res, err 949 } 950 951 // If we are sending a message, and we think the conversation is a KBFS conversation, then set a label 952 // on the client header in case this conversation gets upgraded to impteam. 953 msg.ClientHeader.KbfsCryptKeysUsed = new(bool) 954 if membersType == chat1.ConversationMembersType_KBFS { 955 s.Debug(ctx, "setting KBFS crypt keys used flag") 956 *msg.ClientHeader.KbfsCryptKeysUsed = true 957 } else { 958 *msg.ClientHeader.KbfsCryptKeysUsed = false 959 } 960 961 var convID *chat1.ConversationID 962 if conv != nil { 963 id := conv.GetConvID() 964 convID = &id 965 } 966 botUIDs, err := s.applyTeamBotSettings(ctx, uid, &msg, convID, membersType, atMentions, opts) 967 if err != nil { 968 s.Debug(ctx, "Prepare: failed to apply team bot settings: %s", err) 969 return res, err 970 } 971 if len(botUIDs) > 0 { 972 // TODO HOTPOT-330 Add support for "hidden" messages for multiple bots 973 msg.ClientHeader.BotUID = &botUIDs[0] 974 } 975 s.Debug(ctx, "applyTeamBotSettings: matched %d bots, applied %v", len(botUIDs), msg.ClientHeader.BotUID) 976 977 encInfo, err := s.boxer.GetEncryptionInfo(ctx, &msg, membersType, skp) 978 if err != nil { 979 s.Debug(ctx, "Prepare: error getting encryption info: %s", err) 980 return res, err 981 } 982 boxed, err := s.boxer.BoxMessage(ctx, msg, membersType, skp, &encInfo) 983 if err != nil { 984 s.Debug(ctx, "Prepare: error boxing message: %s", err) 985 return res, err 986 } 987 return types.SenderPrepareResult{ 988 Boxed: boxed, 989 EncryptionInfo: encInfo, 990 PendingAssetDeletes: pendingAssetDeletes, 991 DeleteFlipConv: deleteFlipConvID, 992 AtMentions: atMentions, 993 ChannelMention: chanMention, 994 TopicNameState: topicNameState, 995 TopicNameStateConvs: topicNameStateConvs, 996 }, nil 997} 998 999func (s *BlockingSender) applyTeamBotSettings(ctx context.Context, uid gregor1.UID, 1000 msg *chat1.MessagePlaintext, convID *chat1.ConversationID, membersType chat1.ConversationMembersType, 1001 atMentions []gregor1.UID, opts chat1.SenderPrepareOptions) ([]gregor1.UID, error) { 1002 // no bots in KBFS convs 1003 if membersType == chat1.ConversationMembersType_KBFS { 1004 return nil, nil 1005 } 1006 1007 // Skip checks if botUID already set 1008 if msg.ClientHeader.BotUID != nil { 1009 s.Debug(ctx, "applyTeamBotSettings: found existing botUID %v", msg.ClientHeader.BotUID) 1010 // verify this value is actually a restricted bot of the team. 1011 teamBotSettings, err := CreateNameInfoSource(ctx, s.G(), membersType).TeamBotSettings(ctx, 1012 msg.ClientHeader.TlfName, msg.ClientHeader.Conv.Tlfid, membersType, msg.ClientHeader.TlfPublic) 1013 if err != nil { 1014 return nil, err 1015 } 1016 for uv := range teamBotSettings { 1017 botUID := gregor1.UID(uv.Uid.ToBytes()) 1018 if botUID.Eq(*msg.ClientHeader.BotUID) { 1019 s.Debug(ctx, "applyTeamBotSettings: existing botUID matches, short circuiting.") 1020 return nil, nil 1021 } 1022 } 1023 s.Debug(ctx, "applyTeamBotSettings: existing botUID %v does not match any bot, clearing") 1024 // Caller was mistaken, this uid is not actually a bot so we unset the 1025 // value. 1026 msg.ClientHeader.BotUID = nil 1027 } 1028 1029 // Check if we are superseding a bot message. If so, just take what the 1030 // superseded has. Don't automatically key for replies, run the normal checks. 1031 if msg.ClientHeader.Supersedes > 0 && opts.ReplyTo == nil && convID != nil { 1032 target, err := s.getMessage(ctx, uid, *convID, msg.ClientHeader.Supersedes, false /*resolveSupersedes */) 1033 if err != nil { 1034 return nil, err 1035 } 1036 botUID := target.ClientHeader.BotUID 1037 if botUID == nil { 1038 s.Debug(ctx, "applyTeamBotSettings: skipping, supersedes has nil botUID from msgID %d", msg.ClientHeader.Supersedes) 1039 return nil, nil 1040 } 1041 s.Debug(ctx, "applyTeamBotSettings: supersedes botUID %v from msgID %d", botUID, msg.ClientHeader.Supersedes) 1042 return []gregor1.UID{*botUID}, nil 1043 } 1044 1045 // Fetch the bot settings, if any 1046 teamBotSettings, err := CreateNameInfoSource(ctx, s.G(), membersType).TeamBotSettings(ctx, 1047 msg.ClientHeader.TlfName, msg.ClientHeader.Conv.Tlfid, membersType, msg.ClientHeader.TlfPublic) 1048 if err != nil { 1049 return nil, err 1050 } 1051 1052 mentionMap := make(map[string]struct{}) 1053 for _, uid := range atMentions { 1054 mentionMap[uid.String()] = struct{}{} 1055 } 1056 1057 var botUIDs []gregor1.UID 1058 for uv, botSettings := range teamBotSettings { 1059 botUID := gregor1.UID(uv.Uid.ToBytes()) 1060 1061 // If the bot is the sender encrypt only for them. 1062 if msg.ClientHeader.Sender.Eq(botUID) { 1063 if convID == nil || botSettings.ConvIDAllowed(convID.String()) { 1064 return []gregor1.UID{botUID}, nil 1065 } 1066 // Bot channel restrictions only apply to CHAT types. 1067 conv, err := utils.GetVerifiedConv(ctx, s.G(), uid, *convID, types.InboxSourceDataSourceAll) 1068 if err == nil && conv.GetTopicType() != chat1.TopicType_CHAT { 1069 return []gregor1.UID{botUID}, nil 1070 } 1071 return nil, NewRestrictedBotChannelError() 1072 } 1073 1074 isMatch, err := bots.ApplyTeamBotSettings(ctx, s.G(), botUID, botSettings, *msg, 1075 convID, mentionMap, s.DebugLabeler) 1076 if err != nil { 1077 return nil, err 1078 } 1079 s.Debug(ctx, "applyTeamBotSettings: applied settings for %+v for botuid: %v, senderUID: %v, convID: %v isMatch: %v", 1080 botSettings, uv.Uid, msg.ClientHeader.Sender, convID, isMatch) 1081 if isMatch { 1082 botUIDs = append(botUIDs, botUID) 1083 } 1084 } 1085 return botUIDs, nil 1086} 1087 1088func (s *BlockingSender) getSigningKeyPair(ctx context.Context) (kp libkb.NaclSigningKeyPair, err error) { 1089 // get device signing key for this user 1090 signingKey, err := engine.GetMySecretKey(ctx, s.G().ExternalG(), 1091 libkb.DeviceSigningKeyType, "sign chat message") 1092 if err != nil { 1093 return libkb.NaclSigningKeyPair{}, err 1094 } 1095 kp, ok := signingKey.(libkb.NaclSigningKeyPair) 1096 if !ok || kp.Private == nil { 1097 return libkb.NaclSigningKeyPair{}, libkb.KeyCannotSignError{} 1098 } 1099 1100 return kp, nil 1101} 1102 1103// deleteAssets deletes assets from s3. 1104// Logs but does not return errors. Assets may be left undeleted. 1105func (s *BlockingSender) deleteAssets(ctx context.Context, convID chat1.ConversationID, assets []chat1.Asset) error { 1106 // get s3 params from server 1107 params, err := s.getRi().GetS3Params(ctx, convID) 1108 if err != nil { 1109 s.G().Log.Warning("error getting s3 params: %s", err) 1110 return nil 1111 } 1112 1113 if err := s.store.DeleteAssets(ctx, params, s, assets); err != nil { 1114 s.G().Log.Warning("error deleting assets: %s", err) 1115 1116 // there's no way to get asset information after this point. 1117 // any assets not deleted will be stranded on s3. 1118 1119 return nil 1120 } 1121 1122 s.G().Log.Debug("deleted %d assets", len(assets)) 1123 return nil 1124} 1125 1126// Sign implements github.com/keybase/go/chat/s3.Signer interface. 1127func (s *BlockingSender) Sign(payload []byte) ([]byte, error) { 1128 arg := chat1.S3SignArg{ 1129 Payload: payload, 1130 Version: 1, 1131 } 1132 return s.getRi().S3Sign(context.Background(), arg) 1133} 1134 1135func (s *BlockingSender) presentUIItem(ctx context.Context, uid gregor1.UID, conv *chat1.ConversationLocal) (res *chat1.InboxUIItem) { 1136 if conv != nil { 1137 pc := utils.PresentConversationLocal(ctx, s.G(), uid, *conv, utils.PresentParticipantsModeSkip) 1138 res = &pc 1139 } 1140 return res 1141} 1142 1143func (s *BlockingSender) Send(ctx context.Context, convID chat1.ConversationID, 1144 msg chat1.MessagePlaintext, clientPrev chat1.MessageID, 1145 outboxID *chat1.OutboxID, sendOpts *chat1.SenderSendOptions, prepareOpts *chat1.SenderPrepareOptions) (obid chat1.OutboxID, boxed *chat1.MessageBoxed, err error) { 1146 defer s.Trace(ctx, &err, fmt.Sprintf("Send(%s)", convID))() 1147 defer utils.SuspendComponent(ctx, s.G(), s.G().InboxSource)() 1148 1149 // Get conversation metadata first. If we can't find it, we will just attempt to join 1150 // the conversation in case that is an option. If it succeeds, then we just keep going, 1151 // otherwise we give up and return an error. 1152 var conv chat1.ConversationLocal 1153 sender := gregor1.UID(s.G().Env.GetUID().ToBytes()) 1154 conv, err = utils.GetVerifiedConv(ctx, s.G(), sender, convID, types.InboxSourceDataSourceAll) 1155 if err != nil { 1156 s.Debug(ctx, "Send: error getting conversation metadata: %v", err) 1157 return nil, nil, err 1158 } 1159 s.Debug(ctx, "Send: uid: %s in conversation %s (tlfName: %s) with status: %v", sender, 1160 conv.GetConvID(), conv.Info.TlfName, conv.ReaderInfo.Status) 1161 1162 // If we are in preview mode, then just join the conversation right now. 1163 switch conv.ReaderInfo.Status { 1164 case chat1.ConversationMemberStatus_PREVIEW, chat1.ConversationMemberStatus_NEVER_JOINED: 1165 switch msg.ClientHeader.MessageType { 1166 case chat1.MessageType_JOIN, 1167 chat1.MessageType_LEAVE, 1168 chat1.MessageType_HEADLINE, 1169 chat1.MessageType_METADATA, 1170 chat1.MessageType_SYSTEM: // don't need to join to send a system message. 1171 // pass so we don't loop between Send and Join/Leave or join when 1172 // updating the metadata/headline. 1173 default: 1174 s.Debug(ctx, "Send: user is in mode: %v, joining conversation", conv.ReaderInfo.Status) 1175 if err = JoinConversation(ctx, s.G(), s.DebugLabeler, s.getRi, sender, convID); err != nil { 1176 return nil, nil, err 1177 } 1178 } 1179 default: 1180 // do nothing 1181 } 1182 1183 var prepareRes types.SenderPrepareResult 1184 var plres chat1.PostRemoteRes 1185 // If we get a ChatStalePreviousStateError we blow away in the box cache 1186 // once to allow the retry to get fresh data. 1187 clearedCache := false 1188 // Try this up to 5 times in case we are trying to set the topic name, and the topic name 1189 // state is moving around underneath us. 1190 for i := 0; i < 5; i++ { 1191 // Add a bunch of stuff to the message (like prev pointers, sender info, ...) 1192 if prepareRes, err = s.Prepare(ctx, msg, conv.GetMembersType(), &conv, prepareOpts); err != nil { 1193 s.Debug(ctx, "Send: error in Prepare: %s", err) 1194 return nil, nil, err 1195 } 1196 boxed = &prepareRes.Boxed 1197 1198 // Log some useful information about the message we are sending 1199 obidstr := "(none)" 1200 if boxed.ClientHeader.OutboxID != nil { 1201 obidstr = boxed.ClientHeader.OutboxID.String() 1202 } 1203 s.Debug(ctx, "Send: sending message: convID: %s outboxID: %s", convID, obidstr) 1204 1205 // Keep trying if we get an error on topicNameState for a fixed number of times 1206 rarg := chat1.PostRemoteArg{ 1207 ConversationID: convID, 1208 MessageBoxed: *boxed, 1209 AtMentions: prepareRes.AtMentions, 1210 ChannelMention: prepareRes.ChannelMention, 1211 TopicNameState: prepareRes.TopicNameState, 1212 JoinMentionsAs: sendOpts.GetJoinMentionsAs(), 1213 } 1214 plres, err = s.getRi().PostRemote(ctx, rarg) 1215 if err != nil { 1216 switch e := err.(type) { 1217 case libkb.ChatStalePreviousStateError: 1218 // If we hit the stale previous state error, that means we should try again, since our view is 1219 // out of date. 1220 s.Debug(ctx, "Send: failed because of stale previous state, trying the whole thing again") 1221 if !clearedCache { 1222 s.Debug(ctx, "Send: clearing inbox cache to retry stale previous state") 1223 if err := s.G().InboxSource.Clear(ctx, sender, &types.ClearOpts{ 1224 SendLocalAdminNotification: true, 1225 Reason: "stale previous topic state", 1226 }); err != nil { 1227 s.Debug(ctx, "Send: error clearing: %+v", err) 1228 } 1229 s.Debug(ctx, "Send: clearing conversation cache to retry stale previous state: %d convs", 1230 len(prepareRes.TopicNameStateConvs)) 1231 for _, convID := range prepareRes.TopicNameStateConvs { 1232 if err := s.G().ConvSource.Clear(ctx, convID, sender, nil); err != nil { 1233 s.Debug(ctx, "Send: error clearing: %v %+v", convID, err) 1234 } 1235 } 1236 clearedCache = true 1237 } 1238 continue 1239 case libkb.ChatEphemeralRetentionPolicyViolatedError: 1240 s.Debug(ctx, "Send: failed because of invalid ephemeral policy, trying the whole thing again") 1241 var cerr error 1242 conv, cerr = utils.GetVerifiedConv(ctx, s.G(), sender, convID, 1243 types.InboxSourceDataSourceRemoteOnly) 1244 if cerr != nil { 1245 return nil, nil, cerr 1246 } 1247 continue 1248 case libkb.EphemeralPairwiseMACsMissingUIDsError: 1249 s.Debug(ctx, "Send: failed because of missing KIDs for pairwise MACs, reloading UPAKs for %v and retrying.", e.UIDs) 1250 err := utils.ForceReloadUPAKsForUIDs(ctx, s.G(), e.UIDs) 1251 if err != nil { 1252 s.Debug(ctx, "Send: error forcing reloads: %+v", err) 1253 } 1254 continue 1255 default: 1256 s.Debug(ctx, "Send: failed to PostRemote, bailing: %s", err) 1257 return nil, nil, err 1258 } 1259 } 1260 boxed.ServerHeader = &plres.MsgHeader 1261 1262 // Delete assets associated with a delete operation. 1263 // Logs instead of returning an error. Assets can be left undeleted. 1264 if len(prepareRes.PendingAssetDeletes) > 0 { 1265 err = s.deleteAssets(ctx, convID, prepareRes.PendingAssetDeletes) 1266 if err != nil { 1267 s.Debug(ctx, "Send: failure in deleteAssets: %s", err) 1268 } 1269 } 1270 1271 if prepareRes.DeleteFlipConv != nil { 1272 _, err = s.getRi().DeleteConversation(ctx, *prepareRes.DeleteFlipConv) 1273 if err != nil { 1274 s.Debug(ctx, "Send: failure in DeleteConversation: %s", err) 1275 } 1276 } 1277 break 1278 } 1279 if err != nil { 1280 return nil, nil, err 1281 } 1282 1283 // If this message was sent from the Outbox, then we can remove it now 1284 if boxed.ClientHeader.OutboxID != nil { 1285 if _, err = storage.NewOutbox(s.G(), sender).RemoveMessage(ctx, *boxed.ClientHeader.OutboxID); err != nil { 1286 s.Debug(ctx, "unable to remove outbox message: %v", err) 1287 } 1288 } 1289 1290 // Write new message out to cache and other followup 1291 var cerr error 1292 var convLocal *chat1.ConversationLocal 1293 s.Debug(ctx, "sending local updates to chat sources") 1294 // unbox using encryption info we already have 1295 unboxedMsg, err := s.boxer.UnboxMessage(ctx, *boxed, conv, &prepareRes.EncryptionInfo) 1296 if err != nil { 1297 s.Debug(ctx, "Send: failed to unbox sent message: %s", err) 1298 } else { 1299 if cerr = s.G().ConvSource.PushUnboxed(ctx, conv, boxed.ClientHeader.Sender, 1300 []chat1.MessageUnboxed{unboxedMsg}); cerr != nil { 1301 s.Debug(ctx, "Send: failed to push new message into convsource: %s", err) 1302 } 1303 } 1304 if convLocal, err = s.G().InboxSource.NewMessage(ctx, boxed.ClientHeader.Sender, 0, convID, 1305 *boxed, nil); err != nil { 1306 s.Debug(ctx, "Send: failed to update inbox: %s", err) 1307 } 1308 // Send up to frontend 1309 if cerr == nil && boxed.GetMessageType() != chat1.MessageType_LEAVE { 1310 unboxedMsg, err = NewReplyFiller(s.G()).FillSingle(ctx, boxed.ClientHeader.Sender, convID, 1311 unboxedMsg) 1312 if err != nil { 1313 s.Debug(ctx, "Send: failed to fill reply: %s", err) 1314 } 1315 activity := chat1.NewChatActivityWithIncomingMessage(chat1.IncomingMessage{ 1316 Message: utils.PresentMessageUnboxed(ctx, s.G(), unboxedMsg, boxed.ClientHeader.Sender, 1317 convID), 1318 ConvID: convID, 1319 DisplayDesktopNotification: false, 1320 Conv: s.presentUIItem(ctx, boxed.ClientHeader.Sender, convLocal), 1321 }) 1322 s.G().ActivityNotifier.Activity(ctx, boxed.ClientHeader.Sender, conv.GetTopicType(), &activity, 1323 chat1.ChatActivitySource_LOCAL) 1324 } 1325 if conv.GetTopicType() == chat1.TopicType_CHAT { 1326 // Unfurl 1327 go s.G().Unfurler.UnfurlAndSend(globals.BackgroundChatCtx(ctx, s.G()), boxed.ClientHeader.Sender, 1328 convID, unboxedMsg) 1329 // Start tracking any live location sends 1330 if unboxedMsg.IsValid() && unboxedMsg.GetMessageType() == chat1.MessageType_TEXT && 1331 unboxedMsg.Valid().MessageBody.Text().LiveLocation != nil { 1332 if unboxedMsg.Valid().MessageBody.Text().LiveLocation.EndTime.IsZero() { 1333 s.G().LiveLocationTracker.GetCurrentPosition(ctx, conv.GetConvID(), 1334 unboxedMsg.GetMessageID()) 1335 } else { 1336 s.G().LiveLocationTracker.StartTracking(ctx, conv.GetConvID(), unboxedMsg.GetMessageID(), 1337 gregor1.FromTime(unboxedMsg.Valid().MessageBody.Text().LiveLocation.EndTime)) 1338 } 1339 } 1340 if conv.GetMembersType() == chat1.ConversationMembersType_TEAM { 1341 teamID, err := keybase1.TeamIDFromString(conv.Info.Triple.Tlfid.String()) 1342 if err != nil { 1343 s.Debug(ctx, "Send: failed to get team ID: %v", err) 1344 } else { 1345 go s.G().JourneyCardManager.SentMessage(globals.BackgroundChatCtx(ctx, s.G()), sender, teamID, convID) 1346 } 1347 } 1348 } 1349 return nil, boxed, nil 1350} 1351 1352type NonblockingSender struct { 1353 globals.Contextified 1354 utils.DebugLabeler 1355 sender types.Sender 1356} 1357 1358var _ types.Sender = (*NonblockingSender)(nil) 1359 1360func NewNonblockingSender(g *globals.Context, sender types.Sender) *NonblockingSender { 1361 s := &NonblockingSender{ 1362 Contextified: globals.NewContextified(g), 1363 DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "NonblockingSender", false), 1364 sender: sender, 1365 } 1366 return s 1367} 1368 1369func (s *NonblockingSender) Prepare(ctx context.Context, msg chat1.MessagePlaintext, 1370 membersType chat1.ConversationMembersType, conv *chat1.ConversationLocal, 1371 opts *chat1.SenderPrepareOptions) (types.SenderPrepareResult, error) { 1372 return s.sender.Prepare(ctx, msg, membersType, conv, opts) 1373} 1374 1375func (s *NonblockingSender) Send(ctx context.Context, convID chat1.ConversationID, 1376 msg chat1.MessagePlaintext, clientPrev chat1.MessageID, outboxID *chat1.OutboxID, 1377 sendOpts *chat1.SenderSendOptions, prepareOpts *chat1.SenderPrepareOptions) (chat1.OutboxID, *chat1.MessageBoxed, error) { 1378 uid, err := utils.AssertLoggedInUID(ctx, s.G()) 1379 if err != nil { 1380 return nil, nil, err 1381 } 1382 // The strategy here is to select the larger prev between what the UI provides, and what we have 1383 // stored locally. If we just use the UI version, then we can race for creating ordinals in 1384 // Outbox.PushMessage. However, in rare cases we might not have something locally, in that case just 1385 // fallback to the UI provided number. 1386 var storedPrev chat1.MessageID 1387 conv, err := utils.GetUnverifiedConv(ctx, s.G(), uid, convID, types.InboxSourceDataSourceLocalOnly) 1388 if err != nil { 1389 s.Debug(ctx, "Send: failed to get local inbox info: %s", err) 1390 } else { 1391 storedPrev = conv.Conv.GetMaxMessageID() 1392 } 1393 if storedPrev > clientPrev { 1394 clientPrev = storedPrev 1395 } 1396 if clientPrev == 0 { 1397 clientPrev = 1 1398 } 1399 s.Debug(ctx, "Send: using prevMsgID: %d", clientPrev) 1400 msg.ClientHeader.OutboxInfo = &chat1.OutboxInfo{ 1401 Prev: clientPrev, 1402 ComposeTime: gregor1.ToTime(time.Now()), 1403 } 1404 identifyBehavior, _, _ := globals.CtxIdentifyMode(ctx) 1405 obr, err := s.G().MessageDeliverer.Queue(ctx, convID, msg, outboxID, sendOpts, prepareOpts, 1406 identifyBehavior) 1407 if err != nil { 1408 return obr.OutboxID, nil, err 1409 } 1410 return obr.OutboxID, nil, nil 1411} 1412 1413func (s *NonblockingSender) SendUnfurlNonblock(ctx context.Context, convID chat1.ConversationID, 1414 msg chat1.MessagePlaintext, clientPrev chat1.MessageID, outboxID chat1.OutboxID) (chat1.OutboxID, error) { 1415 res, _, err := s.Send(ctx, convID, msg, clientPrev, &outboxID, nil, nil) 1416 return res, err 1417} 1418