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

Commit 300ed0b

Browse files
authored
Add module callbacks called for reacting to deactivation status change and profile update (#12062)
1 parent f26e390 commit 300ed0b

File tree

7 files changed

+360
-7
lines changed

7 files changed

+360
-7
lines changed

changelog.d/12062.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add module callbacks to react to user deactivation status changes (i.e. deactivations and reactivations) and profile updates.

docs/modules/third_party_rules_callbacks.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,62 @@ deny an incoming event, see [`check_event_for_spam`](spam_checker_callbacks.md#c
148148

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

151+
### `on_profile_update`
152+
153+
_First introduced in Synapse v1.54.0_
154+
155+
```python
156+
async def on_profile_update(
157+
user_id: str,
158+
new_profile: "synapse.module_api.ProfileInfo",
159+
by_admin: bool,
160+
deactivation: bool,
161+
) -> None:
162+
```
163+
164+
Called after updating a local user's profile. The update can be triggered either by the
165+
user themselves or a server admin. The update can also be triggered by a user being
166+
deactivated (in which case their display name is set to an empty string (`""`) and the
167+
avatar URL is set to `None`). The module is passed the Matrix ID of the user whose profile
168+
has been updated, their new profile, as well as a `by_admin` boolean that is `True` if the
169+
update was triggered by a server admin (and `False` otherwise), and a `deactivated`
170+
boolean that is `True` if the update is a result of the user being deactivated.
171+
172+
Note that the `by_admin` boolean is also `True` if the profile change happens as a result
173+
of the user logging in through Single Sign-On, or if a server admin updates their own
174+
profile.
175+
176+
Per-room profile changes do not trigger this callback to be called. Synapse administrators
177+
wishing this callback to be called on every profile change are encouraged to disable
178+
per-room profiles globally using the `allow_per_room_profiles` configuration setting in
179+
Synapse's configuration file.
180+
This callback is not called when registering a user, even when setting it through the
181+
[`get_displayname_for_registration`](https://matrix-org.github.io/synapse/latest/modules/password_auth_provider_callbacks.html#get_displayname_for_registration)
182+
module callback.
183+
184+
If multiple modules implement this callback, Synapse runs them all in order.
185+
186+
### `on_user_deactivation_status_changed`
187+
188+
_First introduced in Synapse v1.54.0_
189+
190+
```python
191+
async def on_user_deactivation_status_changed(
192+
user_id: str, deactivated: bool, by_admin: bool
193+
) -> None:
194+
```
195+
196+
Called after deactivating a local user, or reactivating them through the admin API. The
197+
deactivation can be triggered either by the user themselves or a server admin. The module
198+
is passed the Matrix ID of the user whose status is changed, as well as a `deactivated`
199+
boolean that is `True` if the user is being deactivated and `False` if they're being
200+
reactivated, and a `by_admin` boolean that is `True` if the deactivation was triggered by
201+
a server admin (and `False` otherwise). This latter `by_admin` boolean is always `True`
202+
if the user is being reactivated, as this operation can only be performed through the
203+
admin API.
204+
205+
If multiple modules implement this callback, Synapse runs them all in order.
206+
151207
## Example
152208

153209
The example below is a module that implements the third-party rules callback

synapse/events/third_party_rules.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from synapse.api.errors import ModuleFailedException, SynapseError
1818
from synapse.events import EventBase
1919
from synapse.events.snapshot import EventContext
20+
from synapse.storage.roommember import ProfileInfo
2021
from synapse.types import Requester, StateMap
2122
from synapse.util.async_helpers import maybe_awaitable
2223

@@ -37,6 +38,8 @@
3738
[str, StateMap[EventBase], str], Awaitable[bool]
3839
]
3940
ON_NEW_EVENT_CALLBACK = Callable[[EventBase, StateMap[EventBase]], Awaitable]
41+
ON_PROFILE_UPDATE_CALLBACK = Callable[[str, ProfileInfo, bool, bool], Awaitable]
42+
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK = Callable[[str, bool, bool], Awaitable]
4043

4144

4245
def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
@@ -154,6 +157,10 @@ def __init__(self, hs: "HomeServer"):
154157
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
155158
] = []
156159
self._on_new_event_callbacks: List[ON_NEW_EVENT_CALLBACK] = []
160+
self._on_profile_update_callbacks: List[ON_PROFILE_UPDATE_CALLBACK] = []
161+
self._on_user_deactivation_status_changed_callbacks: List[
162+
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
163+
] = []
157164

158165
def register_third_party_rules_callbacks(
159166
self,
@@ -166,6 +173,8 @@ def register_third_party_rules_callbacks(
166173
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
167174
] = None,
168175
on_new_event: Optional[ON_NEW_EVENT_CALLBACK] = None,
176+
on_profile_update: Optional[ON_PROFILE_UPDATE_CALLBACK] = None,
177+
on_deactivation: Optional[ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK] = None,
169178
) -> None:
170179
"""Register callbacks from modules for each hook."""
171180
if check_event_allowed is not None:
@@ -187,6 +196,12 @@ def register_third_party_rules_callbacks(
187196
if on_new_event is not None:
188197
self._on_new_event_callbacks.append(on_new_event)
189198

199+
if on_profile_update is not None:
200+
self._on_profile_update_callbacks.append(on_profile_update)
201+
202+
if on_deactivation is not None:
203+
self._on_user_deactivation_status_changed_callbacks.append(on_deactivation)
204+
190205
async def check_event_allowed(
191206
self, event: EventBase, context: EventContext
192207
) -> Tuple[bool, Optional[dict]]:
@@ -334,9 +349,6 @@ async def on_new_event(self, event_id: str) -> None:
334349
335350
Args:
336351
event_id: The ID of the event.
337-
338-
Raises:
339-
ModuleFailureError if a callback raised any exception.
340352
"""
341353
# Bail out early without hitting the store if we don't have any callbacks
342354
if len(self._on_new_event_callbacks) == 0:
@@ -370,3 +382,41 @@ async def _get_state_map_for_room(self, room_id: str) -> StateMap[EventBase]:
370382
state_events[key] = room_state_events[event_id]
371383

372384
return state_events
385+
386+
async def on_profile_update(
387+
self, user_id: str, new_profile: ProfileInfo, by_admin: bool, deactivation: bool
388+
) -> None:
389+
"""Called after the global profile of a user has been updated. Does not include
390+
per-room profile changes.
391+
392+
Args:
393+
user_id: The user whose profile was changed.
394+
new_profile: The updated profile for the user.
395+
by_admin: Whether the profile update was performed by a server admin.
396+
deactivation: Whether this change was made while deactivating the user.
397+
"""
398+
for callback in self._on_profile_update_callbacks:
399+
try:
400+
await callback(user_id, new_profile, by_admin, deactivation)
401+
except Exception as e:
402+
logger.exception(
403+
"Failed to run module API callback %s: %s", callback, e
404+
)
405+
406+
async def on_user_deactivation_status_changed(
407+
self, user_id: str, deactivated: bool, by_admin: bool
408+
) -> None:
409+
"""Called after a user has been deactivated or reactivated.
410+
411+
Args:
412+
user_id: The deactivated user.
413+
deactivated: Whether the user is now deactivated.
414+
by_admin: Whether the deactivation was performed by a server admin.
415+
"""
416+
for callback in self._on_user_deactivation_status_changed_callbacks:
417+
try:
418+
await callback(user_id, deactivated, by_admin)
419+
except Exception as e:
420+
logger.exception(
421+
"Failed to run module API callback %s: %s", callback, e
422+
)

synapse/handlers/deactivate_account.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def __init__(self, hs: "HomeServer"):
3838
self._profile_handler = hs.get_profile_handler()
3939
self.user_directory_handler = hs.get_user_directory_handler()
4040
self._server_name = hs.hostname
41+
self._third_party_rules = hs.get_third_party_event_rules()
4142

4243
# Flag that indicates whether the process to part users from rooms is running
4344
self._user_parter_running = False
@@ -135,9 +136,13 @@ async def deactivate_account(
135136
if erase_data:
136137
user = UserID.from_string(user_id)
137138
# Remove avatar URL from this user
138-
await self._profile_handler.set_avatar_url(user, requester, "", by_admin)
139+
await self._profile_handler.set_avatar_url(
140+
user, requester, "", by_admin, deactivation=True
141+
)
139142
# Remove displayname from this user
140-
await self._profile_handler.set_displayname(user, requester, "", by_admin)
143+
await self._profile_handler.set_displayname(
144+
user, requester, "", by_admin, deactivation=True
145+
)
141146

142147
logger.info("Marking %s as erased", user_id)
143148
await self.store.mark_user_erased(user_id)
@@ -160,6 +165,13 @@ async def deactivate_account(
160165
# Remove account data (including ignored users and push rules).
161166
await self.store.purge_account_data_for_user(user_id)
162167

168+
# Let modules know the user has been deactivated.
169+
await self._third_party_rules.on_user_deactivation_status_changed(
170+
user_id,
171+
True,
172+
by_admin,
173+
)
174+
163175
return identity_server_supports_unbinding
164176

165177
async def _reject_pending_invites_for_user(self, user_id: str) -> None:
@@ -264,6 +276,10 @@ async def activate_account(self, user_id: str) -> None:
264276
# Mark the user as active.
265277
await self.store.set_user_deactivated_status(user_id, False)
266278

279+
await self._third_party_rules.on_user_deactivation_status_changed(
280+
user_id, False, True
281+
)
282+
267283
# Add the user to the directory, if necessary. Note that
268284
# this must be done after the user is re-activated, because
269285
# deactivated users are excluded from the user directory.

synapse/handlers/profile.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ def __init__(self, hs: "HomeServer"):
7171

7272
self.server_name = hs.config.server.server_name
7373

74+
self._third_party_rules = hs.get_third_party_event_rules()
75+
7476
if hs.config.worker.run_background_tasks:
7577
self.clock.looping_call(
7678
self._update_remote_profile_cache, self.PROFILE_UPDATE_MS
@@ -171,6 +173,7 @@ async def set_displayname(
171173
requester: Requester,
172174
new_displayname: str,
173175
by_admin: bool = False,
176+
deactivation: bool = False,
174177
) -> None:
175178
"""Set the displayname of a user
176179
@@ -179,6 +182,7 @@ async def set_displayname(
179182
requester: The user attempting to make this change.
180183
new_displayname: The displayname to give this user.
181184
by_admin: Whether this change was made by an administrator.
185+
deactivation: Whether this change was made while deactivating the user.
182186
"""
183187
if not self.hs.is_mine(target_user):
184188
raise SynapseError(400, "User is not hosted on this homeserver")
@@ -227,6 +231,10 @@ async def set_displayname(
227231
target_user.to_string(), profile
228232
)
229233

234+
await self._third_party_rules.on_profile_update(
235+
target_user.to_string(), profile, by_admin, deactivation
236+
)
237+
230238
await self._update_join_states(requester, target_user)
231239

232240
async def get_avatar_url(self, target_user: UserID) -> Optional[str]:
@@ -261,6 +269,7 @@ async def set_avatar_url(
261269
requester: Requester,
262270
new_avatar_url: str,
263271
by_admin: bool = False,
272+
deactivation: bool = False,
264273
) -> None:
265274
"""Set a new avatar URL for a user.
266275
@@ -269,6 +278,7 @@ async def set_avatar_url(
269278
requester: The user attempting to make this change.
270279
new_avatar_url: The avatar URL to give this user.
271280
by_admin: Whether this change was made by an administrator.
281+
deactivation: Whether this change was made while deactivating the user.
272282
"""
273283
if not self.hs.is_mine(target_user):
274284
raise SynapseError(400, "User is not hosted on this homeserver")
@@ -315,6 +325,10 @@ async def set_avatar_url(
315325
target_user.to_string(), profile
316326
)
317327

328+
await self._third_party_rules.on_profile_update(
329+
target_user.to_string(), profile, by_admin, deactivation
330+
)
331+
318332
await self._update_join_states(requester, target_user)
319333

320334
@cached()

synapse/module_api/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@
145145
"JsonDict",
146146
"EventBase",
147147
"StateMap",
148+
"ProfileInfo",
148149
]
149150

150151
logger = logging.getLogger(__name__)

0 commit comments

Comments
 (0)