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