Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Add module callbacks called for reacting to deactivation and profile update #12062

Merged
merged 12 commits into from
Mar 1, 2022
1 change: 1 addition & 0 deletions changelog.d/12062.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add module callbacks to react to user deactivations and profile updates.
48 changes: 48 additions & 0 deletions docs/modules/third_party_rules_callbacks.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,54 @@ 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.

### `on_profile_update`

_First introduced in Synapse v1.5X.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 boolean that is `True` if the update
was triggered by a server admin (and `False` otherwise), and a 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 profile 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_deactivation`

_First introduced in Synapse v1.5X.0_

```python
async def on_deactivation(user_id: str, by_admin: bool) -> None:
```

Called after deactivating a local user. The deactivation can be triggered either by the
user themselves or a server admin. The module is passed the Matrix ID of the user that's
been deactivated, as well as a boolean that is `True` if the deactivation was triggered
by a server admin (and `False` otherwise).

If multiple modules implement this callback, Synapse runs them all in order.

## Example

The example below is a module that implements the third-party rules callback
Expand Down
51 changes: 48 additions & 3 deletions synapse/events/third_party_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
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

Expand All @@ -37,6 +38,8 @@
[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_DEACTIVATION_CALLBACK = Callable[[str, bool], Awaitable]


def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
Expand Down Expand Up @@ -154,6 +157,8 @@ def __init__(self, hs: "HomeServer"):
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_deactivation_callbacks: List[ON_DEACTIVATION_CALLBACK] = []

def register_third_party_rules_callbacks(
self,
Expand All @@ -166,6 +171,8 @@ def register_third_party_rules_callbacks(
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_DEACTIVATION_CALLBACK] = None,
) -> None:
"""Register callbacks from modules for each hook."""
if check_event_allowed is not None:
Expand All @@ -187,6 +194,12 @@ def register_third_party_rules_callbacks(
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_deactivation_callbacks.append(on_deactivation)

async def check_event_allowed(
self, event: EventBase, context: EventContext
) -> Tuple[bool, Optional[dict]]:
Expand Down Expand Up @@ -334,9 +347,6 @@ async def on_new_event(self, event_id: str) -> None:

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:
Expand Down Expand Up @@ -370,3 +380,38 @@ async def _get_state_map_for_room(self, room_id: str) -> StateMap[EventBase]:
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_deactivation(self, user_id: str, by_admin: bool) -> None:
"""Called after a user has been deactivated.

Args:
user_id: The deactivated user.
by_admin: Whether the deactivation was performed by a server admin.
"""
for callback in self._on_deactivation_callbacks:
try:
await callback(user_id, by_admin)
except Exception as e:
logger.exception(
"Failed to run module API callback %s: %s", callback, e
)
12 changes: 10 additions & 2 deletions synapse/handlers/deactivate_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def __init__(self, hs: "HomeServer"):
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
Expand Down Expand Up @@ -135,9 +136,13 @@ async def deactivate_account(
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)
Expand All @@ -160,6 +165,9 @@ async def deactivate_account(
# 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_deactivation(user_id, by_admin)

return identity_server_supports_unbinding

async def _reject_pending_invites_for_user(self, user_id: str) -> None:
Expand Down
14 changes: 14 additions & 0 deletions synapse/handlers/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ def __init__(self, hs: "HomeServer"):

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
Expand Down Expand Up @@ -171,6 +173,7 @@ async def set_displayname(
requester: Requester,
new_displayname: str,
by_admin: bool = False,
deactivation: bool = False,
) -> None:
"""Set the displayname of a user

Expand All @@ -179,6 +182,7 @@ async def set_displayname(
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")
Expand Down Expand Up @@ -227,6 +231,10 @@ async def set_displayname(
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]:
Expand Down Expand Up @@ -261,6 +269,7 @@ async def set_avatar_url(
requester: Requester,
new_avatar_url: str,
by_admin: bool = False,
deactivation: bool = False,
) -> None:
"""Set a new avatar URL for a user.

Expand All @@ -269,6 +278,7 @@ async def set_avatar_url(
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")
Expand Down Expand Up @@ -315,6 +325,10 @@ async def set_avatar_url(
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()
Expand Down
1 change: 1 addition & 0 deletions synapse/module_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
"JsonDict",
"EventBase",
"StateMap",
"ProfileInfo",
]

logger = logging.getLogger(__name__)
Expand Down
Loading