package protocol import ( "crypto/ecdsa" "encoding/hex" "encoding/json" "errors" "math/rand" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/protocol/protobuf" v1protocol "github.com/status-im/status-go/protocol/v1" ) var chatColors = []string{ "#fa6565", // red "#887af9", // blue "#FE8F59", // orange "#7cda00", // green "#51d0f0", // light-blue "#d37ef4", // purple } type ChatType int const ( ChatTypeOneToOne ChatType = iota + 1 ChatTypePublic ChatTypePrivateGroupChat ) type Chat struct { // ID is the id of the chat, for public chats it is the name e.g. status, for one-to-one // is the hex encoded public key and for group chats is a random uuid appended with // the hex encoded pk of the creator of the chat ID string `json:"id"` Name string `json:"name"` Color string `json:"color"` // Active indicates whether the chat has been soft deleted Active bool `json:"active"` ChatType ChatType `json:"chatType"` // Timestamp indicates the last time this chat has received/sent a message Timestamp int64 `json:"timestamp"` // LastClockValue indicates the last clock value to be used when sending messages LastClockValue uint64 `json:"lastClockValue"` // DeletedAtClockValue indicates the clock value at time of deletion, messages // with lower clock value of this should be discarded DeletedAtClockValue uint64 `json:"deletedAtClockValue"` // Denormalized fields UnviewedMessagesCount uint `json:"unviewedMessagesCount"` LastMessage []byte `json:"lastMessage"` // Group chat fields // Members are the members who have been invited to the group chat Members []ChatMember `json:"members"` // MembershipUpdates is all the membership events in the chat MembershipUpdates []v1protocol.MembershipUpdateEvent `json:"membershipUpdateEvents"` // Generated username name of the chat for one-to-ones Alias string `json:"alias,omitempty"` // Identicon generated from public key Identicon string `json:"identicon"` // Muted is used to check whether we want to receive // push notifications for this chat Muted bool `json:"muted,omitempty"` } func (c *Chat) PublicKey() (*ecdsa.PublicKey, error) { // For one to one chatID is an encoded public key if c.ChatType != ChatTypeOneToOne { return nil, nil } pkey, err := hex.DecodeString(c.ID[2:]) if err != nil { return nil, err } // Safety check, make sure is well formed return crypto.UnmarshalPubkey(pkey) } func (c *Chat) Public() bool { return c.ChatType == ChatTypePublic } func (c *Chat) OneToOne() bool { return c.ChatType == ChatTypeOneToOne } func (c *Chat) Validate() error { if c.ID == "" { return errors.New("chatID can't be blank") } if c.OneToOne() { _, err := c.PublicKey() return err } return nil } func (c *Chat) MarshalJSON() ([]byte, error) { type ChatAlias Chat item := struct { *ChatAlias LastMessage json.RawMessage `json:"lastMessage"` }{ ChatAlias: (*ChatAlias)(c), LastMessage: c.LastMessage, } return json.Marshal(item) } func (c *Chat) UnmarshalJSON(data []byte) error { type ChatAlias Chat aux := struct { *ChatAlias LastMessage *Message `json:"lastMessage"` }{ ChatAlias: (*ChatAlias)(c), } if err := json.Unmarshal(data, &aux); err != nil { return err } c.ID = aux.ID c.Name = aux.Name c.Color = aux.Color c.Active = aux.Active c.ChatType = aux.ChatType c.Timestamp = aux.Timestamp c.LastClockValue = aux.LastClockValue c.DeletedAtClockValue = aux.DeletedAtClockValue c.UnviewedMessagesCount = aux.UnviewedMessagesCount c.Members = aux.Members c.MembershipUpdates = aux.MembershipUpdates if aux.LastMessage != nil { data, err := json.Marshal(aux.LastMessage) if err != nil { return err } c.LastMessage = data } return nil } func (c *Chat) MembersAsPublicKeys() ([]*ecdsa.PublicKey, error) { publicKeys := make([]string, len(c.Members)) for idx, item := range c.Members { publicKeys[idx] = item.ID } return stringSliceToPublicKeys(publicKeys, true) } func (c *Chat) HasMember(memberID string) bool { for _, member := range c.Members { if memberID == member.ID { return true } } return false } func (c *Chat) updateChatFromGroupMembershipChanges(myID string, g *v1protocol.Group) { // Check if we were already in the chat hadJoined := c.HasMember(myID) // ID c.ID = g.ChatID() // Name c.Name = g.Name() // Members members := g.Members() admins := g.Admins() joined := g.Joined() chatMembers := make([]ChatMember, 0, len(members)) for _, m := range members { // Check if we joined thanks to these changes, if so, make chat active if m == myID && !hadJoined { c.Active = true } chatMember := ChatMember{ ID: m, } chatMember.Admin = stringSliceContains(admins, m) chatMember.Joined = stringSliceContains(joined, m) chatMembers = append(chatMembers, chatMember) } c.Members = chatMembers // MembershipUpdates c.MembershipUpdates = g.Events() } // NextClockAndTimestamp returns the next clock value // and the current timestamp func (c *Chat) NextClockAndTimestamp(timesource TimeSource) (uint64, uint64) { clock := c.LastClockValue timestamp := timesource.GetCurrentTime() if clock == 0 || clock < timestamp { clock = timestamp } else { clock = clock + 1 } return clock, timestamp } func (c *Chat) UpdateFromMessage(message *Message, timesource TimeSource) error { c.Timestamp = int64(timesource.GetCurrentTime()) if c.LastClockValue <= message.Clock { jsonMessage, err := json.Marshal(message) if err != nil { return err } c.LastClockValue = message.Clock c.LastMessage = jsonMessage } return nil } // ChatMembershipUpdate represent an event on membership of the chat type ChatMembershipUpdate struct { // Unique identifier for the event ID string `json:"id"` // Type indicates the kind of event Type protobuf.MembershipUpdateEvent_EventType `json:"type"` // Name represents the name in the event of changing name events Name string `json:"name,omitempty"` // Clock value of the event ClockValue uint64 `json:"clockValue"` // Signature of the event Signature string `json:"signature"` // Hex encoded public key of the creator of the event From string `json:"from"` // Target of the event for single-target events Member string `json:"member,omitempty"` // Target of the event for multi-target events Members []string `json:"members,omitempty"` } // ChatMember represents a member who participates in a group chat type ChatMember struct { // ID is the hex encoded public key of the member ID string `json:"id"` // Admin indicates if the member is an admin of the group chat Admin bool `json:"admin"` // Joined indicates if the member has joined the group chat Joined bool `json:"joined"` } func (c ChatMember) PublicKey() (*ecdsa.PublicKey, error) { b, err := types.DecodeHex(c.ID) if err != nil { return nil, err } return crypto.UnmarshalPubkey(b) } func oneToOneChatID(publicKey *ecdsa.PublicKey) string { return types.EncodeHex(crypto.FromECDSAPub(publicKey)) } func OneToOneFromPublicKey(pk *ecdsa.PublicKey, timesource TimeSource) *Chat { chatID := types.EncodeHex(crypto.FromECDSAPub(pk)) newChat := CreateOneToOneChat(chatID[:8], pk, timesource) return &newChat } func CreateOneToOneChat(name string, publicKey *ecdsa.PublicKey, timesource TimeSource) Chat { return Chat{ ID: oneToOneChatID(publicKey), Name: name, Timestamp: int64(timesource.GetCurrentTime()), Active: true, ChatType: ChatTypeOneToOne, } } func CreatePublicChat(name string, timesource TimeSource) Chat { return Chat{ ID: name, Name: name, Active: true, Timestamp: int64(timesource.GetCurrentTime()), Color: chatColors[rand.Intn(len(chatColors))], ChatType: ChatTypePublic, } } func CreateGroupChat(timesource TimeSource) Chat { return Chat{ Active: true, Color: chatColors[rand.Intn(len(chatColors))], Timestamp: int64(timesource.GetCurrentTime()), ChatType: ChatTypePrivateGroupChat, } } func stringSliceToPublicKeys(slice []string, prefixed bool) ([]*ecdsa.PublicKey, error) { result := make([]*ecdsa.PublicKey, len(slice)) for idx, item := range slice { var ( b []byte err error ) if prefixed { b, err = types.DecodeHex(item) } else { b, err = hex.DecodeString(item) } if err != nil { return nil, err } result[idx], err = crypto.UnmarshalPubkey(b) if err != nil { return nil, err } } return result, nil } func stringSliceContains(slice []string, item string) bool { for _, s := range slice { if s == item { return true } } return false }