diff --git a/zerver/lib/cache.py b/zerver/lib/cache.py index 9ddb6577e2..1c54a6b377 100644 --- a/zerver/lib/cache.py +++ b/zerver/lib/cache.py @@ -534,6 +534,10 @@ def realm_user_dicts_cache_key(realm_id: int) -> str: return f"realm_user_dicts:{realm_id}" +def get_muting_users_cache_key(muted_user: "UserProfile") -> str: + return f"muting_users_list:{muted_user.id}" + + def get_realm_used_upload_space_cache_key(realm: "Realm") -> str: return f"realm_used_upload_space:{realm.id}" @@ -642,6 +646,11 @@ def flush_user_profile(sender: Any, **kwargs: Any) -> None: cache_delete(bot_dicts_in_realm_cache_key(user_profile.realm)) +def flush_muting_users_cache(sender: Any, **kwargs: Any) -> None: + mute_object = kwargs["instance"] + cache_delete(get_muting_users_cache_key(mute_object.muted_user)) + + # Called by models.py to flush various caches whenever we save # a Realm object. The main tricky thing here is that Realm info is # generally cached indirectly through user_profile objects. diff --git a/zerver/lib/user_mutes.py b/zerver/lib/user_mutes.py index d763a1855f..15cbd8257b 100644 --- a/zerver/lib/user_mutes.py +++ b/zerver/lib/user_mutes.py @@ -1,6 +1,7 @@ import datetime -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Set +from zerver.lib.cache import cache_with_key, get_muting_users_cache_key from zerver.lib.timestamp import datetime_to_timestamp from zerver.models import MutedUser, UserProfile @@ -34,3 +35,18 @@ def get_mute_object(user_profile: UserProfile, muted_user: UserProfile) -> Optio return MutedUser.objects.get(user_profile=user_profile, muted_user=muted_user) except MutedUser.DoesNotExist: return None + + +@cache_with_key(get_muting_users_cache_key, timeout=3600 * 24 * 7) +def get_muting_users(muted_user: UserProfile) -> Set[int]: + """ + This is kind of the inverse of `get_user_mutes` above. + While `get_user_mutes` is mainly used for event system work, + this is used in the message send codepath, to get a list + of IDs of users who have muted a particular user. + The result will also include deactivated users. + """ + rows = MutedUser.objects.filter( + muted_user=muted_user, + ).values("user_profile__id") + return {row["user_profile__id"] for row in rows} diff --git a/zerver/models.py b/zerver/models.py index d944ce63c2..64c6b81255 100644 --- a/zerver/models.py +++ b/zerver/models.py @@ -50,6 +50,7 @@ from zerver.lib.cache import ( cache_set, cache_with_key, flush_message, + flush_muting_users_cache, flush_realm, flush_stream, flush_submessage, @@ -1916,6 +1917,10 @@ class MutedUser(models.Model): return f" {self.muted_user.email}>" +post_save.connect(flush_muting_users_cache, sender=MutedUser) +post_delete.connect(flush_muting_users_cache, sender=MutedUser) + + class Client(models.Model): id: int = models.AutoField(auto_created=True, primary_key=True, verbose_name="ID") name: str = models.CharField(max_length=30, db_index=True, unique=True) diff --git a/zerver/tests/test_muting_users.py b/zerver/tests/test_muting_users.py index 1c5c044db1..ac0aa2a0eb 100644 --- a/zerver/tests/test_muting_users.py +++ b/zerver/tests/test_muting_users.py @@ -3,9 +3,10 @@ from unittest import mock import orjson +from zerver.lib.cache import cache_get, get_muting_users_cache_key from zerver.lib.test_classes import ZulipTestCase from zerver.lib.timestamp import datetime_to_timestamp -from zerver.lib.user_mutes import get_mute_object, get_user_mutes +from zerver.lib.user_mutes import get_mute_object, get_muting_users, get_user_mutes from zerver.models import RealmAuditLog @@ -161,3 +162,26 @@ class MutedUsersTests(ZulipTestCase): orjson.dumps({"unmuted_user_id": cordelia.id}).decode(), ), ) + + def test_get_muting_users(self) -> None: + hamlet = self.example_user("hamlet") + self.login_user(hamlet) + cordelia = self.example_user("cordelia") + + self.assertEqual(None, cache_get(get_muting_users_cache_key(cordelia))) + self.assertEqual(set(), get_muting_users(cordelia)) + self.assertEqual(set(), cache_get(get_muting_users_cache_key(cordelia))[0]) + + url = "/api/v1/users/me/muted_users/{}".format(cordelia.id) + result = self.api_post(hamlet, url) + self.assert_json_success(result) + self.assertEqual(None, cache_get(get_muting_users_cache_key(cordelia))) + self.assertEqual({hamlet.id}, get_muting_users(cordelia)) + self.assertEqual({hamlet.id}, cache_get(get_muting_users_cache_key(cordelia))[0]) + + url = "/api/v1/users/me/muted_users/{}".format(cordelia.id) + result = self.api_delete(hamlet, url) + self.assert_json_success(result) + self.assertEqual(None, cache_get(get_muting_users_cache_key(cordelia))) + self.assertEqual(set(), get_muting_users(cordelia)) + self.assertEqual(set(), cache_get(get_muting_users_cache_key(cordelia))[0])