github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/chat/utils/utils.go (about) 1 package utils 2 3 import ( 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 "mvdan.cc/xurls/v2" 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 37 func 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". 51 func 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 75 func 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 94 func 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 106 func 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 120 func 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 132 func 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. 144 func 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. 212 func 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. 223 func 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 252 func isPhoneOrEmail(username string) bool { 253 return strings.HasSuffix(username, "@phone") || strings.HasSuffix(username, "@email") 254 } 255 256 const ( 257 ChatTopicIDLen = 16 258 ChatTopicIDSuffix = 0x20 259 ) 260 261 func 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 269 func 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 279 type 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 295 type 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 306 func 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 355 func 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 416 type byConversationStatus []chat1.ConversationStatus 417 418 func (c byConversationStatus) Len() int { return len(c) } 419 func (c byConversationStatus) Less(i, j int) bool { return c[i] < c[j] } 420 func (c byConversationStatus) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 421 422 // Which convs show in the inbox. 423 func 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 434 func 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 443 func IsVisibleChatMessageType(messageType chat1.MessageType) bool { 444 return checkMessageTypeQual(messageType, chat1.VisibleChatMessageTypes()) 445 } 446 447 func IsSnippetChatMessageType(messageType chat1.MessageType) bool { 448 return checkMessageTypeQual(messageType, chat1.SnippetChatMessageTypes()) 449 } 450 451 func IsBadgeableMessageType(messageType chat1.MessageType) bool { 452 return checkMessageTypeQual(messageType, chat1.BadgeableMessageTypes()) 453 } 454 455 func IsNonEmptyConvMessageType(messageType chat1.MessageType) bool { 456 return checkMessageTypeQual(messageType, chat1.NonEmptyConvMessageTypes()) 457 } 458 459 func IsEditableByEditMessageType(messageType chat1.MessageType) bool { 460 return checkMessageTypeQual(messageType, chat1.EditableMessageTypesByEdit()) 461 } 462 463 func 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 478 func 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 486 func 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 503 type DebugLabeler struct { 504 libkb.Contextified 505 label string 506 verbose bool 507 } 508 509 func 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 517 func (d DebugLabeler) GetLog() logger.Logger { 518 return d.G().GetLog() 519 } 520 521 func (d DebugLabeler) GetPerfLog() logger.Logger { 522 return d.G().GetPerfLog() 523 } 524 525 func (d DebugLabeler) showVerbose() bool { 526 return false 527 } 528 529 func (d DebugLabeler) showLog() bool { 530 if d.verbose { 531 return d.showVerbose() 532 } 533 return true 534 } 535 536 func (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 542 func (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 546 func (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 550 func (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. 566 func 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) 604 func 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 615 func 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 631 func 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 661 const ServiceDecorationPrefix = `(?:^|[\s([/{:;.,!?"'])` 662 663 var chanNameMentionRegExp = regexp.MustCompile(ServiceDecorationPrefix + `(#(?:[0-9a-zA-Z_-]+))`) 664 665 func 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 687 var atMentionRegExp = regexp.MustCompile(ServiceDecorationPrefix + 688 `(@(?:[a-zA-Z0-9][a-zA-Z0-9._]*[a-zA-Z0-9_]+(?:#[a-z0-9A-Z_-]+)?))`) 689 690 type nameMatch struct { 691 name, normalizedName string 692 position []int 693 } 694 695 func (m nameMatch) Len() int { 696 return m.position[1] - m.position[0] 697 } 698 699 func 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 718 func 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 733 func 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 749 func 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 785 func 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 827 func 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 873 func 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 881 func 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 889 func 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 897 func 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 911 func 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 919 func 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 927 func 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 935 func SanitizeTopicName(topicName string) string { 936 return strings.TrimPrefix(topicName, "#") 937 } 938 939 func 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 956 func 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 967 func 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. 991 func 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 999 type MessageSummaryContainer interface { 1000 GetMaxMessage(typ chat1.MessageType) (chat1.MessageSummary, error) 1001 } 1002 1003 func 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 1022 func 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 1030 func 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 1041 func 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 1054 func 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 1065 func 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 1074 func 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 1086 func 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 1097 func 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 1109 func 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 1143 func 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 1211 func 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 1256 func 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 1307 func StripUsernameFromConvName(name string, username string) (res string) { 1308 res = strings.ReplaceAll(name, fmt.Sprintf(",%s", username), "") 1309 res = strings.ReplaceAll(res, fmt.Sprintf("%s,", username), "") 1310 return res 1311 } 1312 1313 func 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 1328 func 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 1337 func 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 1389 func 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 1397 func SearchableRemoteConversationName(conv types.RemoteConversation, username string) string { 1398 name := GetRemoteConvDisplayName(conv) 1399 return searchableRemoteConversationNameFromStr(name, username) 1400 } 1401 1402 func searchableRemoteConversationNameFromStr(name string, username string) string { 1403 // Check for self conv or big team conv 1404 if name == username || strings.Contains(name, "#") { 1405 return name 1406 } 1407 1408 name = strings.TrimPrefix(name, fmt.Sprintf("%s,", username)) 1409 name = strings.TrimSuffix(name, fmt.Sprintf(",%s", username)) 1410 name = strings.ReplaceAll(name, fmt.Sprintf(",%s,", username), ",") 1411 return name 1412 } 1413 1414 func PresentRemoteConversationAsSearchHit(conv types.RemoteConversation, username string) chat1.UIChatSearchConvHit { 1415 return chat1.UIChatSearchConvHit{ 1416 ConvID: conv.ConvIDStr, 1417 TeamType: conv.GetTeamType(), 1418 Name: SearchableRemoteConversationName(conv, username), 1419 Mtime: conv.GetMtime(), 1420 } 1421 } 1422 1423 func PresentRemoteConversationsAsSearchHits(convs []types.RemoteConversation, username string) (res []chat1.UIChatSearchConvHit) { 1424 res = make([]chat1.UIChatSearchConvHit, 0, len(convs)) 1425 for _, c := range convs { 1426 res = append(res, PresentRemoteConversationAsSearchHit(c, username)) 1427 } 1428 return res 1429 } 1430 1431 func PresentConversationErrorLocal(ctx context.Context, g *globals.Context, uid gregor1.UID, rawConv chat1.ConversationErrorLocal) (res chat1.InboxUIItemError) { 1432 res.Message = rawConv.Message 1433 res.RekeyInfo = rawConv.RekeyInfo 1434 res.RemoteConv = PresentRemoteConversation(ctx, g, uid, types.RemoteConversation{ 1435 Conv: rawConv.RemoteConv, 1436 ConvIDStr: rawConv.RemoteConv.GetConvID().ConvIDStr(), 1437 }) 1438 res.Typ = rawConv.Typ 1439 res.UnverifiedTLFName = rawConv.UnverifiedTLFName 1440 return res 1441 } 1442 1443 func getParticipantType(username string) chat1.UIParticipantType { 1444 if strings.HasSuffix(username, "@phone") { 1445 return chat1.UIParticipantType_PHONENO 1446 } 1447 if strings.HasSuffix(username, "@email") { 1448 return chat1.UIParticipantType_EMAIL 1449 } 1450 return chat1.UIParticipantType_USER 1451 } 1452 1453 func PresentConversationParticipantsLocal(ctx context.Context, rawParticipants []chat1.ConversationLocalParticipant) (participants []chat1.UIParticipant) { 1454 participants = make([]chat1.UIParticipant, 0, len(rawParticipants)) 1455 for _, p := range rawParticipants { 1456 participantType := getParticipantType(p.Username) 1457 participants = append(participants, chat1.UIParticipant{ 1458 Assertion: p.Username, 1459 InConvName: p.InConvName, 1460 ContactName: p.ContactName, 1461 FullName: p.Fullname, 1462 Type: participantType, 1463 }) 1464 } 1465 return participants 1466 } 1467 1468 type PresentParticipantsMode int 1469 1470 const ( 1471 PresentParticipantsModeInclude PresentParticipantsMode = iota 1472 PresentParticipantsModeSkip 1473 ) 1474 1475 func PresentConversationLocal(ctx context.Context, g *globals.Context, uid gregor1.UID, 1476 rawConv chat1.ConversationLocal, partMode PresentParticipantsMode) (res chat1.InboxUIItem) { 1477 res.ConvID = rawConv.GetConvID().ConvIDStr() 1478 res.TlfID = rawConv.Info.Triple.Tlfid.TLFIDStr() 1479 res.TopicType = rawConv.GetTopicType() 1480 res.IsPublic = rawConv.Info.Visibility == keybase1.TLFVisibility_PUBLIC 1481 res.IsDefaultConv = rawConv.Info.IsDefaultConv 1482 res.Name = rawConv.Info.TlfName 1483 res.SnippetDecoration, res.Snippet, res.SnippetDecorated = 1484 GetConvSnippet(ctx, g, uid, rawConv, g.GetEnv().GetUsername().String()) 1485 res.Channel = rawConv.Info.TopicName 1486 res.Headline = rawConv.Info.Headline 1487 res.HeadlineDecorated = DecorateWithLinks(ctx, PresentDecoratedSnippet(ctx, g, rawConv.Info.Headline, uid, 1488 chat1.MessageType_HEADLINE, rawConv.Info.HeadlineEmojis)) 1489 res.ResetParticipants = rawConv.Info.ResetNames 1490 res.Status = rawConv.Info.Status 1491 res.MembersType = rawConv.GetMembersType() 1492 res.MemberStatus = rawConv.Info.MemberStatus 1493 res.Visibility = rawConv.Info.Visibility 1494 res.Time = GetConvMtimeLocal(rawConv) 1495 res.FinalizeInfo = rawConv.GetFinalizeInfo() 1496 res.SupersededBy = rawConv.SupersededBy 1497 res.Supersedes = rawConv.Supersedes 1498 res.IsEmpty = rawConv.IsEmpty 1499 res.Notifications = rawConv.Notifications 1500 res.CreatorInfo = rawConv.CreatorInfo 1501 res.TeamType = rawConv.Info.TeamType 1502 res.Version = rawConv.Info.Version 1503 res.LocalVersion = rawConv.Info.LocalVersion 1504 res.MaxMsgID = rawConv.ReaderInfo.MaxMsgid 1505 res.MaxVisibleMsgID = rawConv.MaxVisibleMsgID() 1506 res.ReadMsgID = rawConv.ReaderInfo.ReadMsgid 1507 res.ConvRetention = rawConv.ConvRetention 1508 res.TeamRetention = rawConv.TeamRetention 1509 res.ConvSettings = rawConv.ConvSettings 1510 res.Commands = rawConv.Commands 1511 res.BotCommands = rawConv.BotCommands 1512 res.BotAliases = rawConv.BotAliases 1513 res.Draft = rawConv.Info.Draft 1514 if rawConv.Info.PinnedMsg != nil { 1515 res.PinnedMsg = new(chat1.UIPinnedMessage) 1516 res.PinnedMsg.Message = PresentMessageUnboxed(ctx, g, rawConv.Info.PinnedMsg.Message, uid, 1517 rawConv.GetConvID()) 1518 res.PinnedMsg.PinnerUsername = rawConv.Info.PinnedMsg.PinnerUsername 1519 } 1520 switch partMode { 1521 case PresentParticipantsModeInclude: 1522 res.Participants = PresentConversationParticipantsLocal(ctx, rawConv.Info.Participants) 1523 default: 1524 } 1525 return res 1526 } 1527 1528 func PresentConversationLocals(ctx context.Context, g *globals.Context, uid gregor1.UID, 1529 convs []chat1.ConversationLocal, partMode PresentParticipantsMode) (res []chat1.InboxUIItem) { 1530 res = make([]chat1.InboxUIItem, 0, len(convs)) 1531 for _, conv := range convs { 1532 res = append(res, PresentConversationLocal(ctx, g, uid, conv, partMode)) 1533 } 1534 return res 1535 } 1536 1537 func PresentThreadView(ctx context.Context, g *globals.Context, uid gregor1.UID, tv chat1.ThreadView, 1538 convID chat1.ConversationID) (res chat1.UIMessages) { 1539 res.Pagination = PresentPagination(tv.Pagination) 1540 res.Messages = make([]chat1.UIMessage, 0, len(tv.Messages)) 1541 for _, msg := range tv.Messages { 1542 res.Messages = append(res.Messages, PresentMessageUnboxed(ctx, g, msg, uid, convID)) 1543 } 1544 return res 1545 } 1546 1547 func computeOutboxOrdinal(obr chat1.OutboxRecord) float64 { 1548 return computeOrdinal(obr.Msg.ClientHeader.OutboxInfo.Prev, obr.Ordinal) 1549 } 1550 1551 // Compute an "ordinal". There are two senses of "ordinal". 1552 // The service considers ordinals ints, like 3, which are the offset after some message ID. 1553 // The frontend considers ordinals floats like "180.03" where before the dot is 1554 // a message ID, and after the dot is a sub-position in thousandths. 1555 // This function translates from the service's sense to the frontend's sense. 1556 func computeOrdinal(messageID chat1.MessageID, serviceOrdinal int) (frontendOrdinal float64) { 1557 return float64(messageID) + float64(serviceOrdinal)/1000.0 1558 } 1559 1560 func PresentChannelNameMentions(ctx context.Context, crs []chat1.ChannelNameMention) (res []chat1.UIChannelNameMention) { 1561 res = make([]chat1.UIChannelNameMention, 0, len(crs)) 1562 for _, cr := range crs { 1563 res = append(res, chat1.UIChannelNameMention{ 1564 Name: cr.TopicName, 1565 ConvID: cr.ConvID.ConvIDStr(), 1566 }) 1567 } 1568 return res 1569 } 1570 1571 func formatVideoDuration(ms int) string { 1572 s := ms / 1000 1573 // see if we have hours 1574 if s >= 3600 { 1575 hours := s / 3600 1576 minutes := (s % 3600) / 60 1577 seconds := s - (hours*3600 + minutes*60) 1578 return fmt.Sprintf("%d:%02d:%02d", hours, minutes, seconds) 1579 } 1580 minutes := s / 60 1581 seconds := s % 60 1582 return fmt.Sprintf("%d:%02d", minutes, seconds) 1583 } 1584 1585 func PresentBytes(bytes int64) string { 1586 const ( 1587 BYTE = 1.0 << (10 * iota) 1588 KILOBYTE 1589 MEGABYTE 1590 GIGABYTE 1591 TERABYTE 1592 ) 1593 unit := "" 1594 value := float64(bytes) 1595 switch { 1596 case bytes >= TERABYTE: 1597 unit = "TB" 1598 value /= TERABYTE 1599 case bytes >= GIGABYTE: 1600 unit = "GB" 1601 value /= GIGABYTE 1602 case bytes >= MEGABYTE: 1603 unit = "MB" 1604 value /= MEGABYTE 1605 case bytes >= KILOBYTE: 1606 unit = "KB" 1607 value /= KILOBYTE 1608 case bytes >= BYTE: 1609 unit = "B" 1610 case bytes == 0: 1611 return "0" 1612 } 1613 return fmt.Sprintf("%.02f%s", value, unit) 1614 } 1615 1616 func formatVideoSize(bytes int64) string { 1617 return PresentBytes(bytes) 1618 } 1619 1620 func presentAttachmentAssetInfo(ctx context.Context, g *globals.Context, msg chat1.MessageUnboxed, 1621 convID chat1.ConversationID) *chat1.UIAssetUrlInfo { 1622 body := msg.Valid().MessageBody 1623 typ, err := body.MessageType() 1624 if err != nil { 1625 return nil 1626 } 1627 switch typ { 1628 case chat1.MessageType_ATTACHMENT, chat1.MessageType_ATTACHMENTUPLOADED: 1629 var hasFullURL, hasPreviewURL bool 1630 var asset chat1.Asset 1631 var info chat1.UIAssetUrlInfo 1632 if typ == chat1.MessageType_ATTACHMENT { 1633 asset = body.Attachment().Object 1634 info.MimeType = asset.MimeType 1635 hasFullURL = asset.Path != "" 1636 hasPreviewURL = body.Attachment().Preview != nil && 1637 body.Attachment().Preview.Path != "" 1638 } else { 1639 asset = body.Attachmentuploaded().Object 1640 info.MimeType = asset.MimeType 1641 hasFullURL = asset.Path != "" 1642 hasPreviewURL = len(body.Attachmentuploaded().Previews) > 0 && 1643 body.Attachmentuploaded().Previews[0].Path != "" 1644 } 1645 if hasFullURL { 1646 var cached bool 1647 info.FullUrl = g.AttachmentURLSrv.GetURL(ctx, convID, msg.GetMessageID(), false, false, false) 1648 cached, err = g.AttachmentURLSrv.GetAttachmentFetcher().IsAssetLocal(ctx, asset) 1649 if err != nil { 1650 cached = false 1651 } 1652 info.FullUrlCached = cached 1653 } 1654 if hasPreviewURL { 1655 info.PreviewUrl = g.AttachmentURLSrv.GetURL(ctx, convID, msg.GetMessageID(), true, false, false) 1656 } 1657 atyp, err := asset.Metadata.AssetType() 1658 if err == nil && atyp == chat1.AssetMetadataType_VIDEO && strings.HasPrefix(info.MimeType, "video") { 1659 if asset.Metadata.Video().DurationMs > 1 { 1660 info.VideoDuration = new(string) 1661 *info.VideoDuration = formatVideoDuration(asset.Metadata.Video().DurationMs) + ", " + 1662 formatVideoSize(asset.Size) 1663 } 1664 info.InlineVideoPlayable = true 1665 } 1666 if info.FullUrl == "" && info.PreviewUrl == "" && info.MimeType == "" { 1667 return nil 1668 } 1669 return &info 1670 } 1671 return nil 1672 } 1673 1674 func presentPaymentInfo(ctx context.Context, g *globals.Context, msgID chat1.MessageID, 1675 convID chat1.ConversationID, msg chat1.MessageUnboxedValid) []chat1.UIPaymentInfo { 1676 typ, err := msg.MessageBody.MessageType() 1677 if err != nil { 1678 return nil 1679 } 1680 var infos []chat1.UIPaymentInfo 1681 switch typ { 1682 case chat1.MessageType_SENDPAYMENT: 1683 body := msg.MessageBody.Sendpayment() 1684 info := g.StellarLoader.LoadPayment(ctx, convID, msgID, msg.SenderUsername, body.PaymentID) 1685 if info != nil { 1686 infos = []chat1.UIPaymentInfo{*info} 1687 } 1688 case chat1.MessageType_TEXT: 1689 body := msg.MessageBody.Text() 1690 // load any payments that were in the body of the text message 1691 for _, payment := range body.Payments { 1692 rtyp, err := payment.Result.ResultTyp() 1693 if err != nil { 1694 continue 1695 } 1696 switch rtyp { 1697 case chat1.TextPaymentResultTyp_SENT: 1698 paymentID := payment.Result.Sent() 1699 info := g.StellarLoader.LoadPayment(ctx, convID, msgID, msg.SenderUsername, paymentID) 1700 if info != nil { 1701 infos = append(infos, *info) 1702 } 1703 default: 1704 // Nothing to do for other payment result types. 1705 } 1706 } 1707 } 1708 for index := range infos { 1709 infos[index].Note = EscapeForDecorate(ctx, infos[index].Note) 1710 } 1711 return infos 1712 } 1713 1714 func presentRequestInfo(ctx context.Context, g *globals.Context, msgID chat1.MessageID, 1715 convID chat1.ConversationID, msg chat1.MessageUnboxedValid) *chat1.UIRequestInfo { 1716 1717 typ, err := msg.MessageBody.MessageType() 1718 if err != nil { 1719 return nil 1720 } 1721 switch typ { 1722 case chat1.MessageType_REQUESTPAYMENT: 1723 body := msg.MessageBody.Requestpayment() 1724 return g.StellarLoader.LoadRequest(ctx, convID, msgID, msg.SenderUsername, body.RequestID) 1725 default: 1726 // Nothing to do for other message types. 1727 } 1728 return nil 1729 } 1730 1731 func PresentUnfurl(ctx context.Context, g *globals.Context, convID chat1.ConversationID, u chat1.Unfurl) *chat1.UnfurlDisplay { 1732 ud, err := display.DisplayUnfurl(ctx, g.AttachmentURLSrv, convID, u) 1733 if err != nil { 1734 g.GetLog().CDebugf(ctx, "PresentUnfurl: failed to display unfurl: %s", err) 1735 return nil 1736 } 1737 return &ud 1738 } 1739 1740 func PresentUnfurls(ctx context.Context, g *globals.Context, uid gregor1.UID, 1741 convID chat1.ConversationID, unfurls map[chat1.MessageID]chat1.UnfurlResult) (res []chat1.UIMessageUnfurlInfo) { 1742 collapses := NewCollapses(g) 1743 res = make([]chat1.UIMessageUnfurlInfo, 0, len(unfurls)) 1744 for unfurlMessageID, u := range unfurls { 1745 ud := PresentUnfurl(ctx, g, convID, u.Unfurl) 1746 if ud != nil { 1747 res = append(res, chat1.UIMessageUnfurlInfo{ 1748 IsCollapsed: collapses.IsCollapsed(ctx, uid, convID, unfurlMessageID, 1749 chat1.MessageType_UNFURL), 1750 Unfurl: *ud, 1751 UnfurlMessageID: unfurlMessageID, 1752 Url: u.Url, 1753 }) 1754 } 1755 } 1756 return res 1757 } 1758 1759 func PresentDecoratedReactionMap(ctx context.Context, g *globals.Context, uid gregor1.UID, 1760 convID chat1.ConversationID, msg chat1.MessageUnboxedValid, reactions chat1.ReactionMap) (res chat1.UIReactionMap) { 1761 shouldDecorate := len(msg.Emojis) > 0 1762 res.Reactions = make(map[string]chat1.UIReactionDesc, len(reactions.Reactions)) 1763 for key, value := range reactions.Reactions { 1764 var desc chat1.UIReactionDesc 1765 if shouldDecorate { 1766 desc.Decorated = g.EmojiSource.Decorate(ctx, key, uid, 1767 chat1.MessageType_REACTION, msg.Emojis) 1768 } 1769 desc.Users = make(map[string]chat1.Reaction) 1770 for username, reaction := range value { 1771 desc.Users[username] = reaction 1772 } 1773 res.Reactions[key] = desc 1774 } 1775 return res 1776 } 1777 1778 func PresentDecoratedUserBio(ctx context.Context, bio string) (res string) { 1779 res = EscapeForDecorate(ctx, bio) 1780 res = EscapeShrugs(ctx, res) 1781 res = DecorateWithLinks(ctx, res) 1782 return res 1783 } 1784 1785 func systemMsgPresentText(ctx context.Context, uid gregor1.UID, msg chat1.MessageUnboxedValid) string { 1786 if !msg.MessageBody.IsType(chat1.MessageType_SYSTEM) { 1787 return "" 1788 } 1789 sysMsg := msg.MessageBody.System() 1790 typ, err := sysMsg.SystemType() 1791 if err != nil { 1792 return "" 1793 } 1794 switch typ { 1795 case chat1.MessageSystemType_NEWCHANNEL: 1796 var author string 1797 if uid.Eq(msg.ClientHeader.Sender) { 1798 author = "You " 1799 } 1800 if len(msg.ChannelNameMentions) == 1 { 1801 return fmt.Sprintf("%screated a new channel #%s", author, msg.ChannelNameMentions[0].TopicName) 1802 } else if len(msg.ChannelNameMentions) > 1 { 1803 return fmt.Sprintf("%screated #%s and %d other new channels", author, msg.ChannelNameMentions[0].TopicName, len(sysMsg.Newchannel().ConvIDs)-1) 1804 } 1805 default: 1806 } 1807 return "" 1808 } 1809 1810 func PresentDecoratedTextNoMentions(ctx context.Context, body string) string { 1811 // escape before applying xforms 1812 body = EscapeForDecorate(ctx, body) 1813 body = EscapeShrugs(ctx, body) 1814 1815 // This needs to happen before (deep) links. 1816 kbfsPaths := ParseKBFSPaths(ctx, body) 1817 body = DecorateWithKBFSPath(ctx, body, kbfsPaths) 1818 1819 // Links 1820 body = DecorateWithLinks(ctx, body) 1821 return body 1822 } 1823 1824 func PresentDecoratedSnippet(ctx context.Context, g *globals.Context, body string, 1825 uid gregor1.UID, msgType chat1.MessageType, emojis []chat1.HarvestedEmoji) string { 1826 body = EscapeForDecorate(ctx, body) 1827 body = EscapeShrugs(ctx, body) 1828 return g.EmojiSource.Decorate(ctx, body, uid, msgType, emojis) 1829 } 1830 1831 func PresentDecoratedPendingTextBody(ctx context.Context, g *globals.Context, uid gregor1.UID, 1832 msg chat1.MessagePlaintext) *string { 1833 typ, err := msg.MessageBody.MessageType() 1834 if err != nil { 1835 return nil 1836 } 1837 body := msg.MessageBody.TextForDecoration() 1838 body = PresentDecoratedTextNoMentions(ctx, body) 1839 body = g.EmojiSource.Decorate(ctx, body, uid, typ, msg.Emojis) 1840 return &body 1841 } 1842 1843 func PresentDecoratedTextBody(ctx context.Context, g *globals.Context, uid gregor1.UID, 1844 convID chat1.ConversationID, msg chat1.MessageUnboxedValid) *string { 1845 msgBody := msg.MessageBody 1846 typ, err := msgBody.MessageType() 1847 if err != nil { 1848 return nil 1849 } 1850 body := msgBody.TextForDecoration() 1851 if len(body) == 0 { 1852 return nil 1853 } 1854 var payments []chat1.TextPayment 1855 switch typ { 1856 case chat1.MessageType_TEXT: 1857 payments = msgBody.Text().Payments 1858 case chat1.MessageType_SYSTEM: 1859 body = systemMsgPresentText(ctx, uid, msg) 1860 } 1861 1862 body = PresentDecoratedTextNoMentions(ctx, body) 1863 // Payments 1864 body = g.StellarSender.DecorateWithPayments(ctx, body, payments) 1865 // Emojis 1866 body = g.EmojiSource.Decorate(ctx, body, uid, typ, msg.Emojis) 1867 // Mentions 1868 body = DecorateWithMentions(ctx, body, msg.AtMentionUsernames, msg.MaybeMentions, msg.ChannelMention, 1869 msg.ChannelNameMentions) 1870 return &body 1871 } 1872 1873 func loadTeamMentions(ctx context.Context, g *globals.Context, uid gregor1.UID, 1874 valid chat1.MessageUnboxedValid) { 1875 var knownTeamMentions []chat1.KnownTeamMention 1876 typ, err := valid.MessageBody.MessageType() 1877 if err != nil { 1878 return 1879 } 1880 switch typ { 1881 case chat1.MessageType_TEXT: 1882 knownTeamMentions = valid.MessageBody.Text().TeamMentions 1883 case chat1.MessageType_FLIP: 1884 knownTeamMentions = valid.MessageBody.Flip().TeamMentions 1885 case chat1.MessageType_EDIT: 1886 knownTeamMentions = valid.MessageBody.Edit().TeamMentions 1887 } 1888 for _, tm := range valid.MaybeMentions { 1889 if err := g.TeamMentionLoader.LoadTeamMention(ctx, uid, tm, knownTeamMentions, false); err != nil { 1890 g.GetLog().CDebugf(ctx, "loadTeamMentions: error loading team mentions: %+v", err) 1891 } 1892 } 1893 } 1894 1895 func presentFlipGameID(ctx context.Context, g *globals.Context, uid gregor1.UID, 1896 convID chat1.ConversationID, msg chat1.MessageUnboxed) *chat1.FlipGameIDStr { 1897 typ, err := msg.State() 1898 if err != nil { 1899 return nil 1900 } 1901 var body chat1.MessageBody 1902 switch typ { 1903 case chat1.MessageUnboxedState_VALID: 1904 body = msg.Valid().MessageBody 1905 case chat1.MessageUnboxedState_OUTBOX: 1906 body = msg.Outbox().Msg.MessageBody 1907 default: 1908 return nil 1909 } 1910 if !body.IsType(chat1.MessageType_FLIP) { 1911 return nil 1912 } 1913 if msg.GetTopicType() == chat1.TopicType_CHAT && !msg.IsOutbox() { 1914 // only queue up a flip load for the flip messages in chat channels 1915 g.CoinFlipManager.LoadFlip(ctx, uid, convID, msg.GetMessageID(), body.Flip().FlipConvID, 1916 body.Flip().GameID) 1917 } 1918 ret := body.Flip().GameID.FlipGameIDStr() 1919 return &ret 1920 } 1921 1922 func PresentMessagesUnboxed(ctx context.Context, g *globals.Context, msgs []chat1.MessageUnboxed, 1923 uid gregor1.UID, convID chat1.ConversationID) (res []chat1.UIMessage) { 1924 res = make([]chat1.UIMessage, 0, len(msgs)) 1925 for _, msg := range msgs { 1926 res = append(res, PresentMessageUnboxed(ctx, g, msg, uid, convID)) 1927 } 1928 return res 1929 } 1930 1931 func PresentMessageUnboxed(ctx context.Context, g *globals.Context, rawMsg chat1.MessageUnboxed, 1932 uid gregor1.UID, convID chat1.ConversationID) (res chat1.UIMessage) { 1933 miscErr := func(err error) chat1.UIMessage { 1934 return chat1.NewUIMessageWithError(chat1.MessageUnboxedError{ 1935 ErrType: chat1.MessageUnboxedErrorType_MISC, 1936 ErrMsg: err.Error(), 1937 MessageID: rawMsg.GetMessageID(), 1938 }) 1939 } 1940 1941 collapses := NewCollapses(g) 1942 state, err := rawMsg.State() 1943 if err != nil { 1944 return miscErr(err) 1945 } 1946 switch state { 1947 case chat1.MessageUnboxedState_VALID: 1948 valid := rawMsg.Valid() 1949 if !rawMsg.IsValidFull() { 1950 // If we have an expired ephemeral message, don't show an error 1951 // message. 1952 if !(valid.IsEphemeral() && valid.IsEphemeralExpired(time.Now())) { 1953 return miscErr(fmt.Errorf("unexpected deleted %v message", 1954 strings.ToLower(rawMsg.GetMessageType().String()))) 1955 } 1956 } 1957 var strOutboxID *string 1958 if valid.ClientHeader.OutboxID != nil { 1959 so := valid.ClientHeader.OutboxID.String() 1960 strOutboxID = &so 1961 } 1962 var replyTo *chat1.UIMessage 1963 if valid.ReplyTo != nil { 1964 replyTo = new(chat1.UIMessage) 1965 *replyTo = PresentMessageUnboxed(ctx, g, *valid.ReplyTo, uid, convID) 1966 } 1967 var pinnedMessageID *chat1.MessageID 1968 if valid.MessageBody.IsType(chat1.MessageType_PIN) { 1969 pinnedMessageID = new(chat1.MessageID) 1970 *pinnedMessageID = valid.MessageBody.Pin().MsgID 1971 } 1972 loadTeamMentions(ctx, g, uid, valid) 1973 bodySummary, _ := GetMsgSnippetBody(ctx, g, uid, convID, rawMsg) 1974 res = chat1.NewUIMessageWithValid(chat1.UIMessageValid{ 1975 MessageID: rawMsg.GetMessageID(), 1976 Ctime: valid.ServerHeader.Ctime, 1977 OutboxID: strOutboxID, 1978 MessageBody: valid.MessageBody, 1979 DecoratedTextBody: PresentDecoratedTextBody(ctx, g, uid, convID, valid), 1980 BodySummary: bodySummary, 1981 SenderUsername: valid.SenderUsername, 1982 SenderDeviceName: valid.SenderDeviceName, 1983 SenderDeviceType: valid.SenderDeviceType, 1984 SenderDeviceRevokedAt: valid.SenderDeviceRevokedAt, 1985 SenderUID: valid.ClientHeader.Sender, 1986 SenderDeviceID: valid.ClientHeader.SenderDevice, 1987 Superseded: valid.ServerHeader.SupersededBy != 0, 1988 AtMentions: valid.AtMentionUsernames, 1989 ChannelMention: valid.ChannelMention, 1990 ChannelNameMentions: PresentChannelNameMentions(ctx, valid.ChannelNameMentions), 1991 AssetUrlInfo: presentAttachmentAssetInfo(ctx, g, rawMsg, convID), 1992 IsEphemeral: valid.IsEphemeral(), 1993 IsEphemeralExpired: valid.IsEphemeralExpired(time.Now()), 1994 ExplodedBy: valid.ExplodedBy(), 1995 Etime: valid.Etime(), 1996 Reactions: PresentDecoratedReactionMap(ctx, g, uid, convID, valid, valid.Reactions), 1997 HasPairwiseMacs: valid.HasPairwiseMacs(), 1998 FlipGameID: presentFlipGameID(ctx, g, uid, convID, rawMsg), 1999 PaymentInfos: presentPaymentInfo(ctx, g, rawMsg.GetMessageID(), convID, valid), 2000 RequestInfo: presentRequestInfo(ctx, g, rawMsg.GetMessageID(), convID, valid), 2001 Unfurls: PresentUnfurls(ctx, g, uid, convID, valid.Unfurls), 2002 IsDeleteable: IsDeleteableByDeleteMessageType(valid), 2003 IsEditable: IsEditableByEditMessageType(rawMsg.GetMessageType()), 2004 ReplyTo: replyTo, 2005 PinnedMessageID: pinnedMessageID, 2006 BotUsername: valid.BotUsername, 2007 IsCollapsed: collapses.IsCollapsed(ctx, uid, convID, rawMsg.GetMessageID(), 2008 rawMsg.GetMessageType()), 2009 }) 2010 case chat1.MessageUnboxedState_OUTBOX: 2011 var body, title, filename string 2012 var decoratedBody *string 2013 var preview *chat1.MakePreviewRes 2014 typ := rawMsg.Outbox().Msg.ClientHeader.MessageType 2015 switch typ { 2016 case chat1.MessageType_TEXT: 2017 body = rawMsg.Outbox().Msg.MessageBody.Text().Body 2018 decoratedBody = PresentDecoratedPendingTextBody(ctx, g, uid, rawMsg.Outbox().Msg) 2019 case chat1.MessageType_FLIP: 2020 body = rawMsg.Outbox().Msg.MessageBody.Flip().Text 2021 decoratedBody = new(string) 2022 *decoratedBody = EscapeShrugs(ctx, body) 2023 case chat1.MessageType_EDIT: 2024 body = rawMsg.Outbox().Msg.MessageBody.Edit().Body 2025 case chat1.MessageType_ATTACHMENT: 2026 preview = rawMsg.Outbox().Preview 2027 msgBody := rawMsg.Outbox().Msg.MessageBody 2028 btyp, err := msgBody.MessageType() 2029 if err == nil && btyp == chat1.MessageType_ATTACHMENT { 2030 asset := msgBody.Attachment().Object 2031 title = asset.Title 2032 filename = asset.Filename 2033 } 2034 case chat1.MessageType_REACTION: 2035 body = rawMsg.Outbox().Msg.MessageBody.Reaction().Body 2036 decoratedBody = PresentDecoratedPendingTextBody(ctx, g, uid, rawMsg.Outbox().Msg) 2037 } 2038 var replyTo *chat1.UIMessage 2039 if rawMsg.Outbox().ReplyTo != nil { 2040 replyTo = new(chat1.UIMessage) 2041 *replyTo = PresentMessageUnboxed(ctx, g, *rawMsg.Outbox().ReplyTo, uid, convID) 2042 } 2043 res = chat1.NewUIMessageWithOutbox(chat1.UIMessageOutbox{ 2044 State: rawMsg.Outbox().State, 2045 OutboxID: rawMsg.Outbox().OutboxID.String(), 2046 MessageType: typ, 2047 Body: body, 2048 DecoratedTextBody: decoratedBody, 2049 Ctime: rawMsg.Outbox().Ctime, 2050 Ordinal: computeOutboxOrdinal(rawMsg.Outbox()), 2051 Preview: preview, 2052 Title: title, 2053 Filename: filename, 2054 IsEphemeral: rawMsg.Outbox().Msg.IsEphemeral(), 2055 FlipGameID: presentFlipGameID(ctx, g, uid, convID, rawMsg), 2056 ReplyTo: replyTo, 2057 Supersedes: rawMsg.Outbox().Msg.ClientHeader.Supersedes, 2058 }) 2059 case chat1.MessageUnboxedState_ERROR: 2060 res = chat1.NewUIMessageWithError(rawMsg.Error()) 2061 case chat1.MessageUnboxedState_PLACEHOLDER: 2062 res = chat1.NewUIMessageWithPlaceholder(rawMsg.Placeholder()) 2063 case chat1.MessageUnboxedState_JOURNEYCARD: 2064 journeycard := rawMsg.Journeycard() 2065 res = chat1.NewUIMessageWithJourneycard(chat1.UIMessageJourneycard{ 2066 Ordinal: computeOrdinal(journeycard.PrevID, journeycard.Ordinal), 2067 CardType: journeycard.CardType, 2068 HighlightMsgID: journeycard.HighlightMsgID, 2069 OpenTeam: journeycard.OpenTeam, 2070 }) 2071 default: 2072 g.MetaContext(ctx).Debug("PresentMessageUnboxed: unhandled MessageUnboxedState: %v", state) 2073 // res = zero values 2074 } 2075 return res 2076 } 2077 2078 func PresentPagination(p *chat1.Pagination) (res *chat1.UIPagination) { 2079 if p == nil { 2080 return nil 2081 } 2082 res = new(chat1.UIPagination) 2083 res.Last = p.Last 2084 res.Num = p.Num 2085 res.Next = hex.EncodeToString(p.Next) 2086 res.Previous = hex.EncodeToString(p.Previous) 2087 return res 2088 } 2089 2090 func DecodePagination(p *chat1.UIPagination) (res *chat1.Pagination, err error) { 2091 if p == nil { 2092 return nil, nil 2093 } 2094 res = new(chat1.Pagination) 2095 res.Last = p.Last 2096 res.Num = p.Num 2097 if res.Next, err = hex.DecodeString(p.Next); err != nil { 2098 return nil, err 2099 } 2100 if res.Previous, err = hex.DecodeString(p.Previous); err != nil { 2101 return nil, err 2102 } 2103 return res, nil 2104 } 2105 2106 type ConvLocalByConvID []chat1.ConversationLocal 2107 2108 func (c ConvLocalByConvID) Len() int { return len(c) } 2109 func (c ConvLocalByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2110 func (c ConvLocalByConvID) Less(i, j int) bool { 2111 return c[i].GetConvID().Less(c[j].GetConvID()) 2112 } 2113 2114 type ConvByConvID []chat1.Conversation 2115 2116 func (c ConvByConvID) Len() int { return len(c) } 2117 func (c ConvByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2118 func (c ConvByConvID) Less(i, j int) bool { 2119 return c[i].GetConvID().Less(c[j].GetConvID()) 2120 } 2121 2122 type RemoteConvByConvID []types.RemoteConversation 2123 2124 func (c RemoteConvByConvID) Len() int { return len(c) } 2125 func (c RemoteConvByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2126 func (c RemoteConvByConvID) Less(i, j int) bool { 2127 return c[i].GetConvID().Less(c[j].GetConvID()) 2128 } 2129 2130 type RemoteConvByMtime []types.RemoteConversation 2131 2132 func (c RemoteConvByMtime) Len() int { return len(c) } 2133 func (c RemoteConvByMtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2134 func (c RemoteConvByMtime) Less(i, j int) bool { 2135 return GetConvMtime(c[i]) > GetConvMtime(c[j]) 2136 } 2137 2138 type ConvLocalByTopicName []chat1.ConversationLocal 2139 2140 func (c ConvLocalByTopicName) Len() int { return len(c) } 2141 func (c ConvLocalByTopicName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2142 func (c ConvLocalByTopicName) Less(i, j int) bool { 2143 return c[i].Info.TopicName < c[j].Info.TopicName 2144 } 2145 2146 type ByConvID []chat1.ConversationID 2147 2148 func (c ByConvID) Len() int { return len(c) } 2149 func (c ByConvID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2150 func (c ByConvID) Less(i, j int) bool { 2151 return c[i].Less(c[j]) 2152 } 2153 2154 type ByMsgSummaryCtime []chat1.MessageSummary 2155 2156 func (c ByMsgSummaryCtime) Len() int { return len(c) } 2157 func (c ByMsgSummaryCtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2158 func (c ByMsgSummaryCtime) Less(i, j int) bool { 2159 return c[i].Ctime.Before(c[j].Ctime) 2160 } 2161 2162 type ByMsgUnboxedCtime []chat1.MessageUnboxed 2163 2164 func (c ByMsgUnboxedCtime) Len() int { return len(c) } 2165 func (c ByMsgUnboxedCtime) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2166 func (c ByMsgUnboxedCtime) Less(i, j int) bool { 2167 return c[i].Valid().ServerHeader.Ctime.Before(c[j].Valid().ServerHeader.Ctime) 2168 } 2169 2170 type ByMsgUnboxedMsgID []chat1.MessageUnboxed 2171 2172 func (c ByMsgUnboxedMsgID) Len() int { return len(c) } 2173 func (c ByMsgUnboxedMsgID) Swap(i, j int) { c[i], c[j] = c[j], c[i] } 2174 func (c ByMsgUnboxedMsgID) Less(i, j int) bool { 2175 return c[i].GetMessageID() > c[j].GetMessageID() 2176 } 2177 2178 type ByMsgID []chat1.MessageID 2179 2180 func (m ByMsgID) Len() int { return len(m) } 2181 func (m ByMsgID) Swap(i, j int) { m[i], m[j] = m[j], m[i] } 2182 func (m ByMsgID) Less(i, j int) bool { return m[i] > m[j] } 2183 2184 func NotificationInfoSet(settings *chat1.ConversationNotificationInfo, 2185 apptype keybase1.DeviceType, 2186 kind chat1.NotificationKind, enabled bool) { 2187 if settings.Settings == nil { 2188 settings.Settings = make(map[keybase1.DeviceType]map[chat1.NotificationKind]bool) 2189 } 2190 if settings.Settings[apptype] == nil { 2191 settings.Settings[apptype] = make(map[chat1.NotificationKind]bool) 2192 } 2193 settings.Settings[apptype][kind] = enabled 2194 } 2195 2196 func DecodeBase64(enc []byte) ([]byte, error) { 2197 if len(enc) == 0 { 2198 return enc, nil 2199 } 2200 2201 b := make([]byte, base64.StdEncoding.DecodedLen(len(enc))) 2202 n, err := base64.StdEncoding.Decode(b, enc) 2203 return b[:n], err 2204 } 2205 2206 func RemoteConv(conv chat1.Conversation) types.RemoteConversation { 2207 return types.RemoteConversation{ 2208 Conv: conv, 2209 ConvIDStr: conv.GetConvID().ConvIDStr(), 2210 } 2211 } 2212 2213 func RemoteConvs(convs []chat1.Conversation) (res []types.RemoteConversation) { 2214 res = make([]types.RemoteConversation, 0, len(convs)) 2215 for _, conv := range convs { 2216 res = append(res, RemoteConv(conv)) 2217 } 2218 return res 2219 } 2220 2221 func PluckConvs(rcs []types.RemoteConversation) (res []chat1.Conversation) { 2222 res = make([]chat1.Conversation, 0, len(rcs)) 2223 for _, rc := range rcs { 2224 res = append(res, rc.Conv) 2225 } 2226 return res 2227 } 2228 2229 func SplitTLFName(tlfName string) []string { 2230 return strings.Split(strings.Fields(tlfName)[0], ",") 2231 } 2232 2233 func UsernamePackageToParticipant(p libkb.UsernamePackage) chat1.ConversationLocalParticipant { 2234 var fullName *string 2235 if p.FullName != nil { 2236 s := string(p.FullName.FullName) 2237 fullName = &s 2238 } 2239 return chat1.ConversationLocalParticipant{ 2240 Username: p.NormalizedUsername.String(), 2241 Fullname: fullName, 2242 } 2243 } 2244 2245 type pagerMsg struct { 2246 msgID chat1.MessageID 2247 } 2248 2249 func (p pagerMsg) GetMessageID() chat1.MessageID { 2250 return p.msgID 2251 } 2252 2253 func MessageIDControlToPagination(ctx context.Context, logger DebugLabeler, control *chat1.MessageIDControl, 2254 conv *types.RemoteConversation) (res *chat1.Pagination) { 2255 if control == nil { 2256 return res 2257 } 2258 pag := pager.NewThreadPager() 2259 res = new(chat1.Pagination) 2260 res.Num = control.Num 2261 if control.Pivot != nil { 2262 var err error 2263 pm := pagerMsg{msgID: *control.Pivot} 2264 switch control.Mode { 2265 case chat1.MessageIDControlMode_OLDERMESSAGES: 2266 res.Next, err = pag.MakeIndex(pm) 2267 case chat1.MessageIDControlMode_NEWERMESSAGES: 2268 res.Previous, err = pag.MakeIndex(pm) 2269 case chat1.MessageIDControlMode_UNREADLINE: 2270 if conv == nil { 2271 // just bail out of here with no conversation 2272 logger.Debug(ctx, "MessageIDControlToPagination: unreadline mode with no conv, bailing") 2273 return nil 2274 } 2275 pm.msgID = conv.Conv.ReaderInfo.ReadMsgid 2276 fallthrough 2277 case chat1.MessageIDControlMode_CENTERED: 2278 // Heuristic that we might want to revisit, get older messages from a little ahead of where 2279 // we want to center on 2280 if conv == nil { 2281 // just bail out of here with no conversation 2282 logger.Debug(ctx, "MessageIDControlToPagination: centered mode with no conv, bailing") 2283 return nil 2284 } 2285 maxID := int(conv.Conv.MaxVisibleMsgID()) 2286 desired := int(pm.msgID) + control.Num/2 2287 logger.Debug(ctx, "MessageIDControlToPagination: maxID: %d desired: %d", maxID, desired) 2288 if desired > maxID { 2289 desired = maxID 2290 } 2291 pm.msgID = chat1.MessageID(desired + 1) 2292 res.Next, err = pag.MakeIndex(pm) 2293 res.ForceFirstPage = true 2294 } 2295 if err != nil { 2296 return nil 2297 } 2298 } 2299 return res 2300 } 2301 2302 // AssetsForMessage gathers all assets on a message 2303 func AssetsForMessage(g *globals.Context, msgBody chat1.MessageBody) (assets []chat1.Asset) { 2304 typ, err := msgBody.MessageType() 2305 if err != nil { 2306 // Log and drop the error for a malformed MessageBody. 2307 g.Log.Warning("error getting assets for message: %s", err) 2308 return assets 2309 } 2310 switch typ { 2311 case chat1.MessageType_ATTACHMENT: 2312 body := msgBody.Attachment() 2313 if body.Object.Path != "" { 2314 assets = append(assets, body.Object) 2315 } 2316 if body.Preview != nil { 2317 assets = append(assets, *body.Preview) 2318 } 2319 assets = append(assets, body.Previews...) 2320 case chat1.MessageType_ATTACHMENTUPLOADED: 2321 body := msgBody.Attachmentuploaded() 2322 if body.Object.Path != "" { 2323 assets = append(assets, body.Object) 2324 } 2325 assets = append(assets, body.Previews...) 2326 } 2327 return assets 2328 } 2329 2330 func AddUserToTLFName(g *globals.Context, tlfName string, vis keybase1.TLFVisibility, 2331 membersType chat1.ConversationMembersType) string { 2332 switch membersType { 2333 case chat1.ConversationMembersType_IMPTEAMNATIVE, chat1.ConversationMembersType_IMPTEAMUPGRADE, 2334 chat1.ConversationMembersType_KBFS: 2335 if vis == keybase1.TLFVisibility_PUBLIC { 2336 return tlfName 2337 } 2338 2339 username := g.Env.GetUsername().String() 2340 if len(tlfName) == 0 { 2341 return username 2342 } 2343 2344 // KBFS creates TLFs with suffixes (e.g., folder names that 2345 // conflict after an assertion has been resolved) and readers, 2346 // so we need to handle those types of TLF names here so that 2347 // edit history works correctly. 2348 split1 := strings.SplitN(tlfName, " ", 2) // split off suffix 2349 split2 := strings.Split(split1[0], "#") // split off readers 2350 // Add the name to the writers list (assume the current user 2351 // is a writer). 2352 tlfName = split2[0] + "," + username 2353 if len(split2) > 1 { 2354 // Re-append any readers. 2355 tlfName += "#" + split2[1] 2356 } 2357 if len(split1) > 1 { 2358 // Re-append any suffix. 2359 tlfName += " " + split1[1] 2360 } 2361 return tlfName 2362 default: 2363 return tlfName 2364 } 2365 } 2366 2367 func ForceReloadUPAKsForUIDs(ctx context.Context, g *globals.Context, uids []keybase1.UID) error { 2368 getArg := func(i int) *libkb.LoadUserArg { 2369 if i >= len(uids) { 2370 return nil 2371 } 2372 tmp := libkb.NewLoadUserByUIDForceArg(g.GlobalContext, uids[i]) 2373 return &tmp 2374 } 2375 return g.GetUPAKLoader().Batcher(ctx, getArg, nil, 0) 2376 } 2377 2378 func CreateHiddenPlaceholder(msgID chat1.MessageID) chat1.MessageUnboxed { 2379 return chat1.NewMessageUnboxedWithPlaceholder( 2380 chat1.MessageUnboxedPlaceholder{ 2381 MessageID: msgID, 2382 Hidden: true, 2383 }) 2384 } 2385 2386 func GetGregorConn(ctx context.Context, g *globals.Context, log DebugLabeler, 2387 handler func(nist *libkb.NIST) rpc.ConnectionHandler) (conn *rpc.Connection, token gregor1.SessionToken, err error) { 2388 // Get session token 2389 nist, _, _, err := g.ActiveDevice.NISTAndUIDDeviceID(ctx) 2390 if nist == nil { 2391 log.Debug(ctx, "GetGregorConn: got a nil NIST, is the user logged out?") 2392 return conn, token, libkb.LoggedInError{} 2393 } 2394 if err != nil { 2395 log.Debug(ctx, "GetGregorConn: failed to get logged in session: %s", err.Error()) 2396 return conn, token, err 2397 } 2398 token = gregor1.SessionToken(nist.Token().String()) 2399 2400 // Make an ad hoc connection to gregor 2401 uri, err := rpc.ParseFMPURI(g.Env.GetGregorURI()) 2402 if err != nil { 2403 log.Debug(ctx, "GetGregorConn: failed to parse chat server UR: %s", err.Error()) 2404 return conn, token, err 2405 } 2406 2407 if uri.UseTLS() { 2408 rawCA := g.Env.GetBundledCA(uri.Host) 2409 if len(rawCA) == 0 { 2410 err := errors.New("len(rawCA) == 0") 2411 log.Debug(ctx, "GetGregorConn: failed to parse CAs", err.Error()) 2412 return conn, token, err 2413 } 2414 conn = rpc.NewTLSConnectionWithDialable(rpc.NewFixedRemote(uri.HostPort), 2415 []byte(rawCA), libkb.NewContextifiedErrorUnwrapper(g.ExternalG()), 2416 handler(nist), libkb.NewRPCLogFactory(g.ExternalG()), 2417 g.ExternalG().RemoteNetworkInstrumenterStorage, 2418 logger.LogOutputWithDepthAdder{Logger: g.Log}, 2419 rpc.DefaultMaxFrameLength, rpc.ConnectionOpts{}, 2420 libkb.NewProxyDialable(g.Env)) 2421 } else { 2422 t := rpc.NewConnectionTransportWithDialable(uri, nil, 2423 g.ExternalG().RemoteNetworkInstrumenterStorage, 2424 libkb.MakeWrapError(g.ExternalG()), 2425 rpc.DefaultMaxFrameLength, libkb.NewProxyDialable(g.GetEnv())) 2426 conn = rpc.NewConnectionWithTransport(handler(nist), t, 2427 libkb.NewContextifiedErrorUnwrapper(g.ExternalG()), 2428 logger.LogOutputWithDepthAdder{Logger: g.Log}, rpc.ConnectionOpts{}) 2429 } 2430 return conn, token, nil 2431 } 2432 2433 // GetQueryRe returns a regex to match the query string on message text. This 2434 // is used for result highlighting. 2435 func GetQueryRe(query string) (*regexp.Regexp, error) { 2436 return regexp.Compile("(?i)" + regexp.QuoteMeta(query)) 2437 } 2438 2439 func SetUnfurl(mvalid *chat1.MessageUnboxedValid, unfurlMessageID chat1.MessageID, 2440 unfurl chat1.UnfurlResult) { 2441 if mvalid.Unfurls == nil { 2442 mvalid.Unfurls = make(map[chat1.MessageID]chat1.UnfurlResult) 2443 } 2444 mvalid.Unfurls[unfurlMessageID] = unfurl 2445 } 2446 2447 func RemoveUnfurl(mvalid *chat1.MessageUnboxedValid, unfurlMessageID chat1.MessageID) { 2448 if mvalid.Unfurls == nil { 2449 return 2450 } 2451 delete(mvalid.Unfurls, unfurlMessageID) 2452 } 2453 2454 // SuspendComponent will suspend a Suspendable type until the return function 2455 // is called. This allows a succinct call like defer SuspendComponent(ctx, g, 2456 // g.ConvLoader)() in RPC handlers wishing to lock out the conv loader. 2457 func SuspendComponent(ctx context.Context, g *globals.Context, suspendable types.Suspendable) func() { 2458 if canceled := suspendable.Suspend(ctx); canceled { 2459 g.Log.CDebugf(ctx, "SuspendComponent: canceled background task") 2460 } 2461 return func() { 2462 suspendable.Resume(ctx) 2463 } 2464 } 2465 2466 func SuspendComponents(ctx context.Context, g *globals.Context, suspendables []types.Suspendable) func() { 2467 resumeFuncs := make([]func(), 0, len(suspendables)) 2468 for _, s := range suspendables { 2469 resumeFuncs = append(resumeFuncs, SuspendComponent(ctx, g, s)) 2470 } 2471 return func() { 2472 for _, f := range resumeFuncs { 2473 f() 2474 } 2475 } 2476 } 2477 2478 func IsPermanentErr(err error) bool { 2479 if uberr, ok := err.(types.UnboxingError); ok { 2480 return uberr.IsPermanent() 2481 } 2482 return err != nil 2483 } 2484 2485 func EphemeralLifetimeFromConv(ctx context.Context, g *globals.Context, conv chat1.ConversationLocal) (res *gregor1.DurationSec, err error) { 2486 // Check to see if the conversation has an exploding policy 2487 var retentionRes *gregor1.DurationSec 2488 var gregorRes *gregor1.DurationSec 2489 var rentTyp chat1.RetentionPolicyType 2490 var convSet bool 2491 if conv.ConvRetention != nil { 2492 if rentTyp, err = conv.ConvRetention.Typ(); err != nil { 2493 return res, err 2494 } 2495 if rentTyp == chat1.RetentionPolicyType_EPHEMERAL { 2496 e := conv.ConvRetention.Ephemeral() 2497 retentionRes = &e.Age 2498 } 2499 convSet = rentTyp != chat1.RetentionPolicyType_INHERIT 2500 } 2501 if !convSet && conv.TeamRetention != nil { 2502 if rentTyp, err = conv.TeamRetention.Typ(); err != nil { 2503 return res, err 2504 } 2505 if rentTyp == chat1.RetentionPolicyType_EPHEMERAL { 2506 e := conv.TeamRetention.Ephemeral() 2507 retentionRes = &e.Age 2508 } 2509 } 2510 2511 // See if there is anything in Gregor 2512 st, err := g.GregorState.State(ctx) 2513 if err != nil { 2514 return res, err 2515 } 2516 // Note: this value is present on the JS frontend as well 2517 key := fmt.Sprintf("exploding:%s", conv.GetConvID()) 2518 cat, err := gregor1.ObjFactory{}.MakeCategory(key) 2519 if err != nil { 2520 return res, err 2521 } 2522 items, err := st.ItemsWithCategoryPrefix(cat) 2523 if err != nil { 2524 return res, err 2525 } 2526 if len(items) > 0 { 2527 it := items[0] 2528 body := string(it.Body().Bytes()) 2529 sec, err := strconv.ParseInt(body, 0, 0) 2530 if err != nil { 2531 return res, nil 2532 } 2533 gsec := gregor1.DurationSec(sec) 2534 gregorRes = &gsec 2535 } 2536 if retentionRes != nil && gregorRes != nil { 2537 if *gregorRes < *retentionRes { 2538 return gregorRes, nil 2539 } 2540 return retentionRes, nil 2541 } else if retentionRes != nil { 2542 return retentionRes, nil 2543 } else if gregorRes != nil { 2544 return gregorRes, nil 2545 } else { 2546 return nil, nil 2547 } 2548 } 2549 2550 var decorateBegin = "$>kb$" 2551 var decorateEnd = "$<kb$" 2552 var decorateEscapeRe = regexp.MustCompile(`\\*\$\>kb\$`) 2553 2554 func EscapeForDecorate(ctx context.Context, body string) string { 2555 // escape any natural occurrences of begin so we don't bust markdown parser 2556 return decorateEscapeRe.ReplaceAllStringFunc(body, func(s string) string { 2557 if len(s)%2 != 0 { 2558 return `\` + s 2559 } 2560 return s 2561 }) 2562 } 2563 2564 func DecorateBody(ctx context.Context, body string, offset, length int, decoration interface{}) (res string, added int) { 2565 out, err := json.Marshal(decoration) 2566 if err != nil { 2567 return res, 0 2568 } 2569 b64out := base64.StdEncoding.EncodeToString(out) 2570 strDecoration := fmt.Sprintf("%s%s%s", decorateBegin, b64out, decorateEnd) 2571 added = len(strDecoration) - length 2572 res = fmt.Sprintf("%s%s%s", body[:offset], strDecoration, body[offset+length:]) 2573 return res, added 2574 } 2575 2576 var linkRegexp = xurls.Relaxed() 2577 2578 // These indices correspond to the named capture groups in the xurls regexes 2579 var linkRelaxedGroupIndex = 0 2580 var linkStrictGroupIndex = 0 2581 var mailtoRegexp = regexp.MustCompile(`(?:(?:[\w-_.]+)@(?:[\w-]+(?:\.[\w-]+)+))\b`) 2582 2583 func init() { 2584 for index, name := range linkRegexp.SubexpNames() { 2585 if name == "relaxed" { 2586 linkRelaxedGroupIndex = index + 1 2587 } 2588 if name == "strict" { 2589 linkStrictGroupIndex = index + 1 2590 } 2591 } 2592 } 2593 2594 func DecorateWithLinks(ctx context.Context, body string) string { 2595 var added int 2596 offset := 0 2597 origBody := body 2598 2599 // early out of here if there is no dot 2600 if !(strings.Contains(body, ".") || strings.Contains(body, "://")) { 2601 return body 2602 } 2603 shouldSkipLink := func(linkPrefix, link string) bool { 2604 // Check for RTLO character preceeding our match. If one is 2605 // detected, and there isn't a LTRO following it, skip this URL 2606 // from linkification. 2607 if rtloIdx := strings.LastIndex(linkPrefix, "\u202e"); rtloIdx >= 0 && rtloIdx > strings.LastIndex(linkPrefix, "\u202d") { 2608 return true 2609 } 2610 if strings.Contains(strings.Split(link, "/")[0], "@") { 2611 return true 2612 } 2613 for _, scheme := range xurls.SchemesNoAuthority { 2614 if strings.HasPrefix(link, scheme) { 2615 return true 2616 } 2617 } 2618 if strings.HasPrefix(link, "ftp://") || strings.HasPrefix(link, "gopher://") { 2619 return true 2620 } 2621 return false 2622 } 2623 allMatches := linkRegexp.FindAllStringSubmatchIndex(ReplaceQuotedSubstrings(body, true), -1) 2624 for _, match := range allMatches { 2625 var lowhit, highhit int 2626 if len(match) >= linkRelaxedGroupIndex*2 && match[linkRelaxedGroupIndex*2-2] >= 0 { 2627 lowhit = linkRelaxedGroupIndex*2 - 2 2628 highhit = linkRelaxedGroupIndex*2 - 1 2629 } else if len(match) >= linkStrictGroupIndex*2 && match[linkStrictGroupIndex*2-2] >= 0 { 2630 lowhit = linkStrictGroupIndex*2 - 2 2631 highhit = linkStrictGroupIndex*2 - 1 2632 } else { 2633 continue 2634 } 2635 2636 bodyPrefix := origBody[:match[lowhit]] 2637 bodyMatch := origBody[match[lowhit]:match[highhit]] 2638 if shouldSkipLink(bodyPrefix, bodyMatch) { 2639 continue 2640 } 2641 var punycode string 2642 url := bodyMatch 2643 if encoded, err := idna.ToASCII(url); err == nil && encoded != url { 2644 punycode = encoded 2645 } 2646 body, added = DecorateBody(ctx, body, match[lowhit]+offset, match[highhit]-match[lowhit], 2647 chat1.NewUITextDecorationWithLink(chat1.UILinkDecoration{ 2648 Url: bodyMatch, 2649 Punycode: punycode, 2650 })) 2651 offset += added 2652 } 2653 2654 offset = 0 2655 origBody = body 2656 allMatches = mailtoRegexp.FindAllStringIndex(ReplaceQuotedSubstrings(body, true), -1) 2657 for _, match := range allMatches { 2658 if len(match) < 2 { 2659 continue 2660 } 2661 bodyMatch := origBody[match[0]:match[1]] 2662 body, added = DecorateBody(ctx, body, match[0]+offset, match[1]-match[0], 2663 chat1.NewUITextDecorationWithMailto(chat1.UILinkDecoration{ 2664 Url: bodyMatch, 2665 })) 2666 offset += added 2667 } 2668 2669 return body 2670 } 2671 2672 func DecorateWithMentions(ctx context.Context, body string, atMentions []string, 2673 maybeMentions []chat1.MaybeMention, chanMention chat1.ChannelMention, 2674 channelNameMentions []chat1.ChannelNameMention) string { 2675 var added int 2676 offset := 0 2677 if len(atMentions) > 0 || len(maybeMentions) > 0 || chanMention != chat1.ChannelMention_NONE { 2678 atMap := make(map[string]bool) 2679 for _, at := range atMentions { 2680 atMap[at] = true 2681 } 2682 maybeMap := make(map[string]chat1.MaybeMention) 2683 for _, tm := range maybeMentions { 2684 name := tm.Name 2685 if len(tm.Channel) > 0 { 2686 name += "#" + tm.Channel 2687 } 2688 maybeMap[name] = tm 2689 } 2690 inputBody := body 2691 atMatches := parseRegexpNames(ctx, inputBody, atMentionRegExp) 2692 for _, m := range atMatches { 2693 switch { 2694 case m.normalizedName == "here": 2695 fallthrough 2696 case m.normalizedName == "channel": 2697 fallthrough 2698 case m.normalizedName == "everyone": 2699 if chanMention == chat1.ChannelMention_NONE { 2700 continue 2701 } 2702 fallthrough 2703 case atMap[m.normalizedName]: 2704 body, added = DecorateBody(ctx, body, m.position[0]+offset-1, m.Len()+1, 2705 chat1.NewUITextDecorationWithAtmention(m.name)) 2706 offset += added 2707 } 2708 if tm, ok := maybeMap[m.name]; ok { 2709 body, added = DecorateBody(ctx, body, m.position[0]+offset-1, m.Len()+1, 2710 chat1.NewUITextDecorationWithMaybemention(tm)) 2711 offset += added 2712 } 2713 } 2714 } 2715 if len(channelNameMentions) > 0 { 2716 chanMap := make(map[string]chat1.ConversationID) 2717 for _, c := range channelNameMentions { 2718 chanMap[c.TopicName] = c.ConvID 2719 } 2720 offset = 0 2721 inputBody := body 2722 chanMatches := parseRegexpNames(ctx, inputBody, chanNameMentionRegExp) 2723 for _, c := range chanMatches { 2724 convID, ok := chanMap[c.name] 2725 if !ok { 2726 continue 2727 } 2728 body, added = DecorateBody(ctx, body, c.position[0]+offset-1, c.Len()+1, 2729 chat1.NewUITextDecorationWithChannelnamemention(chat1.UIChannelNameMention{ 2730 Name: c.name, 2731 ConvID: convID.ConvIDStr(), 2732 })) 2733 offset += added 2734 } 2735 } 2736 return body 2737 } 2738 2739 func EscapeShrugs(ctx context.Context, body string) string { 2740 return strings.ReplaceAll(body, `¯\_(ツ)_/¯`, `¯\\\_(ツ)_/¯`) 2741 } 2742 2743 var startQuote = ">" 2744 var newline = []rune("\n") 2745 2746 var blockQuoteRegex = regexp.MustCompile("((?s)```.*?```)") 2747 var quoteRegex = regexp.MustCompile("((?s)`.*?`)") 2748 2749 func ReplaceQuotedSubstrings(xs string, skipAngleQuotes bool) string { 2750 replacer := func(s string) string { 2751 return strings.Repeat("$", len(s)) 2752 } 2753 xs = blockQuoteRegex.ReplaceAllStringFunc(xs, replacer) 2754 xs = quoteRegex.ReplaceAllStringFunc(xs, replacer) 2755 2756 // Remove all quoted lines. Because we removed all codeblocks 2757 // before, we only need to consider single lines. 2758 var ret []string 2759 for _, line := range strings.Split(xs, string(newline)) { 2760 if skipAngleQuotes || !strings.HasPrefix(strings.TrimLeft(line, " "), startQuote) { 2761 ret = append(ret, line) 2762 } else { 2763 ret = append(ret, replacer(line)) 2764 } 2765 } 2766 return strings.Join(ret, string(newline)) 2767 } 2768 2769 var ErrGetUnverifiedConvNotFound = errors.New("GetUnverifiedConv: conversation not found") 2770 var ErrGetVerifiedConvNotFound = errors.New("GetVerifiedConv: conversation not found") 2771 2772 func GetUnverifiedConv(ctx context.Context, g *globals.Context, uid gregor1.UID, 2773 convID chat1.ConversationID, dataSource types.InboxSourceDataSourceTyp) (res types.RemoteConversation, err error) { 2774 2775 inbox, err := g.InboxSource.ReadUnverified(ctx, uid, dataSource, &chat1.GetInboxQuery{ 2776 ConvIDs: []chat1.ConversationID{convID}, 2777 MemberStatus: chat1.AllConversationMemberStatuses(), 2778 }) 2779 if err != nil { 2780 return res, err 2781 } 2782 if len(inbox.ConvsUnverified) == 0 { 2783 return res, ErrGetUnverifiedConvNotFound 2784 } 2785 if !inbox.ConvsUnverified[0].GetConvID().Eq(convID) { 2786 return res, fmt.Errorf("GetUnverifiedConv: convID mismatch: %s != %s", 2787 inbox.ConvsUnverified[0].ConvIDStr, convID) 2788 } 2789 return inbox.ConvsUnverified[0], nil 2790 } 2791 2792 func FormatConversationName(info chat1.ConversationInfoLocal, myUsername string) string { 2793 switch info.TeamType { 2794 case chat1.TeamType_COMPLEX: 2795 if len(info.TlfName) > 0 && len(info.TopicName) > 0 { 2796 return fmt.Sprintf("%s#%s", info.TlfName, info.TopicName) 2797 } 2798 return info.TlfName 2799 case chat1.TeamType_SIMPLE: 2800 return info.TlfName 2801 case chat1.TeamType_NONE: 2802 users := info.Participants 2803 if len(users) == 1 { 2804 return "" 2805 } 2806 var usersWithoutYou []string 2807 for _, user := range users { 2808 if user.Username != myUsername && user.InConvName { 2809 usersWithoutYou = append(usersWithoutYou, user.Username) 2810 } 2811 } 2812 return strings.Join(usersWithoutYou, ",") 2813 default: 2814 return "" 2815 } 2816 } 2817 2818 func GetVerifiedConv(ctx context.Context, g *globals.Context, uid gregor1.UID, 2819 convID chat1.ConversationID, dataSource types.InboxSourceDataSourceTyp) (res chat1.ConversationLocal, err error) { 2820 // in case we are being called from within some cancelable context, remove 2821 // it for the purposes of this call, since whatever this is is likely a 2822 // side effect we don't want to get stuck 2823 ctx = globals.CtxRemoveLocalizerCancelable(ctx) 2824 inbox, _, err := g.InboxSource.Read(ctx, uid, types.ConversationLocalizerBlocking, dataSource, nil, 2825 &chat1.GetInboxLocalQuery{ 2826 ConvIDs: []chat1.ConversationID{convID}, 2827 MemberStatus: chat1.AllConversationMemberStatuses(), 2828 }) 2829 if err != nil { 2830 return res, err 2831 } 2832 if len(inbox.Convs) == 0 { 2833 return res, ErrGetVerifiedConvNotFound 2834 } 2835 if !inbox.Convs[0].GetConvID().Eq(convID) { 2836 return res, fmt.Errorf("GetVerifiedConv: convID mismatch: %s != %s", 2837 inbox.Convs[0].GetConvID(), convID) 2838 } 2839 return inbox.Convs[0], nil 2840 } 2841 2842 func IsMapUnfurl(msg chat1.MessageUnboxed) bool { 2843 if !msg.IsValid() { 2844 return false 2845 } 2846 body := msg.Valid().MessageBody 2847 if !body.IsType(chat1.MessageType_UNFURL) { 2848 return false 2849 } 2850 unfurl := body.Unfurl() 2851 typ, err := unfurl.Unfurl.Unfurl.UnfurlType() 2852 if err != nil { 2853 return false 2854 } 2855 if typ != chat1.UnfurlType_GENERIC { 2856 return false 2857 } 2858 return body.Unfurl().Unfurl.Unfurl.Generic().MapInfo != nil 2859 } 2860 2861 func DedupStringLists(lists ...[]string) (res []string) { 2862 seen := make(map[string]struct{}) 2863 for _, list := range lists { 2864 for _, x := range list { 2865 if _, ok := seen[x]; !ok { 2866 seen[x] = struct{}{} 2867 res = append(res, x) 2868 } 2869 } 2870 } 2871 return res 2872 } 2873 2874 func DBConvLess(a pager.InboxEntry, b pager.InboxEntry) bool { 2875 if a.GetMtime() > b.GetMtime() { 2876 return true 2877 } else if a.GetMtime() < b.GetMtime() { 2878 return false 2879 } 2880 return !(a.GetConvID().Eq(b.GetConvID()) || a.GetConvID().Less(b.GetConvID())) 2881 } 2882 2883 func ExportToSummary(i chat1.InboxUIItem) (s chat1.ConvSummary) { 2884 s.Id = i.ConvID 2885 s.IsDefaultConv = i.IsDefaultConv 2886 s.Unread = i.ReadMsgID < i.MaxVisibleMsgID 2887 s.ActiveAt = i.Time.UnixSeconds() 2888 s.ActiveAtMs = i.Time.UnixMilliseconds() 2889 s.FinalizeInfo = i.FinalizeInfo 2890 s.CreatorInfo = i.CreatorInfo 2891 s.MemberStatus = strings.ToLower(i.MemberStatus.String()) 2892 s.Supersedes = make([]string, 0, len(i.Supersedes)) 2893 for _, super := range i.Supersedes { 2894 s.Supersedes = append(s.Supersedes, 2895 super.ConversationID.String()) 2896 } 2897 s.SupersededBy = make([]string, 0, len(i.SupersededBy)) 2898 for _, super := range i.SupersededBy { 2899 s.SupersededBy = append(s.SupersededBy, 2900 super.ConversationID.String()) 2901 } 2902 switch i.MembersType { 2903 case chat1.ConversationMembersType_IMPTEAMUPGRADE, chat1.ConversationMembersType_IMPTEAMNATIVE: 2904 s.ResetUsers = i.ResetParticipants 2905 } 2906 s.Channel = chat1.ChatChannel{ 2907 Name: i.Name, 2908 Public: i.IsPublic, 2909 TopicType: strings.ToLower(i.TopicType.String()), 2910 MembersType: strings.ToLower(i.MembersType.String()), 2911 TopicName: i.Channel, 2912 } 2913 return s 2914 } 2915 2916 func supersedersNotEmpty(ctx context.Context, superseders []chat1.ConversationMetadata, convs []types.RemoteConversation) bool { 2917 for _, superseder := range superseders { 2918 for _, conv := range convs { 2919 if superseder.ConversationID.Eq(conv.GetConvID()) { 2920 for _, msg := range conv.Conv.MaxMsgSummaries { 2921 if IsNonEmptyConvMessageType(msg.GetMessageType()) { 2922 return true 2923 } 2924 } 2925 } 2926 } 2927 } 2928 return false 2929 } 2930 2931 var defaultMemberStatusFilter = []chat1.ConversationMemberStatus{ 2932 chat1.ConversationMemberStatus_ACTIVE, 2933 chat1.ConversationMemberStatus_PREVIEW, 2934 chat1.ConversationMemberStatus_RESET, 2935 } 2936 2937 var defaultExistences = []chat1.ConversationExistence{ 2938 chat1.ConversationExistence_ACTIVE, 2939 } 2940 2941 func ApplyInboxQuery(ctx context.Context, debugLabeler DebugLabeler, query *chat1.GetInboxQuery, rcs []types.RemoteConversation) (res []types.RemoteConversation) { 2942 if query == nil { 2943 query = &chat1.GetInboxQuery{} 2944 } 2945 2946 var queryConvIDMap map[chat1.ConvIDStr]bool 2947 if query.ConvID != nil { 2948 query.ConvIDs = append(query.ConvIDs, *query.ConvID) 2949 } 2950 if len(query.ConvIDs) > 0 { 2951 queryConvIDMap = make(map[chat1.ConvIDStr]bool, len(query.ConvIDs)) 2952 for _, c := range query.ConvIDs { 2953 queryConvIDMap[c.ConvIDStr()] = true 2954 } 2955 } 2956 2957 memberStatus := query.MemberStatus 2958 if len(memberStatus) == 0 { 2959 memberStatus = defaultMemberStatusFilter 2960 } 2961 queryMemberStatusMap := map[chat1.ConversationMemberStatus]bool{} 2962 for _, memberStatus := range memberStatus { 2963 queryMemberStatusMap[memberStatus] = true 2964 } 2965 2966 queryStatusMap := map[chat1.ConversationStatus]bool{} 2967 for _, status := range query.Status { 2968 queryStatusMap[status] = true 2969 } 2970 2971 existences := query.Existences 2972 if len(existences) == 0 { 2973 existences = defaultExistences 2974 } 2975 existenceMap := map[chat1.ConversationExistence]bool{} 2976 for _, status := range existences { 2977 existenceMap[status] = true 2978 } 2979 2980 for _, rc := range rcs { 2981 conv := rc.Conv 2982 // Existence check 2983 if _, ok := existenceMap[conv.Metadata.Existence]; !ok && len(existenceMap) > 0 { 2984 continue 2985 } 2986 // Member status check 2987 if _, ok := queryMemberStatusMap[conv.ReaderInfo.Status]; !ok && len(memberStatus) > 0 { 2988 continue 2989 } 2990 // Status check 2991 if _, ok := queryStatusMap[conv.Metadata.Status]; !ok && len(query.Status) > 0 { 2992 continue 2993 } 2994 // Basic checks 2995 if queryConvIDMap != nil && !queryConvIDMap[rc.ConvIDStr] { 2996 continue 2997 } 2998 if query.After != nil && !conv.ReaderInfo.Mtime.After(*query.After) { 2999 continue 3000 } 3001 if query.Before != nil && !conv.ReaderInfo.Mtime.Before(*query.Before) { 3002 continue 3003 } 3004 if query.TopicType != nil && *query.TopicType != conv.Metadata.IdTriple.TopicType { 3005 continue 3006 } 3007 if query.TlfVisibility != nil && *query.TlfVisibility != keybase1.TLFVisibility_ANY && 3008 *query.TlfVisibility != conv.Metadata.Visibility { 3009 continue 3010 } 3011 if query.UnreadOnly && !conv.IsUnread() { 3012 continue 3013 } 3014 if query.ReadOnly && conv.IsUnread() { 3015 continue 3016 } 3017 if query.TlfID != nil && !query.TlfID.Eq(conv.Metadata.IdTriple.Tlfid) { 3018 continue 3019 } 3020 if query.TopicName != nil && rc.LocalMetadata != nil && 3021 *query.TopicName != rc.LocalMetadata.TopicName { 3022 continue 3023 } 3024 // If we are finalized and are superseded, then don't return this 3025 if query.OneChatTypePerTLF == nil || 3026 (query.OneChatTypePerTLF != nil && *query.OneChatTypePerTLF) { 3027 if conv.Metadata.FinalizeInfo != nil && len(conv.Metadata.SupersededBy) > 0 && len(query.ConvIDs) == 0 { 3028 if supersedersNotEmpty(ctx, conv.Metadata.SupersededBy, rcs) { 3029 continue 3030 } 3031 } 3032 } 3033 res = append(res, rc) 3034 } 3035 filtered := len(rcs) - len(res) 3036 debugLabeler.Debug(ctx, "applyQuery: query: %+v, res size: %d filtered: %d", query, len(res), filtered) 3037 return res 3038 } 3039 3040 func ToLastActiveStatus(mtime gregor1.Time) chat1.LastActiveStatus { 3041 lastActive := int(time.Since(mtime.Time()).Round(time.Hour).Hours()) 3042 switch { 3043 case lastActive <= 24: // 1 day 3044 return chat1.LastActiveStatus_ACTIVE 3045 case lastActive <= 24*7: // 7 days 3046 return chat1.LastActiveStatus_RECENTLY_ACTIVE 3047 default: 3048 return chat1.LastActiveStatus_NONE 3049 } 3050 } 3051 3052 func GetConvParticipantUsernames(ctx context.Context, g *globals.Context, uid gregor1.UID, 3053 convID chat1.ConversationID) (parts []string, err error) { 3054 uids, err := g.ParticipantsSource.Get(ctx, uid, convID, types.InboxSourceDataSourceAll) 3055 if err != nil { 3056 return parts, err 3057 } 3058 kuids := make([]keybase1.UID, 0, len(uids)) 3059 for _, uid := range uids { 3060 kuids = append(kuids, keybase1.UID(uid.String())) 3061 } 3062 rows, err := g.UIDMapper.MapUIDsToUsernamePackages(ctx, g, kuids, 0, 0, false) 3063 if err != nil { 3064 return parts, err 3065 } 3066 parts = make([]string, 0, len(rows)) 3067 for _, row := range rows { 3068 parts = append(parts, row.NormalizedUsername.String()) 3069 } 3070 return parts, nil 3071 } 3072 3073 func IsDeletedConvError(err error) bool { 3074 switch err.(type) { 3075 case libkb.ChatBadConversationError, 3076 libkb.ChatNotInTeamError, 3077 libkb.ChatNotInConvError: 3078 return true 3079 default: 3080 return false 3081 } 3082 }