1package chat1
2
3import (
4	"bytes"
5	"crypto/hmac"
6	"crypto/sha256"
7	"encoding/hex"
8	"encoding/json"
9	"errors"
10	"flag"
11	"fmt"
12	"hash"
13	"math"
14	"path/filepath"
15	"regexp"
16	"sort"
17	"strconv"
18	"strings"
19	"sync"
20	"time"
21	"unicode/utf8"
22
23	"github.com/keybase/client/go/protocol/gregor1"
24	"github.com/keybase/client/go/protocol/keybase1"
25)
26
27// we will show some representation of an exploded message in the UI for a week
28const ShowExplosionLifetime = time.Hour * 24 * 7
29
30// If a conversation is larger, only admins can @channel.
31const MaxChanMentionConvSize = 100
32
33func (i FlipGameIDStr) String() string {
34	return string(i)
35}
36
37func (i TLFIDStr) String() string {
38	return string(i)
39}
40
41func (i ConvIDStr) String() string {
42	return string(i)
43}
44
45type ByUID []gregor1.UID
46type ConvIDShort = []byte
47
48func (b ByUID) Len() int      { return len(b) }
49func (b ByUID) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
50func (b ByUID) Less(i, j int) bool {
51	return bytes.Compare(b[i].Bytes(), b[j].Bytes()) < 0
52}
53
54// Eq compares two TLFIDs
55func (id TLFID) Eq(other TLFID) bool {
56	return bytes.Equal([]byte(id), []byte(other))
57}
58
59// EqString is like EqualsTo, except that it accepts a fmt.Stringer. This
60// can be useful for comparing keybase1.TLFID and chat1.TLFID.
61func (id TLFID) EqString(other fmt.Stringer) bool {
62	return hex.EncodeToString(id) == other.String()
63}
64
65func (id TLFID) String() string {
66	return hex.EncodeToString(id)
67}
68
69func (id TLFID) Bytes() []byte {
70	return []byte(id)
71}
72
73func (id TLFID) TLFIDStr() TLFIDStr {
74	return TLFIDStr(id.String())
75}
76
77func (id TLFID) IsNil() bool {
78	return len(id) == 0
79}
80
81func (id TLFID) IsTeamID() bool {
82	if len(id) != keybase1.TEAMID_LEN {
83		return false
84	}
85	switch id[len(id)-1] {
86	case keybase1.TEAMID_PRIVATE_SUFFIX,
87		keybase1.TEAMID_PUBLIC_SUFFIX,
88		keybase1.SUB_TEAMID_PRIVATE_SUFFIX,
89		keybase1.SUB_TEAMID_PUBLIC_SUFFIX:
90		return true
91	default:
92		return false
93	}
94}
95
96func MakeConvID(val string) (ConversationID, error) {
97	return hex.DecodeString(val)
98}
99
100func (cid ConversationID) String() string {
101	return hex.EncodeToString(cid)
102}
103
104func (cid ConversationID) Bytes() []byte {
105	return []byte(cid)
106}
107
108func (cid ConversationID) ConvIDStr() ConvIDStr {
109	return ConvIDStr(cid.String())
110}
111
112func (cid ConversationID) IsNil() bool {
113	return len(cid) < DbShortFormLen
114}
115
116func (cid ConversationID) Eq(c ConversationID) bool {
117	return bytes.Equal(cid, c)
118}
119
120func (cid ConversationID) Less(c ConversationID) bool {
121	return bytes.Compare(cid, c) < 0
122}
123
124const DbShortFormLen = 10
125
126// DbShortForm should only be used when interacting with the database, and should
127// never leave Gregor
128func (cid ConversationID) DbShortForm() ConvIDShort {
129	end := DbShortFormLen
130	if end > len(cid) {
131		end = len(cid)
132	}
133	return cid[:end]
134}
135
136func (cid ConversationID) DbShortFormString() string {
137	return DbShortFormToString(cid.DbShortForm())
138}
139
140func DbShortFormToString(cid ConvIDShort) string {
141	return hex.EncodeToString(cid)
142}
143
144func DbShortFormFromString(cid string) (ConvIDShort, error) {
145	return hex.DecodeString(cid)
146}
147
148func MakeTLFID(val string) (TLFID, error) {
149	return hex.DecodeString(val)
150}
151
152func MakeTopicID(val string) (TopicID, error) {
153	return hex.DecodeString(val)
154}
155
156func MakeTopicType(val int64) TopicType {
157	return TopicType(val)
158}
159
160func (mid MessageID) String() string {
161	return strconv.FormatUint(uint64(mid), 10)
162}
163
164func (mid MessageID) Min(mid2 MessageID) MessageID {
165	if mid < mid2 {
166		return mid
167	}
168	return mid2
169}
170
171func (mid MessageID) IsNil() bool {
172	return uint(mid) == 0
173}
174
175func (mid MessageID) Advance(num uint) MessageID {
176	return MessageID(uint(mid) + num)
177}
178
179func (t MessageType) String() string {
180	s, ok := MessageTypeRevMap[t]
181	if ok {
182		return s
183	}
184	return "UNKNOWN"
185}
186
187// Message types deletable by a standard DELETE message.
188var deletableMessageTypesByDelete = []MessageType{
189	MessageType_TEXT,
190	MessageType_ATTACHMENT,
191	MessageType_EDIT,
192	MessageType_ATTACHMENTUPLOADED,
193	MessageType_REACTION,
194	MessageType_REQUESTPAYMENT,
195	MessageType_UNFURL,
196	MessageType_PIN,
197	MessageType_HEADLINE,
198	MessageType_SYSTEM,
199	MessageType_FLIP,
200}
201
202// Messages types NOT deletable by a DELETEHISTORY message.
203var nonDeletableMessageTypesByDeleteHistory = []MessageType{
204	MessageType_NONE,
205	MessageType_DELETE,
206	MessageType_TLFNAME,
207	MessageType_DELETEHISTORY,
208}
209
210func DeletableMessageTypesByDelete() []MessageType {
211	return deletableMessageTypesByDelete
212}
213
214func IsSystemMsgDeletableByDelete(typ MessageSystemType) bool {
215	switch typ {
216	case MessageSystemType_ADDEDTOTEAM,
217		MessageSystemType_INVITEADDEDTOTEAM,
218		MessageSystemType_GITPUSH,
219		MessageSystemType_CHANGEAVATAR,
220		MessageSystemType_CHANGERETENTION,
221		MessageSystemType_BULKADDTOCONV,
222		MessageSystemType_SBSRESOLVE,
223		MessageSystemType_NEWCHANNEL:
224		return true
225	case MessageSystemType_COMPLEXTEAM,
226		MessageSystemType_CREATETEAM:
227		return false
228	default:
229		return false
230	}
231}
232
233var visibleMessageTypes = []MessageType{
234	MessageType_TEXT,
235	MessageType_ATTACHMENT,
236	MessageType_JOIN,
237	MessageType_LEAVE,
238	MessageType_SYSTEM,
239	MessageType_SENDPAYMENT,
240	MessageType_REQUESTPAYMENT,
241	MessageType_FLIP,
242	MessageType_HEADLINE,
243	MessageType_PIN,
244}
245
246// Visible chat messages appear visually as a message in the conv.
247// For counterexample REACTION and DELETE_HISTORY have visual effects but do not appear as a message.
248func VisibleChatMessageTypes() []MessageType {
249	return visibleMessageTypes
250}
251
252var badgeableMessageTypes = []MessageType{
253	MessageType_TEXT,
254	MessageType_ATTACHMENT,
255	MessageType_SYSTEM,
256	MessageType_SENDPAYMENT,
257	MessageType_REQUESTPAYMENT,
258	MessageType_FLIP,
259	MessageType_HEADLINE,
260	MessageType_PIN,
261}
262
263// Message types that cause badges.
264// JOIN and LEAVE are Visible but are too minute to badge.
265func BadgeableMessageTypes() []MessageType {
266	return badgeableMessageTypes
267}
268
269// A conversation is considered 'empty' unless it has one of these message types.
270// Used for filtering empty convs out of the the inbox.
271func NonEmptyConvMessageTypes() []MessageType {
272	return badgeableMessageTypes
273}
274
275var snippetMessageTypes = []MessageType{
276	MessageType_TEXT,
277	MessageType_ATTACHMENT,
278	MessageType_SYSTEM,
279	MessageType_DELETEHISTORY,
280	MessageType_SENDPAYMENT,
281	MessageType_REQUESTPAYMENT,
282	MessageType_FLIP,
283	MessageType_HEADLINE,
284	MessageType_PIN,
285}
286
287// Snippet chat messages can be the snippet of a conversation.
288func SnippetChatMessageTypes() []MessageType {
289	return snippetMessageTypes
290}
291
292var editableMessageTypesByEdit = []MessageType{
293	MessageType_TEXT,
294	MessageType_ATTACHMENT,
295}
296
297func EditableMessageTypesByEdit() []MessageType {
298	return editableMessageTypesByEdit
299}
300
301func IsEphemeralSupersederType(typ MessageType) bool {
302	switch typ {
303	case MessageType_EDIT,
304		MessageType_ATTACHMENTUPLOADED,
305		MessageType_REACTION,
306		MessageType_UNFURL:
307		return true
308	default:
309		return false
310	}
311}
312
313func IsEphemeralNonSupersederType(typ MessageType) bool {
314	switch typ {
315	case MessageType_TEXT,
316		MessageType_ATTACHMENT,
317		MessageType_FLIP:
318		return true
319	default:
320		return false
321	}
322}
323
324func IsEphemeralType(typ MessageType) bool {
325	return IsEphemeralNonSupersederType(typ) || IsEphemeralSupersederType(typ)
326}
327
328func DeletableMessageTypesByDeleteHistory() (res []MessageType) {
329	banned := make(map[MessageType]bool)
330	for _, mt := range nonDeletableMessageTypesByDeleteHistory {
331		banned[mt] = true
332	}
333	for _, mt := range MessageTypeMap {
334		if !banned[mt] {
335			res = append(res, mt)
336		}
337	}
338	sort.Slice(res, func(i, j int) bool {
339		return res[i] < res[j]
340	})
341	return res
342}
343
344func IsDeletableByDelete(typ MessageType) bool {
345	for _, typ2 := range deletableMessageTypesByDelete {
346		if typ == typ2 {
347			return true
348		}
349	}
350	return false
351}
352
353func IsDeletableByDeleteHistory(typ MessageType) bool {
354	for _, typ2 := range nonDeletableMessageTypesByDeleteHistory {
355		if typ == typ2 {
356			return false
357		}
358	}
359	return true
360}
361
362// EphemeralAllowed flags if the given topic type is allowed to send ephemeral
363// messages at all.
364func (t TopicType) EphemeralAllowed() bool {
365	switch t {
366	case TopicType_KBFSFILEEDIT,
367		TopicType_EMOJI,
368		TopicType_EMOJICROSS:
369		return false
370	default:
371		return true
372	}
373}
374
375// EphemeralRequired flags if the given topic type required to respect the
376// ephemeral retention policy if set.
377func (t TopicType) EphemeralRequired() bool {
378	switch t {
379	case TopicType_DEV:
380		return false
381	default:
382		return t.EphemeralAllowed()
383	}
384}
385
386func (t TopicType) String() string {
387	s, ok := TopicTypeRevMap[t]
388	if ok {
389		return s
390	}
391	return "UNKNOWN"
392}
393
394func (t TopicID) String() string {
395	return hex.EncodeToString(t)
396}
397
398func (t TopicID) Eq(r TopicID) bool {
399	return bytes.Equal([]byte(t), []byte(r))
400}
401
402func (t ConversationIDTriple) Eq(other ConversationIDTriple) bool {
403	return t.Tlfid.Eq(other.Tlfid) &&
404		bytes.Equal([]byte(t.TopicID), []byte(other.TopicID)) &&
405		t.TopicType == other.TopicType
406}
407
408func (hash Hash) String() string {
409	return hex.EncodeToString(hash)
410}
411
412func (hash Hash) Eq(other Hash) bool {
413	return bytes.Equal(hash, other)
414}
415
416func (m MessageUnboxed) SenderEq(o MessageUnboxed) bool {
417	if state, err := m.State(); err == nil {
418		if ostate, err := o.State(); err == nil && state == ostate {
419			switch state {
420			case MessageUnboxedState_VALID:
421				return m.Valid().SenderEq(o.Valid())
422			case MessageUnboxedState_ERROR:
423				return m.Error().SenderEq(o.Error())
424			case MessageUnboxedState_OUTBOX:
425				return m.Outbox().SenderEq(o.Outbox())
426			}
427		}
428	}
429	return false
430}
431
432func (m MessageUnboxed) OutboxID() *OutboxID {
433	if state, err := m.State(); err == nil {
434		switch state {
435		case MessageUnboxedState_VALID:
436			return m.Valid().ClientHeader.OutboxID
437		case MessageUnboxedState_ERROR:
438			return nil
439		case MessageUnboxedState_PLACEHOLDER:
440			return nil
441		case MessageUnboxedState_OUTBOX:
442			return m.Outbox().Msg.ClientHeader.OutboxID
443		default:
444			return nil
445		}
446	}
447	return nil
448}
449
450func (m MessageUnboxed) GetMessageID() MessageID {
451	if state, err := m.State(); err == nil {
452		switch state {
453		case MessageUnboxedState_VALID:
454			return m.Valid().ServerHeader.MessageID
455		case MessageUnboxedState_ERROR:
456			return m.Error().MessageID
457		case MessageUnboxedState_PLACEHOLDER:
458			return m.Placeholder().MessageID
459		case MessageUnboxedState_OUTBOX:
460			return m.Outbox().Msg.ClientHeader.OutboxInfo.Prev
461		case MessageUnboxedState_JOURNEYCARD:
462			return m.Journeycard().PrevID
463		default:
464			return 0
465		}
466	}
467	return 0
468}
469
470func (m MessageUnboxed) IsEphemeral() bool {
471	if state, err := m.State(); err == nil {
472		switch state {
473		case MessageUnboxedState_VALID:
474			return m.Valid().IsEphemeral()
475		case MessageUnboxedState_ERROR:
476			return m.Error().IsEphemeral
477		}
478	}
479	return false
480}
481
482func (m MessageUnboxed) HideExplosion(maxDeletedUpto MessageID, now time.Time) bool {
483	if state, err := m.State(); err == nil {
484		switch state {
485		case MessageUnboxedState_VALID:
486			return m.Valid().HideExplosion(maxDeletedUpto, now)
487		case MessageUnboxedState_ERROR:
488			return m.Error().HideExplosion(maxDeletedUpto, now)
489		}
490	}
491	return false
492}
493
494func (m MessageUnboxed) GetOutboxID() *OutboxID {
495	if state, err := m.State(); err == nil {
496		switch state {
497		case MessageUnboxedState_VALID:
498			return m.Valid().ClientHeader.OutboxID
499		case MessageUnboxedState_ERROR:
500			return nil
501		case MessageUnboxedState_PLACEHOLDER:
502			return nil
503		case MessageUnboxedState_OUTBOX:
504			obid := m.Outbox().OutboxID
505			return &obid
506		case MessageUnboxedState_JOURNEYCARD:
507			return nil
508		default:
509			return nil
510		}
511	}
512	return nil
513}
514
515func (m MessageUnboxed) GetTopicType() TopicType {
516	if state, err := m.State(); err == nil {
517		switch state {
518		case MessageUnboxedState_VALID:
519			return m.Valid().ClientHeader.Conv.TopicType
520		case MessageUnboxedState_ERROR:
521			return TopicType_NONE
522		case MessageUnboxedState_OUTBOX:
523			return m.Outbox().Msg.ClientHeader.Conv.TopicType
524		case MessageUnboxedState_PLACEHOLDER:
525			return TopicType_NONE
526		case MessageUnboxedState_JOURNEYCARD:
527			return TopicType_NONE
528		}
529	}
530	return TopicType_NONE
531}
532
533func (m MessageUnboxed) GetMessageType() MessageType {
534	if state, err := m.State(); err == nil {
535		switch state {
536		case MessageUnboxedState_VALID:
537			return m.Valid().ClientHeader.MessageType
538		case MessageUnboxedState_ERROR:
539			return m.Error().MessageType
540		case MessageUnboxedState_OUTBOX:
541			return m.Outbox().Msg.ClientHeader.MessageType
542		case MessageUnboxedState_PLACEHOLDER:
543			// All we know about a place holder is the ID, so just
544			// call it type NONE
545			return MessageType_NONE
546		case MessageUnboxedState_JOURNEYCARD:
547			return MessageType_NONE
548		}
549	}
550	return MessageType_NONE
551}
552
553func (m MessageUnboxed) IsValid() bool {
554	if state, err := m.State(); err == nil {
555		return state == MessageUnboxedState_VALID
556	}
557	return false
558}
559
560func (m MessageUnboxed) IsError() bool {
561	if state, err := m.State(); err == nil {
562		return state == MessageUnboxedState_ERROR
563	}
564	return false
565}
566
567func (m MessageUnboxed) IsOutbox() bool {
568	if state, err := m.State(); err == nil {
569		return state == MessageUnboxedState_OUTBOX
570	}
571	return false
572}
573
574func (m MessageUnboxed) IsPlaceholder() bool {
575	if state, err := m.State(); err == nil {
576		return state == MessageUnboxedState_PLACEHOLDER
577	}
578	return false
579}
580
581func (m MessageUnboxed) IsJourneycard() bool {
582	if state, err := m.State(); err == nil {
583		return state == MessageUnboxedState_JOURNEYCARD
584	}
585	return false
586}
587
588// IsValidFull returns whether the message is both:
589// 1. Valid
590// 2. Has a non-deleted body with a type matching the header
591//    (TLFNAME is an exception as it has no body)
592func (m MessageUnboxed) IsValidFull() bool {
593	if !m.IsValid() {
594		return false
595	}
596	valid := m.Valid()
597	headerType := valid.ClientHeader.MessageType
598	switch headerType {
599	case MessageType_NONE:
600		return false
601	case MessageType_TLFNAME:
602		// Skip body check
603		return true
604	}
605	bodyType, err := valid.MessageBody.MessageType()
606	if err != nil {
607		return false
608	}
609	return bodyType == headerType
610}
611
612// IsValidDeleted returns whether a message is valid and has been deleted.
613// This statement does not hold: IsValidFull != IsValidDeleted
614func (m MessageUnboxed) IsValidDeleted() bool {
615	if !m.IsValid() {
616		return false
617	}
618	valid := m.Valid()
619	headerType := valid.ClientHeader.MessageType
620	switch headerType {
621	case MessageType_NONE:
622		return false
623	case MessageType_TLFNAME:
624		// Undeletable and may have no body
625		return false
626	}
627	bodyType, err := valid.MessageBody.MessageType()
628	if err != nil {
629		return false
630	}
631	return bodyType == MessageType_NONE
632}
633
634func (m MessageUnboxed) IsVisible() bool {
635	typ := m.GetMessageType()
636	for _, visType := range VisibleChatMessageTypes() {
637		if typ == visType {
638			return true
639		}
640	}
641	return false
642}
643
644func (m MessageUnboxed) HasReactions() bool {
645	if !m.IsValid() {
646		return false
647	}
648	return len(m.Valid().Reactions.Reactions) > 0
649}
650
651func (m MessageUnboxed) HasUnfurls() bool {
652	if !m.IsValid() {
653		return false
654	}
655	return len(m.Valid().Unfurls) > 0
656}
657
658func (m MessageUnboxed) SearchableText() string {
659	if !m.IsValidFull() {
660		return ""
661	}
662	return m.Valid().MessageBody.SearchableText()
663}
664
665func (m MessageUnboxed) SenderUsername() string {
666	if !m.IsValid() {
667		return ""
668	}
669	return m.Valid().SenderUsername
670}
671
672func (m MessageUnboxed) Ctime() gregor1.Time {
673	if !m.IsValid() {
674		return 0
675	}
676	return m.Valid().ServerHeader.Ctime
677}
678
679func (m MessageUnboxed) AtMentionUsernames() []string {
680	if !m.IsValid() {
681		return nil
682	}
683	return m.Valid().AtMentionUsernames
684}
685
686func (m MessageUnboxed) ChannelMention() ChannelMention {
687	if !m.IsValid() {
688		return ChannelMention_NONE
689	}
690	return m.Valid().ChannelMention
691}
692
693func (m MessageUnboxed) SenderIsBot() bool {
694	if m.IsValid() {
695		valid := m.Valid()
696		return gregor1.UIDPtrEq(valid.ClientHeader.BotUID, &valid.ClientHeader.Sender)
697	}
698	return false
699}
700
701func (m *MessageUnboxed) DebugString() string {
702	if m == nil {
703		return "[nil]"
704	}
705	state, err := m.State()
706	if err != nil {
707		return fmt.Sprintf("[INVALID err:%v]", err)
708	}
709	if state == MessageUnboxedState_ERROR {
710		merr := m.Error()
711		return fmt.Sprintf("[%v %v mt:%v (%v) (%v)]", state, m.GetMessageID(), merr.ErrType, merr.ErrMsg, merr.InternalErrMsg)
712	}
713	switch state {
714	case MessageUnboxedState_VALID:
715		valid := m.Valid()
716		headerType := valid.ClientHeader.MessageType
717		s := fmt.Sprintf("%v %v", state, valid.ServerHeader.MessageID)
718		bodyType, err := valid.MessageBody.MessageType()
719		if err != nil {
720			return fmt.Sprintf("[INVALID-BODY err:%v]", err)
721		}
722		if headerType == bodyType {
723			s = fmt.Sprintf("%v %v", s, headerType)
724		} else {
725			if headerType == MessageType_TLFNAME {
726				s = fmt.Sprintf("%v h:%v (b:%v)", s, headerType, bodyType)
727			} else {
728				s = fmt.Sprintf("%v h:%v != b:%v", s, headerType, bodyType)
729			}
730		}
731		if valid.ServerHeader.SupersededBy != 0 {
732			s = fmt.Sprintf("%v supBy:%v", s, valid.ServerHeader.SupersededBy)
733		}
734		return fmt.Sprintf("[%v]", s)
735	case MessageUnboxedState_OUTBOX:
736		obr := m.Outbox()
737		ostateStr := "CORRUPT"
738		ostate, err := obr.State.State()
739		if err != nil {
740			ostateStr = "CORRUPT"
741		} else {
742			ostateStr = fmt.Sprintf("%v", ostate)
743		}
744		return fmt.Sprintf("[%v obid:%v prev:%v ostate:%v %v]",
745			state, obr.OutboxID, obr.Msg.ClientHeader.OutboxInfo.Prev, ostateStr, obr.Msg.ClientHeader.MessageType)
746	case MessageUnboxedState_JOURNEYCARD:
747		jc := m.Journeycard()
748		return fmt.Sprintf("[JOURNEYCARD %v]", jc.CardType)
749	default:
750		return fmt.Sprintf("[state:%v %v]", state, m.GetMessageID())
751	}
752}
753
754func MessageUnboxedDebugStrings(ms []MessageUnboxed) (res []string) {
755	for _, m := range ms {
756		res = append(res, m.DebugString())
757	}
758	return res
759}
760
761func MessageUnboxedDebugList(ms []MessageUnboxed) string {
762	return fmt.Sprintf("{ %v %v }", len(ms), strings.Join(MessageUnboxedDebugStrings(ms), ","))
763}
764
765func MessageUnboxedDebugLines(ms []MessageUnboxed) string {
766	return strings.Join(MessageUnboxedDebugStrings(ms), "\n")
767}
768
769const (
770	VersionErrorMessageBoxed VersionKind = "messageboxed"
771	VersionErrorHeader       VersionKind = "header"
772	VersionErrorBody         VersionKind = "body"
773)
774
775// NOTE: these values correspond to the maximum accepted values in
776// chat/boxer.go. If these values are changed, they must also be accepted
777// there.
778var MaxMessageBoxedVersion MessageBoxedVersion = MessageBoxedVersion_V4
779var MaxHeaderVersion HeaderPlaintextVersion = HeaderPlaintextVersion_V1
780var MaxBodyVersion BodyPlaintextVersion = BodyPlaintextVersion_V2
781
782// ParseableVersion checks if this error has a version that is now able to be
783// understood by our client.
784func (m MessageUnboxedError) ParseableVersion() bool {
785	switch m.ErrType {
786	case MessageUnboxedErrorType_BADVERSION, MessageUnboxedErrorType_BADVERSION_CRITICAL:
787		// only applies to these types
788	default:
789		return false
790	}
791
792	kind := m.VersionKind
793	version := m.VersionNumber
794	// This error was stored from an old client, we have parse out the info we
795	// need from the error message.
796	// TODO remove this check once it has be live for a few cycles.
797	if kind == "" && version == 0 {
798		re := regexp.MustCompile(`.* Chat version error: \[ unhandled: (\w+) version: (\d+) .*\]`)
799		matches := re.FindStringSubmatch(m.ErrMsg)
800		if len(matches) != 3 {
801			return false
802		}
803		kind = VersionKind(matches[1])
804		var err error
805		version, err = strconv.Atoi(matches[2])
806		if err != nil {
807			return false
808		}
809	}
810
811	var maxVersion int
812	switch kind {
813	case VersionErrorMessageBoxed:
814		maxVersion = int(MaxMessageBoxedVersion)
815	case VersionErrorHeader:
816		maxVersion = int(MaxHeaderVersion)
817	case VersionErrorBody:
818		maxVersion = int(MaxBodyVersion)
819	default:
820		return false
821	}
822	return maxVersion >= version
823}
824
825func (m MessageUnboxedError) IsEphemeralError() bool {
826	return m.IsEphemeral && (m.ErrType == MessageUnboxedErrorType_EPHEMERAL || m.ErrType == MessageUnboxedErrorType_PAIRWISE_MISSING)
827}
828
829func (m MessageUnboxedError) IsEphemeralExpired(now time.Time) bool {
830	if !m.IsEphemeral {
831		return false
832	}
833	etime := m.Etime.Time()
834	// There are a few ways a message could be considered expired
835	// 1. We were "exploded now"
836	// 2. Our lifetime is up
837	return m.ExplodedBy != nil || etime.Before(now) || etime.Equal(now)
838}
839
840func (m MessageUnboxedError) HideExplosion(maxDeletedUpto MessageID, now time.Time) bool {
841	if !m.IsEphemeral {
842		return false
843	}
844	etime := m.Etime
845	// Don't show ash lines for messages that have been expunged.
846	return etime.Time().Add(ShowExplosionLifetime).Before(now) || m.MessageID < maxDeletedUpto
847}
848
849func (m MessageUnboxedError) SenderEq(o MessageUnboxedError) bool {
850	return m.SenderUsername == o.SenderUsername
851}
852
853func (m OutboxRecord) SenderEq(o OutboxRecord) bool {
854	return m.Msg.ClientHeader.Sender.Eq(o.Msg.ClientHeader.Sender)
855}
856
857func (m MessageUnboxedValid) AsDeleteHistory() (res MessageDeleteHistory, err error) {
858	if m.ClientHeader.MessageType != MessageType_DELETEHISTORY {
859		return res, fmt.Errorf("message is %v not %v", m.ClientHeader.MessageType, MessageType_DELETEHISTORY)
860	}
861	if m.MessageBody.IsNil() {
862		return res, fmt.Errorf("missing message body")
863	}
864	btyp, err := m.MessageBody.MessageType()
865	if err != nil {
866		return res, err
867	}
868	if btyp != MessageType_DELETEHISTORY {
869		return res, fmt.Errorf("message has wrong body type: %v", btyp)
870	}
871	return m.MessageBody.Deletehistory(), nil
872}
873
874func (m *MsgEphemeralMetadata) String() string {
875	if m == nil {
876		return "<nil>"
877	}
878	var explodedBy string
879	if m.ExplodedBy == nil {
880		explodedBy = "<nil>"
881	} else {
882		explodedBy = *m.ExplodedBy
883	}
884	return fmt.Sprintf("{ Lifetime: %v, Generation: %v, ExplodedBy: %v }", m.Lifetime.ToDuration(), m.Generation, explodedBy)
885}
886
887func (m MessagePlaintext) MessageType() MessageType {
888	typ, err := m.MessageBody.MessageType()
889	if err != nil {
890		return MessageType_NONE
891	}
892	return typ
893}
894
895func (m MessagePlaintext) IsVisible() bool {
896	typ := m.MessageType()
897	for _, visType := range VisibleChatMessageTypes() {
898		if typ == visType {
899			return true
900		}
901	}
902	return false
903}
904
905func (m MessagePlaintext) IsBadgableType() bool {
906	typ := m.MessageType()
907	switch typ {
908	case MessageType_TEXT, MessageType_ATTACHMENT:
909		return true
910	default:
911		return false
912	}
913}
914
915func (m MessagePlaintext) SearchableText() string {
916	return m.MessageBody.SearchableText()
917}
918
919func (m MessagePlaintext) IsEphemeral() bool {
920	return m.EphemeralMetadata() != nil
921}
922
923func (m MessagePlaintext) EphemeralMetadata() *MsgEphemeralMetadata {
924	return m.ClientHeader.EphemeralMetadata
925}
926
927func (o *MsgEphemeralMetadata) Eq(r *MsgEphemeralMetadata) bool {
928	if o != nil && r != nil {
929		return *o == *r
930	}
931	return (o == nil) && (r == nil)
932}
933
934func (m MessageUnboxedValid) SenderEq(o MessageUnboxedValid) bool {
935	return m.ClientHeader.Sender.Eq(o.ClientHeader.Sender)
936}
937
938func (m MessageUnboxedValid) HasPairwiseMacs() bool {
939	return m.ClientHeader.HasPairwiseMacs
940}
941
942func (m MessageUnboxedValid) IsEphemeral() bool {
943	return m.EphemeralMetadata() != nil
944}
945
946func (m MessageUnboxedValid) EphemeralMetadata() *MsgEphemeralMetadata {
947	return m.ClientHeader.EphemeralMetadata
948}
949
950func (m MessageUnboxedValid) ExplodedBy() *string {
951	if !m.IsEphemeral() {
952		return nil
953	}
954	return m.EphemeralMetadata().ExplodedBy
955}
956
957func Etime(lifetime gregor1.DurationSec, ctime, rtime, now gregor1.Time) gregor1.Time {
958	originalLifetime := lifetime.ToDuration()
959	elapsedLifetime := now.Time().Sub(ctime.Time())
960	remainingLifetime := originalLifetime - elapsedLifetime
961	// If the server's view doesn't make sense, just use the signed lifetime
962	// from the message.
963	if remainingLifetime > originalLifetime {
964		remainingLifetime = originalLifetime
965	}
966	etime := rtime.Time().Add(remainingLifetime)
967	return gregor1.ToTime(etime)
968}
969
970func (m MessageUnboxedValid) Etime() gregor1.Time {
971	// The server sends us (untrusted) ctime of the message and server's view
972	// of now. We use these to calculate the remaining lifetime on an ephemeral
973	// message, returning an etime based on our received time.
974	metadata := m.EphemeralMetadata()
975	if metadata == nil {
976		return 0
977	}
978	header := m.ServerHeader
979	return Etime(metadata.Lifetime, header.Ctime, m.ClientHeader.Rtime, header.Now)
980}
981
982func (m MessageUnboxedValid) RemainingEphemeralLifetime(now time.Time) time.Duration {
983	remainingLifetime := m.Etime().Time().Sub(now).Round(time.Second)
984	return remainingLifetime
985}
986
987func (m MessageUnboxedValid) IsEphemeralExpired(now time.Time) bool {
988	if !m.IsEphemeral() {
989		return false
990	}
991	etime := m.Etime().Time()
992	// There are a few ways a message could be considered expired
993	// 1. Our body is already nil (deleted from DELETEHISTORY or a server purge)
994	// 2. We were "exploded now"
995	// 3. Our lifetime is up
996	return m.MessageBody.IsNil() || m.EphemeralMetadata().ExplodedBy != nil || etime.Before(now) || etime.Equal(now)
997}
998
999func (m MessageUnboxedValid) HideExplosion(maxDeletedUpto MessageID, now time.Time) bool {
1000	if !m.IsEphemeral() {
1001		return false
1002	}
1003	etime := m.Etime()
1004	// Don't show ash lines for messages that have been expunged.
1005	return etime.Time().Add(ShowExplosionLifetime).Before(now) || m.ServerHeader.MessageID < maxDeletedUpto
1006}
1007
1008func (b MessageBody) IsNil() bool {
1009	return b == MessageBody{}
1010}
1011
1012func (b MessageBody) IsType(typ MessageType) bool {
1013	btyp, err := b.MessageType()
1014	if err != nil {
1015		return false
1016	}
1017	return btyp == typ
1018}
1019
1020func (b MessageBody) TextForDecoration() string {
1021	typ, err := b.MessageType()
1022	if err != nil {
1023		return ""
1024	}
1025	switch typ {
1026	case MessageType_REACTION:
1027		return b.Reaction().Body
1028	case MessageType_HEADLINE:
1029		return b.Headline().Headline
1030	case MessageType_ATTACHMENT:
1031		// Exclude the filename for text decoration.
1032		return b.Attachment().Object.Title
1033	default:
1034		return b.SearchableText()
1035	}
1036}
1037
1038func (b MessageBody) SearchableText() string {
1039	typ, err := b.MessageType()
1040	if err != nil {
1041		return ""
1042	}
1043	switch typ {
1044	case MessageType_TEXT:
1045		return b.Text().Body
1046	case MessageType_EDIT:
1047		return b.Edit().Body
1048	case MessageType_REQUESTPAYMENT:
1049		return b.Requestpayment().Note
1050	case MessageType_ATTACHMENT:
1051		return b.Attachment().GetTitle()
1052	case MessageType_FLIP:
1053		return b.Flip().Text
1054	case MessageType_UNFURL:
1055		return b.Unfurl().SearchableText()
1056	case MessageType_SYSTEM:
1057		return b.System().String()
1058	default:
1059		return ""
1060	}
1061}
1062
1063func (b MessageBody) GetEmojis() map[string]HarvestedEmoji {
1064	typ, err := b.MessageType()
1065	if err != nil {
1066		return nil
1067	}
1068	switch typ {
1069	case MessageType_TEXT:
1070		return b.Text().Emojis
1071	case MessageType_REACTION:
1072		return b.Reaction().Emojis
1073	case MessageType_EDIT:
1074		return b.Edit().Emojis
1075	case MessageType_ATTACHMENT:
1076		return b.Attachment().Emojis
1077	case MessageType_HEADLINE:
1078		return b.Headline().Emojis
1079	default:
1080		return nil
1081	}
1082}
1083
1084func (m UIMessage) IsValid() bool {
1085	if state, err := m.State(); err == nil {
1086		return state == MessageUnboxedState_VALID
1087	}
1088	return false
1089}
1090
1091func (m UIMessage) IsError() bool {
1092	if state, err := m.State(); err == nil {
1093		return state == MessageUnboxedState_ERROR
1094	}
1095	return false
1096}
1097
1098func (m UIMessage) IsOutbox() bool {
1099	if state, err := m.State(); err == nil {
1100		return state == MessageUnboxedState_OUTBOX
1101	}
1102	return false
1103}
1104
1105func (m UIMessage) IsPlaceholder() bool {
1106	if state, err := m.State(); err == nil {
1107		return state == MessageUnboxedState_PLACEHOLDER
1108	}
1109	return false
1110}
1111
1112func (m UIMessage) GetMessageID() MessageID {
1113	if state, err := m.State(); err == nil {
1114		if state == MessageUnboxedState_VALID {
1115			return m.Valid().MessageID
1116		}
1117		if state == MessageUnboxedState_ERROR {
1118			return m.Error().MessageID
1119		}
1120		if state == MessageUnboxedState_PLACEHOLDER {
1121			return m.Placeholder().MessageID
1122		}
1123	}
1124	return 0
1125}
1126
1127func (m UIMessage) GetOutboxID() *OutboxID {
1128	if state, err := m.State(); err == nil {
1129		if state == MessageUnboxedState_VALID {
1130			strOutboxID := m.Valid().OutboxID
1131			if strOutboxID != nil {
1132				outboxID, err := MakeOutboxID(*strOutboxID)
1133				if err != nil {
1134					return nil
1135				}
1136				return &outboxID
1137			}
1138			return nil
1139		}
1140		if state == MessageUnboxedState_ERROR {
1141			return nil
1142		}
1143		if state == MessageUnboxedState_PLACEHOLDER {
1144			return nil
1145		}
1146	}
1147	return nil
1148}
1149
1150func (m UIMessage) GetMessageType() MessageType {
1151	state, err := m.State()
1152	if err != nil {
1153		return MessageType_NONE
1154	}
1155	switch state {
1156	case MessageUnboxedState_VALID:
1157		body := m.Valid().MessageBody
1158		typ, err := body.MessageType()
1159		if err != nil {
1160			return MessageType_NONE
1161		}
1162		return typ
1163	case MessageUnboxedState_ERROR:
1164		return m.Error().MessageType
1165	case MessageUnboxedState_OUTBOX:
1166		return m.Outbox().MessageType
1167	default:
1168		return MessageType_NONE
1169	}
1170}
1171
1172func (m UIMessage) SearchableText() string {
1173	if !m.IsValid() {
1174		return ""
1175	}
1176	return m.Valid().MessageBody.SearchableText()
1177}
1178
1179func (m UIMessage) IsEphemeral() bool {
1180	state, err := m.State()
1181	if err != nil {
1182		return false
1183	}
1184	switch state {
1185	case MessageUnboxedState_VALID:
1186		return m.Valid().IsEphemeral
1187	case MessageUnboxedState_ERROR:
1188		return m.Error().IsEphemeral
1189	default:
1190		return false
1191	}
1192}
1193
1194func (m MessageBoxed) GetMessageID() MessageID {
1195	return m.ServerHeader.MessageID
1196}
1197
1198func (m MessageBoxed) Ctime() gregor1.Time {
1199	return m.ServerHeader.Ctime
1200}
1201
1202func (m MessageBoxed) GetMessageType() MessageType {
1203	return m.ClientHeader.MessageType
1204}
1205
1206func (m MessageBoxed) Summary() MessageSummary {
1207	s := MessageSummary{
1208		MsgID:       m.GetMessageID(),
1209		MessageType: m.GetMessageType(),
1210		TlfName:     m.ClientHeader.TlfName,
1211		TlfPublic:   m.ClientHeader.TlfPublic,
1212	}
1213	if m.ServerHeader != nil {
1214		s.Ctime = m.ServerHeader.Ctime
1215	}
1216	return s
1217}
1218
1219func (m MessageBoxed) OutboxInfo() *OutboxInfo {
1220	return m.ClientHeader.OutboxInfo
1221}
1222
1223func (m MessageBoxed) KBFSEncrypted() bool {
1224	return m.ClientHeader.KbfsCryptKeysUsed == nil || *m.ClientHeader.KbfsCryptKeysUsed
1225}
1226
1227func (m MessageBoxed) EphemeralMetadata() *MsgEphemeralMetadata {
1228	return m.ClientHeader.EphemeralMetadata
1229}
1230
1231func (m MessageBoxed) IsEphemeral() bool {
1232	return m.EphemeralMetadata() != nil
1233}
1234
1235func (m MessageBoxed) Etime() gregor1.Time {
1236	// The server sends us (untrusted) ctime of the message and server's view
1237	// of now. We use these to calculate the remaining lifetime on an ephemeral
1238	// message, returning an etime based on the current time.
1239	metadata := m.EphemeralMetadata()
1240	if metadata == nil {
1241		return 0
1242	}
1243	rtime := gregor1.ToTime(time.Now())
1244	if m.ServerHeader.Rtime != nil {
1245		rtime = *m.ServerHeader.Rtime
1246	}
1247	return Etime(metadata.Lifetime, m.ServerHeader.Ctime, rtime, m.ServerHeader.Now)
1248}
1249
1250func (m MessageBoxed) IsEphemeralExpired(now time.Time) bool {
1251	if !m.IsEphemeral() {
1252		return false
1253	}
1254	etime := m.Etime().Time()
1255	return m.EphemeralMetadata().ExplodedBy != nil || etime.Before(now) || etime.Equal(now)
1256}
1257
1258func (m MessageBoxed) ExplodedBy() *string {
1259	if !m.IsEphemeral() {
1260		return nil
1261	}
1262	return m.EphemeralMetadata().ExplodedBy
1263}
1264
1265var ConversationStatusGregorMap = map[ConversationStatus]string{
1266	ConversationStatus_UNFILED:  "unfiled",
1267	ConversationStatus_FAVORITE: "favorite",
1268	ConversationStatus_IGNORED:  "ignored",
1269	ConversationStatus_BLOCKED:  "blocked",
1270	ConversationStatus_MUTED:    "muted",
1271	ConversationStatus_REPORTED: "reported",
1272}
1273
1274var ConversationStatusGregorRevMap = map[string]ConversationStatus{
1275	"unfiled":  ConversationStatus_UNFILED,
1276	"favorite": ConversationStatus_FAVORITE,
1277	"ignored":  ConversationStatus_IGNORED,
1278	"blocked":  ConversationStatus_BLOCKED,
1279	"muted":    ConversationStatus_MUTED,
1280	"reported": ConversationStatus_REPORTED,
1281}
1282
1283var sha256Pool = sync.Pool{
1284	New: func() interface{} {
1285		return sha256.New()
1286	},
1287}
1288
1289func (t ConversationIDTriple) Hash() []byte {
1290	h := sha256Pool.Get().(hash.Hash)
1291	defer sha256Pool.Put(h)
1292	h.Reset()
1293	h.Write(t.Tlfid)
1294	h.Write(t.TopicID)
1295	h.Write([]byte(strconv.Itoa(int(t.TopicType))))
1296	hash := h.Sum(nil)
1297
1298	return hash
1299}
1300
1301func (t ConversationIDTriple) ToConversationID(shardID [2]byte) ConversationID {
1302	h := t.Hash()
1303	h[0], h[1] = shardID[0], shardID[1]
1304	return ConversationID(h)
1305}
1306
1307func (t ConversationIDTriple) Derivable(cid ConversationID) bool {
1308	h := t.Hash()
1309	if len(h) <= 2 || len(cid) <= 2 {
1310		return false
1311	}
1312	return bytes.Equal(h[2:], []byte(cid[2:]))
1313}
1314
1315func MakeOutboxID(s string) (OutboxID, error) {
1316	b, err := hex.DecodeString(s)
1317	return OutboxID(b), err
1318}
1319
1320func (o *OutboxID) Eq(r *OutboxID) bool {
1321	if o != nil && r != nil {
1322		return bytes.Equal(*o, *r)
1323	}
1324	return (o == nil) && (r == nil)
1325}
1326
1327func (o OutboxID) String() string {
1328	return hex.EncodeToString(o)
1329}
1330
1331func (o OutboxID) Bytes() []byte {
1332	return []byte(o)
1333}
1334
1335func (o *OutboxInfo) Eq(r *OutboxInfo) bool {
1336	if o != nil && r != nil {
1337		return *o == *r
1338	}
1339	return (o == nil) && (r == nil)
1340}
1341
1342func (o OutboxRecord) IsError() bool {
1343	state, err := o.State.State()
1344	if err != nil {
1345		return false
1346	}
1347	return state == OutboxStateType_ERROR
1348}
1349
1350func (o OutboxRecord) IsSending() bool {
1351	state, err := o.State.State()
1352	if err != nil {
1353		return false
1354	}
1355	return state == OutboxStateType_SENDING
1356}
1357
1358func (o OutboxRecord) IsAttachment() bool {
1359	return o.Msg.ClientHeader.MessageType == MessageType_ATTACHMENT
1360}
1361
1362func (o OutboxRecord) IsUnfurl() bool {
1363	return o.Msg.ClientHeader.MessageType == MessageType_UNFURL
1364}
1365
1366func (o OutboxRecord) IsChatFlip() bool {
1367	return o.Msg.ClientHeader.MessageType == MessageType_FLIP &&
1368		o.Msg.ClientHeader.Conv.TopicType == TopicType_CHAT
1369}
1370
1371func (o OutboxRecord) MessageType() MessageType {
1372	return o.Msg.ClientHeader.MessageType
1373}
1374
1375func (o OutboxRecord) IsBadgable() bool {
1376	return o.Msg.ClientHeader.Conv.TopicType == TopicType_CHAT &&
1377		o.Msg.IsBadgableType()
1378}
1379
1380func (p MessagePreviousPointer) Eq(other MessagePreviousPointer) bool {
1381	return (p.Id == other.Id) && (p.Hash.Eq(other.Hash))
1382}
1383
1384// Visibility is a helper to get around a nil pointer for visibility,
1385// and to get around TLFVisibility_ANY.  The default is PRIVATE.
1386// Note:  not sure why visibility is a pointer, or what TLFVisibility_ANY
1387// is for, but don't want to change the API.
1388func (q *GetInboxLocalQuery) Visibility() keybase1.TLFVisibility {
1389	visibility := keybase1.TLFVisibility_PRIVATE
1390	if q.TlfVisibility != nil && *q.TlfVisibility == keybase1.TLFVisibility_PUBLIC {
1391		visibility = keybase1.TLFVisibility_PUBLIC
1392	}
1393	return visibility
1394}
1395
1396// Visibility is a helper to get around a nil pointer for visibility,
1397// and to get around TLFVisibility_ANY.  The default is PRIVATE.
1398// Note:  not sure why visibility is a pointer, or what TLFVisibility_ANY
1399// is for, but don't want to change the API.
1400func (q *GetInboxQuery) Visibility() keybase1.TLFVisibility {
1401	visibility := keybase1.TLFVisibility_PRIVATE
1402	if q.TlfVisibility != nil && *q.TlfVisibility == keybase1.TLFVisibility_PUBLIC {
1403		visibility = keybase1.TLFVisibility_PUBLIC
1404	}
1405	return visibility
1406}
1407
1408// TLFNameExpanded returns a TLF name with a reset suffix if it exists.
1409// This version can be used in requests to lookup the TLF.
1410func (c ConversationInfoLocal) TLFNameExpanded() string {
1411	return ExpandTLFName(c.TlfName, c.FinalizeInfo)
1412}
1413
1414// TLFNameExpandedSummary returns a TLF name with a summary of the
1415// account reset if there was one.
1416// This version is for display purposes only and cannot be used to lookup the TLF.
1417func (c ConversationInfoLocal) TLFNameExpandedSummary() string {
1418	if c.FinalizeInfo == nil {
1419		return c.TlfName
1420	}
1421	return c.TlfName + " " + c.FinalizeInfo.BeforeSummary()
1422}
1423
1424// TLFNameExpanded returns a TLF name with a reset suffix if it exists.
1425// This version can be used in requests to lookup the TLF.
1426func (h MessageClientHeader) TLFNameExpanded(finalizeInfo *ConversationFinalizeInfo) string {
1427	return ExpandTLFName(h.TlfName, finalizeInfo)
1428}
1429
1430// TLFNameExpanded returns a TLF name with a reset suffix if it exists.
1431// This version can be used in requests to lookup the TLF.
1432func (m MessageSummary) TLFNameExpanded(finalizeInfo *ConversationFinalizeInfo) string {
1433	return ExpandTLFName(m.TlfName, finalizeInfo)
1434}
1435
1436func (h MessageClientHeaderVerified) TLFNameExpanded(finalizeInfo *ConversationFinalizeInfo) string {
1437	return ExpandTLFName(h.TlfName, finalizeInfo)
1438}
1439
1440func (h MessageClientHeader) ToVerifiedForTesting() MessageClientHeaderVerified {
1441	if flag.Lookup("test.v") == nil {
1442		panic("MessageClientHeader.ToVerifiedForTesting used outside of test")
1443	}
1444	return MessageClientHeaderVerified{
1445		Conv:         h.Conv,
1446		TlfName:      h.TlfName,
1447		TlfPublic:    h.TlfPublic,
1448		MessageType:  h.MessageType,
1449		Prev:         h.Prev,
1450		Sender:       h.Sender,
1451		SenderDevice: h.SenderDevice,
1452		OutboxID:     h.OutboxID,
1453		OutboxInfo:   h.OutboxInfo,
1454	}
1455}
1456
1457// ExpandTLFName returns a TLF name with a reset suffix if it exists.
1458// This version can be used in requests to lookup the TLF.
1459func ExpandTLFName(name string, finalizeInfo *ConversationFinalizeInfo) string {
1460	if finalizeInfo == nil {
1461		return name
1462	}
1463	if len(finalizeInfo.ResetFull) == 0 {
1464		return name
1465	}
1466	if strings.Contains(name, " account reset ") {
1467		return name
1468	}
1469	return name + " " + finalizeInfo.ResetFull
1470}
1471
1472// BeforeSummary returns a summary of the finalize without "files" in it.
1473// The canonical name for a TLF after reset has a "(files before ... account reset...)" suffix
1474// which doesn't make much sense in other uses (like chat).
1475func (f *ConversationFinalizeInfo) BeforeSummary() string {
1476	return fmt.Sprintf("(before %s account reset %s)", f.ResetUser, f.ResetDate)
1477}
1478
1479func (f *ConversationFinalizeInfo) IsResetForUser(username string) bool {
1480	// If reset user is the given user, or is blank (only way such a thing
1481	// could be in our inbox is if the current user is the one that reset)
1482	return f != nil && (f.ResetUser == username || f.ResetUser == "")
1483}
1484
1485func (p *Pagination) Eq(other *Pagination) bool {
1486	if p == nil && other == nil {
1487		return true
1488	}
1489	if p != nil && other != nil {
1490		return p.Last == other.Last && bytes.Equal(p.Next, other.Next) &&
1491			bytes.Equal(p.Previous, other.Previous) && p.Num == other.Num
1492	}
1493	return false
1494}
1495
1496func (p *Pagination) String() string {
1497	if p == nil {
1498		return "<nil>"
1499	}
1500	return fmt.Sprintf("[Num: %d n: %s p: %s last: %v]", p.Num, hex.EncodeToString(p.Next),
1501		hex.EncodeToString(p.Previous), p.Last)
1502}
1503
1504// FirstPage returns true if the pagination object is not pointing in any direction
1505func (p *Pagination) FirstPage() bool {
1506	return p == nil || p.ForceFirstPage || (len(p.Next) == 0 && len(p.Previous) == 0)
1507}
1508
1509func (c ConversationLocal) GetMtime() gregor1.Time {
1510	return c.ReaderInfo.Mtime
1511}
1512
1513func (c ConversationLocal) GetConvID() ConversationID {
1514	return c.Info.Id
1515}
1516
1517func (c ConversationLocal) GetTopicType() TopicType {
1518	return c.Info.Triple.TopicType
1519}
1520
1521func (c ConversationLocal) GetMembersType() ConversationMembersType {
1522	return c.Info.MembersType
1523}
1524
1525func (c ConversationLocal) GetTeamType() TeamType {
1526	return c.Info.TeamType
1527}
1528
1529func (c ConversationLocal) GetFinalizeInfo() *ConversationFinalizeInfo {
1530	return c.Info.FinalizeInfo
1531}
1532
1533func (c ConversationLocal) GetTopicName() string {
1534	return c.Info.TopicName
1535}
1536
1537func (c ConversationLocal) GetExpunge() *Expunge {
1538	return &c.Expunge
1539}
1540
1541func (c ConversationLocal) IsPublic() bool {
1542	return c.Info.Visibility == keybase1.TLFVisibility_PUBLIC
1543}
1544
1545func (c ConversationLocal) GetMaxMessage(typ MessageType) (MessageSummary, error) {
1546	for _, msg := range c.MaxMessages {
1547		if msg.GetMessageType() == typ {
1548			return msg, nil
1549		}
1550	}
1551	return MessageSummary{}, fmt.Errorf("max message not found: %v", typ)
1552}
1553
1554func (c ConversationLocal) GetMaxDeletedUpTo() MessageID {
1555	var maxExpungeID, maxDelHID MessageID
1556	if expunge := c.GetExpunge(); expunge != nil {
1557		maxExpungeID = expunge.Upto
1558	}
1559	if maxDelH, err := c.GetMaxMessage(MessageType_DELETEHISTORY); err != nil {
1560		maxDelHID = maxDelH.GetMessageID()
1561	}
1562	if maxExpungeID > maxDelHID {
1563		return maxExpungeID
1564	}
1565	return maxDelHID
1566}
1567
1568func maxVisibleMsgIDFromSummaries(maxMessages []MessageSummary) MessageID {
1569	visibleTyps := VisibleChatMessageTypes()
1570	visibleTypsMap := map[MessageType]bool{}
1571	for _, typ := range visibleTyps {
1572		visibleTypsMap[typ] = true
1573	}
1574	maxMsgID := MessageID(0)
1575	for _, msg := range maxMessages {
1576		if _, ok := visibleTypsMap[msg.GetMessageType()]; ok && msg.GetMessageID() > maxMsgID {
1577			maxMsgID = msg.GetMessageID()
1578		}
1579	}
1580	return maxMsgID
1581}
1582
1583func (c ConversationLocal) MaxVisibleMsgID() MessageID {
1584	return maxVisibleMsgIDFromSummaries(c.MaxMessages)
1585}
1586
1587func (c ConversationLocal) ConvNameNames() (res []string) {
1588	for _, p := range c.Info.Participants {
1589		if p.InConvName {
1590			res = append(res, p.Username)
1591		}
1592	}
1593	return res
1594}
1595
1596func (c ConversationLocal) AllNames() (res []string) {
1597	for _, p := range c.Info.Participants {
1598		res = append(res, p.Username)
1599	}
1600	return res
1601}
1602
1603func (c ConversationLocal) FullNamesForSearch() (res []*string) {
1604	for _, p := range c.Info.Participants {
1605		res = append(res, p.Fullname)
1606	}
1607	return res
1608}
1609
1610func (c ConversationLocal) CannotWrite() bool {
1611	if c.ConvSettings == nil || c.ConvSettings.MinWriterRoleInfo == nil {
1612		return false
1613	}
1614
1615	return c.ConvSettings.MinWriterRoleInfo.CannotWrite
1616}
1617
1618func (c Conversation) CannotWrite() bool {
1619	if c.ConvSettings == nil || c.ConvSettings.MinWriterRoleInfo == nil || c.ReaderInfo == nil {
1620		return false
1621	}
1622
1623	return !c.ReaderInfo.UntrustedTeamRole.IsOrAbove(c.ConvSettings.MinWriterRoleInfo.Role)
1624}
1625
1626func (c Conversation) GetMtime() gregor1.Time {
1627	return c.ReaderInfo.Mtime
1628}
1629
1630func (c Conversation) GetConvID() ConversationID {
1631	return c.Metadata.ConversationID
1632}
1633
1634func (c Conversation) GetTopicType() TopicType {
1635	return c.Metadata.IdTriple.TopicType
1636}
1637
1638func (c Conversation) GetMembersType() ConversationMembersType {
1639	return c.Metadata.MembersType
1640}
1641
1642func (c Conversation) GetTeamType() TeamType {
1643	return c.Metadata.TeamType
1644}
1645
1646func (c Conversation) GetFinalizeInfo() *ConversationFinalizeInfo {
1647	return c.Metadata.FinalizeInfo
1648}
1649
1650func (c Conversation) GetExpunge() *Expunge {
1651	return &c.Expunge
1652}
1653
1654func (c Conversation) IsPublic() bool {
1655	return c.Metadata.Visibility == keybase1.TLFVisibility_PUBLIC
1656}
1657
1658var errMaxMessageNotFound = errors.New("max message not found")
1659
1660func (c Conversation) GetMaxMessage(typ MessageType) (MessageSummary, error) {
1661	for _, msg := range c.MaxMsgSummaries {
1662		if msg.GetMessageType() == typ {
1663			return msg, nil
1664		}
1665	}
1666	return MessageSummary{}, errMaxMessageNotFound
1667}
1668
1669func (c Conversation) Includes(uid gregor1.UID) bool {
1670	for _, auid := range c.Metadata.ActiveList {
1671		if uid.Eq(auid) {
1672			return true
1673		}
1674	}
1675	return false
1676}
1677
1678func (c Conversation) GetMaxDeletedUpTo() MessageID {
1679	var maxExpungeID, maxDelHID MessageID
1680	if expunge := c.GetExpunge(); expunge != nil {
1681		maxExpungeID = expunge.Upto
1682	}
1683	if maxDelH, err := c.GetMaxMessage(MessageType_DELETEHISTORY); err != nil {
1684		maxDelHID = maxDelH.GetMessageID()
1685	}
1686	if maxExpungeID > maxDelHID {
1687		return maxExpungeID
1688	}
1689	return maxDelHID
1690}
1691
1692func (c Conversation) GetMaxMessageID() MessageID {
1693	maxMsgID := MessageID(0)
1694	for _, msg := range c.MaxMsgSummaries {
1695		if msg.GetMessageID() > maxMsgID {
1696			maxMsgID = msg.GetMessageID()
1697		}
1698	}
1699	return maxMsgID
1700}
1701
1702func (c Conversation) IsSelfFinalized(username string) bool {
1703	return c.GetMembersType() == ConversationMembersType_KBFS && c.GetFinalizeInfo().IsResetForUser(username)
1704}
1705
1706func (c Conversation) MaxVisibleMsgID() MessageID {
1707	return maxVisibleMsgIDFromSummaries(c.MaxMsgSummaries)
1708}
1709
1710func (c Conversation) IsUnread() bool {
1711	return c.IsUnreadFromMsgID(c.ReaderInfo.ReadMsgid)
1712}
1713
1714func (c Conversation) IsUnreadFromMsgID(readMsgID MessageID) bool {
1715	maxMsgID := c.MaxVisibleMsgID()
1716	return maxMsgID > 0 && maxMsgID > readMsgID
1717}
1718
1719func (c Conversation) HasMemberStatus(status ConversationMemberStatus) bool {
1720	if c.ReaderInfo != nil {
1721		return c.ReaderInfo.Status == status
1722	}
1723	return false
1724}
1725
1726func (m MessageSummary) GetMessageID() MessageID {
1727	return m.MsgID
1728}
1729
1730func (m MessageSummary) GetMessageType() MessageType {
1731	return m.MessageType
1732}
1733
1734/*
1735func ConvertMessageBodyV1ToV2(v1 MessageBodyV1) (MessageBody, error) {
1736	t, err := v1.MessageType()
1737	if err != nil {
1738		return MessageBody{}, err
1739	}
1740	switch t {
1741	case MessageType_TEXT:
1742		return NewMessageBodyWithText(v1.Text()), nil
1743	case MessageType_ATTACHMENT:
1744		previous := v1.Attachment()
1745		upgraded := MessageAttachment{
1746			Object:   previous.Object,
1747			Metadata: previous.Metadata,
1748			Uploaded: true,
1749		}
1750		if previous.Preview != nil {
1751			upgraded.Previews = []Asset{*previous.Preview}
1752		}
1753		return NewMessageBodyWithAttachment(upgraded), nil
1754	case MessageType_EDIT:
1755		return NewMessageBodyWithEdit(v1.Edit()), nil
1756	case MessageType_DELETE:
1757		return NewMessageBodyWithDelete(v1.Delete()), nil
1758	case MessageType_METADATA:
1759		return NewMessageBodyWithMetadata(v1.Metadata()), nil
1760	case MessageType_HEADLINE:
1761		return NewMessageBodyWithHeadline(v1.Headline()), nil
1762	case MessageType_NONE:
1763		return MessageBody{MessageType__: MessageType_NONE}, nil
1764	}
1765
1766	return MessageBody{}, fmt.Errorf("ConvertMessageBodyV1ToV2: unhandled message type %v", t)
1767}
1768*/
1769
1770func (a *MerkleRoot) Eq(b *MerkleRoot) bool {
1771	if a != nil && b != nil {
1772		return (a.Seqno == b.Seqno) && bytes.Equal(a.Hash, b.Hash)
1773	}
1774	return (a == nil) && (b == nil)
1775}
1776
1777func (d *SealedData) AsEncrypted() EncryptedData {
1778	return EncryptedData{
1779		V: d.V,
1780		E: d.E,
1781		N: d.N,
1782	}
1783}
1784
1785func (d *SealedData) AsSignEncrypted() SignEncryptedData {
1786	return SignEncryptedData{
1787		V: d.V,
1788		E: d.E,
1789		N: d.N,
1790	}
1791}
1792
1793func (d *EncryptedData) AsSealed() SealedData {
1794	return SealedData{
1795		V: d.V,
1796		E: d.E,
1797		N: d.N,
1798	}
1799}
1800
1801func (d *SignEncryptedData) AsSealed() SealedData {
1802	return SealedData{
1803		V: d.V,
1804		E: d.E,
1805		N: d.N,
1806	}
1807}
1808
1809func NewConversationErrorLocal(
1810	message string,
1811	remoteConv Conversation,
1812	unverifiedTLFName string,
1813	typ ConversationErrorType,
1814	rekeyInfo *ConversationErrorRekey,
1815) *ConversationErrorLocal {
1816	return &ConversationErrorLocal{
1817		Typ:               typ,
1818		Message:           message,
1819		RemoteConv:        remoteConv,
1820		UnverifiedTLFName: unverifiedTLFName,
1821		RekeyInfo:         rekeyInfo,
1822	}
1823}
1824
1825type OfflinableResult interface {
1826	SetOffline()
1827}
1828
1829func (r *NonblockFetchRes) SetOffline() {
1830	r.Offline = true
1831}
1832
1833func (r *UnreadlineRes) SetOffline() {
1834	r.Offline = true
1835}
1836
1837func (r *MarkAsReadLocalRes) SetOffline() {
1838	r.Offline = true
1839}
1840
1841func (r *MarkTLFAsReadLocalRes) SetOffline() {
1842	r.Offline = true
1843}
1844
1845func (r *GetInboxAndUnboxLocalRes) SetOffline() {
1846	r.Offline = true
1847}
1848
1849func (r *GetInboxAndUnboxUILocalRes) SetOffline() {
1850	r.Offline = true
1851}
1852
1853func (r *GetThreadLocalRes) SetOffline() {
1854	r.Offline = true
1855}
1856
1857func (r *GetInboxSummaryForCLILocalRes) SetOffline() {
1858	r.Offline = true
1859}
1860
1861func (r *GetConversationForCLILocalRes) SetOffline() {
1862	r.Offline = true
1863}
1864
1865func (r *GetMessagesLocalRes) SetOffline() {
1866	r.Offline = true
1867}
1868
1869func (r *GetNextAttachmentMessageLocalRes) SetOffline() {
1870	r.Offline = true
1871}
1872
1873func (r *FindConversationsLocalRes) SetOffline() {
1874	r.Offline = true
1875}
1876
1877func (r *JoinLeaveConversationLocalRes) SetOffline() {
1878	r.Offline = true
1879}
1880
1881func (r *PreviewConversationLocalRes) SetOffline() {
1882	r.Offline = true
1883}
1884
1885func (r *GetTLFConversationsLocalRes) SetOffline() {
1886	r.Offline = true
1887}
1888
1889func (r *GetChannelMembershipsLocalRes) SetOffline() {
1890	r.Offline = true
1891}
1892
1893func (r *GetMutualTeamsLocalRes) SetOffline() {
1894	r.Offline = true
1895}
1896
1897func (r *SetAppNotificationSettingsLocalRes) SetOffline() {
1898	r.Offline = true
1899}
1900
1901func (r *DeleteConversationLocalRes) SetOffline() {
1902	r.Offline = true
1903}
1904
1905func (r *SearchRegexpRes) SetOffline() {
1906	r.Offline = true
1907}
1908
1909func (r *SearchInboxRes) SetOffline() {
1910	r.Offline = true
1911}
1912
1913func (t TyperInfo) String() string {
1914	return fmt.Sprintf("typer(u:%s d:%s)", t.Username, t.DeviceID)
1915}
1916
1917func (o TLFConvOrdinal) Int() int {
1918	return int(o)
1919}
1920
1921func (o TLFConvOrdinal) IsFirst() bool {
1922	return o.Int() == 1
1923}
1924
1925func MakeEmptyUnreadUpdate(convID ConversationID) UnreadUpdate {
1926	counts := make(map[keybase1.DeviceType]int)
1927	counts[keybase1.DeviceType_DESKTOP] = 0
1928	counts[keybase1.DeviceType_MOBILE] = 0
1929	return UnreadUpdate{
1930		ConvID:                  convID,
1931		UnreadMessages:          0,
1932		UnreadNotifyingMessages: counts,
1933	}
1934}
1935
1936func (u UnreadUpdate) String() string {
1937	return fmt.Sprintf("[d:%v c:%s u:%d nd:%d nm:%d]", u.Diff, u.ConvID, u.UnreadMessages,
1938		u.UnreadNotifyingMessages[keybase1.DeviceType_DESKTOP],
1939		u.UnreadNotifyingMessages[keybase1.DeviceType_MOBILE])
1940}
1941
1942func (s TopicNameState) Bytes() []byte {
1943	return []byte(s)
1944}
1945
1946func (s TopicNameState) Eq(o TopicNameState) bool {
1947	return bytes.Equal(s.Bytes(), o.Bytes())
1948}
1949
1950func (i InboxUIItem) GetConvID() ConversationID {
1951	bConvID, _ := hex.DecodeString(i.ConvID.String())
1952	return ConversationID(bConvID)
1953}
1954
1955type ByConversationMemberStatus []ConversationMemberStatus
1956
1957func (m ByConversationMemberStatus) Len() int           { return len(m) }
1958func (m ByConversationMemberStatus) Swap(i, j int)      { m[i], m[j] = m[j], m[i] }
1959func (m ByConversationMemberStatus) Less(i, j int) bool { return m[i] > m[j] }
1960
1961func AllConversationMemberStatuses() (res []ConversationMemberStatus) {
1962	for status := range ConversationMemberStatusRevMap {
1963		res = append(res, status)
1964	}
1965	sort.Sort(ByConversationMemberStatus(res))
1966	return res
1967}
1968
1969type ByConversationExistence []ConversationExistence
1970
1971func (m ByConversationExistence) Len() int           { return len(m) }
1972func (m ByConversationExistence) Swap(i, j int)      { m[i], m[j] = m[j], m[i] }
1973func (m ByConversationExistence) Less(i, j int) bool { return m[i] > m[j] }
1974
1975func AllConversationExistences() (res []ConversationExistence) {
1976	for existence := range ConversationExistenceRevMap {
1977		res = append(res, existence)
1978	}
1979	sort.Sort(ByConversationExistence(res))
1980	return res
1981}
1982
1983func (v InboxVers) ToConvVers() ConversationVers {
1984	return ConversationVers(v)
1985}
1986
1987func (p ConversationIDMessageIDPairs) Contains(convID ConversationID) (MessageID, bool) {
1988	for _, c := range p.Pairs {
1989		if c.ConvID.Eq(convID) {
1990			return c.MsgID, true
1991		}
1992	}
1993	return MessageID(0), false
1994}
1995
1996func (c ConversationMemberStatus) ToGregorDBString() (string, error) {
1997	s, ok := ConversationMemberStatusRevMap[c]
1998	if !ok {
1999		return "", fmt.Errorf("unrecoginzed ConversationMemberStatus: %v", c)
2000	}
2001	return strings.ToLower(s), nil
2002}
2003
2004func (c ConversationMemberStatus) ToGregorDBStringAssert() string {
2005	s, err := c.ToGregorDBString()
2006	if err != nil {
2007		panic(err)
2008	}
2009	return s
2010}
2011
2012func humanizeDuration(duration time.Duration) string {
2013	var value float64
2014	var unit string
2015	if int(duration.Hours()) >= 24 {
2016		value = duration.Hours() / 24
2017		unit = "day"
2018	} else if int(duration.Hours()) >= 1 {
2019		value = duration.Hours()
2020		unit = "hour"
2021	} else if int(duration.Minutes()) >= 1 {
2022		value = duration.Minutes()
2023		unit = "minute"
2024	} else if int(duration.Seconds()) >= 1 {
2025		value = duration.Seconds()
2026		unit = "second"
2027	} else {
2028		return ""
2029	}
2030	if int(value) > 1 {
2031		unit = unit + "s"
2032	}
2033	return fmt.Sprintf("%.0f %s", value, unit)
2034}
2035
2036func (p RetentionPolicy) Eq(o RetentionPolicy) bool {
2037	typ1, err := p.Typ()
2038	if err != nil {
2039		return false
2040	}
2041
2042	typ2, err := o.Typ()
2043	if err != nil {
2044		return false
2045	}
2046	if typ1 != typ2 {
2047		return false
2048	}
2049	switch typ1 {
2050	case RetentionPolicyType_NONE:
2051		return true
2052	case RetentionPolicyType_RETAIN:
2053		return p.Retain() == o.Retain()
2054	case RetentionPolicyType_EXPIRE:
2055		return p.Expire() == o.Expire()
2056	case RetentionPolicyType_INHERIT:
2057		return p.Inherit() == o.Inherit()
2058	case RetentionPolicyType_EPHEMERAL:
2059		return p.Ephemeral() == o.Ephemeral()
2060	default:
2061		return false
2062	}
2063}
2064
2065func (p RetentionPolicy) HumanSummary() (summary string) {
2066	typ, err := p.Typ()
2067	if err != nil {
2068		return ""
2069	}
2070
2071	switch typ {
2072	case RetentionPolicyType_NONE, RetentionPolicyType_RETAIN:
2073		summary = "be retained indefinitely"
2074	case RetentionPolicyType_EXPIRE:
2075		duration := humanizeDuration(p.Expire().Age.ToDuration())
2076		if duration != "" {
2077			summary = fmt.Sprintf("expire after %s", duration)
2078		}
2079	case RetentionPolicyType_EPHEMERAL:
2080		duration := humanizeDuration(p.Ephemeral().Age.ToDuration())
2081		if duration != "" {
2082			summary = fmt.Sprintf("explode after %s by default", duration)
2083		}
2084	}
2085	if summary != "" {
2086		summary = fmt.Sprintf("Messages will %s", summary)
2087	}
2088	return summary
2089}
2090
2091func (p RetentionPolicy) Summary() string {
2092	typ, err := p.Typ()
2093	if err != nil {
2094		return "{variant error}"
2095	}
2096	switch typ {
2097	case RetentionPolicyType_EXPIRE:
2098		return fmt.Sprintf("{%v age:%v}", typ, p.Expire().Age.ToDuration())
2099	case RetentionPolicyType_EPHEMERAL:
2100		return fmt.Sprintf("{%v age:%v}", typ, p.Ephemeral().Age.ToDuration())
2101	default:
2102		return fmt.Sprintf("{%v}", typ)
2103	}
2104}
2105
2106func TeamIDToTLFID(teamID keybase1.TeamID) (TLFID, error) {
2107	return MakeTLFID(teamID.String())
2108}
2109
2110func (r *NonblockFetchRes) GetRateLimit() []RateLimit {
2111	return r.RateLimits
2112}
2113
2114func (r *NonblockFetchRes) SetRateLimits(rl []RateLimit) {
2115	r.RateLimits = rl
2116}
2117
2118func (r *UnreadlineRes) GetRateLimit() []RateLimit {
2119	return r.RateLimits
2120}
2121
2122func (r *UnreadlineRes) SetRateLimits(rl []RateLimit) {
2123	r.RateLimits = rl
2124}
2125
2126func (r *MarkAsReadLocalRes) GetRateLimit() []RateLimit {
2127	return r.RateLimits
2128}
2129
2130func (r *MarkAsReadLocalRes) SetRateLimits(rl []RateLimit) {
2131	r.RateLimits = rl
2132}
2133
2134func (r *MarkTLFAsReadLocalRes) GetRateLimit() (res []RateLimit) {
2135	return r.RateLimits
2136}
2137
2138func (r *MarkTLFAsReadLocalRes) SetRateLimits(rl []RateLimit) {
2139	r.RateLimits = rl
2140}
2141
2142func (r *GetInboxAndUnboxLocalRes) GetRateLimit() []RateLimit {
2143	return r.RateLimits
2144}
2145
2146func (r *GetInboxAndUnboxLocalRes) SetRateLimits(rl []RateLimit) {
2147	r.RateLimits = rl
2148}
2149
2150func (r *GetAllResetConvMembersRes) GetRateLimit() []RateLimit {
2151	return r.RateLimits
2152}
2153
2154func (r *GetAllResetConvMembersRes) SetRateLimits(rl []RateLimit) {
2155	r.RateLimits = rl
2156}
2157
2158func (r *LoadFlipRes) GetRateLimit() []RateLimit {
2159	return r.RateLimits
2160}
2161
2162func (r *LoadFlipRes) SetRateLimits(rl []RateLimit) {
2163	r.RateLimits = rl
2164}
2165
2166func (r *GetInboxAndUnboxUILocalRes) GetRateLimit() []RateLimit {
2167	return r.RateLimits
2168}
2169
2170func (r *GetInboxAndUnboxUILocalRes) SetRateLimits(rl []RateLimit) {
2171	r.RateLimits = rl
2172}
2173
2174func (r *GetThreadLocalRes) GetRateLimit() []RateLimit {
2175	return r.RateLimits
2176}
2177
2178func (r *GetThreadLocalRes) SetRateLimits(rl []RateLimit) {
2179	r.RateLimits = rl
2180}
2181
2182func (r *NewConversationLocalRes) GetRateLimit() []RateLimit {
2183	return r.RateLimits
2184}
2185
2186func (r *NewConversationLocalRes) SetRateLimits(rl []RateLimit) {
2187	r.RateLimits = rl
2188}
2189
2190func (r *GetInboxSummaryForCLILocalRes) GetRateLimit() []RateLimit {
2191	return r.RateLimits
2192}
2193
2194func (r *GetInboxSummaryForCLILocalRes) SetRateLimits(rl []RateLimit) {
2195	r.RateLimits = rl
2196}
2197
2198func (r *GetMessagesLocalRes) GetRateLimit() []RateLimit {
2199	return r.RateLimits
2200}
2201
2202func (r *GetMessagesLocalRes) SetRateLimits(rl []RateLimit) {
2203	r.RateLimits = rl
2204}
2205
2206func (r *GetNextAttachmentMessageLocalRes) GetRateLimit() []RateLimit {
2207	return r.RateLimits
2208}
2209
2210func (r *GetNextAttachmentMessageLocalRes) SetRateLimits(rl []RateLimit) {
2211	r.RateLimits = rl
2212}
2213
2214func (r *SetConversationStatusLocalRes) GetRateLimit() []RateLimit {
2215	return r.RateLimits
2216}
2217
2218func (r *SetConversationStatusLocalRes) SetRateLimits(rl []RateLimit) {
2219	r.RateLimits = rl
2220}
2221
2222func (r *PostLocalRes) GetRateLimit() []RateLimit {
2223	return r.RateLimits
2224}
2225
2226func (r *PostLocalRes) SetRateLimits(rl []RateLimit) {
2227	r.RateLimits = rl
2228}
2229
2230func (r *GetConversationForCLILocalRes) GetRateLimit() []RateLimit {
2231	return r.RateLimits
2232}
2233
2234func (r *GetConversationForCLILocalRes) SetRateLimits(rl []RateLimit) {
2235	r.RateLimits = rl
2236}
2237
2238func (r *PostLocalNonblockRes) GetRateLimit() []RateLimit {
2239	return r.RateLimits
2240}
2241
2242func (r *PostLocalNonblockRes) SetRateLimits(rl []RateLimit) {
2243	r.RateLimits = rl
2244}
2245
2246func (r *DownloadAttachmentLocalRes) GetRateLimit() []RateLimit {
2247	return r.RateLimits
2248}
2249
2250func (r *DownloadAttachmentLocalRes) SetRateLimits(rl []RateLimit) {
2251	r.RateLimits = rl
2252}
2253
2254func (r *DownloadFileAttachmentLocalRes) GetRateLimit() []RateLimit {
2255	return r.RateLimits
2256}
2257
2258func (r *DownloadFileAttachmentLocalRes) SetRateLimits(rl []RateLimit) {
2259	r.RateLimits = rl
2260}
2261
2262func (r *FindConversationsLocalRes) GetRateLimit() []RateLimit {
2263	return r.RateLimits
2264}
2265
2266func (r *FindConversationsLocalRes) SetRateLimits(rl []RateLimit) {
2267	r.RateLimits = rl
2268}
2269
2270func (r *JoinLeaveConversationLocalRes) GetRateLimit() []RateLimit {
2271	return r.RateLimits
2272}
2273
2274func (r *JoinLeaveConversationLocalRes) SetRateLimits(rl []RateLimit) {
2275	r.RateLimits = rl
2276}
2277
2278func (r *PreviewConversationLocalRes) GetRateLimit() []RateLimit {
2279	return r.RateLimits
2280}
2281
2282func (r *PreviewConversationLocalRes) SetRateLimits(rl []RateLimit) {
2283	r.RateLimits = rl
2284}
2285
2286func (r *DeleteConversationLocalRes) GetRateLimit() []RateLimit {
2287	return r.RateLimits
2288}
2289
2290func (r *DeleteConversationLocalRes) SetRateLimits(rl []RateLimit) {
2291	r.RateLimits = rl
2292}
2293
2294func (r *RemoveFromConversationLocalRes) GetRateLimit() []RateLimit {
2295	return r.RateLimits
2296}
2297
2298func (r *RemoveFromConversationLocalRes) SetRateLimits(rl []RateLimit) {
2299	r.RateLimits = rl
2300}
2301
2302func (r *GetTLFConversationsLocalRes) GetRateLimit() []RateLimit {
2303	return r.RateLimits
2304}
2305
2306func (r *GetTLFConversationsLocalRes) SetRateLimits(rl []RateLimit) {
2307	r.RateLimits = rl
2308}
2309
2310func (r *GetChannelMembershipsLocalRes) GetRateLimit() []RateLimit {
2311	return r.RateLimits
2312}
2313
2314func (r *GetChannelMembershipsLocalRes) SetRateLimits(rl []RateLimit) {
2315	r.RateLimits = rl
2316}
2317
2318func (r *GetMutualTeamsLocalRes) GetRateLimit() []RateLimit {
2319	return r.RateLimits
2320}
2321
2322func (r *GetMutualTeamsLocalRes) SetRateLimits(rl []RateLimit) {
2323	r.RateLimits = rl
2324}
2325
2326func (r *SetAppNotificationSettingsLocalRes) GetRateLimit() []RateLimit {
2327	return r.RateLimits
2328}
2329
2330func (r *SetAppNotificationSettingsLocalRes) SetRateLimits(rl []RateLimit) {
2331	r.RateLimits = rl
2332}
2333
2334func (r *SearchRegexpRes) GetRateLimit() []RateLimit {
2335	return r.RateLimits
2336}
2337
2338func (r *SearchRegexpRes) SetRateLimits(rl []RateLimit) {
2339	r.RateLimits = rl
2340}
2341
2342func (r *SearchInboxRes) GetRateLimit() []RateLimit {
2343	return r.RateLimits
2344}
2345
2346func (r *SearchInboxRes) SetRateLimits(rl []RateLimit) {
2347	r.RateLimits = rl
2348}
2349
2350func (r *GetInboxRemoteRes) GetRateLimit() (res []RateLimit) {
2351	if r.RateLimit != nil {
2352		res = []RateLimit{*r.RateLimit}
2353	}
2354	return res
2355}
2356
2357func (r *GetInboxRemoteRes) SetRateLimits(rl []RateLimit) {
2358	if len(rl) > 0 {
2359		r.RateLimit = &rl[0]
2360	}
2361}
2362
2363func (r *GetInboxByTLFIDRemoteRes) GetRateLimit() (res []RateLimit) {
2364	if r.RateLimit != nil {
2365		res = []RateLimit{*r.RateLimit}
2366	}
2367	return res
2368}
2369
2370func (r *GetInboxByTLFIDRemoteRes) SetRateLimits(rl []RateLimit) {
2371	if len(rl) > 0 {
2372		r.RateLimit = &rl[0]
2373	}
2374}
2375
2376func (r *GetThreadRemoteRes) GetRateLimit() (res []RateLimit) {
2377	if r.RateLimit != nil {
2378		res = []RateLimit{*r.RateLimit}
2379	}
2380	return res
2381}
2382
2383func (r *GetThreadRemoteRes) SetRateLimits(rl []RateLimit) {
2384	if len(rl) > 0 {
2385		r.RateLimit = &rl[0]
2386	}
2387}
2388
2389func (r *GetConversationMetadataRemoteRes) GetRateLimit() (res []RateLimit) {
2390	if r.RateLimit != nil {
2391		res = []RateLimit{*r.RateLimit}
2392	}
2393	return res
2394}
2395
2396func (r *GetConversationMetadataRemoteRes) SetRateLimits(rl []RateLimit) {
2397	if len(rl) > 0 {
2398		r.RateLimit = &rl[0]
2399	}
2400}
2401
2402func (r *PostRemoteRes) GetRateLimit() (res []RateLimit) {
2403	if r.RateLimit != nil {
2404		res = []RateLimit{*r.RateLimit}
2405	}
2406	return res
2407}
2408
2409func (r *PostRemoteRes) SetRateLimits(rl []RateLimit) {
2410	if len(rl) > 0 {
2411		r.RateLimit = &rl[0]
2412	}
2413}
2414
2415func (r *NewConversationRemoteRes) GetRateLimit() (res []RateLimit) {
2416	if r.RateLimit != nil {
2417		res = []RateLimit{*r.RateLimit}
2418	}
2419	return res
2420}
2421
2422func (r *NewConversationRemoteRes) SetRateLimits(rl []RateLimit) {
2423	if len(rl) > 0 {
2424		r.RateLimit = &rl[0]
2425	}
2426}
2427
2428func (r *GetMessagesRemoteRes) GetRateLimit() (res []RateLimit) {
2429	if r.RateLimit != nil {
2430		res = []RateLimit{*r.RateLimit}
2431	}
2432	return res
2433}
2434
2435func (r *GetMessagesRemoteRes) SetRateLimits(rl []RateLimit) {
2436	if len(rl) > 0 {
2437		r.RateLimit = &rl[0]
2438	}
2439}
2440
2441func (r *MarkAsReadRes) GetRateLimit() (res []RateLimit) {
2442	if r.RateLimit != nil {
2443		res = []RateLimit{*r.RateLimit}
2444	}
2445	return res
2446}
2447
2448func (r *MarkAsReadRes) SetRateLimits(rl []RateLimit) {
2449	if len(rl) > 0 {
2450		r.RateLimit = &rl[0]
2451	}
2452}
2453
2454func (r *SetConversationStatusRes) GetRateLimit() (res []RateLimit) {
2455	if r.RateLimit != nil {
2456		res = []RateLimit{*r.RateLimit}
2457	}
2458	return res
2459}
2460
2461func (r *SetConversationStatusRes) SetRateLimits(rl []RateLimit) {
2462	if len(rl) > 0 {
2463		r.RateLimit = &rl[0]
2464	}
2465}
2466
2467func (r *GetPublicConversationsRes) GetRateLimit() (res []RateLimit) {
2468	if r.RateLimit != nil {
2469		res = []RateLimit{*r.RateLimit}
2470	}
2471	return res
2472}
2473
2474func (r *GetPublicConversationsRes) SetRateLimits(rl []RateLimit) {
2475	if len(rl) > 0 {
2476		r.RateLimit = &rl[0]
2477	}
2478}
2479
2480func (r *JoinLeaveConversationRemoteRes) GetRateLimit() (res []RateLimit) {
2481	if r.RateLimit != nil {
2482		res = []RateLimit{*r.RateLimit}
2483	}
2484	return res
2485}
2486
2487func (r *JoinLeaveConversationRemoteRes) SetRateLimits(rl []RateLimit) {
2488	if len(rl) > 0 {
2489		r.RateLimit = &rl[0]
2490	}
2491}
2492
2493func (r *DeleteConversationRemoteRes) GetRateLimit() (res []RateLimit) {
2494	if r.RateLimit != nil {
2495		res = []RateLimit{*r.RateLimit}
2496	}
2497	return res
2498}
2499
2500func (r *DeleteConversationRemoteRes) SetRateLimits(rl []RateLimit) {
2501	if len(rl) > 0 {
2502		r.RateLimit = &rl[0]
2503	}
2504}
2505
2506func (r *RemoveFromConversationRemoteRes) GetRateLimit() (res []RateLimit) {
2507	if r.RateLimit != nil {
2508		res = []RateLimit{*r.RateLimit}
2509	}
2510	return res
2511}
2512
2513func (r *RemoveFromConversationRemoteRes) SetRateLimits(rl []RateLimit) {
2514	if len(rl) > 0 {
2515		r.RateLimit = &rl[0]
2516	}
2517}
2518
2519func (r *GetMessageBeforeRes) GetRateLimit() (res []RateLimit) {
2520	if r.RateLimit != nil {
2521		res = []RateLimit{*r.RateLimit}
2522	}
2523	return res
2524}
2525
2526func (r *GetMessageBeforeRes) SetRateLimits(rl []RateLimit) {
2527	if len(rl) > 0 {
2528		r.RateLimit = &rl[0]
2529	}
2530}
2531
2532func (r *GetTLFConversationsRes) GetRateLimit() (res []RateLimit) {
2533	if r.RateLimit != nil {
2534		res = []RateLimit{*r.RateLimit}
2535	}
2536	return res
2537}
2538
2539func (r *GetTLFConversationsRes) SetRateLimits(rl []RateLimit) {
2540	if len(rl) > 0 {
2541		r.RateLimit = &rl[0]
2542	}
2543}
2544
2545func (r *SetAppNotificationSettingsRes) GetRateLimit() (res []RateLimit) {
2546	if r.RateLimit != nil {
2547		res = []RateLimit{*r.RateLimit}
2548	}
2549	return res
2550}
2551
2552func (r *SetAppNotificationSettingsRes) SetRateLimits(rl []RateLimit) {
2553	if len(rl) > 0 {
2554		r.RateLimit = &rl[0]
2555	}
2556}
2557
2558func (r *SetRetentionRes) GetRateLimit() (res []RateLimit) {
2559	if r.RateLimit != nil {
2560		res = []RateLimit{*r.RateLimit}
2561	}
2562	return res
2563}
2564
2565func (r *SetRetentionRes) SetRateLimits(rl []RateLimit) {
2566	if len(rl) > 0 {
2567		r.RateLimit = &rl[0]
2568	}
2569}
2570
2571func (r *LoadGalleryRes) GetRateLimit() []RateLimit {
2572	return r.RateLimits
2573}
2574
2575func (r *LoadGalleryRes) SetRateLimits(rl []RateLimit) {
2576	r.RateLimits = rl
2577}
2578
2579func (r *ListBotCommandsLocalRes) GetRateLimit() []RateLimit {
2580	return r.RateLimits
2581}
2582
2583func (r *ListBotCommandsLocalRes) SetRateLimits(rl []RateLimit) {
2584	r.RateLimits = rl
2585}
2586
2587func (r *PinMessageRes) GetRateLimit() []RateLimit {
2588	return r.RateLimits
2589}
2590
2591func (r *PinMessageRes) SetRateLimits(rl []RateLimit) {
2592	r.RateLimits = rl
2593}
2594
2595func (r *ClearBotCommandsLocalRes) GetRateLimit() []RateLimit {
2596	return r.RateLimits
2597}
2598
2599func (r *ClearBotCommandsLocalRes) SetRateLimits(rl []RateLimit) {
2600	r.RateLimits = rl
2601}
2602
2603func (r *ClearBotCommandsRes) GetRateLimit() (res []RateLimit) {
2604	if r.RateLimit != nil {
2605		res = []RateLimit{*r.RateLimit}
2606	}
2607	return res
2608}
2609
2610func (r *ClearBotCommandsRes) SetRateLimits(rl []RateLimit) {
2611	if len(rl) > 0 {
2612		r.RateLimit = &rl[0]
2613	}
2614}
2615
2616func (r *AdvertiseBotCommandsLocalRes) GetRateLimit() []RateLimit {
2617	return r.RateLimits
2618}
2619
2620func (r *AdvertiseBotCommandsLocalRes) SetRateLimits(rl []RateLimit) {
2621	r.RateLimits = rl
2622}
2623
2624func (r *AdvertiseBotCommandsRes) GetRateLimit() (res []RateLimit) {
2625	if r.RateLimit != nil {
2626		res = []RateLimit{*r.RateLimit}
2627	}
2628	return res
2629}
2630
2631func (r *AdvertiseBotCommandsRes) SetRateLimits(rl []RateLimit) {
2632	if len(rl) > 0 {
2633		r.RateLimit = &rl[0]
2634	}
2635}
2636
2637func (r *GetBotInfoRes) GetRateLimit() (res []RateLimit) {
2638	if r.RateLimit != nil {
2639		res = []RateLimit{*r.RateLimit}
2640	}
2641	return res
2642}
2643
2644func (r *GetBotInfoRes) SetRateLimits(rl []RateLimit) {
2645	if len(rl) > 0 {
2646		r.RateLimit = &rl[0]
2647	}
2648}
2649
2650func (r *NewConversationsLocalRes) GetRateLimit() (res []RateLimit) {
2651	return r.RateLimits
2652}
2653
2654func (r *NewConversationsLocalRes) SetRateLimits(rl []RateLimit) {
2655	r.RateLimits = rl
2656}
2657
2658func (r *GetDefaultTeamChannelsLocalRes) GetRateLimit() (res []RateLimit) {
2659	if r.RateLimit != nil {
2660		res = []RateLimit{*r.RateLimit}
2661	}
2662	return res
2663}
2664
2665func (r *GetDefaultTeamChannelsLocalRes) SetRateLimits(rl []RateLimit) {
2666	if len(rl) > 0 {
2667		r.RateLimit = &rl[0]
2668	}
2669}
2670
2671func (r *SetDefaultTeamChannelsLocalRes) GetRateLimit() (res []RateLimit) {
2672	if r.RateLimit != nil {
2673		res = []RateLimit{*r.RateLimit}
2674	}
2675	return res
2676}
2677
2678func (r *SetDefaultTeamChannelsLocalRes) SetRateLimits(rl []RateLimit) {
2679	if len(rl) > 0 {
2680		r.RateLimit = &rl[0]
2681	}
2682}
2683
2684func (r *AddEmojiRes) GetRateLimit() (res []RateLimit) {
2685	if r.RateLimit != nil {
2686		res = []RateLimit{*r.RateLimit}
2687	}
2688	return res
2689}
2690
2691func (r *AddEmojiRes) SetRateLimits(rl []RateLimit) {
2692	if len(rl) > 0 {
2693		r.RateLimit = &rl[0]
2694	}
2695}
2696
2697func (r *AddEmojisRes) GetRateLimit() (res []RateLimit) {
2698	if r.RateLimit != nil {
2699		res = []RateLimit{*r.RateLimit}
2700	}
2701	return res
2702}
2703
2704func (r *AddEmojisRes) SetRateLimits(rl []RateLimit) {
2705	if len(rl) > 0 {
2706		r.RateLimit = &rl[0]
2707	}
2708}
2709
2710func (r *AddEmojiAliasRes) GetRateLimit() (res []RateLimit) {
2711	if r.RateLimit != nil {
2712		res = []RateLimit{*r.RateLimit}
2713	}
2714	return res
2715}
2716
2717func (r *AddEmojiAliasRes) SetRateLimits(rl []RateLimit) {
2718	if len(rl) > 0 {
2719		r.RateLimit = &rl[0]
2720	}
2721}
2722
2723func (r *RemoveEmojiRes) GetRateLimit() (res []RateLimit) {
2724	if r.RateLimit != nil {
2725		res = []RateLimit{*r.RateLimit}
2726	}
2727	return res
2728}
2729
2730func (r *RemoveEmojiRes) SetRateLimits(rl []RateLimit) {
2731	if len(rl) > 0 {
2732		r.RateLimit = &rl[0]
2733	}
2734}
2735
2736func (r *UserEmojiRes) GetRateLimit() (res []RateLimit) {
2737	if r.RateLimit != nil {
2738		res = []RateLimit{*r.RateLimit}
2739	}
2740	return res
2741}
2742
2743func (r *UserEmojiRes) SetRateLimits(rl []RateLimit) {
2744	if len(rl) > 0 {
2745		r.RateLimit = &rl[0]
2746	}
2747}
2748
2749func (i EphemeralPurgeInfo) IsNil() bool {
2750	return i.IsActive == false && i.NextPurgeTime == 0 && i.MinUnexplodedID <= 1
2751}
2752
2753func (i EphemeralPurgeInfo) String() string {
2754	return fmt.Sprintf("EphemeralPurgeInfo{ ConvID: %v, IsActive: %v, NextPurgeTime: %v, MinUnexplodedID: %v }",
2755		i.ConvID, i.IsActive, i.NextPurgeTime.Time(), i.MinUnexplodedID)
2756}
2757
2758func (i EphemeralPurgeInfo) Eq(o EphemeralPurgeInfo) bool {
2759	return (i.IsActive == o.IsActive &&
2760		i.MinUnexplodedID == o.MinUnexplodedID &&
2761		i.NextPurgeTime == o.NextPurgeTime &&
2762		i.ConvID.Eq(o.ConvID))
2763}
2764
2765func (r ReactionMap) HasReactionFromUser(reactionText, username string) (found bool, reactionMsgID MessageID) {
2766	reactions, ok := r.Reactions[reactionText]
2767	if !ok {
2768		return false, 0
2769	}
2770	reaction, ok := reactions[username]
2771	return ok, reaction.ReactionMsgID
2772}
2773
2774func (r MessageReaction) Eq(o MessageReaction) bool {
2775	return r.Body == o.Body && r.MessageID == o.MessageID
2776}
2777
2778func (i *ConversationMinWriterRoleInfoLocal) String() string {
2779	if i == nil {
2780		return "Minimum writer role for this conversation is not set."
2781	}
2782	changedBySuffix := "."
2783	if i.ChangedBy != "" {
2784		changedBySuffix = fmt.Sprintf(", last set by %v.", i.ChangedBy)
2785	}
2786	return fmt.Sprintf("Minimum writer role for this conversation is %v%v", i.Role, changedBySuffix)
2787}
2788
2789func (s *ConversationSettings) IsNil() bool {
2790	return s == nil || s.MinWriterRoleInfo == nil
2791}
2792
2793func (o SearchOpts) Matches(msg MessageUnboxed) bool {
2794	if o.SentAfter != 0 && msg.Ctime() < o.SentAfter {
2795		return false
2796	}
2797	if o.SentBefore != 0 && msg.Ctime() > o.SentBefore {
2798		return false
2799	}
2800	if o.SentBy != "" && msg.SenderUsername() != o.SentBy {
2801		return false
2802	}
2803	// Check if the user was @mentioned or there was a @here/@channel.
2804	if o.SentTo != "" {
2805		if o.MatchMentions {
2806			switch msg.ChannelMention() {
2807			case ChannelMention_ALL, ChannelMention_HERE:
2808				return true
2809			}
2810		}
2811		for _, username := range msg.AtMentionUsernames() {
2812			if o.SentTo == username {
2813				return true
2814			}
2815		}
2816		return false
2817	}
2818	return true
2819}
2820
2821func (a MessageAttachment) GetTitle() string {
2822	title := a.Object.Title
2823	if title == "" {
2824		title = filepath.Base(a.Object.Filename)
2825	}
2826	return title
2827}
2828
2829func (u MessageUnfurl) SearchableText() string {
2830	typ, err := u.Unfurl.Unfurl.UnfurlType()
2831	if err != nil {
2832		return ""
2833	}
2834	switch typ {
2835	case UnfurlType_GENERIC:
2836		generic := u.Unfurl.Unfurl.Generic()
2837		res := generic.Title
2838		if generic.Description != nil {
2839			res += " " + *generic.Description
2840		}
2841		return res
2842	}
2843	return ""
2844}
2845
2846func (h *ChatSearchInboxHit) Size() int {
2847	if h == nil {
2848		return 0
2849	}
2850	return len(h.Hits)
2851}
2852
2853func (u UnfurlRaw) GetUrl() string {
2854	typ, err := u.UnfurlType()
2855	if err != nil {
2856		return ""
2857	}
2858	switch typ {
2859	case UnfurlType_GENERIC:
2860		return u.Generic().Url
2861	case UnfurlType_GIPHY:
2862		if u.Giphy().ImageUrl != nil {
2863			return *u.Giphy().ImageUrl
2864		}
2865	}
2866	return ""
2867}
2868
2869func (u UnfurlRaw) UnsafeDebugString() string {
2870	typ, err := u.UnfurlType()
2871	if err != nil {
2872		return "<error>"
2873	}
2874	switch typ {
2875	case UnfurlType_GENERIC:
2876		return u.Generic().UnsafeDebugString()
2877	case UnfurlType_GIPHY:
2878		return u.Giphy().UnsafeDebugString()
2879	}
2880	return "<unknown>"
2881}
2882
2883func yieldStr(s *string) string {
2884	if s == nil {
2885		return ""
2886	}
2887	return *s
2888}
2889
2890func (g UnfurlGenericRaw) UnsafeDebugString() string {
2891
2892	publishTime := ""
2893	if g.PublishTime != nil {
2894		publishTime = fmt.Sprintf("%v", time.Unix(int64(*g.PublishTime), 0))
2895	}
2896	return fmt.Sprintf(`Title: %s
2897Url: %s
2898SiteName: %s
2899PublishTime: %s
2900Description: %s
2901ImageUrl: %s
2902Video: %s
2903FaviconUrl: %s`, g.Title, g.Url, g.SiteName, publishTime, yieldStr(g.Description),
2904		yieldStr(g.ImageUrl), g.Video, yieldStr(g.FaviconUrl))
2905}
2906
2907func (g UnfurlGiphyRaw) UnsafeDebugString() string {
2908
2909	return fmt.Sprintf(`GIPHY SPECIAL
2910FaviconUrl: %s
2911ImageUrl: %s
2912Video: %s`, yieldStr(g.FaviconUrl), yieldStr(g.ImageUrl), g.Video)
2913}
2914
2915func (v UnfurlVideo) String() string {
2916	return fmt.Sprintf("[url: %s width: %d height: %d mime: %s]", v.Url, v.Width, v.Height, v.MimeType)
2917}
2918
2919func NewUnfurlSettings() UnfurlSettings {
2920	return UnfurlSettings{
2921		Mode:      UnfurlMode_WHITELISTED,
2922		Whitelist: make(map[string]bool),
2923	}
2924}
2925
2926func GlobalAppNotificationSettingsSorted() (res []GlobalAppNotificationSetting) {
2927	for setting := range GlobalAppNotificationSettingRevMap {
2928		if setting.Usage() != "" && setting.FlagName() != "" {
2929			res = append(res, setting)
2930		}
2931	}
2932	sort.Slice(res, func(i, j int) bool {
2933		return res[i] < res[j]
2934	})
2935	return res
2936}
2937
2938// Add to `Usage`/`FlagName` for a setting to be usable in the CLI via
2939// `keybase notification-settings`
2940func (g GlobalAppNotificationSetting) Usage() string {
2941	switch g {
2942	case GlobalAppNotificationSetting_NEWMESSAGES:
2943		return "Show notifications for new messages"
2944	case GlobalAppNotificationSetting_PLAINTEXTDESKTOP:
2945		return "Show plaintext notifications on desktop devices"
2946	case GlobalAppNotificationSetting_PLAINTEXTMOBILE:
2947		return "Show plaintext notifications on mobile devices"
2948	case GlobalAppNotificationSetting_DEFAULTSOUNDMOBILE:
2949		return "Use the default system sound on mobile devices"
2950	case GlobalAppNotificationSetting_DISABLETYPING:
2951		return "Disable sending/receiving typing notifications"
2952	default:
2953		return ""
2954	}
2955}
2956
2957func (g GlobalAppNotificationSetting) FlagName() string {
2958	switch g {
2959	case GlobalAppNotificationSetting_NEWMESSAGES:
2960		return "new-messages"
2961	case GlobalAppNotificationSetting_PLAINTEXTDESKTOP:
2962		return "plaintext-desktop"
2963	case GlobalAppNotificationSetting_PLAINTEXTMOBILE:
2964		return "plaintext-mobile"
2965	case GlobalAppNotificationSetting_DEFAULTSOUNDMOBILE:
2966		return "default-sound-mobile"
2967	case GlobalAppNotificationSetting_DISABLETYPING:
2968		return "disable-typing"
2969	default:
2970		return ""
2971	}
2972}
2973
2974func (m MessageSystemChangeRetention) String() string {
2975	var appliesTo string
2976	switch m.MembersType {
2977	case ConversationMembersType_TEAM:
2978		if m.IsTeam {
2979			appliesTo = "team"
2980		} else {
2981			appliesTo = "channel"
2982		}
2983	default:
2984		appliesTo = "conversation"
2985	}
2986	var inheritDescription string
2987	if m.IsInherit {
2988		inheritDescription = " to inherit from the team policy"
2989	}
2990
2991	format := "%s changed the %s retention policy%s. %s"
2992	summary := m.Policy.HumanSummary()
2993	return fmt.Sprintf(format, m.User, appliesTo, inheritDescription, summary)
2994}
2995
2996func (m MessageSystemBulkAddToConv) String() string {
2997	prefix := "Added %s to the conversation"
2998	var suffix string
2999	switch len(m.Usernames) {
3000	case 0:
3001		return ""
3002	case 1:
3003		suffix = m.Usernames[0]
3004	case 2:
3005		suffix = fmt.Sprintf("%s and %s", m.Usernames[0], m.Usernames[1])
3006	default:
3007		suffix = fmt.Sprintf("%s and %d others", m.Usernames[0], len(m.Usernames)-1)
3008	}
3009	return fmt.Sprintf(prefix, suffix)
3010}
3011
3012func withDeterminer(s string) string {
3013	r, size := utf8.DecodeRuneInString(s)
3014	if size == 0 || r == utf8.RuneError {
3015		return "a " + s
3016	}
3017	if strings.Contains("aeiou", string(r)) {
3018		return "an " + s
3019	}
3020	return "a " + s
3021}
3022
3023func (m MessageSystem) String() string {
3024	typ, err := m.SystemType()
3025	if err != nil {
3026		return ""
3027	}
3028	switch typ {
3029	case MessageSystemType_ADDEDTOTEAM:
3030		output := fmt.Sprintf("Added @%s to the team", m.Addedtoteam().Addee)
3031		if role := m.Addedtoteam().Role; role != keybase1.TeamRole_NONE {
3032			output += fmt.Sprintf(" as %v", withDeterminer(role.HumanString()))
3033		}
3034		return output
3035	case MessageSystemType_INVITEADDEDTOTEAM:
3036		var roleText string
3037		if role := m.Inviteaddedtoteam().Role; role != keybase1.TeamRole_NONE {
3038			roleText = fmt.Sprintf(" as %v", withDeterminer(role.HumanString()))
3039		}
3040		output := fmt.Sprintf("Added @%s to the team (invited by @%s%s)",
3041			m.Inviteaddedtoteam().Invitee, m.Inviteaddedtoteam().Inviter, roleText)
3042		return output
3043	case MessageSystemType_COMPLEXTEAM:
3044		return fmt.Sprintf("%s is now a 'big' team with multiple channels", m.Complexteam().Team)
3045	case MessageSystemType_CREATETEAM:
3046		return fmt.Sprintf("@%s created the team %s", m.Createteam().Creator, m.Createteam().Team)
3047	case MessageSystemType_GITPUSH:
3048		body := m.Gitpush()
3049		switch body.PushType {
3050		case keybase1.GitPushType_CREATEREPO:
3051			return fmt.Sprintf("git @%s created the repo %s", body.Pusher, body.RepoName)
3052		case keybase1.GitPushType_RENAMEREPO:
3053			return fmt.Sprintf("git @%s changed the name of the repo %s to %s", body.Pusher, body.PreviousRepoName, body.RepoName)
3054		default:
3055			total := keybase1.TotalNumberOfCommits(body.Refs)
3056			names := keybase1.RefNames(body.Refs)
3057			return fmt.Sprintf("git (%s) @%s pushed %d commits to %s", body.RepoName,
3058				body.Pusher, total, names)
3059		}
3060	case MessageSystemType_CHANGEAVATAR:
3061		return fmt.Sprintf("@%s changed team avatar", m.Changeavatar().User)
3062	case MessageSystemType_CHANGERETENTION:
3063		return m.Changeretention().String()
3064	case MessageSystemType_BULKADDTOCONV:
3065		return m.Bulkaddtoconv().String()
3066	case MessageSystemType_SBSRESOLVE:
3067		body := m.Sbsresolve()
3068		switch body.AssertionService {
3069		case "phone":
3070			return fmt.Sprintf("@%s verified their phone number %s and joined"+
3071				" the conversation", body.Prover, body.AssertionUsername)
3072		case "email":
3073			return fmt.Sprintf("@%s verified their email address %s and joined"+
3074				" the conversation", body.Prover, body.AssertionUsername)
3075		}
3076		return fmt.Sprintf("@%s proved they are %s on %s and joined"+
3077			" the conversation", body.Prover, body.AssertionUsername,
3078			body.AssertionService)
3079	case MessageSystemType_NEWCHANNEL:
3080		body := m.Newchannel()
3081		if len(body.ConvIDs) > 1 {
3082			return fmt.Sprintf("@%s created #%s and %d other new channels",
3083				body.Creator, body.NameAtCreation, len(body.ConvIDs)-1)
3084		}
3085		return fmt.Sprintf("@%s created a new channel #%s",
3086			body.Creator, body.NameAtCreation)
3087	default:
3088		return ""
3089	}
3090}
3091
3092func (m MessageHeadline) String() string {
3093	if m.Headline == "" {
3094		return "cleared the channel description"
3095	}
3096	return fmt.Sprintf("set the channel description: %v", m.Headline)
3097}
3098
3099func isZero(v []byte) bool {
3100	for _, b := range v {
3101		if b != 0 {
3102			return false
3103		}
3104	}
3105	return true
3106}
3107
3108func MakeFlipGameID(s string) (FlipGameID, error) { return hex.DecodeString(s) }
3109func (g FlipGameID) String() string               { return hex.EncodeToString(g) }
3110func (g FlipGameID) FlipGameIDStr() FlipGameIDStr { return FlipGameIDStr(g.String()) }
3111func (g FlipGameID) Eq(h FlipGameID) bool         { return hmac.Equal(g[:], h[:]) }
3112func (g FlipGameID) IsZero() bool                 { return isZero(g[:]) }
3113func (g FlipGameID) Check() bool                  { return g != nil && !g.IsZero() }
3114
3115func (o *SenderSendOptions) GetJoinMentionsAs() *ConversationMemberStatus {
3116	if o == nil {
3117		return nil
3118	}
3119	return o.JoinMentionsAs
3120}
3121
3122func (c Coordinate) IsZero() bool {
3123	return c.Lat == 0 && c.Lon == 0
3124}
3125
3126func (c Coordinate) Eq(o Coordinate) bool {
3127	return c.Lat == o.Lat && c.Lon == o.Lon
3128}
3129
3130type safeCoordinate struct {
3131	Lat      float64 `codec:"lat" json:"lat"`
3132	Lon      float64 `codec:"lon" json:"lon"`
3133	Accuracy float64 `codec:"accuracy" json:"accuracy"`
3134}
3135
3136func (c Coordinate) MarshalJSON() ([]byte, error) {
3137	var safe safeCoordinate
3138	safe.Lat = c.Lat
3139	safe.Lon = c.Lon
3140	safe.Accuracy = c.Accuracy
3141	if math.IsNaN(safe.Lat) {
3142		safe.Lat = 0
3143	}
3144	if math.IsNaN(safe.Lon) {
3145		safe.Lon = 0
3146	}
3147	if math.IsNaN(safe.Accuracy) {
3148		safe.Accuracy = 0
3149	}
3150	return json.Marshal(safe)
3151}
3152
3153// Incremented if the client hash algorithm changes. If this value is changed
3154// be sure to add a case in the BotInfo.Hash() function.
3155const ClientBotInfoHashVers BotInfoHashVers = 2
3156
3157// Incremented if the server sends down bad data and needs to bust client
3158// caches.
3159const ServerBotInfoHashVers BotInfoHashVers = 1
3160
3161func (b BotInfo) Hash() BotInfoHash {
3162	hash := sha256Pool.Get().(hash.Hash)
3163	defer sha256Pool.Put(hash)
3164	hash.Reset()
3165
3166	// Always hash in the server/client version.
3167	hash.Write([]byte(strconv.FormatUint(uint64(b.ServerHashVers), 10)))
3168	hash.Write([]byte(strconv.FormatUint(uint64(b.ClientHashVers), 10)))
3169
3170	// This should cover all cases from 0..DefaultBotInfoHashVers. If
3171	// incrementing DefaultBotInfoHashVers be sure to add a case here.
3172	switch b.ClientHashVers {
3173	case 0, 1:
3174		b.hashV1(hash)
3175	case 2:
3176		b.hashV2(hash)
3177	default:
3178		// Every valid client version should be specifically handled, unit
3179		// tests verify that we have a non-empty hash output.
3180		hash.Reset()
3181	}
3182	return BotInfoHash(hash.Sum(nil))
3183}
3184
3185func (b BotInfo) hashV1(hash hash.Hash) {
3186	sort.Slice(b.CommandConvs, func(i, j int) bool {
3187		ikey := b.CommandConvs[i].Uid.String() + b.CommandConvs[i].ConvID.String()
3188		jkey := b.CommandConvs[j].Uid.String() + b.CommandConvs[j].ConvID.String()
3189		return ikey < jkey
3190	})
3191	for _, cconv := range b.CommandConvs {
3192		hash.Write(cconv.ConvID)
3193		hash.Write(cconv.Uid)
3194		hash.Write([]byte(strconv.FormatUint(uint64(cconv.UntrustedTeamRole), 10)))
3195		hash.Write([]byte(strconv.FormatUint(uint64(cconv.Vers), 10)))
3196	}
3197}
3198
3199func (b BotInfo) hashV2(hash hash.Hash) {
3200	sort.Slice(b.CommandConvs, func(i, j int) bool {
3201		ikey := b.CommandConvs[i].Uid.String() + b.CommandConvs[i].ConvID.String()
3202		jkey := b.CommandConvs[j].Uid.String() + b.CommandConvs[j].ConvID.String()
3203		return ikey < jkey
3204	})
3205	for _, cconv := range b.CommandConvs {
3206		hash.Write(cconv.ConvID)
3207		hash.Write(cconv.Uid)
3208		hash.Write([]byte(strconv.FormatUint(uint64(cconv.UntrustedTeamRole), 10)))
3209		hash.Write([]byte(strconv.FormatUint(uint64(cconv.Vers), 10)))
3210		hash.Write([]byte(strconv.FormatUint(uint64(cconv.Typ), 10)))
3211	}
3212}
3213
3214func (b BotInfoHash) Eq(h BotInfoHash) bool {
3215	return bytes.Equal(b, h)
3216}
3217
3218func (p AdvertiseCommandsParam) ToRemote(cmdConvID ConversationID, tlfID *TLFID, adConvID *ConversationID) (res RemoteBotCommandsAdvertisement, err error) {
3219	switch p.Typ {
3220	case BotCommandsAdvertisementTyp_PUBLIC:
3221		if tlfID != nil {
3222			return res, errors.New("TLFID specified for public advertisement")
3223		} else if adConvID != nil {
3224			return res, errors.New("ConvID specified for public advertisement")
3225		}
3226		return NewRemoteBotCommandsAdvertisementWithPublic(RemoteBotCommandsAdvertisementPublic{
3227			ConvID: cmdConvID,
3228		}), nil
3229	case BotCommandsAdvertisementTyp_TLFID_CONVS:
3230		if tlfID == nil {
3231			return res, errors.New("no TLFID specified")
3232		} else if adConvID != nil {
3233			return res, errors.New("ConvID specified")
3234		}
3235		return NewRemoteBotCommandsAdvertisementWithTlfidConvs(RemoteBotCommandsAdvertisementTLFID{
3236			ConvID: cmdConvID,
3237			TlfID:  *tlfID,
3238		}), nil
3239	case BotCommandsAdvertisementTyp_TLFID_MEMBERS:
3240		if tlfID == nil {
3241			return res, errors.New("no TLFID specified")
3242		} else if adConvID != nil {
3243			return res, errors.New("ConvID specified")
3244		}
3245		return NewRemoteBotCommandsAdvertisementWithTlfidMembers(RemoteBotCommandsAdvertisementTLFID{
3246			ConvID: cmdConvID,
3247			TlfID:  *tlfID,
3248		}), nil
3249	case BotCommandsAdvertisementTyp_CONV:
3250		if tlfID != nil {
3251			return res, errors.New("TLFID specified")
3252		} else if adConvID == nil {
3253			return res, errors.New("no ConvID specified")
3254		}
3255		return NewRemoteBotCommandsAdvertisementWithConv(RemoteBotCommandsAdvertisementConv{
3256			ConvID:          cmdConvID,
3257			AdvertiseConvID: *adConvID,
3258		}), nil
3259	default:
3260		return res, errors.New("unknown bot advertisement typ")
3261	}
3262}
3263
3264func (p ClearBotCommandsFilter) ToRemote(tlfID *TLFID, convID *ConversationID) (res RemoteClearBotCommandsFilter, err error) {
3265	switch p.Typ {
3266	case BotCommandsAdvertisementTyp_PUBLIC:
3267		if tlfID != nil {
3268			return res, errors.New("TLFID specified for public advertisement")
3269		} else if convID != nil {
3270			return res, errors.New("ConvID specified for public advertisement")
3271		}
3272		return NewRemoteClearBotCommandsFilterWithPublic(RemoteClearBotCommandsFilterPublic{}), nil
3273	case BotCommandsAdvertisementTyp_TLFID_CONVS:
3274		if tlfID == nil {
3275			return res, errors.New("no TLFID specified")
3276		} else if convID != nil {
3277			return res, errors.New("ConvID specified")
3278		}
3279		return NewRemoteClearBotCommandsFilterWithTlfidConvs(RemoteClearBotCommandsFilterTLFID{
3280			TlfID: *tlfID,
3281		}), nil
3282	case BotCommandsAdvertisementTyp_TLFID_MEMBERS:
3283		if tlfID == nil {
3284			return res, errors.New("no TLFID specified")
3285		} else if convID != nil {
3286			return res, errors.New("ConvID specified")
3287		}
3288		return NewRemoteClearBotCommandsFilterWithTlfidMembers(RemoteClearBotCommandsFilterTLFID{
3289			TlfID: *tlfID,
3290		}), nil
3291	case BotCommandsAdvertisementTyp_CONV:
3292		if tlfID != nil {
3293			return res, errors.New("TLFID specified")
3294		} else if convID == nil {
3295			return res, errors.New("no ConvID specified")
3296		}
3297		return NewRemoteClearBotCommandsFilterWithConv(RemoteClearBotCommandsFilterConv{
3298			ConvID: *convID,
3299		}), nil
3300	default:
3301		return res, errors.New("unknown bot advertisement typ")
3302	}
3303}
3304
3305func GetAdvertTyp(typ string) (BotCommandsAdvertisementTyp, error) {
3306	switch typ {
3307	case "public":
3308		return BotCommandsAdvertisementTyp_PUBLIC, nil
3309	case "teamconvs":
3310		return BotCommandsAdvertisementTyp_TLFID_CONVS, nil
3311	case "teammembers":
3312		return BotCommandsAdvertisementTyp_TLFID_MEMBERS, nil
3313	case "conv":
3314		return BotCommandsAdvertisementTyp_CONV, nil
3315	default:
3316		return BotCommandsAdvertisementTyp_PUBLIC, fmt.Errorf("unknown advertisement type %q, must be one of 'public', 'teamconvs', 'teammembers', or 'conv' see `keybase chat api --help` for more info.", typ)
3317	}
3318}
3319
3320func (c UserBotCommandInput) ToOutput(username string) UserBotCommandOutput {
3321	return UserBotCommandOutput{
3322		Name:                c.Name,
3323		Description:         c.Description,
3324		Usage:               c.Usage,
3325		ExtendedDescription: c.ExtendedDescription,
3326		Username:            username,
3327	}
3328}
3329
3330func (r UIInboxReselectInfo) String() string {
3331	newConvStr := "<none>"
3332	if r.NewConvID != nil {
3333		newConvStr = string(*r.NewConvID)
3334	}
3335	return fmt.Sprintf("[oldconv: %s newconv: %s]", r.OldConvID, newConvStr)
3336}
3337
3338func (e OutboxErrorType) IsBadgableError() bool {
3339	switch e {
3340	case OutboxErrorType_MISC,
3341		OutboxErrorType_OFFLINE,
3342		OutboxErrorType_TOOLONG,
3343		OutboxErrorType_EXPIRED,
3344		OutboxErrorType_TOOMANYATTEMPTS,
3345		OutboxErrorType_UPLOADFAILED:
3346		return true
3347	default:
3348		return false
3349	}
3350}
3351
3352func (c UserBotCommandOutput) Matches(text string) bool {
3353	return strings.HasPrefix(text, fmt.Sprintf("!%s", c.Name))
3354}
3355
3356func (m AssetMetadata) IsType(typ AssetMetadataType) bool {
3357	mtyp, err := m.AssetType()
3358	if err != nil {
3359		return false
3360	}
3361	return mtyp == typ
3362}
3363
3364type safeAssetMetadataImage struct {
3365	Width     int       `codec:"width" json:"width"`
3366	Height    int       `codec:"height" json:"height"`
3367	AudioAmps []float64 `codec:"audioAmps" json:"audioAmps"`
3368}
3369
3370func (m AssetMetadataImage) MarshalJSON() ([]byte, error) {
3371	var safe safeAssetMetadataImage
3372	safe.AudioAmps = make([]float64, 0, len(m.AudioAmps))
3373	for _, amp := range m.AudioAmps {
3374		if math.IsNaN(amp) {
3375			safe.AudioAmps = append(safe.AudioAmps, 0)
3376		} else {
3377			safe.AudioAmps = append(safe.AudioAmps, amp)
3378		}
3379	}
3380	safe.Width = m.Width
3381	safe.Height = m.Height
3382	return json.Marshal(safe)
3383}
3384
3385func (s SnippetDecoration) ToEmoji() string {
3386	switch s {
3387	case SnippetDecoration_PENDING_MESSAGE:
3388		return ":outbox_tray:"
3389	case SnippetDecoration_FAILED_PENDING_MESSAGE:
3390		return ":warning:"
3391	case SnippetDecoration_EXPLODING_MESSAGE:
3392		return ":bomb:"
3393	case SnippetDecoration_EXPLODED_MESSAGE:
3394		return ":boom:"
3395	case SnippetDecoration_AUDIO_ATTACHMENT:
3396		return ":loud_sound:"
3397	case SnippetDecoration_VIDEO_ATTACHMENT:
3398		return ":film_frames:"
3399	case SnippetDecoration_PHOTO_ATTACHMENT:
3400		return ":camera:"
3401	case SnippetDecoration_FILE_ATTACHMENT:
3402		return ":file_folder:"
3403	case SnippetDecoration_STELLAR_RECEIVED:
3404		return ":bank:"
3405	case SnippetDecoration_STELLAR_SENT:
3406		return ":money_with_wings:"
3407	case SnippetDecoration_PINNED_MESSAGE:
3408		return ":pushpin:"
3409	default:
3410		return ""
3411	}
3412}
3413
3414func (r EmojiRemoteSource) IsMessage() bool {
3415	typ, err := r.Typ()
3416	if err != nil {
3417		return false
3418	}
3419	return typ == EmojiRemoteSourceTyp_MESSAGE
3420}
3421
3422func (r EmojiRemoteSource) IsStockAlias() bool {
3423	typ, err := r.Typ()
3424	if err != nil {
3425		return false
3426	}
3427	return typ == EmojiRemoteSourceTyp_STOCKALIAS
3428}
3429
3430func (r EmojiRemoteSource) IsAlias() bool {
3431	return r.IsStockAlias() || (r.IsMessage() && r.Message().IsAlias)
3432}
3433
3434func (r EmojiLoadSource) IsHTTPSrv() bool {
3435	typ, err := r.Typ()
3436	if err != nil {
3437		return false
3438	}
3439	return typ == EmojiLoadSourceTyp_HTTPSRV
3440}
3441
3442func TeamToChatMemberDetails(teamMembers []keybase1.TeamMemberDetails) (chatMembers []ChatMemberDetails) {
3443	chatMembers = make([]ChatMemberDetails, len(teamMembers))
3444	for i, teamMember := range teamMembers {
3445		chatMembers[i] = ChatMemberDetails{
3446			Uid:      teamMember.Uv.Uid,
3447			Username: teamMember.Username,
3448			FullName: teamMember.FullName,
3449		}
3450	}
3451	return chatMembers
3452}
3453
3454func TeamToChatMembersDetails(details keybase1.TeamMembersDetails) ChatMembersDetails {
3455	return ChatMembersDetails{
3456		Owners:         TeamToChatMemberDetails(details.Owners),
3457		Admins:         TeamToChatMemberDetails(details.Admins),
3458		Writers:        TeamToChatMemberDetails(details.Writers),
3459		Readers:        TeamToChatMemberDetails(details.Readers),
3460		Bots:           TeamToChatMemberDetails(details.Bots),
3461		RestrictedBots: TeamToChatMemberDetails(details.RestrictedBots),
3462	}
3463}
3464