diff --git a/synapse/events/third_party_rules.py b/synapse/events/third_party_rules.py
index 72ab696898..e438f712fd 100644
--- a/synapse/events/third_party_rules.py
+++ b/synapse/events/third_party_rules.py
@@ -45,6 +45,9 @@ CHECK_CAN_DEACTIVATE_USER_CALLBACK = Callable[[str, bool], Awaitable[bool]]
ON_PROFILE_UPDATE_CALLBACK = Callable[[str, ProfileInfo, bool, bool], Awaitable]
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK = Callable[[str, bool, bool], Awaitable]
ON_THREEPID_BIND_CALLBACK = Callable[[str, str, str], Awaitable]
+ON_THREEPID_UNBIND_CALLBACK = Callable[
+ [str, str, str, str], Awaitable[Tuple[bool, bool]]
+]
def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
@@ -174,6 +177,7 @@ class ThirdPartyEventRules:
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = []
self._on_threepid_bind_callbacks: List[ON_THREEPID_BIND_CALLBACK] = []
+ self._on_threepid_unbind_callbacks: List[ON_THREEPID_UNBIND_CALLBACK] = []
def register_third_party_rules_callbacks(
self,
@@ -193,6 +197,7 @@ class ThirdPartyEventRules:
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = None,
on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None,
+ on_threepid_unbind: Optional[ON_THREEPID_UNBIND_CALLBACK] = None,
) -> None:
"""Register callbacks from modules for each hook."""
if check_event_allowed is not None:
@@ -230,6 +235,9 @@ class ThirdPartyEventRules:
if on_threepid_bind is not None:
self._on_threepid_bind_callbacks.append(on_threepid_bind)
+ if on_threepid_unbind is not None:
+ self._on_threepid_unbind_callbacks.append(on_threepid_unbind)
+
async def check_event_allowed(
self, event: EventBase, context: EventContext
) -> Tuple[bool, Optional[dict]]:
@@ -523,3 +531,41 @@ class ThirdPartyEventRules:
logger.exception(
"Failed to run module API callback %s: %s", callback, e
)
+
+ async def on_threepid_unbind(
+ self, user_id: str, medium: str, address: str, identity_server: str
+ ) -> Tuple[bool, bool]:
+ """Called before a threepid association is removed.
+
+ Note that this callback is called before an association is deleted on the
+ local homeserver.
+
+ Args:
+ user_id: the user being associated with the threepid.
+ medium: the threepid's medium.
+ address: the threepid's address.
+ identity_server: the identity server where the threepid was successfully registered.
+
+ Returns:
+ A tuple of 2 booleans reporting if a changed happened for the first, and if unbind
+ needs to stop there for the second (True value). In this case no other module unbind will be
+ called, and the default unbind made to the IS that was used on bind will also be skipped.
+ In any case the mapping will be removed from the Synapse 3pid remote table, except if an Exception
+ was raised at some point.
+ """
+
+ global_changed = False
+ for callback in self._on_threepid_unbind_callbacks:
+ try:
+ (changed, stop) = await callback(
+ user_id, medium, address, identity_server
+ )
+ global_changed |= changed
+ if stop:
+ return global_changed, True
+ except Exception as e:
+ logger.exception(
+ "Failed to run module API callback %s: %s", callback, e
+ )
+
+ return global_changed, False
diff --git a/synapse/handlers/identity.py b/synapse/handlers/identity.py
index 848e46eb9b..b98c7b3b40 100644
--- a/synapse/handlers/identity.py
+++ b/synapse/handlers/identity.py
@@ -275,49 +275,67 @@ class IdentityHandler:
server doesn't support unbinding
"""
- if not valid_id_server_location(id_server):
- raise SynapseError(
- 400,
- "id_server must be a valid hostname with optional port and path components",
- )
-
- url = "https://%s/_matrix/identity/v2/3pid/unbind" % (id_server,)
- url_bytes = b"/_matrix/identity/v2/3pid/unbind"
-
- content = {
- "mxid": mxid,
- "threepid": {"medium": threepid["medium"], "address": threepid["address"]},
- }
-
- # we abuse the federation http client to sign the request, but we have to send it
- # using the normal http client since we don't want the SRV lookup and want normal
- # 'browser-like' HTTPS.
- auth_headers = self.federation_http_client.build_auth_headers(
- destination=None,
- method=b"POST",
- url_bytes=url_bytes,
- content=content,
- destination_is=id_server.encode("ascii"),
+ medium = threepid["medium"]
+ address = threepid["address"]
+
+ (
+ changed,
+ stop,
+ ) = await self.hs.get_third_party_event_rules().on_threepid_unbind(
+ mxid, medium, address, id_server
)
- headers = {b"Authorization": auth_headers}
- try:
- # Use the blacklisting http client as this call is only to identity servers
- # provided by a client
- await self.blacklisting_http_client.post_json_get_json(
- url, content, headers
+ # If a module wants to take over unbind it will return stop = True,
+ # in this case we should just purge the table from the 3pid record
+ if not stop:
+ if not valid_id_server_location(id_server):
+ raise SynapseError(
+ 400,
+ "id_server must be a valid hostname with optional port and path components",
+ )
+
+ url = "https://%s/_matrix/identity/v2/3pid/unbind" % (id_server,)
+ url_bytes = b"/_matrix/identity/v2/3pid/unbind"
+
+ content = {
+ "mxid": mxid,
+ "threepid": {
+ "medium": threepid["medium"],
+ "address": threepid["address"],
+ },
+ }
+
+ # we abuse the federation http client to sign the request, but we have to send it
+ # using the normal http client since we don't want the SRV lookup and want normal
+ # 'browser-like' HTTPS.
+ auth_headers = self.federation_http_client.build_auth_headers(
+ destination=None,
+ method=b"POST",
+ url_bytes=url_bytes,
+ content=content,
+ destination_is=id_server.encode("ascii"),
)
- changed = True
- except HttpResponseException as e:
- changed = False
- if e.code in (400, 404, 501):
- # The remote server probably doesn't support unbinding (yet)
- logger.warning("Received %d response while unbinding threepid", e.code)
- else:
- logger.error("Failed to unbind threepid on identity server: %s", e)
- raise SynapseError(500, "Failed to contact identity server")
- except RequestTimedOutError:
- raise SynapseError(500, "Timed out contacting identity server")
+ headers = {b"Authorization": auth_headers}
+
+ try:
+ # Use the blacklisting http client as this call is only to identity servers
+ # provided by a client
+ await self.blacklisting_http_client.post_json_get_json(
+ url, content, headers
+ )
+ changed &= True
+ except HttpResponseException as e:
+ changed &= False
+ if e.code in (400, 404, 501):
+ # The remote server probably doesn't support unbinding (yet)
+ logger.warning(
+ "Received %d response while unbinding threepid", e.code
+ )
+ else:
+ logger.error("Failed to unbind threepid on identity server: %s", e)
+ raise SynapseError(500, "Failed to contact identity server")
+ except RequestTimedOutError:
+ raise SynapseError(500, "Timed out contacting identity server")
await self.store.remove_user_bound_threepid(
user_id=mxid,
diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py
index 96a661177a..38c71b8b43 100644
--- a/synapse/module_api/__init__.py
+++ b/synapse/module_api/__init__.py
@@ -67,6 +67,7 @@ from synapse.events.third_party_rules import (
ON_NEW_EVENT_CALLBACK,
ON_PROFILE_UPDATE_CALLBACK,
ON_THREEPID_BIND_CALLBACK,
+ ON_THREEPID_UNBIND_CALLBACK,
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK,
)
from synapse.handlers.account_data import ON_ACCOUNT_DATA_UPDATED_CALLBACK
@@ -319,6 +320,7 @@ class ModuleApi:
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = None,
on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None,
+ on_threepid_unbind: Optional[ON_THREEPID_UNBIND_CALLBACK] = None,
) -> None:
"""Registers callbacks for third party event rules capabilities.
@@ -335,6 +337,7 @@ class ModuleApi:
on_profile_update=on_profile_update,
on_user_deactivation_status_changed=on_user_deactivation_status_changed,
on_threepid_bind=on_threepid_bind,
+ on_threepid_unbind=on_threepid_unbind,
)
def register_presence_router_callbacks(
|