Correct `check_username_for_spam` annotations and docs (#12246)

* Formally type the UserProfile in user searches
* export UserProfile in synapse.module_api
* Update docs

Co-authored-by: Sean Quah <8349537+squahtx@users.noreply.github.com>
This commit is contained in:
David Robertson 2022-03-18 13:51:41 +00:00 committed by GitHub
parent 12d1f82db2
commit 872dbb0181
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 46 additions and 16 deletions

1
changelog.d/12246.doc Normal file
View File

@ -0,0 +1 @@
Correct `check_username_for_spam` annotations and docs.

View File

@ -172,7 +172,7 @@ any of the subsequent implementations of this callback.
_First introduced in Synapse v1.37.0_ _First introduced in Synapse v1.37.0_
```python ```python
async def check_username_for_spam(user_profile: Dict[str, str]) -> bool async def check_username_for_spam(user_profile: synapse.module_api.UserProfile) -> bool
``` ```
Called when computing search results in the user directory. The module must return a Called when computing search results in the user directory. The module must return a
@ -182,9 +182,11 @@ search results; otherwise return `False`.
The profile is represented as a dictionary with the following keys: The profile is represented as a dictionary with the following keys:
* `user_id`: The Matrix ID for this user. * `user_id: str`. The Matrix ID for this user.
* `display_name`: The user's display name. * `display_name: Optional[str]`. The user's display name, or `None` if this user
* `avatar_url`: The `mxc://` URL to the user's avatar. has not set a display name.
* `avatar_url: Optional[str]`. The `mxc://` URL to the user's avatar, or `None`
if this user has not set an avatar.
The module is given a copy of the original dictionary, so modifying it from within the The module is given a copy of the original dictionary, so modifying it from within the
module cannot modify a user's profile when included in user directory search results. module cannot modify a user's profile when included in user directory search results.

View File

@ -21,7 +21,6 @@ from typing import (
Awaitable, Awaitable,
Callable, Callable,
Collection, Collection,
Dict,
List, List,
Optional, Optional,
Tuple, Tuple,
@ -31,7 +30,7 @@ from typing import (
from synapse.rest.media.v1._base import FileInfo from synapse.rest.media.v1._base import FileInfo
from synapse.rest.media.v1.media_storage import ReadableFileWrapper from synapse.rest.media.v1.media_storage import ReadableFileWrapper
from synapse.spam_checker_api import RegistrationBehaviour from synapse.spam_checker_api import RegistrationBehaviour
from synapse.types import RoomAlias from synapse.types import RoomAlias, UserProfile
from synapse.util.async_helpers import maybe_awaitable from synapse.util.async_helpers import maybe_awaitable
if TYPE_CHECKING: if TYPE_CHECKING:
@ -50,7 +49,7 @@ USER_MAY_SEND_3PID_INVITE_CALLBACK = Callable[[str, str, str, str], Awaitable[bo
USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]] USER_MAY_CREATE_ROOM_CALLBACK = Callable[[str], Awaitable[bool]]
USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[[str, RoomAlias], Awaitable[bool]] USER_MAY_CREATE_ROOM_ALIAS_CALLBACK = Callable[[str, RoomAlias], Awaitable[bool]]
USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[[str, str], Awaitable[bool]] USER_MAY_PUBLISH_ROOM_CALLBACK = Callable[[str, str], Awaitable[bool]]
CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[Dict[str, str]], Awaitable[bool]] CHECK_USERNAME_FOR_SPAM_CALLBACK = Callable[[UserProfile], Awaitable[bool]]
LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[ LEGACY_CHECK_REGISTRATION_FOR_SPAM_CALLBACK = Callable[
[ [
Optional[dict], Optional[dict],
@ -383,7 +382,7 @@ class SpamChecker:
return True return True
async def check_username_for_spam(self, user_profile: Dict[str, str]) -> bool: async def check_username_for_spam(self, user_profile: UserProfile) -> bool:
"""Checks if a user ID or display name are considered "spammy" by this server. """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 If the server considers a username spammy, then it will not be included in

View File

@ -19,8 +19,8 @@ import synapse.metrics
from synapse.api.constants import EventTypes, HistoryVisibility, JoinRules, Membership from synapse.api.constants import EventTypes, HistoryVisibility, JoinRules, Membership
from synapse.handlers.state_deltas import MatchChange, StateDeltasHandler from synapse.handlers.state_deltas import MatchChange, StateDeltasHandler
from synapse.metrics.background_process_metrics import run_as_background_process from synapse.metrics.background_process_metrics import run_as_background_process
from synapse.storage.databases.main.user_directory import SearchResult
from synapse.storage.roommember import ProfileInfo from synapse.storage.roommember import ProfileInfo
from synapse.types import JsonDict
from synapse.util.metrics import Measure from synapse.util.metrics import Measure
if TYPE_CHECKING: if TYPE_CHECKING:
@ -78,7 +78,7 @@ class UserDirectoryHandler(StateDeltasHandler):
async def search_users( async def search_users(
self, user_id: str, search_term: str, limit: int self, user_id: str, search_term: str, limit: int
) -> JsonDict: ) -> SearchResult:
"""Searches for users in directory """Searches for users in directory
Returns: Returns:

View File

@ -111,6 +111,7 @@ from synapse.types import (
StateMap, StateMap,
UserID, UserID,
UserInfo, UserInfo,
UserProfile,
create_requester, create_requester,
) )
from synapse.util import Clock from synapse.util import Clock
@ -150,6 +151,7 @@ __all__ = [
"EventBase", "EventBase",
"StateMap", "StateMap",
"ProfileInfo", "ProfileInfo",
"UserProfile",
] ]
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -19,7 +19,7 @@ from synapse.api.errors import SynapseError
from synapse.http.server import HttpServer from synapse.http.server import HttpServer
from synapse.http.servlet import RestServlet, parse_json_object_from_request from synapse.http.servlet import RestServlet, parse_json_object_from_request
from synapse.http.site import SynapseRequest from synapse.http.site import SynapseRequest
from synapse.types import JsonDict from synapse.types import JsonMapping
from ._base import client_patterns from ._base import client_patterns
@ -38,7 +38,7 @@ class UserDirectorySearchRestServlet(RestServlet):
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.user_directory_handler = hs.get_user_directory_handler() self.user_directory_handler = hs.get_user_directory_handler()
async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]: async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonMapping]:
"""Searches for users in directory """Searches for users in directory
Returns: Returns:

View File

@ -26,6 +26,8 @@ from typing import (
cast, cast,
) )
from typing_extensions import TypedDict
from synapse.api.errors import StoreError from synapse.api.errors import StoreError
if TYPE_CHECKING: if TYPE_CHECKING:
@ -40,7 +42,12 @@ from synapse.storage.database import (
from synapse.storage.databases.main.state import StateFilter from synapse.storage.databases.main.state import StateFilter
from synapse.storage.databases.main.state_deltas import StateDeltasStore from synapse.storage.databases.main.state_deltas import StateDeltasStore
from synapse.storage.engines import PostgresEngine, Sqlite3Engine from synapse.storage.engines import PostgresEngine, Sqlite3Engine
from synapse.types import JsonDict, get_domain_from_id, get_localpart_from_id from synapse.types import (
JsonDict,
UserProfile,
get_domain_from_id,
get_localpart_from_id,
)
from synapse.util.caches.descriptors import cached from synapse.util.caches.descriptors import cached
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -591,6 +598,11 @@ class UserDirectoryBackgroundUpdateStore(StateDeltasStore):
) )
class SearchResult(TypedDict):
limited: bool
results: List[UserProfile]
class UserDirectoryStore(UserDirectoryBackgroundUpdateStore): class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
# How many records do we calculate before sending it to # How many records do we calculate before sending it to
# add_users_who_share_private_rooms? # add_users_who_share_private_rooms?
@ -777,7 +789,7 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
async def search_user_dir( async def search_user_dir(
self, user_id: str, search_term: str, limit: int self, user_id: str, search_term: str, limit: int
) -> JsonDict: ) -> SearchResult:
"""Searches for users in directory """Searches for users in directory
Returns: Returns:
@ -910,8 +922,11 @@ class UserDirectoryStore(UserDirectoryBackgroundUpdateStore):
# This should be unreachable. # This should be unreachable.
raise Exception("Unrecognized database engine") raise Exception("Unrecognized database engine")
results = await self.db_pool.execute( results = cast(
"search_user_dir", self.db_pool.cursor_to_dict, sql, *args List[UserProfile],
await self.db_pool.execute(
"search_user_dir", self.db_pool.cursor_to_dict, sql, *args
),
) )
limited = len(results) > limit limited = len(results) > limit

View File

@ -34,6 +34,7 @@ from typing import (
import attr import attr
from frozendict import frozendict from frozendict import frozendict
from signedjson.key import decode_verify_key_bytes from signedjson.key import decode_verify_key_bytes
from typing_extensions import TypedDict
from unpaddedbase64 import decode_base64 from unpaddedbase64 import decode_base64
from zope.interface import Interface from zope.interface import Interface
@ -63,6 +64,10 @@ MutableStateMap = MutableMapping[StateKey, T]
# JSON types. These could be made stronger, but will do for now. # JSON types. These could be made stronger, but will do for now.
# A JSON-serialisable dict. # A JSON-serialisable dict.
JsonDict = Dict[str, Any] JsonDict = Dict[str, Any]
# A JSON-serialisable mapping; roughly speaking an immutable JSONDict.
# Useful when you have a TypedDict which isn't going to be mutated and you don't want
# to cast to JsonDict everywhere.
JsonMapping = Mapping[str, Any]
# A JSON-serialisable object. # A JSON-serialisable object.
JsonSerializable = object JsonSerializable = object
@ -791,3 +796,9 @@ class UserInfo:
is_deactivated: bool is_deactivated: bool
is_guest: bool is_guest: bool
is_shadow_banned: bool is_shadow_banned: bool
class UserProfile(TypedDict):
user_id: str
display_name: Optional[str]
avatar_url: Optional[str]