diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py
index 71ec100a7f..dd3104faf3 100644
--- a/synapse/events/third_party_rules.py
+++ b/synapse/events/third_party_rules.py
@@ -17,6 +17,7 @@ from typing import TYPE_CHECKING, Any, Awaitable, Callable, List, Optional, Tupl
from synapse.api.errors import ModuleFailedException, SynapseError
from synapse.events import EventBase
from synapse.events.snapshot import EventContext
+from synapse.storage.roommember import ProfileInfo
from synapse.types import Requester, StateMap
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]
]
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:
@@ -154,6 +157,10 @@ class ThirdPartyEventRules:
CHECK_VISIBILITY_CAN_BE_MODIFIED_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(
self,
@@ -166,6 +173,8 @@ class ThirdPartyEventRules:
CHECK_VISIBILITY_CAN_BE_MODIFIED_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:
"""Register callbacks from modules for each hook."""
if check_event_allowed is not None:
@@ -187,6 +196,12 @@ class ThirdPartyEventRules:
if on_new_event is not None:
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(
self, event: EventBase, context: EventContext
) -> Tuple[bool, Optional[dict]]:
@@ -334,9 +349,6 @@ class ThirdPartyEventRules:
Args:
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
if len(self._on_new_event_callbacks) == 0:
@@ -370,3 +382,41 @@ class ThirdPartyEventRules:
state_events[key] = room_state_events[event_id]
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
+ )
diff --git a/synapse/handlers/deactivate_account.py b/synapse/handlers/deactivate_account.py
index e4eae03056..76ae768e6e 100644
--- a/synapse/handlers/deactivate_account.py
+++ b/synapse/handlers/deactivate_account.py
@@ -38,6 +38,7 @@ class DeactivateAccountHandler:
self._profile_handler = hs.get_profile_handler()
self.user_directory_handler = hs.get_user_directory_handler()
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
self._user_parter_running = False
@@ -135,9 +136,13 @@ class DeactivateAccountHandler:
if erase_data:
user = UserID.from_string(user_id)
# 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
- 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)
await self.store.mark_user_erased(user_id)
@@ -160,6 +165,13 @@ class DeactivateAccountHandler:
# Remove account data (including ignored users and push rules).
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
async def _reject_pending_invites_for_user(self, user_id: str) -> None:
@@ -264,6 +276,10 @@ class DeactivateAccountHandler:
# Mark the user as active.
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
# this must be done after the user is re-activated, because
# deactivated users are excluded from the user directory.
diff --git a/synapse/handlers/profile.py b/synapse/handlers/profile.py
index dd27f0accc..6554c0d3c2 100644
--- a/synapse/handlers/profile.py
+++ b/synapse/handlers/profile.py
@@ -71,6 +71,8 @@ class ProfileHandler:
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:
self.clock.looping_call(
self._update_remote_profile_cache, self.PROFILE_UPDATE_MS
@@ -171,6 +173,7 @@ class ProfileHandler:
requester: Requester,
new_displayname: str,
by_admin: bool = False,
+ deactivation: bool = False,
) -> None:
"""Set the displayname of a user
@@ -179,6 +182,7 @@ class ProfileHandler:
requester: The user attempting to make this change.
new_displayname: The displayname to give this user.
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):
raise SynapseError(400, "User is not hosted on this homeserver")
@@ -227,6 +231,10 @@ class ProfileHandler:
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)
async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
@@ -261,6 +269,7 @@ class ProfileHandler:
requester: Requester,
new_avatar_url: str,
by_admin: bool = False,
+ deactivation: bool = False,
) -> None:
"""Set a new avatar URL for a user.
@@ -269,6 +278,7 @@ class ProfileHandler:
requester: The user attempting to make this change.
new_avatar_url: The avatar URL to give this user.
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):
raise SynapseError(400, "User is not hosted on this homeserver")
@@ -315,6 +325,10 @@ class ProfileHandler:
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)
@cached()
diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py
index 902916d800..7e46931869 100644
--- a/synapse/module_api/__init__.py
+++ b/synapse/module_api/__init__.py
@@ -145,6 +145,7 @@ __all__ = [
"JsonDict",
"EventBase",
"StateMap",
+ "ProfileInfo",
]
logger = logging.getLogger(__name__)
|