Merge branch 'develop' of github.com:matrix-org/synapse into stream_refactor

This commit is contained in:
Erik Johnston 2014-08-26 15:45:03 +01:00
commit ff3709e577
7 changed files with 319 additions and 12 deletions

View File

@ -15,6 +15,7 @@
""" This module contains base REST classes for constructing REST servlets. """ """ This module contains base REST classes for constructing REST servlets. """
from synapse.api.urls import CLIENT_PREFIX from synapse.api.urls import CLIENT_PREFIX
from synapse.rest.transactions import HttpTransactionStore
import re import re
@ -59,6 +60,7 @@ class RestServlet(object):
self.handlers = hs.get_handlers() self.handlers = hs.get_handlers()
self.event_factory = hs.get_event_factory() self.event_factory = hs.get_event_factory()
self.auth = hs.get_auth() self.auth = hs.get_auth()
self.txns = HttpTransactionStore()
def register(self, http_server): def register(self, http_server):
""" Register this servlet with the given HTTP server. """ """ Register this servlet with the given HTTP server. """

View File

@ -366,6 +366,51 @@ class RoomTriggerBackfill(RestServlet):
defer.returnValue((200, res)) defer.returnValue((200, res))
class RoomMembershipRestServlet(RestServlet):
def register(self, http_server):
# /rooms/$roomid/[invite|join|leave]
PATTERN = ("/rooms/(?P<room_id>[^/]*)/" +
"(?P<membership_action>join|invite|leave)")
register_txn_path(self, PATTERN, http_server)
@defer.inlineCallbacks
def on_POST(self, request, room_id, membership_action):
user = yield self.auth.get_user_by_req(request)
content = _parse_json(request)
# target user is you unless it is an invite
state_key = user.to_string()
if membership_action == "invite":
if "user_id" not in content:
raise SynapseError(400, "Missing user_id key.")
state_key = content["user_id"]
event = self.event_factory.create_event(
etype=RoomMemberEvent.TYPE,
content={"membership": unicode(membership_action)},
room_id=urllib.unquote(room_id),
user_id=user.to_string(),
state_key=state_key
)
handler = self.handlers.room_member_handler
yield handler.change_membership(event)
defer.returnValue((200, ""))
@defer.inlineCallbacks
def on_PUT(self, request, room_id, membership_action, txn_id):
try:
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
except:
pass
response = yield self.on_POST(request, room_id, membership_action)
self.txns.store_client_transaction(request, txn_id, response)
defer.returnValue(response)
def _parse_json(request): def _parse_json(request):
try: try:
content = json.loads(request.content.read()) content = json.loads(request.content.read())
@ -377,6 +422,30 @@ def _parse_json(request):
raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON) raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
def register_txn_path(servlet, regex_string, http_server):
"""Registers a transaction-based path.
This registers two paths:
PUT regex_string/$txnid
POST regex_string
Args:
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.
"""
http_server.register_path(
"POST",
client_path_pattern(regex_string + "$"),
servlet.on_POST
)
http_server.register_path(
"PUT",
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"),
servlet.on_PUT
)
def register_servlets(hs, http_server): def register_servlets(hs, http_server):
RoomStateEventRestServlet(hs).register(http_server) RoomStateEventRestServlet(hs).register(http_server)
MessageRestServlet(hs).register(http_server) MessageRestServlet(hs).register(http_server)
@ -386,3 +455,4 @@ def register_servlets(hs, http_server):
RoomMessageListRestServlet(hs).register(http_server) RoomMessageListRestServlet(hs).register(http_server)
JoinRoomAliasServlet(hs).register(http_server) JoinRoomAliasServlet(hs).register(http_server)
RoomTriggerBackfill(hs).register(http_server) RoomTriggerBackfill(hs).register(http_server)
RoomMembershipRestServlet(hs).register(http_server)

View File

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
# Copyright 2014 matrix.org
#
# 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.
"""This module contains logic for storing HTTP PUT transactions. This is used
to ensure idempotency when performing PUTs using the REST API."""
import logging
logger = logging.getLogger(__name__)
class HttpTransactionStore(object):
def __init__(self):
# { key : (txn_id, response) }
self.transactions = {}
def get_response(self, key, txn_id):
"""Retrieve a response for this request.
Args:
key (str): A transaction-independent key for this request. Typically
this is a combination of the path (without the transaction id) and
the user's access token.
txn_id (str): The transaction ID for this request
Returns:
A tuple of (HTTP response code, response content) or None.
"""
try:
logger.debug("get_response Key: %s TxnId: %s", key, txn_id)
(last_txn_id, response) = self.transactions[key]
if txn_id == last_txn_id:
logger.info("get_response: Returning a response for %s", key)
return response
except KeyError:
pass
return None
def store_response(self, key, txn_id, response):
"""Stores an HTTP response tuple.
Args:
key (str): A transaction-independent key for this request. Typically
this is a combination of the path (without the transaction id) and
the user's access token.
txn_id (str): The transaction ID for this request.
response (tuple): A tuple of (HTTP response code, response content)
"""
logger.debug("store_response Key: %s TxnId: %s", key, txn_id)
self.transactions[key] = (txn_id, response)
def store_client_transaction(self, request, txn_id, response):
"""Stores the request/response pair of an HTTP transaction.
Args:
request (twisted.web.http.Request): The twisted HTTP request. This
request must have the transaction ID as the last path segment.
response (tuple): A tuple of (response code, response dict)
txn_id (str): The transaction ID for this request.
"""
self.store_response(self._get_key(request), txn_id, response)
def get_client_transaction(self, request, txn_id):
"""Retrieves a stored response if there was one.
Args:
request (twisted.web.http.Request): The twisted HTTP request. This
request must have the transaction ID as the last path segment.
txn_id (str): The transaction ID for this request.
Returns:
The response tuple.
Raises:
KeyError if the transaction was not found.
"""
response = self.get_response(self._get_key(request), txn_id)
if response is None:
raise KeyError("Transaction not found.")
return response
def _get_key(self, request):
token = request.args["access_token"][0]
path_without_txn_id = request.path.rsplit("/", 1)[0]
return path_without_txn_id + "/" + token

View File

@ -20,9 +20,9 @@ limitations under the License.
'use strict'; 'use strict';
angular.module('MatrixWebClientController', ['matrixService']) angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'eventStreamService', .controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService',
function($scope, $location, $rootScope, matrixService, eventStreamService) { function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService) {
// Check current URL to avoid to display the logout button on the login page // Check current URL to avoid to display the logout button on the login page
$scope.location = $location.path(); $scope.location = $location.path();
@ -34,6 +34,7 @@ angular.module('MatrixWebClientController', ['matrixService'])
if (matrixService.isUserLoggedIn()) { if (matrixService.isUserLoggedIn()) {
// eventStreamService.resume(); // eventStreamService.resume();
mPresence.start();
} }
$scope.go = function(url) { $scope.go = function(url) {
@ -42,9 +43,13 @@ angular.module('MatrixWebClientController', ['matrixService'])
// Logs the user out // Logs the user out
$scope.logout = function() { $scope.logout = function() {
// kill the event stream // kill the event stream
eventStreamService.stop(); eventStreamService.stop();
// Do not update presence anymore
mPresence.stop();
// Clean permanent data // Clean permanent data
matrixService.setConfig({}); matrixService.setConfig({});
matrixService.saveConfig(); matrixService.saveConfig();
@ -67,7 +72,6 @@ angular.module('MatrixWebClientController', ['matrixService'])
} }
}; };
}]); }]);

View File

@ -115,7 +115,7 @@ angular.module('matrixService', [])
// Joins a room // Joins a room
join: function(room_id) { join: function(room_id) {
return this.membershipChange(room_id, config.user_id, "join"); return this.membershipChange(room_id, undefined, "join");
}, },
joinAlias: function(room_alias) { joinAlias: function(room_alias) {
@ -134,18 +134,22 @@ angular.module('matrixService', [])
// Leaves a room // Leaves a room
leave: function(room_id) { leave: function(room_id) {
return this.membershipChange(room_id, config.user_id, "leave"); return this.membershipChange(room_id, undefined, "leave");
}, },
membershipChange: function(room_id, user_id, membershipValue) { membershipChange: function(room_id, user_id, membershipValue) {
// The REST path spec // The REST path spec
var path = "/rooms/$room_id/state/m.room.member/$user_id"; var path = "/rooms/$room_id/$membership";
path = path.replace("$room_id", encodeURIComponent(room_id)); path = path.replace("$room_id", encodeURIComponent(room_id));
path = path.replace("$user_id", encodeURIComponent(user_id)); path = path.replace("$membership", encodeURIComponent(membershipValue));
return doRequest("PUT", path, undefined, { var data = {};
membership: membershipValue if (user_id !== undefined) {
}); data = { user_id: user_id };
}
// TODO: Use PUT with transaction IDs
return doRequest("POST", path, undefined, data);
}, },
// Retrieves the room ID corresponding to a room alias // Retrieves the room ID corresponding to a room alias
@ -355,6 +359,23 @@ angular.module('matrixService', [])
} }
}, },
// Enum of presence state
presence: {
offline: "offline",
unavailable: "unavailable",
online: "online",
free_for_chat: "free_for_chat"
},
// Set the logged in user presence state
setUserPresence: function(presence) {
var path = "/presence/$user_id/status";
path = path.replace("$user_id", config.user_id);
return doRequest("PUT", path, undefined, {
state: presence
});
},
/****** Permanent storage of user information ******/ /****** Permanent storage of user information ******/
// Returns the current config // Returns the current config

View File

@ -0,0 +1,113 @@
/*
Copyright 2014 matrix.org
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.
*/
'use strict';
/*
* This service tracks user activity on the page to determine his presence state.
* Any state change will be sent to the Home Server.
*/
angular.module('mPresence', [])
.service('mPresence', ['$timeout', 'matrixService', function ($timeout, matrixService) {
// Time in ms after that a user is considered as offline/away
var OFFLINE_TIME = 5 * 60000; // 5 mins
// The current presence state
var state = undefined;
var self =this;
var timer;
/**
* Start listening the user activity to evaluate his presence state.
* Any state change will be sent to the Home Server.
*/
this.start = function() {
if (undefined === state) {
// The user is online if he moves the mouser or press a key
document.onmousemove = resetTimer;
document.onkeypress = resetTimer;
resetTimer();
}
};
/**
* Stop tracking user activity
*/
this.stop = function() {
if (timer) {
$timeout.cancel(timer);
timer = undefined;
}
state = undefined;
};
/**
* Get the current presence state.
* @returns {matrixService.presence} the presence state
*/
this.getState = function() {
return state;
};
/**
* Set the presence state.
* If the state has changed, the Home Server will be notified.
* @param {matrixService.presence} newState the new presence state
*/
this.setState = function(newState) {
if (newState !== state) {
console.log("mPresence - New state: " + newState);
state = newState;
// Inform the HS on the new user state
matrixService.setUserPresence(state).then(
function() {
},
function(error) {
console.log("mPresence - Failed to send new presence state: " + JSON.stringify(error));
});
}
};
/**
* Callback called when the user made no action on the page for OFFLINE_TIME ms.
* @private
*/
function onOfflineTimerFire() {
self.setState(matrixService.presence.offline);
}
/**
* Callback called when the user made an action on the page
* @private
*/
function resetTimer() {
// User is still here
self.setState(matrixService.presence.online);
// Re-arm the timer
$timeout.cancel(timer);
timer = $timeout(onOfflineTimerFire, OFFLINE_TIME);
}
}]);

View File

@ -26,6 +26,7 @@
<script src="components/matrix/matrix-service.js"></script> <script src="components/matrix/matrix-service.js"></script>
<script src="components/matrix/event-stream-service.js"></script> <script src="components/matrix/event-stream-service.js"></script>
<script src="components/matrix/event-handler-service.js"></script> <script src="components/matrix/event-handler-service.js"></script>
<script src="components/matrix/presence-service.js"></script>
<script src="components/fileInput/file-input-directive.js"></script> <script src="components/fileInput/file-input-directive.js"></script>
<script src="components/fileUpload/file-upload-service.js"></script> <script src="components/fileUpload/file-upload-service.js"></script>
<script src="components/utilities/utilities-service.js"></script> <script src="components/utilities/utilities-service.js"></script>