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

Commit

Permalink
Add admin api for sending server_notices (#5121)
Browse files Browse the repository at this point in the history
  • Loading branch information
richvdh committed May 2, 2019
1 parent c193b39 commit 12f9d51
Show file tree
Hide file tree
Showing 21 changed files with 196 additions and 42 deletions.
1 change: 1 addition & 0 deletions changelog.d/5121.feature
@@ -0,0 +1 @@
Implement an admin API for sending server notices. Many thanks to @krombel who provided a foundation for this work.
48 changes: 48 additions & 0 deletions docs/admin_api/server_notices.md
@@ -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.
25 changes: 6 additions & 19 deletions docs/server_notices.md
@@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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).
4 changes: 3 additions & 1 deletion synapse/rest/__init__.py
Expand Up @@ -117,4 +117,6 @@ def register_servlets(client_resource, hs):
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
)
17 changes: 15 additions & 2 deletions synapse/rest/admin/__init__.py
Expand Up @@ -37,6 +37,7 @@
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

Expand Down Expand Up @@ -813,16 +814,26 @@ def on_POST(self, request):
}
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)
Expand All @@ -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.
100 changes: 100 additions & 0 deletions synapse/rest/admin/server_notice_servlet.py
@@ -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,
)
4 changes: 2 additions & 2 deletions tests/handlers/test_user_directory.py
Expand Up @@ -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,
]

Expand Down Expand Up @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion tests/push/test_email.py
Expand Up @@ -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,
]
Expand Down
2 changes: 1 addition & 1 deletion tests/push/test_http.py
Expand Up @@ -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,
]
Expand Down
8 changes: 4 additions & 4 deletions tests/rest/admin/test_admin.py
Expand Up @@ -30,7 +30,7 @@
class VersionTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
]

Expand Down Expand Up @@ -63,7 +63,7 @@ def test_inaccessible_to_non_admins(self):

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):

Expand Down Expand Up @@ -359,7 +359,7 @@ def nonce():

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,
Expand Down Expand Up @@ -496,7 +496,7 @@ def _assert_peek(self, room_id, expect_code):

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,
]
Expand Down
2 changes: 1 addition & 1 deletion tests/rest/client/test_consent.py
Expand Up @@ -32,7 +32,7 @@
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,
]
Expand Down
2 changes: 1 addition & 1 deletion tests/rest/client/test_identity.py
Expand Up @@ -24,7 +24,7 @@
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,
]
Expand Down
2 changes: 1 addition & 1 deletion tests/rest/client/v1/test_events.py
Expand Up @@ -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,
]

Expand Down
2 changes: 1 addition & 1 deletion tests/rest/client/v1/test_login.py
Expand Up @@ -11,7 +11,7 @@
class LoginRestServletTestCase(unittest.HomeserverTestCase):

servlets = [
synapse.rest.admin.register_servlets,
synapse.rest.admin.register_servlets_for_client_rest_resource,
login.register_servlets,
]

Expand Down
2 changes: 1 addition & 1 deletion tests/rest/client/v1/test_rooms.py
Expand Up @@ -804,7 +804,7 @@ def test_stream_token_is_accepted_for_fwd_pagianation(self):

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,
]
Expand Down
2 changes: 1 addition & 1 deletion tests/rest/client/v2_alpha/test_auth.py
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion tests/rest/client/v2_alpha/test_capabilities.py
Expand Up @@ -23,7 +23,7 @@
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,
]
Expand Down

0 comments on commit 12f9d51

Please sign in to comment.