MSC: https://github.com/matrix-org/matrix-spec-proposals/pull/4069tags/v1.98.0rc1
@@ -0,0 +1 @@ | |||||
Support MSC4069: Inhibit profile propagation. |
@@ -419,3 +419,7 @@ class ExperimentalConfig(Config): | |||||
self.msc4028_push_encrypted_events = experimental.get( | self.msc4028_push_encrypted_events = experimental.get( | ||||
"msc4028_push_encrypted_events", False | "msc4028_push_encrypted_events", False | ||||
) | ) | ||||
self.msc4069_profile_inhibit_propagation = experimental.get( | |||||
"msc4069_profile_inhibit_propagation", False | |||||
) |
@@ -129,6 +129,7 @@ class ProfileHandler: | |||||
new_displayname: str, | new_displayname: str, | ||||
by_admin: bool = False, | by_admin: bool = False, | ||||
deactivation: bool = False, | deactivation: bool = False, | ||||
propagate: bool = True, | |||||
) -> None: | ) -> None: | ||||
"""Set the displayname of a user | """Set the displayname of a user | ||||
@@ -138,6 +139,7 @@ class ProfileHandler: | |||||
new_displayname: The displayname to give this user. | new_displayname: The displayname to give this user. | ||||
by_admin: Whether this change was made by an administrator. | by_admin: Whether this change was made by an administrator. | ||||
deactivation: Whether this change was made while deactivating the user. | deactivation: Whether this change was made while deactivating the user. | ||||
propagate: Whether this change also applies to the user's membership events. | |||||
""" | """ | ||||
if not self.hs.is_mine(target_user): | if not self.hs.is_mine(target_user): | ||||
raise SynapseError(400, "User is not hosted on this homeserver") | raise SynapseError(400, "User is not hosted on this homeserver") | ||||
@@ -188,7 +190,8 @@ class ProfileHandler: | |||||
target_user.to_string(), profile, by_admin, deactivation | target_user.to_string(), profile, by_admin, deactivation | ||||
) | ) | ||||
await self._update_join_states(requester, target_user) | |||||
if propagate: | |||||
await self._update_join_states(requester, target_user) | |||||
async def get_avatar_url(self, target_user: UserID) -> Optional[str]: | async def get_avatar_url(self, target_user: UserID) -> Optional[str]: | ||||
if self.hs.is_mine(target_user): | if self.hs.is_mine(target_user): | ||||
@@ -221,6 +224,7 @@ class ProfileHandler: | |||||
new_avatar_url: str, | new_avatar_url: str, | ||||
by_admin: bool = False, | by_admin: bool = False, | ||||
deactivation: bool = False, | deactivation: bool = False, | ||||
propagate: bool = True, | |||||
) -> None: | ) -> None: | ||||
"""Set a new avatar URL for a user. | """Set a new avatar URL for a user. | ||||
@@ -230,6 +234,7 @@ class ProfileHandler: | |||||
new_avatar_url: The avatar URL to give this user. | new_avatar_url: The avatar URL to give this user. | ||||
by_admin: Whether this change was made by an administrator. | by_admin: Whether this change was made by an administrator. | ||||
deactivation: Whether this change was made while deactivating the user. | deactivation: Whether this change was made while deactivating the user. | ||||
propagate: Whether this change also applies to the user's membership events. | |||||
""" | """ | ||||
if not self.hs.is_mine(target_user): | if not self.hs.is_mine(target_user): | ||||
raise SynapseError(400, "User is not hosted on this homeserver") | raise SynapseError(400, "User is not hosted on this homeserver") | ||||
@@ -278,7 +283,8 @@ class ProfileHandler: | |||||
target_user.to_string(), profile, by_admin, deactivation | target_user.to_string(), profile, by_admin, deactivation | ||||
) | ) | ||||
await self._update_join_states(requester, target_user) | |||||
if propagate: | |||||
await self._update_join_states(requester, target_user) | |||||
@cached() | @cached() | ||||
async def check_avatar_size_and_mime_type(self, mxc: str) -> bool: | async def check_avatar_size_and_mime_type(self, mxc: str) -> bool: | ||||
@@ -13,12 +13,17 @@ | |||||
# limitations under the License. | # limitations under the License. | ||||
""" This module contains REST servlets to do with profile: /profile/<paths> """ | """ This module contains REST servlets to do with profile: /profile/<paths> """ | ||||
from http import HTTPStatus | from http import HTTPStatus | ||||
from typing import TYPE_CHECKING, Tuple | from typing import TYPE_CHECKING, Tuple | ||||
from synapse.api.errors import Codes, SynapseError | from synapse.api.errors import Codes, 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_boolean, | |||||
parse_json_object_from_request, | |||||
) | |||||
from synapse.http.site import SynapseRequest | from synapse.http.site import SynapseRequest | ||||
from synapse.rest.client._base import client_patterns | from synapse.rest.client._base import client_patterns | ||||
from synapse.types import JsonDict, UserID | from synapse.types import JsonDict, UserID | ||||
@@ -27,6 +32,20 @@ if TYPE_CHECKING: | |||||
from synapse.server import HomeServer | from synapse.server import HomeServer | ||||
def _read_propagate(hs: "HomeServer", request: SynapseRequest) -> bool: | |||||
# This will always be set by the time Twisted calls us. | |||||
assert request.args is not None | |||||
propagate = True | |||||
if hs.config.experimental.msc4069_profile_inhibit_propagation: | |||||
do_propagate = request.args.get(b"org.matrix.msc4069.propagate") | |||||
if do_propagate is not None: | |||||
propagate = parse_boolean( | |||||
request, "org.matrix.msc4069.propagate", default=False | |||||
) | |||||
return propagate | |||||
class ProfileDisplaynameRestServlet(RestServlet): | class ProfileDisplaynameRestServlet(RestServlet): | ||||
PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/displayname", v1=True) | PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/displayname", v1=True) | ||||
CATEGORY = "Event sending requests" | CATEGORY = "Event sending requests" | ||||
@@ -80,7 +99,11 @@ class ProfileDisplaynameRestServlet(RestServlet): | |||||
errcode=Codes.BAD_JSON, | errcode=Codes.BAD_JSON, | ||||
) | ) | ||||
await self.profile_handler.set_displayname(user, requester, new_name, is_admin) | |||||
propagate = _read_propagate(self.hs, request) | |||||
await self.profile_handler.set_displayname( | |||||
user, requester, new_name, is_admin, propagate=propagate | |||||
) | |||||
return 200, {} | return 200, {} | ||||
@@ -135,8 +158,10 @@ class ProfileAvatarURLRestServlet(RestServlet): | |||||
400, "Missing key 'avatar_url'", errcode=Codes.MISSING_PARAM | 400, "Missing key 'avatar_url'", errcode=Codes.MISSING_PARAM | ||||
) | ) | ||||
propagate = _read_propagate(self.hs, request) | |||||
await self.profile_handler.set_avatar_url( | await self.profile_handler.set_avatar_url( | ||||
user, requester, new_avatar_url, is_admin | |||||
user, requester, new_avatar_url, is_admin, propagate=propagate | |||||
) | ) | ||||
return 200, {} | return 200, {} | ||||
@@ -129,6 +129,8 @@ class VersionsRestServlet(RestServlet): | |||||
"org.matrix.msc3981": self.config.experimental.msc3981_recurse_relations, | "org.matrix.msc3981": self.config.experimental.msc3981_recurse_relations, | ||||
# Adds support for deleting account data. | # Adds support for deleting account data. | ||||
"org.matrix.msc3391": self.config.experimental.msc3391_enabled, | "org.matrix.msc3391": self.config.experimental.msc3391_enabled, | ||||
# Allows clients to inhibit profile update propagation. | |||||
"org.matrix.msc4069": self.config.experimental.msc4069_profile_inhibit_propagation, | |||||
}, | }, | ||||
}, | }, | ||||
) | ) | ||||
@@ -312,6 +312,166 @@ class ProfileTestCase(unittest.HomeserverTestCase): | |||||
) | ) | ||||
self.assertEqual(channel.code, 200, channel.result) | self.assertEqual(channel.code, 200, channel.result) | ||||
@unittest.override_config( | |||||
{"experimental_features": {"msc4069_profile_inhibit_propagation": True}} | |||||
) | |||||
def test_msc4069_inhibit_propagation(self) -> None: | |||||
"""Tests to ensure profile update propagation can be inhibited.""" | |||||
for prop in ["avatar_url", "displayname"]: | |||||
room_id = self.helper.create_room_as(tok=self.owner_tok) | |||||
channel = self.make_request( | |||||
"PUT", | |||||
f"/rooms/{room_id}/state/m.room.member/{self.owner}", | |||||
content={"membership": "join", prop: "mxc://my.server/existing"}, | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
channel = self.make_request( | |||||
"PUT", | |||||
f"/profile/{self.owner}/{prop}?org.matrix.msc4069.propagate=false", | |||||
content={prop: "http://my.server/pic.gif"}, | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
res = ( | |||||
self._get_avatar_url() | |||||
if prop == "avatar_url" | |||||
else self._get_displayname() | |||||
) | |||||
self.assertEqual(res, "http://my.server/pic.gif") | |||||
channel = self.make_request( | |||||
"GET", | |||||
f"/rooms/{room_id}/state/m.room.member/{self.owner}", | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
self.assertEqual(channel.json_body.get(prop), "mxc://my.server/existing") | |||||
def test_msc4069_inhibit_propagation_disabled(self) -> None: | |||||
"""Tests to ensure profile update propagation inhibit flags are ignored when the | |||||
experimental flag is not enabled. | |||||
""" | |||||
for prop in ["avatar_url", "displayname"]: | |||||
room_id = self.helper.create_room_as(tok=self.owner_tok) | |||||
channel = self.make_request( | |||||
"PUT", | |||||
f"/rooms/{room_id}/state/m.room.member/{self.owner}", | |||||
content={"membership": "join", prop: "mxc://my.server/existing"}, | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
channel = self.make_request( | |||||
"PUT", | |||||
f"/profile/{self.owner}/{prop}?org.matrix.msc4069.propagate=false", | |||||
content={prop: "http://my.server/pic.gif"}, | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
res = ( | |||||
self._get_avatar_url() | |||||
if prop == "avatar_url" | |||||
else self._get_displayname() | |||||
) | |||||
self.assertEqual(res, "http://my.server/pic.gif") | |||||
channel = self.make_request( | |||||
"GET", | |||||
f"/rooms/{room_id}/state/m.room.member/{self.owner}", | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
# The ?propagate=false should be ignored by the server because the config flag | |||||
# isn't enabled. | |||||
self.assertEqual(channel.json_body.get(prop), "http://my.server/pic.gif") | |||||
def test_msc4069_inhibit_propagation_default(self) -> None: | |||||
"""Tests to ensure profile update propagation happens by default.""" | |||||
for prop in ["avatar_url", "displayname"]: | |||||
room_id = self.helper.create_room_as(tok=self.owner_tok) | |||||
channel = self.make_request( | |||||
"PUT", | |||||
f"/rooms/{room_id}/state/m.room.member/{self.owner}", | |||||
content={"membership": "join", prop: "mxc://my.server/existing"}, | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
channel = self.make_request( | |||||
"PUT", | |||||
f"/profile/{self.owner}/{prop}", | |||||
content={prop: "http://my.server/pic.gif"}, | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
res = ( | |||||
self._get_avatar_url() | |||||
if prop == "avatar_url" | |||||
else self._get_displayname() | |||||
) | |||||
self.assertEqual(res, "http://my.server/pic.gif") | |||||
channel = self.make_request( | |||||
"GET", | |||||
f"/rooms/{room_id}/state/m.room.member/{self.owner}", | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
# The ?propagate=false should be ignored by the server because the config flag | |||||
# isn't enabled. | |||||
self.assertEqual(channel.json_body.get(prop), "http://my.server/pic.gif") | |||||
@unittest.override_config( | |||||
{"experimental_features": {"msc4069_profile_inhibit_propagation": True}} | |||||
) | |||||
def test_msc4069_inhibit_propagation_like_default(self) -> None: | |||||
"""Tests to ensure clients can request explicit profile propagation.""" | |||||
for prop in ["avatar_url", "displayname"]: | |||||
room_id = self.helper.create_room_as(tok=self.owner_tok) | |||||
channel = self.make_request( | |||||
"PUT", | |||||
f"/rooms/{room_id}/state/m.room.member/{self.owner}", | |||||
content={"membership": "join", prop: "mxc://my.server/existing"}, | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
channel = self.make_request( | |||||
"PUT", | |||||
f"/profile/{self.owner}/{prop}?org.matrix.msc4069.propagate=true", | |||||
content={prop: "http://my.server/pic.gif"}, | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
res = ( | |||||
self._get_avatar_url() | |||||
if prop == "avatar_url" | |||||
else self._get_displayname() | |||||
) | |||||
self.assertEqual(res, "http://my.server/pic.gif") | |||||
channel = self.make_request( | |||||
"GET", | |||||
f"/rooms/{room_id}/state/m.room.member/{self.owner}", | |||||
access_token=self.owner_tok, | |||||
) | |||||
self.assertEqual(channel.code, 200, channel.result) | |||||
# The client requested ?propagate=true, so it should have happened. | |||||
self.assertEqual(channel.json_body.get(prop), "http://my.server/pic.gif") | |||||
def _setup_local_files(self, names_and_props: Dict[str, Dict[str, Any]]) -> None: | def _setup_local_files(self, names_and_props: Dict[str, Dict[str, Any]]) -> None: | ||||
"""Stores metadata about files in the database. | """Stores metadata about files in the database. | ||||