Add admin api for sending server_notices (#5121)

pull/5142/head
Richard van der Hoff 2019-05-02 11:59:16 +01:00 committed by GitHub
parent c193b39134
commit 12f9d51e82
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 196 additions and 42 deletions

1
changelog.d/5121.feature Normal file
View File

@ -0,0 +1 @@
Implement an admin API for sending server notices. Many thanks to @krombel who provided a foundation for this work.

View File

@ -0,0 +1,48 @@
# Server Notices
The API to send notices is as follows:
```
POST /_synapse/admin/v1/send_server_notice
```
or:
```
PUT /_synapse/admin/v1/send_server_notice/{txnId}
```
You will need to authenticate with an access token for an admin user.
When using the `PUT` form, retransmissions with the same transaction ID will be
ignored in the same way as with `PUT
/_matrix/client/r0/rooms/{roomId}/send/{eventType}/{txnId}`.
The request body should look something like the following:
```json
{
"user_id": "@target_user:server_name",
"content": {
"msgtype": "m.text",
"body": "This is my message"
}
}
```
You can optionally include the following additional parameters:
* `type`: the type of event. Defaults to `m.room.message`.
* `state_key`: Setting this will result in a state event being sent.
Once the notice has been sent, the APU will return the following response:
```json
{
"event_id": "<event_id>"
}
```
Note that server notices must be enabled in `homeserver.yaml` before this API
can be used. See [server_notices.md](../server_notices.md) for more information.

View File

@ -1,5 +1,4 @@
Server Notices
==============
# Server Notices
'Server Notices' are a new feature introduced in Synapse 0.30. They provide a
channel whereby server administrators can send messages to users on the server.
@ -11,8 +10,7 @@ they may also find a use for features such as "Message of the day".
This is a feature specific to Synapse, but it uses standard Matrix
communication mechanisms, so should work with any Matrix client.
User experience
---------------
## User experience
When the user is first sent a server notice, they will get an invitation to a
room (typically called 'Server Notices', though this is configurable in
@ -29,8 +27,7 @@ levels.
Having joined the room, the user can leave the room if they want. Subsequent
server notices will then cause a new room to be created.
Synapse configuration
---------------------
## Synapse configuration
Server notices come from a specific user id on the server. Server
administrators are free to choose the user id - something like `server` is
@ -58,17 +55,7 @@ room which will be created.
`system_mxid_display_name` and `system_mxid_avatar_url` can be used to set the
displayname and avatar of the Server Notices user.
Sending notices
---------------
## Sending notices
As of the current version of synapse, there is no convenient interface for
sending notices (other than the automated ones sent as part of consent
tracking).
In the meantime, it is possible to test this feature using the manhole. Having
gone into the manhole as described in [manhole.md](manhole.md), a notice can be
sent with something like:
```
>>> hs.get_server_notices_manager().send_notice('@user:server.com', {'msgtype':'m.text', 'body':'foo'})
```
To send server notices to users you can use the
[admin_api](admin_api/server_notices.md).

View File

@ -117,4 +117,6 @@ class ClientRestResource(JsonResource):
account_validity.register_servlets(hs, client_resource)
# moving to /_synapse/admin
synapse.rest.admin.register_servlets(hs, client_resource)
synapse.rest.admin.register_servlets_for_client_rest_resource(
hs, client_resource
)

View File

@ -37,6 +37,7 @@ from synapse.http.servlet import (
parse_string,
)
from synapse.rest.admin._base import assert_requester_is_admin, assert_user_is_admin
from synapse.rest.admin.server_notice_servlet import SendServerNoticeServlet
from synapse.types import UserID, create_requester
from synapse.util.versionstring import get_version_string
@ -813,16 +814,26 @@ class AccountValidityRenewServlet(RestServlet):
}
defer.returnValue((200, res))
########################################################################################
#
# please don't add more servlets here: this file is already long and unwieldy. Put
# them in separate files within the 'admin' package.
#
########################################################################################
class AdminRestResource(JsonResource):
"""The REST resource which gets mounted at /_synapse/admin"""
def __init__(self, hs):
JsonResource.__init__(self, hs, canonical_json=False)
register_servlets(hs, self)
register_servlets_for_client_rest_resource(hs, self)
SendServerNoticeServlet(hs).register(self)
def register_servlets(hs, http_server):
def register_servlets_for_client_rest_resource(hs, http_server):
"""Register only the servlets which need to be exposed on /_matrix/client/xxx"""
WhoisRestServlet(hs).register(http_server)
PurgeMediaCacheRestServlet(hs).register(http_server)
PurgeHistoryStatusRestServlet(hs).register(http_server)
@ -839,3 +850,5 @@ def register_servlets(hs, http_server):
VersionServlet(hs).register(http_server)
DeleteGroupAdminRestServlet(hs).register(http_server)
AccountValidityRenewServlet(hs).register(http_server)
# don't add more things here: new servlets should only be exposed on
# /_synapse/admin so should not go here. Instead register them in AdminRestResource.

View File

@ -0,0 +1,100 @@
# -*- coding: utf-8 -*-
# Copyright 2019 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 re
from twisted.internet import defer
from synapse.api.constants import EventTypes
from synapse.api.errors import SynapseError
from synapse.http.servlet import (
RestServlet,
assert_params_in_dict,
parse_json_object_from_request,
)
from synapse.rest.admin import assert_requester_is_admin
from synapse.rest.client.transactions import HttpTransactionCache
from synapse.types import UserID
class SendServerNoticeServlet(RestServlet):
"""Servlet which will send a server notice to a given user
POST /_synapse/admin/v1/send_server_notice
{
"user_id": "@target_user:server_name",
"content": {
"msgtype": "m.text",
"body": "This is my message"
}
}
returns:
{
"event_id": "$1895723857jgskldgujpious"
}
"""
def __init__(self, hs):
"""
Args:
hs (synapse.server.HomeServer): server
"""
self.hs = hs
self.auth = hs.get_auth()
self.txns = HttpTransactionCache(hs)
self.snm = hs.get_server_notices_manager()
def register(self, json_resource):
PATTERN = "^/_synapse/admin/v1/send_server_notice"
json_resource.register_paths(
"POST",
(re.compile(PATTERN + "$"), ),
self.on_POST,
)
json_resource.register_paths(
"PUT",
(re.compile(PATTERN + "/(?P<txn_id>[^/]*)$",), ),
self.on_PUT,
)
@defer.inlineCallbacks
def on_POST(self, request, txn_id=None):
yield assert_requester_is_admin(self.auth, request)
body = parse_json_object_from_request(request)
assert_params_in_dict(body, ("user_id", "content"))
event_type = body.get("type", EventTypes.Message)
state_key = body.get("state_key")
if not self.snm.is_enabled():
raise SynapseError(400, "Server notices are not enabled on this server")
user_id = body["user_id"]
UserID.from_string(user_id)
if not self.hs.is_mine_id(user_id):
raise SynapseError(400, "Server notices can only be sent to local users")
event = yield self.snm.send_notice(
user_id=body["user_id"],
type=event_type,
state_key=state_key,
event_content=body["content"],
)
defer.returnValue((200, {"event_id": event.event_id}))
def on_PUT(self, request, txn_id):
return self.txns.fetch_or_execute_request(
request, self.on_POST, request, txn_id,
)

View File

@ -30,7 +30,7 @@ class UserDirectoryTestCase(unittest.HomeserverTestCase):
servlets = [
login.register_servlets,
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
]
@ -328,7 +328,7 @@ class TestUserDirSearchDisabled(unittest.HomeserverTestCase):
user_directory.register_servlets,
room.register_servlets,
login.register_servlets,
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
]
def make_homeserver(self, reactor, clock):

View File

@ -34,7 +34,7 @@ class EmailPusherTests(HomeserverTestCase):
skip = "No Jinja installed" if not load_jinja2_templates else None
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
login.register_servlets,
]

View File

@ -33,7 +33,7 @@ class HTTPPusherTests(HomeserverTestCase):
skip = "No Jinja installed" if not load_jinja2_templates else None
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
login.register_servlets,
]

View File

@ -30,7 +30,7 @@ from tests import unittest
class VersionTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
]
@ -63,7 +63,7 @@ class VersionTestCase(unittest.HomeserverTestCase):
class UserRegisterTestCase(unittest.HomeserverTestCase):
servlets = [synapse.rest.admin.register_servlets]
servlets = [synapse.rest.admin.register_servlets_for_client_rest_resource]
def make_homeserver(self, reactor, clock):
@ -359,7 +359,7 @@ class UserRegisterTestCase(unittest.HomeserverTestCase):
class ShutdownRoomTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
events.register_servlets,
room.register_servlets,
@ -496,7 +496,7 @@ class ShutdownRoomTestCase(unittest.HomeserverTestCase):
class DeleteGroupTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
groups.register_servlets,
]

View File

@ -32,7 +32,7 @@ except Exception:
class ConsentResourceTestCase(unittest.HomeserverTestCase):
skip = "No Jinja installed" if not load_jinja2_templates else None
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
login.register_servlets,
]

View File

@ -24,7 +24,7 @@ from tests import unittest
class IdentityTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
login.register_servlets,
]

View File

@ -29,7 +29,7 @@ class EventStreamPermissionsTestCase(unittest.HomeserverTestCase):
servlets = [
events.register_servlets,
room.register_servlets,
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
]

View File

@ -11,7 +11,7 @@ LOGIN_URL = b"/_matrix/client/r0/login"
class LoginRestServletTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
]

View File

@ -804,7 +804,7 @@ class RoomMessageListTestCase(RoomBase):
class RoomSearchTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
login.register_servlets,
]

View File

@ -27,7 +27,7 @@ class FallbackAuthTests(unittest.HomeserverTestCase):
servlets = [
auth.register_servlets,
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
register.register_servlets,
]
hijack_auth = False

View File

@ -23,7 +23,7 @@ from tests import unittest
class CapabilitiesTestCase(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
capabilities.register_servlets,
login.register_servlets,
]

View File

@ -199,7 +199,7 @@ class AccountValidityTestCase(unittest.HomeserverTestCase):
servlets = [
register.register_servlets,
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
sync.register_servlets,
account_validity.register_servlets,
@ -308,7 +308,7 @@ class AccountValidityRenewalByEmailTestCase(unittest.HomeserverTestCase):
skip = "No Jinja installed" if not load_jinja2_templates else None
servlets = [
register.register_servlets,
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
sync.register_servlets,
account_validity.register_servlets,

View File

@ -73,7 +73,7 @@ class FilterTestCase(unittest.HomeserverTestCase):
class SyncTypingTests(unittest.HomeserverTestCase):
servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
room.register_servlets,
login.register_servlets,
sync.register_servlets,

View File

@ -23,7 +23,7 @@ class ConsentNoticesTests(unittest.HomeserverTestCase):
servlets = [
sync.register_servlets,
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
room.register_servlets,
]

View File

@ -206,7 +206,10 @@ class ClientIpStoreTestCase(unittest.HomeserverTestCase):
class ClientIpAuthTestCase(unittest.HomeserverTestCase):
servlets = [synapse.rest.admin.register_servlets, login.register_servlets]
servlets = [
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
]
def make_homeserver(self, reactor, clock):
hs = self.setup_test_homeserver()