Server Notices use a special room which the user can't dismiss. They are created on demand when some other bit of the code calls send_notice. (This doesn't actually do much yet becuse we don't call send_notice anywhere)tags/v0.30.0-rc1
@@ -38,6 +38,7 @@ from .spam_checker import SpamCheckerConfig | |||
from .groups import GroupsConfig | |||
from .user_directory import UserDirectoryConfig | |||
from .consent_config import ConsentConfig | |||
from .server_notices_config import ServerNoticesConfig | |||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, | |||
@@ -47,7 +48,9 @@ class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig, | |||
JWTConfig, PasswordConfig, EmailConfig, | |||
WorkerConfig, PasswordAuthProviderConfig, PushConfig, | |||
SpamCheckerConfig, GroupsConfig, UserDirectoryConfig, | |||
ConsentConfig): | |||
ConsentConfig, | |||
ServerNoticesConfig, | |||
): | |||
pass | |||
@@ -0,0 +1,85 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright 2018 New Vector Ltd | |||
# | |||
# Licensed under the Apache License, Version 2.0 (the "License"); | |||
# you may not use this file except in compliance with the License. | |||
# You may obtain a copy of the License at | |||
# | |||
# http://www.apache.org/licenses/LICENSE-2.0 | |||
# | |||
# Unless required by applicable law or agreed to in writing, software | |||
# distributed under the License is distributed on an "AS IS" BASIS, | |||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
# See the License for the specific language governing permissions and | |||
# limitations under the License. | |||
from ._base import Config | |||
from synapse.types import UserID | |||
DEFAULT_CONFIG = """\ | |||
# Server Notices room configuration | |||
# | |||
# Uncomment this section to enable a room which can be used to send notices | |||
# from the server to users. It is a special room which cannot be left; notices | |||
# come from a special "notices" user id. | |||
# | |||
# If you uncomment this section, you *must* define the system_mxid_localpart | |||
# setting, which defines the id of the user which will be used to send the | |||
# notices. | |||
# | |||
# It's also possible to override the room name, or the display name of the | |||
# "notices" user. | |||
# | |||
# server_notices: | |||
# system_mxid_localpart: notices | |||
# system_mxid_display_name: "Server Notices" | |||
# room_name: "Server Notices" | |||
""" | |||
class ServerNoticesConfig(Config): | |||
def __init__(self): | |||
super(ServerNoticesConfig, self).__init__() | |||
"""The MXID to use for server notices. | |||
None if server notices are not enabled. | |||
type: str|None | |||
""" | |||
self.server_notices_mxid = None | |||
"""The display name to use for the server notices user. | |||
None if server notices are not enabled. | |||
type: str|None | |||
""" | |||
self.server_notices_mxid_display_name = None | |||
"""The name to use for the server notices room. | |||
None if server notices are not enabled. | |||
(TODO: i18n) | |||
type: str|None | |||
""" | |||
self.server_notices_room_name = None | |||
def read_config(self, config): | |||
c = config.get("server_notices") | |||
if c is None: | |||
return | |||
mxid_localpart = c['system_mxid_localpart'] | |||
self.server_notices_mxid = UserID( | |||
mxid_localpart, self.server_name, | |||
).to_string() | |||
self.server_notices_mxid_display_name = c.get( | |||
'system_mxid_display_name', 'Server Notices', | |||
) | |||
self.server_notices_room_name = c.get('room_name', "Server Notices") | |||
def default_config(self, **kwargs): | |||
return DEFAULT_CONFIG |
@@ -34,6 +34,11 @@ logger = logging.getLogger(__name__) | |||
class RegistrationHandler(BaseHandler): | |||
def __init__(self, hs): | |||
""" | |||
Args: | |||
hs (synapse.server.HomeServer): | |||
""" | |||
super(RegistrationHandler, self).__init__(hs) | |||
self.auth = hs.get_auth() | |||
@@ -49,6 +54,7 @@ class RegistrationHandler(BaseHandler): | |||
self._generate_user_id_linearizer = Linearizer( | |||
name="_generate_user_id_linearizer", | |||
) | |||
self._server_notices_mxid = hs.config.server_notices_mxid | |||
@defer.inlineCallbacks | |||
def check_username(self, localpart, guest_access_token=None, | |||
@@ -338,6 +344,14 @@ class RegistrationHandler(BaseHandler): | |||
yield identity_handler.bind_threepid(c, user_id) | |||
def check_user_id_not_appservice_exclusive(self, user_id, allowed_appservice=None): | |||
# don't allow people to register the server notices mxid | |||
if self._server_notices_mxid is not None: | |||
if user_id == self._server_notices_mxid: | |||
raise SynapseError( | |||
400, "This user ID is reserved.", | |||
errcode=Codes.EXCLUSIVE | |||
) | |||
# valid user IDs must not clash with any user ID namespaces claimed by | |||
# application services. | |||
services = self.store.get_app_services() | |||
@@ -68,7 +68,8 @@ class RoomCreationHandler(BaseHandler): | |||
self.event_creation_handler = hs.get_event_creation_handler() | |||
@defer.inlineCallbacks | |||
def create_room(self, requester, config, ratelimit=True): | |||
def create_room(self, requester, config, ratelimit=True, | |||
creator_join_profile=None): | |||
""" Creates a new room. | |||
Args: | |||
@@ -76,6 +77,14 @@ class RoomCreationHandler(BaseHandler): | |||
The user who requested the room creation. | |||
config (dict) : A dict of configuration options. | |||
ratelimit (bool): set to False to disable the rate limiter | |||
creator_join_profile (dict|None): | |||
Set to override the displayname and avatar for the creating | |||
user in this room. If unset, displayname and avatar will be | |||
derived from the user's profile. If set, should contain the | |||
values to go in the body of the 'join' event (typically | |||
`avatar_url` and/or `displayname`. | |||
Returns: | |||
Deferred[dict]: | |||
a dict containing the keys `room_id` and, if an alias was | |||
@@ -180,7 +189,8 @@ class RoomCreationHandler(BaseHandler): | |||
initial_state=initial_state, | |||
creation_content=creation_content, | |||
room_alias=room_alias, | |||
power_level_content_override=config.get("power_level_content_override", {}) | |||
power_level_content_override=config.get("power_level_content_override", {}), | |||
creator_join_profile=creator_join_profile, | |||
) | |||
if "name" in config: | |||
@@ -260,6 +270,7 @@ class RoomCreationHandler(BaseHandler): | |||
creation_content, | |||
room_alias, | |||
power_level_content_override, | |||
creator_join_profile, | |||
): | |||
def create(etype, content, **kwargs): | |||
e = { | |||
@@ -303,6 +314,7 @@ class RoomCreationHandler(BaseHandler): | |||
room_id, | |||
"join", | |||
ratelimit=False, | |||
content=creator_join_profile, | |||
) | |||
# We treat the power levels override specially as this needs to be one | |||
@@ -17,11 +17,14 @@ | |||
import abc | |||
import logging | |||
from six.moves import http_client | |||
from signedjson.key import decode_verify_key_bytes | |||
from signedjson.sign import verify_signed_json | |||
from twisted.internet import defer | |||
from unpaddedbase64 import decode_base64 | |||
import synapse.server | |||
import synapse.types | |||
from synapse.api.constants import ( | |||
EventTypes, Membership, | |||
@@ -46,6 +49,11 @@ class RoomMemberHandler(object): | |||
__metaclass__ = abc.ABCMeta | |||
def __init__(self, hs): | |||
""" | |||
Args: | |||
hs (synapse.server.HomeServer): | |||
""" | |||
self.hs = hs | |||
self.store = hs.get_datastore() | |||
self.auth = hs.get_auth() | |||
@@ -63,6 +71,7 @@ class RoomMemberHandler(object): | |||
self.clock = hs.get_clock() | |||
self.spam_checker = hs.get_spam_checker() | |||
self._server_notices_mxid = self.config.server_notices_mxid | |||
@abc.abstractmethod | |||
def _remote_join(self, requester, remote_room_hosts, room_id, user, content): | |||
@@ -289,12 +298,28 @@ class RoomMemberHandler(object): | |||
is_blocked = yield self.store.is_room_blocked(room_id) | |||
if is_blocked: | |||
raise SynapseError(403, "This room has been blocked on this server") | |||
else: | |||
if self._is_server_notice_room(room_id): | |||
# we don't allow people to reject invites to, or leave, the | |||
# server notice room. | |||
raise SynapseError( | |||
http_client.FORBIDDEN, | |||
"You cannot leave this room", | |||
) | |||
if effective_membership_state == "invite": | |||
if effective_membership_state == Membership.INVITE: | |||
block_invite = False | |||
is_requester_admin = yield self.auth.is_server_admin( | |||
requester.user, | |||
) | |||
if (self._server_notices_mxid is not None and | |||
requester.user.to_string() == self._server_notices_mxid): | |||
# allow the server notices mxid to send invites | |||
is_requester_admin = True | |||
else: | |||
is_requester_admin = yield self.auth.is_server_admin( | |||
requester.user, | |||
) | |||
if not is_requester_admin: | |||
if self.config.block_non_admin_invites: | |||
logger.info( | |||
@@ -844,6 +869,13 @@ class RoomMemberHandler(object): | |||
defer.returnValue(False) | |||
@defer.inlineCallbacks | |||
def _is_server_notice_room(self, room_id): | |||
if self._server_notices_mxid is None: | |||
defer.returnValue(False) | |||
user_ids = yield self.store.get_users_in_room(room_id) | |||
defer.returnValue(self._server_notices_mxid in user_ids) | |||
class RoomMemberMasterHandler(RoomMemberHandler): | |||
def __init__(self, hs): | |||
@@ -72,6 +72,7 @@ from synapse.rest.media.v1.media_repository import ( | |||
MediaRepository, | |||
MediaRepositoryResource, | |||
) | |||
from synapse.server_notices.server_notices_manager import ServerNoticesManager | |||
from synapse.state import StateHandler, StateResolutionHandler | |||
from synapse.storage import DataStore | |||
from synapse.streams.events import EventSources | |||
@@ -156,6 +157,7 @@ class HomeServer(object): | |||
'spam_checker', | |||
'room_member_handler', | |||
'federation_registry', | |||
'server_notices_manager', | |||
] | |||
def __init__(self, hostname, **kwargs): | |||
@@ -398,6 +400,9 @@ class HomeServer(object): | |||
def build_federation_registry(self): | |||
return FederationHandlerRegistry() | |||
def build_server_notices_manager(self): | |||
return ServerNoticesManager(self) | |||
def remove_pusher(self, app_id, push_key, user_id): | |||
return self.get_pusherpool().remove_pusher(app_id, push_key, user_id) | |||
@@ -1,4 +1,5 @@ | |||
import synapse.api.auth | |||
import synapse.config.homeserver | |||
import synapse.federation.transaction_queue | |||
import synapse.federation.transport.client | |||
import synapse.handlers | |||
@@ -8,11 +9,16 @@ import synapse.handlers.device | |||
import synapse.handlers.e2e_keys | |||
import synapse.handlers.set_password | |||
import synapse.rest.media.v1.media_repository | |||
import synapse.server_notices.server_notices_manager | |||
import synapse.state | |||
import synapse.storage | |||
class HomeServer(object): | |||
@property | |||
def config(self) -> synapse.config.homeserver.HomeServerConfig: | |||
pass | |||
def get_auth(self) -> synapse.api.auth.Auth: | |||
pass | |||
@@ -43,6 +49,9 @@ class HomeServer(object): | |||
def get_room_creation_handler(self) -> synapse.handlers.room.RoomCreationHandler: | |||
pass | |||
def get_event_creation_handler(self) -> synapse.handlers.message.EventCreationHandler: | |||
pass | |||
def get_set_password_handler(self) -> synapse.handlers.set_password.SetPasswordHandler: | |||
pass | |||
@@ -57,3 +66,6 @@ class HomeServer(object): | |||
def get_media_repository(self) -> synapse.rest.media.v1.media_repository.MediaRepository: | |||
pass | |||
def get_server_notices_manager(self) -> synapse.server_notices.server_notices_manager.ServerNoticesManager: | |||
pass |
@@ -0,0 +1,110 @@ | |||
# -*- coding: utf-8 -*- | |||
# Copyright 2018 New Vector Ltd | |||
# | |||
# Licensed under the Apache License, Version 2.0 (the "License"); | |||
# you may not use this file except in compliance with the License. | |||
# You may obtain a copy of the License at | |||
# | |||
# http://www.apache.org/licenses/LICENSE-2.0 | |||
# | |||
# Unless required by applicable law or agreed to in writing, software | |||
# distributed under the License is distributed on an "AS IS" BASIS, | |||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |||
# See the License for the specific language governing permissions and | |||
# limitations under the License. | |||
import logging | |||
from twisted.internet import defer | |||
from synapse.api.constants import EventTypes, Membership, RoomCreationPreset | |||
from synapse.types import create_requester | |||
from synapse.util.caches.descriptors import cachedInlineCallbacks | |||
logger = logging.getLogger(__name__) | |||
class ServerNoticesManager(object): | |||
def __init__(self, hs): | |||
""" | |||
Args: | |||
hs (synapse.server.HomeServer): | |||
""" | |||
self._store = hs.get_datastore() | |||
self._config = hs.config | |||
self._room_creation_handler = hs.get_room_creation_handler() | |||
self._event_creation_handler = hs.get_event_creation_handler() | |||
def is_enabled(self): | |||
return self._config.server_notices_mxid is not None | |||
@defer.inlineCallbacks | |||
def send_notice(self, user_id, event_content): | |||
room_id = yield self.get_notice_room_for_user(user_id) | |||
system_mxid = self._config.server_notices_mxid | |||
requester = create_requester(system_mxid) | |||
logger.info("Sending server notice to %s", user_id) | |||
yield self._event_creation_handler.create_and_send_nonmember_event( | |||
requester, { | |||
"type": EventTypes.Message, | |||
"room_id": room_id, | |||
"sender": system_mxid, | |||
"content": event_content, | |||
}, | |||
ratelimit=False, | |||
) | |||
@cachedInlineCallbacks() | |||
def get_notice_room_for_user(self, user_id): | |||
"""Get the room for notices for a given user | |||
If we have not yet created a notice room for this user, create it | |||
Args: | |||
user_id (str): complete user id for the user we want a room for | |||
Returns: | |||
str: room id of notice room. | |||
""" | |||
if not self.is_enabled(): | |||
raise Exception("Server notices not enabled") | |||
rooms = yield self._store.get_rooms_for_user_where_membership_is( | |||
user_id, [Membership.INVITE, Membership.JOIN], | |||
) | |||
system_mxid = self._config.server_notices_mxid | |||
for room in rooms: | |||
user_ids = yield self._store.get_users_in_room(room.room_id) | |||
if system_mxid in user_ids: | |||
# we found a room which our user shares with the system notice | |||
# user | |||
logger.info("Using room %s", room.room_id) | |||
defer.returnValue(room.room_id) | |||
# apparently no existing notice room: create a new one | |||
logger.info("Creating server notices room for %s", user_id) | |||
requester = create_requester(system_mxid) | |||
info = yield self._room_creation_handler.create_room( | |||
requester, | |||
config={ | |||
"preset": RoomCreationPreset.PRIVATE_CHAT, | |||
"name": self._config.server_notices_room_name, | |||
"power_level_content_override": { | |||
"users_default": -10, | |||
}, | |||
"invite": (user_id,) | |||
}, | |||
ratelimit=False, | |||
creator_join_profile={ | |||
"displayname": self._config.server_notices_mxid_display_name, | |||
}, | |||
) | |||
room_id = info['room_id'] | |||
logger.info("Created server notices room %s for %s", room_id, user_id) | |||
defer.returnValue(room_id) |