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`
babolivier marked this conversation as resolved.
Show resolved Hide resolved

_First introduced in Synapse v1.5X.0_
babolivier marked this conversation as resolved.
Show resolved Hide resolved

```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
babolivier marked this conversation as resolved.
Show resolved Hide resolved
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
babolivier marked this conversation as resolved.
Show resolved Hide resolved
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
babolivier marked this conversation as resolved.
Show resolved Hide resolved
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
babolivier marked this conversation as resolved.
Show resolved Hide resolved
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_
babolivier marked this conversation as resolved.
Show resolved Hide resolved

```python
async def on_deactivation(user_id: str, by_admin: bool) -> None:
babolivier marked this conversation as resolved.
Show resolved Hide resolved
```

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
babolivier marked this conversation as resolved.
Show resolved Hide resolved
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