1package utils 2 3import ( 4 "crypto/sha256" 5 "encoding/base64" 6 "encoding/hex" 7 "encoding/json" 8 "errors" 9 "fmt" 10 "math" 11 "sort" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/keybase/xurls" 17 18 "github.com/keybase/client/go/chat/pager" 19 "github.com/keybase/client/go/chat/unfurl/display" 20 "github.com/keybase/go-framed-msgpack-rpc/rpc" 21 "github.com/kyokomi/emoji" 22 23 "regexp" 24 25 "github.com/keybase/client/go/chat/globals" 26 "github.com/keybase/client/go/chat/types" 27 "github.com/keybase/client/go/libkb" 28 "github.com/keybase/client/go/logger" 29 "github.com/keybase/client/go/protocol/chat1" 30 "github.com/keybase/client/go/protocol/gregor1" 31 "github.com/keybase/client/go/protocol/keybase1" 32 "github.com/keybase/go-codec/codec" 33 context "golang.org/x/net/context" 34 "golang.org/x/net/idna" 35) 36 37func AssertLoggedInUID(ctx context.Context, g *globals.Context) (uid gregor1.UID, err error) { 38 if !g.ActiveDevice.HaveKeys() { 39 return uid, libkb.LoginRequiredError{} 40 } 41 k1uid := g.Env.GetUID() 42 if k1uid.IsNil() { 43 return uid, libkb.LoginRequiredError{} 44 } 45 return gregor1.UID(k1uid.ToBytes()), nil 46} 47 48// parseDurationExtended is like time.ParseDuration, but adds "d" unit. "1d" is 49// one day, defined as 24*time.Hour. Only whole days are supported for "d" 50// unit, but it can be followed by smaller units, e.g., "1d1h". 51func ParseDurationExtended(s string) (d time.Duration, err error) { 52 p := strings.Index(s, "d") 53 if p == -1 { 54 // no "d" suffix 55 return time.ParseDuration(s) 56 } 57 58 var days int 59 if days, err = strconv.Atoi(s[:p]); err != nil { 60 return time.Duration(0), err 61 } 62 d = time.Duration(days) * 24 * time.Hour 63 64 if p < len(s)-1 { 65 var dur time.Duration 66 if dur, err = time.ParseDuration(s[p+1:]); err != nil { 67 return time.Duration(0), err 68 } 69 d += dur 70 } 71 72 return d, nil 73} 74 75func ParseTimeFromRFC3339OrDurationFromPast(g *globals.Context, s string) (t time.Time, err error) { 76 var errt, errd error 77 var d time.Duration 78 79 if s == "" { 80 return 81 } 82 83 if t, errt = time.Parse(time.RFC3339, s); errt == nil { 84 return t, nil 85 } 86 if d, errd = ParseDurationExtended(s); errd == nil { 87 return g.Clock().Now().Add(-d), nil 88 } 89 90 return time.Time{}, fmt.Errorf("given string is neither a valid time (%s) nor a valid duration (%v)", errt, errd) 91} 92 93// upper bounds takes higher priority 94func Collar(lower int, ideal int, upper int) int { 95 if ideal > upper { 96 return upper 97 } 98 if ideal < lower { 99 return lower 100 } 101 return ideal 102} 103 104// AggRateLimitsP takes a list of rate limit responses and dedups them to the last one received 105// of each category 106func AggRateLimitsP(rlimits []*chat1.RateLimit) (res []chat1.RateLimit) { 107 m := make(map[string]chat1.RateLimit, len(rlimits)) 108 for _, l := range rlimits { 109 if l != nil { 110 m[l.Name] = *l 111 } 112 } 113 res = make([]chat1.RateLimit, 0, len(m)) 114 for _, v := range m { 115 res = append(res, v) 116 } 117 return res 118} 119 120func AggRateLimits(rlimits []chat1.RateLimit) (res []chat1.RateLimit) { 121 m := make(map[string]chat1.RateLimit, len(rlimits)) 122 for _, l := range rlimits { 123 m[l.Name] = l 124 } 125 res = make([]chat1.RateLimit, 0, len(m)) 126 for _, v := range m { 127 res = append(res, v) 128 } 129 return res 130} 131 132func ReorderParticipantsKBFS(mctx libkb.MetaContext, g libkb.UIDMapperContext, umapper libkb.UIDMapper, 133 tlfName string, activeList []gregor1.UID) (writerNames []chat1.ConversationLocalParticipant, err error) { 134 srcWriterNames, _, _, err := splitAndNormalizeTLFNameCanonicalize(mctx, tlfName, false) 135 if err != nil { 136 return writerNames, err 137 } 138 return ReorderParticipants(mctx, g, umapper, tlfName, srcWriterNames, activeList) 139} 140 141// ReorderParticipants based on the order in activeList. 142// Only allows usernames from tlfname in the output. 143// This never fails, worse comes to worst it just returns the split of tlfname. 144func ReorderParticipants(mctx libkb.MetaContext, g libkb.UIDMapperContext, umapper libkb.UIDMapper, 145 tlfName string, verifiedMembers []string, activeList []gregor1.UID) (writerNames []chat1.ConversationLocalParticipant, err error) { 146 srcWriterNames, _, _, err := splitAndNormalizeTLFNameCanonicalize(mctx, tlfName, false) 147 if err != nil { 148 return writerNames, err 149 } 150 activeKuids := make([]keybase1.UID, 0, len(activeList)) 151 for _, a := range activeList { 152 activeKuids = append(activeKuids, keybase1.UID(a.String())) 153 } 154 allowedWriters := make(map[string]bool, len(verifiedMembers)+len(srcWriterNames)) 155 for _, user := range verifiedMembers { 156 allowedWriters[user] = true 157 } 158 convNameUsers := make(map[string]bool, len(srcWriterNames)) 159 for _, user := range srcWriterNames { 160 convNameUsers[user] = true 161 allowedWriters[user] = true 162 } 163 164 packages, err := umapper.MapUIDsToUsernamePackages(mctx.Ctx(), g, activeKuids, time.Hour*24, 165 10*time.Second, true) 166 activeMap := make(map[string]chat1.ConversationLocalParticipant) 167 if err == nil { 168 for i := 0; i < len(activeKuids); i++ { 169 part := UsernamePackageToParticipant(packages[i]) 170 part.InConvName = convNameUsers[part.Username] 171 activeMap[activeKuids[i].String()] = part 172 } 173 } 174 175 // Fill from the active list first. 176 for _, uid := range activeList { 177 kbUID := keybase1.UID(uid.String()) 178 p, ok := activeMap[kbUID.String()] 179 if !ok { 180 continue 181 } 182 if allowed := allowedWriters[p.Username]; allowed { 183 writerNames = append(writerNames, p) 184 // Allow only one occurrence. 185 allowedWriters[p.Username] = false 186 } 187 } 188 189 // Include participants even if they weren't in the active list, in stable order. 190 var leftOvers []chat1.ConversationLocalParticipant 191 for user, available := range allowedWriters { 192 if !available { 193 continue 194 } 195 part := UsernamePackageToParticipant(libkb.UsernamePackage{ 196 NormalizedUsername: libkb.NewNormalizedUsername(user), 197 FullName: nil, 198 }) 199 part.InConvName = convNameUsers[part.Username] 200 leftOvers = append(leftOvers, part) 201 allowedWriters[user] = false 202 } 203 sort.Slice(leftOvers, func(i, j int) bool { 204 return strings.Compare(leftOvers[i].Username, leftOvers[j].Username) < 0 205 }) 206 writerNames = append(writerNames, leftOvers...) 207 208 return writerNames, nil 209} 210 211// Drive splitAndNormalizeTLFName with one attempt to follow TlfNameNotCanonical. 212func splitAndNormalizeTLFNameCanonicalize(mctx libkb.MetaContext, name string, public bool) (writerNames, readerNames []string, extensionSuffix string, err error) { 213 writerNames, readerNames, extensionSuffix, err = SplitAndNormalizeTLFName(mctx, name, public) 214 if retryErr, retry := err.(TlfNameNotCanonical); retry { 215 return SplitAndNormalizeTLFName(mctx, retryErr.NameToTry, public) 216 } 217 return writerNames, readerNames, extensionSuffix, err 218} 219 220// AttachContactNames retrieves display names for SBS phones/emails that are in 221// the phonebook. ConversationLocalParticipant structures are modified in place 222// in `participants` passed in argument. 223func AttachContactNames(mctx libkb.MetaContext, participants []chat1.ConversationLocalParticipant) { 224 syncedContacts := mctx.G().SyncedContactList 225 if syncedContacts == nil { 226 mctx.Debug("AttachContactNames: SyncedContactList is nil") 227 return 228 } 229 var assertionToContactName map[string]string 230 var err error 231 contactsFetched := false 232 for i, participant := range participants { 233 if isPhoneOrEmail(participant.Username) { 234 if !contactsFetched { 235 assertionToContactName, err = syncedContacts.RetrieveAssertionToName(mctx) 236 if err != nil { 237 mctx.Debug("AttachContactNames: error fetching contacts: %s", err) 238 return 239 } 240 contactsFetched = true 241 } 242 if contactName, ok := assertionToContactName[participant.Username]; ok { 243 participant.ContactName = &contactName 244 } else { 245 participant.ContactName = nil 246 } 247 participants[i] = participant 248 } 249 } 250} 251 252func isPhoneOrEmail(username string) bool { 253 return strings.HasSuffix(username, "@phone") || strings.HasSuffix(username, "@email") 254} 255 256const ( 257 ChatTopicIDLen = 16 258 ChatTopicIDSuffix = 0x20 259) 260 261func NewChatTopicID() (id []byte, err error) { 262 if id, err = libkb.RandBytes(ChatTopicIDLen); err != nil { 263 return nil, err 264 } 265 id[len(id)-1] = ChatTopicIDSuffix 266 return id, nil 267} 268 269func AllChatConversationStatuses() (res []chat1.ConversationStatus) { 270 res = make([]chat1.ConversationStatus, 0, len(chat1.ConversationStatusMap)) 271 for _, s := range chat1.ConversationStatusMap { 272 res = append(res, s) 273 } 274 sort.Sort(byConversationStatus(res)) 275 return 276} 277 278// ConversationStatusBehavior describes how a ConversationStatus behaves 279type ConversationStatusBehavior struct { 280 // Whether to show the conv in the inbox 281 ShowInInbox bool 282 // Whether sending to this conv sets it back to UNFILED 283 SendingRemovesStatus bool 284 // Whether any incoming activity sets it back to UNFILED 285 ActivityRemovesStatus bool 286 // Whether to show desktop notifications 287 DesktopNotifications bool 288 // Whether to send push notifications 289 PushNotifications bool 290 // Whether to show as part of badging 291 ShowBadges bool 292} 293 294// ConversationMemberStatusBehavior describes how a ConversationMemberStatus behaves 295type ConversationMemberStatusBehavior struct { 296 // Whether to show the conv in the inbox 297 ShowInInbox bool 298 // Whether to show desktop notifications 299 DesktopNotifications bool 300 // Whether to send push notifications 301 PushNotifications bool 302 // Whether to show as part of badging 303 ShowBadges bool 304} 305 306func GetConversationMemberStatusBehavior(s chat1.ConversationMemberStatus) ConversationMemberStatusBehavior { 307 switch s { 308 case chat1.ConversationMemberStatus_ACTIVE: 309 return ConversationMemberStatusBehavior{ 310 ShowInInbox: true, 311 DesktopNotifications: true, 312 PushNotifications: true, 313 ShowBadges: true, 314 } 315 case chat1.ConversationMemberStatus_PREVIEW: 316 return ConversationMemberStatusBehavior{ 317 ShowInInbox: true, 318 DesktopNotifications: true, 319 PushNotifications: true, 320 ShowBadges: true, 321 } 322 case chat1.ConversationMemberStatus_LEFT: 323 return ConversationMemberStatusBehavior{ 324 ShowInInbox: false, 325 DesktopNotifications: false, 326 PushNotifications: false, 327 ShowBadges: false, 328 } 329 case chat1.ConversationMemberStatus_REMOVED: 330 return ConversationMemberStatusBehavior{ 331 ShowInInbox: false, 332 DesktopNotifications: false, 333 PushNotifications: false, 334 ShowBadges: false, 335 } 336 case chat1.ConversationMemberStatus_RESET: 337 return ConversationMemberStatusBehavior{ 338 ShowInInbox: true, 339 DesktopNotifications: false, 340 PushNotifications: false, 341 ShowBadges: false, 342 } 343 default: 344 return ConversationMemberStatusBehavior{ 345 ShowInInbox: true, 346 DesktopNotifications: true, 347 PushNotifications: true, 348 ShowBadges: true, 349 } 350 } 351} 352 353// GetConversationStatusBehavior gives information about what is allowed for a conversation status. 354// When changing these, be sure to update gregor's postMessage as well 355func GetConversationStatusBehavior(s chat1.ConversationStatus) ConversationStatusBehavior { 356 switch s { 357 case chat1.ConversationStatus_UNFILED: 358 return ConversationStatusBehavior{ 359 ShowInInbox: true, 360 SendingRemovesStatus: false, 361 ActivityRemovesStatus: false, 362 DesktopNotifications: true, 363 PushNotifications: true, 364 ShowBadges: true, 365 } 366 case chat1.ConversationStatus_FAVORITE: 367 return ConversationStatusBehavior{ 368 ShowInInbox: true, 369 SendingRemovesStatus: false, 370 ActivityRemovesStatus: false, 371 DesktopNotifications: true, 372 PushNotifications: true, 373 ShowBadges: true, 374 } 375 case chat1.ConversationStatus_IGNORED: 376 return ConversationStatusBehavior{ 377 ShowInInbox: false, 378 SendingRemovesStatus: true, 379 ActivityRemovesStatus: true, 380 DesktopNotifications: true, 381 PushNotifications: true, 382 ShowBadges: false, 383 } 384 case chat1.ConversationStatus_REPORTED: 385 fallthrough 386 case chat1.ConversationStatus_BLOCKED: 387 return ConversationStatusBehavior{ 388 ShowInInbox: false, 389 SendingRemovesStatus: true, 390 ActivityRemovesStatus: false, 391 DesktopNotifications: false, 392 PushNotifications: false, 393 ShowBadges: false, 394 } 395 case chat1.ConversationStatus_MUTED: 396 return ConversationStatusBehavior{ 397 ShowInInbox: true, 398 SendingRemovesStatus: false, 399 ActivityRemovesStatus: false, 400 DesktopNotifications: false, 401 PushNotifications: false, 402 ShowBadges: false, 403 } 404 default: 405 return ConversationStatusBehavior{ 406 ShowInInbox: true, 407 SendingRemovesStatus: false, 408 ActivityRemovesStatus: false, 409 DesktopNotifications: true, 410 PushNotifications: true, 411 ShowBadges: true, 412 } 413 } 414} 415 416type byConversationStatus []chat1.ConversationStatus 417 418func (c byConversationStatus) Len() int { return len(c) } 419func (c byConversationStatus) Less(i, j int) bool { return c[i] < c[j] } 420func (c byConversationStatus) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 421 422// Which convs show in the inbox. 423func VisibleChatConversationStatuses() (res []chat1.ConversationStatus) { 424 res = make([]chat1.ConversationStatus, 0, len(chat1.ConversationStatusMap)) 425 for _, s := range chat1.ConversationStatusMap { 426 if GetConversationStatusBehavior(s).ShowInInbox { 427 res = append(res, s) 428 } 429 } 430 sort.Sort(byConversationStatus(res)) 431 return 432} 433 434func checkMessageTypeQual(messageType chat1.MessageType, l []chat1.MessageType) bool { 435 for _, mt := range l { 436 if messageType == mt { 437 return true 438 } 439 } 440 return false 441} 442 443func IsVisibleChatMessageType(messageType chat1.MessageType) bool { 444 return checkMessageTypeQual(messageType, chat1.VisibleChatMessageTypes()) 445} 446 447func IsSnippetChatMessageType(messageType chat1.MessageType) bool { 448 return checkMessageTypeQual(messageType, chat1.SnippetChatMessageTypes()) 449} 450 451func IsBadgeableMessageType(messageType chat1.MessageType) bool { 452 return checkMessageTypeQual(messageType, chat1.BadgeableMessageTypes()) 453} 454 455func IsNonEmptyConvMessageType(messageType chat1.MessageType) bool { 456 return checkMessageTypeQual(messageType, chat1.NonEmptyConvMessageTypes()) 457} 458 459func IsEditableByEditMessageType(messageType chat1.MessageType) bool { 460 return checkMessageTypeQual(messageType, chat1.EditableMessageTypesByEdit()) 461} 462 463func IsDeleteableByDeleteMessageType(valid chat1.MessageUnboxedValid) bool { 464 if !checkMessageTypeQual(valid.ClientHeader.MessageType, chat1.DeletableMessageTypesByDelete()) { 465 return false 466 } 467 if !valid.MessageBody.IsType(chat1.MessageType_SYSTEM) { 468 return true 469 } 470 sysMsg := valid.MessageBody.System() 471 typ, err := sysMsg.SystemType() 472 if err != nil { 473 return true 474 } 475 return chat1.IsSystemMsgDeletableByDelete(typ) 476} 477 478func IsCollapsibleMessageType(messageType chat1.MessageType) bool { 479 switch messageType { 480 case chat1.MessageType_UNFURL, chat1.MessageType_ATTACHMENT: 481 return true 482 } 483 return false 484} 485 486func IsNotifiableChatMessageType(messageType chat1.MessageType, atMentions []gregor1.UID, 487 chanMention chat1.ChannelMention) bool { 488 switch messageType { 489 case chat1.MessageType_EDIT: 490 // an edit with atMention or channel mention should generate notifications 491 return len(atMentions) > 0 || chanMention != chat1.ChannelMention_NONE 492 case chat1.MessageType_REACTION: 493 // effect of this is all reactions will notify if they are sent to a person that 494 // is notified for any messages in the conversation 495 return true 496 case chat1.MessageType_JOIN, chat1.MessageType_LEAVE: 497 return false 498 default: 499 return IsVisibleChatMessageType(messageType) 500 } 501} 502 503type DebugLabeler struct { 504 libkb.Contextified 505 label string 506 verbose bool 507} 508 509func NewDebugLabeler(g *libkb.GlobalContext, label string, verbose bool) DebugLabeler { 510 return DebugLabeler{ 511 Contextified: libkb.NewContextified(g), 512 label: label, 513 verbose: verbose, 514 } 515} 516 517func (d DebugLabeler) GetLog() logger.Logger { 518 return d.G().GetLog() 519} 520 521func (d DebugLabeler) GetPerfLog() logger.Logger { 522 return d.G().GetPerfLog() 523} 524 525func (d DebugLabeler) showVerbose() bool { 526 return false 527} 528 529func (d DebugLabeler) showLog() bool { 530 if d.verbose { 531 return d.showVerbose() 532 } 533 return true 534} 535 536func (d DebugLabeler) Debug(ctx context.Context, msg string, args ...interface{}) { 537 if d.showLog() { 538 d.G().GetLog().CDebugf(ctx, "++Chat: | "+d.label+": "+msg, args...) 539 } 540} 541 542func (d DebugLabeler) Trace(ctx context.Context, err *error, format string, args ...interface{}) func() { 543 return d.trace(ctx, d.G().GetLog(), err, format, args...) 544} 545 546func (d DebugLabeler) PerfTrace(ctx context.Context, err *error, format string, args ...interface{}) func() { 547 return d.trace(ctx, d.G().GetPerfLog(), err, format, args...) 548} 549 550func (d DebugLabeler) trace(ctx context.Context, log logger.Logger, err *error, format string, args ...interface{}) func() { 551 if d.showLog() { 552 msg := fmt.Sprintf(format, args...) 553 start := time.Now() 554 log.CDebugf(ctx, "++Chat: + %s: %s", d.label, msg) 555 return func() { 556 log.CDebugf(ctx, "++Chat: - %s: %s -> %s [time=%v]", d.label, msg, 557 libkb.ErrToOkPtr(err), time.Since(start)) 558 } 559 } 560 return func() {} 561} 562 563// FilterByType filters messages based on a query. 564// If includeAllErrors then MessageUnboxedError are all returned. Otherwise, they are filtered based on type. 565// Messages whose type cannot be determined are considered errors. 566func FilterByType(msgs []chat1.MessageUnboxed, query *chat1.GetThreadQuery, includeAllErrors bool) (res []chat1.MessageUnboxed) { 567 useTypeFilter := (query != nil && len(query.MessageTypes) > 0) 568 569 typmap := make(map[chat1.MessageType]bool) 570 if useTypeFilter { 571 for _, mt := range query.MessageTypes { 572 typmap[mt] = true 573 } 574 } 575 576 for _, msg := range msgs { 577 state, err := msg.State() 578 if err != nil { 579 if includeAllErrors { 580 res = append(res, msg) 581 } 582 continue 583 } 584 switch state { 585 case chat1.MessageUnboxedState_ERROR: 586 if includeAllErrors { 587 res = append(res, msg) 588 } 589 case chat1.MessageUnboxedState_PLACEHOLDER: 590 // We don't know what the type is for these, so just include them 591 res = append(res, msg) 592 default: 593 _, match := typmap[msg.GetMessageType()] 594 if !useTypeFilter || match { 595 res = append(res, msg) 596 } 597 } 598 } 599 return res 600} 601 602// Filter messages that are both exploded that are no longer shown in the GUI 603// (as ash lines) 604func FilterExploded(conv types.UnboxConversationInfo, msgs []chat1.MessageUnboxed, now time.Time) (res []chat1.MessageUnboxed) { 605 upto := conv.GetMaxDeletedUpTo() 606 for _, msg := range msgs { 607 if msg.IsEphemeral() && msg.HideExplosion(upto, now) { 608 continue 609 } 610 res = append(res, msg) 611 } 612 return res 613} 614 615func GetReaction(msg chat1.MessageUnboxed) (string, error) { 616 if !msg.IsValid() { 617 return "", errors.New("invalid message") 618 } 619 body := msg.Valid().MessageBody 620 typ, err := body.MessageType() 621 if err != nil { 622 return "", err 623 } 624 if typ != chat1.MessageType_REACTION { 625 return "", fmt.Errorf("not a reaction type: %v", typ) 626 } 627 return body.Reaction().Body, nil 628} 629 630// GetSupersedes must be called with a valid msg 631func GetSupersedes(msg chat1.MessageUnboxed) ([]chat1.MessageID, error) { 632 if !msg.IsValidFull() { 633 return nil, fmt.Errorf("GetSupersedes called with invalid message: %v", msg.GetMessageID()) 634 } 635 body := msg.Valid().MessageBody 636 typ, err := body.MessageType() 637 if err != nil { 638 return nil, err 639 } 640 641 // We use the message ID in the body over the field in the client header to 642 // avoid server trust. 643 switch typ { 644 case chat1.MessageType_EDIT: 645 return []chat1.MessageID{msg.Valid().MessageBody.Edit().MessageID}, nil 646 case chat1.MessageType_REACTION: 647 return []chat1.MessageID{msg.Valid().MessageBody.Reaction().MessageID}, nil 648 case chat1.MessageType_DELETE: 649 return msg.Valid().MessageBody.Delete().MessageIDs, nil 650 case chat1.MessageType_ATTACHMENTUPLOADED: 651 return []chat1.MessageID{msg.Valid().MessageBody.Attachmentuploaded().MessageID}, nil 652 case chat1.MessageType_UNFURL: 653 return []chat1.MessageID{msg.Valid().MessageBody.Unfurl().MessageID}, nil 654 default: 655 return nil, nil 656 } 657} 658 659// Start at the beginning of the line, space, or some hand picked artisanal 660// characters 661const ServiceDecorationPrefix = `(?:^|[\s([/{:;.,!?"'])` 662 663var chanNameMentionRegExp = regexp.MustCompile(ServiceDecorationPrefix + `(#(?:[0-9a-zA-Z_-]+))`) 664 665func ParseChannelNameMentions(ctx context.Context, body string, uid gregor1.UID, teamID chat1.TLFID, 666 ts types.TeamChannelSource) (res []chat1.ChannelNameMention) { 667 names := parseRegexpNames(ctx, body, chanNameMentionRegExp) 668 if len(names) == 0 { 669 return nil 670 } 671 chanResponse, err := ts.GetChannelsTopicName(ctx, uid, teamID, chat1.TopicType_CHAT) 672 if err != nil { 673 return nil 674 } 675 validChans := make(map[string]chat1.ChannelNameMention) 676 for _, cr := range chanResponse { 677 validChans[cr.TopicName] = cr 678 } 679 for _, name := range names { 680 if cr, ok := validChans[name.name]; ok { 681 res = append(res, cr) 682 } 683 } 684 return res 685} 686 687var atMentionRegExp = regexp.MustCompile(ServiceDecorationPrefix + 688 `(@(?:[a-zA-Z0-9][a-zA-Z0-9._]*[a-zA-Z0-9_]+(?:#[a-z0-9A-Z_-]+)?))`) 689 690type nameMatch struct { 691 name, normalizedName string 692 position []int 693} 694 695func (m nameMatch) Len() int { 696 return m.position[1] - m.position[0] 697} 698 699func parseRegexpNames(ctx context.Context, body string, re *regexp.Regexp) (res []nameMatch) { 700 body = ReplaceQuotedSubstrings(body, true) 701 allIndexMatches := re.FindAllStringSubmatchIndex(body, -1) 702 for _, indexMatch := range allIndexMatches { 703 if len(indexMatch) >= 4 { 704 // do +1 so we don't include the @ in the hit. 705 low := indexMatch[2] + 1 706 high := indexMatch[3] 707 hit := body[low:high] 708 res = append(res, nameMatch{ 709 name: hit, 710 normalizedName: strings.ToLower(hit), 711 position: []int{low, high}, 712 }) 713 } 714 } 715 return res 716} 717 718func GetTextAtMentionedItems(ctx context.Context, g *globals.Context, uid gregor1.UID, 719 convID chat1.ConversationID, msg chat1.MessageText, 720 getConvMembs func() ([]string, error), 721 debug *DebugLabeler) (atRes []chat1.KnownUserMention, maybeRes []chat1.MaybeMention, chanRes chat1.ChannelMention) { 722 atRes, maybeRes, chanRes = ParseAtMentionedItems(ctx, g, msg.Body, msg.UserMentions, getConvMembs) 723 atRes = append(atRes, GetPaymentAtMentions(ctx, g.GetUPAKLoader(), msg.Payments, debug)...) 724 if msg.ReplyToUID != nil { 725 atRes = append(atRes, chat1.KnownUserMention{ 726 Text: "", 727 Uid: *msg.ReplyToUID, 728 }) 729 } 730 return atRes, maybeRes, chanRes 731} 732 733func GetPaymentAtMentions(ctx context.Context, upak libkb.UPAKLoader, payments []chat1.TextPayment, 734 l *DebugLabeler) (atMentions []chat1.KnownUserMention) { 735 for _, p := range payments { 736 uid, err := upak.LookupUID(ctx, libkb.NewNormalizedUsername(p.Username)) 737 if err != nil { 738 l.Debug(ctx, "GetPaymentAtMentions: error loading uid: username: %s err: %s", p.Username, err) 739 continue 740 } 741 atMentions = append(atMentions, chat1.KnownUserMention{ 742 Uid: uid.ToBytes(), 743 Text: "", 744 }) 745 } 746 return atMentions 747} 748 749func parseItemAsUID(ctx context.Context, g *globals.Context, name string, 750 knownMentions []chat1.KnownUserMention, 751 getConvMembs func() ([]string, error)) (gregor1.UID, error) { 752 nname := libkb.NewNormalizedUsername(name) 753 shouldLookup := false 754 for _, known := range knownMentions { 755 if known.Text == nname.String() { 756 shouldLookup = true 757 break 758 } 759 } 760 if !shouldLookup { 761 shouldLookup = libkb.IsUserByUsernameOffline(libkb.NewMetaContext(ctx, g.ExternalG()), nname) 762 } 763 if !shouldLookup && getConvMembs != nil { 764 membs, err := getConvMembs() 765 if err != nil { 766 return nil, err 767 } 768 for _, memb := range membs { 769 if memb == nname.String() { 770 shouldLookup = true 771 break 772 } 773 } 774 } 775 if shouldLookup { 776 kuid, err := g.GetUPAKLoader().LookupUID(ctx, nname) 777 if err != nil { 778 return nil, err 779 } 780 return kuid.ToBytes(), nil 781 } 782 return nil, errors.New("not a username") 783} 784 785func ParseAtMentionedItems(ctx context.Context, g *globals.Context, body string, 786 knownMentions []chat1.KnownUserMention, getConvMembs func() ([]string, error)) (atRes []chat1.KnownUserMention, maybeRes []chat1.MaybeMention, chanRes chat1.ChannelMention) { 787 matches := parseRegexpNames(ctx, body, atMentionRegExp) 788 chanRes = chat1.ChannelMention_NONE 789 for _, m := range matches { 790 var channel string 791 toks := strings.Split(m.name, "#") 792 baseName := toks[0] 793 if len(toks) > 1 { 794 channel = toks[1] 795 } 796 797 normalizedBaseName := strings.Split(m.normalizedName, "#")[0] 798 switch normalizedBaseName { 799 case "channel", "everyone": 800 chanRes = chat1.ChannelMention_ALL 801 continue 802 case "here": 803 if chanRes != chat1.ChannelMention_ALL { 804 chanRes = chat1.ChannelMention_HERE 805 } 806 continue 807 default: 808 } 809 810 // Try UID first then team 811 if uid, err := parseItemAsUID(ctx, g, normalizedBaseName, knownMentions, getConvMembs); err == nil { 812 atRes = append(atRes, chat1.KnownUserMention{ 813 Text: baseName, 814 Uid: uid, 815 }) 816 } else { 817 // anything else is a possible mention 818 maybeRes = append(maybeRes, chat1.MaybeMention{ 819 Name: baseName, 820 Channel: channel, 821 }) 822 } 823 } 824 return atRes, maybeRes, chanRes 825} 826 827func SystemMessageMentions(ctx context.Context, g *globals.Context, uid gregor1.UID, 828 body chat1.MessageSystem) (atMentions []gregor1.UID, chanMention chat1.ChannelMention, channelNameMentions []chat1.ChannelNameMention) { 829 typ, err := body.SystemType() 830 if err != nil { 831 return nil, 0, nil 832 } 833 switch typ { 834 case chat1.MessageSystemType_ADDEDTOTEAM: 835 addeeUID, err := g.GetUPAKLoader().LookupUID(ctx, 836 libkb.NewNormalizedUsername(body.Addedtoteam().Addee)) 837 if err == nil { 838 atMentions = append(atMentions, addeeUID.ToBytes()) 839 } 840 case chat1.MessageSystemType_INVITEADDEDTOTEAM: 841 inviteeUID, err := g.GetUPAKLoader().LookupUID(ctx, 842 libkb.NewNormalizedUsername(body.Inviteaddedtoteam().Invitee)) 843 if err == nil { 844 atMentions = append(atMentions, inviteeUID.ToBytes()) 845 } 846 inviterUID, err := g.GetUPAKLoader().LookupUID(ctx, 847 libkb.NewNormalizedUsername(body.Inviteaddedtoteam().Inviter)) 848 if err == nil { 849 atMentions = append(atMentions, inviterUID.ToBytes()) 850 } 851 case chat1.MessageSystemType_COMPLEXTEAM: 852 chanMention = chat1.ChannelMention_ALL 853 case chat1.MessageSystemType_BULKADDTOCONV: 854 for _, username := range body.Bulkaddtoconv().Usernames { 855 uid, err := g.GetUPAKLoader().LookupUID(ctx, libkb.NewNormalizedUsername(username)) 856 if err == nil { 857 atMentions = append(atMentions, uid.ToBytes()) 858 } 859 } 860 case chat1.MessageSystemType_NEWCHANNEL: 861 conv, err := GetVerifiedConv(ctx, g, uid, body.Newchannel().ConvID, types.InboxSourceDataSourceAll) 862 if err == nil { 863 channelNameMentions = append(channelNameMentions, chat1.ChannelNameMention{ 864 ConvID: conv.GetConvID(), 865 TopicName: conv.GetTopicName(), 866 }) 867 } 868 } 869 sort.Sort(chat1.ByUID(atMentions)) 870 return atMentions, chanMention, channelNameMentions 871} 872 873func PluckMessageIDs(msgs []chat1.MessageSummary) []chat1.MessageID { 874 res := make([]chat1.MessageID, len(msgs)) 875 for i, m := range msgs { 876 res[i] = m.GetMessageID() 877 } 878 return res 879} 880 881func PluckUIMessageIDs(msgs []chat1.UIMessage) (res []chat1.MessageID) { 882 res = make([]chat1.MessageID, 0, len(msgs)) 883 for _, m := range msgs { 884 res = append(res, m.GetMessageID()) 885 } 886 return res 887} 888 889func PluckMUMessageIDs(msgs []chat1.MessageUnboxed) (res []chat1.MessageID) { 890 res = make([]chat1.MessageID, 0, len(msgs)) 891 for _, m := range msgs { 892 res = append(res, m.GetMessageID()) 893 } 894 return res 895} 896 897func IsConvEmpty(conv chat1.Conversation) bool { 898 switch conv.GetMembersType() { 899 case chat1.ConversationMembersType_TEAM: 900 return false 901 default: 902 for _, msg := range conv.MaxMsgSummaries { 903 if IsNonEmptyConvMessageType(msg.GetMessageType()) { 904 return false 905 } 906 } 907 return true 908 } 909} 910 911func PluckConvIDsLocal(convs []chat1.ConversationLocal) (res []chat1.ConversationID) { 912 res = make([]chat1.ConversationID, 0, len(convs)) 913 for _, conv := range convs { 914 res = append(res, conv.GetConvID()) 915 } 916 return res 917} 918 919func PluckConvIDs(convs []chat1.Conversation) (res []chat1.ConversationID) { 920 res = make([]chat1.ConversationID, 0, len(convs)) 921 for _, conv := range convs { 922 res = append(res, conv.GetConvID()) 923 } 924 return res 925} 926 927func PluckConvIDsRC(convs []types.RemoteConversation) (res []chat1.ConversationID) { 928 res = make([]chat1.ConversationID, 0, len(convs)) 929 for _, conv := range convs { 930 res = append(res, conv.GetConvID()) 931 } 932 return res 933} 934 935func SanitizeTopicName(topicName string) string { 936 return strings.TrimPrefix(topicName, "#") 937} 938 939func CreateTopicNameState(cmp chat1.ConversationIDMessageIDPairs) (chat1.TopicNameState, error) { 940 var data []byte 941 var err error 942 mh := codec.MsgpackHandle{WriteExt: true} 943 enc := codec.NewEncoderBytes(&data, &mh) 944 if err = enc.Encode(cmp); err != nil { 945 return chat1.TopicNameState{}, err 946 } 947 948 h := sha256.New() 949 if _, err = h.Write(data); err != nil { 950 return chat1.TopicNameState{}, err 951 } 952 953 return h.Sum(nil), nil 954} 955 956func GetConvLastSendTime(rc types.RemoteConversation) gregor1.Time { 957 conv := rc.Conv 958 if conv.ReaderInfo == nil { 959 return 0 960 } 961 if conv.ReaderInfo.LastSendTime == 0 { 962 return GetConvMtime(rc) 963 } 964 return conv.ReaderInfo.LastSendTime 965} 966 967func GetConvMtime(rc types.RemoteConversation) (res gregor1.Time) { 968 conv := rc.Conv 969 var summaries []chat1.MessageSummary 970 for _, typ := range chat1.VisibleChatMessageTypes() { 971 summary, err := conv.GetMaxMessage(typ) 972 if err == nil { 973 summaries = append(summaries, summary) 974 } 975 } 976 sort.Sort(ByMsgSummaryCtime(summaries)) 977 if len(summaries) == 0 { 978 res = conv.ReaderInfo.Mtime 979 } else { 980 res = summaries[len(summaries)-1].Ctime 981 } 982 if res > rc.LocalMtime { 983 return res 984 } 985 return rc.LocalMtime 986} 987 988// GetConvPriorityScore weighs conversations that are fully read above ones 989// that are not, weighting more recently modified conversations higher.. Used 990// to order conversations when background loading. 991func GetConvPriorityScore(rc types.RemoteConversation) float64 { 992 readMsgID := rc.GetReadMsgID() 993 maxMsgID := rc.Conv.ReaderInfo.MaxMsgid 994 mtime := GetConvMtime(rc) 995 dur := math.Abs(float64(time.Since(mtime.Time())) / float64(time.Hour)) 996 return 100 / math.Pow(dur+float64(maxMsgID-readMsgID), 0.5) 997} 998 999type MessageSummaryContainer interface { 1000 GetMaxMessage(typ chat1.MessageType) (chat1.MessageSummary, error) 1001} 1002 1003func PickLatestMessageSummary(conv MessageSummaryContainer, typs []chat1.MessageType) (res chat1.MessageSummary, err error) { 1004 // nil means all 1005 if typs == nil { 1006 for typ := range chat1.MessageTypeRevMap { 1007 typs = append(typs, typ) 1008 } 1009 } 1010 for _, typ := range typs { 1011 msg, err := conv.GetMaxMessage(typ) 1012 if err == nil && (msg.Ctime.After(res.Ctime) || res.Ctime.IsZero()) { 1013 res = msg 1014 } 1015 } 1016 if res.GetMessageID() == 0 { 1017 return res, errors.New("no message summary found") 1018 } 1019 return res, nil 1020} 1021 1022func GetConvMtimeLocal(conv chat1.ConversationLocal) gregor1.Time { 1023 msg, err := PickLatestMessageSummary(conv, chat1.VisibleChatMessageTypes()) 1024 if err != nil { 1025 return conv.ReaderInfo.Mtime 1026 } 1027 return msg.Ctime 1028} 1029 1030func GetRemoteConvTLFName(conv types.RemoteConversation) string { 1031 if conv.LocalMetadata != nil { 1032 return conv.LocalMetadata.Name 1033 } 1034 msg, err := PickLatestMessageSummary(conv.Conv, nil) 1035 if err != nil { 1036 return "" 1037 } 1038 return msg.TlfName 1039} 1040 1041func GetRemoteConvDisplayName(rc types.RemoteConversation) string { 1042 tlfName := GetRemoteConvTLFName(rc) 1043 switch rc.Conv.Metadata.TeamType { 1044 case chat1.TeamType_COMPLEX: 1045 if rc.LocalMetadata != nil && len(rc.Conv.MaxMsgSummaries) > 0 { 1046 return fmt.Sprintf("%s#%s", tlfName, rc.LocalMetadata.TopicName) 1047 } 1048 fallthrough 1049 default: 1050 return tlfName 1051 } 1052} 1053 1054func GetConvSnippet(ctx context.Context, g *globals.Context, uid gregor1.UID, conv chat1.ConversationLocal, 1055 currentUsername string) (chat1.SnippetDecoration, string, string) { 1056 1057 if conv.Info.SnippetMsg == nil { 1058 return chat1.SnippetDecoration_NONE, "", "" 1059 } 1060 msg := *conv.Info.SnippetMsg 1061 1062 return GetMsgSnippet(ctx, g, uid, msg, conv, currentUsername) 1063} 1064 1065func GetMsgSummaryByType(msgs []chat1.MessageSummary, typ chat1.MessageType) (chat1.MessageSummary, error) { 1066 for _, msg := range msgs { 1067 if msg.GetMessageType() == typ { 1068 return msg, nil 1069 } 1070 } 1071 return chat1.MessageSummary{}, errors.New("not found") 1072} 1073 1074func showSenderPrefix(conv chat1.ConversationLocal) (showPrefix bool) { 1075 switch conv.GetMembersType() { 1076 case chat1.ConversationMembersType_TEAM: 1077 showPrefix = true 1078 default: 1079 showPrefix = len(conv.AllNames()) > 2 1080 } 1081 return showPrefix 1082} 1083 1084// Sender prefix for msg snippets. Will show if a conversation has > 2 members 1085// or is of type TEAM 1086func getSenderPrefix(conv chat1.ConversationLocal, currentUsername, senderUsername string) (senderPrefix string) { 1087 if showSenderPrefix(conv) { 1088 if senderUsername == currentUsername { 1089 senderPrefix = "You: " 1090 } else { 1091 senderPrefix = fmt.Sprintf("%s: ", senderUsername) 1092 } 1093 } 1094 return senderPrefix 1095} 1096 1097func formatDuration(dur time.Duration) string { 1098 h := dur / time.Hour 1099 dur -= h * time.Hour 1100 m := dur / time.Minute 1101 dur -= m * time.Minute 1102 s := dur / time.Second 1103 if h > 0 { 1104 return fmt.Sprintf("%02d:%02d:%02d", h, m, s) 1105 } 1106 return fmt.Sprintf("%02d:%02d", m, s) 1107} 1108 1109func getMsgSnippetDecoration(msg chat1.MessageUnboxed) chat1.SnippetDecoration { 1110 var msgBody chat1.MessageBody 1111 if msg.IsValid() { 1112 msgBody = msg.Valid().MessageBody 1113 } else { 1114 msgBody = msg.Outbox().Msg.MessageBody 1115 } 1116 switch msg.GetMessageType() { 1117 case chat1.MessageType_ATTACHMENT: 1118 obj := msgBody.Attachment().Object 1119 atyp, err := obj.Metadata.AssetType() 1120 if err != nil { 1121 return chat1.SnippetDecoration_NONE 1122 } 1123 switch atyp { 1124 case chat1.AssetMetadataType_IMAGE: 1125 return chat1.SnippetDecoration_PHOTO_ATTACHMENT 1126 case chat1.AssetMetadataType_VIDEO: 1127 if obj.Metadata.Video().IsAudio { 1128 return chat1.SnippetDecoration_AUDIO_ATTACHMENT 1129 } 1130 return chat1.SnippetDecoration_VIDEO_ATTACHMENT 1131 } 1132 return chat1.SnippetDecoration_FILE_ATTACHMENT 1133 case chat1.MessageType_REQUESTPAYMENT: 1134 return chat1.SnippetDecoration_STELLAR_RECEIVED 1135 case chat1.MessageType_SENDPAYMENT: 1136 return chat1.SnippetDecoration_STELLAR_SENT 1137 case chat1.MessageType_PIN: 1138 return chat1.SnippetDecoration_PINNED_MESSAGE 1139 } 1140 return chat1.SnippetDecoration_NONE 1141} 1142 1143func GetMsgSnippetBody(ctx context.Context, g *globals.Context, uid gregor1.UID, convID chat1.ConversationID, 1144 msg chat1.MessageUnboxed) (snippet, snippetDecorated string) { 1145 if !(msg.IsValidFull() || msg.IsOutbox()) { 1146 return "", "" 1147 } 1148 defer func() { 1149 if len(snippetDecorated) == 0 { 1150 snippetDecorated = EscapeShrugs(ctx, snippet) 1151 } 1152 }() 1153 var msgBody chat1.MessageBody 1154 var emojis []chat1.HarvestedEmoji 1155 if msg.IsValid() { 1156 msgBody = msg.Valid().MessageBody 1157 emojis = msg.Valid().Emojis 1158 } else { 1159 msgBody = msg.Outbox().Msg.MessageBody 1160 emojis = msg.Outbox().Msg.Emojis 1161 } 1162 switch msg.GetMessageType() { 1163 case chat1.MessageType_TEXT: 1164 return msgBody.Text().Body, 1165 PresentDecoratedSnippet(ctx, g, msgBody.Text().Body, uid, msg.GetMessageType(), emojis) 1166 case chat1.MessageType_EDIT: 1167 return msgBody.Edit().Body, "" 1168 case chat1.MessageType_FLIP: 1169 return msgBody.Flip().Text, "" 1170 case chat1.MessageType_PIN: 1171 return "Pinned message", "" 1172 case chat1.MessageType_ATTACHMENT: 1173 obj := msgBody.Attachment().Object 1174 title := obj.Title 1175 if len(title) == 0 { 1176 atyp, err := obj.Metadata.AssetType() 1177 if err != nil { 1178 return "???", "" 1179 } 1180 switch atyp { 1181 case chat1.AssetMetadataType_IMAGE: 1182 title = "Image attachment" 1183 case chat1.AssetMetadataType_VIDEO: 1184 dur := formatDuration(time.Duration(obj.Metadata.Video().DurationMs) * time.Millisecond) 1185 if obj.Metadata.Video().IsAudio { 1186 title = fmt.Sprintf("Audio message (%s)", dur) 1187 } else { 1188 title = fmt.Sprintf("Video attachment (%s)", dur) 1189 } 1190 default: 1191 if obj.Filename == "" { 1192 title = "File attachment" 1193 } else { 1194 title = obj.Filename 1195 } 1196 } 1197 } 1198 return title, "" 1199 case chat1.MessageType_SYSTEM: 1200 return msgBody.System().String(), "" 1201 case chat1.MessageType_REQUESTPAYMENT: 1202 return "Payment requested", "" 1203 case chat1.MessageType_SENDPAYMENT: 1204 return "Payment sent", "" 1205 case chat1.MessageType_HEADLINE: 1206 return msgBody.Headline().String(), "" 1207 } 1208 return "", "" 1209} 1210 1211func GetMsgSnippet(ctx context.Context, g *globals.Context, uid gregor1.UID, msg chat1.MessageUnboxed, 1212 conv chat1.ConversationLocal, currentUsername string) (decoration chat1.SnippetDecoration, snippet string, snippetDecorated string) { 1213 if !(msg.IsValid() || msg.IsOutbox()) { 1214 return chat1.SnippetDecoration_NONE, "", "" 1215 } 1216 defer func() { 1217 if len(snippetDecorated) == 0 { 1218 snippetDecorated = snippet 1219 } 1220 }() 1221 1222 var senderUsername string 1223 if msg.IsValid() { 1224 senderUsername = msg.Valid().SenderUsername 1225 } else { 1226 senderUsername = currentUsername 1227 } 1228 1229 senderPrefix := getSenderPrefix(conv, currentUsername, senderUsername) 1230 // does not apply to outbox messages, ephemeral timer starts once the server 1231 // assigns a ctime. 1232 if msg.IsValid() && !msg.IsValidFull() { 1233 if msg.Valid().IsEphemeral() && msg.Valid().IsEphemeralExpired(time.Now()) { 1234 return chat1.SnippetDecoration_EXPLODED_MESSAGE, "Message exploded.", "" 1235 } 1236 return chat1.SnippetDecoration_NONE, "", "" 1237 } 1238 1239 if msg.IsOutbox() && msg.Outbox().IsBadgable() { 1240 decoration = chat1.SnippetDecoration_PENDING_MESSAGE 1241 if msg.Outbox().IsError() { 1242 decoration = chat1.SnippetDecoration_FAILED_PENDING_MESSAGE 1243 } 1244 } else if msg.IsValid() && msg.Valid().IsEphemeral() { 1245 decoration = chat1.SnippetDecoration_EXPLODING_MESSAGE 1246 } else { 1247 decoration = getMsgSnippetDecoration(msg) 1248 } 1249 snippet, snippetDecorated = GetMsgSnippetBody(ctx, g, uid, conv.GetConvID(), msg) 1250 if snippet == "" { 1251 decoration = chat1.SnippetDecoration_NONE 1252 } 1253 return decoration, senderPrefix + snippet, senderPrefix + snippetDecorated 1254} 1255 1256func GetDesktopNotificationSnippet(ctx context.Context, g *globals.Context, 1257 uid gregor1.UID, conv *chat1.ConversationLocal, currentUsername string, 1258 fromMsg *chat1.MessageUnboxed, plaintextDesktopDisabled bool) string { 1259 if conv == nil { 1260 return "" 1261 } 1262 var msg chat1.MessageUnboxed 1263 if fromMsg != nil { 1264 msg = *fromMsg 1265 } else if conv.Info.SnippetMsg != nil { 1266 msg = *conv.Info.SnippetMsg 1267 } else { 1268 return "" 1269 } 1270 if !msg.IsValid() { 1271 return "" 1272 } 1273 1274 mvalid := msg.Valid() 1275 if mvalid.IsEphemeral() { 1276 // If the message is already exploded, nothing to see here. 1277 if !msg.IsValidFull() { 1278 return "" 1279 } 1280 switch msg.GetMessageType() { 1281 case chat1.MessageType_TEXT, chat1.MessageType_ATTACHMENT, chat1.MessageType_EDIT: 1282 return " exploding message." 1283 default: 1284 return "" 1285 } 1286 } else if plaintextDesktopDisabled { 1287 return "New message" 1288 } 1289 1290 switch msg.GetMessageType() { 1291 case chat1.MessageType_REACTION: 1292 reaction, err := GetReaction(msg) 1293 if err != nil { 1294 return "" 1295 } 1296 var prefix string 1297 if showSenderPrefix(*conv) { 1298 prefix = mvalid.SenderUsername + " " 1299 } 1300 return emoji.Sprintf("%sreacted to your message with %v", prefix, reaction) 1301 default: 1302 decoration, snippetBody, _ := GetMsgSnippet(ctx, g, uid, msg, *conv, currentUsername) 1303 return emoji.Sprintf("%s %s", decoration.ToEmoji(), snippetBody) 1304 } 1305} 1306 1307func StripUsernameFromConvName(name string, username string) (res string) { 1308 res = strings.Replace(name, fmt.Sprintf(",%s", username), "", -1) 1309 res = strings.Replace(res, fmt.Sprintf("%s,", username), "", -1) 1310 return res 1311} 1312 1313func PresentRemoteConversationAsSmallTeamRow(ctx context.Context, rc types.RemoteConversation, 1314 username string) (res chat1.UIInboxSmallTeamRow) { 1315 res.ConvID = rc.ConvIDStr 1316 res.IsTeam = rc.GetTeamType() != chat1.TeamType_NONE 1317 res.Name = StripUsernameFromConvName(GetRemoteConvDisplayName(rc), username) 1318 res.Time = GetConvMtime(rc) 1319 if rc.LocalMetadata != nil { 1320 res.SnippetDecoration = rc.LocalMetadata.SnippetDecoration 1321 res.Snippet = &rc.LocalMetadata.Snippet 1322 } 1323 res.Draft = rc.LocalDraft 1324 res.IsMuted = rc.Conv.Metadata.Status == chat1.ConversationStatus_MUTED 1325 return res 1326} 1327 1328func PresentRemoteConversationAsBigTeamChannelRow(ctx context.Context, rc types.RemoteConversation) (res chat1.UIInboxBigTeamChannelRow) { 1329 res.ConvID = rc.ConvIDStr 1330 res.Channelname = rc.GetTopicName() 1331 res.Teamname = GetRemoteConvTLFName(rc) 1332 res.Draft = rc.LocalDraft 1333 res.IsMuted = rc.Conv.Metadata.Status == chat1.ConversationStatus_MUTED 1334 return res 1335} 1336 1337func PresentRemoteConversation(ctx context.Context, g *globals.Context, uid gregor1.UID, rc types.RemoteConversation) (res chat1.UnverifiedInboxUIItem) { 1338 var tlfName string 1339 rawConv := rc.Conv 1340 latest, err := PickLatestMessageSummary(rawConv, nil) 1341 if err != nil { 1342 tlfName = "" 1343 } else { 1344 tlfName = latest.TlfName 1345 } 1346 res.ConvID = rc.ConvIDStr 1347 res.TlfID = rawConv.Metadata.IdTriple.Tlfid.TLFIDStr() 1348 res.TopicType = rawConv.GetTopicType() 1349 res.IsPublic = rawConv.Metadata.Visibility == keybase1.TLFVisibility_PUBLIC 1350 res.IsDefaultConv = rawConv.Metadata.IsDefaultConv 1351 res.Name = tlfName 1352 res.Status = rawConv.Metadata.Status 1353 res.Time = GetConvMtime(rc) 1354 res.Visibility = rawConv.Metadata.Visibility 1355 res.Notifications = rawConv.Notifications 1356 res.MembersType = rawConv.GetMembersType() 1357 res.MemberStatus = rawConv.ReaderInfo.Status 1358 res.TeamType = rawConv.Metadata.TeamType 1359 res.Version = rawConv.Metadata.Version 1360 res.LocalVersion = rawConv.Metadata.LocalVersion 1361 res.MaxMsgID = rawConv.ReaderInfo.MaxMsgid 1362 res.MaxVisibleMsgID = rawConv.MaxVisibleMsgID() 1363 res.ReadMsgID = rawConv.ReaderInfo.ReadMsgid 1364 res.Supersedes = rawConv.Metadata.Supersedes 1365 res.SupersededBy = rawConv.Metadata.SupersededBy 1366 res.FinalizeInfo = rawConv.Metadata.FinalizeInfo 1367 res.Commands = 1368 chat1.NewConversationCommandGroupsWithBuiltin(g.CommandsSource.GetBuiltinCommandType(ctx, rc)) 1369 if rc.LocalMetadata != nil { 1370 res.LocalMetadata = &chat1.UnverifiedInboxUIItemMetadata{ 1371 ChannelName: rc.LocalMetadata.TopicName, 1372 Headline: rc.LocalMetadata.Headline, 1373 HeadlineDecorated: DecorateWithLinks(ctx, 1374 PresentDecoratedSnippet(ctx, g, rc.LocalMetadata.Headline, uid, 1375 chat1.MessageType_HEADLINE, rc.LocalMetadata.HeadlineEmojis)), 1376 Snippet: rc.LocalMetadata.Snippet, 1377 SnippetDecoration: rc.LocalMetadata.SnippetDecoration, 1378 WriterNames: rc.LocalMetadata.WriterNames, 1379 ResetParticipants: rc.LocalMetadata.ResetParticipants, 1380 } 1381 res.Name = rc.LocalMetadata.Name 1382 } 1383 res.ConvRetention = rawConv.ConvRetention 1384 res.TeamRetention = rawConv.TeamRetention 1385 res.Draft = rc.LocalDraft 1386 return res 1387} 1388 1389func PresentRemoteConversations(ctx context.Context, g *globals.Context, uid gregor1.UID, rcs []types.RemoteConversation) (res []chat1.UnverifiedInboxUIItem) { 1390 res = make([]chat1.UnverifiedInboxUIItem, 0, len(rcs)) 1391 for _, rc := range rcs { 1392 res = append(res, PresentRemoteConversation(ctx, g, uid, rc)) 1393 } 1394 return res 1395} 1396 1397func SearchableRemoteConversationName(conv types.RemoteConversation, username string) string { 1398 name := GetRemoteConvDisplayName(conv) 1399 // Check for self conv or big team conv 1400 if name == username || strings.Contains(name, "#") { 1401 return name 1402 } 1403 name = strings.Replace(name, fmt.Sprintf(",%s", username), "", -1) 1404 name = strings.Replace(name, fmt.Sprintf("%s,", username), "", -1) 1405 return name 1406} 1407 1408func PresentRemoteConversationAsSearchHit(conv types.RemoteConversation, username string) chat1.UIChatSearchConvHit { 1409 return chat1.UIChatSearchConvHit{ 1410 ConvID: conv.ConvIDStr, 1411 TeamType: conv.GetTeamType(), 1412 Name: SearchableRemoteConversationName(conv, username), 1413 Mtime: conv.GetMtime(), 1414 } 1415} 1416 1417func PresentRemoteConversationsAsSearchHits(convs []types.RemoteConversation, username string) (res []chat1.UIChatSearchConvHit) { 1418 res = make([]chat1.UIChatSearchConvHit, 0, len(convs)) 1419 for _, c := range convs { 1420 res = append(res, PresentRemoteConversationAsSearchHit(c, username)) 1421 } 1422 return res 1423} 1424 1425func PresentConversationErrorLocal(ctx context.Context, g *globals.Context, uid gregor1.UID, rawConv chat1.ConversationErrorLocal) (res chat1.InboxUIItemError) { 1426 res.Message = rawConv.Message 1427 res.RekeyInfo = rawConv.RekeyInfo 1428 res.RemoteConv = PresentRemoteConversation(ctx, g, uid, types.RemoteConversation{ 1429 Conv: rawConv.RemoteConv, 1430 ConvIDStr: rawConv.RemoteConv.GetConvID().ConvIDStr(), 1431 }) 1432 res.Typ = rawConv.Typ 1433 res.UnverifiedTLFName = rawConv.UnverifiedTLFName 1434 return res 1435} 1436 1437func getParticipantType(username string) chat1.UIParticipantType { 1438 if strings.HasSuffix(username, "@phone") { 1439 return chat1.UIParticipantType_PHONENO 1440 } 1441 if strings.HasSuffix(username, "@email") { 1442 return chat1.UIParticipantType_EMAIL 1443 } 1444 return chat1.UIParticipantType_USER 1445} 1446 1447func PresentConversationParticipantsLocal(ctx context.Context, rawParticipants []chat1.ConversationLocalParticipant) (participants []chat1.UIParticipant) { 1448 participants = make([]chat1.UIParticipant, 0, len(rawParticipants)) 1449 for _, p := range rawParticipants { 1450 participantType := getParticipantType(p.Username) 1451 participants = append(participants, chat1.UIParticipant{ 1452 Assertion: p.Username, 1453 InConvName: p.InConvName, 1454 ContactName: p.ContactName, 1455 FullName: p.Fullname, 1456 Type: participantType, 1457 }) 1458 } 1459 return participants 1460} 1461 1462type PresentParticipantsMode int 1463 1464const ( 1465 PresentParticipantsModeInclude PresentParticipantsMode = iota 1466 PresentParticipantsModeSkip 1467) 1468 1469func PresentConversationLocal(ctx context.Context, g *globals.Context, uid gregor1.UID, 1470 rawConv chat1.ConversationLocal, partMode PresentParticipantsMode) (res chat1.InboxUIItem) { 1471 res.ConvID = rawConv.GetConvID().ConvIDStr() 1472 res.TlfID = rawConv.Info.Triple.Tlfid.TLFIDStr() 1473 res.TopicType = rawConv.GetTopicType() 1474 res.IsPublic = rawConv.Info.Visibility == keybase1.TLFVisibility_PUBLIC 1475 res.IsDefaultConv = rawConv.Info.IsDefaultConv 1476 res.Name = rawConv.Info.TlfName 1477 res.SnippetDecoration, res.Snippet, res.SnippetDecorated = 1478 GetConvSnippet(ctx, g, uid, rawConv, g.GetEnv().GetUsername().String()) 1479 res.Channel = rawConv.Info.TopicName 1480 res.Headline = rawConv.Info.Headline 1481 res.HeadlineDecorated = DecorateWithLinks(ctx, PresentDecoratedSnippet(ctx, g, rawConv.Info.Headline, uid, 1482 chat1.MessageType_HEADLINE, rawConv.Info.HeadlineEmojis)) 1483 res.ResetParticipants = rawConv.Info.ResetNames 1484 res.Status = rawConv.Info.Status 1485 res.MembersType = rawConv.GetMembersType() 1486 res.MemberStatus = rawConv.Info.MemberStatus 1487 res.Visibility = rawConv.Info.Visibility 1488 res.Time = GetConvMtimeLocal(rawConv) 1489 res.FinalizeInfo = rawConv.GetFinalizeInfo() 1490 res.SupersededBy = rawConv.SupersededBy 1491 res.Supersedes = rawConv.Supersedes 1492 res.IsEmpty = rawConv.IsEmpty 1493 res.Notifications = rawConv.Notifications 1494 res.CreatorInfo = rawConv.CreatorInfo 1495 res.TeamType = rawConv.Info.TeamType 1496 res.Version = rawConv.Info.Version 1497 res.LocalVersion = rawConv.Info.LocalVersion 1498 res.MaxMsgID = rawConv.ReaderInfo.MaxMsgid 1499 res.MaxVisibleMsgID = rawConv.MaxVisibleMsgID() 1500 res.ReadMsgID = rawConv.ReaderInfo.ReadMsgid 1501 res.ConvRetention = rawConv.ConvRetention 1502 res.TeamRetention = rawConv.TeamRetention 1503 res.ConvSettings = rawConv.ConvSettings 1504 res.Commands = rawConv.Commands 1505 res.BotCommands = rawConv.BotCommands 1506 res.BotAliases = rawConv.BotAliases 1507 res.Draft = rawConv.Info.Draft 1508 if rawConv.Info.PinnedMsg != nil { 1509 res.PinnedMsg = new(chat1.UIPinnedMessage) 1510 res.PinnedMsg.Message = PresentMessageUnboxed(ctx, g, rawConv.Info.PinnedMsg.Message, uid, 1511 rawConv.GetConvID()) 1512 res.PinnedMsg.PinnerUsername = rawConv.Info.PinnedMsg.PinnerUsername 1513 } 1514 switch partMode { 1515 case PresentParticipantsModeInclude: 1516 res.Participants = PresentConversationParticipantsLocal(ctx, rawConv.Info.Participants) 1517 default: 1518 } 1519 return res 1520} 1521 1522func PresentConversationLocals(ctx context.Context, g *globals.Context, uid gregor1.UID, 1523 convs []chat1.ConversationLocal, partMode PresentParticipantsMode) (res []chat1.InboxUIItem) { 1524 res = make([]chat1.InboxUIItem, 0, len(convs)) 1525 for _, conv := range convs { 1526 res = append(res, PresentConversationLocal(ctx, g, uid, conv, partMode)) 1527 } 1528 return res 1529} 1530 1531func PresentThreadView(ctx context.Context, g *globals.Context, uid gregor1.UID, tv chat1.ThreadView, 1532 convID chat1.ConversationID) (res chat1.UIMessages) { 1533 res.Pagination = PresentPagination(tv.Pagination) 1534 res.Messages = make([]chat1.UIMessage, 0, len(tv.Messages)) 1535 for _, msg := range tv.Messages { 1536 res.Messages = append(res.Messages, PresentMessageUnboxed(ctx, g, msg, uid, convID)) 1537 } 1538 return res 1539} 1540 1541func computeOutboxOrdinal(obr chat1.OutboxRecord) float64 { 1542 return computeOrdinal(obr.Msg.ClientHeader.OutboxInfo.Prev, obr.Ordinal) 1543} 1544 1545// Compute an "ordinal". There are two senses of "ordinal". 1546// The service considers ordinals ints, like 3, which are the offset after some message ID. 1547// The frontend considers ordinals floats like "180.03" where before the dot is 1548// a message ID, and after the dot is a sub-position in thousandths. 1549// This function translates from the service's sense to the frontend's sense. 1550func computeOrdinal(messageID chat1.MessageID, serviceOrdinal int) (frontendOrdinal float64) { 1551 return float64(messageID) + float64(serviceOrdinal)/1000.0 1552} 1553 1554func PresentChannelNameMentions(ctx context.Context, crs []chat1.ChannelNameMention) (res []chat1.UIChannelNameMention) { 1555 res = make([]chat1.UIChannelNameMention, 0, len(crs)) 1556 for _, cr := range crs { 1557 res = append(res, chat1.UIChannelNameMention{ 1558 Name: cr.TopicName, 1559 ConvID: cr.ConvID.ConvIDStr(), 1560 }) 1561 } 1562 return res 1563} 1564 1565func formatVideoDuration(ms int) string { 1566 s := ms / 1000 1567 // see if we have hours 1568 if s >= 3600 { 1569 hours := s / 3600 1570 minutes := (s % 3600) / 60 1571 seconds := s - (hours*3600 + minutes*60) 1572 return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds) 1573 } 1574 minutes := s / 60 1575 seconds := s % 60 1576 return fmt.Sprintf("%d:%02d", minutes, seconds) 1577} 1578 1579func PresentBytes(bytes int64) string { 1580 const ( 1581 BYTE = 1.0 << (10 * iota) 1582 KILOBYTE 1583 MEGABYTE 1584 GIGABYTE 1585 TERABYTE 1586 ) 1587 unit := "" 1588 value := float64(bytes) 1589 switch { 1590 case bytes >= TERABYTE: 1591 unit = "TB" 1592 value /= TERABYTE 1593 case bytes >= GIGABYTE: 1594 unit = "GB" 1595 value /= GIGABYTE 1596 case bytes >= MEGABYTE: 1597 unit = "MB" 1598 value /= MEGABYTE 1599 case bytes >= KILOBYTE: 1600 unit = "KB" 1601 value /= KILOBYTE 1602 case bytes >= BYTE: 1603 unit = "B" 1604 case bytes == 0: 1605 return "0" 1606 } 1607 return fmt.Sprintf("%.02f%s", value, unit) 1608} 1609 1610func formatVideoSize(bytes int64) string { 1611 return PresentBytes(bytes) 1612} 1613 1614func presentAttachmentAssetInfo(ctx context.Context, g *globals.Context, msg chat1.MessageUnboxed, 1615 convID chat1.ConversationID) *chat1.UIAssetUrlInfo { 1616 body := msg.Valid().MessageBody 1617 typ, err := body.MessageType() 1618 if err != nil { 1619 return nil 1620 } 1621 switch typ { 1622 case chat1.MessageType_ATTACHMENT, chat1.MessageType_ATTACHMENTUPLOADED: 1623 var hasFullURL, hasPreviewURL bool 1624 var asset chat1.Asset 1625 var info chat1.UIAssetUrlInfo 1626 if typ == chat1.MessageType_ATTACHMENT { 1627 asset = body.Attachment().Object 1628 info.MimeType = asset.MimeType 1629 hasFullURL = asset.Path != "" 1630 hasPreviewURL = body.Attachment().Preview != nil && 1631 body.Attachment().Preview.Path != "" 1632 } else { 1633 asset = body.Attachmentuploaded().Object 1634 info.MimeType = asset.MimeType 1635 hasFullURL = asset.Path != "" 1636 hasPreviewURL = len(body.Attachmentuploaded().Previews) > 0 && 1637 body.Attachmentuploaded().Previews[0].Path != "" 1638 } 1639 if hasFullURL { 1640 var cached bool 1641 info.FullUrl = g.AttachmentURLSrv.GetURL(ctx, convID, msg.GetMessageID(), false, false, false) 1642 cached, err = g.AttachmentURLSrv.GetAttachmentFetcher().IsAssetLocal(ctx, asset) 1643 if err != nil { 1644 cached = false 1645 } 1646 info.FullUrlCached = cached 1647 } 1648 if hasPreviewURL { 1649 info.PreviewUrl = g.AttachmentURLSrv.GetURL(ctx, convID, msg.GetMessageID(), true, false, false) 1650 } 1651 atyp, err := asset.Metadata.AssetType() 1652 if err == nil && atyp == chat1.AssetMetadataType_VIDEO && strings.HasPrefix(info.MimeType, "video") { 1653 if asset.Metadata.Video().DurationMs > 1 { 1654 info.VideoDuration = new(string) 1655 *info.VideoDuration = formatVideoDuration(asset.Metadata.Video().DurationMs) + ", " + 1656 formatVideoSize(asset.Size) 1657 } 1658 info.InlineVideoPlayable = true 1659 } 1660 if info.FullUrl == "" && info.PreviewUrl == "" && info.MimeType == "" { 1661 return nil 1662 } 1663 return &info 1664 } 1665 return nil 1666} 1667 1668func presentPaymentInfo(ctx context.Context, g *globals.Context, msgID chat1.MessageID, 1669 convID chat1.ConversationID, msg chat1.MessageUnboxedValid) []chat1.UIPaymentInfo { 1670 typ, err := msg.MessageBody.MessageType() 1671 if err != nil { 1672 return nil 1673 } 1674 var infos []chat1.UIPaymentInfo 1675 switch typ { 1676 case chat1.MessageType_SENDPAYMENT: 1677 body := msg.MessageBody.Sendpayment() 1678 info := g.StellarLoader.LoadPayment(ctx, convID, msgID, msg.SenderUsername, body.PaymentID) 1679 if info != nil { 1680 infos = []chat1.UIPaymentInfo{*info} 1681 } 1682 case chat1.MessageType_TEXT: 1683 body := msg.MessageBody.Text() 1684 // load any payments that were in the body of the text message 1685 for _, payment := range body.Payments { 1686 rtyp, err := payment.Result.ResultTyp() 1687 if err != nil { 1688 continue 1689 } 1690 switch rtyp { 1691 case chat1.TextPaymentResultTyp_SENT: 1692 paymentID := payment.Result.Sent() 1693 info := g.StellarLoader.LoadPayment(ctx, convID, msgID, msg.SenderUsername, paymentID) 1694 if info != nil { 1695 infos = append(infos, *info) 1696 } 1697 default: 1698 // Nothing to do for other payment result types. 1699 } 1700 } 1701 } 1702 for index := range infos { 1703 infos[index].Note = EscapeForDecorate(ctx, infos[index].Note) 1704 } 1705 return infos 1706} 1707 1708func presentRequestInfo(ctx context.Context, g *globals.Context, msgID chat1.MessageID, 1709 convID chat1.ConversationID, msg chat1.MessageUnboxedValid) *chat1.UIRequestInfo { 1710 1711 typ, err := msg.MessageBody.MessageType() 1712 if err != nil { 1713 return nil 1714 } 1715 switch typ { 1716 case chat1.MessageType_REQUESTPAYMENT: 1717 body := msg.MessageBody.Requestpayment() 1718 return g.StellarLoader.LoadRequest(ctx, convID, msgID, msg.SenderUsername, body.RequestID) 1719 default: 1720 // Nothing to do for other message types. 1721 } 1722 return nil 1723} 1724 1725func PresentUnfurl(ctx context.Context, g *globals.Context, convID chat1.ConversationID, u chat1.Unfurl) *chat1.UnfurlDisplay { 1726 ud, err := display.DisplayUnfurl(ctx, g.AttachmentURLSrv, convID, u) 1727 if err != nil { 1728 g.GetLog().CDebugf(ctx, "PresentUnfurl: failed to display unfurl: %s", err) 1729 return nil 1730 } 1731 return &ud 1732} 1733 1734func PresentUnfurls(ctx context.Context, g *globals.Context, uid gregor1.UID, 1735 convID chat1.ConversationID, unfurls map[chat1.MessageID]chat1.UnfurlResult) (res []chat1.UIMessageUnfurlInfo) { 1736 collapses := NewCollapses(g) 1737 res = make([]chat1.UIMessageUnfurlInfo, 0, len(unfurls)) 1738 for unfurlMessageID, u := range unfurls { 1739 ud := PresentUnfurl(ctx, g, convID, u.Unfurl) 1740 if ud != nil { 1741 res = append(res, chat1.UIMessageUnfurlInfo{ 1742 IsCollapsed: collapses.IsCollapsed(ctx, uid, convID, unfurlMessageID, 1743 chat1.MessageType_UNFURL), 1744 Unfurl: *ud, 1745 UnfurlMessageID: unfurlMessageID, 1746 Url: u.Url, 1747 }) 1748 } 1749 } 1750 return res 1751} 1752 1753func PresentDecoratedReactionMap(ctx context.Context, g *globals.Context, uid gregor1.UID, 1754 convID chat1.ConversationID, msg chat1.MessageUnboxedValid, reactions chat1.ReactionMap) (res chat1.UIReactionMap) { 1755 shouldDecorate := len(msg.Emojis) > 0 1756 res.Reactions = make(map[string]chat1.UIReactionDesc, len(reactions.Reactions)) 1757 for key, value := range reactions.Reactions { 1758 var desc chat1.UIReactionDesc 1759 if shouldDecorate { 1760 desc.Decorated = g.EmojiSource.Decorate(ctx, key, uid, 1761 chat1.MessageType_REACTION, msg.Emojis) 1762 } 1763 desc.Users = make(map[string]chat1.Reaction) 1764 for username, reaction := range value { 1765 desc.Users[username] = reaction 1766 } 1767 res.Reactions[key] = desc 1768 } 1769 return res 1770} 1771 1772func PresentDecoratedUserBio(ctx context.Context, bio string) (res string) { 1773 res = EscapeForDecorate(ctx, bio) 1774 res = EscapeShrugs(ctx, res) 1775 res = DecorateWithLinks(ctx, res) 1776 return res 1777} 1778 1779func systemMsgPresentText(ctx context.Context, uid gregor1.UID, msg chat1.MessageUnboxedValid) string { 1780 if !msg.MessageBody.IsType(chat1.MessageType_SYSTEM) { 1781 return "" 1782 } 1783 sysMsg := msg.MessageBody.System() 1784 typ, err := sysMsg.SystemType() 1785 if err != nil { 1786 return "" 1787 } 1788 switch typ { 1789 case chat1.MessageSystemType_NEWCHANNEL: 1790 var author string 1791 if uid.Eq(msg.ClientHeader.Sender) { 1792 author = "You " 1793 } 1794 if len(msg.ChannelNameMentions) == 1 { 1795 return fmt.Sprintf("%screated a new channel #%s", author, msg.ChannelNameMentions[0].TopicName) 1796 } else if len(msg.ChannelNameMentions) > 1 { 1797 return fmt.Sprintf("%screated #%s and %d other new channels", author, msg.ChannelNameMentions[0].TopicName, len(sysMsg.Newchannel().ConvIDs)-1) 1798 } 1799 default: 1800 } 1801 return "" 1802} 1803 1804func PresentDecoratedTextNoMentions(ctx context.Context, body string) string { 1805 // escape before applying xforms 1806 body = EscapeForDecorate(ctx, body) 1807 body = EscapeShrugs(ctx, body) 1808 1809 // This needs to happen before (deep) links. 1810 kbfsPaths := ParseKBFSPaths(ctx, body) 1811 body = DecorateWithKBFSPath(ctx, body, kbfsPaths) 1812 1813 // Links 1814 body = DecorateWithLinks(ctx, body) 1815 return body 1816} 1817 1818func PresentDecoratedSnippet(ctx context.Context, g *globals.Context, body string, 1819 uid gregor1.UID, msgType chat1.MessageType, emojis []chat1.HarvestedEmoji) string { 1820 body = EscapeForDecorate(ctx, body) 1821 body = EscapeShrugs(ctx, body) 1822 return g.EmojiSource.Decorate(ctx, body, uid, msgType, emojis) 1823} 1824 1825func PresentDecoratedPendingTextBody(ctx context.Context, g *globals.Context, uid gregor1.UID, 1826 msg chat1.MessagePlaintext) *string { 1827 typ, err := msg.MessageBody.MessageType() 1828 if err != nil { 1829 return nil 1830 } 1831 body := msg.MessageBody.TextForDecoration() 1832 body = PresentDecoratedTextNoMentions(ctx, body) 1833 body = g.EmojiSource.Decorate(ctx, body, uid, typ, msg.Emojis) 1834 return &body 1835} 1836 1837func PresentDecoratedTextBody(ctx context.Context, g *globals.Context, uid gregor1.UID, 1838 convID chat1.ConversationID, msg chat1.MessageUnboxedValid) *string { 1839 msgBody := msg.MessageBody 1840 typ, err := msgBody.MessageType() 1841 if err != nil { 1842 return nil 1843 } 1844 body := msgBody.TextForDecoration() 1845 if len(body) == 0 { 1846 return nil 1847 } 1848 var payments []chat1.TextPayment 1849 switch typ { 1850 case chat1.MessageType_TEXT: 1851 payments = msgBody.Text().Payments 1852 case chat1.MessageType_SYSTEM: 1853 body = systemMsgPresentText(ctx, uid, msg) 1854 } 1855 1856 body = PresentDecoratedTextNoMentions(ctx, body) 1857 // Payments 1858 body = g.StellarSender.DecorateWithPayments(ctx, body, payments) 1859 // Emojis 1860 body = g.EmojiSource.Decorate(ctx, body, uid, typ, msg.Emojis) 1861 // Mentions 1862 body = DecorateWithMentions(ctx, body, msg.AtMentionUsernames, msg.MaybeMentions, msg.ChannelMention, 1863 msg.ChannelNameMentions) 1864 return &body 1865} 1866 1867func loadTeamMentions(ctx context.Context, g *globals.Context, uid gregor1.UID, 1868 valid chat1.MessageUnboxedValid) { 1869 var knownTeamMentions []chat1.KnownTeamMention 1870 typ, err := valid.MessageBody.MessageType() 1871 if err != nil { 1872 return 1873 } 1874 switch typ { 1875 case chat1.MessageType_TEXT: 1876 knownTeamMentions = valid.MessageBody.Text().TeamMentions 1877 case chat1.MessageType_FLIP: 1878 knownTeamMentions = valid.MessageBody.Flip().TeamMentions 1879 case chat1.MessageType_EDIT: 1880 knownTeamMentions = valid.MessageBody.Edit().TeamMentions 1881 } 1882 for _, tm := range valid.MaybeMentions { 1883 if err := g.TeamMentionLoader.LoadTeamMention(ctx, uid, tm, knownTeamMentions, false); err != nil { 1884 g.GetLog().CDebugf(ctx, "loadTeamMentions: error loading team mentions: %+v", err) 1885 } 1886 } 1887} 1888 1889func presentFlipGameID(ctx context.Context, g *globals.Context, uid gregor1.UID, 1890 convID chat1.ConversationID, msg chat1.MessageUnboxed) *chat1.FlipGameIDStr { 1891 typ, err := msg.State() 1892 if err != nil { 1893 return nil 1894 } 1895 var body chat1.MessageBody 1896 switch typ { 1897 case chat1.MessageUnboxedState_VALID: 1898 body = msg.Valid().MessageBody 1899 case chat1.MessageUnboxedState_OUTBOX: 1900 body = msg.Outbox().Msg.MessageBody 1901 default: 1902 return nil 1903 } 1904 if !body.IsType(chat1.MessageType_FLIP) { 1905 return nil 1906 } 1907 if msg.GetTopicType() == chat1.TopicType_CHAT && !msg.IsOutbox() { 1908 // only queue up a flip load for the flip messages in chat channels 1909 g.CoinFlipManager.LoadFlip(ctx, uid, convID, msg.GetMessageID(), body.Flip().FlipConvID, 1910 body.Flip().GameID) 1911 } 1912 ret := body.Flip().GameID.FlipGameIDStr() 1913 return &ret 1914} 1915 1916func PresentMessagesUnboxed(ctx context.Context, g *globals.Context, msgs []chat1.MessageUnboxed, 1917 uid gregor1.UID, convID chat1.ConversationID) (res []chat1.UIMessage) { 1918 res = make([]chat1.UIMessage, 0, len(msgs)) 1919 for _, msg := range msgs { 1920 res = append(res, PresentMessageUnboxed(ctx, g, msg, uid, convID)) 1921 } 1922 return res 1923} 1924 1925func PresentMessageUnboxed(ctx context.Context, g *globals.Context, rawMsg chat1.MessageUnboxed, 1926 uid gregor1.UID, convID chat1.ConversationID) (res chat1.UIMessage) { 1927 miscErr := func(err error) chat1.UIMessage { 1928 return chat1.NewUIMessageWithError(chat1.MessageUnboxedError{ 1929 ErrType: chat1.MessageUnboxedErrorType_MISC, 1930 ErrMsg: err.Error(), 1931 MessageID: rawMsg.GetMessageID(), 1932 }) 1933 } 1934 1935 collapses := NewCollapses(g) 1936 state, err := rawMsg.State() 1937 if err != nil { 1938 return miscErr(err) 1939 } 1940 switch state { 1941 case chat1.MessageUnboxedState_VALID: 1942 valid := rawMsg.Valid() 1943 if !rawMsg.IsValidFull() { 1944 // If we have an expired ephemeral message, don't show an error 1945 // message. 1946 if !(valid.IsEphemeral() && valid.IsEphemeralExpired(time.Now())) { 1947 return miscErr(fmt.Errorf("unexpected deleted %v message", 1948 strings.ToLower(rawMsg.GetMessageType().String()))) 1949 } 1950 } 1951 var strOutboxID *string 1952 if valid.ClientHeader.OutboxID != nil { 1953 so := valid.ClientHeader.OutboxID.String() 1954 strOutboxID = &so 1955 } 1956 var replyTo *chat1.UIMessage 1957 if valid.ReplyTo != nil { 1958 replyTo = new(chat1.UIMessage) 1959 *replyTo = PresentMessageUnboxed(ctx, g, *valid.ReplyTo, uid, convID) 1960 } 1961 var pinnedMessageID *chat1.MessageID 1962 if valid.MessageBody.IsType(chat1.MessageType_PIN) { 1963 pinnedMessageID = new(chat1.MessageID) 1964 *pinnedMessageID = valid.MessageBody.Pin().MsgID 1965 } 1966 loadTeamMentions(ctx, g, uid, valid) 1967 bodySummary, _ := GetMsgSnippetBody(ctx, g, uid, convID, rawMsg) 1968 res = chat1.NewUIMessageWithValid(chat1.UIMessageValid{ 1969 MessageID: rawMsg.GetMessageID(), 1970 Ctime: valid.ServerHeader.Ctime, 1971 OutboxID: strOutboxID, 1972 MessageBody: valid.MessageBody, 1973 DecoratedTextBody: PresentDecoratedTextBody(ctx, g, uid, convID, valid), 1974 BodySummary: bodySummary, 1975 SenderUsername: valid.SenderUsername, 1976 SenderDeviceName: valid.SenderDeviceName, 1977 SenderDeviceType: valid.SenderDeviceType, 1978 SenderDeviceRevokedAt: valid.SenderDeviceRevokedAt, 1979 SenderUID: valid.ClientHeader.Sender, 1980 SenderDeviceID: valid.ClientHeader.SenderDevice, 1981 Superseded: valid.ServerHeader.SupersededBy != 0, 1982 AtMentions: valid.AtMentionUsernames, 1983 ChannelMention: valid.ChannelMention, 1984 ChannelNameMentions: PresentChannelNameMentions(ctx, valid.ChannelNameMentions), 1985 AssetUrlInfo: presentAttachmentAssetInfo(ctx, g, rawMsg, convID), 1986 IsEphemeral: valid.IsEphemeral(), 1987 IsEphemeralExpired: valid.IsEphemeralExpired(time.Now()), 1988 ExplodedBy: valid.ExplodedBy(), 1989 Etime: valid.Etime(), 1990 Reactions: PresentDecoratedReactionMap(ctx, g, uid, convID, valid, valid.Reactions), 1991 HasPairwiseMacs: valid.HasPairwiseMacs(), 1992 FlipGameID: presentFlipGameID(ctx, g, uid, convID, rawMsg), 1993 PaymentInfos: presentPaymentInfo(ctx, g, rawMsg.GetMessageID(), convID, valid), 1994 RequestInfo: presentRequestInfo(ctx, g, rawMsg.GetMessageID(), convID, valid), 1995 Unfurls: PresentUnfurls(ctx, g, uid, convID, valid.Unfurls), 1996 IsDeleteable: IsDeleteableByDeleteMessageType(valid), 1997 IsEditable: IsEditableByEditMessageType(rawMsg.GetMessageType()), 1998 ReplyTo: replyTo, 1999 PinnedMessageID: pinnedMessageID, 2000 BotUsername: valid.BotUsername, 2001 IsCollapsed: collapses.IsCollapsed(ctx, uid, convID, rawMsg.GetMessageID(), 2002 rawMsg.GetMessageType()), 2003 }) 2004 case chat1.MessageUnboxedState_OUTBOX: 2005 var body, title, filename string 2006 var decoratedBody *string 2007 var preview *chat1.MakePreviewRes 2008 typ := rawMsg.Outbox().Msg.ClientHeader.MessageType 2009 switch typ { 2010 case chat1.MessageType_TEXT: 2011 body = rawMsg.Outbox().Msg.MessageBody.Text().Body 2012 decoratedBody = PresentDecoratedPendingTextBody(ctx, g, uid, rawMsg.Outbox().Msg) 2013 case chat1.MessageType_FLIP: 2014 body = rawMsg.Outbox().Msg.MessageBody.Flip().Text 2015 decoratedBody = new(string) 2016 *decoratedBody = EscapeShrugs(ctx, body) 2017 case chat1.MessageType_EDIT: 2018 body = rawMsg.Outbox().Msg.MessageBody.Edit().Body 2019 case chat1.MessageType_ATTACHMENT: 2020 preview = rawMsg.Outbox().Preview 2021 msgBody := rawMsg.Outbox().Msg.MessageBody 2022 btyp, err := msgBody.MessageType() 2023 if err == nil && btyp == chat1.MessageType_ATTACHMENT { 2024 asset := msgBody.Attachment().Object 2025 title = asset.Title 2026 filename = asset.Filename 2027 } 2028 case chat1.MessageType_REACTION: 2029 body = rawMsg.Outbox().Msg.MessageBody.Reaction().Body 2030 decoratedBody = PresentDecoratedPendingTextBody(ctx, g, uid, rawMsg.Outbox().Msg) 2031 } 2032 var replyTo *chat1.UIMessage 2033 if rawMsg.Outbox().ReplyTo != nil { 2034 replyTo = new(chat1.UIMessage) 2035 *replyTo = PresentMessageUnboxed(ctx, g, *rawMsg.Outbox().ReplyTo, uid, convID) 2036 } 2037 res = chat1.NewUIMessageWithOutbox(chat1.UIMessageOutbox{ 2038 State: rawMsg.Outbox().State, 2039 OutboxID: rawMsg.Outbox().OutboxID.String(), 2040 MessageType: typ, 2041 Body: body, 2042 DecoratedTextBody: decoratedBody, 2043 Ctime: rawMsg.Outbox().Ctime, 2044 Ordinal: computeOutboxOrdinal(rawMsg.Outbox()), 2045 Preview: preview, 2046 Title: title, 2047 Filename: filename, 2048 IsEphemeral: rawMsg.Outbox().Msg.IsEphemeral(), 2049 FlipGameID: presentFlipGameID(ctx, g, uid, convID, rawMsg), 2050 ReplyTo: replyTo, 2051 Supersedes: rawMsg.Outbox().Msg.ClientHeader.Supersedes, 2052 }) 2053 case chat1.MessageUnboxedState_ERROR: 2054 res = chat1.NewUIMessageWithError(rawMsg.Error()) 2055 case chat1.MessageUnboxedState_PLACEHOLDER: 2056 res = chat1.NewUIMessageWithPlaceholder(rawMsg.Placeholder()) 2057 case chat1.MessageUnboxedState_JOURNEYCARD: 2058 journeycard := rawMsg.Journeycard() 2059 res = chat1.NewUIMessageWithJourneycard(chat1.UIMessageJourneycard{ 2060 Ordinal: computeOrdinal(journeycard.PrevID, journeycard.Ordinal), 2061 CardType: journeycard.CardType, 2062 HighlightMsgID: journeycard.HighlightMsgID, 2063 OpenTeam: journeycard.OpenTeam, 2064 }) 2065 default: 2066 g.MetaContext(ctx).Debug("PresentMessageUnboxed: unhandled MessageUnboxedState: %v", state) 2067 // res = zero values 2068 } 2069 return res 2070} 2071 2072func PresentPagination(p *chat1.Pagination) (res *chat1.UIPagination) { 2073 if p == nil { 2074 return nil 2075 } 2076 res = new(chat1.UIPagination) 2077 res.Last = p.Last 2078 res.Num = p.Num 2079 res.Next = hex.EncodeToString(p.Next) 2080 res.Previous = hex.EncodeToString(p.Previous) 2081 return res 2082} 2083 2084func DecodePagination(p *chat1.UIPagination) (res *chat1.Pagination, err error) { 2085 if p == nil { 2086 return nil, nil 2087 } 2088 res = new(chat1.Pagination) 2089 res.Last = p.Last 2090 res.Num = p.Num 2091 if res.Next, err = hex.DecodeString(p.Next); err != nil { 2092 return nil, err 2093 } 2094 if res.Previous, err = hex.DecodeString(p.Previous); err != nil { 2095 return nil, err 2096 } 2097 return res, nil 2098} 2099 2100type ConvLocalByConvID []chat1.ConversationLocal 2101 2102func (c ConvLocalByConvID) Len() int { return len(c) } 2103func (c ConvLocalByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2104func (c ConvLocalByConvID) Less(i, j int) bool { 2105 return c[i].GetConvID().Less(c[j].GetConvID()) 2106} 2107 2108type ConvByConvID []chat1.Conversation 2109 2110func (c ConvByConvID) Len() int { return len(c) } 2111func (c ConvByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2112func (c ConvByConvID) Less(i, j int) bool { 2113 return c[i].GetConvID().Less(c[j].GetConvID()) 2114} 2115 2116type RemoteConvByConvID []types.RemoteConversation 2117 2118func (c RemoteConvByConvID) Len() int { return len(c) } 2119func (c RemoteConvByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2120func (c RemoteConvByConvID) Less(i, j int) bool { 2121 return c[i].GetConvID().Less(c[j].GetConvID()) 2122} 2123 2124type RemoteConvByMtime []types.RemoteConversation 2125 2126func (c RemoteConvByMtime) Len() int { return len(c) } 2127func (c RemoteConvByMtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2128func (c RemoteConvByMtime) Less(i, j int) bool { 2129 return GetConvMtime(c[i]) > GetConvMtime(c[j]) 2130} 2131 2132type ConvLocalByTopicName []chat1.ConversationLocal 2133 2134func (c ConvLocalByTopicName) Len() int { return len(c) } 2135func (c ConvLocalByTopicName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2136func (c ConvLocalByTopicName) Less(i, j int) bool { 2137 return c[i].Info.TopicName < c[j].Info.TopicName 2138} 2139 2140type ByConvID []chat1.ConversationID 2141 2142func (c ByConvID) Len() int { return len(c) } 2143func (c ByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2144func (c ByConvID) Less(i, j int) bool { 2145 return c[i].Less(c[j]) 2146} 2147 2148type ByMsgSummaryCtime []chat1.MessageSummary 2149 2150func (c ByMsgSummaryCtime) Len() int { return len(c) } 2151func (c ByMsgSummaryCtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2152func (c ByMsgSummaryCtime) Less(i, j int) bool { 2153 return c[i].Ctime.Before(c[j].Ctime) 2154} 2155 2156type ByMsgUnboxedCtime []chat1.MessageUnboxed 2157 2158func (c ByMsgUnboxedCtime) Len() int { return len(c) } 2159func (c ByMsgUnboxedCtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2160func (c ByMsgUnboxedCtime) Less(i, j int) bool { 2161 return c[i].Valid().ServerHeader.Ctime.Before(c[j].Valid().ServerHeader.Ctime) 2162} 2163 2164type ByMsgUnboxedMsgID []chat1.MessageUnboxed 2165 2166func (c ByMsgUnboxedMsgID) Len() int { return len(c) } 2167func (c ByMsgUnboxedMsgID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2168func (c ByMsgUnboxedMsgID) Less(i, j int) bool { 2169 return c[i].GetMessageID() > c[j].GetMessageID() 2170} 2171 2172type ByMsgID []chat1.MessageID 2173 2174func (m ByMsgID) Len() int { return len(m) } 2175func (m ByMsgID) Swap(i, j int) { m[i], m[j] = m[j], m[i] } 2176func (m ByMsgID) Less(i, j int) bool { return m[i] > m[j] } 2177 2178func NotificationInfoSet(settings *chat1.ConversationNotificationInfo, 2179 apptype keybase1.DeviceType, 2180 kind chat1.NotificationKind, enabled bool) { 2181 if settings.Settings == nil { 2182 settings.Settings = make(map[keybase1.DeviceType]map[chat1.NotificationKind]bool) 2183 } 2184 if settings.Settings[apptype] == nil { 2185 settings.Settings[apptype] = make(map[chat1.NotificationKind]bool) 2186 } 2187 settings.Settings[apptype][kind] = enabled 2188} 2189 2190func DecodeBase64(enc []byte) ([]byte, error) { 2191 if len(enc) == 0 { 2192 return enc, nil 2193 } 2194 2195 b := make([]byte, base64.StdEncoding.DecodedLen(len(enc))) 2196 n, err := base64.StdEncoding.Decode(b, enc) 2197 return b[:n], err 2198} 2199 2200func RemoteConv(conv chat1.Conversation) types.RemoteConversation { 2201 return types.RemoteConversation{ 2202 Conv: conv, 2203 ConvIDStr: conv.GetConvID().ConvIDStr(), 2204 } 2205} 2206 2207func RemoteConvs(convs []chat1.Conversation) (res []types.RemoteConversation) { 2208 res = make([]types.RemoteConversation, 0, len(convs)) 2209 for _, conv := range convs { 2210 res = append(res, RemoteConv(conv)) 2211 } 2212 return res 2213} 2214 2215func PluckConvs(rcs []types.RemoteConversation) (res []chat1.Conversation) { 2216 res = make([]chat1.Conversation, 0, len(rcs)) 2217 for _, rc := range rcs { 2218 res = append(res, rc.Conv) 2219 } 2220 return res 2221} 2222 2223func SplitTLFName(tlfName string) []string { 2224 return strings.Split(strings.Fields(tlfName)[0], ",") 2225} 2226 2227func UsernamePackageToParticipant(p libkb.UsernamePackage) chat1.ConversationLocalParticipant { 2228 var fullName *string 2229 if p.FullName != nil { 2230 s := string(p.FullName.FullName) 2231 fullName = &s 2232 } 2233 return chat1.ConversationLocalParticipant{ 2234 Username: p.NormalizedUsername.String(), 2235 Fullname: fullName, 2236 } 2237} 2238 2239type pagerMsg struct { 2240 msgID chat1.MessageID 2241} 2242 2243func (p pagerMsg) GetMessageID() chat1.MessageID { 2244 return p.msgID 2245} 2246 2247func MessageIDControlToPagination(ctx context.Context, logger DebugLabeler, control *chat1.MessageIDControl, 2248 conv *types.RemoteConversation) (res *chat1.Pagination) { 2249 if control == nil { 2250 return res 2251 } 2252 pag := pager.NewThreadPager() 2253 res = new(chat1.Pagination) 2254 res.Num = control.Num 2255 if control.Pivot != nil { 2256 var err error 2257 pm := pagerMsg{msgID: *control.Pivot} 2258 switch control.Mode { 2259 case chat1.MessageIDControlMode_OLDERMESSAGES: 2260 res.Next, err = pag.MakeIndex(pm) 2261 case chat1.MessageIDControlMode_NEWERMESSAGES: 2262 res.Previous, err = pag.MakeIndex(pm) 2263 case chat1.MessageIDControlMode_UNREADLINE: 2264 if conv == nil { 2265 // just bail out of here with no conversation 2266 logger.Debug(ctx, "MessageIDControlToPagination: unreadline mode with no conv, bailing") 2267 return nil 2268 } 2269 pm.msgID = conv.Conv.ReaderInfo.ReadMsgid 2270 fallthrough 2271 case chat1.MessageIDControlMode_CENTERED: 2272 // Heuristic that we might want to revisit, get older messages from a little ahead of where 2273 // we want to center on 2274 if conv == nil { 2275 // just bail out of here with no conversation 2276 logger.Debug(ctx, "MessageIDControlToPagination: centered mode with no conv, bailing") 2277 return nil 2278 } 2279 maxID := int(conv.Conv.MaxVisibleMsgID()) 2280 desired := int(pm.msgID) + control.Num/2 2281 logger.Debug(ctx, "MessageIDControlToPagination: maxID: %d desired: %d", maxID, desired) 2282 if desired > maxID { 2283 desired = maxID 2284 } 2285 pm.msgID = chat1.MessageID(desired + 1) 2286 res.Next, err = pag.MakeIndex(pm) 2287 res.ForceFirstPage = true 2288 } 2289 if err != nil { 2290 return nil 2291 } 2292 } 2293 return res 2294} 2295 2296// AssetsForMessage gathers all assets on a message 2297func AssetsForMessage(g *globals.Context, msgBody chat1.MessageBody) (assets []chat1.Asset) { 2298 typ, err := msgBody.MessageType() 2299 if err != nil { 2300 // Log and drop the error for a malformed MessageBody. 2301 g.Log.Warning("error getting assets for message: %s", err) 2302 return assets 2303 } 2304 switch typ { 2305 case chat1.MessageType_ATTACHMENT: 2306 body := msgBody.Attachment() 2307 if body.Object.Path != "" { 2308 assets = append(assets, body.Object) 2309 } 2310 if body.Preview != nil { 2311 assets = append(assets, *body.Preview) 2312 } 2313 assets = append(assets, body.Previews...) 2314 case chat1.MessageType_ATTACHMENTUPLOADED: 2315 body := msgBody.Attachmentuploaded() 2316 if body.Object.Path != "" { 2317 assets = append(assets, body.Object) 2318 } 2319 assets = append(assets, body.Previews...) 2320 } 2321 return assets 2322} 2323 2324func AddUserToTLFName(g *globals.Context, tlfName string, vis keybase1.TLFVisibility, 2325 membersType chat1.ConversationMembersType) string { 2326 switch membersType { 2327 case chat1.ConversationMembersType_IMPTEAMNATIVE, chat1.ConversationMembersType_IMPTEAMUPGRADE, 2328 chat1.ConversationMembersType_KBFS: 2329 if vis == keybase1.TLFVisibility_PUBLIC { 2330 return tlfName 2331 } 2332 2333 username := g.Env.GetUsername().String() 2334 if len(tlfName) == 0 { 2335 return username 2336 } 2337 2338 // KBFS creates TLFs with suffixes (e.g., folder names that 2339 // conflict after an assertion has been resolved) and readers, 2340 // so we need to handle those types of TLF names here so that 2341 // edit history works correctly. 2342 split1 := strings.SplitN(tlfName, " ", 2) // split off suffix 2343 split2 := strings.Split(split1[0], "#") // split off readers 2344 // Add the name to the writers list (assume the current user 2345 // is a writer). 2346 tlfName = split2[0] + "," + username 2347 if len(split2) > 1 { 2348 // Re-append any readers. 2349 tlfName += "#" + split2[1] 2350 } 2351 if len(split1) > 1 { 2352 // Re-append any suffix. 2353 tlfName += " " + split1[1] 2354 } 2355 return tlfName 2356 default: 2357 return tlfName 2358 } 2359} 2360 2361func ForceReloadUPAKsForUIDs(ctx context.Context, g *globals.Context, uids []keybase1.UID) error { 2362 getArg := func(i int) *libkb.LoadUserArg { 2363 if i >= len(uids) { 2364 return nil 2365 } 2366 tmp := libkb.NewLoadUserByUIDForceArg(g.GlobalContext, uids[i]) 2367 return &tmp 2368 } 2369 return g.GetUPAKLoader().Batcher(ctx, getArg, nil, 0) 2370} 2371 2372func CreateHiddenPlaceholder(msgID chat1.MessageID) chat1.MessageUnboxed { 2373 return chat1.NewMessageUnboxedWithPlaceholder( 2374 chat1.MessageUnboxedPlaceholder{ 2375 MessageID: msgID, 2376 Hidden: true, 2377 }) 2378} 2379 2380func GetGregorConn(ctx context.Context, g *globals.Context, log DebugLabeler, 2381 handler func(nist *libkb.NIST) rpc.ConnectionHandler) (conn *rpc.Connection, token gregor1.SessionToken, err error) { 2382 // Get session token 2383 nist, _, _, err := g.ActiveDevice.NISTAndUIDDeviceID(ctx) 2384 if nist == nil { 2385 log.Debug(ctx, "GetGregorConn: got a nil NIST, is the user logged out?") 2386 return conn, token, libkb.LoggedInError{} 2387 } 2388 if err != nil { 2389 log.Debug(ctx, "GetGregorConn: failed to get logged in session: %s", err.Error()) 2390 return conn, token, err 2391 } 2392 token = gregor1.SessionToken(nist.Token().String()) 2393 2394 // Make an ad hoc connection to gregor 2395 uri, err := rpc.ParseFMPURI(g.Env.GetGregorURI()) 2396 if err != nil { 2397 log.Debug(ctx, "GetGregorConn: failed to parse chat server UR: %s", err.Error()) 2398 return conn, token, err 2399 } 2400 2401 if uri.UseTLS() { 2402 rawCA := g.Env.GetBundledCA(uri.Host) 2403 if len(rawCA) == 0 { 2404 err := errors.New("len(rawCA) == 0") 2405 log.Debug(ctx, "GetGregorConn: failed to parse CAs", err.Error()) 2406 return conn, token, err 2407 } 2408 conn = rpc.NewTLSConnectionWithDialable(rpc.NewFixedRemote(uri.HostPort), 2409 []byte(rawCA), libkb.NewContextifiedErrorUnwrapper(g.ExternalG()), 2410 handler(nist), libkb.NewRPCLogFactory(g.ExternalG()), 2411 g.ExternalG().RemoteNetworkInstrumenterStorage, 2412 logger.LogOutputWithDepthAdder{Logger: g.Log}, 2413 rpc.DefaultMaxFrameLength, rpc.ConnectionOpts{}, 2414 libkb.NewProxyDialable(g.Env)) 2415 } else { 2416 t := rpc.NewConnectionTransportWithDialable(uri, nil, 2417 g.ExternalG().RemoteNetworkInstrumenterStorage, 2418 libkb.MakeWrapError(g.ExternalG()), 2419 rpc.DefaultMaxFrameLength, libkb.NewProxyDialable(g.GetEnv())) 2420 conn = rpc.NewConnectionWithTransport(handler(nist), t, 2421 libkb.NewContextifiedErrorUnwrapper(g.ExternalG()), 2422 logger.LogOutputWithDepthAdder{Logger: g.Log}, rpc.ConnectionOpts{}) 2423 } 2424 return conn, token, nil 2425} 2426 2427// GetQueryRe returns a regex to match the query string on message text. This 2428// is used for result highlighting. 2429func GetQueryRe(query string) (*regexp.Regexp, error) { 2430 return regexp.Compile("(?i)" + regexp.QuoteMeta(query)) 2431} 2432 2433func SetUnfurl(mvalid *chat1.MessageUnboxedValid, unfurlMessageID chat1.MessageID, 2434 unfurl chat1.UnfurlResult) { 2435 if mvalid.Unfurls == nil { 2436 mvalid.Unfurls = make(map[chat1.MessageID]chat1.UnfurlResult) 2437 } 2438 mvalid.Unfurls[unfurlMessageID] = unfurl 2439} 2440 2441func RemoveUnfurl(mvalid *chat1.MessageUnboxedValid, unfurlMessageID chat1.MessageID) { 2442 if mvalid.Unfurls == nil { 2443 return 2444 } 2445 delete(mvalid.Unfurls, unfurlMessageID) 2446} 2447 2448// SuspendComponent will suspend a Suspendable type until the return function 2449// is called. This allows a succinct call like defer SuspendComponent(ctx, g, 2450// g.ConvLoader)() in RPC handlers wishing to lock out the conv loader. 2451func SuspendComponent(ctx context.Context, g *globals.Context, suspendable types.Suspendable) func() { 2452 if canceled := suspendable.Suspend(ctx); canceled { 2453 g.Log.CDebugf(ctx, "SuspendComponent: canceled background task") 2454 } 2455 return func() { 2456 suspendable.Resume(ctx) 2457 } 2458} 2459 2460func SuspendComponents(ctx context.Context, g *globals.Context, suspendables []types.Suspendable) func() { 2461 resumeFuncs := make([]func(), 0, len(suspendables)) 2462 for _, s := range suspendables { 2463 resumeFuncs = append(resumeFuncs, SuspendComponent(ctx, g, s)) 2464 } 2465 return func() { 2466 for _, f := range resumeFuncs { 2467 f() 2468 } 2469 } 2470} 2471 2472func IsPermanentErr(err error) bool { 2473 if uberr, ok := err.(types.UnboxingError); ok { 2474 return uberr.IsPermanent() 2475 } 2476 return err != nil 2477} 2478 2479func EphemeralLifetimeFromConv(ctx context.Context, g *globals.Context, conv chat1.ConversationLocal) (res *gregor1.DurationSec, err error) { 2480 // Check to see if the conversation has an exploding policy 2481 var retentionRes *gregor1.DurationSec 2482 var gregorRes *gregor1.DurationSec 2483 var rentTyp chat1.RetentionPolicyType 2484 var convSet bool 2485 if conv.ConvRetention != nil { 2486 if rentTyp, err = conv.ConvRetention.Typ(); err != nil { 2487 return res, err 2488 } 2489 if rentTyp == chat1.RetentionPolicyType_EPHEMERAL { 2490 e := conv.ConvRetention.Ephemeral() 2491 retentionRes = &e.Age 2492 } 2493 convSet = rentTyp != chat1.RetentionPolicyType_INHERIT 2494 } 2495 if !convSet && conv.TeamRetention != nil { 2496 if rentTyp, err = conv.TeamRetention.Typ(); err != nil { 2497 return res, err 2498 } 2499 if rentTyp == chat1.RetentionPolicyType_EPHEMERAL { 2500 e := conv.TeamRetention.Ephemeral() 2501 retentionRes = &e.Age 2502 } 2503 } 2504 2505 // See if there is anything in Gregor 2506 st, err := g.GregorState.State(ctx) 2507 if err != nil { 2508 return res, err 2509 } 2510 // Note: this value is present on the JS frontend as well 2511 key := fmt.Sprintf("exploding:%s", conv.GetConvID()) 2512 cat, err := gregor1.ObjFactory{}.MakeCategory(key) 2513 if err != nil { 2514 return res, err 2515 } 2516 items, err := st.ItemsWithCategoryPrefix(cat) 2517 if err != nil { 2518 return res, err 2519 } 2520 if len(items) > 0 { 2521 it := items[0] 2522 body := string(it.Body().Bytes()) 2523 sec, err := strconv.ParseInt(body, 0, 0) 2524 if err != nil { 2525 return res, nil 2526 } 2527 gsec := gregor1.DurationSec(sec) 2528 gregorRes = &gsec 2529 } 2530 if retentionRes != nil && gregorRes != nil { 2531 if *gregorRes < *retentionRes { 2532 return gregorRes, nil 2533 } 2534 return retentionRes, nil 2535 } else if retentionRes != nil { 2536 return retentionRes, nil 2537 } else if gregorRes != nil { 2538 return gregorRes, nil 2539 } else { 2540 return nil, nil 2541 } 2542} 2543 2544var decorateBegin = "$>kb$" 2545var decorateEnd = "$<kb$" 2546var decorateEscapeRe = regexp.MustCompile(`\\*\$\>kb\$`) 2547 2548func EscapeForDecorate(ctx context.Context, body string) string { 2549 // escape any natural occurrences of begin so we don't bust markdown parser 2550 return decorateEscapeRe.ReplaceAllStringFunc(body, func(s string) string { 2551 if len(s)%2 != 0 { 2552 return `\` + s 2553 } 2554 return s 2555 }) 2556} 2557 2558func DecorateBody(ctx context.Context, body string, offset, length int, decoration interface{}) (res string, added int) { 2559 out, err := json.Marshal(decoration) 2560 if err != nil { 2561 return res, 0 2562 } 2563 //b64out := string(out) 2564 b64out := base64.StdEncoding.EncodeToString(out) 2565 strDecoration := fmt.Sprintf("%s%s%s", decorateBegin, b64out, decorateEnd) 2566 added = len(strDecoration) - length 2567 res = fmt.Sprintf("%s%s%s", body[:offset], strDecoration, body[offset+length:]) 2568 return res, added 2569} 2570 2571var linkRegexp = xurls.Relaxed() 2572 2573// These indices correspond to the named capture groups in the xurls regexes 2574var linkRelaxedGroupIndex = 0 2575var linkStrictGroupIndex = 0 2576var mailtoRegexp = regexp.MustCompile(`(?:(?:[\w-_.]+)@(?:[\w-]+(?:\.[\w-]+)+))\b`) 2577 2578func init() { 2579 for index, name := range linkRegexp.SubexpNames() { 2580 if name == "relaxed" { 2581 linkRelaxedGroupIndex = index + 1 2582 } 2583 if name == "strict" { 2584 linkStrictGroupIndex = index + 1 2585 } 2586 } 2587} 2588 2589func DecorateWithLinks(ctx context.Context, body string) string { 2590 var added int 2591 offset := 0 2592 origBody := body 2593 2594 // early out of here if there is no dot 2595 if !(strings.Contains(body, ".") || strings.Contains(body, "://")) { 2596 return body 2597 } 2598 shouldSkipLink := func(body string) bool { 2599 if strings.Contains(strings.Split(body, "/")[0], "@") { 2600 return true 2601 } 2602 for _, scheme := range xurls.SchemesNoAuthority { 2603 if strings.HasPrefix(body, scheme) { 2604 return true 2605 } 2606 } 2607 if strings.HasPrefix(body, "ftp://") || strings.HasPrefix(body, "gopher://") { 2608 return true 2609 } 2610 return false 2611 } 2612 allMatches := linkRegexp.FindAllStringSubmatchIndex(ReplaceQuotedSubstrings(body, true), -1) 2613 for _, match := range allMatches { 2614 var lowhit, highhit int 2615 if len(match) >= linkRelaxedGroupIndex*2 && match[linkRelaxedGroupIndex*2-2] >= 0 { 2616 lowhit = linkRelaxedGroupIndex*2 - 2 2617 highhit = linkRelaxedGroupIndex*2 - 1 2618 } else if len(match) >= linkStrictGroupIndex*2 && match[linkStrictGroupIndex*2-2] >= 0 { 2619 lowhit = linkStrictGroupIndex*2 - 2 2620 highhit = linkStrictGroupIndex*2 - 1 2621 } else { 2622 continue 2623 } 2624 2625 bodyMatch := origBody[match[lowhit]:match[highhit]] 2626 url := bodyMatch 2627 var punycode string 2628 if shouldSkipLink(bodyMatch) { 2629 continue 2630 } 2631 if encoded, err := idna.ToASCII(url); err == nil && encoded != url { 2632 punycode = encoded 2633 } 2634 body, added = DecorateBody(ctx, body, match[lowhit]+offset, match[highhit]-match[lowhit], 2635 chat1.NewUITextDecorationWithLink(chat1.UILinkDecoration{ 2636 Url: bodyMatch, 2637 Punycode: punycode, 2638 })) 2639 offset += added 2640 } 2641 2642 offset = 0 2643 origBody = body 2644 allMatches = mailtoRegexp.FindAllStringIndex(ReplaceQuotedSubstrings(body, true), -1) 2645 for _, match := range allMatches { 2646 if len(match) < 2 { 2647 continue 2648 } 2649 bodyMatch := origBody[match[0]:match[1]] 2650 body, added = DecorateBody(ctx, body, match[0]+offset, match[1]-match[0], 2651 chat1.NewUITextDecorationWithMailto(chat1.UILinkDecoration{ 2652 Url: bodyMatch, 2653 })) 2654 offset += added 2655 } 2656 2657 return body 2658} 2659 2660func DecorateWithMentions(ctx context.Context, body string, atMentions []string, 2661 maybeMentions []chat1.MaybeMention, chanMention chat1.ChannelMention, 2662 channelNameMentions []chat1.ChannelNameMention) string { 2663 var added int 2664 offset := 0 2665 if len(atMentions) > 0 || len(maybeMentions) > 0 || chanMention != chat1.ChannelMention_NONE { 2666 atMap := make(map[string]bool) 2667 for _, at := range atMentions { 2668 atMap[at] = true 2669 } 2670 maybeMap := make(map[string]chat1.MaybeMention) 2671 for _, tm := range maybeMentions { 2672 name := tm.Name 2673 if len(tm.Channel) > 0 { 2674 name += "#" + tm.Channel 2675 } 2676 maybeMap[name] = tm 2677 } 2678 inputBody := body 2679 atMatches := parseRegexpNames(ctx, inputBody, atMentionRegExp) 2680 for _, m := range atMatches { 2681 switch { 2682 case m.normalizedName == "here": 2683 fallthrough 2684 case m.normalizedName == "channel": 2685 fallthrough 2686 case m.normalizedName == "everyone": 2687 if chanMention == chat1.ChannelMention_NONE { 2688 continue 2689 } 2690 fallthrough 2691 case atMap[m.normalizedName]: 2692 body, added = DecorateBody(ctx, body, m.position[0]+offset-1, m.Len()+1, 2693 chat1.NewUITextDecorationWithAtmention(m.name)) 2694 offset += added 2695 } 2696 if tm, ok := maybeMap[m.name]; ok { 2697 body, added = DecorateBody(ctx, body, m.position[0]+offset-1, m.Len()+1, 2698 chat1.NewUITextDecorationWithMaybemention(tm)) 2699 offset += added 2700 } 2701 } 2702 } 2703 if len(channelNameMentions) > 0 { 2704 chanMap := make(map[string]chat1.ConversationID) 2705 for _, c := range channelNameMentions { 2706 chanMap[c.TopicName] = c.ConvID 2707 } 2708 offset = 0 2709 inputBody := body 2710 chanMatches := parseRegexpNames(ctx, inputBody, chanNameMentionRegExp) 2711 for _, c := range chanMatches { 2712 convID, ok := chanMap[c.name] 2713 if !ok { 2714 continue 2715 } 2716 body, added = DecorateBody(ctx, body, c.position[0]+offset-1, c.Len()+1, 2717 chat1.NewUITextDecorationWithChannelnamemention(chat1.UIChannelNameMention{ 2718 Name: c.name, 2719 ConvID: convID.ConvIDStr(), 2720 })) 2721 offset += added 2722 } 2723 } 2724 return body 2725} 2726 2727func EscapeShrugs(ctx context.Context, body string) string { 2728 return strings.Replace(body, `¯\_(ツ)_/¯`, `¯\\\_(ツ)_/¯`, -1) 2729} 2730 2731var startQuote = ">" 2732var newline = []rune("\n") 2733 2734var blockQuoteRegex = regexp.MustCompile("((?s)```.*?```)") 2735var quoteRegex = regexp.MustCompile("((?s)`.*?`)") 2736 2737func ReplaceQuotedSubstrings(xs string, skipAngleQuotes bool) string { 2738 replacer := func(s string) string { 2739 return strings.Repeat("$", len(s)) 2740 } 2741 xs = blockQuoteRegex.ReplaceAllStringFunc(xs, replacer) 2742 xs = quoteRegex.ReplaceAllStringFunc(xs, replacer) 2743 2744 // Remove all quoted lines. Because we removed all codeblocks 2745 // before, we only need to consider single lines. 2746 var ret []string 2747 for _, line := range strings.Split(xs, string(newline)) { 2748 if skipAngleQuotes || !strings.HasPrefix(strings.TrimLeft(line, " "), startQuote) { 2749 ret = append(ret, line) 2750 } else { 2751 ret = append(ret, replacer(line)) 2752 } 2753 } 2754 return strings.Join(ret, string(newline)) 2755} 2756 2757var ErrGetUnverifiedConvNotFound = errors.New("GetUnverifiedConv: conversation not found") 2758var ErrGetVerifiedConvNotFound = errors.New("GetVerifiedConv: conversation not found") 2759 2760func GetUnverifiedConv(ctx context.Context, g *globals.Context, uid gregor1.UID, 2761 convID chat1.ConversationID, dataSource types.InboxSourceDataSourceTyp) (res types.RemoteConversation, err error) { 2762 2763 inbox, err := g.InboxSource.ReadUnverified(ctx, uid, dataSource, &chat1.GetInboxQuery{ 2764 ConvIDs: []chat1.ConversationID{convID}, 2765 MemberStatus: chat1.AllConversationMemberStatuses(), 2766 }) 2767 if err != nil { 2768 return res, err 2769 } 2770 if len(inbox.ConvsUnverified) == 0 { 2771 return res, ErrGetUnverifiedConvNotFound 2772 } 2773 if !inbox.ConvsUnverified[0].GetConvID().Eq(convID) { 2774 return res, fmt.Errorf("GetUnverifiedConv: convID mismatch: %s != %s", 2775 inbox.ConvsUnverified[0].ConvIDStr, convID) 2776 } 2777 return inbox.ConvsUnverified[0], nil 2778} 2779 2780func FormatConversationName(info chat1.ConversationInfoLocal, myUsername string) string { 2781 switch info.TeamType { 2782 case chat1.TeamType_COMPLEX: 2783 if len(info.TlfName) > 0 && len(info.TopicName) > 0 { 2784 return fmt.Sprintf("%s#%s", info.TlfName, info.TopicName) 2785 } 2786 return info.TlfName 2787 case chat1.TeamType_SIMPLE: 2788 return info.TlfName 2789 case chat1.TeamType_NONE: 2790 users := info.Participants 2791 if len(users) == 1 { 2792 return "" 2793 } 2794 var usersWithoutYou []string 2795 for _, user := range users { 2796 if user.Username != myUsername && user.InConvName { 2797 usersWithoutYou = append(usersWithoutYou, user.Username) 2798 } 2799 } 2800 return strings.Join(usersWithoutYou, ",") 2801 default: 2802 return "" 2803 } 2804} 2805 2806func GetVerifiedConv(ctx context.Context, g *globals.Context, uid gregor1.UID, 2807 convID chat1.ConversationID, dataSource types.InboxSourceDataSourceTyp) (res chat1.ConversationLocal, err error) { 2808 // in case we are being called from within some cancelable context, remove 2809 // it for the purposes of this call, since whatever this is is likely a 2810 // side effect we don't want to get stuck 2811 ctx = globals.CtxRemoveLocalizerCancelable(ctx) 2812 inbox, _, err := g.InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, dataSource, nil, 2813 &chat1.GetInboxLocalQuery{ 2814 ConvIDs: []chat1.ConversationID{convID}, 2815 MemberStatus: chat1.AllConversationMemberStatuses(), 2816 }) 2817 if err != nil { 2818 return res, err 2819 } 2820 if len(inbox.Convs) == 0 { 2821 return res, ErrGetVerifiedConvNotFound 2822 } 2823 if !inbox.Convs[0].GetConvID().Eq(convID) { 2824 return res, fmt.Errorf("GetVerifiedConv: convID mismatch: %s != %s", 2825 inbox.Convs[0].GetConvID(), convID) 2826 } 2827 return inbox.Convs[0], nil 2828} 2829 2830func IsMapUnfurl(msg chat1.MessageUnboxed) bool { 2831 if !msg.IsValid() { 2832 return false 2833 } 2834 body := msg.Valid().MessageBody 2835 if !body.IsType(chat1.MessageType_UNFURL) { 2836 return false 2837 } 2838 unfurl := body.Unfurl() 2839 typ, err := unfurl.Unfurl.Unfurl.UnfurlType() 2840 if err != nil { 2841 return false 2842 } 2843 if typ != chat1.UnfurlType_GENERIC { 2844 return false 2845 } 2846 return body.Unfurl().Unfurl.Unfurl.Generic().MapInfo != nil 2847} 2848 2849func DedupStringLists(lists ...[]string) (res []string) { 2850 seen := make(map[string]struct{}) 2851 for _, list := range lists { 2852 for _, x := range list { 2853 if _, ok := seen[x]; !ok { 2854 seen[x] = struct{}{} 2855 res = append(res, x) 2856 } 2857 } 2858 } 2859 return res 2860} 2861 2862func DBConvLess(a pager.InboxEntry, b pager.InboxEntry) bool { 2863 if a.GetMtime() > b.GetMtime() { 2864 return true 2865 } else if a.GetMtime() < b.GetMtime() { 2866 return false 2867 } 2868 return !(a.GetConvID().Eq(b.GetConvID()) || a.GetConvID().Less(b.GetConvID())) 2869} 2870 2871func ExportToSummary(i chat1.InboxUIItem) (s chat1.ConvSummary) { 2872 s.Id = i.ConvID 2873 s.IsDefaultConv = i.IsDefaultConv 2874 s.Unread = i.ReadMsgID < i.MaxVisibleMsgID 2875 s.ActiveAt = i.Time.UnixSeconds() 2876 s.ActiveAtMs = i.Time.UnixMilliseconds() 2877 s.FinalizeInfo = i.FinalizeInfo 2878 s.CreatorInfo = i.CreatorInfo 2879 s.MemberStatus = strings.ToLower(i.MemberStatus.String()) 2880 s.Supersedes = make([]string, 0, len(i.Supersedes)) 2881 for _, super := range i.Supersedes { 2882 s.Supersedes = append(s.Supersedes, 2883 super.ConversationID.String()) 2884 } 2885 s.SupersededBy = make([]string, 0, len(i.SupersededBy)) 2886 for _, super := range i.SupersededBy { 2887 s.SupersededBy = append(s.SupersededBy, 2888 super.ConversationID.String()) 2889 } 2890 switch i.MembersType { 2891 case chat1.ConversationMembersType_IMPTEAMUPGRADE, chat1.ConversationMembersType_IMPTEAMNATIVE: 2892 s.ResetUsers = i.ResetParticipants 2893 } 2894 s.Channel = chat1.ChatChannel{ 2895 Name: i.Name, 2896 Public: i.IsPublic, 2897 TopicType: strings.ToLower(i.TopicType.String()), 2898 MembersType: strings.ToLower(i.MembersType.String()), 2899 TopicName: i.Channel, 2900 } 2901 return s 2902} 2903 2904func supersedersNotEmpty(ctx context.Context, superseders []chat1.ConversationMetadata, convs []types.RemoteConversation) bool { 2905 for _, superseder := range superseders { 2906 for _, conv := range convs { 2907 if superseder.ConversationID.Eq(conv.GetConvID()) { 2908 for _, msg := range conv.Conv.MaxMsgSummaries { 2909 if IsNonEmptyConvMessageType(msg.GetMessageType()) { 2910 return true 2911 } 2912 } 2913 } 2914 } 2915 } 2916 return false 2917} 2918 2919var defaultMemberStatusFilter = []chat1.ConversationMemberStatus{ 2920 chat1.ConversationMemberStatus_ACTIVE, 2921 chat1.ConversationMemberStatus_PREVIEW, 2922 chat1.ConversationMemberStatus_RESET, 2923} 2924 2925var defaultExistences = []chat1.ConversationExistence{ 2926 chat1.ConversationExistence_ACTIVE, 2927} 2928 2929func ApplyInboxQuery(ctx context.Context, debugLabeler DebugLabeler, query *chat1.GetInboxQuery, rcs []types.RemoteConversation) (res []types.RemoteConversation) { 2930 if query == nil { 2931 query = &chat1.GetInboxQuery{} 2932 } 2933 2934 var queryConvIDMap map[chat1.ConvIDStr]bool 2935 if query.ConvID != nil { 2936 query.ConvIDs = append(query.ConvIDs, *query.ConvID) 2937 } 2938 if len(query.ConvIDs) > 0 { 2939 queryConvIDMap = make(map[chat1.ConvIDStr]bool, len(query.ConvIDs)) 2940 for _, c := range query.ConvIDs { 2941 queryConvIDMap[c.ConvIDStr()] = true 2942 } 2943 } 2944 2945 memberStatus := query.MemberStatus 2946 if len(memberStatus) == 0 { 2947 memberStatus = defaultMemberStatusFilter 2948 } 2949 queryMemberStatusMap := map[chat1.ConversationMemberStatus]bool{} 2950 for _, memberStatus := range memberStatus { 2951 queryMemberStatusMap[memberStatus] = true 2952 } 2953 2954 queryStatusMap := map[chat1.ConversationStatus]bool{} 2955 for _, status := range query.Status { 2956 queryStatusMap[status] = true 2957 } 2958 2959 existences := query.Existences 2960 if len(existences) == 0 { 2961 existences = defaultExistences 2962 } 2963 existenceMap := map[chat1.ConversationExistence]bool{} 2964 for _, status := range existences { 2965 existenceMap[status] = true 2966 } 2967 2968 for _, rc := range rcs { 2969 conv := rc.Conv 2970 // Existence check 2971 if _, ok := existenceMap[conv.Metadata.Existence]; !ok && len(existenceMap) > 0 { 2972 continue 2973 } 2974 // Member status check 2975 if _, ok := queryMemberStatusMap[conv.ReaderInfo.Status]; !ok && len(memberStatus) > 0 { 2976 continue 2977 } 2978 // Status check 2979 if _, ok := queryStatusMap[conv.Metadata.Status]; !ok && len(query.Status) > 0 { 2980 continue 2981 } 2982 // Basic checks 2983 if queryConvIDMap != nil && !queryConvIDMap[rc.ConvIDStr] { 2984 continue 2985 } 2986 if query.After != nil && !conv.ReaderInfo.Mtime.After(*query.After) { 2987 continue 2988 } 2989 if query.Before != nil && !conv.ReaderInfo.Mtime.Before(*query.Before) { 2990 continue 2991 } 2992 if query.TopicType != nil && *query.TopicType != conv.Metadata.IdTriple.TopicType { 2993 continue 2994 } 2995 if query.TlfVisibility != nil && *query.TlfVisibility != keybase1.TLFVisibility_ANY && 2996 *query.TlfVisibility != conv.Metadata.Visibility { 2997 continue 2998 } 2999 if query.UnreadOnly && !conv.IsUnread() { 3000 continue 3001 } 3002 if query.ReadOnly && conv.IsUnread() { 3003 continue 3004 } 3005 if query.TlfID != nil && !query.TlfID.Eq(conv.Metadata.IdTriple.Tlfid) { 3006 continue 3007 } 3008 if query.TopicName != nil && rc.LocalMetadata != nil && 3009 *query.TopicName != rc.LocalMetadata.TopicName { 3010 continue 3011 } 3012 // If we are finalized and are superseded, then don't return this 3013 if query.OneChatTypePerTLF == nil || 3014 (query.OneChatTypePerTLF != nil && *query.OneChatTypePerTLF) { 3015 if conv.Metadata.FinalizeInfo != nil && len(conv.Metadata.SupersededBy) > 0 && len(query.ConvIDs) == 0 { 3016 if supersedersNotEmpty(ctx, conv.Metadata.SupersededBy, rcs) { 3017 continue 3018 } 3019 } 3020 } 3021 res = append(res, rc) 3022 } 3023 filtered := len(rcs) - len(res) 3024 debugLabeler.Debug(ctx, "applyQuery: query: %+v, res size: %d filtered: %d", query, len(res), filtered) 3025 return res 3026} 3027 3028func ToLastActiveStatus(mtime gregor1.Time) chat1.LastActiveStatus { 3029 lastActive := int(time.Since(mtime.Time()).Round(time.Hour).Hours()) 3030 switch { 3031 case lastActive <= 24: // 1 day 3032 return chat1.LastActiveStatus_ACTIVE 3033 case lastActive <= 24*7: // 7 days 3034 return chat1.LastActiveStatus_RECENTLY_ACTIVE 3035 default: 3036 return chat1.LastActiveStatus_NONE 3037 } 3038} 3039 3040func GetConvParticipantUsernames(ctx context.Context, g *globals.Context, uid gregor1.UID, 3041 convID chat1.ConversationID) (parts []string, err error) { 3042 uids, err := g.ParticipantsSource.Get(ctx, uid, convID, types.InboxSourceDataSourceAll) 3043 if err != nil { 3044 return parts, err 3045 } 3046 kuids := make([]keybase1.UID, 0, len(uids)) 3047 for _, uid := range uids { 3048 kuids = append(kuids, keybase1.UID(uid.String())) 3049 } 3050 rows, err := g.UIDMapper.MapUIDsToUsernamePackages(ctx, g, kuids, 0, 0, false) 3051 if err != nil { 3052 return parts, err 3053 } 3054 parts = make([]string, 0, len(rows)) 3055 for _, row := range rows { 3056 parts = append(parts, row.NormalizedUsername.String()) 3057 } 3058 return parts, nil 3059} 3060 3061func IsDeletedConvError(err error) bool { 3062 switch err.(type) { 3063 case libkb.ChatBadConversationError, 3064 libkb.ChatNotInTeamError, 3065 libkb.ChatNotInConvError: 3066 return true 3067 default: 3068 return false 3069 } 3070} 3071