Skip to content

Commit cd53927

Browse files
committed
fix(reactions): limit reactions to 20 per message
Needed for status-im/status-desktop#18843 Blocks sending or receiving more than 20 different types of reactions on a single message. Adds a functional test testing it all
1 parent 55b9c9f commit cd53927

File tree

6 files changed

+239
-7
lines changed

6 files changed

+239
-7
lines changed

β€Žprotocol/message_persistence.goβ€Ž

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1846,6 +1846,52 @@ func (db sqlitePersistence) EmojiReactionsByChatIDMessageID(chatID string, messa
18461846
return result, nil
18471847
}
18481848

1849+
// EmojiReactionCountByChatIDMessageID returns the count of different emoji types on a message.
1850+
func (db sqlitePersistence) EmojiReactionCountByChatIDMessageID(chatID string, messageID string) (int, error) {
1851+
args := []interface{}{chatID, messageID}
1852+
query := `SELECT
1853+
COUNT(DISTINCT e.emoji)
1854+
FROM
1855+
emoji_reactions e
1856+
WHERE NOT(e.retracted)
1857+
AND
1858+
e.local_chat_id = ?
1859+
AND
1860+
e.message_id = ?`
1861+
1862+
var count int
1863+
err := db.db.QueryRow(query, args...).Scan(&count)
1864+
if err != nil {
1865+
return 0, err
1866+
}
1867+
1868+
return count, nil
1869+
}
1870+
1871+
// EmojiReactionExistsOnMessage checks if a particular emoji type has already been added to a specific message.
1872+
func (db sqlitePersistence) EmojiReactionExistsOnMessage(chatID string, messageID string, emoji string) (bool, error) {
1873+
args := []interface{}{chatID, messageID, emoji}
1874+
query := `SELECT
1875+
COUNT(*)
1876+
FROM
1877+
emoji_reactions e
1878+
WHERE NOT(e.retracted)
1879+
AND
1880+
e.local_chat_id = ?
1881+
AND
1882+
e.message_id = ?
1883+
AND
1884+
e.emoji = ?`
1885+
1886+
var count int
1887+
err := db.db.QueryRow(query, args...).Scan(&count)
1888+
if err != nil {
1889+
return false, err
1890+
}
1891+
1892+
return count > 0, nil
1893+
}
1894+
18491895
// EmojiReactionsByChatIDs returns the emoji reactions for the queried messages, up to a maximum of 100, as it's a potentially unbound number.
18501896
// NOTE: This is not completely accurate, as the messages in the database might have change since the last call to `MessageByChatID`.
18511897
func (db sqlitePersistence) EmojiReactionsByChatIDs(chatIDs []string, currCursor string, limit int) ([]*EmojiReaction, error) {

β€Žprotocol/messenger_emoji_reactions.goβ€Ž

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ import (
1111
"github.com/status-im/status-go/protocol/protobuf"
1212
)
1313

14+
var ErrTooManyEmojiReactionsForMessage = errors.New("too many emoji reactions for message")
15+
16+
const maxEmojiReactionsPerMessage = 20
17+
1418
func ConvertEmojiIDToString(emojiID protobuf.EmojiReaction_Type) string {
1519
switch emojiID {
1620
case protobuf.EmojiReaction_LOVE:
@@ -31,6 +35,27 @@ func ConvertEmojiIDToString(emojiID protobuf.EmojiReaction_Type) string {
3135

3236
}
3337

38+
func (m *Messenger) CheckMaxNumberOfEmojiReactionsPerMessage(chatID string, messageID string, emoji string) error {
39+
// Limit the amount of emoji reactions per message to avoid spam
40+
numberOfEmojis, err := m.persistence.EmojiReactionCountByChatIDMessageID(chatID, messageID)
41+
if err != nil {
42+
return err
43+
}
44+
45+
if numberOfEmojis < maxEmojiReactionsPerMessage {
46+
return nil
47+
}
48+
// We need to check if it's an emoji that was already applied. If not, then we throw an error
49+
exists, err := m.persistence.EmojiReactionExistsOnMessage(chatID, messageID, emoji)
50+
if err != nil {
51+
return err
52+
}
53+
if !exists {
54+
return ErrTooManyEmojiReactionsForMessage
55+
}
56+
return nil
57+
}
58+
3459
// TODO remove emojiID once the client supports sending custom emojis
3560
func (m *Messenger) SendEmojiReaction(ctx context.Context, chatID, messageID string, emojiID protobuf.EmojiReaction_Type, emoji string) (*MessengerResponse, error) {
3661
var response MessengerResponse
@@ -53,6 +78,11 @@ func (m *Messenger) SendEmojiReaction(ctx context.Context, chatID, messageID str
5378
return nil, errors.New("invalid emoji")
5479
}
5580

81+
err := m.CheckMaxNumberOfEmojiReactionsPerMessage(chatID, messageID, emoji)
82+
if err != nil {
83+
return nil, err
84+
}
85+
5686
emojiR := &EmojiReaction{
5787
EmojiReaction: &protobuf.EmojiReaction{
5888
Clock: clock,

β€Žprotocol/messenger_emoji_test.goβ€Ž

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/status-im/status-go/crypto"
1212
"github.com/status-im/status-go/crypto/types"
13+
messagingtypes "github.com/status-im/status-go/messaging/types"
1314
"github.com/status-im/status-go/multiaccounts"
1415
"github.com/status-im/status-go/protocol/common"
1516
"github.com/status-im/status-go/protocol/protobuf"
@@ -180,3 +181,94 @@ func (s *MessengerEmojiSuite) TestCompressedKeyReturnedWithEmoji() {
180181
s.Require().True(strings.Contains(string(encodedReaction), "compressedKey\":\"zQ"))
181182
s.Require().True(strings.Contains(string(encodedReaction), "emojiHash"))
182183
}
184+
185+
func (s *MessengerEmojiSuite) TestMaxEmojiReactionsPerMessage() {
186+
alice := s.m
187+
alice.account = &multiaccounts.Account{KeyUID: "0xdeadbeef"}
188+
189+
bob := s.newMessenger()
190+
defer TearDownMessenger(&s.Suite, bob)
191+
192+
chatID := statusChatID
193+
194+
chat := CreatePublicChat(chatID, alice.getTimesource())
195+
196+
err := alice.SaveChat(chat)
197+
s.Require().NoError(err)
198+
199+
_, err = alice.Join(chat)
200+
s.Require().NoError(err)
201+
202+
err = bob.SaveChat(chat)
203+
s.Require().NoError(err)
204+
205+
_, err = bob.Join(chat)
206+
s.Require().NoError(err)
207+
208+
// Send chat message from alice to bob
209+
210+
message := buildTestMessage(*chat)
211+
_, err = alice.SendChatMessage(context.Background(), message)
212+
s.NoError(err)
213+
214+
// Wait for message to arrive to bob
215+
response, err := WaitOnMessengerResponse(
216+
bob,
217+
func(r *MessengerResponse) bool { return len(r.Messages()) > 0 },
218+
"no messages",
219+
)
220+
s.Require().NoError(err)
221+
222+
s.Require().Len(response.Messages(), 1)
223+
224+
messageID := response.Messages()[0].ID
225+
226+
// Respond with the max amount of emojis
227+
emojis := []string{"πŸ˜€", "πŸ€“", "πŸ˜„", "😁", "πŸ˜†", "πŸ˜…", "🀣", "πŸ˜‚", "πŸ₯Ή", "πŸ™‚", "πŸ™ƒ", "πŸ˜‰", "😊", "πŸ˜‡", "πŸ₯°", "😍", "🀩", "😘", "πŸ˜—", "☺️"}
228+
for _, emoji := range emojis {
229+
response, err = bob.SendEmojiReaction(context.Background(), chat.ID, messageID, protobuf.EmojiReaction_UNKNOWN_EMOJI_REACTION_TYPE, emoji)
230+
s.Require().NoError(err)
231+
s.Require().Len(response.EmojiReactions(), 1)
232+
}
233+
234+
// Try sending one more (should fail)
235+
_, err = bob.SendEmojiReaction(context.Background(), chat.ID, messageID, protobuf.EmojiReaction_UNKNOWN_EMOJI_REACTION_TYPE, "πŸ˜‹")
236+
s.Require().Error(err)
237+
s.Require().Equal(ErrTooManyEmojiReactionsForMessage, err)
238+
239+
messageState := bob.buildMessageState()
240+
messageState.CurrentMessageState = &CurrentMessageState{
241+
Contact: &Contact{
242+
ID: common.PubkeyToHex(&alice.identity.PublicKey),
243+
},
244+
}
245+
246+
// Try handling a new emoji (like if it was sent by Alice) (should fail)
247+
err = bob.HandleEmojiReaction(
248+
messageState,
249+
&protobuf.EmojiReaction{
250+
MessageId: messageID,
251+
ChatId: chat.ID,
252+
Emoji: "πŸ˜‹",
253+
Type: protobuf.EmojiReaction_UNKNOWN_EMOJI_REACTION_TYPE,
254+
Clock: 123,
255+
MessageType: protobuf.MessageType_PUBLIC_GROUP,
256+
},
257+
&messagingtypes.Message{},
258+
)
259+
s.Require().Error(err)
260+
s.Require().Equal(ErrTooManyEmojiReactionsForMessage, err)
261+
262+
// Sending an existing emoji should work
263+
response, err = alice.SendEmojiReaction(context.Background(), chat.ID, messageID, protobuf.EmojiReaction_UNKNOWN_EMOJI_REACTION_TYPE, "πŸ˜€")
264+
s.Require().NoError(err)
265+
s.Require().Len(response.EmojiReactions(), 1)
266+
267+
_, err = WaitOnMessengerResponse(
268+
bob,
269+
func(r *MessengerResponse) bool { return len(r.EmojiReactions()) == 1 },
270+
"no emoji",
271+
)
272+
s.Require().NoError(err)
273+
274+
}

β€Žprotocol/messenger_handler.goβ€Ž

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2927,6 +2927,11 @@ func (m *Messenger) HandleEmojiReaction(state *ReceivedMessageState, pbEmojiR *p
29272927
return nil
29282928
}
29292929

2930+
err = m.CheckMaxNumberOfEmojiReactionsPerMessage(emojiReaction.ChatId, emojiReaction.MessageId, pbEmojiR.Emoji)
2931+
if err != nil {
2932+
return err
2933+
}
2934+
29302935
chat, err := m.matchChatEntity(emojiReaction, protobuf.ApplicationMetadataMessage_EMOJI_REACTION)
29312936
if err != nil {
29322937
return err // matchChatEntity returns a descriptive error message

β€Žtests-functional/clients/services/wakuext.pyβ€Ž

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,3 +510,13 @@ def peer_id(self):
510510
params = []
511511
response = self.rpc_request("peerID", params)
512512
return response
513+
514+
def send_emoji_reaction(self, chat_id: str, message_id: str, emoji: str):
515+
params = [chat_id, message_id, emoji]
516+
response = self.rpc_request(method="sendEmojiReactionV2", params=params)
517+
return response
518+
519+
def send_emoji_reaction_retraction(self, last_emoji_id: str):
520+
params = [last_emoji_id]
521+
response = self.rpc_request(method="sendEmojiReactionRetraction", params=params)
522+
return response

β€Žtests-functional/tests/test_wakuext_message_reactions.pyβ€Ž

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1+
import re
12
import time
2-
33
import pytest
44

55
from clients.signals import SignalType
6+
from clients.api import ApiResponseError
67
from resources.enums import MessageContentType
78
from steps.messenger import MessengerSteps
89

@@ -69,9 +70,8 @@ def test_one_to_one_message_reactions(self, backend_new_profile, waku_light_clie
6970
response = self.sender.wakuext_service.send_one_to_one_message(self.receiver.public_key, "test_message 1")
7071
message_1 = self.get_message_by_content_type(response, content_type=MessageContentType.TEXT_PLAIN.value)[0]
7172
# Send emoji reaction (V2)
72-
emoji_1_id = self.receiver.wakuext_service.rpc_request(method="sendEmojiReactionV2", params=[receiver_chat_id, message_1["id"], "πŸ™‚"])[
73-
"emojiReactions"
74-
][0]["id"]
73+
response = self.receiver.wakuext_service.send_emoji_reaction(receiver_chat_id, message_1["id"], "πŸ™‚")
74+
emoji_1_id = response["emojiReactions"][0]["id"]
7575
self.sender.find_signal_containing_pattern(
7676
SignalType.MESSAGES_NEW.value,
7777
event_pattern=emoji_1_id,
@@ -80,9 +80,8 @@ def test_one_to_one_message_reactions(self, backend_new_profile, waku_light_clie
8080

8181
response = self.receiver.wakuext_service.send_one_to_one_message(self.sender.public_key, "test_message 2")
8282
message_2 = self.get_message_by_content_type(response, content_type=MessageContentType.TEXT_PLAIN.value)[0]
83-
emoji_2_id = self.sender.wakuext_service.rpc_request(method="sendEmojiReactionV2", params=[sender_chat_id, message_2["id"], "πŸ™"])[
84-
"emojiReactions"
85-
][0]["id"]
83+
response = self.sender.wakuext_service.send_emoji_reaction(sender_chat_id, message_2["id"], "πŸ™")
84+
emoji_2_id = response["emojiReactions"][0]["id"]
8685
self.receiver.find_signal_containing_pattern(
8786
SignalType.MESSAGES_NEW.value,
8887
event_pattern=emoji_2_id,
@@ -106,3 +105,53 @@ def test_one_to_one_message_reactions(self, backend_new_profile, waku_light_clie
106105
item["emoji"] == "πŸ™‚",
107106
)
108107
)
108+
109+
@pytest.mark.parametrize("waku_light_client", [False], indirect=True, ids=["wakuV2LightClient_False"])
110+
def test_limit_of_20_reactions(self, backend_new_profile, waku_light_client):
111+
"""Test that you cannot send more than 20 message reactions on a single message"""
112+
# Initialize two backends (sender and receiver) for this test
113+
sender = backend_new_profile("sender", waku_light_client=waku_light_client)
114+
receiver = backend_new_profile("receiver", waku_light_client=waku_light_client)
115+
116+
self.make_contacts(sender, receiver)
117+
response = sender.wakuext_service.send_one_to_one_message(receiver.public_key, "test_message")
118+
message = self.get_message_by_content_type(response, content_type=MessageContentType.TEXT_PLAIN.value)[0]
119+
message_id, sender_chat_id = message["id"], message["chatId"]
120+
receiver_chat_id = receiver.wakuext_service.rpc_request(method="chats")[0]["id"]
121+
122+
# Send 20 emojis
123+
emojis = ["πŸ˜€", "πŸ€“", "πŸ˜„", "😁", "πŸ˜†", "πŸ˜…", "🀣", "πŸ˜‚", "πŸ₯Ή", "πŸ™‚", "πŸ™ƒ", "πŸ˜‰", "😊", "πŸ˜‡", "πŸ₯°", "😍", "🀩", "😘", "πŸ˜—", "☺️"]
124+
last_emoji_id = None
125+
for emoji in emojis:
126+
response = sender.wakuext_service.send_emoji_reaction(sender_chat_id, message_id, emoji)
127+
assert response["emojiReactions"][0]["emoji"] == emoji
128+
last_emoji_id = response["emojiReactions"][0]["id"]
129+
130+
# The 21st emoji sent should fail
131+
with pytest.raises(ApiResponseError, match=re.escape("too many emoji reactions for message")):
132+
_ = sender.wakuext_service.send_emoji_reaction(sender_chat_id, message_id, "πŸ˜‹")
133+
134+
# Test retract the 20th emoji and adding a new one
135+
response = sender.wakuext_service.send_emoji_reaction_retraction(last_emoji_id)
136+
137+
response = sender.wakuext_service.send_emoji_reaction(sender_chat_id, message_id, "βš“οΈ")
138+
assert response["emojiReactions"][0]["emoji"] == "βš“οΈ"
139+
new_emoji_id = response["emojiReactions"][0]["id"]
140+
141+
# Wait for receiver to get the reaction
142+
receiver.find_signal_containing_pattern(
143+
SignalType.MESSAGES_NEW.value,
144+
event_pattern=new_emoji_id,
145+
timeout=60,
146+
)
147+
148+
# Test receiver sending the SAME type of a previous reaction (should be allowed)
149+
response = receiver.wakuext_service.send_emoji_reaction(receiver_chat_id, message_id, "βš“οΈ")
150+
emoji_2_id = response["emojiReactions"][0]["id"]
151+
assert response["emojiReactions"][0]["emoji"] == "βš“οΈ"
152+
153+
sender.find_signal_containing_pattern(
154+
SignalType.MESSAGES_NEW.value,
155+
event_pattern=emoji_2_id,
156+
timeout=60,
157+
)

0 commit comments

Comments
Β (0)