diff --git a/bindings/channels.go b/bindings/channels.go index 24a0b5450712461a385bddbd324ade3167189992..42171d7f913014179b556721f5defdbb08ac98d2 100644 --- a/bindings/channels.go +++ b/bindings/channels.go @@ -1158,22 +1158,26 @@ func ValidForever() int { // to the user should be tracked while all actions should not be. // - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty, // and [GetDefaultCMixParams] will be used internally. -// - pingsJSON - JSON of a slice of public keys of users that should receive -// mobile notifications for the message. -// -// Example pingsJSON: -// -// [ -// "FgJMvgSsY4rrKkS/jSe+vFOJOs5qSSyOUSW7UtF9/KU=", -// "fPqcHtrJ398PAC35QyWXEU9PHzz8Z4BKQTCxSvpSygw=", -// "JnjCgh7g/+hNiI9VPKW01aRSxGOFmNulNCymy3ImXAo=" -// ] +// - pingsMapJSON - JSON of a map of slices of [ed25519.PublicKey] of users +// that should receive mobile notifications for the message. Each slice keys +// on a [channels.PingType] that describes the type of notification it is. +// +// Example pingsMapJSON: +// { +// "usrMention": [ +// "CLdKxbe8D2WVOpx1mT63TZ5CP/nesmxHLT5DUUalpe0=", +// "S2c6NXjNqgR11SCOaiQUughWaLpWBKNufPt6cbTVHMA=" +// ], +// "usrReply": [ +// "aaMzSeA6Cu2Aix2MlOwzrAI+NnpKshzvZRT02PZPVec=" +// ] +// } // // Returns: // - []byte - JSON of [ChannelSendReport]. func (cm *ChannelsManager) SendGeneric(channelIdBytes []byte, messageType int, message []byte, validUntilMS int64, tracked bool, cmixParamsJSON []byte, - pingsJSON []byte) ([]byte, error) { + pingsMapJSON []byte) ([]byte, error) { // Unmarshal channel ID and parameters channelID, params, err := @@ -1190,14 +1194,14 @@ func (cm *ChannelsManager) SendGeneric(channelIdBytes []byte, messageType int, lease = channels.ValidForever } - pings, err := unmarshalPingsJson(pingsJSON) + pingsMap, err := unmarshalPingsMapJson(pingsMapJSON) if err != nil { return nil, err } // Send message messageID, rnd, ephID, err := cm.api.SendGeneric( - channelID, msgType, message, lease, tracked, params.CMIX, pings) + channelID, msgType, message, lease, tracked, params.CMIX, pingsMap) if err != nil { return nil, err } @@ -2025,10 +2029,23 @@ func (cm *ChannelsManager) SetMobileNotificationsLevel( // // Example return: // -// [ -// {"channel":"emV6aW1hAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD","type":1}, -// {"channel":"emV6aW1hAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD","type":2} -// ] +// [ +// { +// "channel": "jOgZopfYj4zrE/AHtKmkf+QEWnfUKv9KfIy/+Bsg0PkD", +// "type": 1, +// "pingType": "usrMention" +// }, +// { +// "channel": "GKmfN/LKXQYM6++TC6DeZYqoxvSUPkh5UAHWODqh9zkD", +// "type": 2, +// "pingType": "usrReply" +// }, +// { +// "channel": "M+28xtj0coHrhDHfojGNcyb2c4maO7ZuheB6egS0Pc4D", +// "type": 1, +// "pingType": "" +// } +// ] func GetChannelNotificationReportsForMe(notificationFilterJSON []byte, notificationDataCSV string) ([]byte, error) { var nfs []channels.NotificationFilter @@ -3410,3 +3427,11 @@ func unmarshalPingsJson(b []byte) ([]ed25519.PublicKey, error) { } return pings, nil } + +func unmarshalPingsMapJson(b []byte) (map[channels.PingType][]ed25519.PublicKey, error) { + var pingsMap map[channels.PingType][]ed25519.PublicKey + if b != nil && len(b) > 0 { + return pingsMap, json.Unmarshal(b, &pingsMap) + } + return pingsMap, nil +} diff --git a/channels/interface.go b/channels/interface.go index 1f29b861446f2e34d93fa45b4e63816ed0b3ec23..2533c86fd01b8654b203e02ec337bca62d92754d 100644 --- a/channels/interface.go +++ b/channels/interface.go @@ -122,7 +122,7 @@ type Manager interface { // ID (i.e., always returns 0) cannot be tracked, or it will cause errors. SendGeneric(channelID *id.ID, messageType MessageType, msg []byte, validUntil time.Duration, tracked bool, params cmix.CMIXParams, - pings []ed25519.PublicKey) ( + pingsMap map[PingType][]ed25519.PublicKey) ( message.ID, rounds.Round, ephemeral.Id, error) // SendMessage is used to send a formatted message over a channel. diff --git a/channels/notifications.go b/channels/notifications.go index 5da23e13ba54cb1167e176cf6a2f196f083aebe2..b340befe0d013f621a17939c6ab2eaa2c3b9aa6a 100644 --- a/channels/notifications.go +++ b/channels/notifications.go @@ -93,7 +93,8 @@ func (n *notifications) addChannel(channelID *id.ID) { err := n.nm.Set( channelID, notificationGroup, NotifyNone.Marshal(), clientNotif.Mute) if err != nil { - jww.WARN.Printf("[CH] Failed to add channel (%s) to notifications manager: %+v", channelID, err) + jww.WARN.Printf("[CH] Failed to add channel (%s) to notifications "+ + "manager: %+v", channelID, err) } } @@ -103,7 +104,8 @@ func (n *notifications) addChannel(channelID *id.ID) { func (n *notifications) removeChannel(channelID *id.ID) { err := n.nm.Delete(channelID) if err != nil { - jww.WARN.Printf("[CH] Failed to remove channel (%s) from notifications manager: %+v", channelID, err) + jww.WARN.Printf("[CH] Failed to remove channel (%s) from notifications "+ + "manager: %+v", channelID, err) } } @@ -226,7 +228,8 @@ func (n *notifications) processesNotificationUpdates(group clientNotif.Group, changed := make([]NotificationState, 0, len(created)+len(edits)) nfs := make([]NotificationFilter, 0, len(group)) - tags := makeUserPingTags(n.pubKey) + tags := makeUserPingTags(map[PingType][]ed25519.PublicKey{ + ReplyPing: {n.pubKey}, MentionPing: {n.pubKey}}) for chanID, notif := range group { channelID := chanID.DeepCopy() @@ -303,6 +306,10 @@ type NotificationReport struct { // Type is the MessageType of the message that the notification belongs to. Type MessageType `json:"type"` + + // PingType describes the type of ping. If it is empty, then it is a generic + // ping. + PingType PingType `json:"pingType,omitempty"` } // GetNotificationReportsForMe checks the notification data against the filter @@ -331,10 +338,12 @@ func GetNotificationReportsForMe(nfs []NotificationFilter, if found { messageType := UnmarshalMessageType(b) - if nf.match(matchedTags, messageType) { + match, pt := nf.match(matchedTags, messageType) + if match { nr = append(nr, NotificationReport{ - Channel: nf.ChannelID, - Type: messageType, + Channel: nf.ChannelID, + Type: messageType, + PingType: pt, }) } } @@ -394,7 +403,9 @@ type AllowLists struct { // match determines if the message with the given tags and message type are // allowed through the filter. func (nf NotificationFilter) match( - matchedTags map[string]struct{}, mt MessageType) bool { + matchedTags map[string]struct{}, mt MessageType) (bool, PingType) { + pt := GenericPing + // Check if any filter tags match the matched tags for _, tag := range nf.Tags { @@ -402,18 +413,30 @@ func (nf NotificationFilter) match( // with tags list if _, exists := matchedTags[tag]; exists { if _, exists = nf.AllowWithTags[mt]; exists { - return true + currentPT, err := pingTypeFromTag(tag) + if err != nil { + jww.WARN.Printf( + "[CH] Failed to get ping type for tag %q: %+v", tag, err) + } + + pt = getRankingPingType(pt, currentPT) + } else { + return false, "" } - return false + } } + if pt != GenericPing { + return true, pt + } + // If no tag matches, then check if the message type is in the allowed // without tags list if _, exists := nf.AllowWithoutTags[mt]; exists { - return true + return true, pt } - return false + return false, "" } //////////////////////////////////////////////////////////////////////////////// @@ -438,7 +461,7 @@ var notificationLevelAllowLists = map[notificationSourceType]map[NotificationLev AllowWithoutTags: map[MessageType]struct{}{}, }, NotifyAll: { - AllowWithTags: map[MessageType]struct{}{}, + AllowWithTags: map[MessageType]struct{}{Text: {}}, AllowWithoutTags: map[MessageType]struct{}{Text: {}}, }, }, @@ -448,7 +471,7 @@ var notificationLevelAllowLists = map[notificationSourceType]map[NotificationLev AllowWithoutTags: map[MessageType]struct{}{Pinned: {}}, }, NotifyAll: { - AllowWithTags: map[MessageType]struct{}{}, + AllowWithTags: map[MessageType]struct{}{AdminText: {}, Pinned: {}}, AllowWithoutTags: map[MessageType]struct{}{AdminText: {}, Pinned: {}}, }, }, diff --git a/channels/notifications_test.go b/channels/notifications_test.go index e04488d8374992a368adc08e23244aa4645e2362..1d37db109f3b03d62339d44b70c0eab0e7686c21 100644 --- a/channels/notifications_test.go +++ b/channels/notifications_test.go @@ -11,6 +11,7 @@ import ( "bytes" "crypto/ed25519" "encoding/json" + "fmt" "math/rand" "reflect" "sort" @@ -320,17 +321,20 @@ func Test_notifications_processesNotificationUpdates(t *testing.T) { }) if level != NotifyNone { + tags := makeUserPingTags(map[PingType][]ed25519.PublicKey{ + ReplyPing: {n.pubKey}, MentionPing: {n.pubKey}}) + sort.Strings(tags) ex = append(ex, NotificationFilter{ Identifier: ch.broadcast.AsymmetricIdentifier(), ChannelID: channelID, - Tags: makeUserPingTags(n.pubKey), + Tags: tags, AllowLists: notificationLevelAllowLists[asymmetric][level], }, NotificationFilter{ Identifier: ch.broadcast.SymmetricIdentifier(), ChannelID: channelID, - Tags: makeUserPingTags(n.pubKey), + Tags: tags, AllowLists: notificationLevelAllowLists[symmetric][level], }) } @@ -359,6 +363,10 @@ func Test_notifications_processesNotificationUpdates(t *testing.T) { return bytes.Compare(nf[i].Identifier, nf[j].Identifier) == -1 }) + for i := range nf { + sort.Strings(nf[i].Tags) + } + if !reflect.DeepEqual(ex, nf) { t.Errorf("Unexpected filter list."+ "\nexpected: %+v\nreceived: %+v", ex, nf) @@ -386,6 +394,7 @@ func TestGetNotificationReportsForMe(t *testing.T) { types := []MessageType{Text, AdminText, Reaction, Delete, Pinned, Mute, AdminReplay, FileTransfer} levels := []NotificationLevel{NotifyPing, NotifyAll} + pingTypes := []PingType{ReplyPing, MentionPing} var expected []NotificationReport var notifData []*primNotif.Data @@ -400,7 +409,8 @@ func TestGetNotificationReportsForMe(t *testing.T) { identifier := append(chanID.Marshal(), []byte("identifier")...) tags := make([]string, 1+rng.Intn(4)) for j := range tags { - tags[j] = makeUserPingTag(makeEd25519PubKey(rng, t)) + tags[j] = makeUserPingTag(makeEd25519PubKey(rng, t), + pingTypes[rng.Intn(len(pingTypes))]) } mtByte := mt.Marshal() @@ -417,8 +427,13 @@ func TestGetNotificationReportsForMe(t *testing.T) { if includeChannel { var filterTags []string + var pt PingType if includeTags { filterTags = []string{tags[rng.Intn(len(tags))]} + pt, err = pingTypeFromTag(filterTags[0]) + if err != nil { + t.Errorf("Failed to get Ping Type: %+v", err) + } } nfs = append(nfs, NotificationFilter{ Identifier: identifier, @@ -436,8 +451,9 @@ func TestGetNotificationReportsForMe(t *testing.T) { } expected = append(expected, NotificationReport{ - Channel: chanID, - Type: mt, + Channel: chanID, + Type: mt, + PingType: pt, }) } } @@ -447,6 +463,9 @@ func TestGetNotificationReportsForMe(t *testing.T) { nrs := GetNotificationReportsForMe(nfs, notifData) + data, _ := json.MarshalIndent(nrs, "// ", " ") + fmt.Printf("// %s\n", data) + if !reflect.DeepEqual(nrs, expected) { t.Errorf("NotificationReport list does not match expected."+ "\nexpected: %+v\nreceived: %+v", expected, nrs) @@ -520,7 +539,8 @@ func TestNotificationFilter_JSON(t *testing.T) { nf := NotificationFilter{ Identifier: append(chanID.Marshal(), []byte("Identifier")...), ChannelID: chanID, - Tags: makeUserPingTags(makeEd25519PubKey(rng, t)), + Tags: makeUserPingTags(map[PingType][]ed25519.PublicKey{ + MentionPing: {makeEd25519PubKey(rng, t)}}), AllowLists: notificationLevelAllowLists[symmetric][NotifyPing], } @@ -553,7 +573,8 @@ func TestNotificationFilter_Slice_JSON(t *testing.T) { nfs[i] = NotificationFilter{ Identifier: append(chanID.Marshal(), []byte("Identifier")...), ChannelID: chanID, - Tags: makeUserPingTags(makeEd25519PubKey(rng, t)), + Tags: makeUserPingTags(map[PingType][]ed25519.PublicKey{ + MentionPing: {makeEd25519PubKey(rng, t)}}), AllowLists: notificationLevelAllowLists[sourceTypes[i%len(sourceTypes)]][levels[i%len(levels)]], } } diff --git a/channels/pingTags.go b/channels/pingTags.go new file mode 100644 index 0000000000000000000000000000000000000000..4ae28f1c9ed2907caabec24785b8b6247f13e5d9 --- /dev/null +++ b/channels/pingTags.go @@ -0,0 +1,86 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "crypto/ed25519" + "encoding/hex" + "strings" + + "github.com/pkg/errors" +) + +// PingType describes a user ping. It is used to describe more information about +// the ping to the user. +type PingType string + +const ( + // GenericPing indicates a generic notification that is not a reply or + // mention. + GenericPing PingType = "" + + // ReplyPing indicates that someone replied to this user's message. + ReplyPing PingType = "usrReply" + + // MentionPing indicates that someone mentioned (tagged) the user in a + // message. + MentionPing PingType = "usrMention" +) + +// getRankingPingType returns the ping that ranks higher. +func getRankingPingType(a, b PingType) PingType { + if pingHierarchy[a] > pingHierarchy[b] { + return a + } + return b +} + +// pingHierarchy describes the ranking of ping types. If a user receives a ping +// on multiple tags, the highest ranking ping tag is reported to the user. +var pingHierarchy = map[PingType]uint32{ + GenericPing: 0, + MentionPing: 100, + ReplyPing: 200, +} + +// delimiter between the public key and ping type in a ping tag. +const pingTagDelim = "-" + +// makeUserPingTags creates a list of tags to include in a cmix.Service from +// channel pings. +func makeUserPingTags(pings map[PingType][]ed25519.PublicKey) []string { + if pings == nil || len(pings) == 0 { + return nil + } + var tags []string + for pt, users := range pings { + s := make([]string, len(users)) + for i := range users { + s[i] = makeUserPingTag(users[i], pt) + } + tags = append(tags, s...) + } + + return tags +} + +// makeUserPingTag creates a tag from a user's public key and ping type to be +// used in a tag list in a cmix.Service. +func makeUserPingTag(user ed25519.PublicKey, pt PingType) string { + return hex.EncodeToString(user) + pingTagDelim + string(pt) +} + +// pingTypeFromTag gets the PingType from the ping tag. +func pingTypeFromTag(tag string) (PingType, error) { + s := strings.SplitN(tag, pingTagDelim, 2) + if len(s) < 2 { + return "", errors.New("invalid ping tag") + } + + return PingType(s[1]), nil +} diff --git a/channels/pingTags_test.go b/channels/pingTags_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0d98c8fd7a3963e806311729123c3a58af314c77 --- /dev/null +++ b/channels/pingTags_test.go @@ -0,0 +1,133 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "crypto/ed25519" + "math/rand" + "reflect" + "sort" + "testing" +) + +// Tests that all combinations of defined PingType passed into +// getRankingPingType result in the expected type. +func Test_getRankingPingType(t *testing.T) { + tests := []struct{ a, b, expected PingType }{ + {GenericPing, ReplyPing, ReplyPing}, + {GenericPing, MentionPing, MentionPing}, + {ReplyPing, GenericPing, ReplyPing}, + {ReplyPing, MentionPing, ReplyPing}, + {MentionPing, GenericPing, MentionPing}, + {MentionPing, ReplyPing, ReplyPing}, + } + + for i, tt := range tests { + pt := getRankingPingType(tt.a, tt.b) + if tt.expected != pt { + t.Errorf("Unexpected PingType when comparing %q with %q (%d)."+ + "\nexpected: %q\nreceived: %q", tt.a, tt.b, i, tt.expected, pt) + } + } +} + +// Unit test of makeUserPingTags. +func Test_makeUserPingTags(t *testing.T) { + prng := rand.New(rand.NewSource(579498)) + types := []PingType{ReplyPing, MentionPing} + + for i := 0; i < 6; i++ { + pings := map[PingType][]ed25519.PublicKey{ + ReplyPing: {}, MentionPing: {}, + } + expected := make([]string, i) + for j := 0; j < i; j++ { + pt := types[prng.Intn(len(types))] + user, _, _ := ed25519.GenerateKey(prng) + pings[pt] = append(pings[pt], user) + expected[j] = makeUserPingTag(user, pt) + } + if i == 0 { + expected = nil + } + tags := makeUserPingTags(pings) + sort.Strings(expected) + sort.Strings(tags) + if !reflect.DeepEqual(expected, tags) { + t.Errorf("Unexpected tags (%d).\nexpected: %#v\nreceived: %#v", + i, expected, tags) + } + } +} + +// Consistency test of makeUserPingTag. +func TestManager_makeUserPingTag_Consistency(t *testing.T) { + prng := rand.New(rand.NewSource(579498)) + expected := []string{ + "32b15a6bd6db85bf239a7fe6aeb84072c2646b96188c9c4f7dec6d35928436c2-usrReply", + "88ab667cdc60e041849a924ed7b6e60fa2de62eca4b67336c86e7620a19d270f-usrReply", + "ddab884d040d9182950aca0eab2899977caf62eef098d0973f99aa1b4c97f541-usrMention", + "76341ec7e5da6fe34b80021c4e0f6362d3a1a37091126f8cb74ec8ee5bb375fb-usrMention", + "d0f05d30b6d62460788ac7a7bde3b0de8065c6b1673ba3a724af90e87f1a152e-usrMention", + "69a8a814d254be6d3652c5f013d063803c6542a94c0dcd52cb9d9f68bcf14041-usrMention", + "215a17025a25d892baa595d6677046fe74d625505ad65a64d367975c94477831-usrMention", + "9b6400094a53921ff041c024265748df635843cb6a50e8a4d6f68f7c52f9e766-usrMention", + "456e99e886cf889b6b9ce63ecece752267b199032a2236239aaedf9d519a491b-usrReply", + "2cf8839b549c5b360fb5434de944918d5680e203e173889fd48dd4c91099762e-usrMention", + "a0a48cffe820a8f7e213b6bb99b99c1270fc8126bbde165b756449895f6f6603-usrReply", + "d1086f945874607d89d9cc59c6e940217bd69c8d71dc45a0f3728cb6146ae154-usrReply", + "db60467fc127405470c73cbc4734d1098d43e85c61904a114d95466cfb30781c-usrReply", + "7bd7b36cf1393fdf8576df1182e5b668bb3f021aa4a1b562d885711153a30395-usrReply", + "61a25b1fbb5dd9490d43b64d6899ae3f1fac2c2e7bf8cade5fbbcced897f10fe-usrReply", + "689c8cc76cb15c457f6666457b360701d4f1a6312f6f1a1f1c910353a5d0ce10-usrMention", + "64c7b1b343f38077d30cb9b99a0ed2844974d984fbd9630999a4a59069a964a7-usrReply", + "f56ee72f86ee39d52887a08927606bce8a9d21726a82792655aee098705c48a9-usrReply", + "008da28a9f8f70dafe533283acabbed77cb92bf5058f044bb50e1167ce6f05b4-usrMention", + "70a659261ad311cf6111639ab6be9677f5afc0ab9e13546b3afc64597ccaabfd-usrReply", + } + types := []PingType{ReplyPing, MentionPing} + + for i, exp := range expected { + pubKey, _, _ := ed25519.GenerateKey(prng) + tag := makeUserPingTag(pubKey, types[prng.Intn(len(types))]) + if exp != tag { + t.Errorf("Unexpected tag for key %X (%d)."+ + "\nexpected: %s\nreceived: %s", pubKey, i, exp, tag) + } + } +} + +// Unit test of pingTypeFromTag. +func Test_pingTypeFromTag(t *testing.T) { + prng := rand.New(rand.NewSource(65881)) + types := []PingType{ReplyPing, MentionPing} + + for i := 0; i < 10; i++ { + pubKey, _, _ := ed25519.GenerateKey(prng) + expected := types[prng.Intn(len(types))] + tag := makeUserPingTag(pubKey, expected) + + pt, err := pingTypeFromTag(tag) + if err != nil { + t.Errorf("Failed to get type (%d): %+v", i, err) + } + if expected != pt { + t.Errorf("Unexpected PingType (%d)."+ + "\nexpected: %s\nreceived: %s", i, expected, pt) + } + } +} + +// Error path: Tests that pingTypeFromTag returns an error for an invalid tag. +func Test_typeFromPingTag_InvalidTagError(t *testing.T) { + _, err := pingTypeFromTag( + "64c7b1b343f38077d30cb9b99a0ed2844974d984fbd9630999a4a59069a964a7") + if err == nil { + t.Errorf("Did not get error for invalid tag.") + } +} diff --git a/channels/send.go b/channels/send.go index f91d323b9e6344c18bbaf7e59030bcab11595347..c0f58e442cc7b5d983fe70b0c07df32874f04489 100644 --- a/channels/send.go +++ b/channels/send.go @@ -13,20 +13,21 @@ import ( "crypto/hmac" "encoding/base64" "fmt" - cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" "time" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" + "golang.org/x/crypto/blake2b" + "google.golang.org/protobuf/proto" + "gitlab.com/elixxir/client/v4/cmix" "gitlab.com/elixxir/client/v4/cmix/rounds" "gitlab.com/elixxir/client/v4/emoji" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" "gitlab.com/elixxir/crypto/message" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" "gitlab.com/xx_network/primitives/netTime" - "golang.org/x/crypto/blake2b" - "google.golang.org/protobuf/proto" ) const ( @@ -109,7 +110,7 @@ func timeNow() string { return netTime.Now().Format("15:04:05.9999999") } // for this message. They must be in the channel and have notifications enabled func (m *manager) SendGeneric(channelID *id.ID, messageType MessageType, msg []byte, validUntil time.Duration, tracked bool, params cmix.CMIXParams, - pings []ed25519.PublicKey) ( + pingsMap map[PingType][]ed25519.PublicKey) ( message.ID, rounds.Round, ephemeral.Id, error) { if hmac.Equal(channelID.Bytes(), emptyChannelID.Bytes()) { @@ -242,7 +243,7 @@ func (m *manager) SendGeneric(channelID *id.ID, messageType MessageType, } log += fmt.Sprintf("Broadcasting message at %s. ", timeNow()) - tags := makeUserPingTags(pings...) + tags := makeUserPingTags(pingsMap) mt := messageType.Marshal() r, ephID, err := ch.broadcast.BroadcastWithAssembler(assemble, tags, [2]byte{mt[0], mt[1]}, params) @@ -301,8 +302,10 @@ func (m *manager) SendMessage(channelID *id.ID, msg string, return message.ID{}, rounds.Round{}, ephemeral.Id{}, err } + pingMap := map[PingType][]ed25519.PublicKey{MentionPing: pings} + return m.SendGeneric( - channelID, Text, txtMarshaled, validUntil, true, params, pings) + channelID, Text, txtMarshaled, validUntil, true, params, pingMap) } // SendReply is used to send a formatted message over a channel. @@ -339,8 +342,20 @@ func (m *manager) SendReply(channelID *id.ID, msg string, return message.ID{}, rounds.Round{}, ephemeral.Id{}, err } + // Get the public key of the sender + mm, err := m.events.model.GetMessage(replyTo) + if err != nil { + return message.ID{}, rounds.Round{}, ephemeral.Id{}, errors.Wrapf(err, + "failed getting message %s from event model", replyTo) + } + + pingMap := map[PingType][]ed25519.PublicKey{ + ReplyPing: {mm.PubKey}, + MentionPing: pings, + } + return m.SendGeneric( - channelID, Text, txtMarshaled, validUntil, true, params, pings) + channelID, Text, txtMarshaled, validUntil, true, params, pingMap) } // SendReaction is used to send a reaction to a message over a channel. The @@ -471,9 +486,11 @@ func (m *manager) SendInvite(channelID *id.ID, msg string, return message.ID{}, rounds.Round{}, ephemeral.Id{}, err } + pingMap := map[PingType][]ed25519.PublicKey{MentionPing: pings} + // Send invitation return m.SendGeneric(channelID, Invitation, invitationMarshalled, - validUntil, true, params, pings) + validUntil, true, params, pingMap) } // replayAdminMessage is used to rebroadcast an admin message asa a norma user. @@ -791,21 +808,5 @@ func makeChaDebugTag( h.Write(id) tripCode := base64.RawStdEncoding.EncodeToString(h.Sum(nil))[:12] - return fmt.Sprintf("%s-%s", baseTag, tripCode) -} - -func makeUserPingTags(users ...ed25519.PublicKey) []string { - if users == nil || len(users) == 0 { - return nil - } - s := make([]string, len(users)) - for i := 0; i < len(s); i++ { - s[i] = makeUserPingTag(users[i]) - } - return s -} - -func makeUserPingTag(user ed25519.PublicKey) string { - return fmt.Sprintf("%x-usrping", user) - + return baseTag + "-" + tripCode } diff --git a/channels/send_test.go b/channels/send_test.go index b09b33d5c66bf67e04d6b391c300d11880ee884f..9881a7406b0dae488399a97225912e848e7bf7e3 100644 --- a/channels/send_test.go +++ b/channels/send_test.go @@ -14,27 +14,25 @@ import ( "testing" "time" + "github.com/golang/protobuf/proto" "github.com/stretchr/testify/require" - "gitlab.com/elixxir/client/v4/collective" - "github.com/golang/protobuf/proto" + "gitlab.com/elixxir/client/v4/broadcast" + "gitlab.com/elixxir/client/v4/cmix" "gitlab.com/elixxir/client/v4/cmix/identity/receptionID" "gitlab.com/elixxir/client/v4/cmix/rounds" + "gitlab.com/elixxir/client/v4/collective" "gitlab.com/elixxir/client/v4/collective/versioned" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" cryptoChannel "gitlab.com/elixxir/crypto/channel" "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/elixxir/crypto/message" "gitlab.com/elixxir/crypto/rsa" "gitlab.com/elixxir/ekv" "gitlab.com/xx_network/crypto/csprng" - "gitlab.com/xx_network/primitives/netTime" - "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" - - "gitlab.com/elixxir/client/v4/broadcast" - "gitlab.com/elixxir/client/v4/cmix" - cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + "gitlab.com/xx_network/primitives/netTime" ) func Test_manager_SendGeneric(t *testing.T) { @@ -289,7 +287,7 @@ func Test_manager_SendReply(t *testing.T) { channels: make(map[id.ID]*joinedChannel), local: kv, rng: crng, - events: initEvents(&mockEventModel{}, 512, kv, crng), + events: initEvents(&MockEvent{}, 512, kv, crng), nicknameManager: &nicknameManager{byChannel: make(map[id.ID]string), remote: nil}, st: loadSendTracker(&mockBroadcastClient{}, kv, func(*id.ID, *userMessageInternal, []byte, time.Time, @@ -816,6 +814,7 @@ func Test_manager_MuteUser(t *testing.T) { } } + //////////////////////////////////////////////////////////////////////////////// // Mock Interfaces // //////////////////////////////////////////////////////////////////////////////// diff --git a/channelsFileTransfer/utils_test.go b/channelsFileTransfer/utils_test.go index 656af2634d8eb3e68790e1e1f63b9901cde6f43f..e11f5e45d4f4f3cc73e5ca1d9fb63af53fceb71e 100644 --- a/channelsFileTransfer/utils_test.go +++ b/channelsFileTransfer/utils_test.go @@ -582,21 +582,19 @@ func (m *mockChannelsManager) GetChannels() []*id.ID { pani func (m *mockChannelsManager) GetChannel(*id.ID) (*cryptoBroadcast.Channel, error) { panic("implement me") } -func (m *mockChannelsManager) SendSilent(channelID *id.ID, validUntil time.Duration, params cmix.CMIXParams) (cryptoMessage.ID, rounds.Round, ephemeral.Id, error) { +func (m *mockChannelsManager) SendSilent(*id.ID, time.Duration, cmix.CMIXParams) (cryptoMessage.ID, rounds.Round, ephemeral.Id, error) { panic("implement me") } - -func (m *mockChannelsManager) SendInvite(channelID *id.ID, msg string, inviteTo *cryptoBroadcast.Channel, host string, validUntil time.Duration, params cmix.CMIXParams, pings []ed25519.PublicKey) (cryptoMessage.ID, rounds.Round, ephemeral.Id, error) { +func (m *mockChannelsManager) SendInvite(*id.ID, string, *cryptoBroadcast.Channel, string, time.Duration, cmix.CMIXParams, []ed25519.PublicKey) (cryptoMessage.ID, rounds.Round, ephemeral.Id, error) { panic("implement me") } - -func (m *mockChannelsManager) GetNotificationStatus(channelID *id.ID) (clientNotif.NotificationState, error) { +func (m *mockChannelsManager) GetNotificationStatus(*id.ID) (clientNotif.NotificationState, error) { panic("implement me") } func (m *mockChannelsManager) SendGeneric(channelID *id.ID, messageType channels.MessageType, msg []byte, validUntil time.Duration, - _ bool, _ cmix.CMIXParams, _ []ed25519.PublicKey) ( + _ bool, _ cmix.CMIXParams, _ map[channels.PingType][]ed25519.PublicKey) ( cryptoMessage.ID, rounds.Round, ephemeral.Id, error) { msgID := cryptoMessage.DeriveChannelMessageID(channelID, 0, msg) @@ -645,11 +643,10 @@ func (m *mockChannelsManager) GetNickname(*id.ID) (nickname string, exists bool) func (m *mockChannelsManager) Muted(*id.ID) bool { panic("implement me") } func (m *mockChannelsManager) GetMutedUsers(*id.ID) []ed25519.PublicKey { panic("implement me") } -func (m *mockChannelsManager) GetNotificationLevel(channelID *id.ID) (channels.NotificationLevel, error) { +func (m *mockChannelsManager) GetNotificationLevel(*id.ID) (channels.NotificationLevel, error) { panic("implement me") } -func (m *mockChannelsManager) SetMobileNotificationsLevel(channelID *id.ID, level channels.NotificationLevel, - status clientNotif.NotificationState) error { +func (m *mockChannelsManager) SetMobileNotificationsLevel(*id.ID, channels.NotificationLevel, clientNotif.NotificationState) error { panic("implement me") } func (m *mockChannelsManager) IsChannelAdmin(*id.ID) bool { panic("implement me") } diff --git a/channelsFileTransfer/wrapper.go b/channelsFileTransfer/wrapper.go index 7b9e6b9e724fb90065677a75666909c76a6937d4..6c4fcaf35f486b21d9833ee2279792802274e7da 100644 --- a/channelsFileTransfer/wrapper.go +++ b/channelsFileTransfer/wrapper.go @@ -343,8 +343,11 @@ func (w *Wrapper) Send(channelID *id.ID, fileLink []byte, fileName, errors.Wrap(err, "error JSON marshalling file info") } + pingMap := map[channels.PingType][]ed25519.PublicKey{ + channels.MentionPing: pings} + return w.ch.SendGeneric(channelID, channels.FileTransfer, - fileInfo, validUntil, true, params.CMIX, pings) + fileInfo, validUntil, true, params.CMIX, pingMap) } // RegisterSentProgressCallback registers the callback to the given file