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

Implement MSC4069: Inhibit profile propagation #16636

Merged
merged 10 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/16636.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support MSC4069: Inhibit profile propagation.
4 changes: 4 additions & 0 deletions synapse/config/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -419,3 +419,7 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
self.msc4028_push_encrypted_events = experimental.get(
"msc4028_push_encrypted_events", False
)

self.msc4069_profile_inhibit_propagation = experimental.get(
"msc4069_profile_inhibit_propagation", False
)
10 changes: 8 additions & 2 deletions synapse/handlers/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ async def set_displayname(
new_displayname: str,
by_admin: bool = False,
deactivation: bool = False,
propagate: bool = True,
) -> None:
"""Set the displayname of a user

Expand All @@ -138,6 +139,7 @@ async def set_displayname(
new_displayname: The displayname to give this user.
by_admin: Whether this change was made by an administrator.
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):
raise SynapseError(400, "User is not hosted on this homeserver")
Expand Down Expand Up @@ -188,7 +190,8 @@ async def set_displayname(
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]:
if self.hs.is_mine(target_user):
Expand Down Expand Up @@ -221,6 +224,7 @@ async def set_avatar_url(
new_avatar_url: str,
by_admin: bool = False,
deactivation: bool = False,
propagate: bool = True,
) -> None:
"""Set a new avatar URL for a user.

Expand All @@ -230,6 +234,7 @@ async def set_avatar_url(
new_avatar_url: The avatar URL to give this user.
by_admin: Whether this change was made by an administrator.
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):
raise SynapseError(400, "User is not hosted on this homeserver")
Expand Down Expand Up @@ -278,7 +283,8 @@ async def set_avatar_url(
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()
async def check_avatar_size_and_mime_type(self, mxc: str) -> bool:
Expand Down
31 changes: 28 additions & 3 deletions synapse/rest/client/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@
# limitations under the License.

""" This module contains REST servlets to do with profile: /profile/<paths> """

from http import HTTPStatus
from typing import TYPE_CHECKING, Tuple

from synapse.api.errors import Codes, SynapseError
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.rest.client._base import client_patterns
from synapse.types import JsonDict, UserID
Expand All @@ -27,6 +32,20 @@
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):
PATTERNS = client_patterns("/profile/(?P<user_id>[^/]*)/displayname", v1=True)
CATEGORY = "Event sending requests"
Expand Down Expand Up @@ -80,7 +99,11 @@ async def on_PUT(
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, {}

Expand Down Expand Up @@ -135,8 +158,10 @@ async def on_PUT(
400, "Missing key 'avatar_url'", errcode=Codes.MISSING_PARAM
)

propagate = _read_propagate(self.hs, request)

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, {}
Expand Down
2 changes: 2 additions & 0 deletions synapse/rest/client/versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ def on_GET(self, request: Request) -> Tuple[int, JsonDict]:
"org.matrix.msc3981": self.config.experimental.msc3981_recurse_relations,
# Adds support for deleting account data.
"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,
},
},
)
Expand Down
160 changes: 160 additions & 0 deletions tests/rest/client/test_profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,166 @@ def test_avatar_allowed_mime_type_per_room(self) -> None:
)
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:
"""Stores metadata about files in the database.

Expand Down
Loading