Closed nicksellen closed 10 months ago
I ran a quick test in the backend. When frontend sends seen_up_to
, the backend seems to create a participant and save it, even when there was no participant before. I guess this shouldn't happen and we can prevent it in backend. Maybe by returning an error. Then we go to frontend and make sure that it doesn't trigger the error...
So in that sense, backend is allowed to create a participant
when the notifications
field is set properly, but otherwise not.
def test_mark_seen_up_to_when_not_in_conversation(self):
user = UserFactory()
group = GroupFactory(members=[self.user2, user])
conversation = ConversationFactory(group=group, participants=[self.user2])
message = conversation.messages.create(author=self.user2, content='yay')
self.client.force_login(user=user)
response = self.client.get('/api/conversations/{}/'.format(conversation.id), format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['seen_up_to'], None)
self.assertFalse(ConversationParticipant.objects.filter(conversation=conversation.id, user=user.id).exists())
data = {'seen_up_to': message.id}
response = self.client.patch('/api/conversations/{}/'.format(conversation.id), data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['seen_up_to'], message.id)
participant = ConversationParticipant.objects.get(conversation=conversation.id, user=user.id)
self.assertFalse(ConversationParticipant.objects.filter(conversation=conversation.id, user=user.id).exists())
print(participant)
self.assertEqual(participant.seen_up_to, message)
Great, sounds promising.... I wonder if it's possible to recreate the frontend sending spurious seen_up_to
API requests? That would make it feel quite likely to be the cause. I couldn't find an obvious way from a glance...
We do check whether the user is a participant here before marking as read:
But in any case, the backend fix could come first. And then we'll see the failures pop up in frontend sentry.
The report is that users can receive notifications for application chats that they did not intend to participate in.
There is no suggestion or evidence that users are receiving messages that they are not allowed to receive, only that their preference is not to receive them.
I compiled a timeline from a reported case. There are 2 users we care about:
[applying]
is the user applying to the group[notified]
is the user that got notified when they did not expect it[other]
is any other user, not relevant to this(The script to generate the timeline is https://gist.github.com/nicksellen/0a69c7130030f3b8f52d3df574d261e3)
The interesting thing from here is when
[notified]
joined the conversation, it is not when the application was created, nor when they sent their first message (which was in response to the notification, via email), but a couple of days after.Unfortunately, the systemd logs are not available for that time any more. I've enabled persistent logs for journald that might keep them longer... (there are lots of configuration options!).
So, the key question is: why was the user added to the conversation. I have not been able to determine that yet.
One area I am suspicious about is the
/conversations/<id>/
update endpoint, which is used for marking where users have read up to, changing notification settings, and maybe other stuff.The code fetches the participant for the conversation, and if they are not part of it, we create a temporary/mock participant to use for the purpose of the request, which may be saved (e.g. they turn notifications on, which is effectively joining the conversation).
Code where we create the temporary/mock participant:
https://github.com/karrot-dev/karrot-backend/blob/9b3c3710792323223023a47459b2b0b95cf426f9/karrot/conversations/api.py#L174-L178
I wonder if that participant is being saved inadvertently?
I would have liked to look at the logs to look for any relevant requests at that time, but they are not available.
I have looked through the code and the update method seems the most likely to save this temporary participant inadvertently:
https://github.com/karrot-dev/karrot-backend/blob/9b3c3710792323223023a47459b2b0b95cf426f9/karrot/conversations/serializers.py#L408-L423
To my mind there are two scenarios I'd want to look at in more detail:
seen_up_to
(and no value fornotifications
), which then saves the participantnotifications
are set to none,participant
is returned from that, does it send something back to the frontend to thinking the user is in the conversation, then subsequently send aseen_up_to
update?I have no evidence for the above to scenarios though.