2019-11-21 16:19:22 +00:00
|
|
|
package protocol
|
2019-07-17 22:25:42 +00:00
|
|
|
|
2019-08-29 06:33:46 +00:00
|
|
|
import (
|
|
|
|
"crypto/ecdsa"
|
2019-10-14 14:10:48 +00:00
|
|
|
"encoding/hex"
|
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
|
|
|
"encoding/json"
|
2020-02-07 11:56:30 +00:00
|
|
|
"errors"
|
2019-12-02 15:34:05 +00:00
|
|
|
"math/rand"
|
2019-08-29 06:33:46 +00:00
|
|
|
|
2019-11-23 17:57:05 +00:00
|
|
|
"github.com/status-im/status-go/eth-node/crypto"
|
|
|
|
"github.com/status-im/status-go/eth-node/types"
|
2019-12-02 15:34:05 +00:00
|
|
|
"github.com/status-im/status-go/protocol/protobuf"
|
2019-11-21 16:19:22 +00:00
|
|
|
v1protocol "github.com/status-im/status-go/protocol/v1"
|
2019-08-29 06:33:46 +00:00
|
|
|
)
|
2019-07-17 22:25:42 +00:00
|
|
|
|
2019-12-02 15:34:05 +00:00
|
|
|
var chatColors = []string{
|
|
|
|
"#fa6565", // red
|
|
|
|
"#887af9", // blue
|
|
|
|
"#FE8F59", // orange
|
|
|
|
"#7cda00", // green
|
|
|
|
"#51d0f0", // light-blue
|
|
|
|
"#d37ef4", // purple
|
|
|
|
}
|
|
|
|
|
2019-07-30 18:39:16 +00:00
|
|
|
type ChatType int
|
|
|
|
|
|
|
|
const (
|
2019-10-09 14:22:53 +00:00
|
|
|
ChatTypeOneToOne ChatType = iota + 1
|
2019-07-30 18:39:16 +00:00
|
|
|
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
|
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
|
|
|
UnviewedMessagesCount uint `json:"unviewedMessagesCount"`
|
|
|
|
LastMessage []byte `json:"lastMessage"`
|
2019-07-30 18:39:16 +00:00
|
|
|
|
|
|
|
// 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
|
2019-12-02 15:34:05 +00:00
|
|
|
MembershipUpdates []v1protocol.MembershipUpdateEvent `json:"membershipUpdateEvents"`
|
2020-05-20 12:16:12 +00:00
|
|
|
|
|
|
|
// Generated username name of the chat for one-to-ones
|
|
|
|
Alias string `json:"alias,omitempty"`
|
|
|
|
// Identicon generated from public key
|
|
|
|
Identicon string `json:"identicon"`
|
2019-12-02 15:34:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
2019-07-30 18:39:16 +00:00
|
|
|
}
|
|
|
|
|
2020-01-15 07:25:09 +00:00
|
|
|
func (c *Chat) Public() bool {
|
|
|
|
return c.ChatType == ChatTypePublic
|
|
|
|
}
|
|
|
|
|
2020-02-07 11:56:30 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
Move to protobuf for Message type (#1706)
* Use a single Message type `v1/message.go` and `message.go` are the same now, and they embed `protobuf.ChatMessage`
* Use `SendChatMessage` for sending chat messages, this is basically the old `Send` but a bit more flexible so we can send different message types (stickers,commands), and not just text.
* Remove dedup from services/shhext. Because now we process in status-protocol, dedup makes less sense, as those messages are going to be processed anyway, so removing for now, we can re-evaluate if bringing it to status-go or not.
* Change the various retrieveX method to a single one:
`RetrieveAll` will be processing those messages that it can process (Currently only `Message`), and return the rest in `RawMessages` (still transit). The format for the response is:
`Chats`: -> The chats updated by receiving the message
`Messages`: -> The messages retrieved (already matched to a chat)
`Contacts`: -> The contacts updated by the messages
`RawMessages` -> Anything else that can't be parsed, eventually as we move everything to status-protocol-go this will go away.
2019-12-05 16:25:34 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-10-14 14:10:48 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-04-22 12:58:28 +00:00
|
|
|
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)
|
|
|
|
|
2019-10-14 14:10:48 +00:00
|
|
|
// 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 {
|
2020-04-22 12:58:28 +00:00
|
|
|
// Check if we joined thanks to these changes, if so, make chat active
|
|
|
|
if m == myID && !hadJoined {
|
|
|
|
c.Active = true
|
|
|
|
}
|
|
|
|
|
2019-10-14 14:10:48 +00:00
|
|
|
chatMember := ChatMember{
|
|
|
|
ID: m,
|
|
|
|
}
|
|
|
|
chatMember.Admin = stringSliceContains(admins, m)
|
|
|
|
chatMember.Joined = stringSliceContains(joined, m)
|
|
|
|
chatMembers = append(chatMembers, chatMember)
|
|
|
|
}
|
|
|
|
c.Members = chatMembers
|
|
|
|
|
|
|
|
// MembershipUpdates
|
2019-12-02 15:34:05 +00:00
|
|
|
c.MembershipUpdates = g.Events()
|
2019-10-14 14:10:48 +00:00
|
|
|
}
|
|
|
|
|
2020-02-07 11:30:26 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2019-07-30 18:39:16 +00:00
|
|
|
// ChatMembershipUpdate represent an event on membership of the chat
|
|
|
|
type ChatMembershipUpdate struct {
|
|
|
|
// Unique identifier for the event
|
|
|
|
ID string `json:"id"`
|
2019-12-02 15:34:05 +00:00
|
|
|
// Type indicates the kind of event
|
|
|
|
Type protobuf.MembershipUpdateEvent_EventType `json:"type"`
|
2019-07-30 18:39:16 +00:00
|
|
|
// Name represents the name in the event of changing name events
|
2019-08-20 11:20:25 +00:00
|
|
|
Name string `json:"name,omitempty"`
|
2019-07-30 18:39:16 +00:00
|
|
|
// 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
|
2019-08-20 11:20:25 +00:00
|
|
|
Member string `json:"member,omitempty"`
|
2019-07-30 18:39:16 +00:00
|
|
|
// Target of the event for multi-target events
|
2019-08-20 11:20:25 +00:00
|
|
|
Members []string `json:"members,omitempty"`
|
2019-07-30 18:39:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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"`
|
2019-07-17 22:25:42 +00:00
|
|
|
}
|
2019-08-29 06:33:46 +00:00
|
|
|
|
|
|
|
func (c ChatMember) PublicKey() (*ecdsa.PublicKey, error) {
|
2019-11-23 17:57:05 +00:00
|
|
|
b, err := types.DecodeHex(c.ID)
|
2019-08-29 06:33:46 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return crypto.UnmarshalPubkey(b)
|
|
|
|
}
|
2019-09-26 07:01:17 +00:00
|
|
|
|
|
|
|
func oneToOneChatID(publicKey *ecdsa.PublicKey) string {
|
2019-11-23 17:57:05 +00:00
|
|
|
return types.EncodeHex(crypto.FromECDSAPub(publicKey))
|
2019-09-26 07:01:17 +00:00
|
|
|
}
|
|
|
|
|
2020-02-07 11:30:26 +00:00
|
|
|
func OneToOneFromPublicKey(pk *ecdsa.PublicKey, timesource TimeSource) *Chat {
|
2020-01-10 18:59:01 +00:00
|
|
|
chatID := types.EncodeHex(crypto.FromECDSAPub(pk))
|
2020-01-20 16:44:32 +00:00
|
|
|
newChat := CreateOneToOneChat(chatID[:8], pk, timesource)
|
2020-01-10 18:59:01 +00:00
|
|
|
|
|
|
|
return &newChat
|
|
|
|
}
|
|
|
|
|
2020-02-07 11:30:26 +00:00
|
|
|
func CreateOneToOneChat(name string, publicKey *ecdsa.PublicKey, timesource TimeSource) Chat {
|
2019-09-26 07:01:17 +00:00
|
|
|
return Chat{
|
2020-01-15 07:25:09 +00:00
|
|
|
ID: oneToOneChatID(publicKey),
|
|
|
|
Name: name,
|
2020-01-20 16:44:32 +00:00
|
|
|
Timestamp: int64(timesource.GetCurrentTime()),
|
2020-01-15 07:25:09 +00:00
|
|
|
Active: true,
|
|
|
|
ChatType: ChatTypeOneToOne,
|
2019-09-26 07:01:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-07 11:30:26 +00:00
|
|
|
func CreatePublicChat(name string, timesource TimeSource) Chat {
|
2019-09-26 07:01:17 +00:00
|
|
|
return Chat{
|
2020-01-15 07:25:09 +00:00
|
|
|
ID: name,
|
|
|
|
Name: name,
|
|
|
|
Active: true,
|
2020-01-20 16:44:32 +00:00
|
|
|
Timestamp: int64(timesource.GetCurrentTime()),
|
2020-01-15 07:25:09 +00:00
|
|
|
Color: chatColors[rand.Intn(len(chatColors))],
|
|
|
|
ChatType: ChatTypePublic,
|
2019-09-26 07:01:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-02-07 11:30:26 +00:00
|
|
|
func CreateGroupChat(timesource TimeSource) Chat {
|
2019-10-09 14:22:53 +00:00
|
|
|
return Chat{
|
2019-12-02 15:34:05 +00:00
|
|
|
Active: true,
|
|
|
|
Color: chatColors[rand.Intn(len(chatColors))],
|
2020-01-20 16:44:32 +00:00
|
|
|
Timestamp: int64(timesource.GetCurrentTime()),
|
2019-12-02 15:34:05 +00:00
|
|
|
ChatType: ChatTypePrivateGroupChat,
|
2019-10-09 14:22:53 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-14 14:10:48 +00:00
|
|
|
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 {
|
2019-11-23 17:57:05 +00:00
|
|
|
b, err = types.DecodeHex(item)
|
2019-10-14 14:10:48 +00:00
|
|
|
} 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
|
|
|
|
}
|