github.com/status-im/status-go@v1.1.0/protocol/chat.go (about) 1 package protocol 2 3 import ( 4 "crypto/ecdsa" 5 "encoding/json" 6 "errors" 7 "math/rand" 8 "strings" 9 "time" 10 11 "github.com/status-im/status-go/deprecation" 12 "github.com/status-im/status-go/eth-node/crypto" 13 "github.com/status-im/status-go/eth-node/types" 14 userimage "github.com/status-im/status-go/images" 15 "github.com/status-im/status-go/protocol/common" 16 "github.com/status-im/status-go/protocol/communities" 17 "github.com/status-im/status-go/protocol/protobuf" 18 "github.com/status-im/status-go/protocol/requests" 19 v1protocol "github.com/status-im/status-go/protocol/v1" 20 "github.com/status-im/status-go/services/utils" 21 ) 22 23 var chatColors = []string{ 24 "#fa6565", // red 25 "#887af9", // blue 26 "#FE8F59", // orange 27 "#7cda00", // green 28 "#51d0f0", // light-blue 29 "#d37ef4", // purple 30 } 31 32 type ChatType int 33 34 type ChatContext string 35 36 const ( 37 ChatTypeOneToOne ChatType = iota + 1 38 ChatTypePublic 39 ChatTypePrivateGroupChat 40 // Deprecated: CreateProfileChat shouldn't be used 41 // and is only left here in case profile chat feature is re-introduced. 42 ChatTypeProfile 43 // Deprecated: ChatTypeTimeline shouldn't be used 44 // and is only left here in case profile chat feature is re-introduced. 45 ChatTypeTimeline 46 ChatTypeCommunityChat 47 ) 48 49 const ( 50 FirstMessageTimestampUndefined = 0 51 FirstMessageTimestampNoMessage = 1 52 ) 53 54 const ( 55 MuteFor1MinDuration = time.Minute 56 MuteFor15MinsDuration = 15 * time.Minute 57 MuteFor1HrsDuration = time.Hour 58 MuteFor8HrsDuration = 8 * time.Hour 59 MuteFor24HrsDuration = 24 * time.Hour 60 MuteFor1WeekDuration = 7 * 24 * time.Hour 61 ) 62 63 // NOTE: Add items to the end of the list, because desktop and mobile 64 // use this enum by number rater than by string. 65 const ( 66 MuteFor15Min requests.MutingVariation = iota + 1 67 MuteFor1Hr 68 MuteFor8Hr 69 MuteFor1Week 70 MuteTillUnmuted 71 MuteTill1Min 72 Unmuted 73 MuteFor24Hr 74 ) 75 76 const pkStringLength = 68 77 78 // timelineChatID is a magic constant id for your own timeline 79 // Deprecated: timeline chats are no more supported 80 const timelineChatID = "@timeline70bd746ddcc12beb96b2c9d572d0784ab137ffc774f5383e50585a932080b57cca0484b259e61cecbaa33a4c98a300a" 81 82 type Chat struct { 83 // ID is the id of the chat, for public chats it is the name e.g. status, for one-to-one 84 // is the hex encoded public key and for group chats is a random uuid appended with 85 // the hex encoded pk of the creator of the chat 86 ID string `json:"id"` 87 Name string `json:"name"` 88 Description string `json:"description"` 89 Color string `json:"color"` 90 Emoji string `json:"emoji"` 91 // Active indicates whether the chat has been soft deleted 92 Active bool `json:"active"` 93 94 // ViewersCanPostReactions indicates whether users can post reactions in view only mode 95 ViewersCanPostReactions bool `json:"viewersCanPostReactions"` 96 97 ChatType ChatType `json:"chatType"` 98 99 // Timestamp indicates the last time this chat has received/sent a message 100 Timestamp int64 `json:"timestamp"` 101 // LastClockValue indicates the last clock value to be used when sending messages 102 LastClockValue uint64 `json:"lastClockValue"` 103 // DeletedAtClockValue indicates the clock value at time of deletion, messages 104 // with lower clock value of this should be discarded 105 DeletedAtClockValue uint64 `json:"deletedAtClockValue"` 106 // ReadMessagesAtClockValue indicates the clock value of time till all 107 // messages are considered as read 108 ReadMessagesAtClockValue uint64 109 // Denormalized fields 110 UnviewedMessagesCount uint `json:"unviewedMessagesCount"` 111 UnviewedMentionsCount uint `json:"unviewedMentionsCount"` 112 LastMessage *common.Message `json:"lastMessage"` 113 114 // Group chat fields 115 // Members are the members who have been invited to the group chat 116 Members []ChatMember `json:"members"` 117 // MembershipUpdates is all the membership events in the chat 118 MembershipUpdates []v1protocol.MembershipUpdateEvent `json:"membershipUpdateEvents"` 119 120 // Generated username name of the chat for one-to-ones 121 Alias string `json:"alias,omitempty"` 122 // Identicon generated from public key 123 Identicon string `json:"identicon"` 124 125 // Muted is used to check whether we want to receive 126 // push notifications for this chat 127 Muted bool `json:"muted"` 128 129 // Time in which chat was muted 130 MuteTill time.Time `json:"muteTill,omitempty"` 131 132 // Public key of administrator who created invitation link 133 InvitationAdmin string `json:"invitationAdmin,omitempty"` 134 135 // Public key of administrator who sent us group invitation 136 ReceivedInvitationAdmin string `json:"receivedInvitationAdmin,omitempty"` 137 138 // Public key of user profile 139 Profile string `json:"profile,omitempty"` 140 141 // CommunityID is the id of the community it belongs to 142 CommunityID string `json:"communityId,omitempty"` 143 144 // CategoryID is the id of the community category this chat belongs to. 145 CategoryID string `json:"categoryId,omitempty"` 146 147 // Joined is a timestamp that indicates when the chat was joined 148 Joined int64 `json:"joined,omitempty"` 149 150 // SyncedTo is the time up until it has synced with a mailserver 151 SyncedTo uint32 `json:"syncedTo,omitempty"` 152 153 // SyncedFrom is the time from when it was synced with a mailserver 154 SyncedFrom uint32 `json:"syncedFrom,omitempty"` 155 156 // FirstMessageTimestamp is the time when first message was sent/received on the chat 157 // valid only for community chats 158 // 0 - undefined 159 // 1 - no messages 160 FirstMessageTimestamp uint32 `json:"firstMessageTimestamp,omitempty"` 161 162 // Highlight is used for highlight chats 163 Highlight bool `json:"highlight,omitempty"` 164 165 // Image of the chat in Base64 format 166 Base64Image string `json:"image,omitempty"` 167 168 // If true, the chat is invisible if permissions are not met 169 HideIfPermissionsNotMet bool `json:"hideIfPermissionsNotMet,omitempty"` 170 } 171 172 type ChatPreview struct { 173 // ID is the id of the chat, for public chats it is the name e.g. status, for one-to-one 174 // is the hex encoded public key and for group chats is a random uuid appended with 175 // the hex encoded pk of the creator of the chat 176 ID string `json:"id"` 177 Name string `json:"name"` 178 Description string `json:"description"` 179 Color string `json:"color"` 180 Emoji string `json:"emoji"` 181 // Active indicates whether the chat has been soft deleted 182 Active bool `json:"active"` 183 184 ChatType ChatType `json:"chatType"` 185 186 // Timestamp indicates the last time this chat has received/sent a message 187 Timestamp int64 `json:"timestamp"` 188 // LastClockValue indicates the last clock value to be used when sending messages 189 LastClockValue uint64 `json:"lastClockValue"` 190 // DeletedAtClockValue indicates the clock value at time of deletion, messages 191 // with lower clock value of this should be discarded 192 DeletedAtClockValue uint64 `json:"deletedAtClockValue"` 193 194 // Denormalized fields 195 UnviewedMessagesCount uint `json:"unviewedMessagesCount"` 196 UnviewedMentionsCount uint `json:"unviewedMentionsCount"` 197 198 // Generated username name of the chat for one-to-ones 199 Alias string `json:"alias,omitempty"` 200 // Identicon generated from public key 201 Identicon string `json:"identicon"` 202 203 // Muted is used to check whether we want to receive 204 // push notifications for this chat 205 Muted bool `json:"muted,omitempty"` 206 207 // Time in which chat will be ummuted 208 MuteTill time.Time `json:"muteTill,omitempty"` 209 210 // Public key of user profile 211 Profile string `json:"profile,omitempty"` 212 213 // CommunityID is the id of the community it belongs to 214 CommunityID string `json:"communityId,omitempty"` 215 216 // CategoryID is the id of the community category this chat belongs to. 217 CategoryID string `json:"categoryId,omitempty"` 218 219 // Joined is a timestamp that indicates when the chat was joined 220 Joined int64 `json:"joined,omitempty"` 221 222 // SyncedTo is the time up until it has synced with a mailserver 223 SyncedTo uint32 `json:"syncedTo,omitempty"` 224 225 // SyncedFrom is the time from when it was synced with a mailserver 226 SyncedFrom uint32 `json:"syncedFrom,omitempty"` 227 228 // ParsedText is the parsed markdown for displaying 229 ParsedText json.RawMessage `json:"parsedText,omitempty"` 230 231 Text string `json:"text,omitempty"` 232 233 ContentType protobuf.ChatMessage_ContentType `json:"contentType,omitempty"` 234 235 // Highlight is used for highlight chats 236 Highlight bool `json:"highlight,omitempty"` 237 238 // Used for display invited community's name in the last message 239 ContentCommunityID string `json:"contentCommunityId,omitempty"` 240 241 // Members array to represent how many there are for chats preview of group chats 242 Members []ChatMember `json:"members"` 243 244 OutgoingStatus string `json:"outgoingStatus,omitempty"` 245 ResponseTo string `json:"responseTo"` 246 AlbumImagesCount uint32 `json:"albumImagesCount,omitempty"` 247 From string `json:"from"` 248 Deleted bool `json:"deleted"` 249 DeletedForMe bool `json:"deletedForMe"` 250 251 // Image of the chat in Base64 format 252 Base64Image string `json:"image,omitempty"` 253 } 254 255 func (c *Chat) PublicKey() (*ecdsa.PublicKey, error) { 256 // For one to one chatID is an encoded public key 257 if c.ChatType != ChatTypeOneToOne { 258 return nil, nil 259 } 260 return common.HexToPubkey(c.ID) 261 } 262 263 func (c *Chat) Public() bool { 264 return c.ChatType == ChatTypePublic 265 } 266 267 // Deprecated: ProfileUpdates shouldn't be used 268 // and is only left here in case profile chat feature is re-introduced. 269 func (c *Chat) ProfileUpdates() bool { 270 return c.ChatType == ChatTypeProfile || len(c.Profile) > 0 271 } 272 273 // Deprecated: Timeline shouldn't be used 274 // and is only left here in case profile chat feature is re-introduced. 275 func (c *Chat) Timeline() bool { 276 return c.ChatType == ChatTypeTimeline 277 } 278 279 func (c *Chat) OneToOne() bool { 280 return c.ChatType == ChatTypeOneToOne 281 } 282 283 func (c *Chat) CommunityChat() bool { 284 return c.ChatType == ChatTypeCommunityChat 285 } 286 287 func (c *Chat) PrivateGroupChat() bool { 288 return c.ChatType == ChatTypePrivateGroupChat 289 } 290 291 func (c *Chat) IsActivePersonalChat() bool { 292 return c.Active && (c.OneToOne() || c.PrivateGroupChat() || c.Public()) && c.CommunityID == "" 293 } 294 295 // DefaultResendType returns the resend type for a chat. 296 // This function currently infers the ResendType from the chat type. 297 // Note that specific message might have different resent types. At times 298 // some messages dictate their ResendType based on their own properties and 299 // context, rather than the chat type it is associated with. 300 func (c *Chat) DefaultResendType() common.ResendType { 301 if c.OneToOne() || c.PrivateGroupChat() { 302 return common.ResendTypeDataSync 303 } 304 305 return common.ResendTypeRawMessage 306 } 307 308 func (c *Chat) shouldBeSynced() bool { 309 isPublicChat := !c.Timeline() && !c.ProfileUpdates() && c.Public() 310 return isPublicChat || c.OneToOne() || c.PrivateGroupChat() 311 } 312 313 func (c *Chat) CommunityChatID() string { 314 if c.ChatType != ChatTypeCommunityChat { 315 return c.ID 316 } 317 318 // Strips out the local prefix of the community-id 319 return c.ID[pkStringLength:] 320 } 321 322 func (c *Chat) Validate() error { 323 if c.ID == "" { 324 return errors.New("chatID can't be blank") 325 } 326 327 if c.OneToOne() { 328 _, err := c.PublicKey() 329 return err 330 } 331 return nil 332 } 333 334 func (c *Chat) MembersAsPublicKeys() ([]*ecdsa.PublicKey, error) { 335 publicKeys := make([]string, len(c.Members)) 336 for idx, item := range c.Members { 337 publicKeys[idx] = item.ID 338 } 339 return stringSliceToPublicKeys(publicKeys) 340 } 341 342 func (c *Chat) HasMember(memberID string) bool { 343 for _, member := range c.Members { 344 if memberID == member.ID { 345 return true 346 } 347 } 348 349 return false 350 } 351 352 func (c *Chat) RemoveMember(memberID string) { 353 members := c.Members 354 c.Members = []ChatMember{} 355 for _, member := range members { 356 if memberID != member.ID { 357 c.Members = append(c.Members, member) 358 } 359 } 360 } 361 362 func (c *Chat) updateChatFromGroupMembershipChanges(g *v1protocol.Group) { 363 364 // ID 365 c.ID = g.ChatID() 366 367 // Name 368 c.Name = g.Name() 369 370 // Color 371 color := g.Color() 372 if color != "" { 373 c.Color = g.Color() 374 } 375 376 // Image 377 base64Image, err := userimage.GetPayloadDataURI(g.Image()) 378 if err == nil { 379 c.Base64Image = base64Image 380 } 381 382 // Members 383 members := g.Members() 384 admins := g.Admins() 385 chatMembers := make([]ChatMember, 0, len(members)) 386 for _, m := range members { 387 388 chatMember := ChatMember{ 389 ID: m, 390 } 391 chatMember.Admin = stringSliceContains(admins, m) 392 chatMembers = append(chatMembers, chatMember) 393 } 394 c.Members = chatMembers 395 396 // MembershipUpdates 397 c.MembershipUpdates = g.Events() 398 } 399 400 // NextClockAndTimestamp returns the next clock value 401 // and the current timestamp 402 func (c *Chat) NextClockAndTimestamp(timesource common.TimeSource) (uint64, uint64) { 403 clock := c.LastClockValue 404 timestamp := timesource.GetCurrentTime() 405 if clock == 0 || clock < timestamp { 406 clock = timestamp 407 } else { 408 clock = clock + 1 409 } 410 c.LastClockValue = clock 411 412 return clock, timestamp 413 } 414 415 func (c *Chat) UpdateFromMessage(message *common.Message, timesource common.TimeSource) error { 416 c.Timestamp = int64(timesource.GetCurrentTime()) 417 418 // If the clock of the last message is lower, we set the message 419 if c.LastMessage == nil || c.LastMessage.Clock <= message.Clock { 420 c.LastMessage = message 421 } 422 // If the clock is higher we set the clock 423 if c.LastClockValue < message.Clock { 424 c.LastClockValue = message.Clock 425 } 426 return nil 427 } 428 429 func (c *Chat) UpdateFirstMessageTimestamp(timestamp uint32) bool { 430 if timestamp == c.FirstMessageTimestamp { 431 return false 432 } 433 434 // Do not allow to assign `Undefined`` or `NoMessage` to already set timestamp 435 if timestamp == FirstMessageTimestampUndefined || 436 (timestamp == FirstMessageTimestampNoMessage && 437 c.FirstMessageTimestamp != FirstMessageTimestampUndefined) { 438 return false 439 } 440 441 if c.FirstMessageTimestamp == FirstMessageTimestampUndefined || 442 c.FirstMessageTimestamp == FirstMessageTimestampNoMessage || 443 timestamp < c.FirstMessageTimestamp { 444 c.FirstMessageTimestamp = timestamp 445 return true 446 } 447 448 return false 449 } 450 451 // ChatMembershipUpdate represent an event on membership of the chat 452 type ChatMembershipUpdate struct { 453 // Unique identifier for the event 454 ID string `json:"id"` 455 // Type indicates the kind of event 456 Type protobuf.MembershipUpdateEvent_EventType `json:"type"` 457 // Name represents the name in the event of changing name events 458 Name string `json:"name,omitempty"` 459 // Clock value of the event 460 ClockValue uint64 `json:"clockValue"` 461 // Signature of the event 462 Signature string `json:"signature"` 463 // Hex encoded public key of the creator of the event 464 From string `json:"from"` 465 // Target of the event for single-target events 466 Member string `json:"member,omitempty"` 467 // Target of the event for multi-target events 468 Members []string `json:"members,omitempty"` 469 } 470 471 // ChatMember represents a member who participates in a group chat 472 type ChatMember struct { 473 // ID is the hex encoded public key of the member 474 ID string `json:"id"` 475 // Admin indicates if the member is an admin of the group chat 476 Admin bool `json:"admin"` 477 } 478 479 func (c ChatMember) PublicKey() (*ecdsa.PublicKey, error) { 480 return common.HexToPubkey(c.ID) 481 } 482 483 func oneToOneChatID(publicKey *ecdsa.PublicKey) string { 484 return types.EncodeHex(crypto.FromECDSAPub(publicKey)) 485 } 486 487 func OneToOneFromPublicKey(pk *ecdsa.PublicKey, timesource common.TimeSource) *Chat { 488 chatID := types.EncodeHex(crypto.FromECDSAPub(pk)) 489 newChat := CreateOneToOneChat(chatID[:8], pk, timesource) 490 491 return newChat 492 } 493 494 func CreateOneToOneChat(name string, publicKey *ecdsa.PublicKey, timesource common.TimeSource) *Chat { 495 timestamp := timesource.GetCurrentTime() 496 return &Chat{ 497 ID: oneToOneChatID(publicKey), 498 Name: name, 499 Timestamp: int64(timestamp), 500 ReadMessagesAtClockValue: 0, 501 Active: true, 502 Joined: int64(timestamp), 503 ChatType: ChatTypeOneToOne, 504 Highlight: true, 505 } 506 } 507 508 func CreateCommunityChat(orgID, chatID string, orgChat *protobuf.CommunityChat, timesource common.TimeSource) *Chat { 509 color := orgChat.Identity.Color 510 if color == "" { 511 color = chatColors[rand.Intn(len(chatColors))] // nolint: gosec 512 } 513 514 timestamp := timesource.GetCurrentTime() 515 516 // Populate community _channel_ members to _chat_ members 517 chatMembers := []ChatMember{} 518 for pubKey := range orgChat.Members { 519 chatMember := ChatMember{ 520 ID: pubKey, 521 Admin: false, 522 } 523 chatMembers = append(chatMembers, chatMember) 524 } 525 526 return &Chat{ 527 CommunityID: orgID, 528 CategoryID: orgChat.CategoryId, 529 HideIfPermissionsNotMet: orgChat.HideIfPermissionsNotMet, 530 Name: orgChat.Identity.DisplayName, 531 Description: orgChat.Identity.Description, 532 Members: chatMembers, 533 Active: true, 534 Color: color, 535 Emoji: orgChat.Identity.Emoji, 536 ID: orgID + chatID, 537 Timestamp: int64(timestamp), 538 Joined: int64(timestamp), 539 ReadMessagesAtClockValue: 0, 540 ChatType: ChatTypeCommunityChat, 541 FirstMessageTimestamp: orgChat.Identity.FirstMessageTimestamp, 542 ViewersCanPostReactions: orgChat.ViewersCanPostReactions, 543 } 544 } 545 546 func (c *Chat) CommunityChannelID() string { 547 return strings.TrimPrefix(c.ID, c.CommunityID) 548 } 549 550 func (c *Chat) DeepLink() string { 551 if c.OneToOne() { 552 return "status-app://p/" + c.ID 553 } 554 if c.PrivateGroupChat() { 555 return "status-app://g/args?a2=" + c.ID 556 } 557 558 if c.CommunityChat() { 559 communityChannelID := c.CommunityChannelID() 560 pubkey, err := types.DecodeHex(c.CommunityID) 561 if err != nil { 562 return "" 563 } 564 565 serializedCommunityID, err := utils.SerializePublicKey(pubkey) 566 567 if err != nil { 568 return "" 569 } 570 571 return "status-app://cc/" + communityChannelID + "#" + serializedCommunityID 572 } 573 574 if c.Public() { 575 return "status-app://" + c.ID 576 } 577 578 return "" 579 } 580 581 func CreateCommunityChats(org *communities.Community, timesource common.TimeSource) []*Chat { 582 var chats []*Chat 583 orgID := org.IDString() 584 585 for chatID, chat := range org.Chats() { 586 chats = append(chats, CreateCommunityChat(orgID, chatID, chat, timesource)) 587 } 588 return chats 589 } 590 591 func CreatePublicChat(name string, timesource common.TimeSource) *Chat { 592 timestamp := timesource.GetCurrentTime() 593 return &Chat{ 594 ID: name, 595 Name: name, 596 Active: true, 597 Timestamp: int64(timestamp), 598 Joined: int64(timestamp), 599 ReadMessagesAtClockValue: 0, 600 Color: chatColors[rand.Intn(len(chatColors))], // nolint: gosec 601 ChatType: ChatTypePublic, 602 Members: []ChatMember{}, 603 } 604 } 605 606 // Deprecated: buildProfileChatID shouldn't be used 607 // and is only left here in case profile chat feature is re-introduced. 608 func buildProfileChatID(publicKeyString string) string { 609 return "@" + publicKeyString 610 } 611 612 // Deprecated: CreateProfileChat shouldn't be used 613 // and is only left here in case profile chat feature is re-introduced. 614 func CreateProfileChat(pubkey string, timesource common.TimeSource) *Chat { 615 // Return nil to prevent usage of deprecated function 616 if deprecation.ChatProfileDeprecated { 617 return nil 618 } 619 620 id := buildProfileChatID(pubkey) 621 return &Chat{ 622 ID: id, 623 Name: id, 624 Active: true, 625 Timestamp: int64(timesource.GetCurrentTime()), 626 Joined: int64(timesource.GetCurrentTime()), 627 Color: chatColors[rand.Intn(len(chatColors))], // nolint: gosec 628 ChatType: ChatTypeProfile, 629 Profile: pubkey, 630 } 631 } 632 633 func CreateGroupChat(timesource common.TimeSource) Chat { 634 timestamp := timesource.GetCurrentTime() 635 synced := uint32(timestamp / 1000) 636 637 return Chat{ 638 Active: true, 639 Color: chatColors[rand.Intn(len(chatColors))], // nolint: gosec 640 Timestamp: int64(timestamp), 641 ReadMessagesAtClockValue: 0, 642 SyncedTo: synced, 643 SyncedFrom: synced, 644 ChatType: ChatTypePrivateGroupChat, 645 Highlight: true, 646 } 647 } 648 649 // Deprecated: CreateTimelineChat shouldn't be used 650 // and is only left here in case profile chat feature is re-introduced. 651 func CreateTimelineChat(timesource common.TimeSource) *Chat { 652 // Return nil to prevent usage of deprecated function 653 if deprecation.ChatTimelineDeprecated { 654 return nil 655 } 656 657 return &Chat{ 658 ID: timelineChatID, 659 Name: "#" + timelineChatID, 660 Timestamp: int64(timesource.GetCurrentTime()), 661 Active: true, 662 ChatType: ChatTypeTimeline, 663 } 664 } 665 666 func stringSliceToPublicKeys(slice []string) ([]*ecdsa.PublicKey, error) { 667 result := make([]*ecdsa.PublicKey, len(slice)) 668 for idx, item := range slice { 669 var err error 670 result[idx], err = common.HexToPubkey(item) 671 if err != nil { 672 return nil, err 673 } 674 } 675 return result, nil 676 } 677 678 func stringSliceContains(slice []string, item string) bool { 679 for _, s := range slice { 680 if s == item { 681 return true 682 } 683 } 684 return false 685 } 686 687 func GetChatContextFromChatType(chatType ChatType) ChatContext { 688 switch chatType { 689 case ChatTypeOneToOne, ChatTypePrivateGroupChat: 690 return privateChat 691 default: 692 return publicChat 693 } 694 }