Merge branch 'master' of github.com:matrix-org/synapse into sql_refactor

Conflicts:
	synapse/storage/roommember.py
paul/schema_breaking_changes
Erik Johnston 2014-08-15 16:21:13 +01:00
commit a17b371384
10 changed files with 330 additions and 88 deletions

24
nuke-room-from-db.sh Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
## CAUTION:
## This script will remove (hopefully) all trace of the given room ID from
## your homeserver.db
## Do not run it lightly.
ROOMID="$1"
sqlite3 homeserver.db <<EOF
DELETE FROM context_depth WHERE context = '$ROOMID';
DELETE FROM current_state WHERE context = '$ROOMID';
DELETE FROM feedback WHERE room_id = '$ROOMID';
DELETE FROM messages WHERE room_id = '$ROOMID';
DELETE FROM pdu_backward_extremities WHERE context = '$ROOMID';
DELETE FROM pdu_edges WHERE context = '$ROOMID';
DELETE FROM pdu_forward_extremities WHERE context = '$ROOMID';
DELETE FROM pdus WHERE context = '$ROOMID';
DELETE FROM room_data WHERE room_id = '$ROOMID';
DELETE FROM room_memberships WHERE room_id = '$ROOMID';
DELETE FROM rooms WHERE room_id = '$ROOMID';
DELETE FROM state_pdus WHERE context = '$ROOMID';
EOF

View File

@ -21,8 +21,8 @@ limitations under the License.
'use strict'; 'use strict';
angular.module('MatrixWebClientController', ['matrixService']) angular.module('MatrixWebClientController', ['matrixService'])
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', .controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'eventStreamService',
function($scope, $location, $rootScope, matrixService) { function($scope, $location, $rootScope, matrixService, 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();
@ -46,9 +46,15 @@ angular.module('MatrixWebClientController', ['matrixService'])
} }
}; };
if (matrixService.config()) {
eventStreamService.resume();
}
// Logs the user out // Logs the user out
$scope.logout = function() { $scope.logout = function() {
// kill the event stream
eventStreamService.stop();
// Clean permanent data // Clean permanent data
matrixService.setConfig({}); matrixService.setConfig({});
matrixService.saveConfig(); matrixService.saveConfig();
@ -57,7 +63,7 @@ angular.module('MatrixWebClientController', ['matrixService'])
$location.path("login"); $location.path("login");
}; };
// Listen to the event indicating that the access token is no more valid. // Listen to the event indicating that the access token is no longer valid.
// In this case, the user needs to log in again. // In this case, the user needs to log in again.
$scope.$on("M_UNKNOWN_TOKEN", function() { $scope.$on("M_UNKNOWN_TOKEN", function() {
console.log("Invalid access token -> log user out"); console.log("Invalid access token -> log user out");

View File

@ -20,7 +20,9 @@ var matrixWebClient = angular.module('matrixWebClient', [
'LoginController', 'LoginController',
'RoomController', 'RoomController',
'RoomsController', 'RoomsController',
'matrixService' 'matrixService',
'eventStreamService',
'eventHandlerService'
]); ]);
matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider', matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
@ -59,12 +61,16 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
$httpProvider.interceptors.push('AccessTokenInterceptor'); $httpProvider.interceptors.push('AccessTokenInterceptor');
}]); }]);
matrixWebClient.run(['$location', 'matrixService' , function($location, matrixService) { matrixWebClient.run(['$location', 'matrixService', 'eventStreamService', function($location, matrixService, eventStreamService) {
// If we have no persistent login information, go to the login page // If we have no persistent login information, go to the login page
var config = matrixService.config(); var config = matrixService.config();
if (!config || !config.access_token) { if (!config || !config.access_token) {
eventStreamService.stop();
$location.path("login"); $location.path("login");
} }
else {
eventStreamService.resume();
}
}]); }]);
matrixWebClient matrixWebClient

View File

@ -0,0 +1,109 @@
/*
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 handles what should happen when you get an event. This service does
not care where the event came from, it only needs enough context to be able to
process them. Events may be coming from the event stream, the REST API (via
direct GETs or via a pagination stream API), etc.
Typically, this service will store events or broadcast them to any listeners
(e.g. controllers) via $broadcast. Alternatively, it may update the $rootScope
if typically all the $on method would do is update its own $scope.
*/
angular.module('eventHandlerService', [])
.factory('eventHandlerService', ['matrixService', '$rootScope', function(matrixService, $rootScope) {
var MSG_EVENT = "MSG_EVENT";
var MEMBER_EVENT = "MEMBER_EVENT";
var PRESENCE_EVENT = "PRESENCE_EVENT";
$rootScope.events = {
rooms: {}, // will contain roomId: { messages:[], members:[] }
};
var initRoom = function(room_id) {
console.log("Creating new handler entry for " + room_id);
$rootScope.events.rooms[room_id] = {};
$rootScope.events.rooms[room_id].messages = [];
$rootScope.events.rooms[room_id].members = [];
}
var handleMessage = function(event, isLiveEvent) {
if ("membership_target" in event.content) {
event.user_id = event.content.membership_target;
}
if (!(event.room_id in $rootScope.events.rooms)) {
initRoom(event.room_id);
}
if (isLiveEvent) {
$rootScope.events.rooms[event.room_id].messages.push(event);
}
else {
$rootScope.events.rooms[event.room_id].messages.unshift(event);
}
// TODO send delivery receipt if isLiveEvent
// $broadcast this, as controllers may want to do funky things such as
// scroll to the bottom, etc which cannot be expressed via simple $scope
// updates.
$rootScope.$broadcast(MSG_EVENT, event, isLiveEvent);
};
var handleRoomMember = function(event, isLiveEvent) {
$rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent);
};
var handlePresence = function(event, isLiveEvent) {
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
};
return {
MSG_EVENT: MSG_EVENT,
MEMBER_EVENT: MEMBER_EVENT,
PRESENCE_EVENT: PRESENCE_EVENT,
handleEvent: function(event, isLiveEvent) {
switch(event.type) {
case "m.room.message":
handleMessage(event, isLiveEvent);
break;
case "m.room.member":
handleRoomMember(event, isLiveEvent);
break;
case "m.presence":
handlePresence(event, isLiveEvent);
break;
default:
console.log("Unable to handle event type " + event.type);
break;
}
},
// isLiveEvents determines whether notifications should be shown, whether
// messages get appended to the start/end of lists, etc.
handleEvents: function(events, isLiveEvents) {
for (var i=0; i<events.length; i++) {
this.handleEvent(events[i], isLiveEvents);
}
}
};
}]);

View File

@ -0,0 +1,131 @@
/*
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 manages where in the event stream the web client currently is,
repolling the event stream, and provides methods to resume/pause/stop the event
stream. This service is not responsible for parsing event data. For that, see
the eventHandlerService.
*/
angular.module('eventStreamService', [])
.factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) {
var END = "END";
var START = "START";
var TIMEOUT_MS = 5000;
var ERR_TIMEOUT_MS = 5000;
var settings = {
from: "END",
to: undefined,
limit: undefined,
shouldPoll: true,
isActive: false
};
// interrupts the stream. Only valid if there is a stream conneciton
// open.
var interrupt = function(shouldPoll) {
console.log("[EventStream] interrupt("+shouldPoll+") "+
JSON.stringify(settings));
settings.shouldPoll = shouldPoll;
settings.isActive = false;
};
var saveStreamSettings = function() {
localStorage.setItem("streamSettings", JSON.stringify(settings));
};
var startEventStream = function() {
settings.shouldPoll = true;
settings.isActive = true;
var deferred = $q.defer();
// run the stream from the latest token
matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
function(response) {
if (!settings.isActive) {
console.log("[EventStream] Got response but now inactive. Dropping data.");
return;
}
settings.from = response.data.end;
console.log("[EventStream] Got response from "+settings.from+" to "+response.data.end);
eventHandlerService.handleEvents(response.data.chunk, true);
deferred.resolve(response);
if (settings.shouldPoll) {
$timeout(startEventStream, 0);
}
else {
console.log("[EventStream] Stopping poll.");
}
},
function(error) {
if (error.status == 403) {
settings.shouldPoll = false;
}
deferred.reject(error);
if (settings.shouldPoll) {
$timeout(startEventStream, ERR_TIMEOUT_MS);
}
else {
console.log("[EventStream] Stopping polling.");
}
}
);
return deferred.promise;
};
return {
// resume the stream from whereever it last got up to. Typically used
// when the page is opened.
resume: function() {
if (settings.isActive) {
console.log("[EventStream] Already active, ignoring resume()");
return;
}
console.log("[EventStream] resume "+JSON.stringify(settings));
return startEventStream();
},
// pause the stream. Resuming it will continue from the current position
pause: function() {
console.log("[EventStream] pause "+JSON.stringify(settings));
// kill any running stream
interrupt(false);
// save the latest token
saveStreamSettings();
},
// stop the stream and wipe the position in the stream. Typically used
// when logging out / logged out.
stop: function() {
console.log("[EventStream] stop "+JSON.stringify(settings));
// kill any running stream
interrupt(false);
// clear the latest token
settings.from = END;
saveStreamSettings();
}
};
}]);

View File

@ -16,6 +16,12 @@ limitations under the License.
'use strict'; 'use strict';
/*
This service wraps up Matrix API calls.
This serves to isolate the caller from changes to the underlying url paths, as
well as attach common params (e.g. access_token) to requests.
*/
angular.module('matrixService', []) angular.module('matrixService', [])
.factory('matrixService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) { .factory('matrixService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) {
@ -36,10 +42,16 @@ angular.module('matrixService', [])
var MAPPING_PREFIX = "alias_for_"; var MAPPING_PREFIX = "alias_for_";
var doRequest = function(method, path, params, data) { var doRequest = function(method, path, params, data) {
if (!config) {
console.warn("No config exists. Cannot perform request to "+path);
return;
}
// Inject the access token // Inject the access token
if (!params) { if (!params) {
params = {}; params = {};
} }
params.access_token = config.access_token; params.access_token = config.access_token;
return doBaseRequest(config.homeserver, method, path, params, data, undefined); return doBaseRequest(config.homeserver, method, path, params, data, undefined);
@ -297,6 +309,15 @@ angular.module('matrixService', [])
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers); return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
}, },
// start listening on /events
getEventStream: function(from, timeout) {
var path = "/events";
var params = {
from: from,
timeout: timeout
};
return doRequest("GET", path, params);
},
// //
testLogin: function() { testLogin: function() {

View File

@ -14,6 +14,8 @@
<script src="room/room-controller.js"></script> <script src="room/room-controller.js"></script>
<script src="rooms/rooms-controller.js"></script> <script src="rooms/rooms-controller.js"></script>
<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-handler-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>
</head> </head>

View File

@ -1,6 +1,6 @@
angular.module('LoginController', ['matrixService']) angular.module('LoginController', ['matrixService'])
.controller('LoginController', ['$scope', '$location', 'matrixService', .controller('LoginController', ['$scope', '$location', 'matrixService', 'eventStreamService',
function($scope, $location, matrixService) { function($scope, $location, matrixService, eventStreamService) {
'use strict'; 'use strict';
@ -51,7 +51,7 @@ angular.module('LoginController', ['matrixService'])
// And permanently save it // And permanently save it
matrixService.saveConfig(); matrixService.saveConfig();
eventStreamService.resume();
// Go to the user's rooms list page // Go to the user's rooms list page
$location.path("rooms"); $location.path("rooms");
}, },
@ -83,6 +83,7 @@ angular.module('LoginController', ['matrixService'])
access_token: response.data.access_token access_token: response.data.access_token
}); });
matrixService.saveConfig(); matrixService.saveConfig();
eventStreamService.resume();
$location.path("rooms"); $location.path("rooms");
} }
else { else {

View File

@ -15,8 +15,8 @@ limitations under the License.
*/ */
angular.module('RoomController', []) angular.module('RoomController', [])
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', .controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService',
function($scope, $http, $timeout, $routeParams, $location, matrixService) { function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService) {
'use strict'; 'use strict';
var MESSAGES_PER_PAGINATION = 10; var MESSAGES_PER_PAGINATION = 10;
$scope.room_id = $routeParams.room_id; $scope.room_id = $routeParams.room_id;
@ -28,9 +28,7 @@ angular.module('RoomController', [])
can_paginate: true, // this is toggled off when we run out of items can_paginate: true, // this is toggled off when we run out of items
stream_failure: undefined // the response when the stream fails stream_failure: undefined // the response when the stream fails
}; };
$scope.messages = [];
$scope.members = {}; $scope.members = {};
$scope.stopPoll = false;
$scope.imageURLToSend = ""; $scope.imageURLToSend = "";
$scope.userIDToInvite = ""; $scope.userIDToInvite = "";
@ -42,34 +40,24 @@ angular.module('RoomController', [])
},0); },0);
}; };
var parseChunk = function(chunks, appendToStart) { $scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
for (var i = 0; i < chunks.length; i++) { if (isLive && event.room_id === $scope.room_id) {
var chunk = chunks[i]; scrollToBottom();
if (chunk.room_id == $scope.room_id && chunk.type == "m.room.message") {
if ("membership_target" in chunk.content) {
chunk.user_id = chunk.content.membership_target;
}
if (appendToStart) {
$scope.messages.unshift(chunk);
}
else {
$scope.messages.push(chunk);
scrollToBottom();
}
}
else if (chunk.room_id == $scope.room_id && chunk.type == "m.room.member") {
updateMemberList(chunk);
}
else if (chunk.type === "m.presence") {
updatePresence(chunk);
}
} }
}; });
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
updateMemberList(event);
});
$scope.$on(eventHandlerService.PRESENCE_EVENT, function(ngEvent, event, isLive) {
updatePresence(event);
});
var paginate = function(numItems) { var paginate = function(numItems) {
matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then( matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then(
function(response) { function(response) {
parseChunk(response.data.chunk, true); eventHandlerService.handleEvents(response.data.chunk, false);
$scope.state.earliest_token = response.data.end; $scope.state.earliest_token = response.data.end;
if (response.data.chunk.length < MESSAGES_PER_PAGINATION) { if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
// no more messages to paginate :( // no more messages to paginate :(
@ -82,43 +70,6 @@ angular.module('RoomController', [])
) )
}; };
var shortPoll = function() {
$http.get(matrixService.config().homeserver + matrixService.prefix + "/events", {
"params": {
"access_token": matrixService.config().access_token,
"from": $scope.state.events_from,
"timeout": 5000
}})
.then(function(response) {
$scope.state.stream_failure = undefined;
console.log("Got response from "+$scope.state.events_from+" to "+response.data.end);
$scope.state.events_from = response.data.end;
$scope.feedback = "";
parseChunk(response.data.chunk, false);
if ($scope.stopPoll) {
console.log("Stopping polling.");
}
else {
$timeout(shortPoll, 0);
}
}, function(response) {
$scope.state.stream_failure = response;
if (response.status == 403) {
$scope.stopPoll = true;
}
if ($scope.stopPoll) {
console.log("Stopping polling.");
}
else {
$timeout(shortPoll, 5000);
}
});
};
var updateMemberList = function(chunk) { var updateMemberList = function(chunk) {
var isNewMember = !(chunk.target_user_id in $scope.members); var isNewMember = !(chunk.target_user_id in $scope.members);
if (isNewMember) { if (isNewMember) {
@ -133,7 +84,6 @@ angular.module('RoomController', [])
function(response) { function(response) {
var member = $scope.members[chunk.target_user_id]; var member = $scope.members[chunk.target_user_id];
if (member !== undefined) { if (member !== undefined) {
console.log("Updated displayname "+chunk.target_user_id+" to " + response.data.displayname);
member.displayname = response.data.displayname; member.displayname = response.data.displayname;
} }
} }
@ -142,7 +92,6 @@ angular.module('RoomController', [])
function(response) { function(response) {
var member = $scope.members[chunk.target_user_id]; var member = $scope.members[chunk.target_user_id];
if (member !== undefined) { if (member !== undefined) {
console.log("Updated image for "+chunk.target_user_id+" to " + response.data.avatar_url);
member.avatar_url = response.data.avatar_url; member.avatar_url = response.data.avatar_url;
} }
} }
@ -218,8 +167,6 @@ angular.module('RoomController', [])
matrixService.join($scope.room_id).then( matrixService.join($scope.room_id).then(
function() { function() {
console.log("Joined room "+$scope.room_id); console.log("Joined room "+$scope.room_id);
// Now start reading from the stream
$timeout(shortPoll, 0);
// Get the current member list // Get the current member list
matrixService.getMemberList($scope.room_id).then( matrixService.getMemberList($scope.room_id).then(
@ -278,9 +225,4 @@ angular.module('RoomController', [])
$scope.loadMoreHistory = function() { $scope.loadMoreHistory = function() {
paginate(MESSAGES_PER_PAGINATION); paginate(MESSAGES_PER_PAGINATION);
}; };
$scope.$on('$destroy', function(e) {
console.log("onDestroyed: Stopping poll.");
$scope.stopPoll = true;
});
}]); }]);

View File

@ -22,14 +22,14 @@
<div class="messageTableWrapper"> <div class="messageTableWrapper">
<table class="messageTable"> <table class="messageTable">
<tr ng-repeat="msg in messages" ng-class="msg.user_id === state.user_id ? 'mine' : ''"> <tr ng-repeat="msg in events.rooms[room_id].messages" ng-class="msg.user_id === state.user_id ? 'mine' : ''">
<td class="leftBlock"> <td class="leftBlock">
<div class="sender" ng-hide="messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div> <div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
<div class="timestamp">{{ msg.content.hsob_ts | date:'MMM d HH:mm:ss' }}</div> <div class="timestamp">{{ msg.content.hsob_ts | date:'MMM d HH:mm:ss' }}</div>
</td> </td>
<td class="avatar"> <td class="avatar">
<img ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32" <img ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
ng-hide="messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/> ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
</td> </td>
<td ng-class="!msg.content.membership_target ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : ''"> <td ng-class="!msg.content.membership_target ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : ''">
<div class="bubble"> <div class="bubble">
@ -40,7 +40,7 @@
</td> </td>
<td class="rightBlock"> <td class="rightBlock">
<img ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32" <img ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
ng-hide="messages[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/> ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id !== state.user_id"/>
</td> </td>
</tr> </tr>
</table> </table>