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
This commit is contained in:
Andrea Maria Piana 2021-03-31 18:23:45 +02:00 committed by GitHub
parent 5a76e93063
commit c55659b4f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 995 additions and 502 deletions

View File

@ -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
}

View File

@ -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

View File

@ -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()

View File

@ -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)

View File

@ -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:

View File

@ -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)
}

View File

@ -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),

View File

@ -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}

View File

@ -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 {

View File

@ -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: "",
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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)

View File

@ -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,
}

View File

@ -76,6 +76,7 @@ message PushNotification {
UNKNOWN_PUSH_NOTIFICATION_TYPE = 0;
MESSAGE = 1;
MENTION = 2;
REQUEST_TO_JOIN_COMMUNITY = 3;
}
bytes author = 7;
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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, &notificationBody{})
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)
}

View File

@ -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, &notification))
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))

View File

@ -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
}

View File

@ -0,0 +1,10 @@
package localnotifications
const (
CategoryTransaction PushCategory = "transaction"
CategoryMessage PushCategory = "newMessage"
CategoryCommunityRequestToJoin = "communityRequestToJoin"
TypeTransaction NotificationType = "transaction"
TypeMessage NotificationType = "message"
)

View File

@ -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)
}

View File

@ -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)
}