Merge branch 'develop' into storage_transactions

paul/schema_breaking_changes
Mark Haines 2014-08-27 11:19:37 +01:00
commit bf05218c4b
17 changed files with 189 additions and 171 deletions

View File

@ -409,10 +409,9 @@ class SynapseCmd(cmd.Cmd):
def do_send(self, line):
"""Sends a message. "send <roomid> <body>" """
args = self._parse(line, ["roomid", "body"])
msg_id = "m%s" % int(time.time())
path = "/rooms/%s/messages/%s/%s" % (urllib.quote(args["roomid"]),
self._usr(),
msg_id)
txn_id = "txn%s" % int(time.time())
path = "/rooms/%s/send/m.room.message/%s" % (urllib.quote(args["roomid"]),
txn_id)
body_json = {
"msgtype": "m.text",
"body": args["body"]

View File

@ -168,6 +168,8 @@ class Auth(object):
"""
try:
user_id = yield self.store.get_user_by_token(token=token)
if not user_id:
raise StoreError()
defer.returnValue(self.hs.parse_userid(user_id))
except StoreError:
raise AuthError(403, "Unrecognised access token.",

View File

@ -31,8 +31,8 @@ class Feedback(object):
"""Represents the types of feedback a user can send in response to a
message."""
DELIVERED = u"d"
READ = u"r"
DELIVERED = u"delivered"
READ = u"read"
LIST = (DELIVERED, READ)

View File

@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
from synapse.api.constants import Membership
from synapse.api.constants import Feedback, Membership
from synapse.api.errors import SynapseError
from . import SynapseEvent
@ -93,17 +93,19 @@ class MessageEvent(SynapseEvent):
class FeedbackEvent(SynapseEvent):
TYPE = "m.room.message.feedback"
valid_keys = SynapseEvent.valid_keys + [
"msg_id", # the message ID being acknowledged
"msg_sender_id", # person who is sending the feedback is 'user_id'
"feedback_type", # the type of feedback (delivery, read, etc)
]
valid_keys = SynapseEvent.valid_keys
def __init__(self, **kwargs):
super(FeedbackEvent, self).__init__(**kwargs)
if not kwargs["content"]["type"] in Feedback.LIST:
raise SynapseError(400, "Bad feedback value.")
def get_content_template(self):
return {}
return {
"type": u"string",
"target_event_id": u"string",
"msg_sender_id": u"string"
}
class InviteJoinEvent(SynapseEvent):

View File

@ -17,7 +17,7 @@ from .register import RegistrationHandler
from .room import (
MessageHandler, RoomCreationHandler, RoomMemberHandler, RoomListHandler
)
from .events import EventStreamHandler
from .events import EventStreamHandler, EventHandler
from .federation import FederationHandler
from .login import LoginHandler
from .profile import ProfileHandler
@ -39,6 +39,7 @@ class Handlers(object):
self.room_creation_handler = RoomCreationHandler(hs)
self.room_member_handler = RoomMemberHandler(hs)
self.event_stream_handler = EventStreamHandler(hs)
self.event_handler = EventHandler(hs)
self.federation_handler = FederationHandler(hs)
self.profile_handler = ProfileHandler(hs)
self.presence_handler = PresenceHandler(hs)

View File

@ -144,3 +144,29 @@ class EventStreamHandler(BaseHandler):
self._stop_timer_per_user[auth_user] = (
self.clock.call_later(5, _later)
)
class EventHandler(BaseHandler):
@defer.inlineCallbacks
def get_event(self, user, event_id):
"""Retrieve a single specified event.
Args:
user (synapse.types.UserID): The user requesting the event
event_id (str): The event ID to obtain.
Returns:
dict: An event, or None if there is no event matching this ID.
Raises:
SynapseError if there was a problem retrieving this event, or
AuthError if the user does not have the rights to inspect this
event.
"""
event = yield self.store.get_event(event_id)
if not event:
defer.returnValue(None)
return
yield self.auth.check(event, raises=True)
defer.returnValue(event)

View File

@ -249,6 +249,10 @@ class MessageHandler(BaseRoomHandler):
# FIXME (erikj): We need to not generate this token,
now_token = "%s_%s" % (now_rooms_token, now_presence_token)
limit = pagin_config.limit
if not limit:
limit = 10
for event in room_list:
d = {
"room_id": event.room_id,
@ -265,7 +269,7 @@ class MessageHandler(BaseRoomHandler):
try:
messages, token = yield self.store.get_recent_events_for_room(
event.room_id,
limit=10,
limit=limit,
end_token=now_rooms_token,
)

View File

@ -47,5 +47,22 @@ class EventStreamRestServlet(RestServlet):
return (200, {})
# TODO: Unit test gets, with and without auth, with different kinds of events.
class EventRestServlet(RestServlet):
PATTERN = client_path_pattern("/events/(?P<event_id>[^/]*)$")
@defer.inlineCallbacks
def on_GET(self, request, event_id):
auth_user = yield self.auth.get_user_by_req(request)
handler = self.handlers.event_handler
event = yield handler.get_event(auth_user, event_id)
if event:
defer.returnValue((200, event.get_dict()))
else:
defer.returnValue((404, "Event not found."))
def register_servlets(hs, http_server):
EventStreamRestServlet(hs).register(http_server)
EventRestServlet(hs).register(http_server)

View File

@ -19,6 +19,7 @@ from synapse.api.streams import PaginationConfig
from base import RestServlet, client_path_pattern
# TODO: Needs unit testing
class InitialSyncRestServlet(RestServlet):
PATTERN = client_path_pattern("/initialSync$")

View File

@ -18,10 +18,8 @@ from twisted.internet import defer
from base import RestServlet, client_path_pattern
from synapse.api.errors import SynapseError, Codes
from synapse.api.events.room import (
MessageEvent, RoomMemberEvent, FeedbackEvent
)
from synapse.api.constants import Feedback
from synapse.api.events.room import RoomMemberEvent
from synapse.api.constants import Membership
from synapse.api.streams import PaginationConfig
import json
@ -96,6 +94,7 @@ class RoomCreateRestServlet(RestServlet):
return (200, {})
# TODO: Needs unit testing for generic events
class RoomStateEventRestServlet(RestServlet):
def register(self, http_server):
# /room/$roomid/state/$eventtype
@ -168,143 +167,107 @@ class RoomStateEventRestServlet(RestServlet):
defer.returnValue((200, ""))
class JoinRoomAliasServlet(RestServlet):
PATTERN = client_path_pattern("/join/(?P<room_alias>[^/]+)$")
# TODO: Needs unit testing for generic events + feedback
class RoomSendEventRestServlet(RestServlet):
def register(self, http_server):
# /rooms/$roomid/send/$event_type[/$txn_id]
PATTERN = ("/rooms/(?P<room_id>[^/]*)/send/(?P<event_type>[^/]*)")
register_txn_path(self, PATTERN, http_server, with_get=True)
@defer.inlineCallbacks
def on_PUT(self, request, room_alias):
def on_POST(self, request, room_id, event_type):
user = yield self.auth.get_user_by_req(request)
if not user:
defer.returnValue((403, "Unrecognized user"))
logger.debug("room_alias: %s", room_alias)
room_alias = self.hs.parse_roomalias(urllib.unquote(room_alias))
handler = self.handlers.room_member_handler
ret_dict = yield handler.join_room_alias(user, room_alias)
defer.returnValue((200, ret_dict))
class MessageRestServlet(RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages/"
+ "(?P<sender_id>[^/]*)/(?P<msg_id>[^/]*)$")
def get_event_type(self):
return MessageEvent.TYPE
@defer.inlineCallbacks
def on_GET(self, request, room_id, sender_id, msg_id):
user = yield self.auth.get_user_by_req(request)
msg_handler = self.handlers.message_handler
msg = yield msg_handler.get_message(room_id=urllib.unquote(room_id),
sender_id=urllib.unquote(sender_id),
msg_id=msg_id,
user_id=user.to_string(),
)
if not msg:
raise SynapseError(404, "Message not found.",
errcode=Codes.NOT_FOUND)
defer.returnValue((200, json.loads(msg.content)))
@defer.inlineCallbacks
def on_PUT(self, request, room_id, sender_id, msg_id):
user = yield self.auth.get_user_by_req(request)
if user.to_string() != urllib.unquote(sender_id):
raise SynapseError(403, "Must send messages as yourself.",
errcode=Codes.FORBIDDEN)
content = _parse_json(request)
event = self.event_factory.create_event(
etype=self.get_event_type(),
etype=event_type,
room_id=urllib.unquote(room_id),
user_id=user.to_string(),
msg_id=msg_id,
content=content
)
)
msg_handler = self.handlers.message_handler
yield msg_handler.send_message(event)
defer.returnValue((200, ""))
defer.returnValue((200, {"event_id": event.event_id}))
class FeedbackRestServlet(RestServlet):
PATTERN = client_path_pattern(
"/rooms/(?P<room_id>[^/]*)/messages/" +
"(?P<msg_sender_id>[^/]*)/(?P<msg_id>[^/]*)/feedback/" +
"(?P<sender_id>[^/]*)/(?P<feedback_type>[^/]*)$"
)
def get_event_type(self):
return FeedbackEvent.TYPE
def on_GET(self, request, room_id, event_type, txn_id):
return (200, "Not implemented")
@defer.inlineCallbacks
def on_GET(self, request, room_id, msg_sender_id, msg_id, fb_sender_id,
feedback_type):
yield (self.auth.get_user_by_req(request))
def on_PUT(self, request, room_id, event_type, txn_id):
try:
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
except KeyError:
pass
# TODO (erikj): Implement this?
raise NotImplementedError("Getting feedback is not supported")
response = yield self.on_POST(request, room_id, event_type)
# if feedback_type not in Feedback.LIST:
# raise SynapseError(400, "Bad feedback type.",
# errcode=Codes.BAD_JSON)
#
# msg_handler = self.handlers.message_handler
# feedback = yield msg_handler.get_feedback(
# room_id=urllib.unquote(room_id),
# msg_sender_id=msg_sender_id,
# msg_id=msg_id,
# user_id=user.to_string(),
# fb_sender_id=fb_sender_id,
# fb_type=feedback_type
# )
#
# if not feedback:
# raise SynapseError(404, "Feedback not found.",
# errcode=Codes.NOT_FOUND)
#
# defer.returnValue((200, json.loads(feedback.content)))
self.txns.store_client_transaction(request, txn_id, response)
defer.returnValue(response)
# TODO: Needs unit testing for room ID + alias joins
class JoinRoomAliasServlet(RestServlet):
def register(self, http_server):
# /join/$room_identifier[/$txn_id]
PATTERN = ("/join/(?P<room_identifier>[^/]*)")
register_txn_path(self, PATTERN, http_server)
@defer.inlineCallbacks
def on_PUT(self, request, room_id, sender_id, msg_id, fb_sender_id,
feedback_type):
user = yield (self.auth.get_user_by_req(request))
def on_POST(self, request, room_identifier):
user = yield self.auth.get_user_by_req(request)
if user.to_string() != fb_sender_id:
raise SynapseError(403, "Must send feedback as yourself.",
errcode=Codes.FORBIDDEN)
# the identifier could be a room alias or a room id. Try one then the
# other if it fails to parse, without swallowing other valid
# SynapseErrors.
if feedback_type not in Feedback.LIST:
raise SynapseError(400, "Bad feedback type.",
errcode=Codes.BAD_JSON)
content = _parse_json(request)
event = self.event_factory.create_event(
etype=self.get_event_type(),
room_id=urllib.unquote(room_id),
msg_sender_id=sender_id,
msg_id=msg_id,
user_id=user.to_string(), # user sending the feedback
feedback_type=feedback_type,
content=content
identifier = None
is_room_alias = False
try:
identifier = self.hs.parse_roomalias(
urllib.unquote(room_identifier)
)
is_room_alias = True
except SynapseError:
identifier = self.hs.parse_roomid(
urllib.unquote(room_identifier)
)
msg_handler = self.handlers.message_handler
yield msg_handler.send_feedback(event)
# TODO: Support for specifying the home server to join with?
defer.returnValue((200, ""))
if is_room_alias:
handler = self.handlers.room_member_handler
ret_dict = yield handler.join_room_alias(user, identifier)
defer.returnValue((200, ret_dict))
else: # room id
event = self.event_factory.create_event(
etype=RoomMemberEvent.TYPE,
content={"membership": Membership.JOIN},
room_id=urllib.unquote(identifier.to_string()),
user_id=user.to_string(),
state_key=user.to_string()
)
handler = self.handlers.room_member_handler
yield handler.change_membership(event)
defer.returnValue((200, ""))
@defer.inlineCallbacks
def on_PUT(self, request, room_identifier, txn_id):
try:
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
except KeyError:
pass
response = yield self.on_POST(request, room_identifier)
self.txns.store_client_transaction(request, txn_id, response)
defer.returnValue(response)
# TODO: Needs unit testing
class RoomMemberListRestServlet(RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members$")
@ -332,6 +295,7 @@ class RoomMemberListRestServlet(RestServlet):
defer.returnValue((200, members))
# TODO: Needs unit testing
class RoomMessageListRestServlet(RestServlet):
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages$")
@ -366,6 +330,7 @@ class RoomTriggerBackfill(RestServlet):
defer.returnValue((200, res))
# TODO: Needs unit testing
class RoomMembershipRestServlet(RestServlet):
def register(self, http_server):
@ -402,7 +367,7 @@ class RoomMembershipRestServlet(RestServlet):
def on_PUT(self, request, room_id, membership_action, txn_id):
try:
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
except:
except KeyError:
pass
response = yield self.on_POST(request, room_id, membership_action)
@ -422,7 +387,7 @@ def _parse_json(request):
raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
def register_txn_path(servlet, regex_string, http_server):
def register_txn_path(servlet, regex_string, http_server, with_get=False):
"""Registers a transaction-based path.
This registers two paths:
@ -433,6 +398,7 @@ def register_txn_path(servlet, regex_string, http_server):
regex_string (str): The regex string to register. Must NOT have a
trailing $ as this string will be appended to.
http_server : The http_server to register paths with.
with_get: True to also register respective GET paths for the PUTs.
"""
http_server.register_path(
"POST",
@ -444,15 +410,20 @@ def register_txn_path(servlet, regex_string, http_server):
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"),
servlet.on_PUT
)
if with_get:
http_server.register_path(
"GET",
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"),
servlet.on_GET
)
def register_servlets(hs, http_server):
RoomStateEventRestServlet(hs).register(http_server)
MessageRestServlet(hs).register(http_server)
FeedbackRestServlet(hs).register(http_server)
RoomCreateRestServlet(hs).register(http_server)
RoomMemberListRestServlet(hs).register(http_server)
RoomMessageListRestServlet(hs).register(http_server)
JoinRoomAliasServlet(hs).register(http_server)
RoomTriggerBackfill(hs).register(http_server)
RoomMembershipRestServlet(hs).register(http_server)
RoomSendEventRestServlet(hs).register(http_server)

View File

@ -27,7 +27,7 @@ from synapse.handlers import Handlers
from synapse.rest import RestServletFactory
from synapse.state import StateHandler
from synapse.storage import DataStore
from synapse.types import UserID, RoomAlias
from synapse.types import UserID, RoomAlias, RoomID
from synapse.util import Clock
from synapse.util.distributor import Distributor
from synapse.util.lockutils import LockManager
@ -115,6 +115,9 @@ class BaseHomeServer(object):
setattr(BaseHomeServer, "get_%s" % (depname), _get)
# TODO: Why are these parse_ methods so high up along with other globals?
# Surely these should be in a util package or in the api package?
# Other utility methods
def parse_userid(self, s):
"""Parse the string given by 's' as a User ID and return a UserID
@ -126,6 +129,11 @@ class BaseHomeServer(object):
object."""
return RoomAlias.from_string(s, hs=self)
def parse_roomid(self, s):
"""Parse the string given by 's' as a Room ID and return a RoomID
object."""
return RoomID.from_string(s, hs=self)
# Build magic accessors for every dependency
for depname in BaseHomeServer.DEPENDENCIES:
BaseHomeServer._make_dependency_method(depname)

View File

@ -88,7 +88,6 @@ class DataStore(RoomMemberStore, RoomStore,
[
"event_id",
"type",
"sender",
"room_id",
"content",
"unrecognized_keys"

View File

@ -23,9 +23,9 @@ class FeedbackStore(SQLBaseStore):
def _store_feedback_txn(self, txn, event):
self._simple_insert_txn(txn, "feedback", {
"event_id": event.event_id,
"feedback_type": event.feedback_type,
"feedback_type": event.content["type"],
"room_id": event.room_id,
"target_event_id": event.target_event,
"target_event_id": event.content["target_event_id"],
"sender": event.user_id,
})

View File

@ -182,7 +182,7 @@ class EventStreamPermissionsTestCase(RestTestCase):
room_id = "!rid1:test"
yield self.create_room_as(room_id, self.other_user,
tok=self.other_token)
yield self.send(room_id, self.other_user, tok=self.other_token)
yield self.send(room_id, tok=self.other_token)
# invited to room (expect no content for room)
yield self.invite(room_id, src=self.other_user, targ=self.user_id,

View File

@ -83,8 +83,8 @@ class RoomPermissionsTestCase(RestTestCase):
is_public=True)
# send a message in one of the rooms
self.created_rmid_msg_path = ("/rooms/%s/messages/%s/midaaa1" %
(self.created_rmid, self.rmcreator_id))
self.created_rmid_msg_path = ("/rooms/%s/send/m.room.message/a1" %
(self.created_rmid))
(code, response) = yield self.mock_resource.trigger(
"PUT",
self.created_rmid_msg_path,
@ -138,14 +138,14 @@ class RoomPermissionsTestCase(RestTestCase):
@defer.inlineCallbacks
def test_send_message(self):
msg_content = '{"msgtype":"m.text","body":"hello"}'
send_msg_path = ("/rooms/%s/messages/%s/mid1" %
(self.created_rmid, self.user_id))
send_msg_path = ("/rooms/%s/send/m.room.message/mid1" %
(self.created_rmid))
# send message in uncreated room, expect 403
(code, response) = yield self.mock_resource.trigger(
"PUT",
"/rooms/%s/messages/%s/mid1" %
(self.uncreated_rmid, self.user_id), msg_content)
"/rooms/%s/send/m.room.message/mid2" %
(self.uncreated_rmid), msg_content)
self.assertEquals(403, code, msg=str(response))
# send message in created room not joined (no state), expect 403
@ -875,9 +875,8 @@ class RoomMessagesTestCase(RestTestCase):
@defer.inlineCallbacks
def test_invalid_puts(self):
path = "/rooms/%s/messages/%s/mid1" % (
urllib.quote(self.room_id), self.user_id
)
path = "/rooms/%s/send/m.room.message/mid1" % (
urllib.quote(self.room_id))
# missing keys or invalid json
(code, response) = yield self.mock_resource.trigger("PUT",
path, '{}')
@ -905,9 +904,8 @@ class RoomMessagesTestCase(RestTestCase):
@defer.inlineCallbacks
def test_rooms_messages_sent(self):
path = "/rooms/%s/messages/%s/mid1" % (
urllib.quote(self.room_id), self.user_id
)
path = "/rooms/%s/send/m.room.message/mid1" % (
urllib.quote(self.room_id))
content = '{"body":"test","msgtype":{"type":"a"}}'
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
@ -923,9 +921,8 @@ class RoomMessagesTestCase(RestTestCase):
# self.assert_dict(json.loads(content), response)
# m.text message type
path = "/rooms/%s/messages/%s/mid2" % (
urllib.quote(self.room_id), self.user_id
)
path = "/rooms/%s/send/m.room.message/mid2" % (
urllib.quote(self.room_id))
content = '{"body":"test2","msgtype":"m.text"}'
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(200, code, msg=str(response))
@ -933,11 +930,3 @@ class RoomMessagesTestCase(RestTestCase):
# (code, response) = yield self.mock_resource.trigger("GET", path, None)
# self.assertEquals(200, code, msg=str(response))
# self.assert_dict(json.loads(content), response)
# trying to send message in different user path
path = "/rooms/%s/messages/%s/mid2" % (
urllib.quote(self.room_id), "invalid" + self.user_id
)
content = '{"body":"test2","msgtype":"m.text"}'
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
self.assertEquals(403, code, msg=str(response))

View File

@ -99,14 +99,14 @@ class RestTestCase(unittest.TestCase):
defer.returnValue(response)
@defer.inlineCallbacks
def send(self, room_id, sender_id, body=None, msg_id=None, tok=None,
def send(self, room_id, body=None, txn_id=None, tok=None,
expect_code=200):
if msg_id is None:
msg_id = "m%s" % (str(time.time()))
if txn_id is None:
txn_id = "m%s" % (str(time.time()))
if body is None:
body = "body_text_here"
path = "/rooms/%s/messages/%s/%s" % (room_id, sender_id, msg_id)
path = "/rooms/%s/send/m.room.message/%s" % (room_id, txn_id)
content = '{"msgtype":"m.text","body":"%s"}' % body
if tok:
path = path + "?access_token=%s" % tok

View File

@ -162,12 +162,12 @@ angular.module('matrixService', [])
return doRequest("GET", path, undefined, {});
},
sendMessage: function(room_id, msg_id, content) {
sendMessage: function(room_id, txn_id, content) {
// The REST path spec
var path = "/rooms/$room_id/messages/$from/$msg_id";
var path = "/rooms/$room_id/send/m.room.message/$txn_id";
if (!msg_id) {
msg_id = "m" + new Date().getTime();
if (!txn_id) {
txn_id = "m" + new Date().getTime();
}
// Like the cmd client, escape room ids
@ -175,8 +175,7 @@ angular.module('matrixService', [])
// Customize it
path = path.replace("$room_id", room_id);
path = path.replace("$from", config.user_id);
path = path.replace("$msg_id", msg_id);
path = path.replace("$txn_id", txn_id);
return doRequest("PUT", path, undefined, content);
},