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 ( ChatPreviewFilterTypeCommunity ChatPreviewFilterType = iota 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 } func (m *Messenger) ChatsPreview(filter ChatPreviewFilterType) []*ChatPreview { var chats []*ChatPreview 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 }