diff --git a/synapse/module_api/__init__.py b/synapse/module_api/__init__.py
index f6bfd93d3c..e22d6f3ab7 100644
--- a/synapse/module_api/__init__.py
+++ b/synapse/module_api/__init__.py
@@ -18,7 +18,6 @@
# [This file includes modifications made by New Vector Limited]
#
#
-import email.utils
import logging
from typing import (
TYPE_CHECKING,
@@ -45,6 +44,7 @@ from twisted.internet.interfaces import IDelayedCall
from twisted.web.resource import Resource
from synapse.api import errors
+from synapse.api.constants import ProfileFields
from synapse.api.errors import SynapseError
from synapse.api.presence import UserPresenceState
from synapse.config import ConfigError
@@ -89,6 +89,14 @@ from synapse.module_api.callbacks.account_validity_callbacks import (
ON_USER_LOGIN_CALLBACK,
ON_USER_REGISTRATION_CALLBACK,
)
+from synapse.module_api.callbacks.media_repository_callbacks import (
+ GET_MEDIA_CONFIG_FOR_USER_CALLBACK,
+ IS_USER_ALLOWED_TO_UPLOAD_MEDIA_OF_SIZE_CALLBACK,
+)
+from synapse.module_api.callbacks.ratelimit_callbacks import (
+ GET_RATELIMIT_OVERRIDE_FOR_USER_CALLBACK,
+ RatelimitOverride,
+)
from synapse.module_api.callbacks.spamchecker_callbacks import (
CHECK_EVENT_FOR_SPAM_CALLBACK,
CHECK_LOGIN_FOR_SPAM_CALLBACK,
@@ -101,21 +109,17 @@ from synapse.module_api.callbacks.spamchecker_callbacks import (
USER_MAY_INVITE_CALLBACK,
USER_MAY_JOIN_ROOM_CALLBACK,
USER_MAY_PUBLISH_ROOM_CALLBACK,
- USER_MAY_SEND_3PID_INVITE_CALLBACK,
+ USER_MAY_SEND_STATE_EVENT_CALLBACK,
SpamCheckerModuleApiCallbacks,
)
from synapse.module_api.callbacks.third_party_event_rules_callbacks import (
CHECK_CAN_DEACTIVATE_USER_CALLBACK,
CHECK_CAN_SHUTDOWN_ROOM_CALLBACK,
CHECK_EVENT_ALLOWED_CALLBACK,
- CHECK_THREEPID_CAN_BE_INVITED_CALLBACK,
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK,
- ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK,
ON_CREATE_ROOM_CALLBACK,
ON_NEW_EVENT_CALLBACK,
ON_PROFILE_UPDATE_CALLBACK,
- ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK,
- ON_THREEPID_BIND_CALLBACK,
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK,
)
from synapse.push.httppusher import HttpPusher
@@ -188,6 +192,7 @@ __all__ = [
"ProfileInfo",
"RoomAlias",
"UserProfile",
+ "RatelimitOverride",
]
logger = logging.getLogger(__name__)
@@ -260,7 +265,6 @@ class ModuleApi:
self._state = hs.get_state_handler()
self._clock: Clock = hs.get_clock()
self._registration_handler = hs.get_registration_handler()
- self._send_email_handler = hs.get_send_email_handler()
self._push_rules_handler = hs.get_push_rules_handler()
self._pusherpool = hs.get_pusherpool()
self._device_handler = hs.get_device_handler()
@@ -269,20 +273,6 @@ class ModuleApi:
self.msc3861_oauth_delegation_enabled = hs.config.experimental.msc3861.enabled
self._event_serializer = hs.get_event_client_serializer()
- try:
- app_name = self._hs.config.email.email_app_name
-
- self._from_string = self._hs.config.email.email_notif_from % {
- "app": app_name
- }
- except (KeyError, TypeError):
- # If substitution failed (which can happen if the string contains
- # placeholders other than just "app", or if the type of the placeholder is
- # not a string), fall back to the bare strings.
- self._from_string = self._hs.config.email.email_notif_from
-
- self._raw_from = email.utils.parseaddr(self._from_string)[1]
-
# We expose these as properties below in order to attach a helpful docstring.
self._http_client: SimpleHttpClient = hs.get_simple_http_client()
self._public_room_list_manager = PublicRoomListManager(hs)
@@ -304,12 +294,12 @@ class ModuleApi:
] = None,
user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None,
user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None,
- user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None,
user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None,
user_may_create_room_alias: Optional[
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
] = None,
user_may_publish_room: Optional[USER_MAY_PUBLISH_ROOM_CALLBACK] = None,
+ user_may_send_state_event: Optional[USER_MAY_SEND_STATE_EVENT_CALLBACK] = None,
check_username_for_spam: Optional[CHECK_USERNAME_FOR_SPAM_CALLBACK] = None,
check_registration_for_spam: Optional[
CHECK_REGISTRATION_FOR_SPAM_CALLBACK
@@ -326,7 +316,6 @@ class ModuleApi:
should_drop_federated_event=should_drop_federated_event,
user_may_join_room=user_may_join_room,
user_may_invite=user_may_invite,
- user_may_send_3pid_invite=user_may_send_3pid_invite,
user_may_create_room=user_may_create_room,
user_may_create_room_alias=user_may_create_room_alias,
user_may_publish_room=user_may_publish_room,
@@ -334,6 +323,7 @@ class ModuleApi:
check_registration_for_spam=check_registration_for_spam,
check_media_file_for_spam=check_media_file_for_spam,
check_login_for_spam=check_login_for_spam,
+ user_may_send_state_event=user_may_send_state_event,
)
def register_account_validity_callbacks(
@@ -359,14 +349,41 @@ class ModuleApi:
on_legacy_admin_request=on_legacy_admin_request,
)
+ def register_ratelimit_callbacks(
+ self,
+ *,
+ get_ratelimit_override_for_user: Optional[
+ GET_RATELIMIT_OVERRIDE_FOR_USER_CALLBACK
+ ] = None,
+ ) -> None:
+ """Registers callbacks for ratelimit capabilities.
+ Added in Synapse v1.132.0.
+ """
+ return self._callbacks.ratelimit.register_callbacks(
+ get_ratelimit_override_for_user=get_ratelimit_override_for_user,
+ )
+
+ def register_media_repository_callbacks(
+ self,
+ *,
+ get_media_config_for_user: Optional[GET_MEDIA_CONFIG_FOR_USER_CALLBACK] = None,
+ is_user_allowed_to_upload_media_of_size: Optional[
+ IS_USER_ALLOWED_TO_UPLOAD_MEDIA_OF_SIZE_CALLBACK
+ ] = None,
+ ) -> None:
+ """Registers callbacks for media repository capabilities.
+ Added in Synapse v1.132.0.
+ """
+ return self._callbacks.media_repository.register_callbacks(
+ get_media_config_for_user=get_media_config_for_user,
+ is_user_allowed_to_upload_media_of_size=is_user_allowed_to_upload_media_of_size,
+ )
+
def register_third_party_rules_callbacks(
self,
*,
check_event_allowed: Optional[CHECK_EVENT_ALLOWED_CALLBACK] = None,
on_create_room: Optional[ON_CREATE_ROOM_CALLBACK] = None,
- check_threepid_can_be_invited: Optional[
- CHECK_THREEPID_CAN_BE_INVITED_CALLBACK
- ] = None,
check_visibility_can_be_modified: Optional[
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
] = None,
@@ -377,13 +394,6 @@ class ModuleApi:
on_user_deactivation_status_changed: Optional[
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = None,
- on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None,
- on_add_user_third_party_identifier: Optional[
- ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
- ] = None,
- on_remove_user_third_party_identifier: Optional[
- ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
- ] = None,
) -> None:
"""Registers callbacks for third party event rules capabilities.
@@ -392,16 +402,12 @@ class ModuleApi:
return self._callbacks.third_party_event_rules.register_third_party_rules_callbacks(
check_event_allowed=check_event_allowed,
on_create_room=on_create_room,
- check_threepid_can_be_invited=check_threepid_can_be_invited,
check_visibility_can_be_modified=check_visibility_can_be_modified,
on_new_event=on_new_event,
check_can_shutdown_room=check_can_shutdown_room,
check_can_deactivate_user=check_can_deactivate_user,
on_profile_update=on_profile_update,
on_user_deactivation_status_changed=on_user_deactivation_status_changed,
- on_threepid_bind=on_threepid_bind,
- on_add_user_third_party_identifier=on_add_user_third_party_identifier,
- on_remove_user_third_party_identifier=on_remove_user_third_party_identifier,
)
def register_presence_router_callbacks(
@@ -561,14 +567,6 @@ class ModuleApi:
return self._hs.config.server.public_baseurl
@property
- def email_app_name(self) -> str:
- """The application name configured in the homeserver's configuration.
-
- Added in Synapse v1.39.0.
- """
- return self._hs.config.email.email_app_name
-
- @property
def server_name(self) -> str:
"""The server name for the local homeserver.
@@ -695,23 +693,6 @@ class ModuleApi:
user_id = UserID.from_string(f"@{localpart}:{server_name}")
return await self._store.get_profileinfo(user_id)
- async def get_threepids_for_user(self, user_id: str) -> List[Dict[str, str]]:
- """Look up the threepids (email addresses and phone numbers) associated with the
- given Matrix user ID.
-
- Added in Synapse v1.39.0.
-
- Args:
- user_id: The Matrix user ID to look up threepids for.
-
- Returns:
- A list of threepids, each threepid being represented by a dictionary
- containing a "medium" key which value is "email" for email addresses and
- "msisdn" for phone numbers, and an "address" key which value is the
- threepid's address.
- """
- return [attr.asdict(t) for t in await self._store.user_get_threepids(user_id)]
-
def check_user_exists(self, user_id: str) -> "defer.Deferred[Optional[str]]":
"""Check if user exists.
@@ -893,9 +874,9 @@ class ModuleApi:
Raises:
synapse.api.errors.AuthError: the access token is invalid
"""
- assert isinstance(
- self._device_handler, DeviceHandler
- ), "invalidate_access_token can only be called on the main process"
+ assert isinstance(self._device_handler, DeviceHandler), (
+ "invalidate_access_token can only be called on the main process"
+ )
# see if the access token corresponds to a device
user_info = yield defer.ensureDeferred(
@@ -1086,7 +1067,10 @@ class ModuleApi:
content = {}
# Set the profile if not already done by the module.
- if "avatar_url" not in content or "displayname" not in content:
+ if (
+ ProfileFields.AVATAR_URL not in content
+ or ProfileFields.DISPLAYNAME not in content
+ ):
try:
# Try to fetch the user's profile.
profile = await self._hs.get_profile_handler().get_profile(
@@ -1095,8 +1079,8 @@ class ModuleApi:
except SynapseError as e:
# If the profile couldn't be found, use default values.
profile = {
- "displayname": target_user_id.localpart,
- "avatar_url": None,
+ ProfileFields.DISPLAYNAME: target_user_id.localpart,
+ ProfileFields.AVATAR_URL: None,
}
if e.code != 404:
@@ -1109,11 +1093,9 @@ class ModuleApi:
)
# Set the profile where it needs to be set.
- if "avatar_url" not in content:
- content["avatar_url"] = profile["avatar_url"]
-
- if "displayname" not in content:
- content["displayname"] = profile["displayname"]
+ for field_name in [ProfileFields.AVATAR_URL, ProfileFields.DISPLAYNAME]:
+ if field_name not in content and field_name in profile:
+ content[field_name] = profile[field_name]
event_id, _ = await self._hs.get_room_member_handler().update_membership(
requester=requester,
@@ -1398,31 +1380,6 @@ class ModuleApi:
status[p.device_id] = sent
return status
- async def send_mail(
- self,
- recipient: str,
- subject: str,
- html: str,
- text: str,
- ) -> None:
- """Send an email on behalf of the homeserver.
-
- Added in Synapse v1.39.0.
-
- Args:
- recipient: The email address for the recipient.
- subject: The email's subject.
- html: The email's HTML content.
- text: The email's text content.
- """
- await self._send_email_handler.send_email(
- email_address=recipient,
- subject=subject,
- app_name=self.email_app_name,
- html=html,
- text=text,
- )
-
def read_templates(
self,
filenames: List[str],
@@ -1584,30 +1541,6 @@ class ModuleApi:
"""
await self._registration_handler.check_username(username)
- async def store_remote_3pid_association(
- self, user_id: str, medium: str, address: str, id_server: str
- ) -> None:
- """Stores an existing association between a user ID and a third-party identifier.
-
- The association must already exist on the remote identity server.
-
- Added in Synapse v1.56.0.
-
- Args:
- user_id: The user ID that's been associated with the 3PID.
- medium: The medium of the 3PID (current supported values are "msisdn" and
- "email").
- address: The address of the 3PID.
- id_server: The identity server the 3PID association has been registered on.
- This should only be the domain (or IP address, optionally with the port
- number) for the identity server. This will be used to reach out to the
- identity server using HTTPS (unless specified otherwise by Synapse's
- configuration) when attempting to unbind the third-party identifier.
-
-
- """
- await self._store.add_user_bound_threepid(user_id, medium, address, id_server)
-
def check_push_rule_actions(
self, actions: List[Union[str, Dict[str, str]]]
) -> None:
@@ -1844,6 +1777,10 @@ class ModuleApi:
deactivation=deactivation,
)
+ def get_current_time_msec(self) -> int:
+ """Returns the current server time in milliseconds."""
+ return self._clock.time_msec()
+
class PublicRoomListManager:
"""Contains methods for adding to, removing from and querying whether a room
diff --git a/synapse/module_api/callbacks/__init__.py b/synapse/module_api/callbacks/__init__.py
index c20d9543fb..16ef7a4b47 100644
--- a/synapse/module_api/callbacks/__init__.py
+++ b/synapse/module_api/callbacks/__init__.py
@@ -27,6 +27,12 @@ if TYPE_CHECKING:
from synapse.module_api.callbacks.account_validity_callbacks import (
AccountValidityModuleApiCallbacks,
)
+from synapse.module_api.callbacks.media_repository_callbacks import (
+ MediaRepositoryModuleApiCallbacks,
+)
+from synapse.module_api.callbacks.ratelimit_callbacks import (
+ RatelimitModuleApiCallbacks,
+)
from synapse.module_api.callbacks.spamchecker_callbacks import (
SpamCheckerModuleApiCallbacks,
)
@@ -38,5 +44,7 @@ from synapse.module_api.callbacks.third_party_event_rules_callbacks import (
class ModuleApiCallbacks:
def __init__(self, hs: "HomeServer") -> None:
self.account_validity = AccountValidityModuleApiCallbacks()
+ self.media_repository = MediaRepositoryModuleApiCallbacks(hs)
+ self.ratelimit = RatelimitModuleApiCallbacks(hs)
self.spam_checker = SpamCheckerModuleApiCallbacks(hs)
self.third_party_event_rules = ThirdPartyEventRulesModuleApiCallbacks(hs)
diff --git a/synapse/module_api/callbacks/media_repository_callbacks.py b/synapse/module_api/callbacks/media_repository_callbacks.py
new file mode 100644
index 0000000000..6fa80a8eab
--- /dev/null
+++ b/synapse/module_api/callbacks/media_repository_callbacks.py
@@ -0,0 +1,76 @@
+#
+# This file is licensed under the Affero General Public License (AGPL) version 3.
+#
+# Copyright (C) 2025 New Vector, Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# See the GNU Affero General Public License for more details:
+# <https://www.gnu.org/licenses/agpl-3.0.html>.
+#
+
+import logging
+from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional
+
+from synapse.types import JsonDict
+from synapse.util.async_helpers import delay_cancellation
+from synapse.util.metrics import Measure
+
+if TYPE_CHECKING:
+ from synapse.server import HomeServer
+
+logger = logging.getLogger(__name__)
+
+GET_MEDIA_CONFIG_FOR_USER_CALLBACK = Callable[[str], Awaitable[Optional[JsonDict]]]
+
+IS_USER_ALLOWED_TO_UPLOAD_MEDIA_OF_SIZE_CALLBACK = Callable[[str, int], Awaitable[bool]]
+
+
+class MediaRepositoryModuleApiCallbacks:
+ def __init__(self, hs: "HomeServer") -> None:
+ self.clock = hs.get_clock()
+ self._get_media_config_for_user_callbacks: List[
+ GET_MEDIA_CONFIG_FOR_USER_CALLBACK
+ ] = []
+ self._is_user_allowed_to_upload_media_of_size_callbacks: List[
+ IS_USER_ALLOWED_TO_UPLOAD_MEDIA_OF_SIZE_CALLBACK
+ ] = []
+
+ def register_callbacks(
+ self,
+ get_media_config_for_user: Optional[GET_MEDIA_CONFIG_FOR_USER_CALLBACK] = None,
+ is_user_allowed_to_upload_media_of_size: Optional[
+ IS_USER_ALLOWED_TO_UPLOAD_MEDIA_OF_SIZE_CALLBACK
+ ] = None,
+ ) -> None:
+ """Register callbacks from module for each hook."""
+ if get_media_config_for_user is not None:
+ self._get_media_config_for_user_callbacks.append(get_media_config_for_user)
+
+ if is_user_allowed_to_upload_media_of_size is not None:
+ self._is_user_allowed_to_upload_media_of_size_callbacks.append(
+ is_user_allowed_to_upload_media_of_size
+ )
+
+ async def get_media_config_for_user(self, user_id: str) -> Optional[JsonDict]:
+ for callback in self._get_media_config_for_user_callbacks:
+ with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
+ res: Optional[JsonDict] = await delay_cancellation(callback(user_id))
+ if res:
+ return res
+
+ return None
+
+ async def is_user_allowed_to_upload_media_of_size(
+ self, user_id: str, size: int
+ ) -> bool:
+ for callback in self._is_user_allowed_to_upload_media_of_size_callbacks:
+ with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
+ res: bool = await delay_cancellation(callback(user_id, size))
+ if not res:
+ return res
+
+ return True
diff --git a/synapse/module_api/callbacks/ratelimit_callbacks.py b/synapse/module_api/callbacks/ratelimit_callbacks.py
new file mode 100644
index 0000000000..64f9cc81e8
--- /dev/null
+++ b/synapse/module_api/callbacks/ratelimit_callbacks.py
@@ -0,0 +1,74 @@
+#
+# This file is licensed under the Affero General Public License (AGPL) version 3.
+#
+# Copyright (C) 2025 New Vector, Ltd
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# See the GNU Affero General Public License for more details:
+# <https://www.gnu.org/licenses/agpl-3.0.html>.
+#
+
+import logging
+from typing import TYPE_CHECKING, Awaitable, Callable, List, Optional
+
+import attr
+
+from synapse.util.async_helpers import delay_cancellation
+from synapse.util.metrics import Measure
+
+if TYPE_CHECKING:
+ from synapse.server import HomeServer
+
+logger = logging.getLogger(__name__)
+
+
+@attr.s(auto_attribs=True)
+class RatelimitOverride:
+ """Represents a ratelimit being overridden."""
+
+ per_second: float
+ """The number of actions that can be performed in a second. `0.0` means that ratelimiting is disabled."""
+ burst_count: int
+ """How many actions that can be performed before being limited."""
+
+
+GET_RATELIMIT_OVERRIDE_FOR_USER_CALLBACK = Callable[
+ [str, str], Awaitable[Optional[RatelimitOverride]]
+]
+
+
+class RatelimitModuleApiCallbacks:
+ def __init__(self, hs: "HomeServer") -> None:
+ self.clock = hs.get_clock()
+ self._get_ratelimit_override_for_user_callbacks: List[
+ GET_RATELIMIT_OVERRIDE_FOR_USER_CALLBACK
+ ] = []
+
+ def register_callbacks(
+ self,
+ get_ratelimit_override_for_user: Optional[
+ GET_RATELIMIT_OVERRIDE_FOR_USER_CALLBACK
+ ] = None,
+ ) -> None:
+ """Register callbacks from module for each hook."""
+ if get_ratelimit_override_for_user is not None:
+ self._get_ratelimit_override_for_user_callbacks.append(
+ get_ratelimit_override_for_user
+ )
+
+ async def get_ratelimit_override_for_user(
+ self, user_id: str, limiter_name: str
+ ) -> Optional[RatelimitOverride]:
+ for callback in self._get_ratelimit_override_for_user_callbacks:
+ with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
+ res: Optional[RatelimitOverride] = await delay_cancellation(
+ callback(user_id, limiter_name)
+ )
+ if res:
+ return res
+
+ return None
diff --git a/synapse/module_api/callbacks/spamchecker_callbacks.py b/synapse/module_api/callbacks/spamchecker_callbacks.py
index 17079ff781..30cab9eb7e 100644
--- a/synapse/module_api/callbacks/spamchecker_callbacks.py
+++ b/synapse/module_api/callbacks/spamchecker_callbacks.py
@@ -19,8 +19,10 @@
#
#
+import functools
import inspect
import logging
+from copy import deepcopy
from typing import (
TYPE_CHECKING,
Any,
@@ -28,14 +30,13 @@ from typing import (
Callable,
Collection,
List,
+ Literal,
Optional,
Tuple,
Union,
+ cast,
)
-# `Literal` appears with Python 3.8.
-from typing_extensions import Literal
-
import synapse
from synapse.api.errors import Codes
from synapse.logging.opentracing import trace
@@ -104,24 +105,28 @@ USER_MAY_INVITE_CALLBACK = Callable[
]
],
]
-USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[
- [str, str, str, str],
- Awaitable[
- Union[
- Literal["NOT_SPAM"],
- Codes,
- # Highly experimental, not officially part of the spamchecker API, may
- # disappear without warning depending on the results of ongoing
- # experiments.
- # Use this to return additional information as part of an error.
- Tuple[Codes, JsonDict],
- # Deprecated
- bool,
- ]
+USER_MAY_CREATE_ROOM_CALLBACK_RETURN_VALUE = Union[
+ Literal["NOT_SPAM"],
+ Codes,
+ # Highly experimental, not officially part of the spamchecker API, may
+ # disappear without warning depending on the results of ongoing
+ # experiments.
+ # Use this to return additional information as part of an error.
+ Tuple[Codes, JsonDict],
+ # Deprecated
+ bool,
+]
+USER_MAY_CREATE_ROOM_CALLBACK = Union[
+ Callable[
+ [str, JsonDict],
+ Awaitable[USER_MAY_CREATE_ROOM_CALLBACK_RETURN_VALUE],
+ ],
+ Callable[ # Single argument variant for backwards compatibility
+ [str], Awaitable[USER_MAY_CREATE_ROOM_CALLBACK_RETURN_VALUE]
],
]
-USER_MAY_CREATE_ROOM_CALLBACK = Callable[
- [str],
+USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[
+ [str, RoomAlias],
Awaitable[
Union[
Literal["NOT_SPAM"],
@@ -136,8 +141,8 @@ USER_MAY_CREATE_ROOM_CALLBACK = Callable[
]
],
]
-USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[
- [str, RoomAlias],
+USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[
+ [str, str],
Awaitable[
Union[
Literal["NOT_SPAM"],
@@ -152,8 +157,8 @@ USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[
]
],
]
-USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[
- [str, str],
+USER_MAY_SEND_STATE_EVENT_CALLBACK = Callable[
+ [str, str, str, str, JsonDict],
Awaitable[
Union[
Literal["NOT_SPAM"],
@@ -163,12 +168,13 @@ USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[
# experiments.
# Use this to return additional information as part of an error.
Tuple[Codes, JsonDict],
- # Deprecated
- bool,
]
],
]
-CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[UserProfile], Awaitable[bool]]
+CHECK_USERNAME_FOR_SPAM_CALLBACK = Union[
+ Callable[[UserProfile], Awaitable[bool]],
+ Callable[[UserProfile, str], Awaitable[bool]],
+]
LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[
[
Optional[dict],
@@ -293,6 +299,7 @@ def load_legacy_spam_checkers(hs: "synapse.server.HomeServer") -> None:
"Bad signature for callback check_registration_for_spam",
)
+ @functools.wraps(wrapped_func)
def run(*args: Any, **kwargs: Any) -> Awaitable:
# Assertion required because mypy can't prove we won't change `f`
# back to `None`. See
@@ -324,10 +331,10 @@ class SpamCheckerModuleApiCallbacks:
] = []
self._user_may_join_room_callbacks: List[USER_MAY_JOIN_ROOM_CALLBACK] = []
self._user_may_invite_callbacks: List[USER_MAY_INVITE_CALLBACK] = []
- self._user_may_send_3pid_invite_callbacks: List[
- USER_MAY_SEND_3PID_INVITE_CALLBACK
- ] = []
self._user_may_create_room_callbacks: List[USER_MAY_CREATE_ROOM_CALLBACK] = []
+ self._user_may_send_state_event_callbacks: List[
+ USER_MAY_SEND_STATE_EVENT_CALLBACK
+ ] = []
self._user_may_create_room_alias_callbacks: List[
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
] = []
@@ -351,7 +358,6 @@ class SpamCheckerModuleApiCallbacks:
] = None,
user_may_join_room: Optional[USER_MAY_JOIN_ROOM_CALLBACK] = None,
user_may_invite: Optional[USER_MAY_INVITE_CALLBACK] = None,
- user_may_send_3pid_invite: Optional[USER_MAY_SEND_3PID_INVITE_CALLBACK] = None,
user_may_create_room: Optional[USER_MAY_CREATE_ROOM_CALLBACK] = None,
user_may_create_room_alias: Optional[
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK
@@ -363,6 +369,7 @@ class SpamCheckerModuleApiCallbacks:
] = None,
check_media_file_for_spam: Optional[CHECK_MEDIA_FILE_FOR_SPAM_CALLBACK] = None,
check_login_for_spam: Optional[CHECK_LOGIN_FOR_SPAM_CALLBACK] = None,
+ user_may_send_state_event: Optional[USER_MAY_SEND_STATE_EVENT_CALLBACK] = None,
) -> None:
"""Register callbacks from module for each hook."""
if check_event_for_spam is not None:
@@ -379,14 +386,14 @@ class SpamCheckerModuleApiCallbacks:
if user_may_invite is not None:
self._user_may_invite_callbacks.append(user_may_invite)
- if user_may_send_3pid_invite is not None:
- self._user_may_send_3pid_invite_callbacks.append(
- user_may_send_3pid_invite,
- )
-
if user_may_create_room is not None:
self._user_may_create_room_callbacks.append(user_may_create_room)
+ if user_may_send_state_event is not None:
+ self._user_may_send_state_event_callbacks.append(
+ user_may_send_state_event,
+ )
+
if user_may_create_room_alias is not None:
self._user_may_create_room_alias_callbacks.append(
user_may_create_room_alias,
@@ -573,29 +580,42 @@ class SpamCheckerModuleApiCallbacks:
# No spam-checker has rejected the request, let it pass.
return self.NOT_SPAM
- async def user_may_send_3pid_invite(
- self, inviter_userid: str, medium: str, address: str, room_id: str
+ async def user_may_create_room(
+ self, userid: str, room_config: JsonDict
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
- """Checks if a given user may invite a given threepid into the room
-
- Note that if the threepid is already associated with a Matrix user ID, Synapse
- will call user_may_invite with said user ID instead.
+ """Checks if a given user may create a room
Args:
- inviter_userid: The user ID of the sender of the invitation
- medium: The 3PID's medium (e.g. "email")
- address: The 3PID's address (e.g. "alice@example.com")
- room_id: The room ID
-
- Returns:
- NOT_SPAM if the operation is permitted, Codes otherwise.
+ userid: The ID of the user attempting to create a room
+ room_config: The room creation configuration which is the body of the /createRoom request
"""
- for callback in self._user_may_send_3pid_invite_callbacks:
+ for callback in self._user_may_create_room_callbacks:
with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
- res = await delay_cancellation(
- callback(inviter_userid, medium, address, room_id)
- )
- # Normalize return values to `Codes` or `"NOT_SPAM"`.
+ checker_args = inspect.signature(callback)
+ # Also ensure backwards compatibility with spam checker callbacks
+ # that don't expect the room_config argument.
+ if len(checker_args.parameters) == 2:
+ callback_with_requester_id = cast(
+ Callable[
+ [str, JsonDict],
+ Awaitable[USER_MAY_CREATE_ROOM_CALLBACK_RETURN_VALUE],
+ ],
+ callback,
+ )
+ # We make a copy of the config to ensure the spam checker cannot modify it.
+ res = await delay_cancellation(
+ callback_with_requester_id(userid, deepcopy(room_config))
+ )
+ else:
+ callback_without_requester_id = cast(
+ Callable[
+ [str], Awaitable[USER_MAY_CREATE_ROOM_CALLBACK_RETURN_VALUE]
+ ],
+ callback,
+ )
+ res = await delay_cancellation(
+ callback_without_requester_id(userid)
+ )
if res is True or res is self.NOT_SPAM:
continue
elif res is False:
@@ -611,36 +631,38 @@ class SpamCheckerModuleApiCallbacks:
return res
else:
logger.warning(
- "Module returned invalid value, rejecting 3pid invite as spam"
+ "Module returned invalid value, rejecting room creation as spam"
)
return synapse.api.errors.Codes.FORBIDDEN, {}
return self.NOT_SPAM
- async def user_may_create_room(
- self, userid: str
+ async def user_may_send_state_event(
+ self,
+ user_id: str,
+ room_id: str,
+ event_type: str,
+ state_key: str,
+ content: JsonDict,
) -> Union[Tuple[Codes, dict], Literal["NOT_SPAM"]]:
- """Checks if a given user may create a room
-
+ """Checks if a given user may create a room with a given visibility
Args:
- userid: The ID of the user attempting to create a room
+ user_id: The ID of the user attempting to create a room
+ room_id: The ID of the room that the event will be sent to
+ event_type: The type of the state event
+ state_key: The state key of the state event
+ content: The content of the state event
"""
- for callback in self._user_may_create_room_callbacks:
+ for callback in self._user_may_send_state_event_callbacks:
with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
- res = await delay_cancellation(callback(userid))
- if res is True or res is self.NOT_SPAM:
+ # We make a copy of the content to ensure that the spam checker cannot modify it.
+ res = await delay_cancellation(
+ callback(user_id, room_id, event_type, state_key, deepcopy(content))
+ )
+ if res is self.NOT_SPAM:
continue
- elif res is False:
- return synapse.api.errors.Codes.FORBIDDEN, {}
elif isinstance(res, synapse.api.errors.Codes):
return res, {}
- elif (
- isinstance(res, tuple)
- and len(res) == 2
- and isinstance(res[0], synapse.api.errors.Codes)
- and isinstance(res[1], dict)
- ):
- return res
else:
logger.warning(
"Module returned invalid value, rejecting room creation as spam"
@@ -716,7 +738,9 @@ class SpamCheckerModuleApiCallbacks:
return self.NOT_SPAM
- async def check_username_for_spam(self, user_profile: UserProfile) -> bool:
+ async def check_username_for_spam(
+ self, user_profile: UserProfile, requester_id: str
+ ) -> bool:
"""Checks if a user ID or display name are considered "spammy" by this server.
If the server considers a username spammy, then it will not be included in
@@ -727,15 +751,33 @@ class SpamCheckerModuleApiCallbacks:
* user_id
* display_name
* avatar_url
+ requester_id: The user ID of the user making the user directory search request.
Returns:
True if the user is spammy.
"""
for callback in self._check_username_for_spam_callbacks:
with Measure(self.clock, f"{callback.__module__}.{callback.__qualname__}"):
+ checker_args = inspect.signature(callback)
# Make a copy of the user profile object to ensure the spam checker cannot
# modify it.
- res = await delay_cancellation(callback(user_profile.copy()))
+ # Also ensure backwards compatibility with spam checker callbacks
+ # that don't expect the requester_id argument.
+ if len(checker_args.parameters) == 2:
+ callback_with_requester_id = cast(
+ Callable[[UserProfile, str], Awaitable[bool]], callback
+ )
+ res = await delay_cancellation(
+ callback_with_requester_id(user_profile.copy(), requester_id)
+ )
+ else:
+ callback_without_requester_id = cast(
+ Callable[[UserProfile], Awaitable[bool]], callback
+ )
+ res = await delay_cancellation(
+ callback_without_requester_id(user_profile.copy())
+ )
+
if res:
return True
@@ -755,8 +797,8 @@ class SpamCheckerModuleApiCallbacks:
username: The request user name, if any
request_info: List of tuples of user agent and IP that
were used during the registration process.
- auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml",
- "cas". If any. Note this does not include users registered
+ auth_provider_id: The SSO IdP the user used, e.g "oidc".
+ If any. Note this does not include users registered
via a password provider.
Returns:
@@ -844,8 +886,8 @@ class SpamCheckerModuleApiCallbacks:
user_id: The request user ID
request_info: List of tuples of user agent and IP that
were used during the registration process.
- auth_provider_id: The SSO IdP the user used, e.g "oidc", "saml",
- "cas". If any. Note this does not include users registered
+ auth_provider_id: The SSO IdP the user used, e.g "oidc".
+ If any. Note this does not include users registered
via a password provider.
Returns:
diff --git a/synapse/module_api/callbacks/third_party_event_rules_callbacks.py b/synapse/module_api/callbacks/third_party_event_rules_callbacks.py
index 9f7a04372d..13508cc582 100644
--- a/synapse/module_api/callbacks/third_party_event_rules_callbacks.py
+++ b/synapse/module_api/callbacks/third_party_event_rules_callbacks.py
@@ -40,9 +40,6 @@ CHECK_EVENT_ALLOWED_CALLBACK = Callable[
[EventBase, StateMap[EventBase]], Awaitable[Tuple[bool, Optional[dict]]]
]
ON_CREATE_ROOM_CALLBACK = Callable[[Requester, dict, bool], Awaitable]
-CHECK_THREEPID_CAN_BE_INVITED_CALLBACK = Callable[
- [str, str, StateMap[EventBase]], Awaitable[bool]
-]
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK = Callable[
[str, StateMap[EventBase], str], Awaitable[bool]
]
@@ -51,9 +48,6 @@ CHECK_CAN_SHUTDOWN_ROOM_CALLBACK = Callable[[Optional[str], str], Awaitable[bool
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_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK = Callable[[str, str, str], Awaitable]
-ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK = Callable[[str, str, str], Awaitable]
def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
@@ -73,7 +67,6 @@ def load_legacy_third_party_event_rules(hs: "HomeServer") -> None:
third_party_event_rules_methods = {
"check_event_allowed",
"on_create_room",
- "check_threepid_can_be_invited",
"check_visibility_can_be_modified",
}
@@ -161,9 +154,6 @@ class ThirdPartyEventRulesModuleApiCallbacks:
self._check_event_allowed_callbacks: List[CHECK_EVENT_ALLOWED_CALLBACK] = []
self._on_create_room_callbacks: List[ON_CREATE_ROOM_CALLBACK] = []
- self._check_threepid_can_be_invited_callbacks: List[
- CHECK_THREEPID_CAN_BE_INVITED_CALLBACK
- ] = []
self._check_visibility_can_be_modified_callbacks: List[
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
] = []
@@ -178,21 +168,11 @@ class ThirdPartyEventRulesModuleApiCallbacks:
self._on_user_deactivation_status_changed_callbacks: List[
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
] = []
- self._on_threepid_bind_callbacks: List[ON_THREEPID_BIND_CALLBACK] = []
- self._on_add_user_third_party_identifier_callbacks: List[
- ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
- ] = []
- self._on_remove_user_third_party_identifier_callbacks: List[
- ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
- ] = []
def register_third_party_rules_callbacks(
self,
check_event_allowed: Optional[CHECK_EVENT_ALLOWED_CALLBACK] = None,
on_create_room: Optional[ON_CREATE_ROOM_CALLBACK] = None,
- check_threepid_can_be_invited: Optional[
- CHECK_THREEPID_CAN_BE_INVITED_CALLBACK
- ] = None,
check_visibility_can_be_modified: Optional[
CHECK_VISIBILITY_CAN_BE_MODIFIED_CALLBACK
] = None,
@@ -202,14 +182,7 @@ class ThirdPartyEventRulesModuleApiCallbacks:
on_profile_update: Optional[ON_PROFILE_UPDATE_CALLBACK] = None,
on_user_deactivation_status_changed: Optional[
ON_USER_DEACTIVATION_STATUS_CHANGED_CALLBACK
- ] = None,
- on_threepid_bind: Optional[ON_THREEPID_BIND_CALLBACK] = None,
- on_add_user_third_party_identifier: Optional[
- ON_ADD_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
- ] = None,
- on_remove_user_third_party_identifier: Optional[
- ON_REMOVE_USER_THIRD_PARTY_IDENTIFIER_CALLBACK
- ] = None,
+ ] = None
) -> None:
"""Register callbacks from modules for each hook."""
if check_event_allowed is not None:
@@ -218,11 +191,6 @@ class ThirdPartyEventRulesModuleApiCallbacks:
if on_create_room is not None:
self._on_create_room_callbacks.append(on_create_room)
- if check_threepid_can_be_invited is not None:
- self._check_threepid_can_be_invited_callbacks.append(
- check_threepid_can_be_invited,
- )
-
if check_visibility_can_be_modified is not None:
self._check_visibility_can_be_modified_callbacks.append(
check_visibility_can_be_modified,
@@ -236,6 +204,7 @@ class ThirdPartyEventRulesModuleApiCallbacks:
if check_can_deactivate_user is not None:
self._check_can_deactivate_user_callbacks.append(check_can_deactivate_user)
+
if on_profile_update is not None:
self._on_profile_update_callbacks.append(on_profile_update)
@@ -244,19 +213,6 @@ class ThirdPartyEventRulesModuleApiCallbacks:
on_user_deactivation_status_changed,
)
- if on_threepid_bind is not None:
- self._on_threepid_bind_callbacks.append(on_threepid_bind)
-
- if on_add_user_third_party_identifier is not None:
- self._on_add_user_third_party_identifier_callbacks.append(
- on_add_user_third_party_identifier
- )
-
- if on_remove_user_third_party_identifier is not None:
- self._on_remove_user_third_party_identifier_callbacks.append(
- on_remove_user_third_party_identifier
- )
-
async def check_event_allowed(
self,
event: EventBase,
@@ -349,39 +305,6 @@ class ThirdPartyEventRulesModuleApiCallbacks:
raise e
- async def check_threepid_can_be_invited(
- self, medium: str, address: str, room_id: str
- ) -> bool:
- """Check if a provided 3PID can be invited in the given room.
-
- Args:
- medium: The 3PID's medium.
- address: The 3PID's address.
- room_id: The room we want to invite the threepid to.
-
- Returns:
- True if the 3PID can be invited, False if not.
- """
- # Bail out early without hitting the store if we don't have any callbacks to run.
- if len(self._check_threepid_can_be_invited_callbacks) == 0:
- return True
-
- state_events = await self._storage_controllers.state.get_current_state(room_id)
-
- for callback in self._check_threepid_can_be_invited_callbacks:
- try:
- threepid_can_be_invited = await delay_cancellation(
- callback(medium, address, state_events)
- )
- if threepid_can_be_invited is False:
- return False
- except CancelledError:
- raise
- except Exception as e:
- logger.warning("Failed to run module API callback %s: %s", callback, e)
-
- return True
-
async def check_visibility_can_be_modified(
self, room_id: str, new_visibility: str
) -> bool:
@@ -533,67 +456,3 @@ class ThirdPartyEventRulesModuleApiCallbacks:
logger.exception(
"Failed to run module API callback %s: %s", callback, e
)
-
- async def on_threepid_bind(self, user_id: str, medium: str, address: str) -> None:
- """Called after a threepid association has been verified and stored.
-
- Note that this callback is called when an association is created on the
- local homeserver, not when it's created on an identity server (and then kept track
- of so that it can be unbound on the same IS later on).
-
- THIS MODULE CALLBACK METHOD HAS BEEN DEPRECATED. Please use the
- `on_add_user_third_party_identifier` callback method instead.
-
- Args:
- user_id: the user being associated with the threepid.
- medium: the threepid's medium.
- address: the threepid's address.
- """
- for callback in self._on_threepid_bind_callbacks:
- try:
- await callback(user_id, medium, address)
- except Exception as e:
- logger.exception(
- "Failed to run module API callback %s: %s", callback, e
- )
-
- async def on_add_user_third_party_identifier(
- self, user_id: str, medium: str, address: str
- ) -> None:
- """Called when an association between a user's Matrix ID and a third-party ID
- (email, phone number) has successfully been registered on the homeserver.
-
- Args:
- user_id: The User ID included in the association.
- medium: The medium of the third-party ID (email, msisdn).
- address: The address of the third-party ID (i.e. an email address).
- """
- for callback in self._on_add_user_third_party_identifier_callbacks:
- try:
- await callback(user_id, medium, address)
- except Exception as e:
- logger.exception(
- "Failed to run module API callback %s: %s", callback, e
- )
-
- async def on_remove_user_third_party_identifier(
- self, user_id: str, medium: str, address: str
- ) -> None:
- """Called when an association between a user's Matrix ID and a third-party ID
- (email, phone number) has been successfully removed on the homeserver.
-
- This is called *after* any known bindings on identity servers for this
- association have been removed.
-
- Args:
- user_id: The User ID included in the removed association.
- medium: The medium of the third-party ID (email, msisdn).
- address: The address of the third-party ID (i.e. an email address).
- """
- for callback in self._on_remove_user_third_party_identifier_callbacks:
- try:
- await callback(user_id, medium, address)
- except Exception as e:
- logger.exception(
- "Failed to run module API callback %s: %s", callback, e
- )
|