mirror of
https://git.anonymousland.org/anonymousland/synapse-product.git
synced 2024-10-01 08:25:44 -04:00
Add module callbacks called for reacting to deactivation status change and profile update (#12062)
This commit is contained in:
parent
f26e390a40
commit
300ed0b8a6
1
changelog.d/12062.feature
Normal file
1
changelog.d/12062.feature
Normal file
@ -0,0 +1 @@
|
|||||||
|
Add module callbacks to react to user deactivation status changes (i.e. deactivations and reactivations) and profile updates.
|
@ -148,6 +148,62 @@ deny an incoming event, see [`check_event_for_spam`](spam_checker_callbacks.md#c
|
|||||||
|
|
||||||
If multiple modules implement this callback, Synapse runs them all in order.
|
If multiple modules implement this callback, Synapse runs them all in order.
|
||||||
|
|
||||||
|
### `on_profile_update`
|
||||||
|
|
||||||
|
_First introduced in Synapse v1.54.0_
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def on_profile_update(
|
||||||
|
user_id: str,
|
||||||
|
new_profile: "synapse.module_api.ProfileInfo",
|
||||||
|
by_admin: bool,
|
||||||
|
deactivation: bool,
|
||||||
|
) -> None:
|
||||||
|
```
|
||||||
|
|
||||||
|
Called after updating a local user's profile. The update can be triggered either by the
|
||||||
|
user themselves or a server admin. The update can also be triggered by a user being
|
||||||
|
deactivated (in which case their display name is set to an empty string (`""`) and the
|
||||||
|
avatar URL is set to `None`). The module is passed the Matrix ID of the user whose profile
|
||||||
|
has been updated, their new profile, as well as a `by_admin` boolean that is `True` if the
|
||||||
|
update was triggered by a server admin (and `False` otherwise), and a `deactivated`
|
||||||
|
boolean that is `True` if the update is a result of the user being deactivated.
|
||||||
|
|
||||||
|
Note that the `by_admin` boolean is also `True` if the profile change happens as a result
|
||||||
|
of the user logging in through Single Sign-On, or if a server admin updates their own
|
||||||
|
profile.
|
||||||
|
|
||||||
|
Per-room profile changes do not trigger this callback to be called. Synapse administrators
|
||||||
|
wishing this callback to be called on every profile change are encouraged to disable
|
||||||
|
per-room profiles globally using the `allow_per_room_profiles` configuration setting in
|
||||||
|
Synapse's configuration file.
|
||||||
|
This callback is not called when registering a user, even when setting it through the
|
||||||
|
[`get_displayname_for_registration`](https://matrix-org.github.io/synapse/latest/modules/password_auth_provider_callbacks.html#get_displayname_for_registration)
|
||||||
|
module callback.
|
||||||
|
|
||||||
|
If multiple modules implement this callback, Synapse runs them all in order.
|
||||||
|
|
||||||
|
### `on_user_deactivation_status_changed`
|
||||||
|
|
||||||
|
_First introduced in Synapse v1.54.0_
|
||||||
|
|
||||||
|
```python
|
||||||
|
async def on_user_deactivation_status_changed(
|
||||||
|
user_id: str, deactivated: bool, by_admin: bool
|
||||||
|
) -> None:
|
||||||
|
```
|
||||||
|
|
||||||
|
Called after deactivating a local user, or reactivating them through the admin API. The
|
||||||
|
deactivation can be triggered either by the user themselves or a server admin. The module
|
||||||
|
is passed the Matrix ID of the user whose status is changed, as well as a `deactivated`
|
||||||
|
boolean that is `True` if the user is being deactivated and `False` if they're being
|
||||||
|
reactivated, and a `by_admin` boolean that is `True` if the deactivation was triggered by
|
||||||
|
a server admin (and `False` otherwise). This latter `by_admin` boolean is always `True`
|
||||||
|
if the user is being reactivated, as this operation can only be performed through the
|
||||||
|
admin API.
|
||||||
|
|
||||||
|
If multiple modules implement this callback, Synapse runs them all in order.
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
The example below is a module that implements the third-party rules callback
|
The example below is a module that implements the third-party rules callback
|
||||||
|
@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tupl
|
|||||||
from synapse.api.errors import ModuleFailedException, SynapseError
|
from synapse.api.errors import ModuleFailedException, SynapseError
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
from synapse.events.snapshot import EventContext
|
from synapse.events.snapshot import EventContext
|
||||||
|
from synapse.storage.roommember import ProfileInfo
|
||||||
from synapse.types import Requester, StateMap
|
from synapse.types import Requester, StateMap
|
||||||
from synapse.util.async_helpers import maybe_awaitable
|
from synapse.util.async_helpers import maybe_awaitable
|
||||||
|
|
||||||
@ -37,6 +38,8 @@ CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK = Callable[
|
|||||||
[str, StateMap[EventBase], str], Awaitable[bool]
|
[str, StateMap[EventBase], str], Awaitable[bool]
|
||||||
]
|
]
|
||||||
ON_NEW_EVENT_CALLBACK = Callable[[EventBase, StateMap[EventBase]], Awaitable]
|
ON_NEW_EVENT_CALLBACK = Callable[[EventBase, StateMap[EventBase]], Awaitable]
|
||||||
|
ON_PROFILE_UPDATE_CALLBACK = Callable[[str, ProfileInfo, bool, bool], Awaitable]
|
||||||
|
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK = Callable[[str, bool, bool], Awaitable]
|
||||||
|
|
||||||
|
|
||||||
def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
|
def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
|
||||||
@ -154,6 +157,10 @@ class ThirdPartyEventRules:
|
|||||||
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
|
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
|
||||||
] = []
|
] = []
|
||||||
self._on_new_event_callbacks: List[ON_NEW_EVENT_CALLBACK] = []
|
self._on_new_event_callbacks: List[ON_NEW_EVENT_CALLBACK] = []
|
||||||
|
self._on_profile_update_callbacks: List[ON_PROFILE_UPDATE_CALLBACK] = []
|
||||||
|
self._on_user_deactivation_status_changed_callbacks: List[
|
||||||
|
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
|
||||||
|
] = []
|
||||||
|
|
||||||
def register_third_party_rules_callbacks(
|
def register_third_party_rules_callbacks(
|
||||||
self,
|
self,
|
||||||
@ -166,6 +173,8 @@ class ThirdPartyEventRules:
|
|||||||
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
|
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
|
||||||
] = None,
|
] = None,
|
||||||
on_new_event: Optional[ON_NEW_EVENT_CALLBACK] = None,
|
on_new_event: Optional[ON_NEW_EVENT_CALLBACK] = None,
|
||||||
|
on_profile_update: Optional[ON_PROFILE_UPDATE_CALLBACK] = None,
|
||||||
|
on_deactivation: Optional[ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Register callbacks from modules for each hook."""
|
"""Register callbacks from modules for each hook."""
|
||||||
if check_event_allowed is not None:
|
if check_event_allowed is not None:
|
||||||
@ -187,6 +196,12 @@ class ThirdPartyEventRules:
|
|||||||
if on_new_event is not None:
|
if on_new_event is not None:
|
||||||
self._on_new_event_callbacks.append(on_new_event)
|
self._on_new_event_callbacks.append(on_new_event)
|
||||||
|
|
||||||
|
if on_profile_update is not None:
|
||||||
|
self._on_profile_update_callbacks.append(on_profile_update)
|
||||||
|
|
||||||
|
if on_deactivation is not None:
|
||||||
|
self._on_user_deactivation_status_changed_callbacks.append(on_deactivation)
|
||||||
|
|
||||||
async def check_event_allowed(
|
async def check_event_allowed(
|
||||||
self, event: EventBase, context: EventContext
|
self, event: EventBase, context: EventContext
|
||||||
) -> Tuple[bool, Optional[dict]]:
|
) -> Tuple[bool, Optional[dict]]:
|
||||||
@ -334,9 +349,6 @@ class ThirdPartyEventRules:
|
|||||||
|
|
||||||
Args:
|
Args:
|
||||||
event_id: The ID of the event.
|
event_id: The ID of the event.
|
||||||
|
|
||||||
Raises:
|
|
||||||
ModuleFailureError if a callback raised any exception.
|
|
||||||
"""
|
"""
|
||||||
# Bail out early without hitting the store if we don't have any callbacks
|
# Bail out early without hitting the store if we don't have any callbacks
|
||||||
if len(self._on_new_event_callbacks) == 0:
|
if len(self._on_new_event_callbacks) == 0:
|
||||||
@ -370,3 +382,41 @@ class ThirdPartyEventRules:
|
|||||||
state_events[key] = room_state_events[event_id]
|
state_events[key] = room_state_events[event_id]
|
||||||
|
|
||||||
return state_events
|
return state_events
|
||||||
|
|
||||||
|
async def on_profile_update(
|
||||||
|
self, user_id: str, new_profile: ProfileInfo, by_admin: bool, deactivation: bool
|
||||||
|
) -> None:
|
||||||
|
"""Called after the global profile of a user has been updated. Does not include
|
||||||
|
per-room profile changes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The user whose profile was changed.
|
||||||
|
new_profile: The updated profile for the user.
|
||||||
|
by_admin: Whether the profile update was performed by a server admin.
|
||||||
|
deactivation: Whether this change was made while deactivating the user.
|
||||||
|
"""
|
||||||
|
for callback in self._on_profile_update_callbacks:
|
||||||
|
try:
|
||||||
|
await callback(user_id, new_profile, by_admin, deactivation)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to run module API callback %s: %s", callback, e
|
||||||
|
)
|
||||||
|
|
||||||
|
async def on_user_deactivation_status_changed(
|
||||||
|
self, user_id: str, deactivated: bool, by_admin: bool
|
||||||
|
) -> None:
|
||||||
|
"""Called after a user has been deactivated or reactivated.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id: The deactivated user.
|
||||||
|
deactivated: Whether the user is now deactivated.
|
||||||
|
by_admin: Whether the deactivation was performed by a server admin.
|
||||||
|
"""
|
||||||
|
for callback in self._on_user_deactivation_status_changed_callbacks:
|
||||||
|
try:
|
||||||
|
await callback(user_id, deactivated, by_admin)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(
|
||||||
|
"Failed to run module API callback %s: %s", callback, e
|
||||||
|
)
|
||||||
|
@ -38,6 +38,7 @@ class DeactivateAccountHandler:
|
|||||||
self._profile_handler = hs.get_profile_handler()
|
self._profile_handler = hs.get_profile_handler()
|
||||||
self.user_directory_handler = hs.get_user_directory_handler()
|
self.user_directory_handler = hs.get_user_directory_handler()
|
||||||
self._server_name = hs.hostname
|
self._server_name = hs.hostname
|
||||||
|
self._third_party_rules = hs.get_third_party_event_rules()
|
||||||
|
|
||||||
# Flag that indicates whether the process to part users from rooms is running
|
# Flag that indicates whether the process to part users from rooms is running
|
||||||
self._user_parter_running = False
|
self._user_parter_running = False
|
||||||
@ -135,9 +136,13 @@ class DeactivateAccountHandler:
|
|||||||
if erase_data:
|
if erase_data:
|
||||||
user = UserID.from_string(user_id)
|
user = UserID.from_string(user_id)
|
||||||
# Remove avatar URL from this user
|
# Remove avatar URL from this user
|
||||||
await self._profile_handler.set_avatar_url(user, requester, "", by_admin)
|
await self._profile_handler.set_avatar_url(
|
||||||
|
user, requester, "", by_admin, deactivation=True
|
||||||
|
)
|
||||||
# Remove displayname from this user
|
# Remove displayname from this user
|
||||||
await self._profile_handler.set_displayname(user, requester, "", by_admin)
|
await self._profile_handler.set_displayname(
|
||||||
|
user, requester, "", by_admin, deactivation=True
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("Marking %s as erased", user_id)
|
logger.info("Marking %s as erased", user_id)
|
||||||
await self.store.mark_user_erased(user_id)
|
await self.store.mark_user_erased(user_id)
|
||||||
@ -160,6 +165,13 @@ class DeactivateAccountHandler:
|
|||||||
# Remove account data (including ignored users and push rules).
|
# Remove account data (including ignored users and push rules).
|
||||||
await self.store.purge_account_data_for_user(user_id)
|
await self.store.purge_account_data_for_user(user_id)
|
||||||
|
|
||||||
|
# Let modules know the user has been deactivated.
|
||||||
|
await self._third_party_rules.on_user_deactivation_status_changed(
|
||||||
|
user_id,
|
||||||
|
True,
|
||||||
|
by_admin,
|
||||||
|
)
|
||||||
|
|
||||||
return identity_server_supports_unbinding
|
return identity_server_supports_unbinding
|
||||||
|
|
||||||
async def _reject_pending_invites_for_user(self, user_id: str) -> None:
|
async def _reject_pending_invites_for_user(self, user_id: str) -> None:
|
||||||
@ -264,6 +276,10 @@ class DeactivateAccountHandler:
|
|||||||
# Mark the user as active.
|
# Mark the user as active.
|
||||||
await self.store.set_user_deactivated_status(user_id, False)
|
await self.store.set_user_deactivated_status(user_id, False)
|
||||||
|
|
||||||
|
await self._third_party_rules.on_user_deactivation_status_changed(
|
||||||
|
user_id, False, True
|
||||||
|
)
|
||||||
|
|
||||||
# Add the user to the directory, if necessary. Note that
|
# Add the user to the directory, if necessary. Note that
|
||||||
# this must be done after the user is re-activated, because
|
# this must be done after the user is re-activated, because
|
||||||
# deactivated users are excluded from the user directory.
|
# deactivated users are excluded from the user directory.
|
||||||
|
@ -71,6 +71,8 @@ class ProfileHandler:
|
|||||||
|
|
||||||
self.server_name = hs.config.server.server_name
|
self.server_name = hs.config.server.server_name
|
||||||
|
|
||||||
|
self._third_party_rules = hs.get_third_party_event_rules()
|
||||||
|
|
||||||
if hs.config.worker.run_background_tasks:
|
if hs.config.worker.run_background_tasks:
|
||||||
self.clock.looping_call(
|
self.clock.looping_call(
|
||||||
self._update_remote_profile_cache, self.PROFILE_UPDATE_MS
|
self._update_remote_profile_cache, self.PROFILE_UPDATE_MS
|
||||||
@ -171,6 +173,7 @@ class ProfileHandler:
|
|||||||
requester: Requester,
|
requester: Requester,
|
||||||
new_displayname: str,
|
new_displayname: str,
|
||||||
by_admin: bool = False,
|
by_admin: bool = False,
|
||||||
|
deactivation: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set the displayname of a user
|
"""Set the displayname of a user
|
||||||
|
|
||||||
@ -179,6 +182,7 @@ class ProfileHandler:
|
|||||||
requester: The user attempting to make this change.
|
requester: The user attempting to make this change.
|
||||||
new_displayname: The displayname to give this user.
|
new_displayname: The displayname to give this user.
|
||||||
by_admin: Whether this change was made by an administrator.
|
by_admin: Whether this change was made by an administrator.
|
||||||
|
deactivation: Whether this change was made while deactivating the user.
|
||||||
"""
|
"""
|
||||||
if not self.hs.is_mine(target_user):
|
if not self.hs.is_mine(target_user):
|
||||||
raise SynapseError(400, "User is not hosted on this homeserver")
|
raise SynapseError(400, "User is not hosted on this homeserver")
|
||||||
@ -227,6 +231,10 @@ class ProfileHandler:
|
|||||||
target_user.to_string(), profile
|
target_user.to_string(), profile
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await self._third_party_rules.on_profile_update(
|
||||||
|
target_user.to_string(), profile, by_admin, deactivation
|
||||||
|
)
|
||||||
|
|
||||||
await self._update_join_states(requester, target_user)
|
await self._update_join_states(requester, target_user)
|
||||||
|
|
||||||
async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
|
async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
|
||||||
@ -261,6 +269,7 @@ class ProfileHandler:
|
|||||||
requester: Requester,
|
requester: Requester,
|
||||||
new_avatar_url: str,
|
new_avatar_url: str,
|
||||||
by_admin: bool = False,
|
by_admin: bool = False,
|
||||||
|
deactivation: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set a new avatar URL for a user.
|
"""Set a new avatar URL for a user.
|
||||||
|
|
||||||
@ -269,6 +278,7 @@ class ProfileHandler:
|
|||||||
requester: The user attempting to make this change.
|
requester: The user attempting to make this change.
|
||||||
new_avatar_url: The avatar URL to give this user.
|
new_avatar_url: The avatar URL to give this user.
|
||||||
by_admin: Whether this change was made by an administrator.
|
by_admin: Whether this change was made by an administrator.
|
||||||
|
deactivation: Whether this change was made while deactivating the user.
|
||||||
"""
|
"""
|
||||||
if not self.hs.is_mine(target_user):
|
if not self.hs.is_mine(target_user):
|
||||||
raise SynapseError(400, "User is not hosted on this homeserver")
|
raise SynapseError(400, "User is not hosted on this homeserver")
|
||||||
@ -315,6 +325,10 @@ class ProfileHandler:
|
|||||||
target_user.to_string(), profile
|
target_user.to_string(), profile
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await self._third_party_rules.on_profile_update(
|
||||||
|
target_user.to_string(), profile, by_admin, deactivation
|
||||||
|
)
|
||||||
|
|
||||||
await self._update_join_states(requester, target_user)
|
await self._update_join_states(requester, target_user)
|
||||||
|
|
||||||
@cached()
|
@cached()
|
||||||
|
@ -145,6 +145,7 @@ __all__ = [
|
|||||||
"JsonDict",
|
"JsonDict",
|
||||||
"EventBase",
|
"EventBase",
|
||||||
"StateMap",
|
"StateMap",
|
||||||
|
"ProfileInfo",
|
||||||
]
|
]
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -15,12 +15,12 @@ import threading
|
|||||||
from typing import TYPE_CHECKING, Dict, Optional, Tuple
|
from typing import TYPE_CHECKING, Dict, Optional, Tuple
|
||||||
from unittest.mock import Mock
|
from unittest.mock import Mock
|
||||||
|
|
||||||
from synapse.api.constants import EventTypes, Membership
|
from synapse.api.constants import EventTypes, LoginType, Membership
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.events import EventBase
|
from synapse.events import EventBase
|
||||||
from synapse.events.third_party_rules import load_legacy_third_party_event_rules
|
from synapse.events.third_party_rules import load_legacy_third_party_event_rules
|
||||||
from synapse.rest import admin
|
from synapse.rest import admin
|
||||||
from synapse.rest.client import login, room
|
from synapse.rest.client import account, login, profile, room
|
||||||
from synapse.types import JsonDict, Requester, StateMap
|
from synapse.types import JsonDict, Requester, StateMap
|
||||||
from synapse.util.frozenutils import unfreeze
|
from synapse.util.frozenutils import unfreeze
|
||||||
|
|
||||||
@ -80,6 +80,8 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase):
|
|||||||
admin.register_servlets,
|
admin.register_servlets,
|
||||||
login.register_servlets,
|
login.register_servlets,
|
||||||
room.register_servlets,
|
room.register_servlets,
|
||||||
|
profile.register_servlets,
|
||||||
|
account.register_servlets,
|
||||||
]
|
]
|
||||||
|
|
||||||
def make_homeserver(self, reactor, clock):
|
def make_homeserver(self, reactor, clock):
|
||||||
@ -530,3 +532,216 @@ class ThirdPartyRulesTestCase(unittest.FederatingHomeserverTestCase):
|
|||||||
},
|
},
|
||||||
tok=self.tok,
|
tok=self.tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_on_profile_update(self):
|
||||||
|
"""Tests that the on_profile_update module callback is correctly called on
|
||||||
|
profile updates.
|
||||||
|
"""
|
||||||
|
displayname = "Foo"
|
||||||
|
avatar_url = "mxc://matrix.org/oWQDvfewxmlRaRCkVbfetyEo"
|
||||||
|
|
||||||
|
# Register a mock callback.
|
||||||
|
m = Mock(return_value=make_awaitable(None))
|
||||||
|
self.hs.get_third_party_event_rules()._on_profile_update_callbacks.append(m)
|
||||||
|
|
||||||
|
# Change the display name.
|
||||||
|
channel = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_matrix/client/v3/profile/%s/displayname" % self.user_id,
|
||||||
|
{"displayname": displayname},
|
||||||
|
access_token=self.tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Check that the callback has been called once for our user.
|
||||||
|
m.assert_called_once()
|
||||||
|
args = m.call_args[0]
|
||||||
|
self.assertEqual(args[0], self.user_id)
|
||||||
|
|
||||||
|
# Test that by_admin is False.
|
||||||
|
self.assertFalse(args[2])
|
||||||
|
# Test that deactivation is False.
|
||||||
|
self.assertFalse(args[3])
|
||||||
|
|
||||||
|
# Check that we've got the right profile data.
|
||||||
|
profile_info = args[1]
|
||||||
|
self.assertEqual(profile_info.display_name, displayname)
|
||||||
|
self.assertIsNone(profile_info.avatar_url)
|
||||||
|
|
||||||
|
# Change the avatar.
|
||||||
|
channel = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_matrix/client/v3/profile/%s/avatar_url" % self.user_id,
|
||||||
|
{"avatar_url": avatar_url},
|
||||||
|
access_token=self.tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Check that the callback has been called once for our user.
|
||||||
|
self.assertEqual(m.call_count, 2)
|
||||||
|
args = m.call_args[0]
|
||||||
|
self.assertEqual(args[0], self.user_id)
|
||||||
|
|
||||||
|
# Test that by_admin is False.
|
||||||
|
self.assertFalse(args[2])
|
||||||
|
# Test that deactivation is False.
|
||||||
|
self.assertFalse(args[3])
|
||||||
|
|
||||||
|
# Check that we've got the right profile data.
|
||||||
|
profile_info = args[1]
|
||||||
|
self.assertEqual(profile_info.display_name, displayname)
|
||||||
|
self.assertEqual(profile_info.avatar_url, avatar_url)
|
||||||
|
|
||||||
|
def test_on_profile_update_admin(self):
|
||||||
|
"""Tests that the on_profile_update module callback is correctly called on
|
||||||
|
profile updates triggered by a server admin.
|
||||||
|
"""
|
||||||
|
displayname = "Foo"
|
||||||
|
avatar_url = "mxc://matrix.org/oWQDvfewxmlRaRCkVbfetyEo"
|
||||||
|
|
||||||
|
# Register a mock callback.
|
||||||
|
m = Mock(return_value=make_awaitable(None))
|
||||||
|
self.hs.get_third_party_event_rules()._on_profile_update_callbacks.append(m)
|
||||||
|
|
||||||
|
# Register an admin user.
|
||||||
|
self.register_user("admin", "password", admin=True)
|
||||||
|
admin_tok = self.login("admin", "password")
|
||||||
|
|
||||||
|
# Change a user's profile.
|
||||||
|
channel = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_synapse/admin/v2/users/%s" % self.user_id,
|
||||||
|
{"displayname": displayname, "avatar_url": avatar_url},
|
||||||
|
access_token=admin_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Check that the callback has been called twice (since we update the display name
|
||||||
|
# and avatar separately).
|
||||||
|
self.assertEqual(m.call_count, 2)
|
||||||
|
|
||||||
|
# Get the arguments for the last call and check it's about the right user.
|
||||||
|
args = m.call_args[0]
|
||||||
|
self.assertEqual(args[0], self.user_id)
|
||||||
|
|
||||||
|
# Check that by_admin is True.
|
||||||
|
self.assertTrue(args[2])
|
||||||
|
# Test that deactivation is False.
|
||||||
|
self.assertFalse(args[3])
|
||||||
|
|
||||||
|
# Check that we've got the right profile data.
|
||||||
|
profile_info = args[1]
|
||||||
|
self.assertEqual(profile_info.display_name, displayname)
|
||||||
|
self.assertEqual(profile_info.avatar_url, avatar_url)
|
||||||
|
|
||||||
|
def test_on_user_deactivation_status_changed(self):
|
||||||
|
"""Tests that the on_user_deactivation_status_changed module callback is called
|
||||||
|
correctly when processing a user's deactivation.
|
||||||
|
"""
|
||||||
|
# Register a mocked callback.
|
||||||
|
deactivation_mock = Mock(return_value=make_awaitable(None))
|
||||||
|
third_party_rules = self.hs.get_third_party_event_rules()
|
||||||
|
third_party_rules._on_user_deactivation_status_changed_callbacks.append(
|
||||||
|
deactivation_mock,
|
||||||
|
)
|
||||||
|
# Also register a mocked callback for profile updates, to check that the
|
||||||
|
# deactivation code calls it in a way that let modules know the user is being
|
||||||
|
# deactivated.
|
||||||
|
profile_mock = Mock(return_value=make_awaitable(None))
|
||||||
|
self.hs.get_third_party_event_rules()._on_profile_update_callbacks.append(
|
||||||
|
profile_mock,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Register a user that we'll deactivate.
|
||||||
|
user_id = self.register_user("altan", "password")
|
||||||
|
tok = self.login("altan", "password")
|
||||||
|
|
||||||
|
# Deactivate that user.
|
||||||
|
channel = self.make_request(
|
||||||
|
"POST",
|
||||||
|
"/_matrix/client/v3/account/deactivate",
|
||||||
|
{
|
||||||
|
"auth": {
|
||||||
|
"type": LoginType.PASSWORD,
|
||||||
|
"password": "password",
|
||||||
|
"identifier": {
|
||||||
|
"type": "m.id.user",
|
||||||
|
"user": user_id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"erase": True,
|
||||||
|
},
|
||||||
|
access_token=tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Check that the mock was called once.
|
||||||
|
deactivation_mock.assert_called_once()
|
||||||
|
args = deactivation_mock.call_args[0]
|
||||||
|
|
||||||
|
# Check that the mock was called with the right user ID, and with a True
|
||||||
|
# deactivated flag and a False by_admin flag.
|
||||||
|
self.assertEqual(args[0], user_id)
|
||||||
|
self.assertTrue(args[1])
|
||||||
|
self.assertFalse(args[2])
|
||||||
|
|
||||||
|
# Check that the profile update callback was called twice (once for the display
|
||||||
|
# name and once for the avatar URL), and that the "deactivation" boolean is true.
|
||||||
|
self.assertEqual(profile_mock.call_count, 2)
|
||||||
|
args = profile_mock.call_args[0]
|
||||||
|
self.assertTrue(args[3])
|
||||||
|
|
||||||
|
def test_on_user_deactivation_status_changed_admin(self):
|
||||||
|
"""Tests that the on_user_deactivation_status_changed module callback is called
|
||||||
|
correctly when processing a user's deactivation triggered by a server admin as
|
||||||
|
well as a reactivation.
|
||||||
|
"""
|
||||||
|
# Register a mock callback.
|
||||||
|
m = Mock(return_value=make_awaitable(None))
|
||||||
|
third_party_rules = self.hs.get_third_party_event_rules()
|
||||||
|
third_party_rules._on_user_deactivation_status_changed_callbacks.append(m)
|
||||||
|
|
||||||
|
# Register an admin user.
|
||||||
|
self.register_user("admin", "password", admin=True)
|
||||||
|
admin_tok = self.login("admin", "password")
|
||||||
|
|
||||||
|
# Register a user that we'll deactivate.
|
||||||
|
user_id = self.register_user("altan", "password")
|
||||||
|
|
||||||
|
# Deactivate the user.
|
||||||
|
channel = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_synapse/admin/v2/users/%s" % user_id,
|
||||||
|
{"deactivated": True},
|
||||||
|
access_token=admin_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Check that the mock was called once.
|
||||||
|
m.assert_called_once()
|
||||||
|
args = m.call_args[0]
|
||||||
|
|
||||||
|
# Check that the mock was called with the right user ID, and with True deactivated
|
||||||
|
# and by_admin flags.
|
||||||
|
self.assertEqual(args[0], user_id)
|
||||||
|
self.assertTrue(args[1])
|
||||||
|
self.assertTrue(args[2])
|
||||||
|
|
||||||
|
# Reactivate the user.
|
||||||
|
channel = self.make_request(
|
||||||
|
"PUT",
|
||||||
|
"/_synapse/admin/v2/users/%s" % user_id,
|
||||||
|
{"deactivated": False, "password": "hackme"},
|
||||||
|
access_token=admin_tok,
|
||||||
|
)
|
||||||
|
self.assertEqual(channel.code, 200, channel.json_body)
|
||||||
|
|
||||||
|
# Check that the mock was called once.
|
||||||
|
self.assertEqual(m.call_count, 2)
|
||||||
|
args = m.call_args[0]
|
||||||
|
|
||||||
|
# Check that the mock was called with the right user ID, and with a False
|
||||||
|
# deactivated flag and a True by_admin flag.
|
||||||
|
self.assertEqual(args[0], user_id)
|
||||||
|
self.assertFalse(args[1])
|
||||||
|
self.assertTrue(args[2])
|
||||||
|
Loading…
Reference in New Issue
Block a user