2018-08-09 23:16:00 +02:00
|
|
|
# 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
|
2021-03-24 11:48:46 +01:00
|
|
|
from typing import TYPE_CHECKING, List, Tuple
|
2018-08-09 23:16:00 +02:00
|
|
|
|
2018-08-22 18:00:29 +02:00
|
|
|
from synapse.api.constants import (
|
|
|
|
EventTypes,
|
2019-10-24 12:48:46 +02:00
|
|
|
LimitBlockingTypes,
|
2018-08-22 18:00:29 +02:00
|
|
|
ServerNoticeLimitReached,
|
|
|
|
ServerNoticeMsgType,
|
|
|
|
)
|
2018-08-17 16:21:34 +02:00
|
|
|
from synapse.api.errors import AuthError, ResourceLimitError, SynapseError
|
2018-08-16 18:02:04 +02:00
|
|
|
from synapse.server_notices.server_notices_manager import SERVER_NOTICE_ROOM_TAG
|
2018-08-09 23:16:00 +02:00
|
|
|
|
2021-03-24 11:48:46 +01:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from synapse.server import HomeServer
|
|
|
|
|
2018-08-09 23:16:00 +02:00
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
2020-09-04 12:54:56 +02:00
|
|
|
class ResourceLimitsServerNotices:
|
2021-02-16 23:32:34 +01:00
|
|
|
"""Keeps track of whether the server has reached it's resource limit and
|
2018-08-17 16:21:34 +02:00
|
|
|
ensures that the client is kept up to date.
|
2018-08-09 23:16:00 +02:00
|
|
|
"""
|
2019-06-20 11:32:02 +02:00
|
|
|
|
2021-03-24 11:48:46 +01:00
|
|
|
def __init__(self, hs: "HomeServer"):
|
2018-08-09 23:16:00 +02:00
|
|
|
self._server_notices_manager = hs.get_server_notices_manager()
|
|
|
|
self._store = hs.get_datastore()
|
2018-08-17 16:21:34 +02:00
|
|
|
self._auth = hs.get_auth()
|
|
|
|
self._config = hs.config
|
2018-08-09 23:16:00 +02:00
|
|
|
self._resouce_limited = False
|
2021-01-20 11:44:52 +01:00
|
|
|
self._account_data_handler = hs.get_account_data_handler()
|
2018-08-16 12:10:53 +02:00
|
|
|
self._message_handler = hs.get_message_handler()
|
|
|
|
self._state = hs.get_state_handler()
|
2018-08-09 23:16:00 +02:00
|
|
|
|
2018-08-24 15:50:03 +02:00
|
|
|
self._notifier = hs.get_notifier()
|
|
|
|
|
2020-05-22 22:47:07 +02:00
|
|
|
self._enabled = (
|
|
|
|
hs.config.limit_usage_by_mau
|
|
|
|
and self._server_notices_manager.is_enabled()
|
|
|
|
and not hs.config.hs_disabled
|
|
|
|
)
|
|
|
|
|
2020-07-31 22:22:06 +02:00
|
|
|
async def maybe_send_server_notice_to_user(self, user_id: str) -> None:
|
2018-08-17 16:21:34 +02:00
|
|
|
"""Check if we need to send a notice to this user, this will be true in
|
|
|
|
two cases.
|
|
|
|
1. The server has reached its limit does not reflect this
|
|
|
|
2. The room state indicates that the server has reached its limit when
|
|
|
|
actually the server is fine
|
2018-08-09 23:16:00 +02:00
|
|
|
|
|
|
|
Args:
|
2020-07-31 22:22:06 +02:00
|
|
|
user_id: user to check
|
2018-08-09 23:16:00 +02:00
|
|
|
"""
|
2020-05-22 22:47:07 +02:00
|
|
|
if not self._enabled:
|
2018-08-24 12:31:31 +02:00
|
|
|
return
|
|
|
|
|
2020-05-01 16:15:36 +02:00
|
|
|
timestamp = await self._store.user_last_seen_monthly_active(user_id)
|
2018-08-16 18:02:04 +02:00
|
|
|
if timestamp is None:
|
|
|
|
# This user will be blocked from receiving the notice anyway.
|
|
|
|
# In practice, not sure we can ever get here
|
|
|
|
return
|
2018-08-16 16:48:34 +02:00
|
|
|
|
2020-05-01 16:15:36 +02:00
|
|
|
room_id = await self._server_notices_manager.get_or_create_notice_room_for_user(
|
Server notices: Dissociate room creation/lookup from invite (#7199)
Fixes #6815
Before figuring out whether we should alert a user on MAU, we call get_notice_room_for_user to get some info on the existing server notices room for this user. This function, if the room doesn't exist, creates it and invites the user in it. This means that, if we decide later that no server notice is needed, the user gets invited in a room with no message in it. This happens at every restart of the server, since the room ID returned by get_notice_room_for_user is cached.
This PR fixes that by moving the inviting bit to a dedicated function, that's only called when the server actually needs to send a notice to the user. A potential issue with this approach is that the room that's created by get_notice_room_for_user doesn't match how that same function looks for an existing room (i.e. it creates a room that doesn't have an invite or a join for the current user in it, so it could lead to a new room being created each time a user syncs), but I'm not sure this is a problem given it's cached until the server restarts, so that function won't run very often.
It also renames get_notice_room_for_user into get_or_create_notice_room_for_user to make what it does clearer.
2020-04-04 17:27:45 +02:00
|
|
|
user_id
|
|
|
|
)
|
2018-08-16 18:02:04 +02:00
|
|
|
|
2018-08-23 17:20:51 +02:00
|
|
|
if not room_id:
|
2019-10-31 11:23:24 +01:00
|
|
|
logger.warning("Failed to get server notices room")
|
2018-08-23 17:20:51 +02:00
|
|
|
return
|
|
|
|
|
2020-05-01 16:15:36 +02:00
|
|
|
await self._check_and_set_tags(user_id, room_id)
|
2019-10-24 12:48:46 +02:00
|
|
|
|
|
|
|
# Determine current state of room
|
2020-05-01 16:15:36 +02:00
|
|
|
currently_blocked, ref_events = await self._is_room_currently_blocked(room_id)
|
2018-08-16 18:02:04 +02:00
|
|
|
|
2019-10-24 12:48:46 +02:00
|
|
|
limit_msg = None
|
|
|
|
limit_type = None
|
2018-08-16 18:02:04 +02:00
|
|
|
try:
|
2019-10-24 12:48:46 +02:00
|
|
|
# Normally should always pass in user_id to check_auth_blocking
|
|
|
|
# if you have it, but in this case are checking what would happen
|
|
|
|
# to other users if they were to arrive.
|
2020-05-01 16:15:36 +02:00
|
|
|
await self._auth.check_auth_blocking()
|
2019-10-24 12:48:46 +02:00
|
|
|
except ResourceLimitError as e:
|
|
|
|
limit_msg = e.msg
|
|
|
|
limit_type = e.limit_type
|
2018-08-16 18:02:04 +02:00
|
|
|
|
2019-10-24 12:48:46 +02:00
|
|
|
try:
|
|
|
|
if (
|
|
|
|
limit_type == LimitBlockingTypes.MONTHLY_ACTIVE_USER
|
|
|
|
and not self._config.mau_limit_alerting
|
|
|
|
):
|
|
|
|
# We have hit the MAU limit, but MAU alerting is disabled:
|
|
|
|
# reset room if necessary and return
|
|
|
|
if currently_blocked:
|
2020-05-01 16:15:36 +02:00
|
|
|
await self._remove_limit_block_notification(user_id, ref_events)
|
2019-10-24 12:48:46 +02:00
|
|
|
return
|
|
|
|
|
|
|
|
if currently_blocked and not limit_msg:
|
|
|
|
# Room is notifying of a block, when it ought not to be.
|
2020-05-01 16:15:36 +02:00
|
|
|
await self._remove_limit_block_notification(user_id, ref_events)
|
2019-10-24 12:48:46 +02:00
|
|
|
elif not currently_blocked and limit_msg:
|
2018-08-17 16:21:34 +02:00
|
|
|
# Room is not notifying of a block, when it ought to be.
|
2020-05-01 16:15:36 +02:00
|
|
|
await self._apply_limit_block_notification(
|
2020-07-31 22:22:06 +02:00
|
|
|
user_id, limit_msg, limit_type # type: ignore
|
2018-08-17 16:21:34 +02:00
|
|
|
)
|
|
|
|
except SynapseError as e:
|
|
|
|
logger.error("Error sending resource limits server notice: %s", e)
|
2018-08-16 18:02:04 +02:00
|
|
|
|
2020-07-31 22:22:06 +02:00
|
|
|
async def _remove_limit_block_notification(
|
|
|
|
self, user_id: str, ref_events: List[str]
|
|
|
|
) -> None:
|
2019-10-24 12:48:46 +02:00
|
|
|
"""Utility method to remove limit block notifications from the server
|
|
|
|
notices room.
|
|
|
|
|
|
|
|
Args:
|
2020-07-31 22:22:06 +02:00
|
|
|
user_id: user to notify
|
|
|
|
ref_events: The event_ids of pinned events that are unrelated to
|
|
|
|
limit blocking and need to be preserved.
|
2019-10-24 12:48:46 +02:00
|
|
|
"""
|
|
|
|
content = {"pinned": ref_events}
|
2020-05-01 16:15:36 +02:00
|
|
|
await self._server_notices_manager.send_notice(
|
2019-10-24 12:48:46 +02:00
|
|
|
user_id, content, EventTypes.Pinned, ""
|
|
|
|
)
|
|
|
|
|
2020-05-01 16:15:36 +02:00
|
|
|
async def _apply_limit_block_notification(
|
2020-07-31 22:22:06 +02:00
|
|
|
self, user_id: str, event_body: str, event_limit_type: str
|
|
|
|
) -> None:
|
2019-10-24 12:48:46 +02:00
|
|
|
"""Utility method to apply limit block notifications in the server
|
|
|
|
notices room.
|
|
|
|
|
|
|
|
Args:
|
2020-07-31 22:22:06 +02:00
|
|
|
user_id: user to notify
|
|
|
|
event_body: The human readable text that describes the block.
|
|
|
|
event_limit_type: Specifies the type of block e.g. monthly active user
|
|
|
|
limit has been exceeded.
|
2019-10-24 12:48:46 +02:00
|
|
|
"""
|
|
|
|
content = {
|
|
|
|
"body": event_body,
|
|
|
|
"msgtype": ServerNoticeMsgType,
|
|
|
|
"server_notice_type": ServerNoticeLimitReached,
|
|
|
|
"admin_contact": self._config.admin_contact,
|
|
|
|
"limit_type": event_limit_type,
|
|
|
|
}
|
2020-05-01 16:15:36 +02:00
|
|
|
event = await self._server_notices_manager.send_notice(
|
2019-10-24 12:48:46 +02:00
|
|
|
user_id, content, EventTypes.Message
|
|
|
|
)
|
|
|
|
|
|
|
|
content = {"pinned": [event.event_id]}
|
2020-05-01 16:15:36 +02:00
|
|
|
await self._server_notices_manager.send_notice(
|
2019-10-24 12:48:46 +02:00
|
|
|
user_id, content, EventTypes.Pinned, ""
|
|
|
|
)
|
|
|
|
|
2020-07-31 22:22:06 +02:00
|
|
|
async def _check_and_set_tags(self, user_id: str, room_id: str) -> None:
|
2018-08-16 18:02:04 +02:00
|
|
|
"""
|
|
|
|
Since server notices rooms were originally not with tags,
|
|
|
|
important to check that tags have been set correctly
|
|
|
|
Args:
|
|
|
|
user_id(str): the user in question
|
|
|
|
room_id(str): the server notices room for that user
|
|
|
|
"""
|
2020-05-01 21:28:59 +02:00
|
|
|
tags = await self._store.get_tags_for_room(user_id, room_id)
|
2018-08-16 18:02:04 +02:00
|
|
|
need_to_set_tag = True
|
2018-08-24 17:22:37 +02:00
|
|
|
if tags:
|
|
|
|
if SERVER_NOTICE_ROOM_TAG in tags:
|
2018-08-16 18:02:04 +02:00
|
|
|
# tag already present, nothing to do here
|
|
|
|
need_to_set_tag = False
|
|
|
|
if need_to_set_tag:
|
2021-01-20 11:44:52 +01:00
|
|
|
max_id = await self._account_data_handler.add_tag_to_room(
|
2018-08-24 15:44:16 +02:00
|
|
|
user_id, room_id, SERVER_NOTICE_ROOM_TAG, {}
|
2018-08-16 18:02:04 +02:00
|
|
|
)
|
2019-06-20 11:32:02 +02:00
|
|
|
self._notifier.on_new_event("account_data_key", max_id, users=[user_id])
|
2018-08-16 18:02:04 +02:00
|
|
|
|
2020-07-31 22:22:06 +02:00
|
|
|
async def _is_room_currently_blocked(self, room_id: str) -> Tuple[bool, List[str]]:
|
2018-08-16 18:02:04 +02:00
|
|
|
"""
|
|
|
|
Determines if the room is currently blocked
|
|
|
|
|
|
|
|
Args:
|
2020-07-31 22:22:06 +02:00
|
|
|
room_id: The room id of the server notices room
|
2018-08-16 18:02:04 +02:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
bool: Is the room currently blocked
|
2020-07-31 22:22:06 +02:00
|
|
|
list: The list of pinned event IDs that are unrelated to limit blocking
|
2018-08-16 18:02:04 +02:00
|
|
|
This list can be used as a convenience in the case where the block
|
|
|
|
is to be lifted and the remaining pinned event references need to be
|
|
|
|
preserved
|
|
|
|
"""
|
|
|
|
currently_blocked = False
|
|
|
|
pinned_state_event = None
|
|
|
|
try:
|
2020-05-01 21:28:59 +02:00
|
|
|
pinned_state_event = await self._state.get_current_state(
|
2018-08-16 18:02:04 +02:00
|
|
|
room_id, event_type=EventTypes.Pinned
|
|
|
|
)
|
2018-08-16 18:32:22 +02:00
|
|
|
except AuthError:
|
2018-08-16 18:02:04 +02:00
|
|
|
# The user has yet to join the server notices room
|
|
|
|
pass
|
|
|
|
|
2021-07-15 12:02:43 +02:00
|
|
|
referenced_events: List[str] = []
|
2018-08-16 18:02:04 +02:00
|
|
|
if pinned_state_event is not None:
|
2019-06-20 11:32:02 +02:00
|
|
|
referenced_events = list(pinned_state_event.content.get("pinned", []))
|
2018-08-16 18:02:04 +02:00
|
|
|
|
2020-05-01 21:28:59 +02:00
|
|
|
events = await self._store.get_events(referenced_events)
|
2020-06-15 13:03:36 +02:00
|
|
|
for event_id, event in events.items():
|
2018-08-22 18:00:29 +02:00
|
|
|
if event.type != EventTypes.Message:
|
|
|
|
continue
|
|
|
|
if event.content.get("msgtype") == ServerNoticeMsgType:
|
2018-08-16 18:02:04 +02:00
|
|
|
currently_blocked = True
|
|
|
|
# remove event in case we need to disable blocking later on.
|
|
|
|
if event_id in referenced_events:
|
|
|
|
referenced_events.remove(event.event_id)
|
|
|
|
|
2019-08-30 17:28:26 +02:00
|
|
|
return currently_blocked, referenced_events
|