/
githubmirror
/
client
Обзор
Документация
Войти
/
githubmirror
/
client
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
go/protocol/chat1/extras.go
3 432 строки
84 KB
chrisnojima-zoom
Version 670 - clean2 (#29122)
08 июн 2026, 19:31
Не верифицирован
08 июн 2026, 19:31
1943197
Код
Авторство
О чём код?
package chat1 import ( "bytes" "crypto/hmac" "crypto/sha256" "encoding/hex" "encoding/json" "errors" "flag" "fmt" "hash" "math" "path/filepath" "regexp" "slices" "sort" "strconv" "strings" "sync" "time" "unicode/utf8" "github.com/keybase/client/go/protocol/gregor1" "github.com/keybase/client/go/protocol/keybase1" ) // we will show some representation of an exploded message in the UI for a week const ShowExplosionLifetime = time.Hour * 24 * 7 // If a conversation is larger, only admins can @channel. const MaxChanMentionConvSize = 100 func (i FlipGameIDStr) String() string { return string(i) } func (i TLFIDStr) String() string { return string(i) } func (i ConvIDStr) String() string { return string(i) } type ( ByUID []gregor1.UID ConvIDShort = []byte ) func (b ByUID) Len() int { return len(b) } func (b ByUID) Swap(i, j int) { b[i], b[j] = b[j], b[i] } func (b ByUID) Less(i, j int) bool { return bytes.Compare(b[i].Bytes(), b[j].Bytes()) < 0 } // Eq compares two TLFIDs func (id TLFID) Eq(other TLFID) bool { return bytes.Equal([]byte(id), []byte(other)) } // EqString is like EqualsTo, except that it accepts a fmt.Stringer. This // can be useful for comparing keybase1.TLFID and chat1.TLFID. func (id TLFID) EqString(other fmt.Stringer) bool { return hex.EncodeToString(id) == other.String() } func (id TLFID) String() string { return hex.EncodeToString(id) } func (id TLFID) Bytes() []byte { return []byte(id) } func (id TLFID) TLFIDStr() TLFIDStr { return TLFIDStr(id.String()) } func (id TLFID) IsNil() bool { return len(id) == 0 } func (id TLFID) IsTeamID() bool { if len(id) != keybase1.TEAMID_LEN { return false } switch id[len(id)-1] { case keybase1.TEAMID_PRIVATE_SUFFIX, keybase1.TEAMID_PUBLIC_SUFFIX, keybase1.SUB_TEAMID_PRIVATE_SUFFIX, keybase1.SUB_TEAMID_PUBLIC_SUFFIX: return true default: return false } } func MakeConvID(val string) (ConversationID, error) { return hex.DecodeString(val) } func (cid ConversationID) String() string { return hex.EncodeToString(cid) } func (cid ConversationID) Bytes() []byte { return []byte(cid) } func (cid ConversationID) ConvIDStr() ConvIDStr { return ConvIDStr(cid.String()) } func (cid ConversationID) IsNil() bool { return len(cid) < DbShortFormLen } func (cid ConversationID) Eq(c ConversationID) bool { return bytes.Equal(cid, c) } func (cid ConversationID) Less(c ConversationID) bool { return bytes.Compare(cid, c) < 0 } const DbShortFormLen = 10 // DbShortForm should only be used when interacting with the database, and should // never leave Gregor func (cid ConversationID) DbShortForm() ConvIDShort { end := min(DbShortFormLen, len(cid)) return cid[:end] } func (cid ConversationID) DbShortFormString() string { return DbShortFormToString(cid.DbShortForm()) } func DbShortFormToString(cid ConvIDShort) string { return hex.EncodeToString(cid) } func DbShortFormFromString(cid string) (ConvIDShort, error) { return hex.DecodeString(cid) } func MakeTLFID(val string) (TLFID, error) { return hex.DecodeString(val) } func MakeTopicID(val string) (TopicID, error) { return hex.DecodeString(val) } func MakeTopicType(val int64) TopicType { return TopicType(val) } func (mid MessageID) String() string { return strconv.FormatUint(uint64(mid), 10) } func (mid MessageID) Min(mid2 MessageID) MessageID { if mid < mid2 { return mid } return mid2 } func (mid MessageID) IsNil() bool { return uint(mid) == 0 } func (mid MessageID) Advance(num uint) MessageID { return MessageID(uint(mid) + num) } func (t MessageType) String() string { s, ok := MessageTypeRevMap[t] if ok { return s } return "UNKNOWN" } // Message types deletable by a standard DELETE message. var deletableMessageTypesByDelete = []MessageType{ MessageType_TEXT, MessageType_ATTACHMENT, MessageType_EDIT, MessageType_ATTACHMENTUPLOADED, MessageType_REACTION, MessageType_REQUESTPAYMENT, MessageType_UNFURL, MessageType_PIN, MessageType_HEADLINE, MessageType_SYSTEM, MessageType_FLIP, } // Messages types NOT deletable by a DELETEHISTORY message. var nonDeletableMessageTypesByDeleteHistory = []MessageType{ MessageType_NONE, MessageType_DELETE, MessageType_TLFNAME, MessageType_DELETEHISTORY, } func DeletableMessageTypesByDelete() []MessageType { return deletableMessageTypesByDelete } func IsSystemMsgDeletableByDelete(typ MessageSystemType) bool { switch typ { case MessageSystemType_ADDEDTOTEAM, MessageSystemType_INVITEADDEDTOTEAM, MessageSystemType_GITPUSH, MessageSystemType_CHANGEAVATAR, MessageSystemType_CHANGERETENTION, MessageSystemType_BULKADDTOCONV, MessageSystemType_SBSRESOLVE, MessageSystemType_NEWCHANNEL: return true case MessageSystemType_COMPLEXTEAM, MessageSystemType_CREATETEAM: return false default: return false } } var visibleMessageTypes = []MessageType{ MessageType_TEXT, MessageType_ATTACHMENT, MessageType_JOIN, MessageType_LEAVE, MessageType_SYSTEM, MessageType_SENDPAYMENT, MessageType_REQUESTPAYMENT, MessageType_FLIP, MessageType_HEADLINE, MessageType_PIN, } // Visible chat messages appear visually as a message in the conv. // For counterexample REACTION and DELETE_HISTORY have visual effects but do not appear as a message. func VisibleChatMessageTypes() []MessageType { return visibleMessageTypes } var badgeableMessageTypes = []MessageType{ MessageType_TEXT, MessageType_ATTACHMENT, MessageType_SYSTEM, MessageType_SENDPAYMENT, MessageType_REQUESTPAYMENT, MessageType_FLIP, MessageType_HEADLINE, MessageType_PIN, } // Message types that cause badges. // JOIN and LEAVE are Visible but are too minute to badge. func BadgeableMessageTypes() []MessageType { return badgeableMessageTypes } // A conversation is considered 'empty' unless it has one of these message types. // Used for filtering empty convs out of the the inbox. func NonEmptyConvMessageTypes() []MessageType { return badgeableMessageTypes } var snippetMessageTypes = []MessageType{ MessageType_TEXT, MessageType_ATTACHMENT, MessageType_SYSTEM, MessageType_DELETEHISTORY, MessageType_SENDPAYMENT, MessageType_REQUESTPAYMENT, MessageType_FLIP, MessageType_HEADLINE, MessageType_PIN, } // Snippet chat messages can be the snippet of a conversation. func SnippetChatMessageTypes() []MessageType { return snippetMessageTypes } var editableMessageTypesByEdit = []MessageType{ MessageType_TEXT, MessageType_ATTACHMENT, } func EditableMessageTypesByEdit() []MessageType { return editableMessageTypesByEdit } func IsEphemeralSupersederType(typ MessageType) bool { switch typ { case MessageType_EDIT, MessageType_ATTACHMENTUPLOADED, MessageType_REACTION, MessageType_UNFURL: return true default: return false } } func IsEphemeralNonSupersederType(typ MessageType) bool { switch typ { case MessageType_TEXT, MessageType_ATTACHMENT, MessageType_FLIP: return true default: return false } } func IsEphemeralType(typ MessageType) bool { return IsEphemeralNonSupersederType(typ) || IsEphemeralSupersederType(typ) } func DeletableMessageTypesByDeleteHistory() (res []MessageType) { banned := make(map[MessageType]bool) for _, mt := range nonDeletableMessageTypesByDeleteHistory { banned[mt] = true } for _, mt := range MessageTypeMap { if !banned[mt] { res = append(res, mt) } } slices.Sort(res) return res } func IsDeletableByDelete(typ MessageType) bool { return slices.Contains(deletableMessageTypesByDelete, typ) } func IsDeletableByDeleteHistory(typ MessageType) bool { return !slices.Contains(nonDeletableMessageTypesByDeleteHistory, typ) } // EphemeralAllowed flags if the given topic type is allowed to send ephemeral // messages at all. func (t TopicType) EphemeralAllowed() bool { switch t { case TopicType_KBFSFILEEDIT, TopicType_EMOJI, TopicType_EMOJICROSS: return false default: return true } } // EphemeralRequired flags if the given topic type required to respect the // ephemeral retention policy if set. func (t TopicType) EphemeralRequired() bool { switch t { case TopicType_DEV: return false default: return t.EphemeralAllowed() } } func (t TopicType) String() string { s, ok := TopicTypeRevMap[t] if ok { return s } return "UNKNOWN" } func (t TopicID) String() string { return hex.EncodeToString(t) } func (t TopicID) Eq(r TopicID) bool { return bytes.Equal([]byte(t), []byte(r)) } func (t ConversationIDTriple) Eq(other ConversationIDTriple) bool { return t.Tlfid.Eq(other.Tlfid) && bytes.Equal([]byte(t.TopicID), []byte(other.TopicID)) && t.TopicType == other.TopicType } func (hash Hash) String() string { return hex.EncodeToString(hash) } func (hash Hash) Eq(other Hash) bool { return bytes.Equal(hash, other) } func (m MessageUnboxed) SenderEq(o MessageUnboxed) bool { if state, err := m.State(); err == nil { if ostate, err := o.State(); err == nil && state == ostate { switch state { case MessageUnboxedState_VALID: return m.Valid().SenderEq(o.Valid()) case MessageUnboxedState_ERROR: return m.Error().SenderEq(o.Error()) case MessageUnboxedState_OUTBOX: return m.Outbox().SenderEq(o.Outbox()) } } } return false } func (m MessageUnboxed) OutboxID() *OutboxID { if state, err := m.State(); err == nil { switch state { case MessageUnboxedState_VALID: return m.Valid().ClientHeader.OutboxID case MessageUnboxedState_ERROR: return nil case MessageUnboxedState_PLACEHOLDER: return nil case MessageUnboxedState_OUTBOX: return m.Outbox().Msg.ClientHeader.OutboxID default: return nil } } return nil } func (m MessageUnboxed) GetMessageID() MessageID { if state, err := m.State(); err == nil { switch state { case MessageUnboxedState_VALID: return m.Valid().ServerHeader.MessageID case MessageUnboxedState_ERROR: return m.Error().MessageID case MessageUnboxedState_PLACEHOLDER: return m.Placeholder().MessageID case MessageUnboxedState_OUTBOX: return m.Outbox().Msg.ClientHeader.OutboxInfo.Prev case MessageUnboxedState_JOURNEYCARD: return m.Journeycard().PrevID default: return 0 } } return 0 } func (m MessageUnboxed) IsEphemeral() bool { if state, err := m.State(); err == nil { switch state { case MessageUnboxedState_VALID: return m.Valid().IsEphemeral() case MessageUnboxedState_ERROR: return m.Error().IsEphemeral } } return false } func (m MessageUnboxed) HideExplosion(maxDeletedUpto MessageID, now time.Time) bool { if state, err := m.State(); err == nil { switch state { case MessageUnboxedState_VALID: return m.Valid().HideExplosion(maxDeletedUpto, now) case MessageUnboxedState_ERROR: return m.Error().HideExplosion(maxDeletedUpto, now) } } return false } func (m MessageUnboxed) GetOutboxID() *OutboxID { if state, err := m.State(); err == nil { switch state { case MessageUnboxedState_VALID: return m.Valid().ClientHeader.OutboxID case MessageUnboxedState_ERROR: return nil case MessageUnboxedState_PLACEHOLDER: return nil case MessageUnboxedState_OUTBOX: obid := m.Outbox().OutboxID return &obid case MessageUnboxedState_JOURNEYCARD: return nil default: return nil } } return nil } func (m MessageUnboxed) GetTopicType() TopicType { if state, err := m.State(); err == nil { switch state { case MessageUnboxedState_VALID: return m.Valid().ClientHeader.Conv.TopicType case MessageUnboxedState_ERROR: return TopicType_NONE case MessageUnboxedState_OUTBOX: return m.Outbox().Msg.ClientHeader.Conv.TopicType case MessageUnboxedState_PLACEHOLDER: return TopicType_NONE case MessageUnboxedState_JOURNEYCARD: return TopicType_NONE } } return TopicType_NONE } func (m MessageUnboxed) GetMessageType() MessageType { if state, err := m.State(); err == nil { switch state { case MessageUnboxedState_VALID: return m.Valid().ClientHeader.MessageType case MessageUnboxedState_ERROR: return m.Error().MessageType case MessageUnboxedState_OUTBOX: return m.Outbox().Msg.ClientHeader.MessageType case MessageUnboxedState_PLACEHOLDER: // All we know about a place holder is the ID, so just // call it type NONE return MessageType_NONE case MessageUnboxedState_JOURNEYCARD: return MessageType_NONE } } return MessageType_NONE } func (m MessageUnboxed) IsValid() bool { if state, err := m.State(); err == nil { return state == MessageUnboxedState_VALID } return false } func (m MessageUnboxed) IsError() bool { if state, err := m.State(); err == nil { return state == MessageUnboxedState_ERROR } return false } func (m MessageUnboxed) IsOutbox() bool { if state, err := m.State(); err == nil { return state == MessageUnboxedState_OUTBOX } return false } func (m MessageUnboxed) IsPlaceholder() bool { if state, err := m.State(); err == nil { return state == MessageUnboxedState_PLACEHOLDER } return false } func (m MessageUnboxed) IsJourneycard() bool { if state, err := m.State(); err == nil { return state == MessageUnboxedState_JOURNEYCARD } return false } // IsValidFull returns whether the message is both: // 1. Valid // 2. Has a non-deleted body with a type matching the header // (TLFNAME is an exception as it has no body) func (m MessageUnboxed) IsValidFull() bool { if !m.IsValid() { return false } valid := m.Valid() headerType := valid.ClientHeader.MessageType switch headerType { case MessageType_NONE: return false case MessageType_TLFNAME: // Skip body check return true } bodyType, err := valid.MessageBody.MessageType() if err != nil { return false } return bodyType == headerType } // IsValidDeleted returns whether a message is valid and has been deleted. // This statement does not hold: IsValidFull != IsValidDeleted func (m MessageUnboxed) IsValidDeleted() bool { if !m.IsValid() { return false } valid := m.Valid() headerType := valid.ClientHeader.MessageType switch headerType { case MessageType_NONE: return false case MessageType_TLFNAME: // Undeletable and may have no body return false } bodyType, err := valid.MessageBody.MessageType() if err != nil { return false } return bodyType == MessageType_NONE } func (m MessageUnboxed) IsVisible() bool { typ := m.GetMessageType() return slices.Contains(VisibleChatMessageTypes(), typ) } func (m MessageUnboxed) HasReactions() bool { if !m.IsValid() { return false } return len(m.Valid().Reactions.Reactions) > 0 } func (m MessageUnboxed) HasUnfurls() bool { if !m.IsValid() { return false } return len(m.Valid().Unfurls) > 0 } func (m MessageUnboxed) SearchableText() string { if !m.IsValidFull() { return "" } return m.Valid().MessageBody.SearchableText() } func (m MessageUnboxed) SenderUsername() string { if !m.IsValid() { return "" } return m.Valid().SenderUsername } func (m MessageUnboxed) Ctime() gregor1.Time { if !m.IsValid() { return 0 } return m.Valid().ServerHeader.Ctime } func (m MessageUnboxed) AtMentionUsernames() []string { if !m.IsValid() { return nil } return m.Valid().AtMentionUsernames } func (m MessageUnboxed) ChannelMention() ChannelMention { if !m.IsValid() { return ChannelMention_NONE } return m.Valid().ChannelMention } func (m MessageUnboxed) SenderIsBot() bool { if m.IsValid() { valid := m.Valid() return gregor1.UIDPtrEq(valid.ClientHeader.BotUID, &valid.ClientHeader.Sender) } return false } func (m *MessageUnboxed) DebugString() string { if m == nil { return "[nil]" } state, err := m.State() if err != nil { return fmt.Sprintf("[INVALID err:%v]", err) } if state == MessageUnboxedState_ERROR { merr := m.Error() return fmt.Sprintf("[%v %v mt:%v (%v) (%v)]", state, m.GetMessageID(), merr.ErrType, merr.ErrMsg, merr.InternalErrMsg) } switch state { case MessageUnboxedState_VALID: valid := m.Valid() headerType := valid.ClientHeader.MessageType s := fmt.Sprintf("%v %v", state, valid.ServerHeader.MessageID) bodyType, err := valid.MessageBody.MessageType() if err != nil { return fmt.Sprintf("[INVALID-BODY err:%v]", err) } if headerType == bodyType { s = fmt.Sprintf("%v %v", s, headerType) } else { if headerType == MessageType_TLFNAME { s = fmt.Sprintf("%v h:%v (b:%v)", s, headerType, bodyType) } else { s = fmt.Sprintf("%v h:%v != b:%v", s, headerType, bodyType) } } if valid.ServerHeader.SupersededBy != 0 { s = fmt.Sprintf("%v supBy:%v", s, valid.ServerHeader.SupersededBy) } return fmt.Sprintf("[%v]", s) case MessageUnboxedState_OUTBOX: obr := m.Outbox() var ostateStr string ostate, err := obr.State.State() if err != nil { ostateStr = "CORRUPT" } else { ostateStr = fmt.Sprintf("%v", ostate) } return fmt.Sprintf("[%v obid:%v prev:%v ostate:%v %v]", state, obr.OutboxID, obr.Msg.ClientHeader.OutboxInfo.Prev, ostateStr, obr.Msg.ClientHeader.MessageType) case MessageUnboxedState_JOURNEYCARD: jc := m.Journeycard() return fmt.Sprintf("[JOURNEYCARD %v]", jc.CardType) default: return fmt.Sprintf("[state:%v %v]", state, m.GetMessageID()) } } func MessageUnboxedDebugStrings(ms []MessageUnboxed) (res []string) { for _, m := range ms { res = append(res, m.DebugString()) } return res } func MessageUnboxedDebugList(ms []MessageUnboxed) string { return fmt.Sprintf("{ %v %v }", len(ms), strings.Join(MessageUnboxedDebugStrings(ms), ",")) } func MessageUnboxedDebugLines(ms []MessageUnboxed) string { return strings.Join(MessageUnboxedDebugStrings(ms), "\n") } const ( VersionErrorMessageBoxed VersionKind = "messageboxed" VersionErrorHeader VersionKind = "header" VersionErrorBody VersionKind = "body" ) // NOTE: these values correspond to the maximum accepted values in // chat/boxer.go. If these values are changed, they must also be accepted // there. var ( MaxMessageBoxedVersion MessageBoxedVersion = MessageBoxedVersion_V4 MaxHeaderVersion HeaderPlaintextVersion = HeaderPlaintextVersion_V1 MaxBodyVersion BodyPlaintextVersion = BodyPlaintextVersion_V2 ) // ParseableVersion checks if this error has a version that is now able to be // understood by our client. func (m MessageUnboxedError) ParseableVersion() bool { switch m.ErrType { case MessageUnboxedErrorType_BADVERSION, MessageUnboxedErrorType_BADVERSION_CRITICAL: // only applies to these types default: return false } kind := m.VersionKind version := m.VersionNumber // This error was stored from an old client, we have parse out the info we // need from the error message. // TODO remove this check once it has be live for a few cycles. if kind == "" && version == 0 { re := regexp.MustCompile(`.* Chat version error: \[ unhandled: (\w+) version: (\d+) .*\]`) matches := re.FindStringSubmatch(m.ErrMsg) if len(matches) != 3 { return false } kind = VersionKind(matches[1]) var err error version, err = strconv.Atoi(matches[2]) if err != nil { return false } } var maxVersion int switch kind { case VersionErrorMessageBoxed: maxVersion = int(MaxMessageBoxedVersion) case VersionErrorHeader: maxVersion = int(MaxHeaderVersion) case VersionErrorBody: maxVersion = int(MaxBodyVersion) default: return false } return maxVersion >= version } func (m MessageUnboxedError) IsEphemeralError() bool { return m.IsEphemeral && (m.ErrType == MessageUnboxedErrorType_EPHEMERAL || m.ErrType == MessageUnboxedErrorType_PAIRWISE_MISSING) } func (m MessageUnboxedError) IsEphemeralExpired(now time.Time) bool { if !m.IsEphemeral { return false } etime := m.Etime.Time() // There are a few ways a message could be considered expired // 1. We were "exploded now" // 2. Our lifetime is up return m.ExplodedBy != nil || etime.Before(now) || etime.Equal(now) } func (m MessageUnboxedError) HideExplosion(maxDeletedUpto MessageID, now time.Time) bool { if !m.IsEphemeral { return false } etime := m.Etime // Don't show ash lines for messages that have been expunged. return etime.Time().Add(ShowExplosionLifetime).Before(now) || m.MessageID < maxDeletedUpto } func (m MessageUnboxedError) SenderEq(o MessageUnboxedError) bool { return m.SenderUsername == o.SenderUsername } func (m OutboxRecord) SenderEq(o OutboxRecord) bool { return m.Msg.ClientHeader.Sender.Eq(o.Msg.ClientHeader.Sender) } func (m MessageUnboxedValid) AsDeleteHistory() (res MessageDeleteHistory, err error) { if m.ClientHeader.MessageType != MessageType_DELETEHISTORY { return res, fmt.Errorf("message is %v not %v", m.ClientHeader.MessageType, MessageType_DELETEHISTORY) } if m.MessageBody.IsNil() { return res, fmt.Errorf("missing message body") } btyp, err := m.MessageBody.MessageType() if err != nil { return res, err } if btyp != MessageType_DELETEHISTORY { return res, fmt.Errorf("message has wrong body type: %v", btyp) } return m.MessageBody.Deletehistory(), nil } func (m *MsgEphemeralMetadata) String() string { if m == nil { return "<nil>" } var explodedBy string if m.ExplodedBy == nil { explodedBy = "<nil>" } else { explodedBy = *m.ExplodedBy } return fmt.Sprintf("{ Lifetime: %v, Generation: %v, ExplodedBy: %v }", m.Lifetime.ToDuration(), m.Generation, explodedBy) } func (m MessagePlaintext) MessageType() MessageType { typ, err := m.MessageBody.MessageType() if err != nil { return MessageType_NONE } return typ } func (m MessagePlaintext) IsVisible() bool { typ := m.MessageType() return slices.Contains(VisibleChatMessageTypes(), typ) } func (m MessagePlaintext) IsBadgableType() bool { typ := m.MessageType() switch typ { case MessageType_TEXT, MessageType_ATTACHMENT: return true default: return false } } func (m MessagePlaintext) SearchableText() string { return m.MessageBody.SearchableText() } func (m MessagePlaintext) IsEphemeral() bool { return m.EphemeralMetadata() != nil } func (m MessagePlaintext) EphemeralMetadata() *MsgEphemeralMetadata { return m.ClientHeader.EphemeralMetadata } func (o *MsgEphemeralMetadata) Eq(r *MsgEphemeralMetadata) bool { if o != nil && r != nil { return *o == *r } return (o == nil) && (r == nil) } func (m MessageUnboxedValid) SenderEq(o MessageUnboxedValid) bool { return m.ClientHeader.Sender.Eq(o.ClientHeader.Sender) } func (m MessageUnboxedValid) HasPairwiseMacs() bool { return m.ClientHeader.HasPairwiseMacs } func (m MessageUnboxedValid) IsEphemeral() bool { return m.EphemeralMetadata() != nil } func (m MessageUnboxedValid) EphemeralMetadata() *MsgEphemeralMetadata { return m.ClientHeader.EphemeralMetadata } func (m MessageUnboxedValid) ExplodedBy() *string { if !m.IsEphemeral() { return nil } return m.EphemeralMetadata().ExplodedBy } func Etime(lifetime gregor1.DurationSec, ctime, rtime, now gregor1.Time) gregor1.Time { originalLifetime := lifetime.ToDuration() elapsedLifetime := now.Time().Sub(ctime.Time()) remainingLifetime := min( // If the server's view doesn't make sense, just use the signed lifetime // from the message. originalLifetime-elapsedLifetime, originalLifetime) etime := rtime.Time().Add(remainingLifetime) return gregor1.ToTime(etime) } func (m MessageUnboxedValid) Etime() gregor1.Time { // The server sends us (untrusted) ctime of the message and server's view // of now. We use these to calculate the remaining lifetime on an ephemeral // message, returning an etime based on our received time. metadata := m.EphemeralMetadata() if metadata == nil { return 0 } header := m.ServerHeader return Etime(metadata.Lifetime, header.Ctime, m.ClientHeader.Rtime, header.Now) } func (m MessageUnboxedValid) RemainingEphemeralLifetime(now time.Time) time.Duration { remainingLifetime := m.Etime().Time().Sub(now).Round(time.Second) return remainingLifetime } func (m MessageUnboxedValid) IsEphemeralExpired(now time.Time) bool { if !m.IsEphemeral() { return false } etime := m.Etime().Time() // There are a few ways a message could be considered expired // 1. Our body is already nil (deleted from DELETEHISTORY or a server purge) // 2. We were "exploded now" // 3. Our lifetime is up return m.MessageBody.IsNil() || m.EphemeralMetadata().ExplodedBy != nil || etime.Before(now) || etime.Equal(now) } func (m MessageUnboxedValid) HideExplosion(maxDeletedUpto MessageID, now time.Time) bool { if !m.IsEphemeral() { return false } etime := m.Etime() // Don't show ash lines for messages that have been expunged. return etime.Time().Add(ShowExplosionLifetime).Before(now) || m.ServerHeader.MessageID < maxDeletedUpto } func (b MessageBody) IsNil() bool { return b == MessageBody{} } func (b MessageBody) IsType(typ MessageType) bool { btyp, err := b.MessageType() if err != nil { return false } return btyp == typ } func (b MessageBody) TextForDecoration() string { typ, err := b.MessageType() if err != nil { return "" } switch typ { case MessageType_REACTION: return b.Reaction().Body case MessageType_HEADLINE: return b.Headline().Headline case MessageType_ATTACHMENT: // Exclude the filename for text decoration. return b.Attachment().Object.Title default: return b.SearchableText() } } func (b MessageBody) SearchableText() string { typ, err := b.MessageType() if err != nil { return "" } switch typ { case MessageType_TEXT: return b.Text().Body case MessageType_EDIT: return b.Edit().Body case MessageType_REQUESTPAYMENT: return b.Requestpayment().Note case MessageType_ATTACHMENT: return b.Attachment().GetTitle() case MessageType_FLIP: return b.Flip().Text case MessageType_UNFURL: return b.Unfurl().SearchableText() case MessageType_SYSTEM: return b.System().String() default: return "" } } func (b MessageBody) GetEmojis() map[string]HarvestedEmoji { typ, err := b.MessageType() if err != nil { return nil } switch typ { case MessageType_TEXT: return b.Text().Emojis case MessageType_REACTION: return b.Reaction().Emojis case MessageType_EDIT: return b.Edit().Emojis case MessageType_ATTACHMENT: return b.Attachment().Emojis case MessageType_HEADLINE: return b.Headline().Emojis default: return nil } } func (m UIMessage) IsValid() bool { if state, err := m.State(); err == nil { return state == MessageUnboxedState_VALID } return false } func (m UIMessage) IsError() bool { if state, err := m.State(); err == nil { return state == MessageUnboxedState_ERROR } return false } func (m UIMessage) IsOutbox() bool { if state, err := m.State(); err == nil { return state == MessageUnboxedState_OUTBOX } return false } func (m UIMessage) IsPlaceholder() bool { if state, err := m.State(); err == nil { return state == MessageUnboxedState_PLACEHOLDER } return false } func (m UIMessage) GetMessageID() MessageID { if state, err := m.State(); err == nil { if state == MessageUnboxedState_VALID { return m.Valid().MessageID } if state == MessageUnboxedState_ERROR { return m.Error().MessageID } if state == MessageUnboxedState_PLACEHOLDER { return m.Placeholder().MessageID } } return 0 } func (m UIMessage) GetOutboxID() *OutboxID { if state, err := m.State(); err == nil { if state == MessageUnboxedState_VALID { strOutboxID := m.Valid().OutboxID if strOutboxID != nil { outboxID, err := MakeOutboxID(*strOutboxID) if err != nil { return nil } return &outboxID } return nil } if state == MessageUnboxedState_ERROR { return nil } if state == MessageUnboxedState_PLACEHOLDER { return nil } } return nil } func (m UIMessage) GetMessageType() MessageType { state, err := m.State() if err != nil { return MessageType_NONE } switch state { case MessageUnboxedState_VALID: body := m.Valid().MessageBody typ, err := body.MessageType() if err != nil { return MessageType_NONE } return typ case MessageUnboxedState_ERROR: return m.Error().MessageType case MessageUnboxedState_OUTBOX: return m.Outbox().MessageType default: return MessageType_NONE } } func (m UIMessage) SearchableText() string { if !m.IsValid() { return "" } return m.Valid().MessageBody.SearchableText() } func (m UIMessage) IsEphemeral() bool { state, err := m.State() if err != nil { return false } switch state { case MessageUnboxedState_VALID: return m.Valid().IsEphemeral case MessageUnboxedState_ERROR: return m.Error().IsEphemeral default: return false } } func (m MessageBoxed) GetMessageID() MessageID { return m.ServerHeader.MessageID } func (m MessageBoxed) Ctime() gregor1.Time { return m.ServerHeader.Ctime } func (m MessageBoxed) GetMessageType() MessageType { return m.ClientHeader.MessageType } func (m MessageBoxed) Summary() MessageSummary { s := MessageSummary{ MsgID: m.GetMessageID(), MessageType: m.GetMessageType(), TlfName: m.ClientHeader.TlfName, TlfPublic: m.ClientHeader.TlfPublic, } if m.ServerHeader != nil { s.Ctime = m.ServerHeader.Ctime } return s } func (m MessageBoxed) OutboxInfo() *OutboxInfo { return m.ClientHeader.OutboxInfo } func (m MessageBoxed) KBFSEncrypted() bool { return m.ClientHeader.KbfsCryptKeysUsed == nil || *m.ClientHeader.KbfsCryptKeysUsed } func (m MessageBoxed) EphemeralMetadata() *MsgEphemeralMetadata { return m.ClientHeader.EphemeralMetadata } func (m MessageBoxed) IsEphemeral() bool { return m.EphemeralMetadata() != nil } func (m MessageBoxed) Etime() gregor1.Time { // The server sends us (untrusted) ctime of the message and server's view // of now. We use these to calculate the remaining lifetime on an ephemeral // message, returning an etime based on the current time. metadata := m.EphemeralMetadata() if metadata == nil { return 0 } rtime := gregor1.ToTime(time.Now()) if m.ServerHeader.Rtime != nil { rtime = *m.ServerHeader.Rtime } return Etime(metadata.Lifetime, m.ServerHeader.Ctime, rtime, m.ServerHeader.Now) } func (m MessageBoxed) IsEphemeralExpired(now time.Time) bool { if !m.IsEphemeral() { return false } etime := m.Etime().Time() return m.EphemeralMetadata().ExplodedBy != nil || etime.Before(now) || etime.Equal(now) } func (m MessageBoxed) ExplodedBy() *string { if !m.IsEphemeral() { return nil } return m.EphemeralMetadata().ExplodedBy } var ConversationStatusGregorMap = map[ConversationStatus]string{ ConversationStatus_UNFILED: "unfiled", ConversationStatus_FAVORITE: "favorite", ConversationStatus_IGNORED: "ignored", ConversationStatus_BLOCKED: "blocked", ConversationStatus_MUTED: "muted", ConversationStatus_REPORTED: "reported", } var ConversationStatusGregorRevMap = map[string]ConversationStatus{ "unfiled": ConversationStatus_UNFILED, "favorite": ConversationStatus_FAVORITE, "ignored": ConversationStatus_IGNORED, "blocked": ConversationStatus_BLOCKED, "muted": ConversationStatus_MUTED, "reported": ConversationStatus_REPORTED, } var sha256Pool = sync.Pool{ New: func() any { return sha256.New() }, } func (t ConversationIDTriple) Hash() []byte { h := sha256Pool.Get().(hash.Hash) defer sha256Pool.Put(h) h.Reset() h.Write(t.Tlfid) h.Write(t.TopicID) h.Write([]byte(strconv.Itoa(int(t.TopicType)))) hash := h.Sum(nil) return hash } func (t ConversationIDTriple) ToConversationID(shardID [2]byte) ConversationID { h := t.Hash() h[0], h[1] = shardID[0], shardID[1] return ConversationID(h) } func (t ConversationIDTriple) Derivable(cid ConversationID) bool { h := t.Hash() if len(h) <= 2 || len(cid) <= 2 { return false } return bytes.Equal(h[2:], []byte(cid[2:])) } func MakeOutboxID(s string) (OutboxID, error) { b, err := hex.DecodeString(s) return OutboxID(b), err } func (o *OutboxID) Eq(r *OutboxID) bool { if o != nil && r != nil { return bytes.Equal(*o, *r) } return (o == nil) && (r == nil) } func (o OutboxID) String() string { return hex.EncodeToString(o) } func (o OutboxID) Bytes() []byte { return []byte(o) } func (o *OutboxInfo) Eq(r *OutboxInfo) bool { if o != nil && r != nil { return *o == *r } return (o == nil) && (r == nil) } func (o OutboxRecord) IsError() bool { state, err := o.State.State() if err != nil { return false } return state == OutboxStateType_ERROR } func (o OutboxRecord) IsSending() bool { state, err := o.State.State() if err != nil { return false } return state == OutboxStateType_SENDING } func (o OutboxRecord) IsAttachment() bool { return o.Msg.ClientHeader.MessageType == MessageType_ATTACHMENT } func (o OutboxRecord) IsUnfurl() bool { return o.Msg.ClientHeader.MessageType == MessageType_UNFURL } func (o OutboxRecord) IsChatFlip() bool { return o.Msg.ClientHeader.MessageType == MessageType_FLIP && o.Msg.ClientHeader.Conv.TopicType == TopicType_CHAT } func (o OutboxRecord) MessageType() MessageType { return o.Msg.ClientHeader.MessageType } func (o OutboxRecord) IsBadgable() bool { return o.Msg.ClientHeader.Conv.TopicType == TopicType_CHAT && o.Msg.IsBadgableType() } func (p MessagePreviousPointer) Eq(other MessagePreviousPointer) bool { return (p.Id == other.Id) && (p.Hash.Eq(other.Hash)) } // Visibility is a helper to get around a nil pointer for visibility, // and to get around TLFVisibility_ANY. The default is PRIVATE. // Note: not sure why visibility is a pointer, or what TLFVisibility_ANY // is for, but don't want to change the API. func (q *GetInboxLocalQuery) Visibility() keybase1.TLFVisibility { visibility := keybase1.TLFVisibility_PRIVATE if q.TlfVisibility != nil && *q.TlfVisibility == keybase1.TLFVisibility_PUBLIC { visibility = keybase1.TLFVisibility_PUBLIC } return visibility } // Visibility is a helper to get around a nil pointer for visibility, // and to get around TLFVisibility_ANY. The default is PRIVATE. // Note: not sure why visibility is a pointer, or what TLFVisibility_ANY // is for, but don't want to change the API. func (q *GetInboxQuery) Visibility() keybase1.TLFVisibility { visibility := keybase1.TLFVisibility_PRIVATE if q.TlfVisibility != nil && *q.TlfVisibility == keybase1.TLFVisibility_PUBLIC { visibility = keybase1.TLFVisibility_PUBLIC } return visibility } // TLFNameExpanded returns a TLF name with a reset suffix if it exists. // This version can be used in requests to lookup the TLF. func (c ConversationInfoLocal) TLFNameExpanded() string { return ExpandTLFName(c.TlfName, c.FinalizeInfo) } // TLFNameExpandedSummary returns a TLF name with a summary of the // account reset if there was one. // This version is for display purposes only and cannot be used to lookup the TLF. func (c ConversationInfoLocal) TLFNameExpandedSummary() string { if c.FinalizeInfo == nil { return c.TlfName } return c.TlfName + " " + c.FinalizeInfo.BeforeSummary() } // TLFNameExpanded returns a TLF name with a reset suffix if it exists. // This version can be used in requests to lookup the TLF. func (h MessageClientHeader) TLFNameExpanded(finalizeInfo *ConversationFinalizeInfo) string { return ExpandTLFName(h.TlfName, finalizeInfo) } // TLFNameExpanded returns a TLF name with a reset suffix if it exists. // This version can be used in requests to lookup the TLF. func (m MessageSummary) TLFNameExpanded(finalizeInfo *ConversationFinalizeInfo) string { return ExpandTLFName(m.TlfName, finalizeInfo) } func (h MessageClientHeaderVerified) TLFNameExpanded(finalizeInfo *ConversationFinalizeInfo) string { return ExpandTLFName(h.TlfName, finalizeInfo) } func (h MessageClientHeader) ToVerifiedForTesting() MessageClientHeaderVerified { if flag.Lookup("test.v") == nil { panic("MessageClientHeader.ToVerifiedForTesting used outside of test") } return MessageClientHeaderVerified{ Conv: h.Conv, TlfName: h.TlfName, TlfPublic: h.TlfPublic, MessageType: h.MessageType, Prev: h.Prev, Sender: h.Sender, SenderDevice: h.SenderDevice, OutboxID: h.OutboxID, OutboxInfo: h.OutboxInfo, } } // ExpandTLFName returns a TLF name with a reset suffix if it exists. // This version can be used in requests to lookup the TLF. func ExpandTLFName(name string, finalizeInfo *ConversationFinalizeInfo) string { if finalizeInfo == nil { return name } if len(finalizeInfo.ResetFull) == 0 { return name } if strings.Contains(name, " account reset ") { return name } return name + " " + finalizeInfo.ResetFull } // BeforeSummary returns a summary of the finalize without "files" in it. // The canonical name for a TLF after reset has a "(files before ... account reset...)" suffix // which doesn't make much sense in other uses (like chat). func (f *ConversationFinalizeInfo) BeforeSummary() string { return fmt.Sprintf("(before %s account reset %s)", f.ResetUser, f.ResetDate) } func (f *ConversationFinalizeInfo) IsResetForUser(username string) bool { // If reset user is the given user, or is blank (only way such a thing // could be in our inbox is if the current user is the one that reset) return f != nil && (f.ResetUser == username || f.ResetUser == "") } func (p *Pagination) Eq(other *Pagination) bool { if p == nil && other == nil { return true } if p != nil && other != nil { return p.Last == other.Last && bytes.Equal(p.Next, other.Next) && bytes.Equal(p.Previous, other.Previous) && p.Num == other.Num } return false } func (p *Pagination) String() string { if p == nil { return "<nil>" } return fmt.Sprintf("[Num: %d n: %s p: %s last: %v]", p.Num, hex.EncodeToString(p.Next), hex.EncodeToString(p.Previous), p.Last) } // FirstPage returns true if the pagination object is not pointing in any direction func (p *Pagination) FirstPage() bool { return p == nil || p.ForceFirstPage || (len(p.Next) == 0 && len(p.Previous) == 0) } func (c ConversationLocal) GetMtime() gregor1.Time { return c.ReaderInfo.Mtime } func (c ConversationLocal) GetConvID() ConversationID { return c.Info.Id } func (c ConversationLocal) GetTopicType() TopicType { return c.Info.Triple.TopicType } func (c ConversationLocal) GetMembersType() ConversationMembersType { return c.Info.MembersType } func (c ConversationLocal) GetTeamType() TeamType { return c.Info.TeamType } func (c ConversationLocal) GetFinalizeInfo() *ConversationFinalizeInfo { return c.Info.FinalizeInfo } func (c ConversationLocal) GetTopicName() string { return c.Info.TopicName } func (c ConversationLocal) GetExpunge() *Expunge { return &c.Expunge } func (c ConversationLocal) IsPublic() bool { return c.Info.Visibility == keybase1.TLFVisibility_PUBLIC } func (c ConversationLocal) GetMaxMessage(typ MessageType) (MessageSummary, error) { for _, msg := range c.MaxMessages { if msg.GetMessageType() == typ { return msg, nil } } return MessageSummary{}, fmt.Errorf("max message not found: %v", typ) } func (c ConversationLocal) GetMaxDeletedUpTo() MessageID { var maxExpungeID, maxDelHID MessageID if expunge := c.GetExpunge(); expunge != nil { maxExpungeID = expunge.Upto } if maxDelH, err := c.GetMaxMessage(MessageType_DELETEHISTORY); err != nil { maxDelHID = maxDelH.GetMessageID() } if maxExpungeID > maxDelHID { return maxExpungeID } return maxDelHID } func maxVisibleMsgIDFromSummaries(maxMessages []MessageSummary) MessageID { visibleTyps := VisibleChatMessageTypes() visibleTypsMap := map[MessageType]bool{} for _, typ := range visibleTyps { visibleTypsMap[typ] = true } maxMsgID := MessageID(0) for _, msg := range maxMessages { if _, ok := visibleTypsMap[msg.GetMessageType()]; ok && msg.GetMessageID() > maxMsgID { maxMsgID = msg.GetMessageID() } } return maxMsgID } func (c ConversationLocal) MaxVisibleMsgID() MessageID { return maxVisibleMsgIDFromSummaries(c.MaxMessages) } func (c ConversationLocal) ConvNameNames() (res []string) { for _, p := range c.Info.Participants { if p.InConvName { res = append(res, p.Username) } } return res } func (c ConversationLocal) AllNames() (res []string) { for _, p := range c.Info.Participants { res = append(res, p.Username) } return res } func (c ConversationLocal) FullNamesForSearch() (res []*string) { for _, p := range c.Info.Participants { res = append(res, p.Fullname) } return res } func (c ConversationLocal) CannotWrite() bool { if c.ConvSettings == nil || c.ConvSettings.MinWriterRoleInfo == nil { return false } return c.ConvSettings.MinWriterRoleInfo.CannotWrite } func (c Conversation) CannotWrite() bool { if c.ConvSettings == nil || c.ConvSettings.MinWriterRoleInfo == nil || c.ReaderInfo == nil { return false } return !c.ReaderInfo.UntrustedTeamRole.IsOrAbove(c.ConvSettings.MinWriterRoleInfo.Role) } func (c Conversation) GetMtime() gregor1.Time { return c.ReaderInfo.Mtime } func (c Conversation) GetConvID() ConversationID { return c.Metadata.ConversationID } func (c Conversation) GetTopicType() TopicType { return c.Metadata.IdTriple.TopicType } func (c Conversation) GetMembersType() ConversationMembersType { return c.Metadata.MembersType } func (c Conversation) GetTeamType() TeamType { return c.Metadata.TeamType } func (c Conversation) GetFinalizeInfo() *ConversationFinalizeInfo { return c.Metadata.FinalizeInfo } func (c Conversation) GetExpunge() *Expunge { return &c.Expunge } func (c Conversation) IsPublic() bool { return c.Metadata.Visibility == keybase1.TLFVisibility_PUBLIC } var errMaxMessageNotFound = errors.New("max message not found") func (c Conversation) GetMaxMessage(typ MessageType) (MessageSummary, error) { for _, msg := range c.MaxMsgSummaries { if msg.GetMessageType() == typ { return msg, nil } } return MessageSummary{}, errMaxMessageNotFound } func (c Conversation) Includes(uid gregor1.UID) bool { return slices.ContainsFunc(c.Metadata.ActiveList, uid.Eq) } func (c Conversation) GetMaxDeletedUpTo() MessageID { var maxExpungeID, maxDelHID MessageID if expunge := c.GetExpunge(); expunge != nil { maxExpungeID = expunge.Upto } if maxDelH, err := c.GetMaxMessage(MessageType_DELETEHISTORY); err != nil { maxDelHID = maxDelH.GetMessageID() } if maxExpungeID > maxDelHID { return maxExpungeID } return maxDelHID } func (c Conversation) GetMaxMessageID() MessageID { maxMsgID := MessageID(0) for _, msg := range c.MaxMsgSummaries { if msg.GetMessageID() > maxMsgID { maxMsgID = msg.GetMessageID() } } return maxMsgID } func (c Conversation) IsSelfFinalized(username string) bool { return c.GetMembersType() == ConversationMembersType_KBFS && c.GetFinalizeInfo().IsResetForUser(username) } func (c Conversation) MaxVisibleMsgID() MessageID { return maxVisibleMsgIDFromSummaries(c.MaxMsgSummaries) } func (c Conversation) IsUnread() bool { return c.IsUnreadFromMsgID(c.ReaderInfo.ReadMsgid) } func (c Conversation) IsUnreadFromMsgID(readMsgID MessageID) bool { maxMsgID := c.MaxVisibleMsgID() return maxMsgID > 0 && maxMsgID > readMsgID } func (c Conversation) HasMemberStatus(status ConversationMemberStatus) bool { if c.ReaderInfo != nil { return c.ReaderInfo.Status == status } return false } func (m MessageSummary) GetMessageID() MessageID { return m.MsgID } func (m MessageSummary) GetMessageType() MessageType { return m.MessageType } /* func ConvertMessageBodyV1ToV2(v1 MessageBodyV1) (MessageBody, error) { t, err := v1.MessageType() if err != nil { return MessageBody{}, err } switch t { case MessageType_TEXT: return NewMessageBodyWithText(v1.Text()), nil case MessageType_ATTACHMENT: previous := v1.Attachment() upgraded := MessageAttachment{ Object: previous.Object, Metadata: previous.Metadata, Uploaded: true, } if previous.Preview != nil { upgraded.Previews = []Asset{*previous.Preview} } return NewMessageBodyWithAttachment(upgraded), nil case MessageType_EDIT: return NewMessageBodyWithEdit(v1.Edit()), nil case MessageType_DELETE: return NewMessageBodyWithDelete(v1.Delete()), nil case MessageType_METADATA: return NewMessageBodyWithMetadata(v1.Metadata()), nil case MessageType_HEADLINE: return NewMessageBodyWithHeadline(v1.Headline()), nil case MessageType_NONE: return MessageBody{MessageType__: MessageType_NONE}, nil } return MessageBody{}, fmt.Errorf("ConvertMessageBodyV1ToV2: unhandled message type %v", t) } */ func (a *MerkleRoot) Eq(b *MerkleRoot) bool { if a != nil && b != nil { return (a.Seqno == b.Seqno) && bytes.Equal(a.Hash, b.Hash) } return (a == nil) && (b == nil) } func (d *SealedData) AsEncrypted() EncryptedData { return EncryptedData{ V: d.V, E: d.E, N: d.N, } } func (d *SealedData) AsSignEncrypted() SignEncryptedData { return SignEncryptedData{ V: d.V, E: d.E, N: d.N, } } func (d *EncryptedData) AsSealed() SealedData { return SealedData{ V: d.V, E: d.E, N: d.N, } } func (d *SignEncryptedData) AsSealed() SealedData { return SealedData{ V: d.V, E: d.E, N: d.N, } } func NewConversationErrorLocal( message string, remoteConv Conversation, unverifiedTLFName string, typ ConversationErrorType, rekeyInfo *ConversationErrorRekey, ) *ConversationErrorLocal { return &ConversationErrorLocal{ Typ: typ, Message: message, RemoteConv: remoteConv, UnverifiedTLFName: unverifiedTLFName, RekeyInfo: rekeyInfo, } } type OfflinableResult interface { SetOffline() } func (r *NonblockFetchRes) SetOffline() { r.Offline = true } func (r *UnreadlineRes) SetOffline() { r.Offline = true } func (r *MarkAsReadLocalRes) SetOffline() { r.Offline = true } func (r *MarkTLFAsReadLocalRes) SetOffline() { r.Offline = true } func (r *GetInboxAndUnboxLocalRes) SetOffline() { r.Offline = true } func (r *GetInboxAndUnboxUILocalRes) SetOffline() { r.Offline = true } func (r *GetThreadLocalRes) SetOffline() { r.Offline = true } func (r *GetInboxSummaryForCLILocalRes) SetOffline() { r.Offline = true } func (r *GetConversationForCLILocalRes) SetOffline() { r.Offline = true } func (r *GetMessagesLocalRes) SetOffline() { r.Offline = true } func (r *GetNextAttachmentMessageLocalRes) SetOffline() { r.Offline = true } func (r *FindConversationsLocalRes) SetOffline() { r.Offline = true } func (r *JoinLeaveConversationLocalRes) SetOffline() { r.Offline = true } func (r *PreviewConversationLocalRes) SetOffline() { r.Offline = true } func (r *GetTLFConversationsLocalRes) SetOffline() { r.Offline = true } func (r *GetChannelMembershipsLocalRes) SetOffline() { r.Offline = true } func (r *GetMutualTeamsLocalRes) SetOffline() { r.Offline = true } func (r *SetAppNotificationSettingsLocalRes) SetOffline() { r.Offline = true } func (r *DeleteConversationLocalRes) SetOffline() { r.Offline = true } func (r *SearchRegexpRes) SetOffline() { r.Offline = true } func (r *SearchInboxRes) SetOffline() { r.Offline = true } func (t TyperInfo) String() string { return fmt.Sprintf("typer(u:%s d:%s)", t.Username, t.DeviceID) } func (o TLFConvOrdinal) Int() int { return int(o) } func (o TLFConvOrdinal) IsFirst() bool { return o.Int() == 1 } func MakeEmptyUnreadUpdate(convID ConversationID) UnreadUpdate { counts := make(map[keybase1.DeviceType]int) counts[keybase1.DeviceType_DESKTOP] = 0 counts[keybase1.DeviceType_MOBILE] = 0 return UnreadUpdate{ ConvID: convID, UnreadMessages: 0, UnreadNotifyingMessages: counts, } } func (u UnreadUpdate) String() string { return fmt.Sprintf("[d:%v c:%s u:%d nd:%d nm:%d]", u.Diff, u.ConvID, u.UnreadMessages, u.UnreadNotifyingMessages[keybase1.DeviceType_DESKTOP], u.UnreadNotifyingMessages[keybase1.DeviceType_MOBILE]) } func (s TopicNameState) Bytes() []byte { return []byte(s) } func (s TopicNameState) Eq(o TopicNameState) bool { return bytes.Equal(s.Bytes(), o.Bytes()) } func (i InboxUIItem) GetConvID() ConversationID { bConvID, _ := hex.DecodeString(i.ConvID.String()) return ConversationID(bConvID) } type ByConversationMemberStatus []ConversationMemberStatus func (m ByConversationMemberStatus) Len() int { return len(m) } func (m ByConversationMemberStatus) Swap(i, j int) { m[i], m[j] = m[j], m[i] } func (m ByConversationMemberStatus) Less(i, j int) bool { return m[i] > m[j] } func AllConversationMemberStatuses() (res []ConversationMemberStatus) { for status := range ConversationMemberStatusRevMap { res = append(res, status) } sort.Sort(ByConversationMemberStatus(res)) return res } type ByConversationExistence []ConversationExistence func (m ByConversationExistence) Len() int { return len(m) } func (m ByConversationExistence) Swap(i, j int) { m[i], m[j] = m[j], m[i] } func (m ByConversationExistence) Less(i, j int) bool { return m[i] > m[j] } func AllConversationExistences() (res []ConversationExistence) { for existence := range ConversationExistenceRevMap { res = append(res, existence) } sort.Sort(ByConversationExistence(res)) return res } func (v InboxVers) ToConvVers() ConversationVers { return ConversationVers(v) } func (p ConversationIDMessageIDPairs) Contains(convID ConversationID) (MessageID, bool) { for _, c := range p.Pairs { if c.ConvID.Eq(convID) { return c.MsgID, true } } return MessageID(0), false } func (c ConversationMemberStatus) ToGregorDBString() (string, error) { s, ok := ConversationMemberStatusRevMap[c] if !ok { return "", fmt.Errorf("unrecoginzed ConversationMemberStatus: %v", c) } return strings.ToLower(s), nil } func (c ConversationMemberStatus) ToGregorDBStringAssert() string { s, err := c.ToGregorDBString() if err != nil { panic(err) } return s } func humanizeDuration(duration time.Duration) string { var value float64 var unit string if int(duration.Hours()) >= 24 { value = duration.Hours() / 24 unit = "day" } else if int(duration.Hours()) >= 1 { value = duration.Hours() unit = "hour" } else if int(duration.Minutes()) >= 1 { value = duration.Minutes() unit = "minute" } else if int(duration.Seconds()) >= 1 { value = duration.Seconds() unit = "second" } else { return "" } if int(value) > 1 { unit += "s" } return fmt.Sprintf("%.0f %s", value, unit) } func (p RetentionPolicy) Eq(o RetentionPolicy) bool { typ1, err := p.Typ() if err != nil { return false } typ2, err := o.Typ() if err != nil { return false } if typ1 != typ2 { return false } switch typ1 { case RetentionPolicyType_NONE: return true case RetentionPolicyType_RETAIN: return p.Retain() == o.Retain() case RetentionPolicyType_EXPIRE: return p.Expire() == o.Expire() case RetentionPolicyType_INHERIT: return p.Inherit() == o.Inherit() case RetentionPolicyType_EPHEMERAL: return p.Ephemeral() == o.Ephemeral() default: return false } } func (p RetentionPolicy) HumanSummary() (summary string) { typ, err := p.Typ() if err != nil { return "" } switch typ { case RetentionPolicyType_NONE, RetentionPolicyType_RETAIN: summary = "be retained indefinitely" case RetentionPolicyType_EXPIRE: duration := humanizeDuration(p.Expire().Age.ToDuration()) if duration != "" { summary = fmt.Sprintf("expire after %s", duration) } case RetentionPolicyType_EPHEMERAL: duration := humanizeDuration(p.Ephemeral().Age.ToDuration()) if duration != "" { summary = fmt.Sprintf("explode after %s by default", duration) } } if summary != "" { summary = fmt.Sprintf("Messages will %s", summary) } return summary } func (p RetentionPolicy) Summary() string { typ, err := p.Typ() if err != nil { return "{variant error}" } switch typ { case RetentionPolicyType_EXPIRE: return fmt.Sprintf("{%v age:%v}", typ, p.Expire().Age.ToDuration()) case RetentionPolicyType_EPHEMERAL: return fmt.Sprintf("{%v age:%v}", typ, p.Ephemeral().Age.ToDuration()) default: return fmt.Sprintf("{%v}", typ) } } func TeamIDToTLFID(teamID keybase1.TeamID) (TLFID, error) { return MakeTLFID(teamID.String()) } func (r *NonblockFetchRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *NonblockFetchRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *UnreadlineRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *UnreadlineRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *MarkAsReadLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *MarkAsReadLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *MarkTLFAsReadLocalRes) GetRateLimit() (res []RateLimit) { return r.RateLimits } func (r *MarkTLFAsReadLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetInboxAndUnboxLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetInboxAndUnboxLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetAllResetConvMembersRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetAllResetConvMembersRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *LoadFlipRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *LoadFlipRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetInboxAndUnboxUILocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetInboxAndUnboxUILocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetThreadLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetThreadLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *NewConversationLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *NewConversationLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetInboxSummaryForCLILocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetInboxSummaryForCLILocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetMessagesLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetMessagesLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetNextAttachmentMessageLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetNextAttachmentMessageLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *SetConversationStatusLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *SetConversationStatusLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *PostLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *PostLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetConversationForCLILocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetConversationForCLILocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *PostLocalNonblockRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *PostLocalNonblockRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *DownloadAttachmentLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *DownloadAttachmentLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *DownloadFileAttachmentLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *DownloadFileAttachmentLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *FindConversationsLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *FindConversationsLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *JoinLeaveConversationLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *JoinLeaveConversationLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *PreviewConversationLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *PreviewConversationLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *DeleteConversationLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *DeleteConversationLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *RemoveFromConversationLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *RemoveFromConversationLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetTLFConversationsLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetTLFConversationsLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetChannelMembershipsLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetChannelMembershipsLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetMutualTeamsLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *GetMutualTeamsLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *SetAppNotificationSettingsLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *SetAppNotificationSettingsLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *SearchRegexpRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *SearchRegexpRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *SearchInboxRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *SearchInboxRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetInboxRemoteRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *GetInboxRemoteRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *GetInboxByTLFIDRemoteRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *GetInboxByTLFIDRemoteRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *GetThreadRemoteRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *GetThreadRemoteRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *GetConversationMetadataRemoteRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *GetConversationMetadataRemoteRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *PostRemoteRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *PostRemoteRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *NewConversationRemoteRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *NewConversationRemoteRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *GetMessagesRemoteRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *GetMessagesRemoteRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *MarkAsReadRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *MarkAsReadRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *SetConversationStatusRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *SetConversationStatusRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *GetPublicConversationsRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *GetPublicConversationsRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *JoinLeaveConversationRemoteRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *JoinLeaveConversationRemoteRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *DeleteConversationRemoteRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *DeleteConversationRemoteRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *RemoveFromConversationRemoteRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *RemoveFromConversationRemoteRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *GetMessageBeforeRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *GetMessageBeforeRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *GetTLFConversationsRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *GetTLFConversationsRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *SetAppNotificationSettingsRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *SetAppNotificationSettingsRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *SetRetentionRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *SetRetentionRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *LoadGalleryRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *LoadGalleryRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *ListBotCommandsLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *ListBotCommandsLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *PinMessageRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *PinMessageRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *ClearBotCommandsLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *ClearBotCommandsLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *ClearBotCommandsRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *ClearBotCommandsRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *AdvertiseBotCommandsLocalRes) GetRateLimit() []RateLimit { return r.RateLimits } func (r *AdvertiseBotCommandsLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *AdvertiseBotCommandsRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *AdvertiseBotCommandsRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *GetBotInfoRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *GetBotInfoRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *NewConversationsLocalRes) GetRateLimit() (res []RateLimit) { return r.RateLimits } func (r *NewConversationsLocalRes) SetRateLimits(rl []RateLimit) { r.RateLimits = rl } func (r *GetDefaultTeamChannelsLocalRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *GetDefaultTeamChannelsLocalRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *SetDefaultTeamChannelsLocalRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *SetDefaultTeamChannelsLocalRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *AddEmojiRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *AddEmojiRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *AddEmojisRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *AddEmojisRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *AddEmojiAliasRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *AddEmojiAliasRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *RemoveEmojiRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *RemoveEmojiRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (r *UserEmojiRes) GetRateLimit() (res []RateLimit) { if r.RateLimit != nil { res = []RateLimit{*r.RateLimit} } return res } func (r *UserEmojiRes) SetRateLimits(rl []RateLimit) { if len(rl) > 0 { r.RateLimit = &rl[0] } } func (i EphemeralPurgeInfo) IsNil() bool { return !i.IsActive && i.NextPurgeTime == 0 && i.MinUnexplodedID <= 1 } func (i EphemeralPurgeInfo) String() string { return fmt.Sprintf("EphemeralPurgeInfo{ ConvID: %v, IsActive: %v, NextPurgeTime: %v, MinUnexplodedID: %v }", i.ConvID, i.IsActive, i.NextPurgeTime.Time(), i.MinUnexplodedID) } func (i EphemeralPurgeInfo) Eq(o EphemeralPurgeInfo) bool { return (i.IsActive == o.IsActive && i.MinUnexplodedID == o.MinUnexplodedID && i.NextPurgeTime == o.NextPurgeTime && i.ConvID.Eq(o.ConvID)) } func (r ReactionMap) HasReactionFromUser(reactionText, username string) (found bool, reactionMsgID MessageID) { reactions, ok := r.Reactions[reactionText] if !ok { return false, 0 } reaction, ok := reactions[username] return ok, reaction.ReactionMsgID } func (r MessageReaction) Eq(o MessageReaction) bool { return r.Body == o.Body && r.MessageID == o.MessageID } func (i *ConversationMinWriterRoleInfoLocal) String() string { if i == nil { return "Minimum writer role for this conversation is not set." } changedBySuffix := "." if i.ChangedBy != "" { changedBySuffix = fmt.Sprintf(", last set by %v.", i.ChangedBy) } return fmt.Sprintf("Minimum writer role for this conversation is %v%v", i.Role, changedBySuffix) } func (s *ConversationSettings) IsNil() bool { return s == nil || s.MinWriterRoleInfo == nil } func (o SearchOpts) Matches(msg MessageUnboxed) bool { if o.SentAfter != 0 && msg.Ctime() < o.SentAfter { return false } if o.SentBefore != 0 && msg.Ctime() > o.SentBefore { return false } if o.SentBy != "" && msg.SenderUsername() != o.SentBy { return false } // Check if the user was @mentioned or there was a @here/@channel. if o.SentTo != "" { if o.MatchMentions { switch msg.ChannelMention() { case ChannelMention_ALL, ChannelMention_HERE: return true } } return slices.Contains(msg.AtMentionUsernames(), o.SentTo) } return true } func (a MessageAttachment) GetTitle() string { title := a.Object.Title if title == "" { title = filepath.Base(a.Object.Filename) } return title } func (u MessageUnfurl) SearchableText() string { typ, err := u.Unfurl.Unfurl.UnfurlType() if err != nil { return "" } switch typ { case UnfurlType_GENERIC: generic := u.Unfurl.Unfurl.Generic() res := generic.Title if generic.Description != nil { res += " " + *generic.Description } return res } return "" } func (h *ChatSearchInboxHit) Size() int { if h == nil { return 0 } return len(h.Hits) } func (u UnfurlRaw) GetUrl() string { typ, err := u.UnfurlType() if err != nil { return "" } switch typ { case UnfurlType_GENERIC: return u.Generic().Url case UnfurlType_GIPHY: if u.Giphy().ImageUrl != nil { return *u.Giphy().ImageUrl } } return "" } func (u UnfurlRaw) UnsafeDebugString() string { typ, err := u.UnfurlType() if err != nil { return "<error>" } switch typ { case UnfurlType_GENERIC: return u.Generic().UnsafeDebugString() case UnfurlType_GIPHY: return u.Giphy().UnsafeDebugString() } return "<unknown>" } func yieldStr(s *string) string { if s == nil { return "" } return *s } func (g UnfurlGenericRaw) UnsafeDebugString() string { publishTime := "" if g.PublishTime != nil { publishTime = fmt.Sprintf("%v", time.Unix(int64(*g.PublishTime), 0)) } return fmt.Sprintf(`Title: %s Url: %s SiteName: %s PublishTime: %s Description: %s ImageUrl: %s Video: %s FaviconUrl: %s`, g.Title, g.Url, g.SiteName, publishTime, yieldStr(g.Description), yieldStr(g.ImageUrl), g.Video, yieldStr(g.FaviconUrl)) } func (g UnfurlGiphyRaw) UnsafeDebugString() string { return fmt.Sprintf(`GIPHY SPECIAL FaviconUrl: %s ImageUrl: %s Video: %s`, yieldStr(g.FaviconUrl), yieldStr(g.ImageUrl), g.Video) } func (v UnfurlVideo) String() string { return fmt.Sprintf("[url: %s width: %d height: %d mime: %s]", v.Url, v.Width, v.Height, v.MimeType) } func NewUnfurlSettings() UnfurlSettings { return UnfurlSettings{ Mode: UnfurlMode_WHITELISTED, Whitelist: make(map[string]bool), } } func GlobalAppNotificationSettingsSorted() (res []GlobalAppNotificationSetting) { for setting := range GlobalAppNotificationSettingRevMap { if setting.Usage() != "" && setting.FlagName() != "" { res = append(res, setting) } } slices.Sort(res) return res } // Add to `Usage`/`FlagName` for a setting to be usable in the CLI via // `keybase notification-settings` func (g GlobalAppNotificationSetting) Usage() string { switch g { case GlobalAppNotificationSetting_NEWMESSAGES: return "Show notifications for new messages" case GlobalAppNotificationSetting_PLAINTEXTDESKTOP: return "Show plaintext notifications on desktop devices" case GlobalAppNotificationSetting_PLAINTEXTMOBILE: return "Show plaintext notifications on mobile devices" case GlobalAppNotificationSetting_DEFAULTSOUNDMOBILE: return "Use the default system sound on mobile devices" case GlobalAppNotificationSetting_DISABLETYPING: return "Disable sending/receiving typing notifications" case GlobalAppNotificationSetting_CONVERTHEIC: return "Convert HEIC images to JPEG for chat attachments (macOS and iOS only)" default: return "" } } func (g GlobalAppNotificationSetting) FlagName() string { switch g { case GlobalAppNotificationSetting_NEWMESSAGES: return "new-messages" case GlobalAppNotificationSetting_PLAINTEXTDESKTOP: return "plaintext-desktop" case GlobalAppNotificationSetting_PLAINTEXTMOBILE: return "plaintext-mobile" case GlobalAppNotificationSetting_DEFAULTSOUNDMOBILE: return "default-sound-mobile" case GlobalAppNotificationSetting_DISABLETYPING: return "disable-typing" case GlobalAppNotificationSetting_CONVERTHEIC: return "convert-heic" default: return "" } } func (m MessageSystemChangeRetention) String() string { var appliesTo string switch m.MembersType { case ConversationMembersType_TEAM: if m.IsTeam { appliesTo = "team" } else { appliesTo = "channel" } default: appliesTo = "conversation" } var inheritDescription string if m.IsInherit { inheritDescription = " to inherit from the team policy" } format := "%s changed the %s retention policy%s. %s" summary := m.Policy.HumanSummary() return fmt.Sprintf(format, m.User, appliesTo, inheritDescription, summary) } func (m MessageSystemBulkAddToConv) String() string { prefix := "Added %s to the conversation" var suffix string switch len(m.Usernames) { case 0: return "" case 1: suffix = m.Usernames[0] case 2: suffix = fmt.Sprintf("%s and %s", m.Usernames[0], m.Usernames[1]) default: suffix = fmt.Sprintf("%s and %d others", m.Usernames[0], len(m.Usernames)-1) } return fmt.Sprintf(prefix, suffix) } func withDeterminer(s string) string { r, size := utf8.DecodeRuneInString(s) if size == 0 || r == utf8.RuneError { return "a " + s } vowels := "aeiou" if strings.Contains(vowels, string(r)) { return "an " + s } return "a " + s } func (m MessageSystem) String() string { typ, err := m.SystemType() if err != nil { return "" } switch typ { case MessageSystemType_ADDEDTOTEAM: output := fmt.Sprintf("Added @%s to the team", m.Addedtoteam().Addee) if role := m.Addedtoteam().Role; role != keybase1.TeamRole_NONE { output += fmt.Sprintf(" as %v", withDeterminer(role.HumanString())) } return output case MessageSystemType_INVITEADDEDTOTEAM: var roleText string if role := m.Inviteaddedtoteam().Role; role != keybase1.TeamRole_NONE { roleText = fmt.Sprintf(" as %v", withDeterminer(role.HumanString())) } output := fmt.Sprintf("Added @%s to the team (invited by @%s%s)", m.Inviteaddedtoteam().Invitee, m.Inviteaddedtoteam().Inviter, roleText) return output case MessageSystemType_COMPLEXTEAM: return fmt.Sprintf("%s is now a 'big' team with multiple channels", m.Complexteam().Team) case MessageSystemType_CREATETEAM: return fmt.Sprintf("@%s created the team %s", m.Createteam().Creator, m.Createteam().Team) case MessageSystemType_GITPUSH: body := m.Gitpush() switch body.PushType { case keybase1.GitPushType_CREATEREPO: return fmt.Sprintf("git @%s created the repo %s", body.Pusher, body.RepoName) case keybase1.GitPushType_RENAMEREPO: return fmt.Sprintf("git @%s changed the name of the repo %s to %s", body.Pusher, body.PreviousRepoName, body.RepoName) default: total := keybase1.TotalNumberOfCommits(body.Refs) names := keybase1.RefNames(body.Refs) return fmt.Sprintf("git (%s) @%s pushed %d commits to %s", body.RepoName, body.Pusher, total, names) } case MessageSystemType_CHANGEAVATAR: return fmt.Sprintf("@%s changed team avatar", m.Changeavatar().User) case MessageSystemType_CHANGERETENTION: return m.Changeretention().String() case MessageSystemType_BULKADDTOCONV: return m.Bulkaddtoconv().String() case MessageSystemType_SBSRESOLVE: body := m.Sbsresolve() switch body.AssertionService { case "phone": return fmt.Sprintf("@%s verified their phone number %s and joined"+ " the conversation", body.Prover, body.AssertionUsername) case "email": return fmt.Sprintf("@%s verified their email address %s and joined"+ " the conversation", body.Prover, body.AssertionUsername) } return fmt.Sprintf("@%s proved they are %s on %s and joined"+ " the conversation", body.Prover, body.AssertionUsername, body.AssertionService) case MessageSystemType_NEWCHANNEL: body := m.Newchannel() if len(body.ConvIDs) > 1 { return fmt.Sprintf("@%s created #%s and %d other new channels", body.Creator, body.NameAtCreation, len(body.ConvIDs)-1) } return fmt.Sprintf("@%s created a new channel #%s", body.Creator, body.NameAtCreation) default: return "" } } func (m MessageHeadline) String() string { if m.Headline == "" { return "cleared the channel description" } return fmt.Sprintf("set the channel description: %v", m.Headline) } func isZero(v []byte) bool { for _, b := range v { if b != 0 { return false } } return true } func MakeFlipGameID(s string) (FlipGameID, error) { return hex.DecodeString(s) } func (g FlipGameID) String() string { return hex.EncodeToString(g) } func (g FlipGameID) FlipGameIDStr() FlipGameIDStr { return FlipGameIDStr(g.String()) } func (g FlipGameID) Eq(h FlipGameID) bool { return hmac.Equal(g[:], h[:]) } func (g FlipGameID) IsZero() bool { return isZero(g[:]) } func (g FlipGameID) Check() bool { return g != nil && !g.IsZero() } func (o *SenderSendOptions) GetJoinMentionsAs() *ConversationMemberStatus { if o == nil { return nil } return o.JoinMentionsAs } func (c Coordinate) IsZero() bool { return c.Lat == 0 && c.Lon == 0 } func (c Coordinate) Eq(o Coordinate) bool { return c.Lat == o.Lat && c.Lon == o.Lon } type safeCoordinate struct { Lat float64 `codec:"lat" json:"lat"` Lon float64 `codec:"lon" json:"lon"` Accuracy float64 `codec:"accuracy" json:"accuracy"` } func (c Coordinate) MarshalJSON() ([]byte, error) { var safe safeCoordinate safe.Lat = c.Lat safe.Lon = c.Lon safe.Accuracy = c.Accuracy if math.IsNaN(safe.Lat) { safe.Lat = 0 } if math.IsNaN(safe.Lon) { safe.Lon = 0 } if math.IsNaN(safe.Accuracy) { safe.Accuracy = 0 } return json.Marshal(safe) } // Incremented if the client hash algorithm changes. If this value is changed // be sure to add a case in the BotInfo.Hash() function. const ClientBotInfoHashVers BotInfoHashVers = 2 // Incremented if the server sends down bad data and needs to bust client // caches. const ServerBotInfoHashVers BotInfoHashVers = 1 func (b BotInfo) Hash() BotInfoHash { hash := sha256Pool.Get().(hash.Hash) defer sha256Pool.Put(hash) hash.Reset() // Always hash in the server/client version. hash.Write([]byte(strconv.FormatUint(uint64(b.ServerHashVers), 10))) hash.Write([]byte(strconv.FormatUint(uint64(b.ClientHashVers), 10))) // This should cover all cases from 0..DefaultBotInfoHashVers. If // incrementing DefaultBotInfoHashVers be sure to add a case here. switch b.ClientHashVers { case 0, 1: b.hashV1(hash) case 2: b.hashV2(hash) default: // Every valid client version should be specifically handled, unit // tests verify that we have a non-empty hash output. hash.Reset() } return BotInfoHash(hash.Sum(nil)) } func (b BotInfo) hashV1(hash hash.Hash) { sort.Slice(b.CommandConvs, func(i, j int) bool { ikey := b.CommandConvs[i].Uid.String() + b.CommandConvs[i].ConvID.String() jkey := b.CommandConvs[j].Uid.String() + b.CommandConvs[j].ConvID.String() return ikey < jkey }) for _, cconv := range b.CommandConvs { hash.Write(cconv.ConvID) hash.Write(cconv.Uid) hash.Write([]byte(strconv.FormatUint(uint64(cconv.UntrustedTeamRole), 10))) hash.Write([]byte(strconv.FormatUint(uint64(cconv.Vers), 10))) } } func (b BotInfo) hashV2(hash hash.Hash) { sort.Slice(b.CommandConvs, func(i, j int) bool { ikey := b.CommandConvs[i].Uid.String() + b.CommandConvs[i].ConvID.String() jkey := b.CommandConvs[j].Uid.String() + b.CommandConvs[j].ConvID.String() return ikey < jkey }) for _, cconv := range b.CommandConvs { hash.Write(cconv.ConvID) hash.Write(cconv.Uid) hash.Write([]byte(strconv.FormatUint(uint64(cconv.UntrustedTeamRole), 10))) hash.Write([]byte(strconv.FormatUint(uint64(cconv.Vers), 10))) hash.Write([]byte(strconv.FormatUint(uint64(cconv.Typ), 10))) } } func (b BotInfoHash) Eq(h BotInfoHash) bool { return bytes.Equal(b, h) } func (p AdvertiseCommandsParam) ToRemote(cmdConvID ConversationID, tlfID *TLFID, adConvID *ConversationID) (res RemoteBotCommandsAdvertisement, err error) { switch p.Typ { case BotCommandsAdvertisementTyp_PUBLIC: if tlfID != nil { return res, errors.New("TLFID specified for public advertisement") } else if adConvID != nil { return res, errors.New("ConvID specified for public advertisement") } return NewRemoteBotCommandsAdvertisementWithPublic(RemoteBotCommandsAdvertisementPublic{ ConvID: cmdConvID, }), nil case BotCommandsAdvertisementTyp_TLFID_CONVS: if tlfID == nil { return res, errors.New("no TLFID specified") } else if adConvID != nil { return res, errors.New("ConvID specified") } return NewRemoteBotCommandsAdvertisementWithTlfidConvs(RemoteBotCommandsAdvertisementTLFID{ ConvID: cmdConvID, TlfID: *tlfID, }), nil case BotCommandsAdvertisementTyp_TLFID_MEMBERS: if tlfID == nil { return res, errors.New("no TLFID specified") } else if adConvID != nil { return res, errors.New("ConvID specified") } return NewRemoteBotCommandsAdvertisementWithTlfidMembers(RemoteBotCommandsAdvertisementTLFID{ ConvID: cmdConvID, TlfID: *tlfID, }), nil case BotCommandsAdvertisementTyp_CONV: if tlfID != nil { return res, errors.New("TLFID specified") } else if adConvID == nil { return res, errors.New("no ConvID specified") } return NewRemoteBotCommandsAdvertisementWithConv(RemoteBotCommandsAdvertisementConv{ ConvID: cmdConvID, AdvertiseConvID: *adConvID, }), nil default: return res, errors.New("unknown bot advertisement typ") } } func (p ClearBotCommandsFilter) ToRemote(tlfID *TLFID, convID *ConversationID) (res RemoteClearBotCommandsFilter, err error) { switch p.Typ { case BotCommandsAdvertisementTyp_PUBLIC: if tlfID != nil { return res, errors.New("TLFID specified for public advertisement") } else if convID != nil { return res, errors.New("ConvID specified for public advertisement") } return NewRemoteClearBotCommandsFilterWithPublic(RemoteClearBotCommandsFilterPublic{}), nil case BotCommandsAdvertisementTyp_TLFID_CONVS: if tlfID == nil { return res, errors.New("no TLFID specified") } else if convID != nil { return res, errors.New("ConvID specified") } return NewRemoteClearBotCommandsFilterWithTlfidConvs(RemoteClearBotCommandsFilterTLFID{ TlfID: *tlfID, }), nil case BotCommandsAdvertisementTyp_TLFID_MEMBERS: if tlfID == nil { return res, errors.New("no TLFID specified") } else if convID != nil { return res, errors.New("ConvID specified") } return NewRemoteClearBotCommandsFilterWithTlfidMembers(RemoteClearBotCommandsFilterTLFID{ TlfID: *tlfID, }), nil case BotCommandsAdvertisementTyp_CONV: if tlfID != nil { return res, errors.New("TLFID specified") } else if convID == nil { return res, errors.New("no ConvID specified") } return NewRemoteClearBotCommandsFilterWithConv(RemoteClearBotCommandsFilterConv{ ConvID: *convID, }), nil default: return res, errors.New("unknown bot advertisement typ") } } func GetAdvertTyp(typ string) (BotCommandsAdvertisementTyp, error) { switch typ { case "public": return BotCommandsAdvertisementTyp_PUBLIC, nil case "teamconvs": return BotCommandsAdvertisementTyp_TLFID_CONVS, nil case "teammembers": return BotCommandsAdvertisementTyp_TLFID_MEMBERS, nil case "conv": return BotCommandsAdvertisementTyp_CONV, nil default: 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) } } func (c UserBotCommandInput) ToOutput(username string) UserBotCommandOutput { return UserBotCommandOutput{ Name: c.Name, Description: c.Description, Usage: c.Usage, ExtendedDescription: c.ExtendedDescription, Username: username, } } func (r UIInboxReselectInfo) String() string { newConvStr := "<none>" if r.NewConvID != nil { newConvStr = string(*r.NewConvID) } return fmt.Sprintf("[oldconv: %s newconv: %s]", r.OldConvID, newConvStr) } func (e OutboxErrorType) IsBadgableError() bool { switch e { case OutboxErrorType_MISC, OutboxErrorType_OFFLINE, OutboxErrorType_TOOLONG, OutboxErrorType_EXPIRED, OutboxErrorType_TOOMANYATTEMPTS, OutboxErrorType_UPLOADFAILED: return true default: return false } } func (c UserBotCommandOutput) Matches(text string) bool { return strings.HasPrefix(text, fmt.Sprintf("!%s", c.Name)) } func (m AssetMetadata) IsType(typ AssetMetadataType) bool { mtyp, err := m.AssetType() if err != nil { return false } return mtyp == typ } type safeAssetMetadataImage struct { Width int `codec:"width" json:"width"` Height int `codec:"height" json:"height"` AudioAmps []float64 `codec:"audioAmps" json:"audioAmps"` } func (m AssetMetadataImage) MarshalJSON() ([]byte, error) { var safe safeAssetMetadataImage safe.AudioAmps = make([]float64, 0, len(m.AudioAmps)) for _, amp := range m.AudioAmps { if math.IsNaN(amp) { safe.AudioAmps = append(safe.AudioAmps, 0) } else { safe.AudioAmps = append(safe.AudioAmps, amp) } } safe.Width = m.Width safe.Height = m.Height return json.Marshal(safe) } func (s SnippetDecoration) ToEmoji() string { switch s { case SnippetDecoration_PENDING_MESSAGE: return ":outbox_tray:" case SnippetDecoration_FAILED_PENDING_MESSAGE: return ":warning:" case SnippetDecoration_EXPLODING_MESSAGE: return ":bomb:" case SnippetDecoration_EXPLODED_MESSAGE: return ":boom:" case SnippetDecoration_AUDIO_ATTACHMENT: return ":loud_sound:" case SnippetDecoration_VIDEO_ATTACHMENT: return ":film_frames:" case SnippetDecoration_PHOTO_ATTACHMENT: return ":camera:" case SnippetDecoration_FILE_ATTACHMENT: return ":file_folder:" case SnippetDecoration_STELLAR_RECEIVED: return ":bank:" case SnippetDecoration_STELLAR_SENT: return ":money_with_wings:" case SnippetDecoration_PINNED_MESSAGE: return ":pushpin:" default: return "" } } func (r EmojiRemoteSource) IsMessage() bool { typ, err := r.Typ() if err != nil { return false } return typ == EmojiRemoteSourceTyp_MESSAGE } func (r EmojiRemoteSource) IsStockAlias() bool { typ, err := r.Typ() if err != nil { return false } return typ == EmojiRemoteSourceTyp_STOCKALIAS } func (r EmojiRemoteSource) IsAlias() bool { return r.IsStockAlias() || (r.IsMessage() && r.Message().IsAlias) } func (r EmojiLoadSource) IsHTTPSrv() bool { typ, err := r.Typ() if err != nil { return false } return typ == EmojiLoadSourceTyp_HTTPSRV } func TeamToChatMemberDetails(teamMembers []keybase1.TeamMemberDetails) (chatMembers []ChatMemberDetails) { chatMembers = make([]ChatMemberDetails, len(teamMembers)) for i, teamMember := range teamMembers { chatMembers[i] = ChatMemberDetails{ Uid: teamMember.Uv.Uid, Username: teamMember.Username, FullName: teamMember.FullName, } } return chatMembers } func TeamToChatMembersDetails(details keybase1.TeamMembersDetails) ChatMembersDetails { return ChatMembersDetails{ Owners: TeamToChatMemberDetails(details.Owners), Admins: TeamToChatMemberDetails(details.Admins), Writers: TeamToChatMemberDetails(details.Writers), Readers: TeamToChatMemberDetails(details.Readers), Bots: TeamToChatMemberDetails(details.Bots), RestrictedBots: TeamToChatMemberDetails(details.RestrictedBots), } }