From c55659b4f66429219cafe7f51b45598959ac0d61 Mon Sep 17 00:00:00 2001 From: Andrea Maria Piana Date: Wed, 31 Mar 2021 18:23:45 +0200 Subject: [PATCH] Add community notifications (#2160) This commit introduces the following changes: - `local-notifications` require as body an interface complying with `json.Marshaler` - removed unmarshaling of `Notifications` as not used (we only Marshal notifications) - `protocol/messenger.go` creates directly a `Notification` instead of having an intermediate format - add community notifications on request to join - move parsing of text in status-go for notifications --- cmd/statusd/main.go | 5 +- multiaccounts/accounts/database.go | 9 + protocol/chat.go | 27 ++ protocol/chat_test.go | 1 + protocol/common/message.go | 77 +++- protocol/common/message_test.go | 29 ++ protocol/communities/community.go | 61 ++- protocol/communities_messenger_test.go | 8 + protocol/contact.go | 16 + protocol/local_notifications.go | 129 ++++++- protocol/message_handler.go | 11 + protocol/messenger.go | 78 ++-- protocol/messenger_communities.go | 21 +- protocol/messenger_response.go | 51 ++- protocol/protobuf/push_notifications.pb.go | 137 +++---- protocol/protobuf/push_notifications.proto | 1 + protocol/push_notification_test.go | 135 +++++++ protocol/pushnotificationclient/client.go | 13 +- protocol/pushnotificationserver/gorush.go | 3 + protocol/pushnotificationserver/server.go | 18 +- services/ext/service.go | 7 +- services/local-notifications/core.go | 392 ++++---------------- services/local-notifications/core_test.go | 17 +- services/local-notifications/transaction.go | 229 ++++++++++++ services/local-notifications/types.go | 10 + signal/events_shhext.go | 7 +- sqlite/sqlite.go | 5 +- 27 files changed, 995 insertions(+), 502 deletions(-) create mode 100644 services/local-notifications/transaction.go create mode 100644 services/local-notifications/types.go diff --git a/cmd/statusd/main.go b/cmd/statusd/main.go index fe74ec8cf..31d001716 100644 --- a/cmd/statusd/main.go +++ b/cmd/statusd/main.go @@ -31,7 +31,6 @@ import ( "github.com/status-im/status-go/params" "github.com/status-im/status-go/profiling" "github.com/status-im/status-go/protocol" - localnotifications "github.com/status-im/status-go/services/local-notifications" ) const ( @@ -400,13 +399,11 @@ func retrieveMessagesLoop(messenger *protocol.Messenger, tick time.Duration, can for { select { case <-ticker.C: - mr, err := messenger.RetrieveAll() + _, err := messenger.RetrieveAll() if err != nil { logger.Error("failed to retrieve raw messages", "err", err) continue } - - localnotifications.SendMessageNotifications(mr.Notifications) case <-cancel: return } diff --git a/multiaccounts/accounts/database.go b/multiaccounts/accounts/database.go index 81797d14e..9ff1c2178 100644 --- a/multiaccounts/accounts/database.go +++ b/multiaccounts/accounts/database.go @@ -530,6 +530,15 @@ func (db *Database) DeleteAccount(address types.Address) error { return err } +func (db *Database) GetNotificationsEnabled() (bool, error) { + var result bool + err := db.db.QueryRow("SELECT notifications_enabled FROM settings WHERE synthetic_id = 'id'").Scan(&result) + if err == sql.ErrNoRows { + return result, nil + } + return result, err +} + func (db *Database) GetWalletAddress() (rst types.Address, err error) { err = db.db.QueryRow("SELECT address FROM accounts WHERE wallet = 1").Scan(&rst) return diff --git a/protocol/chat.go b/protocol/chat.go index e535b1068..de4982485 100644 --- a/protocol/chat.go +++ b/protocol/chat.go @@ -108,6 +108,14 @@ func (c *Chat) OneToOne() bool { return c.ChatType == ChatTypeOneToOne } +func (c *Chat) CommunityChat() bool { + return c.ChatType == ChatTypeCommunityChat +} + +func (c *Chat) PrivateGroupChat() bool { + return c.ChatType == ChatTypePrivateGroupChat +} + func (c *Chat) CommunityChatID() string { if c.ChatType != ChatTypeCommunityChat { return c.ID @@ -291,6 +299,25 @@ func CreateCommunityChat(orgID, chatID string, orgChat *protobuf.CommunityChat, } } +func (c *Chat) DeepLink() string { + if c.OneToOne() { + return "status-im://p/" + c.ID + } + if c.PrivateGroupChat() { + return "status-im://g/args?a2=" + c.ID + } + + if c.CommunityChat() { + return "status-im://cc/" + c.ID + } + + if c.Public() { + return "status-im://" + c.ID + } + + return "" +} + func CreateCommunityChats(org *communities.Community, timesource common.TimeSource) []*Chat { var chats []*Chat orgID := org.IDString() diff --git a/protocol/chat_test.go b/protocol/chat_test.go index 1261e8cde..8cf390d4a 100644 --- a/protocol/chat_test.go +++ b/protocol/chat_test.go @@ -130,6 +130,7 @@ func (s *ChatTestSuite) TestSerializeJSON() { message.Clock = 1 message.Text = "`some markdown text`" s.Require().NoError(message.PrepareContent()) + message.ParsedTextAst = nil chat.LastMessage = message encodedJSON, err := json.Marshal(chat) diff --git a/protocol/common/message.go b/protocol/common/message.go index 8d8e2ed4f..61fb78903 100644 --- a/protocol/common/message.go +++ b/protocol/common/message.go @@ -110,6 +110,8 @@ type Message struct { RTL bool `json:"rtl"` // ParsedText is the parsed markdown for displaying ParsedText []byte `json:"parsedText,omitempty"` + // ParsedTextAst is the ast of the parsed text + ParsedTextAst *ast.Node `json:"-"` // LineCount is the count of newlines in the message LineCount int `json:"lineCount"` // Base64Image is the converted base64 image @@ -323,12 +325,52 @@ func (m *Message) parseAudio() error { } // implement interface of https://github.com/status-im/markdown/blob/b9fe921681227b1dace4b56364e15edb3b698308/ast/node.go#L701 -type NodeVisitor struct { +type SimplifiedTextVisitor struct { + text string + canonicalNames map[string]string +} + +func (v *SimplifiedTextVisitor) Visit(node ast.Node, entering bool) ast.WalkStatus { + // only on entering we fetch, otherwise we go on + if !entering { + return ast.GoToNext + } + + switch n := node.(type) { + case *ast.Mention: + literal := string(n.Literal) + canonicalName, ok := v.canonicalNames[literal] + if ok { + v.text += canonicalName + } else { + v.text += literal + } + case *ast.Link: + destination := string(n.Destination) + v.text += destination + default: + var literal string + + leaf := node.AsLeaf() + container := node.AsContainer() + if leaf != nil { + literal = string(leaf.Literal) + } else if container != nil { + literal = string(container.Literal) + } + v.text += literal + } + + return ast.GoToNext +} + +// implement interface of https://github.com/status-im/markdown/blob/b9fe921681227b1dace4b56364e15edb3b698308/ast/node.go#L701 +type MentionsAndLinksVisitor struct { mentions []string links []string } -func (v *NodeVisitor) Visit(node ast.Node, entering bool) ast.WalkStatus { +func (v *MentionsAndLinksVisitor) Visit(node ast.Node, entering bool) ast.WalkStatus { // only on entering we fetch, otherwise we go on if !entering { return ast.GoToNext @@ -344,7 +386,7 @@ func (v *NodeVisitor) Visit(node ast.Node, entering bool) ast.WalkStatus { } func extractMentionsAndLinks(parsedText ast.Node) ([]string, []string) { - visitor := &NodeVisitor{} + visitor := &MentionsAndLinksVisitor{} ast.Walk(parsedText, visitor) return visitor.mentions, visitor.links } @@ -358,6 +400,7 @@ func (m *Message) PrepareContent() error { if err != nil { return err } + m.ParsedTextAst = &parsedText m.ParsedText = jsonParsedText m.LineCount = strings.Count(m.Text, "\n") m.RTL = isRTL(m.Text) @@ -367,6 +410,34 @@ func (m *Message) PrepareContent() error { return m.parseAudio() } +// GetSimplifiedText returns a the text stripped of all the markdown and with mentions +// replaced by canonical names +func (m *Message) GetSimplifiedText(canonicalNames map[string]string) (string, error) { + + if m.ContentType == protobuf.ChatMessage_AUDIO { + return "Audio", nil + } + if m.ContentType == protobuf.ChatMessage_STICKER { + return "Sticker", nil + } + if m.ContentType == protobuf.ChatMessage_IMAGE { + return "Image", nil + } + if m.ContentType == protobuf.ChatMessage_COMMUNITY { + return "Community", nil + } + + if m.ParsedTextAst == nil { + err := m.PrepareContent() + if err != nil { + return "", err + } + } + visitor := &SimplifiedTextVisitor{canonicalNames: canonicalNames} + ast.Walk(*m.ParsedTextAst, visitor) + return visitor.text, nil +} + func getAudioMessageMIME(i *protobuf.AudioMessage) (string, error) { switch i.Type { case protobuf.AudioMessage_AAC: diff --git a/protocol/common/message_test.go b/protocol/common/message_test.go index 8d845c035..ae1020bb5 100644 --- a/protocol/common/message_test.go +++ b/protocol/common/message_test.go @@ -98,3 +98,32 @@ func TestPrepareContentLinks(t *testing.T) { require.Equal(t, message.Links[0], link1) require.Equal(t, message.Links[1], link2) } + +func TestPrepareSimplifiedText(t *testing.T) { + canonicalName1 := "canonical-name-1" + canonicalName2 := "canonical-name-2" + + message := &Message{} + pk1, err := crypto.GenerateKey() + require.NoError(t, err) + pk1String := types.EncodeHex(crypto.FromECDSAPub(&pk1.PublicKey)) + + pk2, err := crypto.GenerateKey() + require.NoError(t, err) + pk2String := types.EncodeHex(crypto.FromECDSAPub(&pk2.PublicKey)) + + message.Text = "hey @" + pk1String + " @" + pk2String + + require.NoError(t, message.PrepareContent()) + require.Len(t, message.Mentions, 2) + require.Equal(t, message.Mentions[0], pk1String) + require.Equal(t, message.Mentions[1], pk2String) + + canonicalNames := make(map[string]string) + canonicalNames[pk1String] = canonicalName1 + canonicalNames[pk2String] = canonicalName2 + + simplifiedText, err := message.GetSimplifiedText(canonicalNames) + require.NoError(t, err) + require.Equal(t, "hey "+canonicalName1+" "+canonicalName2, simplifiedText) +} diff --git a/protocol/communities/community.go b/protocol/communities/community.go index 51d6957c6..d96098deb 100644 --- a/protocol/communities/community.go +++ b/protocol/communities/community.go @@ -116,7 +116,7 @@ func (o *Community) MarshalJSON() ([]byte, error) { communityItem.Members = o.config.CommunityDescription.Members communityItem.Permissions = o.config.CommunityDescription.Permissions if o.config.CommunityDescription.Identity != nil { - communityItem.Name = o.config.CommunityDescription.Identity.DisplayName + communityItem.Name = o.Name() communityItem.Color = o.config.CommunityDescription.Identity.Color communityItem.Description = o.config.CommunityDescription.Identity.Description for t, i := range o.config.CommunityDescription.Identity.Images { @@ -132,6 +132,10 @@ func (o *Community) MarshalJSON() ([]byte, error) { return json.Marshal(communityItem) } +func (o *Community) Name() string { + return o.config.CommunityDescription.Identity.DisplayName +} + func (o *Community) initialize() { if o.config.CommunityDescription == nil { o.config.CommunityDescription = &protobuf.CommunityDescription{} @@ -339,19 +343,22 @@ func (o *Community) isBanned(pk *ecdsa.PublicKey) bool { return false } -func (o *Community) hasPermission(pk *ecdsa.PublicKey, role protobuf.CommunityMember_Roles) bool { +func (o *Community) hasMemberPermission(member *protobuf.CommunityMember, permissions map[protobuf.CommunityMember_Roles]bool) bool { + for _, r := range member.Roles { + if permissions[r] { + return true + } + } + return false +} + +func (o *Community) hasPermission(pk *ecdsa.PublicKey, roles map[protobuf.CommunityMember_Roles]bool) bool { member := o.getMember(pk) if member == nil { return false } - for _, r := range member.Roles { - if r == role { - return true - } - } - - return false + return o.hasMemberPermission(member, roles) } func (o *Community) HasMember(pk *ecdsa.PublicKey) bool { @@ -605,6 +612,22 @@ func (o *Community) IsAdmin() bool { return o.config.PrivateKey != nil } +func (o *Community) IsMemberAdmin(publicKey *ecdsa.PublicKey) bool { + return o.hasPermission(publicKey, adminRolePermissions()) +} + +func canManageUsersRolePermissions() map[protobuf.CommunityMember_Roles]bool { + roles := adminRolePermissions() + roles[protobuf.CommunityMember_ROLE_MANAGE_USERS] = true + return roles +} + +func adminRolePermissions() map[protobuf.CommunityMember_Roles]bool { + roles := make(map[protobuf.CommunityMember_Roles]bool) + roles[protobuf.CommunityMember_ROLE_ALL] = true + return roles +} + func (o *Community) validateRequestToJoinWithChatID(request *protobuf.CommunityRequestToJoin) error { chat, ok := o.config.CommunityDescription.Chats[request.ChatId] @@ -901,7 +924,8 @@ func (o *Community) CanManageUsers(pk *ecdsa.PublicKey) bool { return false } - return o.hasPermission(pk, protobuf.CommunityMember_ROLE_ALL) || o.hasPermission(pk, protobuf.CommunityMember_ROLE_MANAGE_USERS) + roles := canManageUsersRolePermissions() + return o.hasPermission(pk, roles) } func (o *Community) isMember() bool { @@ -933,6 +957,23 @@ func (o *Community) nextClock() uint64 { return o.config.CommunityDescription.Clock + 1 } +func (o *Community) CanManageUsersPublicKeys() ([]*ecdsa.PublicKey, error) { + var response []*ecdsa.PublicKey + roles := canManageUsersRolePermissions() + for pkString, member := range o.config.CommunityDescription.Members { + if o.hasMemberPermission(member, roles) { + pk, err := common.HexToPubkey(pkString) + if err != nil { + return nil, err + } + + response = append(response, pk) + } + + } + return response, nil +} + func emptyCommunityChanges() *CommunityChanges { return &CommunityChanges{ MembersAdded: make(map[string]*protobuf.CommunityMember), diff --git a/protocol/communities_messenger_test.go b/protocol/communities_messenger_test.go index 7e47c989b..9d93972f4 100644 --- a/protocol/communities_messenger_test.go +++ b/protocol/communities_messenger_test.go @@ -309,6 +309,8 @@ func (s *MessengerCommunitiesSuite) TestInviteUsersToCommunity() { s.Require().NoError(err) s.Require().NotNil(response) s.Require().Len(response.Communities(), 1) + s.Require().True(response.Communities()[0].HasMember(&s.bob.identity.PublicKey)) + s.Require().True(response.Communities()[0].IsMemberAdmin(&s.bob.identity.PublicKey)) community := response.Communities()[0] @@ -787,6 +789,12 @@ func (s *MessengerCommunitiesSuite) TestRequestAccessAgain() { s.Require().Equal(requestToJoin1.ID, requestToJoin2.ID) + // Check that a notification is been added to messenger + + notifications := response.Notifications() + s.Require().Len(notifications, 1) + s.Require().NotEqual(notifications[0].ID.Hex(), "0x0000000000000000000000000000000000000000000000000000000000000000") + // Accept request acceptRequestToJoin := &requests.AcceptRequestToJoinCommunity{ID: requestToJoin1.ID} diff --git a/protocol/contact.go b/protocol/contact.go index 269a51969..1c392f239 100644 --- a/protocol/contact.go +++ b/protocol/contact.go @@ -26,6 +26,22 @@ type ContactDeviceInfo struct { FCMToken string `json:"fcmToken"` } +func (c *Contact) CanonicalName() string { + if c.LocalNickname != "" { + return c.LocalNickname + } + + if c.ENSVerified { + return c.Name + } + + return c.Alias +} + +func (c *Contact) CanonicalImage() string { + return c.Identicon +} + // Contact has information about a "Contact". A contact is not necessarily one // that we added or added us, that's based on SystemTags. type Contact struct { diff --git a/protocol/local_notifications.go b/protocol/local_notifications.go index 94e32d464..7eed2b15d 100644 --- a/protocol/local_notifications.go +++ b/protocol/local_notifications.go @@ -1,9 +1,128 @@ package protocol -import "github.com/status-im/status-go/protocol/common" +import ( + "crypto/ecdsa" + "encoding/json" -type MessageNotificationBody struct { - Message *common.Message `json:"message"` - Contact *Contact `json:"contact"` - Chat *Chat `json:"chat"` + gethcommon "github.com/ethereum/go-ethereum/common" + "github.com/status-im/status-go/protocol/common" + "github.com/status-im/status-go/protocol/communities" + localnotifications "github.com/status-im/status-go/services/local-notifications" +) + +type NotificationBody struct { + Message *common.Message `json:"message"` + Contact *Contact `json:"contact"` + Chat *Chat `json:"chat"` + Community *communities.Community `json:"community"` +} + +func showMessageNotification(publicKey ecdsa.PublicKey, message *common.Message, chat *Chat, responseTo *common.Message) bool { + if chat != nil && (chat.OneToOne() || chat.PrivateGroupChat()) { + return true + } + + publicKeyString := common.PubkeyToHex(&publicKey) + mentioned := false + for _, mention := range message.Mentions { + if publicKeyString == mention { + mentioned = true + } + } + + if mentioned { + return true + } + + if responseTo != nil { + return responseTo.From == publicKeyString + } + + return false +} + +func (n NotificationBody) MarshalJSON() ([]byte, error) { + type Alias NotificationBody + item := struct{ *Alias }{Alias: (*Alias)(&n)} + return json.Marshal(item) +} + +func NewMessageNotification(id string, message *common.Message, chat *Chat, contact *Contact, contacts map[string]*Contact) (*localnotifications.Notification, error) { + body := &NotificationBody{ + Message: message, + Chat: chat, + Contact: contact, + } + + return body.toMessageNotification(id, contacts) +} + +func NewCommunityRequestToJoinNotification(id string, community *communities.Community, contact *Contact) *localnotifications.Notification { + body := &NotificationBody{ + Community: community, + Contact: contact, + } + + return body.toCommunityRequestToJoinNotification(id) +} + +func (n NotificationBody) toMessageNotification(id string, contacts map[string]*Contact) (*localnotifications.Notification, error) { + var title string + if n.Chat.PrivateGroupChat() || n.Chat.Public() || n.Chat.CommunityChat() { + title = n.Chat.Name + } else if n.Chat.OneToOne() { + title = n.Contact.CanonicalName() + + } + + canonicalNames := make(map[string]string) + for _, id := range n.Message.Mentions { + contact, ok := contacts[id] + if !ok { + var err error + contact, err = buildContactFromPkString(id) + if err != nil { + return nil, err + } + } + canonicalNames[id] = contact.CanonicalName() + } + + simplifiedText, err := n.Message.GetSimplifiedText(canonicalNames) + if err != nil { + return nil, err + } + + return &localnotifications.Notification{ + Body: n, + ID: gethcommon.HexToHash(id), + BodyType: localnotifications.TypeMessage, + Category: localnotifications.CategoryMessage, + Deeplink: n.Chat.DeepLink(), + Title: title, + Message: simplifiedText, + IsConversation: true, + IsGroupConversation: true, + Author: localnotifications.NotificationAuthor{ + Name: n.Contact.CanonicalName(), + Icon: n.Contact.CanonicalImage(), + ID: n.Contact.ID, + }, + Timestamp: n.Message.WhisperTimestamp, + ConversationID: n.Chat.ID, + Image: "", + }, nil +} + +func (n NotificationBody) toCommunityRequestToJoinNotification(id string) *localnotifications.Notification { + return &localnotifications.Notification{ + ID: gethcommon.HexToHash(id), + Body: n, + Title: n.Contact.CanonicalName() + " wants to join " + n.Community.Name(), + Message: n.Contact.CanonicalName() + " wants to join message " + n.Community.Name(), + BodyType: localnotifications.TypeMessage, + Category: localnotifications.CategoryCommunityRequestToJoin, + Deeplink: "status-im://cr/" + n.Community.IDString(), + Image: "", + } } diff --git a/protocol/message_handler.go b/protocol/message_handler.go index bc4c2d7dd..7c7ab9891 100644 --- a/protocol/message_handler.go +++ b/protocol/message_handler.go @@ -415,6 +415,17 @@ func (m *MessageHandler) HandleCommunityRequestToJoin(state *ReceivedMessageStat state.Response.RequestsToJoinCommunity = append(state.Response.RequestsToJoinCommunity, requestToJoin) + community, err := m.communitiesManager.GetByID(requestToJoinProto.CommunityId) + if err != nil { + return err + } + + contactID := contactIDFromPublicKey(signer) + + contact := state.AllContacts[contactID] + + state.Response.AddNotification(NewCommunityRequestToJoinNotification(requestToJoin.ID.String(), community, contact)) + return nil } diff --git a/protocol/messenger.go b/protocol/messenger.go index 74dbfdeb5..a01caa9d2 100644 --- a/protocol/messenger.go +++ b/protocol/messenger.go @@ -21,10 +21,12 @@ import ( "github.com/davecgh/go-spew/spew" "github.com/golang/protobuf/proto" + "github.com/status-im/status-go/appdatabase" "github.com/status-im/status-go/eth-node/crypto" "github.com/status-im/status-go/eth-node/types" userimage "github.com/status-im/status-go/images" "github.com/status-im/status-go/multiaccounts" + "github.com/status-im/status-go/multiaccounts/accounts" "github.com/status-im/status-go/protocol/audio" "github.com/status-im/status-go/protocol/common" "github.com/status-im/status-go/protocol/communities" @@ -99,6 +101,7 @@ type Messenger struct { mailserver []byte database *sql.DB multiAccounts *multiaccounts.Database + settings *accounts.Database account *multiaccounts.Account mailserversDatabase *mailservers.Database quit chan struct{} @@ -186,7 +189,7 @@ func NewMessenger( if c.db == nil { logger.Info("opening a database", zap.String("dbPath", c.dbConfig.dbPath)) var err error - database, err = sqlite.Open(c.dbConfig.dbPath, c.dbConfig.dbKey) + database, err = appdatabase.InitializeDB(c.dbConfig.dbPath, c.dbConfig.dbKey) if err != nil { return nil, errors.Wrap(err, "failed to initialize database from the db config") } @@ -304,6 +307,7 @@ func NewMessenger( verifyTransactionClient: c.verifyTransactionClient, database: database, multiAccounts: c.multiAccount, + settings: accounts.NewDB(database), mailserversDatabase: c.mailserversDatabase, account: c.account, quit: make(chan struct{}), @@ -2455,7 +2459,7 @@ func (m *Messenger) markDeliveredMessages(acks [][]byte) { } } -// addNewMessageNotification takes a common.Message and generates a new MessageNotificationBody and appends it to the +// addNewMessageNotification takes a common.Message and generates a new NotificationBody and appends it to the // []Response.Notifications if the message is m.New func (r *ReceivedMessageState) addNewMessageNotification(publicKey ecdsa.PublicKey, m *common.Message, responseTo *common.Message) error { if !m.New { @@ -2469,17 +2473,14 @@ func (r *ReceivedMessageState) addNewMessageNotification(publicKey ecdsa.PublicK contactID := contactIDFromPublicKey(pubKey) chat := r.AllChats[m.LocalChatID] - notification := MessageNotificationBody{ - Message: m, - Contact: r.AllContacts[contactID], - Chat: chat, - } + contact := r.AllContacts[contactID] - if showNotification(publicKey, notification, responseTo) { - r.Response.Notifications = append( - r.Response.Notifications, - notification, - ) + if showMessageNotification(publicKey, m, chat, responseTo) { + notification, err := NewMessageNotification(m.ID, m, chat, contact, r.AllContacts) + if err != nil { + return err + } + r.Response.AddNotification(notification) } return nil @@ -3009,13 +3010,19 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte } messageState.Response.Messages = messagesWithResponses + notificationsEnabled, err := m.settings.GetNotificationsEnabled() + if err != nil { + return nil, err + } for _, message := range messageState.Response.Messages { if _, ok := newMessagesIds[message.ID]; ok { message.New = true - // Create notification body to be eventually passed to `localnotifications.SendMessageNotifications()` - if err = messageState.addNewMessageNotification(m.identity.PublicKey, message, messagesByID[message.ResponseTo]); err != nil { - return nil, err + if notificationsEnabled { + // Create notification body to be eventually passed to `localnotifications.SendMessageNotifications()` + if err = messageState.addNewMessageNotification(m.identity.PublicKey, message, messagesByID[message.ResponseTo]); err != nil { + return nil, err + } } } } @@ -3026,30 +3033,6 @@ func (m *Messenger) handleRetrievedMessages(chatWithMessages map[transport.Filte return messageState.Response, nil } -func showNotification(publicKey ecdsa.PublicKey, n MessageNotificationBody, responseTo *common.Message) bool { - if n.Chat != nil && n.Chat.ChatType == ChatTypeOneToOne { - return true - } - - publicKeyString := common.PubkeyToHex(&publicKey) - mentioned := false - for _, mention := range n.Message.Mentions { - if publicKeyString == mention { - mentioned = true - } - } - - if mentioned { - return true - } - - if responseTo != nil { - return responseTo.From == publicKeyString - } - - return false -} - // SetMailserver sets the currently used mailserver func (m *Messenger) SetMailserver(peer []byte) { m.mailserver = peer @@ -3941,11 +3924,18 @@ func (m *Messenger) ValidateTransactions(ctx context.Context, addresses []types. if err != nil { return nil, err } - response.Notifications = append(response.Notifications, MessageNotificationBody{ - Message: message, - Contact: contact, - Chat: chat, - }) + + notificationsEnabled, err := m.settings.GetNotificationsEnabled() + if err != nil { + return nil, err + } + if notificationsEnabled { + notification, err := NewMessageNotification(message.ID, message, chat, contact, m.allContacts) + if err != nil { + return nil, err + } + response.AddNotification(notification) + } } diff --git a/protocol/messenger_communities.go b/protocol/messenger_communities.go index 0d7fec7dc..f4a61a8eb 100644 --- a/protocol/messenger_communities.go +++ b/protocol/messenger_communities.go @@ -201,6 +201,25 @@ func (m *Messenger) RequestToJoinCommunity(request *requests.RequestToJoinCommun response := &MessengerResponse{RequestsToJoinCommunity: []*communities.RequestToJoin{requestToJoin}} response.AddCommunity(community) + // We send a push notification in the background + go func() { + if m.pushNotificationClient != nil { + pks, err := community.CanManageUsersPublicKeys() + if err != nil { + m.logger.Error("failed to get pks", zap.Error(err)) + return + } + for _, publicKey := range pks { + pkString := common.PubkeyToHex(publicKey) + _, err = m.pushNotificationClient.SendNotification(publicKey, nil, requestToJoin.ID, pkString, protobuf.PushNotification_REQUEST_TO_JOIN_COMMUNITY) + if err != nil { + m.logger.Error("error sending notification", zap.Error(err)) + return + } + } + } + }() + return response, nil } @@ -314,7 +333,7 @@ func (m *Messenger) CreateCommunity(request *requests.CreateCommunity) (*Messeng } description.Members = make(map[string]*protobuf.CommunityMember) - description.Members[common.PubkeyToHex(&m.identity.PublicKey)] = &protobuf.CommunityMember{} + description.Members[common.PubkeyToHex(&m.identity.PublicKey)] = &protobuf.CommunityMember{Roles: []protobuf.CommunityMember_Roles{protobuf.CommunityMember_ROLE_ALL}} community, err := m.communitiesManager.CreateCommunity(description) if err != nil { diff --git a/protocol/messenger_response.go b/protocol/messenger_response.go index b8e5e57f7..f17d4da66 100644 --- a/protocol/messenger_response.go +++ b/protocol/messenger_response.go @@ -7,6 +7,7 @@ import ( "github.com/status-im/status-go/protocol/communities" "github.com/status-im/status-go/protocol/encryption/multidevice" "github.com/status-im/status-go/protocol/transport" + localnotifications "github.com/status-im/status-go/services/local-notifications" "github.com/status-im/status-go/services/mailservers" ) @@ -23,12 +24,13 @@ type MessengerResponse struct { Mailservers []mailservers.Mailserver MailserverTopics []mailservers.MailserverTopic MailserverRanges []mailservers.ChatRequestRange - // Notifications a list of MessageNotificationBody derived from received messages that are useful to notify the user about - Notifications []MessageNotificationBody - chats map[string]*Chat - removedChats map[string]bool - communities map[string]*communities.Community + // notifications a list of notifications derived from messenger events + // that are useful to notify the user about + notifications map[string]*localnotifications.Notification + chats map[string]*Chat + removedChats map[string]bool + communities map[string]*communities.Community } func (r *MessengerResponse) MarshalJSON() ([]byte, error) { @@ -47,9 +49,10 @@ func (r *MessengerResponse) MarshalJSON() ([]byte, error) { Mailservers []mailservers.Mailserver `json:"mailservers,omitempty"` MailserverTopics []mailservers.MailserverTopic `json:"mailserverTopics,omitempty"` MailserverRanges []mailservers.ChatRequestRange `json:"mailserverRanges,omitempty"` - // Notifications a list of MessageNotificationBody derived from received messages that are useful to notify the user about - Notifications []MessageNotificationBody `json:"notifications"` - Communities []*communities.Community `json:"communities,omitempty"` + // Notifications a list of notifications derived from messenger events + // that are useful to notify the user about + Notifications []*localnotifications.Notification `json:"notifications"` + Communities []*communities.Community `json:"communities,omitempty"` }{ Messages: r.Messages, Contacts: r.Contacts, @@ -63,9 +66,9 @@ func (r *MessengerResponse) MarshalJSON() ([]byte, error) { Mailservers: r.Mailservers, MailserverTopics: r.MailserverTopics, MailserverRanges: r.MailserverRanges, - Notifications: r.Notifications, } + responseItem.Notifications = r.Notifications() responseItem.Chats = r.Chats() responseItem.Communities = r.Communities() responseItem.RemovedChats = r.RemovedChats() @@ -97,6 +100,14 @@ func (r *MessengerResponse) Communities() []*communities.Community { return communities } +func (r *MessengerResponse) Notifications() []*localnotifications.Notification { + var notifications []*localnotifications.Notification + for _, n := range r.notifications { + notifications = append(notifications, n) + } + return notifications +} + func (r *MessengerResponse) IsEmpty() bool { return len(r.chats)+ len(r.Messages)+ @@ -112,7 +123,7 @@ func (r *MessengerResponse) IsEmpty() bool { len(r.MailserverTopics)+ len(r.Mailservers)+ len(r.MailserverRanges)+ - len(r.Notifications)+ + len(r.notifications)+ len(r.RequestsToJoinCommunity) == 0 } @@ -127,7 +138,6 @@ func (r *MessengerResponse) Merge(response *MessengerResponse) error { len(response.Mailservers)+ len(response.MailserverTopics)+ len(response.MailserverRanges)+ - len(response.Notifications)+ len(response.EmojiReactions)+ len(response.CommunityChanges) != 0 { return ErrNotImplemented @@ -135,6 +145,7 @@ func (r *MessengerResponse) Merge(response *MessengerResponse) error { r.AddChats(response.Chats()) r.AddRemovedChats(response.RemovedChats()) + r.AddNotifications(response.Notifications()) r.overrideMessages(response.Messages) r.overrideFilters(response.Filters) r.overrideRemovedFilters(response.Filters) @@ -219,6 +230,24 @@ func (r *MessengerResponse) AddChats(chats []*Chat) { } } +func (r *MessengerResponse) AddNotification(n *localnotifications.Notification) { + if r.notifications == nil { + r.notifications = make(map[string]*localnotifications.Notification) + } + + r.notifications[n.ID.String()] = n +} + +func (r *MessengerResponse) ClearNotifications() { + r.notifications = nil +} + +func (r *MessengerResponse) AddNotifications(notifications []*localnotifications.Notification) { + for _, c := range notifications { + r.AddNotification(c) + } +} + func (r *MessengerResponse) AddRemovedChats(chats []string) { for _, c := range chats { r.AddRemovedChat(c) diff --git a/protocol/protobuf/push_notifications.pb.go b/protocol/protobuf/push_notifications.pb.go index 18a661803..eddd9c09f 100644 --- a/protocol/protobuf/push_notifications.pb.go +++ b/protocol/protobuf/push_notifications.pb.go @@ -88,18 +88,21 @@ const ( PushNotification_UNKNOWN_PUSH_NOTIFICATION_TYPE PushNotification_PushNotificationType = 0 PushNotification_MESSAGE PushNotification_PushNotificationType = 1 PushNotification_MENTION PushNotification_PushNotificationType = 2 + PushNotification_REQUEST_TO_JOIN_COMMUNITY PushNotification_PushNotificationType = 3 ) var PushNotification_PushNotificationType_name = map[int32]string{ 0: "UNKNOWN_PUSH_NOTIFICATION_TYPE", 1: "MESSAGE", 2: "MENTION", + 3: "REQUEST_TO_JOIN_COMMUNITY", } var PushNotification_PushNotificationType_value = map[string]int32{ "UNKNOWN_PUSH_NOTIFICATION_TYPE": 0, "MESSAGE": 1, "MENTION": 2, + "REQUEST_TO_JOIN_COMMUNITY": 3, } func (x PushNotification_PushNotificationType) String() string { @@ -833,70 +836,72 @@ func init() { } var fileDescriptor_200acd86044eaa5d = []byte{ - // 1038 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0xdd, 0x6e, 0xe3, 0x44, - 0x14, 0xc6, 0x4e, 0xda, 0x24, 0x27, 0x69, 0x9a, 0x0e, 0x6d, 0x6a, 0x0a, 0x5d, 0x82, 0x01, 0x11, - 0xf5, 0xa2, 0xa0, 0x22, 0xb1, 0x2b, 0xf6, 0x86, 0x90, 0xba, 0x5d, 0xab, 0x8d, 0x1d, 0x26, 0x2e, - 0xab, 0x95, 0x90, 0x2c, 0xd7, 0x9e, 0xb6, 0x56, 0x53, 0x8f, 0xf1, 0x4c, 0x8a, 0x72, 0xc7, 0x03, - 0x70, 0xc3, 0x2d, 0x57, 0x3c, 0xc3, 0xbe, 0x12, 0x2f, 0x82, 0x3c, 0x1e, 0xa7, 0x6e, 0x93, 0xa6, - 0x45, 0xe2, 0x2a, 0x39, 0xdf, 0xf9, 0x99, 0xf3, 0xfb, 0x19, 0xb4, 0x78, 0xc2, 0xae, 0xdc, 0x88, - 0xf2, 0xf0, 0x22, 0xf4, 0x3d, 0x1e, 0xd2, 0x88, 0xed, 0xc7, 0x09, 0xe5, 0x14, 0x55, 0xc5, 0xcf, - 0xf9, 0xe4, 0x62, 0xe7, 0x43, 0xff, 0xca, 0xe3, 0x6e, 0x18, 0x90, 0x88, 0x87, 0x7c, 0x9a, 0xa9, - 0xf5, 0xbf, 0x57, 0xe0, 0x93, 0xe1, 0x84, 0x5d, 0x59, 0x05, 0x57, 0x4c, 0x2e, 0x43, 0xc6, 0x13, - 0xf1, 0x1f, 0xd9, 0x00, 0x9c, 0x5e, 0x93, 0xc8, 0xe5, 0xd3, 0x98, 0x68, 0x4a, 0x47, 0xe9, 0x36, - 0x0f, 0xbe, 0xd9, 0xcf, 0x83, 0xee, 0x2f, 0xf3, 0xdd, 0x77, 0x52, 0x47, 0x67, 0x1a, 0x13, 0x5c, - 0xe3, 0xf9, 0x5f, 0xf4, 0x19, 0x34, 0x02, 0x72, 0x1b, 0xfa, 0xc4, 0x15, 0x98, 0xa6, 0x76, 0x94, - 0x6e, 0x0d, 0xd7, 0x33, 0x4c, 0x78, 0xa0, 0xaf, 0x60, 0x3d, 0x8c, 0x18, 0xf7, 0xc6, 0x63, 0x11, - 0xc7, 0x0d, 0x03, 0xad, 0x24, 0xac, 0x9a, 0x45, 0xd8, 0x0c, 0xd2, 0x58, 0x9e, 0xef, 0x13, 0xc6, - 0x64, 0xac, 0x72, 0x16, 0x2b, 0xc3, 0xb2, 0x58, 0x1a, 0x54, 0x48, 0xe4, 0x9d, 0x8f, 0x49, 0xa0, - 0xad, 0x74, 0x94, 0x6e, 0x15, 0xe7, 0x62, 0xaa, 0xb9, 0x25, 0x09, 0x0b, 0x69, 0xa4, 0xad, 0x76, - 0x94, 0x6e, 0x19, 0xe7, 0x22, 0xea, 0x42, 0xcb, 0x1b, 0x8f, 0xe9, 0x6f, 0x24, 0x70, 0xaf, 0xc9, - 0xd4, 0x1d, 0x87, 0x8c, 0x6b, 0x95, 0x4e, 0xa9, 0xdb, 0xc0, 0x4d, 0x89, 0x9f, 0x90, 0xe9, 0x69, - 0xc8, 0x38, 0xda, 0x83, 0x8d, 0xf3, 0x31, 0xf5, 0xaf, 0x49, 0xe0, 0x8a, 0xee, 0x0a, 0xd3, 0xaa, - 0x30, 0x5d, 0x97, 0x8a, 0xfe, 0x95, 0xc7, 0x85, 0xed, 0x0b, 0x80, 0x49, 0x94, 0x88, 0xfe, 0x90, - 0x44, 0xab, 0x89, 0x64, 0x0a, 0x08, 0xda, 0x84, 0x95, 0xcb, 0xc4, 0x8b, 0xb8, 0x06, 0x1d, 0xa5, - 0xdb, 0xc0, 0x99, 0x80, 0x5e, 0x82, 0x26, 0xde, 0x74, 0x2f, 0x12, 0x7a, 0xe3, 0xfa, 0x34, 0xe2, - 0x9e, 0xcf, 0x99, 0x4b, 0xa3, 0xf1, 0x54, 0xab, 0x8b, 0x18, 0x5b, 0x42, 0x7f, 0x94, 0xd0, 0x9b, - 0xbe, 0xd4, 0xda, 0xd1, 0x78, 0x8a, 0x3e, 0x86, 0x9a, 0x17, 0x47, 0x2e, 0xa7, 0x71, 0xe8, 0x6b, - 0x0d, 0xd1, 0x98, 0xaa, 0x17, 0x47, 0x4e, 0x2a, 0xa3, 0x2f, 0xa1, 0x29, 0xd2, 0x73, 0x6f, 0xd2, - 0x6d, 0xa0, 0x11, 0xd3, 0xd6, 0x44, 0xac, 0x35, 0x81, 0x0e, 0x24, 0x88, 0x5e, 0xc3, 0x4e, 0xde, - 0x88, 0xdc, 0xb0, 0x50, 0x67, 0x53, 0xd4, 0xb9, 0x2d, 0x2d, 0x72, 0xa7, 0xbc, 0x5e, 0xfd, 0x08, - 0x6a, 0xb3, 0x05, 0x40, 0x6d, 0x40, 0x67, 0xd6, 0x89, 0x65, 0xbf, 0xb5, 0x5c, 0xc7, 0x3e, 0x31, - 0x2c, 0xd7, 0x79, 0x37, 0x34, 0x5a, 0x1f, 0xa0, 0x35, 0xa8, 0xf5, 0x86, 0x12, 0x6b, 0x29, 0x08, - 0x41, 0xf3, 0xc8, 0xc4, 0xc6, 0x8f, 0xbd, 0x91, 0x21, 0x31, 0x55, 0x7f, 0xaf, 0xc2, 0x17, 0xcb, - 0xd6, 0x0c, 0x13, 0x16, 0xd3, 0x88, 0x91, 0x74, 0xa0, 0x6c, 0x22, 0x46, 0x2f, 0xf6, 0xb4, 0x8a, - 0x73, 0x11, 0x59, 0xb0, 0x42, 0x92, 0x84, 0x26, 0x62, 0xd9, 0x9a, 0x07, 0xaf, 0x9e, 0xb7, 0xbf, - 0x79, 0xe0, 0x7d, 0x23, 0xf5, 0x15, 0x7b, 0x9c, 0x85, 0x41, 0xbb, 0x00, 0x09, 0xf9, 0x75, 0x42, - 0x18, 0xcf, 0x77, 0xb3, 0x81, 0x6b, 0x12, 0x31, 0x03, 0xfd, 0x77, 0x05, 0x6a, 0x33, 0x9f, 0x62, - 0xe9, 0x06, 0xc6, 0x36, 0xce, 0x4b, 0xdf, 0x82, 0x8d, 0x41, 0xef, 0xf4, 0xc8, 0xc6, 0x03, 0xe3, - 0xd0, 0x1d, 0x18, 0xa3, 0x51, 0xef, 0xd8, 0x68, 0x29, 0x68, 0x13, 0x5a, 0x3f, 0x1b, 0x78, 0x64, - 0xda, 0x96, 0x3b, 0x30, 0x47, 0x83, 0x9e, 0xd3, 0x7f, 0xd3, 0x52, 0xd1, 0x0e, 0xb4, 0xcf, 0xac, - 0xd1, 0xd9, 0x70, 0x68, 0x63, 0xc7, 0x38, 0x2c, 0xf6, 0xb0, 0x94, 0x36, 0xcd, 0xb4, 0x1c, 0x03, - 0x5b, 0xbd, 0xd3, 0xec, 0x85, 0x56, 0x59, 0x7f, 0xaf, 0x80, 0x26, 0xd7, 0xa1, 0x4f, 0x03, 0xd2, - 0x0b, 0x6e, 0x49, 0xc2, 0x43, 0x46, 0xd2, 0x31, 0xa2, 0x77, 0xd0, 0x9e, 0xe3, 0x0b, 0x37, 0x8c, - 0x2e, 0xa8, 0xa6, 0x74, 0x4a, 0xdd, 0xfa, 0xc1, 0xe7, 0x8f, 0xf7, 0xe7, 0xa7, 0x09, 0x49, 0xa6, - 0x66, 0x74, 0x41, 0xf1, 0x66, 0xfc, 0x40, 0x95, 0xa2, 0xe8, 0x35, 0xac, 0xdd, 0xa3, 0x19, 0xd1, - 0xf1, 0xfa, 0x41, 0xfb, 0x2e, 0x62, 0xba, 0x1f, 0xa6, 0xd4, 0xe2, 0x86, 0x5f, 0x90, 0xf4, 0x57, - 0xb0, 0xb5, 0xf0, 0x3d, 0xf4, 0x29, 0xd4, 0xe3, 0xc9, 0xf9, 0x38, 0xf4, 0xd3, 0x7b, 0x64, 0x22, - 0xcb, 0x06, 0x86, 0x0c, 0x3a, 0x21, 0x53, 0xa6, 0xff, 0xa1, 0xc2, 0x47, 0x8f, 0xa6, 0x3a, 0x47, - 0x13, 0xca, 0x3c, 0x4d, 0x2c, 0xa0, 0x1c, 0x75, 0x21, 0xe5, 0xec, 0x02, 0xdc, 0xa5, 0x92, 0x8f, - 0x7e, 0x96, 0xc9, 0x42, 0xea, 0x28, 0x2f, 0xa4, 0x8e, 0xd9, 0xb9, 0xaf, 0x14, 0xcf, 0xfd, 0x71, - 0x52, 0xda, 0x83, 0x0d, 0x46, 0x92, 0x5b, 0x92, 0xb8, 0x85, 0xf7, 0x2b, 0xc2, 0x77, 0x3d, 0x53, - 0x0c, 0xf3, 0x2c, 0xf4, 0x3f, 0x15, 0xd8, 0x5d, 0xd8, 0x8e, 0xd9, 0xad, 0xbc, 0x84, 0xf2, 0x7f, - 0x1d, 0xb8, 0x70, 0x48, 0xeb, 0xbf, 0x21, 0x8c, 0x79, 0x97, 0x24, 0xef, 0x51, 0x03, 0xd7, 0x24, - 0x62, 0x06, 0xc5, 0x1b, 0x2c, 0xdd, 0xbb, 0x41, 0xfd, 0x1f, 0x15, 0x5a, 0x0f, 0x83, 0x3f, 0x67, - 0x32, 0xdb, 0x50, 0x91, 0x1b, 0x25, 0x5f, 0x5b, 0xcd, 0x76, 0xe6, 0xa9, 0x49, 0x2c, 0x98, 0x68, - 0x79, 0xe1, 0x44, 0x35, 0xa8, 0xc8, 0xfc, 0xe5, 0x28, 0x72, 0x11, 0xf5, 0xa1, 0x2c, 0xbe, 0x7a, - 0xab, 0x82, 0x35, 0xbe, 0x7e, 0xbc, 0x49, 0x73, 0x80, 0x20, 0x0b, 0xe1, 0x8c, 0xda, 0xb0, 0xea, - 0x4d, 0xf8, 0x15, 0x4d, 0xe4, 0xb0, 0xa4, 0xa4, 0x3b, 0xb0, 0xb9, 0xc8, 0x0b, 0xe9, 0xf0, 0x22, - 0xa7, 0x8b, 0xe1, 0xd9, 0xe8, 0x8d, 0x6b, 0xd9, 0x8e, 0x79, 0x64, 0xf6, 0x7b, 0x4e, 0xca, 0x08, - 0x92, 0x3a, 0xea, 0x50, 0xb9, 0x23, 0x0c, 0x21, 0x58, 0xa9, 0xba, 0xa5, 0xea, 0x31, 0x6c, 0xcf, - 0x53, 0x9a, 0xe0, 0x25, 0xf4, 0x1d, 0x54, 0x25, 0x45, 0x31, 0x39, 0xf6, 0x9d, 0x25, 0x3c, 0x38, - 0xb3, 0x7d, 0x62, 0xe2, 0xfa, 0x5f, 0x2a, 0xb4, 0xe7, 0x9f, 0x8c, 0x69, 0xc2, 0x97, 0x10, 0xf2, - 0x0f, 0xf7, 0x09, 0x79, 0x6f, 0x19, 0x21, 0xa7, 0xa1, 0x16, 0x52, 0xf0, 0xff, 0x31, 0x7d, 0xfd, - 0x97, 0xe7, 0x50, 0xf5, 0x3a, 0xd4, 0xdf, 0x62, 0xdb, 0x3a, 0x2e, 0x7e, 0xa7, 0x1e, 0x50, 0xae, - 0x9a, 0x62, 0x96, 0xed, 0xb8, 0xd8, 0x38, 0x36, 0x47, 0x8e, 0x81, 0x8d, 0xc3, 0x56, 0x49, 0x9f, - 0x80, 0x36, 0x5f, 0x90, 0x3c, 0xc1, 0xfb, 0x7d, 0x55, 0x1e, 0x5e, 0xd2, 0xf7, 0x50, 0x49, 0x44, - 0xed, 0x4c, 0x53, 0xc5, 0xb4, 0x3a, 0x4f, 0x35, 0x09, 0xe7, 0x0e, 0xe7, 0xab, 0xc2, 0xf2, 0xdb, - 0x7f, 0x03, 0x00, 0x00, 0xff, 0xff, 0x30, 0x4e, 0x6f, 0x73, 0x17, 0x0a, 0x00, 0x00, + // 1064 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xac, 0x55, 0x4d, 0x6f, 0xe3, 0xc4, + 0x1b, 0xff, 0x3b, 0x49, 0x9b, 0xe4, 0x49, 0x9a, 0x7a, 0xe7, 0xdf, 0x4d, 0xbd, 0x85, 0x2e, 0xc1, + 0x80, 0x88, 0x7a, 0x28, 0xa8, 0x48, 0xec, 0x8a, 0xbd, 0x10, 0x52, 0xb7, 0x6b, 0xda, 0xd8, 0xd9, + 0x89, 0xc3, 0xaa, 0x12, 0xd2, 0xc8, 0xb5, 0xa7, 0xad, 0xd5, 0xd4, 0x36, 0x9e, 0x49, 0x51, 0x6e, + 0x88, 0x33, 0x17, 0xae, 0x9c, 0xf8, 0x0c, 0xfb, 0x09, 0x91, 0xc7, 0xe3, 0xd4, 0x6d, 0xd2, 0x17, + 0x24, 0x4e, 0xc9, 0xf3, 0x7b, 0x5e, 0xe6, 0x79, 0xfd, 0x19, 0xb4, 0x78, 0xca, 0x2e, 0x48, 0x18, + 0xf1, 0xe0, 0x2c, 0xf0, 0x5c, 0x1e, 0x44, 0x21, 0xdb, 0x8d, 0x93, 0x88, 0x47, 0xa8, 0x26, 0x7e, + 0x4e, 0xa7, 0x67, 0x5b, 0xff, 0xf7, 0x2e, 0x5c, 0x4e, 0x02, 0x9f, 0x86, 0x3c, 0xe0, 0xb3, 0x4c, + 0xad, 0xff, 0xbd, 0x02, 0x1f, 0x0f, 0xa7, 0xec, 0xc2, 0x2a, 0xb8, 0x62, 0x7a, 0x1e, 0x30, 0x9e, + 0x88, 0xff, 0xc8, 0x06, 0xe0, 0xd1, 0x25, 0x0d, 0x09, 0x9f, 0xc5, 0x54, 0x53, 0x3a, 0x4a, 0xb7, + 0xb5, 0xf7, 0xf5, 0x6e, 0x1e, 0x74, 0xf7, 0x21, 0xdf, 0x5d, 0x27, 0x75, 0x74, 0x66, 0x31, 0xc5, + 0x75, 0x9e, 0xff, 0x45, 0x9f, 0x42, 0xd3, 0xa7, 0xd7, 0x81, 0x47, 0x89, 0xc0, 0xb4, 0x52, 0x47, + 0xe9, 0xd6, 0x71, 0x23, 0xc3, 0x84, 0x07, 0xfa, 0x12, 0xd6, 0x83, 0x90, 0x71, 0x77, 0x32, 0x11, + 0x71, 0x48, 0xe0, 0x6b, 0x65, 0x61, 0xd5, 0x2a, 0xc2, 0xa6, 0x9f, 0xc6, 0x72, 0x3d, 0x8f, 0x32, + 0x26, 0x63, 0x55, 0xb2, 0x58, 0x19, 0x96, 0xc5, 0xd2, 0xa0, 0x4a, 0x43, 0xf7, 0x74, 0x42, 0x7d, + 0x6d, 0xa5, 0xa3, 0x74, 0x6b, 0x38, 0x17, 0x53, 0xcd, 0x35, 0x4d, 0x58, 0x10, 0x85, 0xda, 0x6a, + 0x47, 0xe9, 0x56, 0x70, 0x2e, 0xa2, 0x2e, 0xa8, 0xee, 0x64, 0x12, 0xfd, 0x4a, 0x7d, 0x72, 0x49, + 0x67, 0x64, 0x12, 0x30, 0xae, 0x55, 0x3b, 0xe5, 0x6e, 0x13, 0xb7, 0x24, 0x7e, 0x44, 0x67, 0xc7, + 0x01, 0xe3, 0x68, 0x07, 0x9e, 0x9d, 0x4e, 0x22, 0xef, 0x92, 0xfa, 0x44, 0x74, 0x57, 0x98, 0xd6, + 0x84, 0xe9, 0xba, 0x54, 0xf4, 0x2f, 0x5c, 0x2e, 0x6c, 0x5f, 0x02, 0x4c, 0xc3, 0x44, 0xf4, 0x87, + 0x26, 0x5a, 0x5d, 0x24, 0x53, 0x40, 0xd0, 0x06, 0xac, 0x9c, 0x27, 0x6e, 0xc8, 0x35, 0xe8, 0x28, + 0xdd, 0x26, 0xce, 0x04, 0xf4, 0x0a, 0x34, 0xf1, 0x26, 0x39, 0x4b, 0xa2, 0x2b, 0xe2, 0x45, 0x21, + 0x77, 0x3d, 0xce, 0x48, 0x14, 0x4e, 0x66, 0x5a, 0x43, 0xc4, 0x78, 0x2e, 0xf4, 0x07, 0x49, 0x74, + 0xd5, 0x97, 0x5a, 0x3b, 0x9c, 0xcc, 0xd0, 0x47, 0x50, 0x77, 0xe3, 0x90, 0xf0, 0x28, 0x0e, 0x3c, + 0xad, 0x29, 0x1a, 0x53, 0x73, 0xe3, 0xd0, 0x49, 0x65, 0xf4, 0x05, 0xb4, 0x44, 0x7a, 0xe4, 0x2a, + 0xdd, 0x86, 0x28, 0x64, 0xda, 0x9a, 0x88, 0xb5, 0x26, 0xd0, 0x81, 0x04, 0xd1, 0x1b, 0xd8, 0xca, + 0x1b, 0x91, 0x1b, 0x16, 0xea, 0x6c, 0x89, 0x3a, 0x37, 0xa5, 0x45, 0xee, 0x94, 0xd7, 0xab, 0x1f, + 0x40, 0x7d, 0xbe, 0x00, 0xa8, 0x0d, 0x68, 0x6c, 0x1d, 0x59, 0xf6, 0x7b, 0x8b, 0x38, 0xf6, 0x91, + 0x61, 0x11, 0xe7, 0x64, 0x68, 0xa8, 0xff, 0x43, 0x6b, 0x50, 0xef, 0x0d, 0x25, 0xa6, 0x2a, 0x08, + 0x41, 0xeb, 0xc0, 0xc4, 0xc6, 0x0f, 0xbd, 0x91, 0x21, 0xb1, 0x92, 0xfe, 0xa1, 0x04, 0x9f, 0x3f, + 0xb4, 0x66, 0x98, 0xb2, 0x38, 0x0a, 0x19, 0x4d, 0x07, 0xca, 0xa6, 0x62, 0xf4, 0x62, 0x4f, 0x6b, + 0x38, 0x17, 0x91, 0x05, 0x2b, 0x34, 0x49, 0xa2, 0x44, 0x2c, 0x5b, 0x6b, 0xef, 0xf5, 0xd3, 0xf6, + 0x37, 0x0f, 0xbc, 0x6b, 0xa4, 0xbe, 0x62, 0x8f, 0xb3, 0x30, 0x68, 0x1b, 0x20, 0xa1, 0xbf, 0x4c, + 0x29, 0xe3, 0xf9, 0x6e, 0x36, 0x71, 0x5d, 0x22, 0xa6, 0xaf, 0xff, 0xa6, 0x40, 0x7d, 0xee, 0x53, + 0x2c, 0xdd, 0xc0, 0xd8, 0xc6, 0x79, 0xe9, 0xcf, 0xe1, 0xd9, 0xa0, 0x77, 0x7c, 0x60, 0xe3, 0x81, + 0xb1, 0x4f, 0x06, 0xc6, 0x68, 0xd4, 0x3b, 0x34, 0x54, 0x05, 0x6d, 0x80, 0xfa, 0x93, 0x81, 0x47, + 0xa6, 0x6d, 0x91, 0x81, 0x39, 0x1a, 0xf4, 0x9c, 0xfe, 0x5b, 0xb5, 0x84, 0xb6, 0xa0, 0x3d, 0xb6, + 0x46, 0xe3, 0xe1, 0xd0, 0xc6, 0x8e, 0xb1, 0x5f, 0xec, 0x61, 0x39, 0x6d, 0x9a, 0x69, 0x39, 0x06, + 0xb6, 0x7a, 0xc7, 0xd9, 0x0b, 0x6a, 0x45, 0xff, 0xa0, 0x80, 0x26, 0xd7, 0xa1, 0x1f, 0xf9, 0xb4, + 0xe7, 0x5f, 0xd3, 0x84, 0x07, 0x8c, 0xa6, 0x63, 0x44, 0x27, 0xd0, 0x5e, 0xe0, 0x0b, 0x12, 0x84, + 0x67, 0x91, 0xa6, 0x74, 0xca, 0xdd, 0xc6, 0xde, 0x67, 0xf7, 0xf7, 0xe7, 0xdd, 0x94, 0x26, 0x33, + 0x33, 0x3c, 0x8b, 0xf0, 0x46, 0x7c, 0x47, 0x95, 0xa2, 0xe8, 0x0d, 0xac, 0xdd, 0xa2, 0x19, 0xd1, + 0xf1, 0xc6, 0x5e, 0xfb, 0x26, 0x62, 0xba, 0x1f, 0xa6, 0xd4, 0xe2, 0xa6, 0x57, 0x90, 0xf4, 0xd7, + 0xf0, 0x7c, 0xe9, 0x7b, 0xe8, 0x13, 0x68, 0xc4, 0xd3, 0xd3, 0x49, 0xe0, 0xa5, 0xf7, 0xc8, 0x44, + 0x96, 0x4d, 0x0c, 0x19, 0x74, 0x44, 0x67, 0x4c, 0xff, 0xa3, 0x04, 0x2f, 0xee, 0x4d, 0x75, 0x81, + 0x26, 0x94, 0x45, 0x9a, 0x58, 0x42, 0x39, 0xa5, 0xa5, 0x94, 0xb3, 0x0d, 0x70, 0x93, 0x4a, 0x3e, + 0xfa, 0x79, 0x26, 0x4b, 0xa9, 0xa3, 0xb2, 0x94, 0x3a, 0xe6, 0xe7, 0xbe, 0x52, 0x3c, 0xf7, 0xfb, + 0x49, 0x69, 0x07, 0x9e, 0x31, 0x9a, 0x5c, 0xd3, 0x84, 0x14, 0xde, 0xaf, 0x0a, 0xdf, 0xf5, 0x4c, + 0x31, 0xcc, 0xb3, 0xd0, 0xff, 0x54, 0x60, 0x7b, 0x69, 0x3b, 0xe6, 0xb7, 0xf2, 0x0a, 0x2a, 0xff, + 0x76, 0xe0, 0xc2, 0x21, 0xad, 0xff, 0x8a, 0x32, 0xe6, 0x9e, 0xd3, 0xbc, 0x47, 0x4d, 0x5c, 0x97, + 0x88, 0xe9, 0x17, 0x6f, 0xb0, 0x7c, 0xeb, 0x06, 0xf5, 0xdf, 0xcb, 0xa0, 0xde, 0x0d, 0xfe, 0x94, + 0xc9, 0x6c, 0x42, 0x55, 0x6e, 0x94, 0x7c, 0x6d, 0x35, 0xdb, 0x99, 0xc7, 0x26, 0xb1, 0x64, 0xa2, + 0x95, 0xa5, 0x13, 0xd5, 0xa0, 0x2a, 0xf3, 0x97, 0xa3, 0xc8, 0x45, 0xd4, 0x87, 0x8a, 0xf8, 0xea, + 0xad, 0x0a, 0xd6, 0xf8, 0xea, 0xfe, 0x26, 0x2d, 0x00, 0x82, 0x2c, 0x84, 0x33, 0x6a, 0xc3, 0xaa, + 0x3b, 0xe5, 0x17, 0x51, 0x22, 0x87, 0x25, 0x25, 0x9d, 0xc1, 0xc6, 0x32, 0x2f, 0xa4, 0xc3, 0xcb, + 0x9c, 0x2e, 0x86, 0xe3, 0xd1, 0x5b, 0x62, 0xd9, 0x8e, 0x79, 0x60, 0xf6, 0x7b, 0x4e, 0xca, 0x08, + 0x92, 0x3a, 0x1a, 0x50, 0xbd, 0x21, 0x0c, 0x21, 0x58, 0xa9, 0x5a, 0x2d, 0xa1, 0x6d, 0x78, 0x81, + 0x8d, 0x77, 0x63, 0x63, 0xe4, 0x10, 0xc7, 0x26, 0x3f, 0xda, 0xa6, 0x45, 0xfa, 0xf6, 0x60, 0x30, + 0xb6, 0x4c, 0xe7, 0x44, 0x2d, 0xeb, 0x31, 0x6c, 0x2e, 0x32, 0x9e, 0xa0, 0x2d, 0xf4, 0x2d, 0xd4, + 0x24, 0x83, 0x31, 0xb9, 0x15, 0x5b, 0x0f, 0xd0, 0xe4, 0xdc, 0xf6, 0x91, 0x85, 0xd0, 0xff, 0x2a, + 0x41, 0x7b, 0xf1, 0xc9, 0x38, 0x4a, 0xf8, 0x03, 0x7c, 0xfd, 0xfd, 0x6d, 0xbe, 0xde, 0x79, 0x88, + 0xaf, 0xd3, 0x50, 0x4b, 0x19, 0xfa, 0xbf, 0x58, 0x0e, 0xfd, 0xe7, 0xa7, 0x30, 0xf9, 0x3a, 0x34, + 0xde, 0x63, 0xdb, 0x3a, 0x2c, 0x7e, 0xc6, 0xee, 0x30, 0x72, 0x29, 0xc5, 0x2c, 0xdb, 0x21, 0xd8, + 0x38, 0x34, 0x47, 0x8e, 0x81, 0x8d, 0x7d, 0xb5, 0xac, 0x4f, 0x41, 0x5b, 0x2c, 0x48, 0x5e, 0xe8, + 0xed, 0xbe, 0x2a, 0x77, 0x0f, 0xed, 0x3b, 0xa8, 0x26, 0xa2, 0x76, 0xa6, 0x95, 0xc4, 0xb4, 0x3a, + 0x8f, 0x35, 0x09, 0xe7, 0x0e, 0xa7, 0xab, 0xc2, 0xf2, 0x9b, 0x7f, 0x02, 0x00, 0x00, 0xff, 0xff, + 0x78, 0x43, 0x10, 0x6f, 0x36, 0x0a, 0x00, 0x00, } diff --git a/protocol/protobuf/push_notifications.proto b/protocol/protobuf/push_notifications.proto index 17c5f7990..e69fb9cd8 100644 --- a/protocol/protobuf/push_notifications.proto +++ b/protocol/protobuf/push_notifications.proto @@ -76,6 +76,7 @@ message PushNotification { UNKNOWN_PUSH_NOTIFICATION_TYPE = 0; MESSAGE = 1; MENTION = 2; + REQUEST_TO_JOIN_COMMUNITY = 3; } bytes author = 7; } diff --git a/protocol/push_notification_test.go b/protocol/push_notification_test.go index dcec9158c..b09bc377b 100644 --- a/protocol/push_notification_test.go +++ b/protocol/push_notification_test.go @@ -14,9 +14,11 @@ import ( "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/common" + "github.com/status-im/status-go/protocol/communities" "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/pushnotificationclient" "github.com/status-im/status-go/protocol/pushnotificationserver" + "github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/tt" "github.com/status-im/status-go/waku" ) @@ -843,3 +845,136 @@ func (s *MessengerPushNotificationSuite) TestReceivePushNotificationMention() { s.Require().NoError(alice.Shutdown()) s.Require().NoError(server.Shutdown()) } + +func (s *MessengerPushNotificationSuite) TestReceivePushNotificationCommunityRequest() { + + bob := s.m + + serverKey, err := crypto.GenerateKey() + s.Require().NoError(err) + server := s.newPushNotificationServer(s.shh, serverKey) + + alice := s.newMessenger(s.shh) + // start alice and enable sending push notifications + _, err = alice.Start() + s.Require().NoError(err) + s.Require().NoError(alice.EnableSendingPushNotifications()) + + // Register bob + err = bob.AddPushNotificationsServer(context.Background(), &server.identity.PublicKey, pushnotificationclient.ServerTypeCustom) + s.Require().NoError(err) + + err = bob.RegisterForPushNotifications(context.Background(), bob1DeviceToken, testAPNTopic, protobuf.PushNotificationRegistration_APN_TOKEN) + + // Pull servers and check we registered + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = bob.RetrieveAll() + if err != nil { + return err + } + registered, err := bob.RegisteredForPushNotifications() + if err != nil { + return err + } + if !registered { + return errors.New("not registered") + } + + bobServers, err := bob.GetPushNotificationsServers() + if err != nil { + return err + } + + if len(bobServers) == 0 { + return errors.New("not registered") + } + + return nil + }) + // Make sure we receive it + s.Require().NoError(err) + _, err = bob.GetPushNotificationsServers() + s.Require().NoError(err) + + description := &requests.CreateCommunity{ + Membership: protobuf.CommunityPermissions_ON_REQUEST, + Name: "status", + Color: "#ffffff", + Description: "status community description", + } + + response, err := bob.CreateCommunity(description) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.Communities(), 1) + community := response.Communities()[0] + + // Send an community message + chat := CreateOneToOneChat(common.PubkeyToHex(&alice.identity.PublicKey), &alice.identity.PublicKey, alice.transport) + + inputMessage := &common.Message{} + inputMessage.ChatId = chat.ID + inputMessage.Text = "some text" + inputMessage.CommunityID = community.IDString() + + err = bob.SaveChat(chat) + s.NoError(err) + _, err = bob.SendChatMessage(context.Background(), inputMessage) + s.NoError(err) + + // Pull message and make sure org is received + err = tt.RetryWithBackOff(func() error { + response, err = alice.RetrieveAll() + if err != nil { + return err + } + if len(response.Communities()) == 0 { + return errors.New("community not received") + } + return nil + }) + + request := &requests.RequestToJoinCommunity{CommunityID: community.ID()} + + // We try to join the org + response, err = alice.RequestToJoinCommunity(request) + s.Require().NoError(err) + s.Require().NotNil(response) + s.Require().Len(response.RequestsToJoinCommunity, 1) + + requestToJoin1 := response.RequestsToJoinCommunity[0] + s.Require().NotNil(requestToJoin1) + s.Require().Equal(community.ID(), requestToJoin1.CommunityID) + s.Require().True(requestToJoin1.Our) + s.Require().NotEmpty(requestToJoin1.ID) + s.Require().NotEmpty(requestToJoin1.Clock) + s.Require().Equal(requestToJoin1.PublicKey, common.PubkeyToHex(&alice.identity.PublicKey)) + s.Require().Equal(communities.RequestToJoinStatePending, requestToJoin1.State) + + err = tt.RetryWithBackOff(func() error { + _, err = server.RetrieveAll() + if err != nil { + return err + } + _, err = alice.RetrieveAll() + if err != nil { + return err + } + + if server.pushNotificationServer.SentRequests != 1 { + return errors.New("request not sent") + } + + return nil + + }) + + s.Require().NoError(err) + + s.Require().NoError(alice.Shutdown()) + s.Require().NoError(server.Shutdown()) +} diff --git a/protocol/pushnotificationclient/client.go b/protocol/pushnotificationclient/client.go index b85e0edd9..353ac1df6 100644 --- a/protocol/pushnotificationclient/client.go +++ b/protocol/pushnotificationclient/client.go @@ -484,6 +484,7 @@ func (c *Client) HandlePushNotificationQueryResponse(serverPublicKey *ecdsa.Publ // get the public key associated with this query clientPublicKey, err := c.persistence.GetQueryPublicKey(response.MessageId) if err != nil { + c.config.Logger.Error("failed to query client publicKey", zap.Error(err)) return err } if clientPublicKey == nil { @@ -900,7 +901,7 @@ func (c *Client) handlePublicMessageSent(sentMessage *common.SentMessage) error c.config.Logger.Debug("should no mention", zap.Any("publickey", shouldNotify)) // we send the notifications and return the info of the devices notified - infos, err := c.sendNotification(publicKey, nil, messageID, message.LocalChatID, protobuf.PushNotification_MENTION) + infos, err := c.SendNotification(publicKey, nil, messageID, message.LocalChatID, protobuf.PushNotification_MENTION) if err != nil { return err } @@ -998,7 +999,7 @@ func (c *Client) handleDirectMessageSent(sentMessage *common.SentMessage) error } // we send the notifications and return the info of the devices notified - infos, err := c.sendNotification(publicKey, installationIDs, trackedMessageIDs[0], chatID, protobuf.PushNotification_MESSAGE) + infos, err := c.SendNotification(publicKey, installationIDs, trackedMessageIDs[0], chatID, protobuf.PushNotification_MESSAGE) if err != nil { return err } @@ -1280,9 +1281,9 @@ func (c *Client) registerWithServer(registration *protobuf.PushNotificationRegis return nil } -// sendNotification sends an actual notification to the push notification server. +// SendNotification sends an actual notification to the push notification server. // the notification is sent using an ephemeral key to shield the real identity of the sender -func (c *Client) sendNotification(publicKey *ecdsa.PublicKey, installationIDs []string, messageID []byte, chatID string, notificationType protobuf.PushNotification_PushNotificationType) ([]*PushNotificationInfo, error) { +func (c *Client) SendNotification(publicKey *ecdsa.PublicKey, installationIDs []string, messageID []byte, chatID string, notificationType protobuf.PushNotification_PushNotificationType) ([]*PushNotificationInfo, error) { // get latest push notification infos err := c.queryNotificationInfo(publicKey, false) @@ -1316,6 +1317,8 @@ func (c *Client) sendNotification(publicKey *ecdsa.PublicKey, installationIDs [] // one info per installation id, grouped by server actionableInfos := make(map[string][]*PushNotificationInfo) + + c.config.Logger.Info("INFOS", zap.Any("info", info)) for _, i := range info { if !installationIDsMap[i.InstallationID] { @@ -1406,7 +1409,7 @@ func (c *Client) resendNotification(pn *SentNotification) error { return err } - _, err = c.sendNotification(pn.PublicKey, []string{pn.InstallationID}, pn.MessageID, pn.ChatID, pn.NotificationType) + _, err = c.SendNotification(pn.PublicKey, []string{pn.InstallationID}, pn.MessageID, pn.ChatID, pn.NotificationType) return err } diff --git a/protocol/pushnotificationserver/gorush.go b/protocol/pushnotificationserver/gorush.go index 29f9483f9..18c508997 100644 --- a/protocol/pushnotificationserver/gorush.go +++ b/protocol/pushnotificationserver/gorush.go @@ -14,6 +14,7 @@ import ( const defaultNewMessageNotificationText = "You have a new message" const defaultMentionNotificationText = "Someone mentioned you" +const defaultRequestToJoinCommunityNotificationText = "Someone requested to join a community you are an admin of" type GoRushRequestData struct { EncryptedMessage string `json:"encryptedMessage"` @@ -56,6 +57,8 @@ func PushNotificationRegistrationToGoRushRequest(requestAndRegistrations []*Requ var text string if request.Type == protobuf.PushNotification_MESSAGE { text = defaultNewMessageNotificationText + } else if request.Type == protobuf.PushNotification_REQUEST_TO_JOIN_COMMUNITY { + text = defaultRequestToJoinCommunityNotificationText } else { text = defaultMentionNotificationText } diff --git a/protocol/pushnotificationserver/server.go b/protocol/pushnotificationserver/server.go index 9f06d916f..6cde204cc 100644 --- a/protocol/pushnotificationserver/server.go +++ b/protocol/pushnotificationserver/server.go @@ -37,6 +37,8 @@ type Server struct { persistence Persistence config *Config messageProcessor *common.MessageProcessor + // SentRequests keeps track of the requests sent to gorush, for testing only + SentRequests int64 } func New(config *Config, persistence Persistence, messageProcessor *common.MessageProcessor) *Server { @@ -376,7 +378,7 @@ func (s *Server) buildPushNotificationReport(pn *protobuf.PushNotification, regi } else if registration.AccessToken != pn.AccessToken { s.config.Logger.Debug("invalid token") report.Error = protobuf.PushNotificationReport_WRONG_TOKEN - } else if (s.isMessageNotification(pn) && !s.isValidMessageNotification(pn, registration)) || (s.isMentionNotification(pn) && !s.isValidMentionNotification(pn, registration)) { + } else if (s.isMessageNotification(pn) && !s.isValidMessageNotification(pn, registration)) || (s.isMentionNotification(pn) && !s.isValidMentionNotification(pn, registration)) || (s.isRequestToJoinCommunityNotification(pn) && !s.isValidRequestToJoinCommunityNotification(pn, registration)) { s.config.Logger.Debug("filtered notification") // We report as successful but don't send the notification // for privacy reasons, as otherwise we would disclose that @@ -450,6 +452,7 @@ func (s *Server) sendPushNotification(requestAndRegistrations []*RequestAndRegis if len(requestAndRegistrations) == 0 { return nil } + s.SentRequests++ goRushRequest := PushNotificationRegistrationToGoRushRequest(requestAndRegistrations) return sendGoRushNotification(goRushRequest, s.config.GorushURL, s.config.Logger) } @@ -529,10 +532,21 @@ func (s *Server) isMessageNotification(pn *protobuf.PushNotification) bool { return pn.Type == protobuf.PushNotification_MESSAGE } -// isValidMentionNotification checks: +// isValidMessageNotification checks: // this is a message // the chat is not muted // the author is not blocked func (s *Server) isValidMessageNotification(pn *protobuf.PushNotification, registration *protobuf.PushNotificationRegistration) bool { return s.isMessageNotification(pn) && !s.contains(registration.BlockedChatList, pn.ChatId) && !s.contains(registration.BlockedChatList, pn.Author) } + +func (s *Server) isRequestToJoinCommunityNotification(pn *protobuf.PushNotification) bool { + return pn.Type == protobuf.PushNotification_REQUEST_TO_JOIN_COMMUNITY +} + +// isValidRequestToJoinCommunityNotification checks: +// this is a request to join a community +// the author is not blocked +func (s *Server) isValidRequestToJoinCommunityNotification(pn *protobuf.PushNotification, registration *protobuf.PushNotificationRegistration) bool { + return s.isRequestToJoinCommunityNotification(pn) && !s.contains(registration.BlockedChatList, pn.Author) +} diff --git a/services/ext/service.go b/services/ext/service.go index b049c3cad..b06ab0b4b 100644 --- a/services/ext/service.go +++ b/services/ext/service.go @@ -185,10 +185,11 @@ func (s *Service) StartMessenger() (*protocol.MessengerResponse, error) { func publishMessengerResponse(response *protocol.MessengerResponse) { if !response.IsEmpty() { - PublisherSignalHandler{}.NewMessages(response) - localnotifications.SendMessageNotifications(response.Notifications) + notifications := response.Notifications() // Clear notifications as not used for now - response.Notifications = nil + response.ClearNotifications() + PublisherSignalHandler{}.NewMessages(response) + localnotifications.PushMessages(notifications) } } diff --git a/services/local-notifications/core.go b/services/local-notifications/core.go index d9138b987..7970b3c45 100644 --- a/services/local-notifications/core.go +++ b/services/local-notifications/core.go @@ -3,92 +3,69 @@ package localnotifications import ( "database/sql" "encoding/json" - "fmt" - "math/big" "sync" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/event" "github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" - "github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/multiaccounts/accounts" - "github.com/status-im/status-go/protocol" "github.com/status-im/status-go/services/wallet" "github.com/status-im/status-go/signal" ) type PushCategory string -type transactionState string - type NotificationType string -const ( - walletDeeplinkPrefix = "status-im://wallet/" - - failed transactionState = "failed" - inbound transactionState = "inbound" - outbound transactionState = "outbound" - - CategoryTransaction PushCategory = "transaction" - CategoryMessage PushCategory = "newMessage" - - TypeTransaction NotificationType = "transaction" - TypeMessage NotificationType = "message" -) - -var ( - marshalTypeMismatchErr = "notification type mismatch, expected '%s', Body could not be marshalled into this type" -) - -type notificationBody struct { - State transactionState `json:"state"` - From common.Address `json:"from"` - To common.Address `json:"to"` - FromAccount *accounts.Account `json:"fromAccount,omitempty"` - ToAccount *accounts.Account `json:"toAccount,omitempty"` - Value *hexutil.Big `json:"value"` - ERC20 bool `json:"erc20"` - Contract common.Address `json:"contract"` - Network uint64 `json:"network"` +type NotificationBody interface { + json.Marshaler } type Notification struct { - ID common.Hash `json:"id"` - Platform float32 `json:"platform,omitempty"` - Body interface{} - BodyType NotificationType `json:"bodyType"` - Category PushCategory `json:"category,omitempty"` - Deeplink string `json:"deepLink,omitempty"` - Image string `json:"imageUrl,omitempty"` - IsScheduled bool `json:"isScheduled,omitempty"` - ScheduledTime string `json:"scheduleTime,omitempty"` + ID common.Hash + Platform float32 + Body NotificationBody + BodyType NotificationType + Title string + Message string + Category PushCategory + Deeplink string + Image string + IsScheduled bool + ScheduledTime string + IsConversation bool + IsGroupConversation bool + ConversationID string + Timestamp uint64 + Author NotificationAuthor +} + +type NotificationAuthor struct { + ID string `json:"id"` + Icon string `json:"icon"` + Name string `json:"name"` } // notificationAlias is an interim struct used for json un/marshalling type notificationAlias struct { - ID common.Hash `json:"id"` - Platform float32 `json:"platform,omitempty"` - Body json.RawMessage `json:"body"` - BodyType NotificationType `json:"bodyType"` - Category PushCategory `json:"category,omitempty"` - Deeplink string `json:"deepLink,omitempty"` - Image string `json:"imageUrl,omitempty"` - IsScheduled bool `json:"isScheduled,omitempty"` - ScheduledTime string `json:"scheduleTime,omitempty"` -} - -// TransactionEvent - structure used to pass messages from wallet to bus -type TransactionEvent struct { - Type string `json:"type"` - BlockNumber *big.Int `json:"block-number"` - Accounts []common.Address `json:"accounts"` - NewTransactionsPerAccount map[common.Address]int `json:"new-transactions"` - ERC20 bool `json:"erc20"` - MaxKnownBlocks map[common.Address]*big.Int `json:"max-known-blocks"` + ID common.Hash `json:"id"` + Platform float32 `json:"platform,omitempty"` + Body json.RawMessage `json:"body"` + BodyType NotificationType `json:"bodyType"` + Title string `json:"title,omitempty"` + Message string `json:"message,omitempty"` + Category PushCategory `json:"category,omitempty"` + Deeplink string `json:"deepLink,omitempty"` + Image string `json:"imageUrl,omitempty"` + IsScheduled bool `json:"isScheduled,omitempty"` + ScheduledTime string `json:"scheduleTime,omitempty"` + IsConversation bool `json:"isConversation,omitempty"` + IsGroupConversation bool `json:"isGroupConversation,omitempty"` + ConversationID string `json:"conversationId,omitempty"` + Timestamp uint64 `json:"timestamp,omitempty"` + Author NotificationAuthor `json:"notificationAuthor,omitempty"` } // MessageEvent - structure used to pass messages from chat to bus @@ -132,274 +109,49 @@ func NewService(appDB *sql.DB, network uint64) *Service { } func (n *Notification) MarshalJSON() ([]byte, error) { + var body json.RawMessage - var err error - - switch n.BodyType { - case TypeTransaction: - if nb, ok := n.Body.(notificationBody); ok { - body, err = json.Marshal(nb) - if err != nil { - return nil, err - } - } else { - return nil, fmt.Errorf(marshalTypeMismatchErr, n.BodyType) + if n.Body != nil { + encodedBody, err := n.Body.MarshalJSON() + if err != nil { + return nil, err } - - case TypeMessage: - if nmb, ok := n.Body.(protocol.MessageNotificationBody); ok { - body, err = json.Marshal(nmb) - if err != nil { - return nil, err - } - } else { - return nil, fmt.Errorf(marshalTypeMismatchErr, n.BodyType) - } - - default: - return nil, fmt.Errorf("unknown NotificationType '%s'", n.BodyType) + body = encodedBody } alias := notificationAlias{ - n.ID, - n.Platform, - body, - n.BodyType, - n.Category, - n.Deeplink, - n.Image, - n.IsScheduled, - n.ScheduledTime, + ID: n.ID, + Platform: n.Platform, + Body: body, + BodyType: n.BodyType, + Category: n.Category, + Title: n.Title, + Message: n.Message, + Deeplink: n.Deeplink, + Image: n.Image, + IsScheduled: n.IsScheduled, + ScheduledTime: n.ScheduledTime, + IsConversation: n.IsConversation, + IsGroupConversation: n.IsGroupConversation, + ConversationID: n.ConversationID, + Timestamp: n.Timestamp, + Author: n.Author, } return json.Marshal(alias) } -func (n *Notification) UnmarshalJSON(data []byte) error { - var alias notificationAlias - err := json.Unmarshal(data, &alias) - if err != nil { - return err - } - - n.BodyType = alias.BodyType - n.Category = alias.Category - n.Platform = alias.Platform - n.ID = alias.ID - n.Image = alias.Image - n.Deeplink = alias.Deeplink - n.IsScheduled = alias.IsScheduled - n.ScheduledTime = alias.ScheduledTime - - switch n.BodyType { - case TypeTransaction: - return n.unmarshalAndAttachBody(alias.Body, ¬ificationBody{}) - - case TypeMessage: - return n.unmarshalAndAttachBody(alias.Body, &protocol.MessageNotificationBody{}) - - default: - return fmt.Errorf("unknown NotificationType '%s'", n.BodyType) - } -} - -func (n *Notification) unmarshalAndAttachBody(body json.RawMessage, bodyStruct interface{}) error { - err := json.Unmarshal(body, &bodyStruct) - if err != nil { - return err - } - - n.Body = bodyStruct - return nil -} - -func pushMessages(ns []*Notification) { +func PushMessages(ns []*Notification) { for _, n := range ns { pushMessage(n) } } func pushMessage(notification *Notification) { - log.Info("Pushing a new push notification", "info", notification) + log.Debug("Pushing a new push notification", "notification", notification) signal.SendLocalNotifications(notification) } -func (s *Service) buildTransactionNotification(rawTransfer wallet.Transfer) *Notification { - log.Info("Handled a new transfer in buildTransactionNotification", "info", rawTransfer) - - var deeplink string - var state transactionState - transfer := wallet.CastToTransferView(rawTransfer) - - switch { - case transfer.TxStatus == hexutil.Uint64(0): - state = failed - case transfer.Address == transfer.To: - state = inbound - default: - state = outbound - } - - from, err := s.accountsDB.GetAccountByAddress(types.Address(transfer.From)) - - if err != nil { - log.Debug("Could not select From account by address", "error", err) - } - - to, err := s.accountsDB.GetAccountByAddress(types.Address(transfer.To)) - - if err != nil { - log.Debug("Could not select To account by address", "error", err) - } - - if from != nil { - deeplink = walletDeeplinkPrefix + from.Address.String() - } else if to != nil { - deeplink = walletDeeplinkPrefix + to.Address.String() - } - - body := notificationBody{ - State: state, - From: transfer.From, - To: transfer.Address, - FromAccount: from, - ToAccount: to, - Value: transfer.Value, - ERC20: string(transfer.Type) == "erc20", - Contract: transfer.Contract, - Network: transfer.NetworkID, - } - - return &Notification{ - BodyType: TypeTransaction, - ID: transfer.ID, - Body: body, - Deeplink: deeplink, - Category: CategoryTransaction, - } -} - -func (s *Service) transactionsHandler(payload TransactionEvent) { - log.Info("Handled a new transaction", "info", payload) - - limit := 20 - if payload.BlockNumber != nil { - for _, address := range payload.Accounts { - if payload.BlockNumber.Cmp(payload.MaxKnownBlocks[address]) >= 0 { - log.Info("Handled transfer for address", "info", address) - transfers, err := s.walletDB.GetTransfersByAddressAndBlock(address, payload.BlockNumber, int64(limit)) - if err != nil { - log.Error("Could not fetch transfers", "error", err) - } - - for _, transaction := range transfers { - n := s.buildTransactionNotification(transaction) - pushMessage(n) - } - } - } - } -} - -// SubscribeWallet - Subscribes to wallet signals -func (s *Service) SubscribeWallet(publisher *event.Feed) error { - s.walletTransmitter.publisher = publisher - - preference, err := s.db.GetWalletPreference() - - if err != nil { - log.Error("Failed to get wallet preference", "error", err) - s.WatchingEnabled = false - } else { - s.WatchingEnabled = preference.Enabled - } - - s.StartWalletWatcher() - - return nil -} - -// StartWalletWatcher - Forward wallet events to notifications -func (s *Service) StartWalletWatcher() { - if s.walletTransmitter.quit != nil { - // already running, nothing to do - return - } - - if s.walletTransmitter.publisher == nil { - log.Error("wallet publisher was not initialized") - return - } - - s.walletTransmitter.quit = make(chan struct{}) - events := make(chan wallet.Event, 10) - sub := s.walletTransmitter.publisher.Subscribe(events) - - s.walletTransmitter.wg.Add(1) - - maxKnownBlocks := map[common.Address]*big.Int{} - go func() { - defer s.walletTransmitter.wg.Done() - for { - select { - case <-s.walletTransmitter.quit: - sub.Unsubscribe() - return - case err := <-sub.Err(): - // technically event.Feed cannot send an error to subscription.Err channel. - // the only time we will get an event is when that channel is closed. - if err != nil { - log.Error("wallet signals transmitter failed with", "error", err) - } - return - case event := <-events: - if event.Type == wallet.EventNewBlock && len(maxKnownBlocks) > 0 { - newBlocks := false - for _, address := range event.Accounts { - if _, ok := maxKnownBlocks[address]; !ok { - newBlocks = true - maxKnownBlocks[address] = event.BlockNumber - } else if event.BlockNumber.Cmp(maxKnownBlocks[address]) == 1 { - maxKnownBlocks[address] = event.BlockNumber - newBlocks = true - } - } - if newBlocks && s.WatchingEnabled { - s.transmitter.publisher.Send(TransactionEvent{ - Type: string(event.Type), - BlockNumber: event.BlockNumber, - Accounts: event.Accounts, - NewTransactionsPerAccount: event.NewTransactionsPerAccount, - ERC20: event.ERC20, - MaxKnownBlocks: maxKnownBlocks, - }) - } - } else if event.Type == wallet.EventMaxKnownBlock { - for _, address := range event.Accounts { - if _, ok := maxKnownBlocks[address]; !ok { - maxKnownBlocks[address] = event.BlockNumber - } - } - } - } - } - }() -} - -// StopWalletWatcher - stops watching for new wallet events -func (s *Service) StopWalletWatcher() { - if s.walletTransmitter.quit != nil { - close(s.walletTransmitter.quit) - s.walletTransmitter.wg.Wait() - s.walletTransmitter.quit = nil - } -} - -// IsWatchingWallet - check if local-notifications are subscribed to wallet updates -func (s *Service) IsWatchingWallet() bool { - return s.walletTransmitter.quit != nil -} - // Start Worker which processes all incoming messages func (s *Service) Start(_ *p2p.Server) error { s.started = true @@ -473,19 +225,3 @@ func (s *Service) Protocols() []p2p.Protocol { func (s *Service) IsStarted() bool { return s.started } - -func SendMessageNotifications(mnb []protocol.MessageNotificationBody) { - var ns []*Notification - for _, n := range mnb { - ns = append(ns, &Notification{ - Body: n, - BodyType: TypeMessage, - Category: CategoryMessage, - Deeplink: "", // TODO find what if any Deeplink should be used here - Image: "", // TODO do we want to attach any image data contained on the MessageBody{}? - }) - } - - // sends notifications messages to the OS level application - pushMessages(ns) -} diff --git a/services/local-notifications/core_test.go b/services/local-notifications/core_test.go index ce0bfd658..8c4b896ce 100644 --- a/services/local-notifications/core_test.go +++ b/services/local-notifications/core_test.go @@ -2,9 +2,9 @@ package localnotifications import ( "database/sql" - "encoding/json" "fmt" "math/big" + "strings" "testing" "time" @@ -128,19 +128,8 @@ func TestTransactionNotification(t *testing.T) { if signalEvent == nil { return fmt.Errorf("signal was not handled") } - notification := struct { - Type string - Event Notification - }{} - - require.NoError(t, json.Unmarshal(signalEvent, ¬ification)) - - if notification.Type != "local-notifications" { - return fmt.Errorf("wrong signal was sent") - } - if notification.Event.Body.(*notificationBody).To != header.Address { - return fmt.Errorf("transaction to address is wrong") - } + require.True(t, strings.Contains(string(signalEvent), `"type":"local-notifications"`)) + require.True(t, strings.Contains(string(signalEvent), `"to":"`+header.Address.Hex())) return nil }, 2*time.Second, 100*time.Millisecond)) diff --git a/services/local-notifications/transaction.go b/services/local-notifications/transaction.go new file mode 100644 index 000000000..5fa59ac97 --- /dev/null +++ b/services/local-notifications/transaction.go @@ -0,0 +1,229 @@ +package localnotifications + +import ( + "encoding/json" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/log" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/multiaccounts/accounts" + "github.com/status-im/status-go/services/wallet" +) + +type transactionState string + +const ( + walletDeeplinkPrefix = "status-im://wallet/" + + failed transactionState = "failed" + inbound transactionState = "inbound" + outbound transactionState = "outbound" +) + +// TransactionEvent - structure used to pass messages from wallet to bus +type TransactionEvent struct { + Type string `json:"type"` + BlockNumber *big.Int `json:"block-number"` + Accounts []common.Address `json:"accounts"` + NewTransactionsPerAccount map[common.Address]int `json:"new-transactions"` + ERC20 bool `json:"erc20"` + MaxKnownBlocks map[common.Address]*big.Int `json:"max-known-blocks"` +} + +type transactionBody struct { + State transactionState `json:"state"` + From common.Address `json:"from"` + To common.Address `json:"to"` + FromAccount *accounts.Account `json:"fromAccount,omitempty"` + ToAccount *accounts.Account `json:"toAccount,omitempty"` + Value *hexutil.Big `json:"value"` + ERC20 bool `json:"erc20"` + Contract common.Address `json:"contract"` + Network uint64 `json:"network"` +} + +func (t transactionBody) MarshalJSON() ([]byte, error) { + type Alias transactionBody + item := struct{ *Alias }{Alias: (*Alias)(&t)} + return json.Marshal(item) +} + +func (s *Service) buildTransactionNotification(rawTransfer wallet.Transfer) *Notification { + log.Info("Handled a new transfer in buildTransactionNotification", "info", rawTransfer) + + var deeplink string + var state transactionState + transfer := wallet.CastToTransferView(rawTransfer) + + switch { + case transfer.TxStatus == hexutil.Uint64(0): + state = failed + case transfer.Address == transfer.To: + state = inbound + default: + state = outbound + } + + from, err := s.accountsDB.GetAccountByAddress(types.Address(transfer.From)) + + if err != nil { + log.Debug("Could not select From account by address", "error", err) + } + + to, err := s.accountsDB.GetAccountByAddress(types.Address(transfer.To)) + + if err != nil { + log.Debug("Could not select To account by address", "error", err) + } + + if from != nil { + deeplink = walletDeeplinkPrefix + from.Address.String() + } else if to != nil { + deeplink = walletDeeplinkPrefix + to.Address.String() + } + + body := transactionBody{ + State: state, + From: transfer.From, + To: transfer.Address, + FromAccount: from, + ToAccount: to, + Value: transfer.Value, + ERC20: string(transfer.Type) == "erc20", + Contract: transfer.Contract, + Network: transfer.NetworkID, + } + + return &Notification{ + BodyType: TypeTransaction, + ID: transfer.ID, + Body: body, + Deeplink: deeplink, + Category: CategoryTransaction, + } +} + +func (s *Service) transactionsHandler(payload TransactionEvent) { + log.Info("Handled a new transaction", "info", payload) + + limit := 20 + if payload.BlockNumber != nil { + for _, address := range payload.Accounts { + if payload.BlockNumber.Cmp(payload.MaxKnownBlocks[address]) >= 0 { + log.Info("Handled transfer for address", "info", address) + transfers, err := s.walletDB.GetTransfersByAddressAndBlock(address, payload.BlockNumber, int64(limit)) + if err != nil { + log.Error("Could not fetch transfers", "error", err) + } + + for _, transaction := range transfers { + n := s.buildTransactionNotification(transaction) + pushMessage(n) + } + } + } + } +} + +// SubscribeWallet - Subscribes to wallet signals +func (s *Service) SubscribeWallet(publisher *event.Feed) error { + s.walletTransmitter.publisher = publisher + + preference, err := s.db.GetWalletPreference() + + if err != nil { + log.Error("Failed to get wallet preference", "error", err) + s.WatchingEnabled = false + } else { + s.WatchingEnabled = preference.Enabled + } + + s.StartWalletWatcher() + + return nil +} + +// StartWalletWatcher - Forward wallet events to notifications +func (s *Service) StartWalletWatcher() { + if s.walletTransmitter.quit != nil { + // already running, nothing to do + return + } + + if s.walletTransmitter.publisher == nil { + log.Error("wallet publisher was not initialized") + return + } + + s.walletTransmitter.quit = make(chan struct{}) + events := make(chan wallet.Event, 10) + sub := s.walletTransmitter.publisher.Subscribe(events) + + s.walletTransmitter.wg.Add(1) + + maxKnownBlocks := map[common.Address]*big.Int{} + go func() { + defer s.walletTransmitter.wg.Done() + for { + select { + case <-s.walletTransmitter.quit: + sub.Unsubscribe() + return + case err := <-sub.Err(): + // technically event.Feed cannot send an error to subscription.Err channel. + // the only time we will get an event is when that channel is closed. + if err != nil { + log.Error("wallet signals transmitter failed with", "error", err) + } + return + case event := <-events: + if event.Type == wallet.EventNewBlock && len(maxKnownBlocks) > 0 { + newBlocks := false + for _, address := range event.Accounts { + if _, ok := maxKnownBlocks[address]; !ok { + newBlocks = true + maxKnownBlocks[address] = event.BlockNumber + } else if event.BlockNumber.Cmp(maxKnownBlocks[address]) == 1 { + maxKnownBlocks[address] = event.BlockNumber + newBlocks = true + } + } + if newBlocks && s.WatchingEnabled { + s.transmitter.publisher.Send(TransactionEvent{ + Type: string(event.Type), + BlockNumber: event.BlockNumber, + Accounts: event.Accounts, + NewTransactionsPerAccount: event.NewTransactionsPerAccount, + ERC20: event.ERC20, + MaxKnownBlocks: maxKnownBlocks, + }) + } + } else if event.Type == wallet.EventMaxKnownBlock { + for _, address := range event.Accounts { + if _, ok := maxKnownBlocks[address]; !ok { + maxKnownBlocks[address] = event.BlockNumber + } + } + } + } + } + }() +} + +// StopWalletWatcher - stops watching for new wallet events +func (s *Service) StopWalletWatcher() { + if s.walletTransmitter.quit != nil { + close(s.walletTransmitter.quit) + s.walletTransmitter.wg.Wait() + s.walletTransmitter.quit = nil + } +} + +// IsWatchingWallet - check if local-notifications are subscribed to wallet updates +func (s *Service) IsWatchingWallet() bool { + return s.walletTransmitter.quit != nil +} diff --git a/services/local-notifications/types.go b/services/local-notifications/types.go new file mode 100644 index 000000000..f6a126239 --- /dev/null +++ b/services/local-notifications/types.go @@ -0,0 +1,10 @@ +package localnotifications + +const ( + CategoryTransaction PushCategory = "transaction" + CategoryMessage PushCategory = "newMessage" + CategoryCommunityRequestToJoin = "communityRequestToJoin" + + TypeTransaction NotificationType = "transaction" + TypeMessage NotificationType = "message" +) diff --git a/signal/events_shhext.go b/signal/events_shhext.go index e0da0ad6d..cd5d9561b 100644 --- a/signal/events_shhext.go +++ b/signal/events_shhext.go @@ -2,11 +2,10 @@ package signal import ( "encoding/hex" + "encoding/json" "github.com/ethereum/go-ethereum/common/hexutil" - statusproto "github.com/status-im/status-go/protocol" - "github.com/status-im/status-go/eth-node/types" ) @@ -158,6 +157,6 @@ func SendWhisperFilterAdded(filters []*Filter) { send(EventWhisperFilterAdded, WhisperFilterAddedSignal{Filters: filters}) } -func SendNewMessages(response *statusproto.MessengerResponse) { - send(EventNewMessages, response) +func SendNewMessages(obj json.Marshaler) { + send(EventNewMessages, obj) } diff --git a/sqlite/sqlite.go b/sqlite/sqlite.go index 37ad3392a..17869b99e 100644 --- a/sqlite/sqlite.go +++ b/sqlite/sqlite.go @@ -16,7 +16,8 @@ const ( // https://notes.status.im/i8Y_l7ccTiOYq09HVgoFwA kdfIterationsNumber = 3200 // WALMode for sqlite. - WALMode = "wal" + WALMode = "wal" + inMemoryPath = ":memory:" ) // DecryptDB completely removes the encryption from the db @@ -97,7 +98,7 @@ func openDB(path, key string) (*sql.DB, error) { if err != nil { return nil, err } - if mode != WALMode { + if mode != WALMode && path != inMemoryPath { return nil, fmt.Errorf("unable to set journal_mode to WAL. actual mode %s", mode) }