690 lines
16 KiB
Go
690 lines
16 KiB
Go
package protocol
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
|
|
"github.com/status-im/status-go/deprecation"
|
|
"github.com/status-im/status-go/protocol/common"
|
|
"github.com/status-im/status-go/protocol/protobuf"
|
|
"github.com/status-im/status-go/protocol/requests"
|
|
"github.com/status-im/status-go/protocol/transport"
|
|
)
|
|
|
|
type ChatPreviewFilterType int
|
|
|
|
const (
|
|
ChatPreviewFilterTypeAll ChatPreviewFilterType = iota
|
|
ChatPreviewFilterTypeCommunity
|
|
ChatPreviewFilterTypeNonCommunity
|
|
)
|
|
|
|
func (m *Messenger) getOneToOneAndNextClock(contact *Contact) (*Chat, uint64, error) {
|
|
chat, ok := m.allChats.Load(contact.ID)
|
|
if !ok {
|
|
publicKey, err := contact.PublicKey()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
chat = OneToOneFromPublicKey(publicKey, m.getTimesource())
|
|
|
|
// We don't want to show the chat to the user by default
|
|
chat.Active = false
|
|
|
|
if err := m.saveChat(chat); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
m.allChats.Store(chat.ID, chat)
|
|
}
|
|
clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
|
|
|
|
return chat, clock, nil
|
|
}
|
|
|
|
func (m *Messenger) Chats() []*Chat {
|
|
var chats []*Chat
|
|
|
|
m.allChats.Range(func(chatID string, chat *Chat) (shouldContinue bool) {
|
|
chats = append(chats, chat)
|
|
return true
|
|
})
|
|
|
|
return chats
|
|
}
|
|
|
|
// ChatsPreview returns a list of chat previews.
|
|
// When onlyCommunityChats is nil, returns all chats
|
|
// When onlyCommunityChats is true, only returns community chats
|
|
// When onlyCommunityChats is false, returns all non-community chats
|
|
func (m *Messenger) ChatsPreview(filterPointer *ChatPreviewFilterType) []*ChatPreview {
|
|
var chats []*ChatPreview
|
|
filter := ChatPreviewFilterTypeAll
|
|
if filterPointer != nil {
|
|
filter = *filterPointer
|
|
}
|
|
m.allChats.Range(func(chatID string, chat *Chat) (shouldContinue bool) {
|
|
// Skip if chat doesn't match the filter
|
|
isCommunityChat := chat.ChatType == ChatTypeCommunityChat
|
|
if filter == ChatPreviewFilterTypeCommunity && !isCommunityChat {
|
|
return true
|
|
}
|
|
if filter == ChatPreviewFilterTypeNonCommunity && isCommunityChat {
|
|
return true
|
|
}
|
|
if chat.Active || chat.Muted {
|
|
chatPreview := &ChatPreview{
|
|
ID: chat.ID,
|
|
Name: chat.Name,
|
|
Description: chat.Description,
|
|
Color: chat.Color,
|
|
Emoji: chat.Emoji,
|
|
Active: chat.Active,
|
|
ChatType: chat.ChatType,
|
|
Timestamp: chat.Timestamp,
|
|
LastClockValue: chat.LastClockValue,
|
|
DeletedAtClockValue: chat.DeletedAtClockValue,
|
|
UnviewedMessagesCount: chat.UnviewedMessagesCount,
|
|
UnviewedMentionsCount: chat.UnviewedMentionsCount,
|
|
Alias: chat.Alias,
|
|
Identicon: chat.Identicon,
|
|
Muted: chat.Muted,
|
|
MuteTill: chat.MuteTill,
|
|
Profile: chat.Profile,
|
|
CommunityID: chat.CommunityID,
|
|
CategoryID: chat.CategoryID,
|
|
Joined: chat.Joined,
|
|
SyncedTo: chat.SyncedTo,
|
|
SyncedFrom: chat.SyncedFrom,
|
|
Highlight: chat.Highlight,
|
|
Members: chat.Members,
|
|
Base64Image: chat.Base64Image,
|
|
}
|
|
|
|
if chat.LastMessage != nil {
|
|
|
|
chatPreview.OutgoingStatus = chat.LastMessage.OutgoingStatus
|
|
chatPreview.ResponseTo = chat.LastMessage.ResponseTo
|
|
chatPreview.ContentType = chat.LastMessage.ContentType
|
|
chatPreview.From = chat.LastMessage.From
|
|
chatPreview.Deleted = chat.LastMessage.Deleted
|
|
chatPreview.DeletedForMe = chat.LastMessage.DeletedForMe
|
|
|
|
if chat.LastMessage.ContentType == protobuf.ChatMessage_IMAGE {
|
|
chatPreview.ParsedText = chat.LastMessage.ParsedText
|
|
|
|
image := chat.LastMessage.GetImage()
|
|
if image != nil {
|
|
chatPreview.AlbumImagesCount = image.AlbumImagesCount
|
|
chatPreview.ParsedText = chat.LastMessage.ParsedText
|
|
}
|
|
}
|
|
|
|
if chat.LastMessage.ContentType == protobuf.ChatMessage_TEXT_PLAIN {
|
|
|
|
simplifiedText, err := chat.LastMessage.GetSimplifiedText("", nil)
|
|
|
|
if err == nil {
|
|
if len(simplifiedText) > 100 {
|
|
chatPreview.Text = simplifiedText[:100]
|
|
} else {
|
|
chatPreview.Text = simplifiedText
|
|
}
|
|
if strings.Contains(chatPreview.Text, "0x") {
|
|
//if there is a mention, we would like to send parsed text as well
|
|
chatPreview.ParsedText = chat.LastMessage.ParsedText
|
|
}
|
|
}
|
|
} else if chat.LastMessage.ContentType == protobuf.ChatMessage_EMOJI ||
|
|
chat.LastMessage.ContentType == protobuf.ChatMessage_TRANSACTION_COMMAND {
|
|
|
|
chatPreview.Text = chat.LastMessage.Text
|
|
chatPreview.ParsedText = chat.LastMessage.ParsedText
|
|
}
|
|
if chat.LastMessage.ContentType == protobuf.ChatMessage_COMMUNITY {
|
|
chatPreview.ContentCommunityID = chat.LastMessage.CommunityID
|
|
}
|
|
}
|
|
|
|
chats = append(chats, chatPreview)
|
|
}
|
|
|
|
return true
|
|
})
|
|
|
|
return chats
|
|
}
|
|
|
|
func (m *Messenger) Chat(chatID string) *Chat {
|
|
chat, _ := m.allChats.Load(chatID)
|
|
|
|
return chat
|
|
}
|
|
|
|
func (m *Messenger) ActiveChats() []*Chat {
|
|
m.mutex.Lock()
|
|
defer m.mutex.Unlock()
|
|
|
|
var chats []*Chat
|
|
|
|
m.allChats.Range(func(chatID string, c *Chat) bool {
|
|
if c.Active {
|
|
chats = append(chats, c)
|
|
}
|
|
return true
|
|
})
|
|
|
|
return chats
|
|
}
|
|
|
|
func (m *Messenger) initChatSyncFields(chat *Chat) error {
|
|
defaultSyncPeriod, err := m.settings.GetDefaultSyncPeriod()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
timestamp := uint32(m.getTimesource().GetCurrentTime()/1000) - defaultSyncPeriod
|
|
chat.SyncedTo = timestamp
|
|
chat.SyncedFrom = timestamp
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Messenger) createPublicChat(chatID string, response *MessengerResponse) (*MessengerResponse, error) {
|
|
chat, ok := m.allChats.Load(chatID)
|
|
if !ok {
|
|
chat = CreatePublicChat(chatID, m.getTimesource())
|
|
|
|
}
|
|
chat.Active = true
|
|
chat.DeletedAtClockValue = 0
|
|
|
|
// Save topics
|
|
_, err := m.Join(chat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store chat
|
|
m.allChats.Store(chat.ID, chat)
|
|
|
|
willSync, err := m.scheduleSyncChat(chat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We set the synced to, synced from to the default time
|
|
if !willSync {
|
|
if err := m.initChatSyncFields(chat); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = m.saveChat(chat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = m.reregisterForPushNotifications()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response.AddChat(chat)
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (m *Messenger) CreatePublicChat(request *requests.CreatePublicChat) (*MessengerResponse, error) {
|
|
if err := request.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chatID := request.ID
|
|
response := &MessengerResponse{}
|
|
|
|
return m.createPublicChat(chatID, response)
|
|
}
|
|
|
|
// Deprecated: CreateProfileChat shouldn't be used
|
|
// and is only left here in case profile chat feature is re-introduced.
|
|
func (m *Messenger) CreateProfileChat(request *requests.CreateProfileChat) (*MessengerResponse, error) {
|
|
// Return error to prevent usage of deprecated function
|
|
if deprecation.ChatProfileDeprecated {
|
|
return nil, errors.New("profile chats are deprecated")
|
|
}
|
|
|
|
if err := request.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
publicKey, err := common.HexToPubkey(request.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chat := m.buildProfileChat(request.ID)
|
|
|
|
chat.Active = true
|
|
|
|
// Save topics
|
|
_, err = m.Join(chat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check contact code
|
|
filter, err := m.transport.JoinPrivate(publicKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Store chat
|
|
m.allChats.Store(chat.ID, chat)
|
|
|
|
response := &MessengerResponse{}
|
|
response.AddChat(chat)
|
|
|
|
willSync, err := m.scheduleSyncChat(chat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We set the synced to, synced from to the default time
|
|
if !willSync {
|
|
if err := m.initChatSyncFields(chat); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
_, err = m.scheduleSyncFilters([]*transport.Filter{filter})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = m.saveChat(chat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return response, nil
|
|
}
|
|
|
|
func (m *Messenger) CreateOneToOneChat(request *requests.CreateOneToOneChat) (*MessengerResponse, error) {
|
|
if err := request.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
chatID := request.ID.String()
|
|
pk, err := common.HexToPubkey(chatID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
response := &MessengerResponse{}
|
|
|
|
ensName := request.ENSName
|
|
if ensName != "" {
|
|
clock := m.getTimesource().GetCurrentTime()
|
|
err := m.ensVerifier.ENSVerified(chatID, ensName, clock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
contact, err := m.BuildContact(&requests.BuildContact{PublicKey: chatID})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
contact.EnsName = ensName
|
|
contact.ENSVerified = true
|
|
err = m.persistence.SaveContact(contact, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
response.Contacts = []*Contact{contact}
|
|
}
|
|
|
|
chat, ok := m.allChats.Load(chatID)
|
|
if !ok {
|
|
chat = CreateOneToOneChat(chatID, pk, m.getTimesource())
|
|
}
|
|
chat.Active = true
|
|
|
|
filters, err := m.Join(chat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// TODO(Samyoul) remove storing of an updated reference pointer?
|
|
m.allChats.Store(chatID, chat)
|
|
|
|
response.AddChat(chat)
|
|
|
|
willSync, err := m.scheduleSyncFilters(filters)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We set the synced to, synced from to the default time
|
|
if !willSync {
|
|
if err := m.initChatSyncFields(chat); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = m.saveChat(chat)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (m *Messenger) DeleteChat(chatID string) error {
|
|
return m.deleteChat(chatID)
|
|
}
|
|
|
|
func (m *Messenger) deleteChat(chatID string) error {
|
|
err := m.persistence.DeleteChat(chatID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We clean the cache to be able to receive the messages again later
|
|
err = m.transport.ClearProcessedMessageIDsCache()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
chat, ok := m.allChats.Load(chatID)
|
|
|
|
if ok && chat.Active && chat.Public() {
|
|
m.allChats.Delete(chatID)
|
|
return m.reregisterForPushNotifications()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Messenger) SaveChat(chat *Chat) error {
|
|
return m.saveChat(chat)
|
|
}
|
|
|
|
func (m *Messenger) DeactivateChat(request *requests.DeactivateChat) (*MessengerResponse, error) {
|
|
if err := request.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
doClearHistory := !request.PreserveHistory
|
|
|
|
return m.deactivateChat(request.ID, 0, true, doClearHistory)
|
|
}
|
|
|
|
func (m *Messenger) deactivateChat(chatID string, deactivationClock uint64, shouldBeSynced bool, doClearHistory bool) (*MessengerResponse, error) {
|
|
var response MessengerResponse
|
|
chat, ok := m.allChats.Load(chatID)
|
|
if !ok {
|
|
return nil, ErrChatNotFound
|
|
}
|
|
|
|
// Reset mailserver last request to allow re-fetching messages if joining a chat again
|
|
filters, err := m.filtersForChat(chatID)
|
|
if err != nil && err != ErrNoFiltersForChat {
|
|
return nil, err
|
|
}
|
|
|
|
if m.mailserversDatabase != nil {
|
|
for _, filter := range filters {
|
|
if !filter.Listen || filter.Ephemeral {
|
|
continue
|
|
}
|
|
|
|
err := m.mailserversDatabase.ResetLastRequest(filter.PubsubTopic, filter.ContentTopic.String())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
}
|
|
|
|
if deactivationClock == 0 {
|
|
deactivationClock, _ = chat.NextClockAndTimestamp(m.getTimesource())
|
|
}
|
|
|
|
err = m.persistence.DeactivateChat(chat, deactivationClock, doClearHistory)
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We re-register as our options have changed and we don't want to
|
|
// receive PN from mentions in this chat anymore
|
|
if chat.Public() || chat.ProfileUpdates() {
|
|
err := m.reregisterForPushNotifications()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = m.transport.ClearProcessedMessageIDsCache()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// TODO(samyoul) remove storing of an updated reference pointer?
|
|
m.allChats.Store(chatID, chat)
|
|
|
|
response.AddChat(chat)
|
|
// TODO: Remove filters
|
|
|
|
if shouldBeSynced {
|
|
err := m.syncChatRemoving(context.Background(), chat.ID, m.dispatchMessage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return &response, nil
|
|
}
|
|
|
|
func (m *Messenger) saveChats(chats []*Chat) error {
|
|
err := m.persistence.SaveChats(chats)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, chat := range chats {
|
|
m.allChats.Store(chat.ID, chat)
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
func (m *Messenger) saveChat(chat *Chat) error {
|
|
_, ok := m.allChats.Load(chat.ID)
|
|
if chat.OneToOne() {
|
|
name, identicon, err := generateAliasAndIdenticon(chat.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
chat.Alias = name
|
|
chat.Identicon = identicon
|
|
}
|
|
|
|
// Sync chat if it's a new public, 1-1 or group chat, but not a timeline chat
|
|
if !ok && chat.shouldBeSynced() {
|
|
if err := m.syncChat(context.Background(), chat, m.dispatchMessage); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
err := m.persistence.SaveChat(*chat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// We store the chat has it might not have been in the store in the first place
|
|
m.allChats.Store(chat.ID, chat)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (m *Messenger) Join(chat *Chat) ([]*transport.Filter, error) {
|
|
switch chat.ChatType {
|
|
case ChatTypeOneToOne:
|
|
pk, err := chat.PublicKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
f, err := m.transport.JoinPrivate(pk)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return []*transport.Filter{f}, nil
|
|
case ChatTypePrivateGroupChat:
|
|
members, err := chat.MembersAsPublicKeys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.transport.JoinGroup(members)
|
|
case ChatTypePublic, ChatTypeProfile, ChatTypeTimeline:
|
|
f, err := m.transport.JoinPublic(chat.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []*transport.Filter{f}, nil
|
|
default:
|
|
return nil, errors.New("chat is neither public nor private")
|
|
}
|
|
}
|
|
|
|
// Deprecated: buildProfileChat shouldn't be used
|
|
// and is only left here in case profile chat feature is re-introduced.
|
|
func (m *Messenger) buildProfileChat(id string) *Chat {
|
|
// Return nil to prevent usage of deprecated function
|
|
if deprecation.ChatProfileDeprecated {
|
|
return nil
|
|
}
|
|
|
|
// Create the corresponding profile chat
|
|
profileChatID := buildProfileChatID(id)
|
|
profileChat, ok := m.allChats.Load(profileChatID)
|
|
|
|
if !ok {
|
|
profileChat = CreateProfileChat(id, m.getTimesource())
|
|
}
|
|
|
|
return profileChat
|
|
|
|
}
|
|
|
|
// Deprecated: ensureTimelineChat shouldn't be used
|
|
// and is only left here in case profile chat feature is re-introduced.
|
|
func (m *Messenger) ensureTimelineChat() error {
|
|
// Return error to prevent usage of deprecated function
|
|
if deprecation.ChatProfileDeprecated {
|
|
return errors.New("timeline chats are deprecated")
|
|
}
|
|
|
|
chat, err := m.persistence.Chat(timelineChatID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if chat != nil {
|
|
return nil
|
|
}
|
|
|
|
chat = CreateTimelineChat(m.getTimesource())
|
|
m.allChats.Store(timelineChatID, chat)
|
|
return m.saveChat(chat)
|
|
}
|
|
|
|
// Deprecated: ensureMyOwnProfileChat shouldn't be used
|
|
// and is only left here in case profile chat feature is re-introduced.
|
|
func (m *Messenger) ensureMyOwnProfileChat() error {
|
|
// Return error to prevent usage of deprecated function
|
|
if deprecation.ChatProfileDeprecated {
|
|
return errors.New("profile chats are deprecated")
|
|
}
|
|
|
|
chatID := common.PubkeyToHex(&m.identity.PublicKey)
|
|
_, ok := m.allChats.Load(chatID)
|
|
if ok {
|
|
return nil
|
|
}
|
|
|
|
chat := m.buildProfileChat(chatID)
|
|
|
|
chat.Active = true
|
|
|
|
// Save topics
|
|
_, err := m.Join(chat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return m.saveChat(chat)
|
|
}
|
|
|
|
func (m *Messenger) ClearHistory(request *requests.ClearHistory) (*MessengerResponse, error) {
|
|
if err := request.Validate(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return m.clearHistory(request.ID)
|
|
}
|
|
|
|
func (m *Messenger) clearHistory(id string) (*MessengerResponse, error) {
|
|
chat, ok := m.allChats.Load(id)
|
|
if !ok {
|
|
return nil, ErrChatNotFound
|
|
}
|
|
|
|
clock, _ := chat.NextClockAndTimestamp(m.transport)
|
|
|
|
err := m.persistence.ClearHistory(chat, clock)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if chat.Public() {
|
|
err = m.transport.ClearProcessedMessageIDsCache()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
err = m.syncClearHistory(context.Background(), chat, m.dispatchMessage)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
m.allChats.Store(id, chat)
|
|
|
|
response := &MessengerResponse{}
|
|
response.AddChat(chat)
|
|
return response, nil
|
|
}
|
|
|
|
func (m *Messenger) FetchMessages(request *requests.FetchMessages) error {
|
|
|
|
if err := request.Validate(); err != nil {
|
|
return err
|
|
}
|
|
|
|
id := request.ID
|
|
|
|
chat, ok := m.allChats.Load(id)
|
|
if !ok {
|
|
return ErrChatNotFound
|
|
}
|
|
|
|
_, err := m.fetchMessages(chat.ID, oneMonthDuration)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|