status-go/protocol/chat.go

344 lines
8.5 KiB
Go

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
}