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

Conflicts:
	synapse/app/homeserver.py
This commit is contained in:
Erik Johnston 2015-01-22 13:35:34 +00:00
commit 7f058c5ff7
70 changed files with 818 additions and 1658 deletions

View file

@ -1,3 +1,11 @@
Changes in synapse 0.6.1 (2015-01-07)
=====================================
* Major optimizations to improve performance of initial sync and event sending
in large rooms (by up to 10x)
* Media repository now includes a Content-Length header on media downloads.
* Improve quality of thumbnails by changing resizing algorithm.
Changes in synapse 0.6.0 (2014-12-16) Changes in synapse 0.6.0 (2014-12-16)
===================================== =====================================

View file

@ -108,6 +108,15 @@ To install the synapse homeserver run::
This installs synapse, along with the libraries it uses, into This installs synapse, along with the libraries it uses, into
``$HOME/.local/lib/`` on Linux or ``$HOME/Library/Python/2.7/lib/`` on OSX. ``$HOME/.local/lib/`` on Linux or ``$HOME/Library/Python/2.7/lib/`` on OSX.
Your python may not give priority to locally installed libraries over system
libraries, in which case you must add your local packages to your python path::
$ # on Linux:
$ export PYTHONPATH=$HOME/.local/lib/python2.7/site-packages:$PYTHONPATH
$ # on OSX:
$ export PYTHONPATH=$HOME/Library/Python/2.7/lib/python/site-packages:$PYTHONPATH
For reliable VoIP calls to be routed via this homeserver, you MUST configure For reliable VoIP calls to be routed via this homeserver, you MUST configure
a TURN server. See docs/turn-howto.rst for details. a TURN server. See docs/turn-howto.rst for details.

View file

@ -52,7 +52,7 @@ resulting conflicts during the upgrade process.
Before running the command the homeserver should be first completely Before running the command the homeserver should be first completely
shutdown. To run it, simply specify the location of the database, e.g.: shutdown. To run it, simply specify the location of the database, e.g.:
./database-prepare-for-0.5.0.sh "homeserver.db" ./scripts/database-prepare-for-0.5.0.sh "homeserver.db"
Once this has successfully completed it will be safe to restart the Once this has successfully completed it will be safe to restart the
homeserver. You may notice that the homeserver takes a few seconds longer to homeserver. You may notice that the homeserver takes a few seconds longer to
@ -147,7 +147,7 @@ rooms the home server was a member of and room alias mappings.
Before running the command the homeserver should be first completely Before running the command the homeserver should be first completely
shutdown. To run it, simply specify the location of the database, e.g.: shutdown. To run it, simply specify the location of the database, e.g.:
./database-prepare-for-0.0.1.sh "homeserver.db" ./scripts/database-prepare-for-0.0.1.sh "homeserver.db"
Once this has successfully completed it will be safe to restart the Once this has successfully completed it will be safe to restart the
homeserver. You may notice that the homeserver takes a few seconds longer to homeserver. You may notice that the homeserver takes a few seconds longer to

View file

@ -1 +1 @@
0.6.0 0.6.1b

View file

@ -23,14 +23,27 @@ import argparse
from synapse.events import FrozenEvent from synapse.events import FrozenEvent
def make_graph(db_name, room_id, file_prefix): def make_graph(db_name, room_id, file_prefix, limit):
conn = sqlite3.connect(db_name) conn = sqlite3.connect(db_name)
c = conn.execute( sql = (
"SELECT json FROM event_json where room_id = ?", "SELECT json FROM event_json as j "
(room_id,) "INNER JOIN events as e ON e.event_id = j.event_id "
"WHERE j.room_id = ?"
) )
args = [room_id]
if limit:
sql += (
" ORDER BY topological_ordering DESC, stream_ordering DESC "
"LIMIT ?"
)
args.append(limit)
c = conn.execute(sql, args)
events = [FrozenEvent(json.loads(e[0])) for e in c.fetchall()] events = [FrozenEvent(json.loads(e[0])) for e in c.fetchall()]
events.sort(key=lambda e: e.depth) events.sort(key=lambda e: e.depth)
@ -128,11 +141,16 @@ if __name__ == "__main__":
) )
parser.add_argument( parser.add_argument(
"-p", "--prefix", dest="prefix", "-p", "--prefix", dest="prefix",
help="String to prefix output files with" help="String to prefix output files with",
default="graph_output"
)
parser.add_argument(
"-l", "--limit",
help="Only retrieve the last N events.",
) )
parser.add_argument('db') parser.add_argument('db')
parser.add_argument('room') parser.add_argument('room')
args = parser.parse_args() args = parser.parse_args()
make_graph(args.db, args.room, args.prefix) make_graph(args.db, args.room, args.prefix, args.limit)

View file

@ -1,17 +0,0 @@
.loggedin {
visibility: hidden;
}
p {
font-family: monospace;
}
table
{
border-spacing:5px;
}
th,td
{
padding:5px;
}

View file

@ -1,30 +0,0 @@
<div>
<p>This room creation / message sending demo requires a home server to be running on http://localhost:8008</p>
</div>
<form class="loginForm">
<input type="text" id="userLogin" placeholder="Username"></input>
<input type="password" id="passwordLogin" placeholder="Password"></input>
<input type="button" class="login" value="Login"></input>
</form>
<div class="loggedin">
<form class="createRoomForm">
<input type="text" id="roomAlias" placeholder="Room alias (optional)"></input>
<input type="button" class="createRoom" value="Create Room"></input>
</form>
<form class="sendMessageForm">
<input type="text" id="roomId" placeholder="Room ID"></input>
<input type="text" id="messageBody" placeholder="Message body"></input>
<input type="button" class="sendMessage" value="Send Message"></input>
</form>
<table id="rooms">
<tbody>
<tr>
<th>Room ID</th>
<th>My state</th>
<th>Room Alias</th>
<th>Latest message</th>
</tr>
</tbody>
</table>
</div>

View file

@ -1,113 +0,0 @@
var accountInfo = {};
var showLoggedIn = function(data) {
accountInfo = data;
getCurrentRoomList();
$(".loggedin").css({visibility: "visible"});
};
$('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8008/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
dataType: "json",
success: function(data) {
showLoggedIn(data);
},
error: function(err) {
var errMsg = "To try this, you need a home server running!";
var errJson = $.parseJSON(err.responseText);
if (errJson) {
errMsg = JSON.stringify(errJson);
}
alert(errMsg);
}
});
});
var getCurrentRoomList = function() {
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
addRoom(rooms[i]);
}
}).fail(function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
});
};
$('.createRoom').live('click', function() {
var roomAlias = $("#roomAlias").val();
var data = {};
if (roomAlias.length > 0) {
data.room_alias_name = roomAlias;
}
$.ajax({
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",
success: function(data) {
data.membership = "join"; // you are automatically joined into every room you make.
data.latest_message = "";
addRoom(data);
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
});
var addRoom = function(data) {
row = "<tr>" +
"<td>"+data.room_id+"</td>" +
"<td>"+data.membership+"</td>" +
"<td>"+data.room_alias+"</td>" +
"<td>"+data.latest_message+"</td>" +
"</tr>";
$("#rooms").append(row);
};
$('.sendMessage').live('click', function() {
var roomId = $("#roomId").val();
var body = $("#messageBody").val();
var msgId = $.now();
if (roomId.length === 0 || body.length === 0) {
return;
}
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
var data = {
msgtype: "m.text",
body: body
};
$.ajax({
url: url,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",
success: function(data) {
$("#messageBody").val("");
// wipe the table and reload it. Using the event stream would be the best
// solution but that is out of scope of this fiddle.
$("#rooms").find("tr:gt(0)").remove();
getCurrentRoomList();
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
});

View file

@ -1,17 +0,0 @@
.loggedin {
visibility: hidden;
}
p {
font-family: monospace;
}
table
{
border-spacing:5px;
}
th,td
{
padding:5px;
}

View file

@ -1,23 +0,0 @@
<div>
<p>This event stream demo requires a home server to be running on http://localhost:8008</p>
</div>
<form class="loginForm">
<input type="text" id="userLogin" placeholder="Username"></input>
<input type="password" id="passwordLogin" placeholder="Password"></input>
<input type="button" class="login" value="Login"></input>
</form>
<div class="loggedin">
<form class="sendMessageForm">
<input type="button" class="sendMessage" value="Send random message"></input>
</form>
<p id="streamErrorText"></p>
<table id="rooms">
<tbody>
<tr>
<th>Room ID</th>
<th>Latest message</th>
</tr>
</tbody>
</table>
</div>

View file

@ -1,145 +0,0 @@
var accountInfo = {};
var eventStreamInfo = {
from: "END"
};
var roomInfo = [];
var longpollEventStream = function() {
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$from", eventStreamInfo.from);
$.getJSON(url, function(data) {
eventStreamInfo.from = data.end;
var hasNewLatestMessage = false;
for (var i=0; i<data.chunk.length; ++i) {
if (data.chunk[i].type === "m.room.message") {
for (var j=0; j<roomInfo.length; ++j) {
if (roomInfo[j].room_id === data.chunk[i].room_id) {
roomInfo[j].latest_message = data.chunk[i].content.body;
hasNewLatestMessage = true;
}
}
}
}
if (hasNewLatestMessage) {
setRooms(roomInfo);
}
$("#streamErrorText").text("");
longpollEventStream();
}).fail(function(err) {
$("#streamErrorText").text("Event stream error: "+JSON.stringify($.parseJSON(err.responseText)));
setTimeout(longpollEventStream, 5000);
});
};
var showLoggedIn = function(data) {
accountInfo = data;
longpollEventStream();
getCurrentRoomList();
$(".loggedin").css({visibility: "visible"});
};
$('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8008/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
dataType: "json",
success: function(data) {
$("#rooms").find("tr:gt(0)").remove();
showLoggedIn(data);
},
error: function(err) {
var errMsg = "To try this, you need a home server running!";
var errJson = $.parseJSON(err.responseText);
if (errJson) {
errMsg = JSON.stringify(errJson);
}
alert(errMsg);
}
});
});
var getCurrentRoomList = function() {
$("#roomId").val("");
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
if ("messages" in rooms[i]) {
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
}
}
roomInfo = rooms;
setRooms(roomInfo);
}).fail(function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
});
};
$('.sendMessage').live('click', function() {
if (roomInfo.length === 0) {
alert("There is no room to send a message to!");
return;
}
var index = Math.floor(Math.random() * roomInfo.length);
sendMessage(roomInfo[index].room_id);
});
var sendMessage = function(roomId) {
var body = "jsfiddle message @" + $.now();
if (roomId.length === 0) {
return;
}
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
var data = {
msgtype: "m.text",
body: body
};
$.ajax({
url: url,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",
success: function(data) {
$("#messageBody").val("");
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
};
var setRooms = function(roomList) {
// wipe existing entries
$("#rooms").find("tr:gt(0)").remove();
var rows = "";
for (var i=0; i<roomList.length; ++i) {
row = "<tr>" +
"<td>"+roomList[i].room_id+"</td>" +
"<td>"+roomList[i].latest_message+"</td>" +
"</tr>";
rows += row;
}
$("#rooms").append(rows);
};

View file

@ -1,43 +0,0 @@
.roomListDashboard, .roomContents, .sendMessageForm {
visibility: hidden;
}
.roomList {
background-color: #909090;
}
.messageWrapper {
background-color: #EEEEEE;
height: 400px;
overflow: scroll;
}
.membersWrapper {
background-color: #EEEEEE;
height: 200px;
width: 50%;
overflow: scroll;
}
.textEntry {
width: 100%
}
p {
font-family: monospace;
}
table
{
border-spacing:5px;
}
th,td
{
padding:5px;
}
.roomList tr:not(:first-child):hover {
background-color: orange;
cursor: pointer;
}

View file

@ -1,7 +0,0 @@
name: Example Matrix Client
description: Includes login, live event streaming, creating rooms, sending messages and viewing member lists.
authors:
- matrix.org
resources:
- http://matrix.org
normalize_css: no

View file

@ -1,56 +0,0 @@
<div class="signUp">
<p>Matrix example application: Requires a local home server running at http://localhost:8008</p>
<form class="registrationForm">
<p>No account? Register:</p>
<input type="text" id="userReg" placeholder="Username"></input>
<input type="password" id="passwordReg" placeholder="Password"></input>
<input type="button" class="register" value="Register"></input>
</form>
<form class="loginForm">
<p>Got an account? Login:</p>
<input type="text" id="userLogin" placeholder="Username"></input>
<input type="password" id="passwordLogin" placeholder="Password"></input>
<input type="button" class="login" value="Login"></input>
</form>
</div>
<div class="roomListDashboard">
<form class="createRoomForm">
<input type="text" id="roomAlias" placeholder="Room alias"></input>
<input type="button" class="createRoom" value="Create Room"></input>
</form>
<table id="rooms" class="roomList">
<tbody>
<tr>
<th>Room</th>
<th>My state</th>
<th>Latest message</th>
</tr>
</tbody>
</table>
</div>
<div class="roomContents">
<p id="roomName">Select a room</p>
<div class="messageWrapper">
<table id="messages">
<tbody>
</tbody>
</table>
</div>
<form class="sendMessageForm">
<input type="text" class="textEntry" id="body" placeholder="Enter text here..." onkeydown="javascript:if (event.keyCode == 13) document.getElementById('sendMsg').focus()"></input>
<input type="button" class="sendMessage" id="sendMsg" value="Send"></input>
</form>
</div>
<div>
<p>Member list:</p>
<div class="membersWrapper">
<table id="members">
<tbody>
</tbody>
</table>
</div>
</div>

View file

@ -1,327 +0,0 @@
var accountInfo = {};
var eventStreamInfo = {
from: "END"
};
var roomInfo = [];
var memberInfo = [];
var viewingRoomId;
// ************** Event Streaming **************
var longpollEventStream = function() {
var url = "http://localhost:8008/_matrix/client/api/v1/events?access_token=$token&from=$from";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$from", eventStreamInfo.from);
$.getJSON(url, function(data) {
eventStreamInfo.from = data.end;
var hasNewLatestMessage = false;
var updatedMemberList = false;
var i=0;
var j=0;
for (i=0; i<data.chunk.length; ++i) {
if (data.chunk[i].type === "m.room.message") {
console.log("Got new message: " + JSON.stringify(data.chunk[i]));
if (viewingRoomId === data.chunk[i].room_id) {
addMessage(data.chunk[i]);
}
for (j=0; j<roomInfo.length; ++j) {
if (roomInfo[j].room_id === data.chunk[i].room_id) {
roomInfo[j].latest_message = data.chunk[i].content.body;
hasNewLatestMessage = true;
}
}
}
else if (data.chunk[i].type === "m.room.member") {
if (viewingRoomId === data.chunk[i].room_id) {
console.log("Got new member: " + JSON.stringify(data.chunk[i]));
addMessage(data.chunk[i]);
for (j=0; j<memberInfo.length; ++j) {
if (memberInfo[j].state_key === data.chunk[i].state_key) {
memberInfo[j] = data.chunk[i];
updatedMemberList = true;
break;
}
}
if (!updatedMemberList) {
memberInfo.push(data.chunk[i]);
updatedMemberList = true;
}
}
if (data.chunk[i].state_key === accountInfo.user_id) {
getCurrentRoomList(); // update our join/invite list
}
}
else {
console.log("Discarding: " + JSON.stringify(data.chunk[i]));
}
}
if (hasNewLatestMessage) {
setRooms(roomInfo);
}
if (updatedMemberList) {
$("#members").empty();
for (i=0; i<memberInfo.length; ++i) {
addMember(memberInfo[i]);
}
}
longpollEventStream();
}).fail(function(err) {
setTimeout(longpollEventStream, 5000);
});
};
// ************** Registration and Login **************
var onLoggedIn = function(data) {
accountInfo = data;
longpollEventStream();
getCurrentRoomList();
$(".roomListDashboard").css({visibility: "visible"});
$(".roomContents").css({visibility: "visible"});
$(".signUp").css({display: "none"});
};
$('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8008/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
dataType: "json",
success: function(data) {
onLoggedIn(data);
},
error: function(err) {
alert("Unable to login: is the home server running?");
}
});
});
$('.register').live('click', function() {
var user = $("#userReg").val();
var password = $("#passwordReg").val();
$.ajax({
url: "http://localhost:8008/_matrix/client/api/v1/register",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
dataType: "json",
success: function(data) {
onLoggedIn(data);
},
error: function(err) {
var msg = "Is the home server running?";
var errJson = $.parseJSON(err.responseText);
if (errJson !== null) {
msg = errJson.error;
}
alert("Unable to register: "+msg);
}
});
});
// ************** Creating a room ******************
$('.createRoom').live('click', function() {
var roomAlias = $("#roomAlias").val();
var data = {};
if (roomAlias.length > 0) {
data.room_alias_name = roomAlias;
}
$.ajax({
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",
success: function(response) {
$("#roomAlias").val("");
response.membership = "join"; // you are automatically joined into every room you make.
response.latest_message = "";
roomInfo.push(response);
setRooms(roomInfo);
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
});
// ************** Getting current state **************
var getCurrentRoomList = function() {
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
if ("messages" in rooms[i]) {
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
}
}
roomInfo = rooms;
setRooms(roomInfo);
}).fail(function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
});
};
var loadRoomContent = function(roomId) {
console.log("loadRoomContent " + roomId);
viewingRoomId = roomId;
$("#roomName").text("Room: "+roomId);
$(".sendMessageForm").css({visibility: "visible"});
getMessages(roomId);
getMemberList(roomId);
};
var getMessages = function(roomId) {
$("#messages").empty();
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" +
encodeURIComponent(roomId) + "/messages?access_token=" + accountInfo.access_token + "&from=END&dir=b&limit=10";
$.getJSON(url, function(data) {
for (var i=data.chunk.length-1; i>=0; --i) {
addMessage(data.chunk[i]);
}
});
};
var getMemberList = function(roomId) {
$("#members").empty();
memberInfo = [];
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/" +
encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token;
$.getJSON(url, function(data) {
for (var i=0; i<data.chunk.length; ++i) {
memberInfo.push(data.chunk[i]);
addMember(data.chunk[i]);
}
});
};
// ************** Sending messages **************
$('.sendMessage').live('click', function() {
if (viewingRoomId === undefined) {
alert("There is no room to send a message to!");
return;
}
var body = $("#body").val();
sendMessage(viewingRoomId, body);
});
var sendMessage = function(roomId, body) {
var msgId = $.now();
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
var data = {
msgtype: "m.text",
body: body
};
$.ajax({
url: url,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",
success: function(data) {
$("#body").val("");
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
};
// ************** Navigation and DOM manipulation **************
var setRooms = function(roomList) {
// wipe existing entries
$("#rooms").find("tr:gt(0)").remove();
var rows = "";
for (var i=0; i<roomList.length; ++i) {
row = "<tr>" +
"<td>"+roomList[i].room_id+"</td>" +
"<td>"+roomList[i].membership+"</td>" +
"<td>"+roomList[i].latest_message+"</td>" +
"</tr>";
rows += row;
}
$("#rooms").append(rows);
$('#rooms').find("tr").click(function(){
var roomId = $(this).find('td:eq(0)').text();
var membership = $(this).find('td:eq(1)').text();
if (membership !== "join") {
console.log("Joining room " + roomId);
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
$.ajax({
url: url,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({membership: "join"}),
dataType: "json",
success: function(data) {
loadRoomContent(roomId);
getCurrentRoomList();
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
}
else {
loadRoomContent(roomId);
}
});
};
var addMessage = function(data) {
var msg = data.content.body;
if (data.type === "m.room.member") {
if (data.content.membership === undefined) {
return;
}
if (data.content.membership === "invite") {
msg = "<em>invited " + data.state_key + " to the room</em>";
}
else if (data.content.membership === "join") {
msg = "<em>joined the room</em>";
}
else if (data.content.membership === "leave") {
msg = "<em>left the room</em>";
}
else if (data.content.membership === "ban") {
msg = "<em>was banned from the room</em>";
}
}
if (msg === undefined) {
return;
}
var row = "<tr>" +
"<td>"+data.user_id+"</td>" +
"<td>"+msg+"</td>" +
"</tr>";
$("#messages").append(row);
};
var addMember = function(data) {
var row = "<tr>" +
"<td>"+data.state_key+"</td>" +
"<td>"+data.content.membership+"</td>" +
"</tr>";
$("#members").append(row);
};

View file

@ -1,7 +0,0 @@
.loggedin {
visibility: hidden;
}
p {
font-family: monospace;
}

View file

@ -1,20 +0,0 @@
<div>
<p>This registration/login demo requires a home server to be running on http://localhost:8008</p>
</div>
<form class="registrationForm">
<input type="text" id="user" placeholder="Username"></input>
<input type="password" id="password" placeholder="Password"></input>
<input type="button" class="register" value="Register"></input>
</form>
<form class="loginForm">
<input type="text" id="userLogin" placeholder="Username"></input>
<input type="password" id="passwordLogin" placeholder="Password"></input>
<input type="button" class="login" value="Login"></input>
</form>
<div class="loggedin">
<p id="welcomeText"></p>
<input type="button" class="testToken" value="Test token"></input>
<input type="button" class="logout" value="Logout"></input>
<p id="imSyncText"></p>
</div>

View file

@ -1,79 +0,0 @@
var accountInfo = {};
var showLoggedIn = function(data) {
accountInfo = data;
$(".loggedin").css({visibility: "visible"});
$("#welcomeText").text("Welcome " + accountInfo.user_id+". Your access token is: " +
accountInfo.access_token);
};
$('.register').live('click', function() {
var user = $("#user").val();
var password = $("#password").val();
$.ajax({
url: "http://localhost:8008/_matrix/client/api/v1/register",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
dataType: "json",
success: function(data) {
showLoggedIn(data);
},
error: function(err) {
var errMsg = "To try this, you need a home server running!";
var errJson = $.parseJSON(err.responseText);
if (errJson) {
errMsg = JSON.stringify(errJson);
}
alert(errMsg);
}
});
});
var login = function(user, password) {
$.ajax({
url: "http://localhost:8008/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
dataType: "json",
success: function(data) {
showLoggedIn(data);
},
error: function(err) {
var errMsg = "To try this, you need a home server running!";
var errJson = $.parseJSON(err.responseText);
if (errJson) {
errMsg = JSON.stringify(errJson);
}
alert(errMsg);
}
});
};
$('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.getJSON("http://localhost:8008/_matrix/client/api/v1/login", function(data) {
if (data.flows[0].type !== "m.login.password") {
alert("I don't know how to login with this type: " + data.type);
return;
}
login(user, password);
});
});
$('.logout').live('click', function() {
accountInfo = {};
$("#imSyncText").text("");
$(".loggedin").css({visibility: "hidden"});
});
$('.testToken').live('click', function() {
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
$("#imSyncText").text(JSON.stringify(data, undefined, 2));
}).fail(function(err) {
$("#imSyncText").text(JSON.stringify($.parseJSON(err.responseText)));
});
});

View file

@ -1,17 +0,0 @@
.loggedin {
visibility: hidden;
}
p {
font-family: monospace;
}
table
{
border-spacing:5px;
}
th,td
{
padding:5px;
}

View file

@ -1,37 +0,0 @@
<div>
<p>This room membership demo requires a home server to be running on http://localhost:8008</p>
</div>
<form class="loginForm">
<input type="text" id="userLogin" placeholder="Username"></input>
<input type="password" id="passwordLogin" placeholder="Password"></input>
<input type="button" class="login" value="Login"></input>
</form>
<div class="loggedin">
<form class="createRoomForm">
<input type="button" class="createRoom" value="Create Room"></input>
</form>
<form class="changeMembershipForm">
<input type="text" id="roomId" placeholder="Room ID"></input>
<input type="text" id="targetUser" placeholder="Target User ID"></input>
<select id="membership">
<option value="invite">invite</option>
<option value="join">join</option>
<option value="leave">leave</option>
</select>
<input type="button" class="changeMembership" value="Change Membership"></input>
</form>
<form class="joinAliasForm">
<input type="text" id="roomAlias" placeholder="Room Alias (#name:domain)"></input>
<input type="button" class="joinAlias" value="Join via Alias"></input>
</form>
<table id="rooms">
<tbody>
<tr>
<th>Room ID</th>
<th>My state</th>
<th>Room Alias</th>
</tr>
</tbody>
</table>
</div>

View file

@ -1,141 +0,0 @@
var accountInfo = {};
var showLoggedIn = function(data) {
accountInfo = data;
getCurrentRoomList();
$(".loggedin").css({visibility: "visible"});
$("#membership").change(function() {
if ($("#membership").val() === "invite") {
$("#targetUser").css({visibility: "visible"});
}
else {
$("#targetUser").css({visibility: "hidden"});
}
});
};
$('.login').live('click', function() {
var user = $("#userLogin").val();
var password = $("#passwordLogin").val();
$.ajax({
url: "http://localhost:8008/_matrix/client/api/v1/login",
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
dataType: "json",
success: function(data) {
$("#rooms").find("tr:gt(0)").remove();
showLoggedIn(data);
},
error: function(err) {
var errMsg = "To try this, you need a home server running!";
var errJson = $.parseJSON(err.responseText);
if (errJson) {
errMsg = JSON.stringify(errJson);
}
alert(errMsg);
}
});
});
var getCurrentRoomList = function() {
$("#roomId").val("");
// wipe the table and reload it. Using the event stream would be the best
// solution but that is out of scope of this fiddle.
$("#rooms").find("tr:gt(0)").remove();
var url = "http://localhost:8008/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
$.getJSON(url, function(data) {
var rooms = data.rooms;
for (var i=0; i<rooms.length; ++i) {
addRoom(rooms[i]);
}
}).fail(function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
});
};
$('.createRoom').live('click', function() {
var data = {};
$.ajax({
url: "http://localhost:8008/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",
success: function(data) {
data.membership = "join"; // you are automatically joined into every room you make.
data.latest_message = "";
addRoom(data);
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
});
var addRoom = function(data) {
row = "<tr>" +
"<td>"+data.room_id+"</td>" +
"<td>"+data.membership+"</td>" +
"<td>"+data.room_alias+"</td>" +
"</tr>";
$("#rooms").append(row);
};
$('.changeMembership').live('click', function() {
var roomId = $("#roomId").val();
var member = $("#targetUser").val();
var membership = $("#membership").val();
if (roomId.length === 0) {
return;
}
var url = "http://localhost:8008/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomid", encodeURIComponent(roomId));
url = url.replace("$membership", membership);
var data = {};
if (membership === "invite") {
data = {
user_id: member
};
}
$.ajax({
url: url,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify(data),
dataType: "json",
success: function(data) {
getCurrentRoomList();
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
});
$('.joinAlias').live('click', function() {
var roomAlias = $("#roomAlias").val();
var url = "http://localhost:8008/_matrix/client/api/v1/join/$roomalias?access_token=$token";
url = url.replace("$token", accountInfo.access_token);
url = url.replace("$roomalias", encodeURIComponent(roomAlias));
$.ajax({
url: url,
type: "POST",
contentType: "application/json; charset=utf-8",
data: JSON.stringify({}),
dataType: "json",
success: function(data) {
getCurrentRoomList();
},
error: function(err) {
alert(JSON.stringify($.parseJSON(err.responseText)));
}
});
});

33
scripts/copyrighter-sql.pl Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/perl -pi
# Copyright 2015 OpenMarket 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.
$copyright = <<EOT;
/* Copyright 2015 OpenMarket 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.
*/
EOT
s/^(# -\*- coding: utf-8 -\*-\n)?/$1$copyright/ if ($. == 1);

View file

@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
""" This is a reference implementation of a synapse home server. """ This is a reference implementation of a Matrix home server.
""" """
__version__ = "0.6.0" __version__ = "0.6.1b"

View file

@ -18,6 +18,8 @@ from synapse.storage import prepare_database, UpgradeDatabaseException
from synapse.server import HomeServer from synapse.server import HomeServer
from synapse.python_dependencies import check_requirements
from twisted.internet import reactor from twisted.internet import reactor
from twisted.application import service from twisted.application import service
from twisted.enterprise import adbapi from twisted.enterprise import adbapi
@ -40,6 +42,8 @@ from synapse.util.logcontext import LoggingContext
from daemonize import Daemonize from daemonize import Daemonize
import twisted.manhole.telnet import twisted.manhole.telnet
import synapse
import logging import logging
import os import os
import re import re
@ -199,7 +203,10 @@ def setup(config_options, should_run=True):
config.setup_logging() config.setup_logging()
check_requirements()
logger.info("Server hostname: %s", config.server_name) logger.info("Server hostname: %s", config.server_name)
logger.info("Server version: %s", synapse.__version__)
if re.search(":[0-9]+$", config.server_name): if re.search(":[0-9]+$", config.server_name):
domain_with_port = config.server_name domain_with_port = config.server_name
@ -235,13 +242,20 @@ def setup(config_options, should_run=True):
except UpgradeDatabaseException: except UpgradeDatabaseException:
sys.stderr.write( sys.stderr.write(
"\nFailed to upgrade database.\n" "\nFailed to upgrade database.\n"
"Have you checked for version specific instructions in UPGRADES.rst?\n" "Have you checked for version specific instructions in"
" UPGRADES.rst?\n"
) )
sys.exit(1) sys.exit(1)
logger.info("Database prepared in %s.", db_name) logger.info("Database prepared in %s.", db_name)
hs.get_db_pool() db_pool = hs.get_db_pool()
if db_name == ":memory:":
# Memory databases will need to be setup each time they are opened.
reactor.callWhenRunning(
db_pool.runWithConnection, prepare_database
)
if config.manhole: if config.manhole:
f = twisted.manhole.telnet.ShellFactory() f = twisted.manhole.telnet.ShellFactory()
@ -292,6 +306,7 @@ def run():
def main(): def main():
with LoggingContext("main"): with LoggingContext("main"):
check_requirements()
setup(sys.argv[1:]) setup(sys.argv[1:])

View file

@ -20,7 +20,10 @@ import os
class DatabaseConfig(Config): class DatabaseConfig(Config):
def __init__(self, args): def __init__(self, args):
super(DatabaseConfig, self).__init__(args) super(DatabaseConfig, self).__init__(args)
self.database_path = self.abspath(args.database_path) if args.database_path == ":memory:":
self.database_path = ":memory:"
else:
self.database_path = self.abspath(args.database_path)
@classmethod @classmethod
def add_arguments(cls, parser): def add_arguments(cls, parser):

View file

@ -66,7 +66,10 @@ class LoggingConfig(Config):
formatter = logging.Formatter(log_format) formatter = logging.Formatter(log_format)
if self.log_file: if self.log_file:
handler = logging.FileHandler(self.log_file) # TODO: Customisable file size / backup count
handler = logging.handlers.RotatingFileHandler(
self.log_file, maxBytes=(1000 * 1000 * 100), backupCount=3
)
else: else:
handler = logging.StreamHandler() handler = logging.StreamHandler()
handler.setFormatter(formatter) handler.setFormatter(formatter)

View file

@ -47,8 +47,12 @@ class ServerConfig(Config):
def add_arguments(cls, parser): def add_arguments(cls, parser):
super(ServerConfig, cls).add_arguments(parser) super(ServerConfig, cls).add_arguments(parser)
server_group = parser.add_argument_group("server") server_group = parser.add_argument_group("server")
server_group.add_argument("-H", "--server-name", default="localhost", server_group.add_argument(
help="The name of the server") "-H", "--server-name", default="localhost",
help="The domain name of the server, with optional explicit port. "
"This is used by remote servers to connect to this server, "
"e.g. matrix.org, localhost:8080, etc."
)
server_group.add_argument("--signing-key-path", server_group.add_argument("--signing-key-path",
help="The signing key to sign messages with") help="The signing key to sign messages with")
server_group.add_argument("-p", "--bind-port", metavar="PORT", server_group.add_argument("-p", "--bind-port", metavar="PORT",

View file

@ -33,12 +33,6 @@ class EventBuilder(EventBase):
unsigned=unsigned unsigned=unsigned
) )
def update_event_key(self, key, value):
self._event_dict[key] = value
def update_event_keys(self, other_dict):
self._event_dict.update(other_dict)
def build(self): def build(self):
return FrozenEvent.from_event(self) return FrozenEvent.from_event(self)

View file

@ -89,16 +89,24 @@ def prune_event(event):
return type(event)(allowed_fields) return type(event)(allowed_fields)
def serialize_event(hs, e): def serialize_event(hs, e, client_event=True):
# FIXME(erikj): To handle the case of presence events and the like # FIXME(erikj): To handle the case of presence events and the like
if not isinstance(e, EventBase): if not isinstance(e, EventBase):
return e return e
# Should this strip out None's? # Should this strip out None's?
d = {k: v for k, v in e.get_dict().items()} d = {k: v for k, v in e.get_dict().items()}
if not client_event:
# set the age and keep all other keys
if "age_ts" in d["unsigned"]:
now = int(hs.get_clock().time_msec())
d["unsigned"]["age"] = now - d["unsigned"]["age_ts"]
return d
if "age_ts" in d["unsigned"]: if "age_ts" in d["unsigned"]:
now = int(hs.get_clock().time_msec()) now = int(hs.get_clock().time_msec())
d["unsigned"]["age"] = now - d["unsigned"]["age_ts"] d["age"] = now - d["unsigned"]["age_ts"]
del d["unsigned"]["age_ts"] del d["unsigned"]["age_ts"]
d["user_id"] = d.pop("sender", None) d["user_id"] = d.pop("sender", None)

View file

@ -256,23 +256,21 @@ class ReplicationLayer(object):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def get_state_for_context(self, destination, context, event_id): def get_state_for_room(self, destination, room_id, event_id):
"""Requests all of the `current` state PDUs for a given context from """Requests all of the `current` state PDUs for a given room from
a remote home server. a remote home server.
Args: Args:
destination (str): The remote homeserver to query for the state. destination (str): The remote homeserver to query for the state.
context (str): The context we're interested in. room_id (str): The id of the room we're interested in.
event_id (str): The id of the event we want the state at. event_id (str): The id of the event we want the state at.
Returns: Returns:
Deferred: Results in a list of PDUs. Deferred: Results in a list of PDUs.
""" """
result = yield self.transport_layer.get_context_state( result = yield self.transport_layer.get_room_state(
destination, destination, room_id, event_id=event_id,
context,
event_id=event_id,
) )
pdus = [ pdus = [
@ -288,9 +286,9 @@ class ReplicationLayer(object):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def get_event_auth(self, destination, context, event_id): def get_event_auth(self, destination, room_id, event_id):
res = yield self.transport_layer.get_event_auth( res = yield self.transport_layer.get_event_auth(
destination, context, event_id, destination, room_id, event_id,
) )
auth_chain = [ auth_chain = [
@ -304,9 +302,9 @@ class ReplicationLayer(object):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def on_backfill_request(self, origin, context, versions, limit): def on_backfill_request(self, origin, room_id, versions, limit):
pdus = yield self.handler.on_backfill_request( pdus = yield self.handler.on_backfill_request(
origin, context, versions, limit origin, room_id, versions, limit
) )
defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict())) defer.returnValue((200, self._transaction_from_pdus(pdus).get_dict()))
@ -380,12 +378,10 @@ class ReplicationLayer(object):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def on_context_state_request(self, origin, context, event_id): def on_context_state_request(self, origin, room_id, event_id):
if event_id: if event_id:
pdus = yield self.handler.get_state_for_pdu( pdus = yield self.handler.get_state_for_pdu(
origin, origin, room_id, event_id,
context,
event_id,
) )
auth_chain = yield self.store.get_auth_chain( auth_chain = yield self.store.get_auth_chain(
[pdu.event_id for pdu in pdus] [pdu.event_id for pdu in pdus]
@ -413,7 +409,7 @@ class ReplicationLayer(object):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def on_pull_request(self, origin, versions): def on_pull_request(self, origin, versions):
raise NotImplementedError("Pull transacions not implemented") raise NotImplementedError("Pull transactions not implemented")
@defer.inlineCallbacks @defer.inlineCallbacks
def on_query_request(self, query_type, args): def on_query_request(self, query_type, args):
@ -422,30 +418,21 @@ class ReplicationLayer(object):
defer.returnValue((200, response)) defer.returnValue((200, response))
else: else:
defer.returnValue( defer.returnValue(
(404, "No handler for Query type '%s'" % (query_type, )) (404, "No handler for Query type '%s'" % (query_type,))
) )
@defer.inlineCallbacks @defer.inlineCallbacks
def on_make_join_request(self, context, user_id): def on_make_join_request(self, room_id, user_id):
pdu = yield self.handler.on_make_join_request(context, user_id) pdu = yield self.handler.on_make_join_request(room_id, user_id)
time_now = self._clock.time_msec() time_now = self._clock.time_msec()
defer.returnValue({ defer.returnValue({"event": pdu.get_pdu_json(time_now)})
"event": pdu.get_pdu_json(time_now),
})
@defer.inlineCallbacks @defer.inlineCallbacks
def on_invite_request(self, origin, content): def on_invite_request(self, origin, content):
pdu = self.event_from_pdu_json(content) pdu = self.event_from_pdu_json(content)
ret_pdu = yield self.handler.on_invite_request(origin, pdu) ret_pdu = yield self.handler.on_invite_request(origin, pdu)
time_now = self._clock.time_msec() time_now = self._clock.time_msec()
defer.returnValue( defer.returnValue((200, {"event": ret_pdu.get_pdu_json(time_now)}))
(
200,
{
"event": ret_pdu.get_pdu_json(time_now),
}
)
)
@defer.inlineCallbacks @defer.inlineCallbacks
def on_send_join_request(self, origin, content): def on_send_join_request(self, origin, content):
@ -462,26 +449,17 @@ class ReplicationLayer(object):
})) }))
@defer.inlineCallbacks @defer.inlineCallbacks
def on_event_auth(self, origin, context, event_id): def on_event_auth(self, origin, room_id, event_id):
time_now = self._clock.time_msec() time_now = self._clock.time_msec()
auth_pdus = yield self.handler.on_event_auth(event_id) auth_pdus = yield self.handler.on_event_auth(event_id)
defer.returnValue( defer.returnValue((200, {
( "auth_chain": [a.get_pdu_json(time_now) for a in auth_pdus],
200, }))
{
"auth_chain": [
a.get_pdu_json(time_now) for a in auth_pdus
],
}
)
)
@defer.inlineCallbacks @defer.inlineCallbacks
def make_join(self, destination, context, user_id): def make_join(self, destination, room_id, user_id):
ret = yield self.transport_layer.make_join( ret = yield self.transport_layer.make_join(
destination=destination, destination, room_id, user_id
context=context,
user_id=user_id,
) )
pdu_dict = ret["event"] pdu_dict = ret["event"]
@ -494,10 +472,10 @@ class ReplicationLayer(object):
def send_join(self, destination, pdu): def send_join(self, destination, pdu):
time_now = self._clock.time_msec() time_now = self._clock.time_msec()
_, content = yield self.transport_layer.send_join( _, content = yield self.transport_layer.send_join(
destination, destination=destination,
pdu.room_id, room_id=pdu.room_id,
pdu.event_id, event_id=pdu.event_id,
pdu.get_pdu_json(time_now), content=pdu.get_pdu_json(time_now),
) )
logger.debug("Got content: %s", content) logger.debug("Got content: %s", content)
@ -507,9 +485,6 @@ class ReplicationLayer(object):
for p in content.get("state", []) for p in content.get("state", [])
] ]
# FIXME: We probably want to do something with the auth_chain given
# to us
auth_chain = [ auth_chain = [
self.event_from_pdu_json(p, outlier=True) self.event_from_pdu_json(p, outlier=True)
for p in content.get("auth_chain", []) for p in content.get("auth_chain", [])
@ -523,11 +498,11 @@ class ReplicationLayer(object):
}) })
@defer.inlineCallbacks @defer.inlineCallbacks
def send_invite(self, destination, context, event_id, pdu): def send_invite(self, destination, room_id, event_id, pdu):
time_now = self._clock.time_msec() time_now = self._clock.time_msec()
code, content = yield self.transport_layer.send_invite( code, content = yield self.transport_layer.send_invite(
destination=destination, destination=destination,
context=context, room_id=room_id,
event_id=event_id, event_id=event_id,
content=pdu.get_pdu_json(time_now), content=pdu.get_pdu_json(time_now),
) )
@ -657,7 +632,7 @@ class ReplicationLayer(object):
"_handle_new_pdu getting state for %s", "_handle_new_pdu getting state for %s",
pdu.room_id pdu.room_id
) )
state, auth_chain = yield self.get_state_for_context( state, auth_chain = yield self.get_state_for_room(
origin, pdu.room_id, pdu.event_id, origin, pdu.room_id, pdu.event_id,
) )
@ -816,7 +791,7 @@ class _TransactionQueue(object):
logger.info("TX [%s] is ready for retry", destination) logger.info("TX [%s] is ready for retry", destination)
logger.info("TX [%s] _attempt_new_transaction", destination) logger.info("TX [%s] _attempt_new_transaction", destination)
if destination in self.pending_transactions: if destination in self.pending_transactions:
# XXX: pending_transactions can get stuck on by a never-ending # XXX: pending_transactions can get stuck on by a never-ending
# request at which point pending_pdus_by_dest just keeps growing. # request at which point pending_pdus_by_dest just keeps growing.
@ -830,14 +805,15 @@ class _TransactionQueue(object):
pending_failures = self.pending_failures_by_dest.pop(destination, []) pending_failures = self.pending_failures_by_dest.pop(destination, [])
if pending_pdus: if pending_pdus:
logger.info("TX [%s] len(pending_pdus_by_dest[dest]) = %d", destination, len(pending_pdus)) logger.info("TX [%s] len(pending_pdus_by_dest[dest]) = %d",
destination, len(pending_pdus))
if not pending_pdus and not pending_edus and not pending_failures: if not pending_pdus and not pending_edus and not pending_failures:
return return
logger.debug( logger.debug(
"TX [%s] Attempting new transaction " "TX [%s] Attempting new transaction"
"(pdus: %d, edus: %d, failures: %d)", " (pdus: %d, edus: %d, failures: %d)",
destination, destination,
len(pending_pdus), len(pending_pdus),
len(pending_edus), len(pending_edus),

View file

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
# Copyright 2014, 2015 OpenMarket 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.
"""The transport layer is responsible for both sending transactions to remote
home servers and receiving a variety of requests from other home servers.
By default this is done over HTTPS (and all home servers are required to
support HTTPS), however individual pairings of servers may decide to
communicate over a different (albeit still reliable) protocol.
"""
from .server import TransportLayerServer
from .client import TransportLayerClient
class TransportLayer(TransportLayerServer, TransportLayerClient):
"""This is a basic implementation of the transport layer that translates
transactions and other requests to/from HTTP.
Attributes:
server_name (str): Local home server host
server (synapse.http.server.HttpServer): the http server to
register listeners on
client (synapse.http.client.HttpClient): the http client used to
send requests
request_handler (TransportRequestHandler): The handler to fire when we
receive requests for data.
received_handler (TransportReceivedHandler): The handler to fire when
we receive data.
"""
def __init__(self, homeserver, server_name, server, client):
"""
Args:
server_name (str): Local home server host
server (synapse.protocol.http.HttpServer): the http server to
register listeners on
client (synapse.protocol.http.HttpClient): the http client used to
send requests
"""
self.keyring = homeserver.get_keyring()
self.server_name = server_name
self.server = server
self.client = client
self.request_handler = None
self.received_handler = None

View file

@ -0,0 +1,215 @@
# -*- coding: utf-8 -*-
# Copyright 2014, 2015 OpenMarket 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.
from twisted.internet import defer
from synapse.api.urls import FEDERATION_PREFIX as PREFIX
from synapse.util.logutils import log_function
import logging
import json
logger = logging.getLogger(__name__)
class TransportLayerClient(object):
"""Sends federation HTTP requests to other servers"""
@log_function
def get_room_state(self, destination, room_id, event_id):
""" Requests all state for a given room from the given server at the
given event.
Args:
destination (str): The host name of the remote home server we want
to get the state from.
context (str): The name of the context we want the state of
event_id (str): The event we want the context at.
Returns:
Deferred: Results in a dict received from the remote homeserver.
"""
logger.debug("get_room_state dest=%s, room=%s",
destination, room_id)
path = PREFIX + "/state/%s/" % room_id
return self.client.get_json(
destination, path=path, args={"event_id": event_id},
)
@log_function
def get_event(self, destination, event_id):
""" Requests the pdu with give id and origin from the given server.
Args:
destination (str): The host name of the remote home server we want
to get the state from.
event_id (str): The id of the event being requested.
Returns:
Deferred: Results in a dict received from the remote homeserver.
"""
logger.debug("get_pdu dest=%s, event_id=%s",
destination, event_id)
path = PREFIX + "/event/%s/" % (event_id, )
return self.client.get_json(destination, path=path)
@log_function
def backfill(self, destination, room_id, event_tuples, limit):
""" Requests `limit` previous PDUs in a given context before list of
PDUs.
Args:
dest (str)
room_id (str)
event_tuples (list)
limt (int)
Returns:
Deferred: Results in a dict received from the remote homeserver.
"""
logger.debug(
"backfill dest=%s, room_id=%s, event_tuples=%s, limit=%s",
destination, room_id, repr(event_tuples), str(limit)
)
if not event_tuples:
# TODO: raise?
return
path = PREFIX + "/backfill/%s/" % (room_id,)
args = {
"v": event_tuples,
"limit": [str(limit)],
}
return self.client.get_json(
destination,
path=path,
args=args,
)
@defer.inlineCallbacks
@log_function
def send_transaction(self, transaction, json_data_callback=None):
""" Sends the given Transaction to its destination
Args:
transaction (Transaction)
Returns:
Deferred: Results of the deferred is a tuple in the form of
(response_code, response_body) where the response_body is a
python dict decoded from json
"""
logger.debug(
"send_data dest=%s, txid=%s",
transaction.destination, transaction.transaction_id
)
if transaction.destination == self.server_name:
raise RuntimeError("Transport layer cannot send to itself!")
# FIXME: This is only used by the tests. The actual json sent is
# generated by the json_data_callback.
json_data = transaction.get_dict()
code, response = yield self.client.put_json(
transaction.destination,
path=PREFIX + "/send/%s/" % transaction.transaction_id,
data=json_data,
json_data_callback=json_data_callback,
)
logger.debug(
"send_data dest=%s, txid=%s, got response: %d",
transaction.destination, transaction.transaction_id, code
)
defer.returnValue((code, response))
@defer.inlineCallbacks
@log_function
def make_query(self, destination, query_type, args, retry_on_dns_fail):
path = PREFIX + "/query/%s" % query_type
response = yield self.client.get_json(
destination=destination,
path=path,
args=args,
retry_on_dns_fail=retry_on_dns_fail,
)
defer.returnValue(response)
@defer.inlineCallbacks
@log_function
def make_join(self, destination, room_id, user_id, retry_on_dns_fail=True):
path = PREFIX + "/make_join/%s/%s" % (room_id, user_id)
response = yield self.client.get_json(
destination=destination,
path=path,
retry_on_dns_fail=retry_on_dns_fail,
)
defer.returnValue(response)
@defer.inlineCallbacks
@log_function
def send_join(self, destination, room_id, event_id, content):
path = PREFIX + "/send_join/%s/%s" % (room_id, event_id)
code, content = yield self.client.put_json(
destination=destination,
path=path,
data=content,
)
if not 200 <= code < 300:
raise RuntimeError("Got %d from send_join", code)
defer.returnValue(json.loads(content))
@defer.inlineCallbacks
@log_function
def send_invite(self, destination, room_id, event_id, content):
path = PREFIX + "/invite/%s/%s" % (room_id, event_id)
code, content = yield self.client.put_json(
destination=destination,
path=path,
data=content,
)
if not 200 <= code < 300:
raise RuntimeError("Got %d from send_invite", code)
defer.returnValue(json.loads(content))
@defer.inlineCallbacks
@log_function
def get_event_auth(self, destination, room_id, event_id):
path = PREFIX + "/event_auth/%s/%s" % (room_id, event_id)
response = yield self.client.get_json(
destination=destination,
path=path,
)
defer.returnValue(response)

View file

@ -13,14 +13,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
"""The transport layer is responsible for both sending transactions to remote
home servers and receiving a variety of requests from other home servers.
Typically, this is done over HTTP (and all home servers are required to
support HTTP), however individual pairings of servers may decide to communicate
over a different (albeit still reliable) protocol.
"""
from twisted.internet import defer from twisted.internet import defer
from synapse.api.urls import FEDERATION_PREFIX as PREFIX from synapse.api.urls import FEDERATION_PREFIX as PREFIX
@ -35,241 +27,8 @@ import re
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class TransportLayer(object): class TransportLayerServer(object):
"""This is a basic implementation of the transport layer that translates """Handles incoming federation HTTP requests"""
transactions and other requests to/from HTTP.
Attributes:
server_name (str): Local home server host
server (synapse.http.server.HttpServer): the http server to
register listeners on
client (synapse.http.client.HttpClient): the http client used to
send requests
request_handler (TransportRequestHandler): The handler to fire when we
receive requests for data.
received_handler (TransportReceivedHandler): The handler to fire when
we receive data.
"""
def __init__(self, homeserver, server_name, server, client):
"""
Args:
server_name (str): Local home server host
server (synapse.protocol.http.HttpServer): the http server to
register listeners on
client (synapse.protocol.http.HttpClient): the http client used to
send requests
"""
self.keyring = homeserver.get_keyring()
self.server_name = server_name
self.server = server
self.client = client
self.request_handler = None
self.received_handler = None
@log_function
def get_context_state(self, destination, context, event_id=None):
""" Requests all state for a given context (i.e. room) from the
given server.
Args:
destination (str): The host name of the remote home server we want
to get the state from.
context (str): The name of the context we want the state of
Returns:
Deferred: Results in a dict received from the remote homeserver.
"""
logger.debug("get_context_state dest=%s, context=%s",
destination, context)
subpath = "/state/%s/" % context
args = {}
if event_id:
args["event_id"] = event_id
return self._do_request_for_transaction(
destination, subpath, args=args
)
@log_function
def get_event(self, destination, event_id):
""" Requests the pdu with give id and origin from the given server.
Args:
destination (str): The host name of the remote home server we want
to get the state from.
event_id (str): The id of the event being requested.
Returns:
Deferred: Results in a dict received from the remote homeserver.
"""
logger.debug("get_pdu dest=%s, event_id=%s",
destination, event_id)
subpath = "/event/%s/" % (event_id, )
return self._do_request_for_transaction(destination, subpath)
@log_function
def backfill(self, dest, context, event_tuples, limit):
""" Requests `limit` previous PDUs in a given context before list of
PDUs.
Args:
dest (str)
context (str)
event_tuples (list)
limt (int)
Returns:
Deferred: Results in a dict received from the remote homeserver.
"""
logger.debug(
"backfill dest=%s, context=%s, event_tuples=%s, limit=%s",
dest, context, repr(event_tuples), str(limit)
)
if not event_tuples:
# TODO: raise?
return
subpath = "/backfill/%s/" % (context,)
args = {
"v": event_tuples,
"limit": [str(limit)],
}
return self._do_request_for_transaction(
dest,
subpath,
args=args,
)
@defer.inlineCallbacks
@log_function
def send_transaction(self, transaction, json_data_callback=None):
""" Sends the given Transaction to its destination
Args:
transaction (Transaction)
Returns:
Deferred: Results of the deferred is a tuple in the form of
(response_code, response_body) where the response_body is a
python dict decoded from json
"""
logger.debug(
"send_data dest=%s, txid=%s",
transaction.destination, transaction.transaction_id
)
if transaction.destination == self.server_name:
raise RuntimeError("Transport layer cannot send to itself!")
# FIXME: This is only used by the tests. The actual json sent is
# generated by the json_data_callback.
json_data = transaction.get_dict()
code, response = yield self.client.put_json(
transaction.destination,
path=PREFIX + "/send/%s/" % transaction.transaction_id,
data=json_data,
json_data_callback=json_data_callback,
)
logger.debug(
"send_data dest=%s, txid=%s, got response: %d",
transaction.destination, transaction.transaction_id, code
)
defer.returnValue((code, response))
@defer.inlineCallbacks
@log_function
def make_query(self, destination, query_type, args, retry_on_dns_fail):
path = PREFIX + "/query/%s" % query_type
response = yield self.client.get_json(
destination=destination,
path=path,
args=args,
retry_on_dns_fail=retry_on_dns_fail,
)
defer.returnValue(response)
@defer.inlineCallbacks
@log_function
def make_join(self, destination, context, user_id, retry_on_dns_fail=True):
path = PREFIX + "/make_join/%s/%s" % (context, user_id,)
response = yield self.client.get_json(
destination=destination,
path=path,
retry_on_dns_fail=retry_on_dns_fail,
)
defer.returnValue(response)
@defer.inlineCallbacks
@log_function
def send_join(self, destination, context, event_id, content):
path = PREFIX + "/send_join/%s/%s" % (
context,
event_id,
)
code, content = yield self.client.put_json(
destination=destination,
path=path,
data=content,
)
if not 200 <= code < 300:
raise RuntimeError("Got %d from send_join", code)
defer.returnValue(json.loads(content))
@defer.inlineCallbacks
@log_function
def send_invite(self, destination, context, event_id, content):
path = PREFIX + "/invite/%s/%s" % (
context,
event_id,
)
code, content = yield self.client.put_json(
destination=destination,
path=path,
data=content,
)
if not 200 <= code < 300:
raise RuntimeError("Got %d from send_invite", code)
defer.returnValue(json.loads(content))
@defer.inlineCallbacks
@log_function
def get_event_auth(self, destination, context, event_id):
path = PREFIX + "/event_auth/%s/%s" % (
context,
event_id,
)
response = yield self.client.get_json(
destination=destination,
path=path,
)
defer.returnValue(response)
@defer.inlineCallbacks @defer.inlineCallbacks
def _authenticate_request(self, request): def _authenticate_request(self, request):
@ -373,8 +132,6 @@ class TransportLayer(object):
""" """
self.request_handler = handler self.request_handler = handler
# TODO(markjh): Namespace the federation URI paths
# This is for when someone asks us for everything since version X # This is for when someone asks us for everything since version X
self.server.register_path( self.server.register_path(
"GET", "GET",
@ -528,34 +285,6 @@ class TransportLayer(object):
defer.returnValue((code, response)) defer.returnValue((code, response))
@defer.inlineCallbacks
@log_function
def _do_request_for_transaction(self, destination, subpath, args={}):
"""
Args:
destination (str)
path (str)
args (dict): This is parsed directly to the HttpClient.
Returns:
Deferred: Results in a dict.
"""
data = yield self.client.get_json(
destination,
path=PREFIX + subpath,
args=args,
)
# Add certain keys to the JSON, ready for decoding as a Transaction
data.update(
origin=destination,
destination=self.server_name,
transaction_id=None
)
defer.returnValue(data)
@log_function @log_function
def _on_backfill_request(self, origin, context, v_list, limits): def _on_backfill_request(self, origin, context, v_list, limits):
if not limits: if not limits:

View file

@ -144,7 +144,5 @@ class BaseHandler(object):
yield self.notifier.on_new_room_event(event, extra_users=extra_users) yield self.notifier.on_new_room_event(event, extra_users=extra_users)
yield federation_handler.handle_new_event( yield federation_handler.handle_new_event(
event, event, destinations=destinations,
None,
destinations=destinations,
) )

View file

@ -46,7 +46,8 @@ class EventStreamHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def get_stream(self, auth_user_id, pagin_config, timeout=0): def get_stream(self, auth_user_id, pagin_config, timeout=0,
as_client_event=True):
auth_user = self.hs.parse_userid(auth_user_id) auth_user = self.hs.parse_userid(auth_user_id)
try: try:
@ -69,16 +70,16 @@ class EventStreamHandler(BaseHandler):
pagin_config.from_token = None pagin_config.from_token = None
rm_handler = self.hs.get_handlers().room_member_handler rm_handler = self.hs.get_handlers().room_member_handler
logger.debug("BETA")
room_ids = yield rm_handler.get_rooms_for_user(auth_user) room_ids = yield rm_handler.get_rooms_for_user(auth_user)
logger.debug("ALPHA")
with PreserveLoggingContext(): with PreserveLoggingContext():
events, tokens = yield self.notifier.get_events_for( events, tokens = yield self.notifier.get_events_for(
auth_user, room_ids, pagin_config, timeout auth_user, room_ids, pagin_config, timeout
) )
chunks = [self.hs.serialize_event(e) for e in events] chunks = [
self.hs.serialize_event(e, as_client_event) for e in events
]
chunk = { chunk = {
"chunk": chunks, "chunk": chunks,

View file

@ -75,14 +75,14 @@ class FederationHandler(BaseHandler):
@log_function @log_function
@defer.inlineCallbacks @defer.inlineCallbacks
def handle_new_event(self, event, snapshot, destinations): def handle_new_event(self, event, destinations):
""" Takes in an event from the client to server side, that has already """ Takes in an event from the client to server side, that has already
been authed and handled by the state module, and sends it to any been authed and handled by the state module, and sends it to any
remote home servers that may be interested. remote home servers that may be interested.
Args: Args:
event event: The event to send
snapshot (.storage.Snapshot): THe snapshot the event happened after destinations: A list of destinations to send it to
Returns: Returns:
Deferred: Resolved when it has successfully been queued for Deferred: Resolved when it has successfully been queued for
@ -154,7 +154,7 @@ class FederationHandler(BaseHandler):
replication = self.replication_layer replication = self.replication_layer
if not state: if not state:
state, auth_chain = yield replication.get_state_for_context( state, auth_chain = yield replication.get_state_for_room(
origin, context=event.room_id, event_id=event.event_id, origin, context=event.room_id, event_id=event.event_id,
) )
@ -281,7 +281,7 @@ class FederationHandler(BaseHandler):
""" """
pdu = yield self.replication_layer.send_invite( pdu = yield self.replication_layer.send_invite(
destination=target_host, destination=target_host,
context=event.room_id, room_id=event.room_id,
event_id=event.event_id, event_id=event.event_id,
pdu=event pdu=event
) )
@ -617,13 +617,13 @@ class FederationHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
@log_function @log_function
def on_backfill_request(self, origin, context, pdu_list, limit): def on_backfill_request(self, origin, room_id, pdu_list, limit):
in_room = yield self.auth.check_host_in_room(context, origin) in_room = yield self.auth.check_host_in_room(room_id, origin)
if not in_room: if not in_room:
raise AuthError(403, "Host not in room.") raise AuthError(403, "Host not in room.")
events = yield self.store.get_backfill_events( events = yield self.store.get_backfill_events(
context, room_id,
pdu_list, pdu_list,
limit limit
) )

View file

@ -67,7 +67,7 @@ class MessageHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def get_messages(self, user_id=None, room_id=None, pagin_config=None, def get_messages(self, user_id=None, room_id=None, pagin_config=None,
feedback=False): feedback=False, as_client_event=True):
"""Get messages in a room. """Get messages in a room.
Args: Args:
@ -76,6 +76,7 @@ class MessageHandler(BaseHandler):
pagin_config (synapse.api.streams.PaginationConfig): The pagination pagin_config (synapse.api.streams.PaginationConfig): The pagination
config rules to apply, if any. config rules to apply, if any.
feedback (bool): True to get compressed feedback with the messages feedback (bool): True to get compressed feedback with the messages
as_client_event (bool): True to get events in client-server format.
Returns: Returns:
dict: Pagination API results dict: Pagination API results
""" """
@ -99,7 +100,9 @@ class MessageHandler(BaseHandler):
) )
chunk = { chunk = {
"chunk": [self.hs.serialize_event(e) for e in events], "chunk": [
self.hs.serialize_event(e, as_client_event) for e in events
],
"start": pagin_config.from_token.to_string(), "start": pagin_config.from_token.to_string(),
"end": next_token.to_string(), "end": next_token.to_string(),
} }
@ -211,7 +214,7 @@ class MessageHandler(BaseHandler):
@defer.inlineCallbacks @defer.inlineCallbacks
def snapshot_all_rooms(self, user_id=None, pagin_config=None, def snapshot_all_rooms(self, user_id=None, pagin_config=None,
feedback=False): feedback=False, as_client_event=True):
"""Retrieve a snapshot of all rooms the user is invited or has joined. """Retrieve a snapshot of all rooms the user is invited or has joined.
This snapshot may include messages for all rooms where the user is This snapshot may include messages for all rooms where the user is
@ -222,6 +225,7 @@ class MessageHandler(BaseHandler):
pagin_config (synapse.api.streams.PaginationConfig): The pagination pagin_config (synapse.api.streams.PaginationConfig): The pagination
config used to determine how many messages *PER ROOM* to return. config used to determine how many messages *PER ROOM* to return.
feedback (bool): True to get feedback along with these messages. feedback (bool): True to get feedback along with these messages.
as_client_event (bool): True to get events in client-server format.
Returns: Returns:
A list of dicts with "room_id" and "membership" keys for all rooms A list of dicts with "room_id" and "membership" keys for all rooms
the user is currently invited or joined in on. Rooms where the user the user is currently invited or joined in on. Rooms where the user
@ -280,7 +284,10 @@ class MessageHandler(BaseHandler):
end_token = now_token.copy_and_replace("room_key", token[1]) end_token = now_token.copy_and_replace("room_key", token[1])
d["messages"] = { d["messages"] = {
"chunk": [self.hs.serialize_event(m) for m in messages], "chunk": [
self.hs.serialize_event(m, as_client_event)
for m in messages
],
"start": start_token.to_string(), "start": start_token.to_string(),
"end": end_token.to_string(), "end": end_token.to_string(),
} }

View file

@ -425,10 +425,22 @@ class RoomMemberHandler(BaseHandler):
event.room_id, event.room_id,
self.hs.hostname self.hs.hostname
) )
if not is_host_in_room:
# is *anyone* in the room?
room_member_keys = [
v for (k, v) in context.current_state.keys() if (
k == "m.room.member"
)
]
if len(room_member_keys) == 0:
# has the room been created so we can join it?
create_event = context.current_state.get(("m.room.create", ""))
if create_event:
is_host_in_room = True
if is_host_in_room: if is_host_in_room:
should_do_dance = False should_do_dance = False
elif room_host: elif room_host: # TODO: Shouldn't this be remote_room_host?
should_do_dance = True should_do_dance = True
else: else:
# TODO(markjh): get prev_state from snapshot # TODO(markjh): get prev_state from snapshot
@ -442,7 +454,8 @@ class RoomMemberHandler(BaseHandler):
should_do_dance = not self.hs.is_mine(inviter) should_do_dance = not self.hs.is_mine(inviter)
room_host = inviter.domain room_host = inviter.domain
else: else:
should_do_dance = False # return the same error as join_room_alias does
raise SynapseError(404, "No known servers")
if should_do_dance: if should_do_dance:
handler = self.hs.get_handlers().federation_handler handler = self.hs.get_handlers().federation_handler

View file

@ -83,9 +83,15 @@ class TypingNotificationHandler(BaseHandler):
if member in self._member_typing_timer: if member in self._member_typing_timer:
self.clock.cancel_call_later(self._member_typing_timer[member]) self.clock.cancel_call_later(self._member_typing_timer[member])
def _cb():
logger.debug(
"%s has timed out in %s", target_user.to_string(), room_id
)
self._stopped_typing(member)
self._member_typing_until[member] = until self._member_typing_until[member] = until
self._member_typing_timer[member] = self.clock.call_later( self._member_typing_timer[member] = self.clock.call_later(
timeout / 1000, lambda: self._stopped_typing(member) timeout / 1000.0, _cb
) )
if was_present: if was_present:
@ -114,6 +120,10 @@ class TypingNotificationHandler(BaseHandler):
member = RoomMember(room_id=room_id, user=target_user) member = RoomMember(room_id=room_id, user=target_user)
if member in self._member_typing_timer:
self.clock.cancel_call_later(self._member_typing_timer[member])
del self._member_typing_timer[member]
yield self._stopped_typing(member) yield self._stopped_typing(member)
@defer.inlineCallbacks @defer.inlineCallbacks
@ -136,8 +146,10 @@ class TypingNotificationHandler(BaseHandler):
del self._member_typing_until[member] del self._member_typing_until[member]
self.clock.cancel_call_later(self._member_typing_timer[member]) if member in self._member_typing_timer:
del self._member_typing_timer[member] # Don't cancel it - either it already expired, or the real
# stopped_typing() will cancel it
del self._member_typing_timer[member]
@defer.inlineCallbacks @defer.inlineCallbacks
def _push_update(self, room_id, user, typing): def _push_update(self, room_id, user, typing):

View file

@ -53,7 +53,7 @@ class SimpleHttpClient(object):
uri.encode("ascii"), uri.encode("ascii"),
headers=Headers({ headers=Headers({
b"Content-Type": [b"application/x-www-form-urlencoded"], b"Content-Type": [b"application/x-www-form-urlencoded"],
b"User-Agent": AGENT_NAME, b"User-Agent": [AGENT_NAME],
}), }),
bodyProducer=FileBodyProducer(StringIO(query_bytes)) bodyProducer=FileBodyProducer(StringIO(query_bytes))
) )
@ -89,7 +89,7 @@ class SimpleHttpClient(object):
"GET", "GET",
uri.encode("ascii"), uri.encode("ascii"),
headers=Headers({ headers=Headers({
b"User-Agent": AGENT_NAME, b"User-Agent": [AGENT_NAME],
}) })
) )
@ -114,7 +114,7 @@ class CaptchaServerHttpClient(SimpleHttpClient):
bodyProducer=FileBodyProducer(StringIO(query_bytes)), bodyProducer=FileBodyProducer(StringIO(query_bytes)),
headers=Headers({ headers=Headers({
b"Content-Type": [b"application/x-www-form-urlencoded"], b"Content-Type": [b"application/x-www-form-urlencoded"],
b"User-Agent": AGENT_NAME, b"User-Agent": [AGENT_NAME],
}) })
) )

View file

@ -72,7 +72,6 @@ class MatrixFederationHttpClient(object):
requests. requests.
""" """
def __init__(self, hs): def __init__(self, hs):
self.hs = hs self.hs = hs
self.signing_key = hs.config.signing_key[0] self.signing_key = hs.config.signing_key[0]

View file

@ -22,7 +22,8 @@ except IOError as e:
if str(e).startswith("decoder jpeg not available"): if str(e).startswith("decoder jpeg not available"):
raise Exception( raise Exception(
"FATAL: jpeg codec not supported. Install pillow correctly! " "FATAL: jpeg codec not supported. Install pillow correctly! "
" 'sudo apt-get install libjpeg-dev' then 'pip install -I pillow'" " 'sudo apt-get install libjpeg-dev' then 'pip uninstall pillow &&"
" pip install pillow --user'"
) )
except Exception: except Exception:
# any other exception is fine # any other exception is fine
@ -36,7 +37,8 @@ except IOError as e:
if str(e).startswith("decoder zip not available"): if str(e).startswith("decoder zip not available"):
raise Exception( raise Exception(
"FATAL: zip codec not supported. Install pillow correctly! " "FATAL: zip codec not supported. Install pillow correctly! "
" 'sudo apt-get install libjpeg-dev' then 'pip install -I pillow'" " 'sudo apt-get install libjpeg-dev' then 'pip uninstall pillow &&"
" pip install pillow --user'"
) )
except Exception: except Exception:
# any other exception is fine # any other exception is fine

View file

@ -82,7 +82,7 @@ class Thumbnailer(object):
def save_image(self, output_image, output_type, output_path): def save_image(self, output_image, output_type, output_path):
output_bytes_io = BytesIO() output_bytes_io = BytesIO()
output_image.save(output_bytes_io, self.FORMATS[output_type]) output_image.save(output_bytes_io, self.FORMATS[output_type], quality=70)
output_bytes = output_bytes_io.getvalue() output_bytes = output_bytes_io.getvalue()
with open(output_path, "wb") as output_file: with open(output_path, "wb") as output_file:
output_file.write(output_bytes) output_file.write(output_bytes)

View file

@ -244,14 +244,14 @@ class Notifier(object):
) )
if timeout: if timeout:
self.clock.call_later(timeout/1000.0, _timeout_listener)
self._register_with_keys(listener) self._register_with_keys(listener)
yield self._check_for_updates(listener) yield self._check_for_updates(listener)
if not timeout: if not timeout:
_timeout_listener() _timeout_listener()
else:
self.clock.call_later(timeout/1000.0, _timeout_listener)
return return

View file

@ -0,0 +1,80 @@
import logging
from distutils.version import LooseVersion
logger = logging.getLogger(__name__)
REQUIREMENTS = {
"syutil==0.0.2": ["syutil"],
"matrix_angular_sdk==0.6.0": ["syweb==0.6.0"],
"Twisted>=14.0.0": ["twisted>=14.0.0"],
"service_identity>=1.0.0": ["service_identity>=1.0.0"],
"pyopenssl>=0.14": ["OpenSSL>=0.14"],
"pyyaml": ["yaml"],
"pyasn1": ["pyasn1"],
"pynacl": ["nacl"],
"daemonize": ["daemonize"],
"py-bcrypt": ["bcrypt"],
"frozendict>=0.4": ["frozendict"],
"pillow": ["PIL"],
}
class MissingRequirementError(Exception):
pass
def check_requirements():
"""Checks that all the modules needed by synapse have been correctly
installed and are at the correct version"""
for dependency, module_requirements in REQUIREMENTS.items():
for module_requirement in module_requirements:
if ">=" in module_requirement:
module_name, required_version = module_requirement.split(">=")
version_test = ">="
elif "==" in module_requirement:
module_name, required_version = module_requirement.split("==")
version_test = "=="
else:
module_name = module_requirement
version_test = None
try:
module = __import__(module_name)
except ImportError:
logging.exception(
"Can't import %r which is part of %r",
module_name, dependency
)
raise MissingRequirementError(
"Can't import %r which is part of %r"
% (module_name, dependency)
)
version = getattr(module, "__version__", None)
file_path = getattr(module, "__file__", None)
logger.info(
"Using %r version %r from %r to satisfy %r",
module_name, version, file_path, dependency
)
if version_test == ">=":
if version is None:
raise MissingRequirementError(
"Version of %r isn't set as __version__ of module %r"
% (dependency, module_name)
)
if LooseVersion(version) < LooseVersion(required_version):
raise MissingRequirementError(
"Version of %r in %r is too old. %r < %r"
% (dependency, file_path, version, required_version)
)
elif version_test == "==":
if version is None:
raise MissingRequirementError(
"Version of %r isn't set as __version__ of module %r"
% (dependency, module_name)
)
if LooseVersion(version) != LooseVersion(required_version):
raise MissingRequirementError(
"Unexpected version of %r in %r. %r != %r"
% (dependency, file_path, version, required_version)
)

View file

@ -44,8 +44,11 @@ class EventStreamRestServlet(RestServlet):
except ValueError: except ValueError:
raise SynapseError(400, "timeout must be in milliseconds.") raise SynapseError(400, "timeout must be in milliseconds.")
as_client_event = "raw" not in request.args
chunk = yield handler.get_stream( chunk = yield handler.get_stream(
auth_user.to_string(), pagin_config, timeout=timeout auth_user.to_string(), pagin_config, timeout=timeout,
as_client_event=as_client_event
) )
except: except:
logger.exception("Event stream failed") logger.exception("Event stream failed")

View file

@ -27,12 +27,15 @@ class InitialSyncRestServlet(RestServlet):
def on_GET(self, request): def on_GET(self, request):
user = yield self.auth.get_user_by_req(request) user = yield self.auth.get_user_by_req(request)
with_feedback = "feedback" in request.args with_feedback = "feedback" in request.args
as_client_event = "raw" not in request.args
pagination_config = PaginationConfig.from_request(request) pagination_config = PaginationConfig.from_request(request)
handler = self.handlers.message_handler handler = self.handlers.message_handler
content = yield handler.snapshot_all_rooms( content = yield handler.snapshot_all_rooms(
user_id=user.to_string(), user_id=user.to_string(),
pagin_config=pagination_config, pagin_config=pagination_config,
feedback=with_feedback) feedback=with_feedback,
as_client_event=as_client_event
)
defer.returnValue((200, content)) defer.returnValue((200, content))

View file

@ -246,7 +246,7 @@ class JoinRoomAliasServlet(RestServlet):
} }
) )
defer.returnValue((200, {})) defer.returnValue((200, {"room_id": identifier.to_string()}))
@defer.inlineCallbacks @defer.inlineCallbacks
def on_PUT(self, request, room_identifier, txn_id): def on_PUT(self, request, room_identifier, txn_id):
@ -314,12 +314,15 @@ class RoomMessageListRestServlet(RestServlet):
request, default_limit=10, request, default_limit=10,
) )
with_feedback = "feedback" in request.args with_feedback = "feedback" in request.args
as_client_event = "raw" not in request.args
handler = self.handlers.message_handler handler = self.handlers.message_handler
msgs = yield handler.get_messages( msgs = yield handler.get_messages(
room_id=room_id, room_id=room_id,
user_id=user.to_string(), user_id=user.to_string(),
pagin_config=pagination_config, pagin_config=pagination_config,
feedback=with_feedback) feedback=with_feedback,
as_client_event=as_client_event
)
defer.returnValue((200, msgs)) defer.returnValue((200, msgs))

View file

@ -149,8 +149,8 @@ class BaseHomeServer(object):
object.""" object."""
return EventID.from_string(s) return EventID.from_string(s)
def serialize_event(self, e): def serialize_event(self, e, as_client_event=True):
return serialize_event(self, e) return serialize_event(self, e, as_client_event)
def get_ip_from_request(self, request): def get_ip_from_request(self, request):
# May be an X-Forwarding-For header depending on config # May be an X-Forwarding-For header depending on config

View file

@ -58,13 +58,6 @@ class RoomStore(SQLBaseStore):
logger.error("store_room with room_id=%s failed: %s", room_id, e) logger.error("store_room with room_id=%s failed: %s", room_id, e)
raise StoreError(500, "Problem creating room.") raise StoreError(500, "Problem creating room.")
def store_room_config(self, room_id, visibility):
return self._simple_update_one(
table=RoomsTable.table_name,
keyvalues={"room_id": room_id},
updatevalues={"is_public": visibility}
)
def get_room(self, room_id): def get_room(self, room_id):
"""Retrieve a room. """Retrieve a room.

View file

@ -78,12 +78,6 @@ class StateStore(SQLBaseStore):
f, f,
) )
def store_state_groups(self, event):
return self.runInteraction(
"store_state_groups",
self._store_state_groups_txn, event
)
def _store_state_groups_txn(self, txn, event, context): def _store_state_groups_txn(self, txn, event, context):
if context.current_state is None: if context.current_state is None:
return return

View file

@ -39,6 +39,8 @@ from ._base import SQLBaseStore
from synapse.api.errors import SynapseError from synapse.api.errors import SynapseError
from synapse.util.logutils import log_function from synapse.util.logutils import log_function
from collections import namedtuple
import logging import logging
@ -52,91 +54,79 @@ _STREAM_TOKEN = "stream"
_TOPOLOGICAL_TOKEN = "topological" _TOPOLOGICAL_TOKEN = "topological"
def _parse_stream_token(string): class _StreamToken(namedtuple("_StreamToken", "topological stream")):
try: """Tokens are positions between events. The token "s1" comes after event 1.
if string[0] != 's':
raise
return int(string[1:])
except:
raise SynapseError(400, "Invalid token")
s0 s1
| |
[0] V [1] V [2]
def _parse_topological_token(string): Tokens can either be a point in the live event stream or a cursor going
try: through historic events.
if string[0] != 't':
raise
parts = string[1:].split('-', 1)
return (int(parts[0]), int(parts[1]))
except:
raise SynapseError(400, "Invalid token")
When traversing the live event stream events are ordered by when they
arrived at the homeserver.
def is_stream_token(string): When traversing historic events the events are ordered by their depth in
try: the event graph "topological_ordering" and then by when they arrived at the
_parse_stream_token(string) homeserver "stream_ordering".
return True
except:
return False
Live tokens start with an "s" followed by the "stream_ordering" id of the
event it comes after. Historic tokens start with a "t" followed by the
"topological_ordering" id of the event it comes after, follewed by "-",
followed by the "stream_ordering" id of the event it comes after.
"""
__slots__ = []
def is_topological_token(string): @classmethod
try: def parse(cls, string):
_parse_topological_token(string) try:
return True if string[0] == 's':
except: return cls(None, int(string[1:]))
return False if string[0] == 't':
parts = string[1:].split('-', 1)
return cls(int(parts[1]), int(parts[0]))
except:
pass
raise SynapseError(400, "Invalid token %r" % (string,))
@classmethod
def parse_stream_token(cls, string):
try:
if string[0] == 's':
return cls(None, int(string[1:]))
except:
pass
raise SynapseError(400, "Invalid token %r" % (string,))
def _get_token_bound(token, comparison): def __str__(self):
try: if self.topological is not None:
s = _parse_stream_token(token) return "t%d-%d" % (self.topological, self.stream)
return "%s %s %d" % ("stream_ordering", comparison, s) else:
except: return "s%d" % (self.stream,)
pass
try: def lower_bound(self):
top, stream = _parse_topological_token(token) if self.topological is None:
return "%s %s %d AND %s %s %d" % ( return "(%d < %s)" % (self.stream, "stream_ordering")
"topological_ordering", comparison, top, else:
"stream_ordering", comparison, stream, return "(%d < %s OR (%d == %s AND %d < %s))" % (
) self.topological, "topological_ordering",
except: self.topological, "topological_ordering",
pass self.stream, "stream_ordering",
)
raise SynapseError(400, "Invalid token") def upper_bound(self):
if self.topological is None:
return "(%d >= %s)" % (self.stream, "stream_ordering")
else:
return "(%d > %s OR (%d == %s AND %d >= %s))" % (
self.topological, "topological_ordering",
self.topological, "topological_ordering",
self.stream, "stream_ordering",
)
class StreamStore(SQLBaseStore): class StreamStore(SQLBaseStore):
@log_function
def get_room_events(self, user_id, from_key, to_key, room_id, limit=0,
direction='f', with_feedback=False):
# We deal with events request in two different ways depending on if
# this looks like an /events request or a pagination request.
is_events = (
direction == 'f'
and user_id
and is_stream_token(from_key)
and to_key and is_stream_token(to_key)
)
if is_events:
return self.get_room_events_stream(
user_id=user_id,
from_key=from_key,
to_key=to_key,
room_id=room_id,
limit=limit,
with_feedback=with_feedback,
)
else:
return self.paginate_room_events(
from_key=from_key,
to_key=to_key,
room_id=room_id,
limit=limit,
with_feedback=with_feedback,
)
@log_function @log_function
def get_room_events_stream(self, user_id, from_key, to_key, room_id, def get_room_events_stream(self, user_id, from_key, to_key, room_id,
limit=0, with_feedback=False): limit=0, with_feedback=False):
@ -162,8 +152,8 @@ class StreamStore(SQLBaseStore):
limit = MAX_STREAM_SIZE limit = MAX_STREAM_SIZE
# From and to keys should be integers from ordering. # From and to keys should be integers from ordering.
from_id = _parse_stream_token(from_key) from_id = _StreamToken.parse_stream_token(from_key)
to_id = _parse_stream_token(to_key) to_id = _StreamToken.parse_stream_token(to_key)
if from_key == to_key: if from_key == to_key:
return defer.succeed(([], to_key)) return defer.succeed(([], to_key))
@ -181,7 +171,7 @@ class StreamStore(SQLBaseStore):
} }
def f(txn): def f(txn):
txn.execute(sql, (user_id, user_id, from_id, to_id,)) txn.execute(sql, (user_id, user_id, from_id.stream, to_id.stream,))
rows = self.cursor_to_dict(txn) rows = self.cursor_to_dict(txn)
@ -211,17 +201,21 @@ class StreamStore(SQLBaseStore):
# Tokens really represent positions between elements, but we use # Tokens really represent positions between elements, but we use
# the convention of pointing to the event before the gap. Hence # the convention of pointing to the event before the gap. Hence
# we have a bit of asymmetry when it comes to equalities. # we have a bit of asymmetry when it comes to equalities.
from_comp = '<=' if direction == 'b' else '>'
to_comp = '>' if direction == 'b' else '<='
order = "DESC" if direction == 'b' else "ASC"
args = [room_id] args = [room_id]
if direction == 'b':
bounds = _get_token_bound(from_key, from_comp) order = "DESC"
if to_key: bounds = _StreamToken.parse(from_key).upper_bound()
bounds = "%s AND %s" % ( if to_key:
bounds, _get_token_bound(to_key, to_comp) bounds = "%s AND %s" % (
) bounds, _StreamToken.parse(to_key).lower_bound()
)
else:
order = "ASC"
bounds = _StreamToken.parse(from_key).lower_bound()
if to_key:
bounds = "%s AND %s" % (
bounds, _StreamToken.parse(to_key).upper_bound()
)
if int(limit) > 0: if int(limit) > 0:
args.append(int(limit)) args.append(int(limit))
@ -249,9 +243,13 @@ class StreamStore(SQLBaseStore):
topo = rows[-1]["topological_ordering"] topo = rows[-1]["topological_ordering"]
toke = rows[-1]["stream_ordering"] toke = rows[-1]["stream_ordering"]
if direction == 'b': if direction == 'b':
topo -= 1 # Tokens are positions between events.
# This token points *after* the last event in the chunk.
# We need it to point to the event before it in the chunk
# when we are going backwards so we subtract one from the
# stream part.
toke -= 1 toke -= 1
next_token = "t%s-%s" % (topo, toke) next_token = str(_StreamToken(topo, toke))
else: else:
# TODO (erikj): We should work out what to do here instead. # TODO (erikj): We should work out what to do here instead.
next_token = to_key if to_key else from_key next_token = to_key if to_key else from_key
@ -284,9 +282,14 @@ class StreamStore(SQLBaseStore):
rows.reverse() # As we selected with reverse ordering rows.reverse() # As we selected with reverse ordering
if rows: if rows:
# Tokens are positions between events.
# This token points *after* the last event in the chunk.
# We need it to point to the event before it in the chunk
# since we are going backwards so we subtract one from the
# stream part.
topo = rows[0]["topological_ordering"] topo = rows[0]["topological_ordering"]
toke = rows[0]["stream_ordering"] toke = rows[0]["stream_ordering"] - 1
start_token = "t%s-%s" % (topo, toke) start_token = str(_StreamToken(topo, toke))
token = (start_token, end_token) token = (start_token, end_token)
else: else:

View file

@ -59,23 +59,29 @@ class JustPresenceHandlers(object):
def __init__(self, hs): def __init__(self, hs):
self.presence_handler = PresenceHandler(hs) self.presence_handler = PresenceHandler(hs)
class PresenceStateTestCase(unittest.TestCase):
""" Tests presence management. """
class PresenceTestCase(unittest.TestCase):
@defer.inlineCallbacks @defer.inlineCallbacks
def setUp(self): def setUp(self):
db_pool = SQLiteMemoryDbPool() db_pool = SQLiteMemoryDbPool()
yield db_pool.prepare() yield db_pool.prepare()
self.clock = MockClock()
self.mock_config = NonCallableMock() self.mock_config = NonCallableMock()
self.mock_config.signing_key = [MockKey()] self.mock_config.signing_key = [MockKey()]
self.mock_federation_resource = MockHttpResource()
self.mock_http_client = Mock(spec=[])
self.mock_http_client.put_json = DeferredMockCallable()
hs = HomeServer("test", hs = HomeServer("test",
clock=MockClock(), clock=self.clock,
db_pool=db_pool, db_pool=db_pool,
handlers=None, handlers=None,
resource_for_federation=Mock(), resource_for_federation=self.mock_federation_resource,
http_client=None, http_client=self.mock_http_client,
config=self.mock_config, config=self.mock_config,
keyring=Mock(), keyring=Mock(),
) )
@ -92,24 +98,33 @@ class PresenceStateTestCase(unittest.TestCase):
self.u_banana = hs.parse_userid("@banana:test") self.u_banana = hs.parse_userid("@banana:test")
self.u_clementine = hs.parse_userid("@clementine:test") self.u_clementine = hs.parse_userid("@clementine:test")
yield self.store.create_presence(self.u_apple.localpart) for u in self.u_apple, self.u_banana, self.u_clementine:
yield self.store.create_presence(u.localpart)
yield self.store.set_presence_state( yield self.store.set_presence_state(
self.u_apple.localpart, {"state": ONLINE, "status_msg": "Online"} self.u_apple.localpart, {"state": ONLINE, "status_msg": "Online"}
) )
# ID of a local user that does not exist
self.u_durian = hs.parse_userid("@durian:test")
# A remote user
self.u_cabbage = hs.parse_userid("@cabbage:elsewhere")
self.handler = hs.get_handlers().presence_handler self.handler = hs.get_handlers().presence_handler
self.room_id = "a-room"
self.room_members = [] self.room_members = []
def get_rooms_for_user(user): def get_rooms_for_user(user):
if user in self.room_members: if user in self.room_members:
return defer.succeed(["a-room"]) return defer.succeed([self.room_id])
else: else:
return defer.succeed([]) return defer.succeed([])
room_member_handler.get_rooms_for_user = get_rooms_for_user room_member_handler.get_rooms_for_user = get_rooms_for_user
def get_room_members(room_id): def get_room_members(room_id):
if room_id == "a-room": if room_id == self.room_id:
return defer.succeed(self.room_members) return defer.succeed(self.room_members)
else: else:
return defer.succeed([]) return defer.succeed([])
@ -128,6 +143,10 @@ class PresenceStateTestCase(unittest.TestCase):
self.handler.start_polling_presence = self.mock_start self.handler.start_polling_presence = self.mock_start
self.handler.stop_polling_presence = self.mock_stop self.handler.stop_polling_presence = self.mock_stop
class PresenceStateTestCase(PresenceTestCase):
""" Tests presence management. """
@defer.inlineCallbacks @defer.inlineCallbacks
def test_get_my_state(self): def test_get_my_state(self):
state = yield self.handler.get_state( state = yield self.handler.get_state(
@ -206,56 +225,9 @@ class PresenceStateTestCase(unittest.TestCase):
self.mock_stop.assert_called_with(self.u_apple) self.mock_stop.assert_called_with(self.u_apple)
class PresenceInvitesTestCase(unittest.TestCase): class PresenceInvitesTestCase(PresenceTestCase):
""" Tests presence management. """ """ Tests presence management. """
@defer.inlineCallbacks
def setUp(self):
self.mock_http_client = Mock(spec=[])
self.mock_http_client.put_json = DeferredMockCallable()
self.mock_federation_resource = MockHttpResource()
db_pool = SQLiteMemoryDbPool()
yield db_pool.prepare()
self.mock_config = NonCallableMock()
self.mock_config.signing_key = [MockKey()]
hs = HomeServer("test",
clock=MockClock(),
db_pool=db_pool,
handlers=None,
resource_for_client=Mock(),
resource_for_federation=self.mock_federation_resource,
http_client=self.mock_http_client,
config=self.mock_config,
keyring=Mock(),
)
hs.handlers = JustPresenceHandlers(hs)
self.store = hs.get_datastore()
# Some local users to test with
self.u_apple = hs.parse_userid("@apple:test")
self.u_banana = hs.parse_userid("@banana:test")
yield self.store.create_presence(self.u_apple.localpart)
yield self.store.create_presence(self.u_banana.localpart)
# ID of a local user that does not exist
self.u_durian = hs.parse_userid("@durian:test")
# A remote user
self.u_cabbage = hs.parse_userid("@cabbage:elsewhere")
self.handler = hs.get_handlers().presence_handler
self.mock_start = Mock()
self.mock_stop = Mock()
self.handler.start_polling_presence = self.mock_start
self.handler.stop_polling_presence = self.mock_stop
@defer.inlineCallbacks @defer.inlineCallbacks
def test_invite_local(self): def test_invite_local(self):
# TODO(paul): This test will likely break if/when real auth permissions # TODO(paul): This test will likely break if/when real auth permissions
@ -558,24 +530,25 @@ class PresencePushTestCase(unittest.TestCase):
]) ])
self.room_member_handler = hs.handlers.room_member_handler self.room_member_handler = hs.handlers.room_member_handler
self.room_id = "a-room"
self.room_members = [] self.room_members = []
def get_rooms_for_user(user): def get_rooms_for_user(user):
if user in self.room_members: if user in self.room_members:
return defer.succeed(["a-room"]) return defer.succeed([self.room_id])
else: else:
return defer.succeed([]) return defer.succeed([])
self.room_member_handler.get_rooms_for_user = get_rooms_for_user self.room_member_handler.get_rooms_for_user = get_rooms_for_user
def get_room_members(room_id): def get_room_members(room_id):
if room_id == "a-room": if room_id == self.room_id:
return defer.succeed(self.room_members) return defer.succeed(self.room_members)
else: else:
return defer.succeed([]) return defer.succeed([])
self.room_member_handler.get_room_members = get_room_members self.room_member_handler.get_room_members = get_room_members
def get_room_hosts(room_id): def get_room_hosts(room_id):
if room_id == "a-room": if room_id == self.room_id:
hosts = set([u.domain for u in self.room_members]) hosts = set([u.domain for u in self.room_members])
return defer.succeed(hosts) return defer.succeed(hosts)
else: else:
@ -911,7 +884,7 @@ class PresencePushTestCase(unittest.TestCase):
) )
yield self.distributor.fire("user_joined_room", self.u_clementine, yield self.distributor.fire("user_joined_room", self.u_clementine,
"a-room" self.room_id
) )
self.room_members.append(self.u_clementine) self.room_members.append(self.u_clementine)
@ -974,7 +947,7 @@ class PresencePushTestCase(unittest.TestCase):
self.room_members = [self.u_apple, self.u_banana] self.room_members = [self.u_apple, self.u_banana]
yield self.distributor.fire("user_joined_room", self.u_potato, yield self.distributor.fire("user_joined_room", self.u_potato,
"a-room" self.room_id
) )
yield put_json.await_calls() yield put_json.await_calls()
@ -1003,7 +976,7 @@ class PresencePushTestCase(unittest.TestCase):
self.room_members.append(self.u_potato) self.room_members.append(self.u_potato)
yield self.distributor.fire("user_joined_room", self.u_clementine, yield self.distributor.fire("user_joined_room", self.u_clementine,
"a-room" self.room_id
) )
put_json.await_calls() put_json.await_calls()

View file

@ -223,7 +223,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
yield room_handler.change_membership(event, context) yield room_handler.change_membership(event, context)
self.federation.handle_new_event.assert_called_once_with( self.federation.handle_new_event.assert_called_once_with(
event, None, destinations=set() event, destinations=set()
) )
self.datastore.persist_event.assert_called_once_with( self.datastore.persist_event.assert_called_once_with(
@ -301,7 +301,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
yield room_handler.change_membership(event, context) yield room_handler.change_membership(event, context)
self.federation.handle_new_event.assert_called_once_with( self.federation.handle_new_event.assert_called_once_with(
event, None, destinations=set(['red']) event, destinations=set(['red'])
) )
self.datastore.persist_event.assert_called_once_with( self.datastore.persist_event.assert_called_once_with(

View file

@ -352,3 +352,29 @@ class TypingNotificationsTestCase(unittest.TestCase):
}}, }},
] ]
) )
# SYN-230 - see if we can still set after timeout
yield self.handler.started_typing(
target_user=self.u_apple,
auth_user=self.u_apple,
room_id=self.room_id,
timeout=10000,
)
self.on_new_user_event.assert_has_calls([
call(rooms=[self.room_id]),
])
self.on_new_user_event.reset_mock()
self.assertEquals(self.event_source.get_current_key(), 3)
self.assertEquals(
self.event_source.get_new_events_for_user(self.u_apple, 0, None)[0],
[
{"type": "m.typing",
"room_id": self.room_id,
"content": {
"user_ids": [self.u_apple.to_string()],
}},
]
)

View file

@ -294,7 +294,7 @@ class RoomPermissionsTestCase(RestTestCase):
# set [invite/join/left] of self, set [invite/join/left] of other, # set [invite/join/left] of self, set [invite/join/left] of other,
# expect all 403s # expect all 403s
for usr in [self.user_id, self.rmcreator_id]: for usr in [self.user_id, self.rmcreator_id]:
yield self.join(room=room, user=usr, expect_code=403) yield self.join(room=room, user=usr, expect_code=404)
yield self.leave(room=room, user=usr, expect_code=403) yield self.leave(room=room, user=usr, expect_code=403)
@defer.inlineCallbacks @defer.inlineCallbacks

View file

@ -21,7 +21,7 @@ from twisted.internet import defer
import synapse.rest.room import synapse.rest.room
from synapse.server import HomeServer from synapse.server import HomeServer
from ..utils import MockHttpResource, SQLiteMemoryDbPool, MockKey from ..utils import MockHttpResource, MockClock, SQLiteMemoryDbPool, MockKey
from .utils import RestTestCase from .utils import RestTestCase
from mock import Mock, NonCallableMock from mock import Mock, NonCallableMock
@ -36,6 +36,8 @@ class RoomTypingTestCase(RestTestCase):
@defer.inlineCallbacks @defer.inlineCallbacks
def setUp(self): def setUp(self):
self.clock = MockClock()
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX) self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
self.auth_user_id = self.user_id self.auth_user_id = self.user_id
@ -47,6 +49,7 @@ class RoomTypingTestCase(RestTestCase):
hs = HomeServer( hs = HomeServer(
"red", "red",
clock=self.clock,
db_pool=db_pool, db_pool=db_pool,
http_client=None, http_client=None,
replication_layer=Mock(), replication_layer=Mock(),
@ -77,6 +80,30 @@ class RoomTypingTestCase(RestTestCase):
return defer.succeed(None) return defer.succeed(None)
hs.get_datastore().insert_client_ip = _insert_client_ip hs.get_datastore().insert_client_ip = _insert_client_ip
def get_room_members(room_id):
if room_id == self.room_id:
return defer.succeed([hs.parse_userid(self.user_id)])
else:
return defer.succeed([])
@defer.inlineCallbacks
def fetch_room_distributions_into(room_id, localusers=None,
remotedomains=None, ignore_user=None):
members = yield get_room_members(room_id)
for member in members:
if ignore_user is not None and member == ignore_user:
continue
if hs.is_mine(member):
if localusers is not None:
localusers.add(member)
else:
if remotedomains is not None:
remotedomains.add(member.domain)
hs.get_handlers().room_member_handler.fetch_room_distributions_into = (
fetch_room_distributions_into)
synapse.rest.room.register_servlets(hs, self.mock_resource) synapse.rest.room.register_servlets(hs, self.mock_resource)
self.room_id = yield self.create_room_as(self.user_id) self.room_id = yield self.create_room_as(self.user_id)
@ -113,3 +140,25 @@ class RoomTypingTestCase(RestTestCase):
'{"typing": false}' '{"typing": false}'
) )
self.assertEquals(200, code) self.assertEquals(200, code)
@defer.inlineCallbacks
def test_typing_timeout(self):
(code, _) = yield self.mock_resource.trigger("PUT",
"/rooms/%s/typing/%s" % (self.room_id, self.user_id),
'{"typing": true, "timeout": 30000}'
)
self.assertEquals(200, code)
self.assertEquals(self.event_source.get_current_key(), 1)
self.clock.advance_time(31);
self.assertEquals(self.event_source.get_current_key(), 2)
(code, _) = yield self.mock_resource.trigger("PUT",
"/rooms/%s/typing/%s" % (self.room_id, self.user_id),
'{"typing": true, "timeout": 30000}'
)
self.assertEquals(200, code)
self.assertEquals(self.event_source.get_current_key(), 3)

View file

@ -56,17 +56,6 @@ class RoomStoreTestCase(unittest.TestCase):
(yield self.store.get_room(self.room.to_string())) (yield self.store.get_room(self.room.to_string()))
) )
@defer.inlineCallbacks
def test_store_room_config(self):
yield self.store.store_room_config(self.room.to_string(),
visibility=False
)
self.assertObjectHasAttributes(
{"is_public": False},
(yield self.store.get_room(self.room.to_string()))
)
@defer.inlineCallbacks @defer.inlineCallbacks
def test_get_rooms(self): def test_get_rooms(self):
# get_rooms does an INNER JOIN on the room_aliases table :( # get_rooms does an INNER JOIN on the room_aliases table :(

View file

@ -69,6 +69,8 @@ class TestCase(unittest.TestCase):
return ret return ret
logging.getLogger().setLevel(level) logging.getLogger().setLevel(level)
# Don't set SQL logging
logging.getLogger("synapse.storage").setLevel(old_level)
return orig() return orig()
def assertObjectHasAttributes(self, attrs, obj): def assertObjectHasAttributes(self, attrs, obj):

View file

@ -138,7 +138,8 @@ class MockClock(object):
now = 1000 now = 1000
def __init__(self): def __init__(self):
# list of tuples of (absolute_time, callback) in no particular order # list of lists of [absolute_time, callback, expired] in no particular
# order
self.timers = [] self.timers = []
def time(self): def time(self):
@ -154,11 +155,16 @@ class MockClock(object):
LoggingContext.thread_local.current_context = current_context LoggingContext.thread_local.current_context = current_context
callback() callback()
t = (self.now + delay, wrapped_callback) t = [self.now + delay, wrapped_callback, False]
self.timers.append(t) self.timers.append(t)
return t return t
def cancel_call_later(self, timer): def cancel_call_later(self, timer):
if timer[2]:
raise Exception("Cannot cancel an expired timer")
timer[2] = True
self.timers = [t for t in self.timers if t != timer] self.timers = [t for t in self.timers if t != timer]
# For unit testing # For unit testing
@ -168,11 +174,17 @@ class MockClock(object):
timers = self.timers timers = self.timers
self.timers = [] self.timers = []
for time, callback in timers: for t in timers:
time, callback, expired = t
if expired:
raise Exception("Timer already expired")
if self.now >= time: if self.now >= time:
t[2] = True
callback() callback()
else: else:
self.timers.append((time, callback)) self.timers.append(t)
class SQLiteMemoryDbPool(ConnectionPool, object): class SQLiteMemoryDbPool(ConnectionPool, object):