Compare commits
191 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
463b95f0c2 | ||
|
|
bf6466f02a | ||
|
|
4b7f6dd7fc | ||
|
|
073bec4830 | ||
|
|
cc413be446 | ||
|
|
ee06023573 | ||
|
|
3e6a19cf09 | ||
|
|
5308e3026a | ||
|
|
eab463fda5 | ||
|
|
47fb286184 | ||
|
|
5dd38d579b | ||
|
|
ac56ac67cc | ||
|
|
171d8b032f | ||
|
|
41d02ab674 | ||
|
|
1abc93d65c | ||
|
|
ee079cd250 | ||
|
|
d1bf659ed7 | ||
|
|
089d1b1b78 | ||
|
|
9b2cb41dcf | ||
|
|
96baf62e7a | ||
|
|
246b2a3c3e | ||
|
|
ca7426eee0 | ||
|
|
8113eb7c79 | ||
|
|
aaf4fd98ee | ||
|
|
722c19d033 | ||
|
|
1b7686329e | ||
|
|
068b348e7e | ||
|
|
2c7c12bc6e | ||
|
|
54d0a75573 | ||
|
|
a8d318cf82 | ||
|
|
efc5f3440d | ||
|
|
113342a756 | ||
|
|
b1da3fa0a7 | ||
|
|
c46c806126 | ||
|
|
eb3094ed31 | ||
|
|
b09e531159 | ||
|
|
62dfa3c741 | ||
|
|
7b079a26a5 | ||
|
|
bddc1d9fff | ||
|
|
e0ba81344c | ||
|
|
c44293db2f | ||
|
|
7c99ebdbd1 | ||
|
|
06c79a23d4 | ||
|
|
466fbe4c4e | ||
|
|
b8b52ca09d | ||
|
|
8d7d251c35 | ||
|
|
52cfdfd5f1 | ||
|
|
7acede1e42 | ||
|
|
15ab5f5ad8 | ||
|
|
b485d622cc | ||
|
|
64e927108b | ||
|
|
f3f32addca | ||
|
|
6ac298f2f1 | ||
|
|
660129deb1 | ||
|
|
7d34a1c108 | ||
|
|
d027e859cd | ||
|
|
407c86c013 | ||
|
|
c2b4b73751 | ||
|
|
04fdcf302d | ||
|
|
357dd1871d | ||
|
|
e111a06e0a | ||
|
|
410a74b0f3 | ||
|
|
92033e4ebc | ||
|
|
2aeaa7b77c | ||
|
|
7c89d5e97a | ||
|
|
226025e9ca | ||
|
|
f54b70520a | ||
|
|
f53c4300fd | ||
|
|
6ad9d9c226 | ||
|
|
234c50b834 | ||
|
|
1d95e78759 | ||
|
|
b30358f439 | ||
|
|
f64887e15c | ||
|
|
52cb5e6324 | ||
|
|
4e8d19ee2b | ||
|
|
8af5e360d6 | ||
|
|
d9155b6a25 | ||
|
|
7ee5288849 | ||
|
|
e179ed1f60 | ||
|
|
89c044c2a0 | ||
|
|
7917ff1271 | ||
|
|
abe2035d85 | ||
|
|
08881d808d | ||
|
|
bfe9faad5a | ||
|
|
05672a6a8c | ||
|
|
fb9661898d | ||
|
|
a0d1f5a014 | ||
|
|
87190a9673 | ||
|
|
308c9273fa | ||
|
|
c67cac134f | ||
|
|
43242a0657 | ||
|
|
b1352f97ac | ||
|
|
6691ca6f8d | ||
|
|
e40d829363 | ||
|
|
c585c87c4b | ||
|
|
1d9d287c7c | ||
|
|
46a2f6a816 | ||
|
|
a03c7f27a8 | ||
|
|
77a255c7c3 | ||
|
|
47519cd8c2 | ||
|
|
bd16b93e8f | ||
|
|
474d913712 | ||
|
|
dddf5c0cc8 | ||
|
|
05fa81fee4 | ||
|
|
71095f4e6e | ||
|
|
6c609425ba | ||
|
|
5eff05a4ce | ||
|
|
d63f775e06 | ||
|
|
e677a3114e | ||
|
|
648796ef1d | ||
|
|
a8774cf351 | ||
|
|
135a1aa229 | ||
|
|
474dcecb11 | ||
|
|
dd661769e1 | ||
|
|
bf05218c4b | ||
|
|
c65885e166 | ||
|
|
dfa0cd1d90 | ||
|
|
d2798de660 | ||
|
|
67c5f89244 | ||
|
|
c1cf0b334e | ||
|
|
93cff1668c | ||
|
|
3a2a5b959c | ||
|
|
6966971a28 | ||
|
|
a498df0428 | ||
|
|
64e2a5d58e | ||
|
|
f84ddc75cb | ||
|
|
5dd8087ea4 | ||
|
|
73a1022bca | ||
|
|
5a3df1d029 | ||
|
|
6f0bba1934 | ||
|
|
5a93bfe1f0 | ||
|
|
ad6d5ac06c | ||
|
|
8885c8546c | ||
|
|
9a93e83d90 | ||
|
|
66a4d33524 | ||
|
|
d0103400b5 | ||
|
|
2e70de09b9 | ||
|
|
47c1a3d454 | ||
|
|
3281fec07a | ||
|
|
a29d12a18a | ||
|
|
4b63b06cad | ||
|
|
3df5cb804f | ||
|
|
b1e98ddc09 | ||
|
|
ac21dfff6d | ||
|
|
32347bfcc9 | ||
|
|
bcf8eb687a | ||
|
|
0e7a41dc99 | ||
|
|
8bd55cfdcb | ||
|
|
ff3709e577 | ||
|
|
c21fcb3373 | ||
|
|
b07bc9bdbd | ||
|
|
27979028b2 | ||
|
|
9ff9caeb74 | ||
|
|
5c0be8fde3 | ||
|
|
4b2ad549d5 | ||
|
|
732d954f89 | ||
|
|
485bb64ddb | ||
|
|
1291ac93f3 | ||
|
|
a664ec20e0 | ||
|
|
7d79021c42 | ||
|
|
f6daa9f170 | ||
|
|
b01aeac842 | ||
|
|
5796232cb1 | ||
|
|
52b64617f9 | ||
|
|
fea7b60cf3 | ||
|
|
b52b33acf6 | ||
|
|
47c3a089c5 | ||
|
|
cab3095803 | ||
|
|
be6abdff19 | ||
|
|
95839212a7 | ||
|
|
66d752dd1b | ||
|
|
1bd380c816 | ||
|
|
8b0473d5b9 | ||
|
|
2c4908ed26 | ||
|
|
4521c2d277 | ||
|
|
0c3b4a1f63 | ||
|
|
9d86c8c7a6 | ||
|
|
a9a5329a11 | ||
|
|
3f08a7ad21 | ||
|
|
d2bb28d2df | ||
|
|
45e70a6b70 | ||
|
|
31e7cec486 | ||
|
|
41d1db2d4a | ||
|
|
de0706493a | ||
|
|
4c7df52360 | ||
|
|
1379dcae6f | ||
|
|
61cac4df6e | ||
|
|
aaf623fa53 | ||
|
|
f690b7b827 | ||
|
|
81a95937de | ||
|
|
7bec359408 |
9
.gitignore
vendored
9
.gitignore
vendored
@@ -10,10 +10,17 @@ docs/build/
|
||||
*.egg-info
|
||||
|
||||
cmdclient_config.json
|
||||
homeserver.db
|
||||
homeserver*.db
|
||||
|
||||
.coverage
|
||||
htmlcov
|
||||
|
||||
demo/*.db
|
||||
demo/*.log
|
||||
demo/*.pid
|
||||
|
||||
graph/*.svg
|
||||
graph/*.png
|
||||
graph/*.dot
|
||||
|
||||
uploads
|
||||
|
||||
22
CHANGES.rst
22
CHANGES.rst
@@ -1,3 +1,25 @@
|
||||
Changes in synapse 0.1.0 (2014-08-29)
|
||||
=====================================
|
||||
Presence has been reenabled in this release.
|
||||
|
||||
Homeserver:
|
||||
* Update client to server API, including:
|
||||
- Use a more consistent url scheme.
|
||||
- Provide more useful information in the initial sync api.
|
||||
* Change the presence handling to be much more efficient.
|
||||
* Change the presence server to server API to not require explicit polling of
|
||||
all users who share a room with a user.
|
||||
* Fix races in the event streaming logic.
|
||||
|
||||
Webclient:
|
||||
* Update to use new client to server API.
|
||||
* Add basic VOIP support.
|
||||
* Add idle timers that change your status to away.
|
||||
* Add recent rooms column when viewing a room.
|
||||
* Various network efficiency improvements.
|
||||
* Add basic mobile browser support.
|
||||
* Add a settings page.
|
||||
|
||||
Changes in synapse 0.0.1 (2014-08-22)
|
||||
=====================================
|
||||
Presence has been disabled in this release due to a bug that caused the
|
||||
|
||||
@@ -34,6 +34,10 @@ To get up and running:
|
||||
machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and
|
||||
say hi! :)
|
||||
|
||||
For more detailed setup instructions, please see further down this document.
|
||||
|
||||
[1] VoIP currently in development
|
||||
|
||||
|
||||
About Matrix
|
||||
============
|
||||
@@ -85,8 +89,6 @@ https://github.com/matrix-org/synapse/issues or at matrix@matrix.org.
|
||||
|
||||
Thanks for trying Matrix!
|
||||
|
||||
[1] VoIP currently in development
|
||||
|
||||
[2] Cryptographic signing of messages isn't turned on yet
|
||||
|
||||
[3] End-to-end encryption is currently in development
|
||||
|
||||
@@ -61,7 +61,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
"send_delivery_receipts": "on"
|
||||
}
|
||||
self.path_prefix = "/matrix/client/api/v1"
|
||||
self.event_stream_token = "START"
|
||||
self.event_stream_token = "END"
|
||||
self.prompt = ">>> "
|
||||
|
||||
def do_EOF(self, line): # allows CTRL+D quitting
|
||||
@@ -225,8 +225,13 @@ class SynapseCmd(cmd.Cmd):
|
||||
json_res = yield self.http_client.do_request("GET", url)
|
||||
print json_res
|
||||
|
||||
if ("type" not in json_res or "m.login.password" != json_res["type"] or
|
||||
"stages" in json_res):
|
||||
if "flows" not in json_res:
|
||||
print "Failed to find any login flows."
|
||||
defer.returnValue(False)
|
||||
|
||||
flow = json_res["flows"][0] # assume first is the one we want.
|
||||
if ("type" not in flow or "m.login.password" != flow["type"] or
|
||||
"stages" in flow):
|
||||
fallback_url = self._url() + "/login/fallback"
|
||||
print ("Unable to login via the command line client. Please visit "
|
||||
"%s to login." % fallback_url)
|
||||
@@ -402,19 +407,16 @@ class SynapseCmd(cmd.Cmd):
|
||||
"""Leaves a room: "leave <roomid>" """
|
||||
try:
|
||||
args = self._parse(line, ["roomid"], force_keys=True)
|
||||
path = ("/rooms/%s/members/%s/state" %
|
||||
(urllib.quote(args["roomid"]), self._usr()))
|
||||
reactor.callFromThread(self._run_and_pprint, "DELETE", path)
|
||||
self._do_membership_change(args["roomid"], "leave", self._usr())
|
||||
except Exception as e:
|
||||
print e
|
||||
|
||||
def do_send(self, line):
|
||||
"""Sends a message. "send <roomid> <body>" """
|
||||
args = self._parse(line, ["roomid", "body"])
|
||||
msg_id = "m%s" % int(time.time())
|
||||
path = "/rooms/%s/messages/%s/%s" % (urllib.quote(args["roomid"]),
|
||||
self._usr(),
|
||||
msg_id)
|
||||
txn_id = "txn%s" % int(time.time())
|
||||
path = "/rooms/%s/send/m.room.message/%s" % (urllib.quote(args["roomid"]),
|
||||
txn_id)
|
||||
body_json = {
|
||||
"msgtype": "m.text",
|
||||
"body": args["body"]
|
||||
@@ -438,7 +440,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
print "Unrecognised type: %s" % args["type"]
|
||||
return
|
||||
room_id = args["roomid"]
|
||||
path = "/rooms/%s/%s/list" % (urllib.quote(room_id), args["type"])
|
||||
path = "/rooms/%s/%s" % (urllib.quote(room_id), args["type"])
|
||||
|
||||
qp = {"access_token": self._tok()}
|
||||
if "qp" in args:
|
||||
@@ -474,7 +476,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
room_name = args["vis"]
|
||||
body["room_alias_name"] = room_name
|
||||
|
||||
reactor.callFromThread(self._run_and_pprint, "POST", "/rooms", body)
|
||||
reactor.callFromThread(self._run_and_pprint, "POST", "/createRoom", body)
|
||||
|
||||
def do_raw(self, line):
|
||||
"""Directly send a JSON object: "raw <method> <path> <data> <notoken>"
|
||||
@@ -567,7 +569,7 @@ class SynapseCmd(cmd.Cmd):
|
||||
alt_text="Sent receipt for %s" % event["msg_id"])
|
||||
|
||||
def _do_membership_change(self, roomid, membership, userid):
|
||||
path = "/rooms/%s/members/%s/state" % (urllib.quote(roomid), userid)
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (urllib.quote(roomid), urllib.quote(userid))
|
||||
data = {
|
||||
"membership": membership
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ CWD=$(pwd)
|
||||
|
||||
cd "$DIR/.."
|
||||
|
||||
for port in "8080" "8081" "8082"; do
|
||||
for port in 8080 8081 8082; do
|
||||
echo "Starting server on port $port... "
|
||||
|
||||
python -m synapse.app.homeserver \
|
||||
@@ -15,7 +15,8 @@ for port in "8080" "8081" "8082"; do
|
||||
-f "$DIR/$port.log" \
|
||||
-d "$DIR/$port.db" \
|
||||
-vv \
|
||||
-D --pid-file "$DIR/$port.pid"
|
||||
-D --pid-file "$DIR/$port.pid" \
|
||||
--manhole $((port + 1000))
|
||||
done
|
||||
|
||||
echo "Starting webclient on port 8000..."
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
{
|
||||
"path": "/presence",
|
||||
"description": "Presence operations"
|
||||
},
|
||||
{
|
||||
"path": "/events",
|
||||
"description": "Event operations"
|
||||
},
|
||||
{
|
||||
"path": "/directory",
|
||||
"description": "Directory operations"
|
||||
}
|
||||
],
|
||||
"authorizations": {
|
||||
|
||||
83
docs/client-server/swagger_matrix/directory
Normal file
83
docs/client-server/swagger_matrix/directory
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/directory",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"path": "/directory/room/{roomAlias}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get the room ID corresponding to this room alias.",
|
||||
"type": "DirectoryResponse",
|
||||
"nickname": "get_room_id_for_alias",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomAlias",
|
||||
"description": "The room alias.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Create a new mapping from room alias to room ID.",
|
||||
"type": "void",
|
||||
"nickname": "add_room_alias",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomAlias",
|
||||
"description": "The room alias to set.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The room ID to set.",
|
||||
"required": true,
|
||||
"type": "RoomAliasRequest",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"DirectoryResponse": {
|
||||
"id": "DirectoryResponse",
|
||||
"properties": {
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The fully-qualified room ID.",
|
||||
"required": true
|
||||
},
|
||||
"servers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "string"
|
||||
},
|
||||
"description": "A list of servers that know about this room.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"RoomAliasRequest": {
|
||||
"id": "RoomAliasRequest",
|
||||
"properties": {
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room ID to map the alias to.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,299 +1,246 @@
|
||||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://petstore.swagger.wordnik.com/api",
|
||||
"resourcePath": "/user",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/events",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"path": "/user",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Create user",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "createUser",
|
||||
"authorizations": {
|
||||
"oauth2": [
|
||||
{
|
||||
"scope": "test:anything",
|
||||
"description": "anything"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "Created user object",
|
||||
"required": true,
|
||||
"type": "User",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/logout",
|
||||
"path": "/events",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Logs out current logged in user session",
|
||||
"notes": "",
|
||||
"type": "void",
|
||||
"nickname": "logoutUser",
|
||||
"authorizations": {},
|
||||
"parameters": []
|
||||
"summary": "Listen on the event stream",
|
||||
"notes": "This can only be done by the logged in user. This will block until an event is received, or until the timeout is reached.",
|
||||
"type": "PaginationChunk",
|
||||
"nickname": "get_event_stream"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/createWithArray",
|
||||
"operations": [
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Creates list of users with given input array",
|
||||
"notes": "",
|
||||
"type": "void",
|
||||
"nickname": "createUsersWithArrayInput",
|
||||
"authorizations": {
|
||||
"oauth2": [
|
||||
{
|
||||
"scope": "test:anything",
|
||||
"description": "anything"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "List of user object",
|
||||
"required": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "User"
|
||||
},
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/createWithList",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Creates list of users with given list input",
|
||||
"notes": "",
|
||||
"type": "void",
|
||||
"nickname": "createUsersWithListInput",
|
||||
"authorizations": {
|
||||
"oauth2": [
|
||||
{
|
||||
"scope": "test:anything",
|
||||
"description": "anything"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "List of user object",
|
||||
"required": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "User"
|
||||
},
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/{username}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Updated user",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "updateUser",
|
||||
"authorizations": {
|
||||
"oauth2": [
|
||||
{
|
||||
"scope": "test:anything",
|
||||
"description": "anything"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"description": "name that need to be deleted",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "Updated user object",
|
||||
"required": true,
|
||||
"type": "User",
|
||||
"paramType": "body"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid username supplied"
|
||||
},
|
||||
{
|
||||
"code": 404,
|
||||
"message": "User not found"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"summary": "Delete user",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "deleteUser",
|
||||
"authorizations": {
|
||||
"oauth2": [
|
||||
{
|
||||
"scope": "test:anything",
|
||||
"description": "anything"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"description": "The name that needs to be deleted",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid username supplied"
|
||||
},
|
||||
{
|
||||
"code": 404,
|
||||
"message": "User not found"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get user by user name",
|
||||
"notes": "",
|
||||
"type": "User",
|
||||
"nickname": "getUserByName",
|
||||
"authorizations": {},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"description": "The name that needs to be fetched. Use user1 for testing.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid username supplied"
|
||||
},
|
||||
{
|
||||
"code": 404,
|
||||
"message": "User not found"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/login",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Logs user into the system",
|
||||
"notes": "",
|
||||
"name": "from",
|
||||
"description": "The token to stream from.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"nickname": "loginUser",
|
||||
"authorizations": {},
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "timeout",
|
||||
"description": "The maximum time in milliseconds to wait for an event.",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"paramType": "query"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad pagination token."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/events/{eventId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get information about a single event.",
|
||||
"notes": "Get information about a single event.",
|
||||
"type": "Event",
|
||||
"nickname": "get_event",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"description": "The user name for login",
|
||||
"name": "eventId",
|
||||
"description": "The event ID to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"description": "The password for login in clear text",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid username and password combination"
|
||||
"code": 404,
|
||||
"message": "Event not found."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/initialSync",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get this user's current state.",
|
||||
"notes": "Get this user's current state.",
|
||||
"type": "InitialSyncResponse",
|
||||
"nickname": "initial_sync",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "The maximum number of messages to return for each room.",
|
||||
"type": "integer",
|
||||
"paramType": "query",
|
||||
"required": false
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/publicRooms",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a list of publicly visible rooms.",
|
||||
"type": "PublicRoomsPaginationChunk",
|
||||
"nickname": "get_public_room_list"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"User": {
|
||||
"id": "User",
|
||||
"PaginationChunk": {
|
||||
"id": "PaginationChunk",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"start": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
"chunk": {
|
||||
"type": "array",
|
||||
"description": "An array of events.",
|
||||
"required": true,
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Event": {
|
||||
"id": "Event",
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "An ID which uniquely identifies this event.",
|
||||
"required": true
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room in which this event occurred.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"PublicRoomInfo": {
|
||||
"id": "PublicRoomInfo",
|
||||
"properties": {
|
||||
"aliases": {
|
||||
"type": "array",
|
||||
"description": "A list of room aliases for this room.",
|
||||
"items": {
|
||||
"$ref": "string"
|
||||
}
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name of the room, as given by the m.room.name state event."
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room ID for this public room.",
|
||||
"required": true
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
"topic": {
|
||||
"type": "string",
|
||||
"description": "The topic of this room, as given by the m.room.topic state event."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PublicRoomsPaginationChunk": {
|
||||
"id": "PublicRoomsPaginationChunk",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"userStatus": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "User Status",
|
||||
"enum": [
|
||||
"1-registered",
|
||||
"2-active",
|
||||
"3-closed"
|
||||
]
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"chunk": {
|
||||
"type": "array",
|
||||
"description": "A list of public room data.",
|
||||
"required": true,
|
||||
"items": {
|
||||
"$ref": "PublicRoomInfo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"InitialSyncResponse": {
|
||||
"id": "InitialSyncResponse",
|
||||
"properties": {
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A streaming token which can be used with /events to continue from this snapshot of data.",
|
||||
"required": true
|
||||
},
|
||||
"presence": {
|
||||
"type": "array",
|
||||
"description": "A list of presence events.",
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
},
|
||||
"required": false
|
||||
},
|
||||
"rooms": {
|
||||
"type": "array",
|
||||
"description": "A list of initial sync room data.",
|
||||
"required": false,
|
||||
"items": {
|
||||
"$ref": "InitialSyncRoomData"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"InitialSyncRoomData": {
|
||||
"id": "InitialSyncRoomData",
|
||||
"properties": {
|
||||
"membership": {
|
||||
"type": "string",
|
||||
"description": "This user's membership state in this room.",
|
||||
"required": true
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The ID of this room.",
|
||||
"required": true
|
||||
},
|
||||
"messages": {
|
||||
"type": "PaginationChunk",
|
||||
"description": "The most recent messages for this room, governed by the limit parameter.",
|
||||
"required": false
|
||||
},
|
||||
"state": {
|
||||
"type": "array",
|
||||
"description": "A list of state events representing the current state of the room.",
|
||||
"required": false,
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/presence_list/{userId}",
|
||||
"path": "/presence/list/{userId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
|
||||
@@ -14,13 +14,103 @@
|
||||
},
|
||||
"apis": [
|
||||
{
|
||||
"path": "/rooms/{roomId}/messages/{userId}/{messageId}",
|
||||
"path": "/rooms/{roomId}/send/{eventType}/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send a generic non-state event to this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/{eventType}",
|
||||
"type": "EventId",
|
||||
"nickname": "send_non_state_event",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The event contents",
|
||||
"required": true,
|
||||
"type": "EventContent",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "eventType",
|
||||
"description": "The type of event to send.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/state/{eventType}/{stateKey}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send a generic state event to this room.",
|
||||
"notes": "The state key can be omitted, such that you can PUT to /rooms/{roomId}/state/{eventType}. The state key defaults to a 0 length string in this case.",
|
||||
"type": "void",
|
||||
"nickname": "send_state_event",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The event contents",
|
||||
"required": true,
|
||||
"type": "EventContent",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "eventType",
|
||||
"description": "The type of event to send.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "stateKey",
|
||||
"description": "An identifier used to specify clobbering semantics. State events with the same (roomId, eventType, stateKey) will be replaced.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/send/m.room.message/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send a message in this room.",
|
||||
"notes": "Send a message in this room.",
|
||||
"type": "void",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message",
|
||||
"type": "EventId",
|
||||
"nickname": "send_message",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
@@ -41,67 +131,18 @@
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The fully qualified message sender's user ID.",
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "messageId",
|
||||
"description": "A message ID which is unique for each room and user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Must send messages as yourself."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a message from this room.",
|
||||
"notes": "Get a message from this room.",
|
||||
"type": "Message",
|
||||
"nickname": "get_message",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The fully qualified message sender's user ID.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "messageId",
|
||||
"description": "A message ID which is unique for each room and user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Message not found."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/topic",
|
||||
"path": "/rooms/{roomId}/state/m.room.topic",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
@@ -127,12 +168,6 @@
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Must send messages as yourself."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -160,13 +195,13 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/messages/{msgSenderId}/{messageId}/feedback/{senderId}/{feedbackType}",
|
||||
"path": "/rooms/{roomId}/send/m.room.message.feedback/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send feedback to a message.",
|
||||
"notes": "Send feedback to a message.",
|
||||
"type": "void",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message.feedback",
|
||||
"type": "EventId",
|
||||
"nickname": "send_feedback",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
@@ -187,107 +222,124 @@
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "msgSenderId",
|
||||
"description": "The fully qualified message sender's user ID.",
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID to ensure idempotency. This can only be omitted if the HTTP method becomes a POST.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "messageId",
|
||||
"description": "A message ID which is unique for each room and user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "senderId",
|
||||
"description": "The fully qualified feedback sender's user ID.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "feedbackType",
|
||||
"description": "The type of feedback being sent.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path",
|
||||
"enum": [
|
||||
"d",
|
||||
"r"
|
||||
]
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Must send feedback as yourself."
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad feedback type."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get feedback for a message.",
|
||||
"notes": "Get feedback for a message.",
|
||||
"type": "Feedback",
|
||||
"nickname": "get_feedback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "msgSenderId",
|
||||
"description": "The fully qualified message sender's user ID.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "messageId",
|
||||
"description": "A message ID which is unique for each room and user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "senderId",
|
||||
"description": "The fully qualified feedback sender's user ID.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "feedbackType",
|
||||
"description": "Enum: The type of feedback being sent.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path",
|
||||
"enum": [
|
||||
"d",
|
||||
"r"
|
||||
]
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Feedback not found."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/members/{userId}/state",
|
||||
"path": "/rooms/{roomId}/invite/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Invite a user to this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/invite",
|
||||
"type": "void",
|
||||
"nickname": "invite",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has this user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The user to invite.",
|
||||
"required": true,
|
||||
"type": "InviteRequest",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/join/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Join this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/join",
|
||||
"type": "void",
|
||||
"nickname": "join_room",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to join.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/leave/{txnId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Leave this room.",
|
||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/leave",
|
||||
"type": "void",
|
||||
"nickname": "leave",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to leave.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "txnId",
|
||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/state/m.room.member/{userId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
@@ -376,58 +428,25 @@
|
||||
"message": "Member not found."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"summary": "Leave a room.",
|
||||
"notes": "Leave a room.",
|
||||
"type": "void",
|
||||
"nickname": "remove_membership",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user who is leaving.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has this user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 403,
|
||||
"message": "You are not in the room."
|
||||
},
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Cannot force another user to leave."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/join/{roomAlias}",
|
||||
"path": "/join/{roomAliasOrId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Join a room via a room alias.",
|
||||
"notes": "Join a room via a room alias.",
|
||||
"summary": "Join a room via a room alias or room ID.",
|
||||
"notes": "Join a room via a room alias or room ID.",
|
||||
"type": "RoomInfo",
|
||||
"nickname": "join_room_via_alias",
|
||||
"nickname": "join",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomAlias",
|
||||
"description": "The room alias to join.",
|
||||
"name": "roomAliasOrId",
|
||||
"description": "The room alias or room ID to join.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
@@ -443,7 +462,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms",
|
||||
"path": "/createRoom",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
@@ -477,7 +496,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/messages/list",
|
||||
"path": "/rooms/{roomId}/messages",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
@@ -519,7 +538,7 @@
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/members/list",
|
||||
"path": "/rooms/{roomId}/members",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
@@ -559,6 +578,51 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/state",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a list of all the current state events for this room.",
|
||||
"notes": "Get a list of all the current state events for this room.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
},
|
||||
"nickname": "get_state_events",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to get a list of current state events from.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/initialSync",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get all the current information for this room, including messages and state events.",
|
||||
"notes": "Get all the current information for this room, including messages and state events.",
|
||||
"type": "InitialSyncRoomData",
|
||||
"nickname": "get_room_sync_data",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to get information for.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
@@ -704,12 +768,17 @@
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "An ID which uniquely identifies this event.",
|
||||
"description": "An ID which uniquely identifies this event. This is automatically set by the server.",
|
||||
"required": true
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room in which this event occurred.",
|
||||
"description": "The room in which this event occurred. This is automatically set by the server.",
|
||||
"required": true
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "The event type.",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
@@ -717,6 +786,26 @@
|
||||
"MessageEvent"
|
||||
]
|
||||
},
|
||||
"EventId": {
|
||||
"id": "EventId",
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "The allocated event ID for this event.",
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"EventContent": {
|
||||
"id": "EventContent",
|
||||
"properties": {
|
||||
"__event_content_keys__": {
|
||||
"type": "string",
|
||||
"description": "Event-specific content keys and values.",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"MessageEvent": {
|
||||
"id": "MessageEvent",
|
||||
"properties": {
|
||||
@@ -733,73 +822,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Tag": {
|
||||
"id": "Tag",
|
||||
"InviteRequest": {
|
||||
"id": "InviteRequest",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pet": {
|
||||
"id": "Pet",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "unique identifier for the pet",
|
||||
"minimum": "0.0",
|
||||
"maximum": "100.0"
|
||||
},
|
||||
"category": {
|
||||
"$ref": "Category"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"photoUrls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Tag"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"description": "pet status in the store",
|
||||
"enum": [
|
||||
"available",
|
||||
"pending",
|
||||
"sold"
|
||||
]
|
||||
"description": "The fully-qualified user ID."
|
||||
}
|
||||
}
|
||||
},
|
||||
"Category": {
|
||||
"id": "Category",
|
||||
"InitialSyncRoomData": {
|
||||
"id": "InitialSyncRoomData",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
"membership": {
|
||||
"type": "string",
|
||||
"description": "This user's membership state in this room.",
|
||||
"required": true
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The ID of this room.",
|
||||
"required": true
|
||||
},
|
||||
"pet": {
|
||||
"$ref": "Pet"
|
||||
"messages": {
|
||||
"type": "MessagePaginationChunk",
|
||||
"description": "The most recent messages for this room, governed by the limit parameter.",
|
||||
"required": false
|
||||
},
|
||||
"state": {
|
||||
"type": "array",
|
||||
"description": "A list of state events representing the current state of the room.",
|
||||
"required": false,
|
||||
"items": {
|
||||
"$ref": "Event"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
=========================
|
||||
Client-Server URL Summary
|
||||
=========================
|
||||
|
||||
A brief overview of the URL scheme involved in the Synapse Client-Server API.
|
||||
|
||||
|
||||
URLs
|
||||
====
|
||||
|
||||
Fetch events:
|
||||
GET /events
|
||||
|
||||
Registering an account
|
||||
POST /register
|
||||
|
||||
Unregistering an account
|
||||
POST /unregister
|
||||
|
||||
Rooms
|
||||
-----
|
||||
|
||||
Creating a room by ID
|
||||
PUT /rooms/$roomid
|
||||
|
||||
Creating an anonymous room
|
||||
POST /rooms
|
||||
|
||||
Room topic
|
||||
GET /rooms/$roomid/topic
|
||||
PUT /rooms/$roomid/topic
|
||||
|
||||
List rooms
|
||||
GET /rooms/list
|
||||
|
||||
Invite/Join/Leave
|
||||
GET /rooms/$roomid/members/$userid/state
|
||||
PUT /rooms/$roomid/members/$userid/state
|
||||
DELETE /rooms/$roomid/members/$userid/state
|
||||
|
||||
List members
|
||||
GET /rooms/$roomid/members/list
|
||||
|
||||
Sending/reading messages
|
||||
PUT /rooms/$roomid/messages/$sender/$msgid
|
||||
|
||||
Feedback
|
||||
GET /rooms/$roomid/messages/$sender/$msgid/feedback/$feedbackuser/$feedback
|
||||
PUT /rooms/$roomid/messages/$sender/$msgid/feedback/$feedbackuser/$feedback
|
||||
|
||||
Paginating messages
|
||||
GET /rooms/$roomid/messages/list
|
||||
|
||||
Profiles
|
||||
--------
|
||||
|
||||
Display name
|
||||
GET /profile/$userid/displayname
|
||||
PUT /profile/$userid/displayname
|
||||
|
||||
Avatar URL
|
||||
GET /profile/$userid/avatar_url
|
||||
PUT /profile/$userid/avatar_url
|
||||
|
||||
Metadata
|
||||
GET /profile/$userid/metadata
|
||||
POST /profile/$userid/metadata
|
||||
|
||||
Presence
|
||||
--------
|
||||
|
||||
My state or status message
|
||||
GET /presence/$userid/status
|
||||
PUT /presence/$userid/status
|
||||
also 'GET' for fetching others
|
||||
|
||||
TODO(paul): per-device idle time, device type; similar to above
|
||||
|
||||
My presence list
|
||||
GET /presence_list/$myuserid
|
||||
POST /presence_list/$myuserid
|
||||
body is JSON-encoded dict of keys:
|
||||
invite: list of UserID strings to invite
|
||||
drop: list of UserID strings to remove
|
||||
TODO(paul): define other ops: accept, group management, ordering?
|
||||
|
||||
Presence polling start/stop
|
||||
POST /presence_list/$myuserid?op=start
|
||||
POST /presence_list/$myuserid?op=stop
|
||||
|
||||
Presence invite
|
||||
POST /presence_list/$myuserid/invite/$targetuserid
|
||||
839
docs/specification.rst
Normal file
839
docs/specification.rst
Normal file
@@ -0,0 +1,839 @@
|
||||
Matrix Specification
|
||||
====================
|
||||
|
||||
TODO(Introduction) : Matthew
|
||||
- Similar to intro paragraph from README.
|
||||
- Explaining the overall mission, what this spec describes...
|
||||
- "What is Matrix?"
|
||||
- Draw parallels with email?
|
||||
|
||||
Architecture
|
||||
============
|
||||
|
||||
Clients transmit data to other clients through home servers (HSes). Clients do not communicate with each
|
||||
other directly.
|
||||
|
||||
::
|
||||
|
||||
How data flows between clients
|
||||
==============================
|
||||
|
||||
{ Matrix client A } { Matrix client B }
|
||||
^ | ^ |
|
||||
| events | | events |
|
||||
| V | V
|
||||
+------------------+ +------------------+
|
||||
| |---------( HTTP )---------->| |
|
||||
| Home Server | | Home Server |
|
||||
| |<--------( HTTP )-----------| |
|
||||
+------------------+ Federation +------------------+
|
||||
|
||||
A "Client" is an end-user, typically a human using a web application or mobile app. Clients use the
|
||||
"Client-to-Server" (C-S) API to communicate with their home server. A single Client is usually
|
||||
responsible for a single user account. A user account is represented by their "User ID". This ID is
|
||||
namespaced to the home server which allocated the account and looks like::
|
||||
|
||||
@localpart:domain
|
||||
|
||||
The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user.
|
||||
|
||||
|
||||
A "Home Server" is a server which provides C-S APIs and has the ability to federate with other HSes.
|
||||
It is typically responsible for multiple clients. "Federation" is the term used to describe the
|
||||
sharing of data between two or more home servers.
|
||||
|
||||
Data in Matrix is encapsulated in an "Event". An event is an action within the system. Typically each
|
||||
action (e.g. sending a message) correlates with exactly one event. Each event has a ``type`` which is
|
||||
used to differentiate different kinds of data. ``type`` values SHOULD be namespaced according to standard
|
||||
Java package naming conventions, e.g. ``com.example.myapp.event``. Events are usually sent in the context
|
||||
of a "Room".
|
||||
|
||||
Room structure
|
||||
--------------
|
||||
|
||||
A room is a conceptual place where users can send and receive events. Rooms
|
||||
can be created, joined and left. Events are sent to a room, and all
|
||||
participants in that room will receive the event. Rooms are uniquely
|
||||
identified via a "Room ID", which look like::
|
||||
|
||||
!opaque_id:domain
|
||||
|
||||
There is exactly one room ID for each room. Whilst the room ID does contain a
|
||||
domain, it is simply for namespacing room IDs. The room does NOT reside on the
|
||||
domain specified. Room IDs are not meant to be human readable.
|
||||
|
||||
The following diagram shows an ``m.room.message`` event being sent in the room
|
||||
``!qporfwt:matrix.org``::
|
||||
|
||||
{ @alice:matrix.org } { @bob:domain.com }
|
||||
| ^
|
||||
| |
|
||||
Room ID: !qporfwt:matrix.org Room ID: !qporfwt:matrix.org
|
||||
Event type: m.room.message Event type: m.room.message
|
||||
Content: { JSON object } Content: { JSON object }
|
||||
| |
|
||||
V |
|
||||
+------------------+ +------------------+
|
||||
| Home Server | | Home Server |
|
||||
| matrix.org |<-------Federation------->| domain.com |
|
||||
+------------------+ +------------------+
|
||||
| ................................. |
|
||||
|______| Partially Shared State |_______|
|
||||
| Room ID: !qporfwt:matrix.org |
|
||||
| Servers: matrix.org, domain.com |
|
||||
| Members: |
|
||||
| - @alice:matrix.org |
|
||||
| - @bob:domain.com |
|
||||
|.................................|
|
||||
|
||||
Federation maintains shared state between multiple home servers, such that when an event is
|
||||
sent to a room, the home server knows where to forward the event on to, and how to process
|
||||
the event. Home servers do not need to have completely shared state in order to participate
|
||||
in a room. State is scoped to a single room, and federation ensures that all home servers
|
||||
have the information they need, even if that means the home server has to request more
|
||||
information from another home server before processing the event.
|
||||
|
||||
Room Aliases
|
||||
------------
|
||||
|
||||
Each room can also have multiple "Room Aliases", which looks like::
|
||||
|
||||
#room_alias:domain
|
||||
|
||||
A room alias "points" to a room ID. The room ID the alias is pointing to can be obtained
|
||||
by visiting the domain specified. Room aliases are designed to be human readable strings
|
||||
which can be used to publicise rooms. Note that the mapping from a room alias to a
|
||||
room ID is not fixed, and may change over time to point to a different room ID. For this
|
||||
reason, Clients SHOULD resolve the room alias to a room ID once and then use that ID on
|
||||
subsequent requests.
|
||||
|
||||
::
|
||||
|
||||
GET
|
||||
#matrix:domain.com !aaabaa:matrix.org
|
||||
| ^
|
||||
| |
|
||||
_______V____________________|____
|
||||
| domain.com |
|
||||
| Mappings: |
|
||||
| #matrix >> !aaabaa:matrix.org |
|
||||
| #golf >> !wfeiofh:sport.com |
|
||||
| #bike >> !4rguxf:matrix.org |
|
||||
|________________________________|
|
||||
|
||||
|
||||
Identity
|
||||
--------
|
||||
- Identity in relation to 3PIDs. Discovery of users based on 3PIDs.
|
||||
- Identity servers; trusted clique of servers which replicate content.
|
||||
- They govern the mapping of 3PIDs to user IDs and the creation of said mappings.
|
||||
- Not strictly required in order to communicate.
|
||||
|
||||
|
||||
API Standards
|
||||
-------------
|
||||
- All HTTP[S]
|
||||
- Uses JSON as HTTP bodies
|
||||
- Standard error response format { errcode: M_WHATEVER, error: "some message" }
|
||||
- C-S API provides POST for operations, or PUT with txn IDs. Explain txn IDs.
|
||||
|
||||
Receiving live updates on a client
|
||||
----------------------------------
|
||||
- C-S longpoll event stream
|
||||
- Concept of start/end tokens.
|
||||
- Mention /initialSync to get token.
|
||||
|
||||
|
||||
Rooms
|
||||
=====
|
||||
- How are they created? PDU anchor point: "root of the tree".
|
||||
- Adding / removing aliases.
|
||||
- Invite/join dance
|
||||
- State and non-state data (+extensibility)
|
||||
|
||||
TODO : Room permissions / config / power levels.
|
||||
|
||||
Messages
|
||||
========
|
||||
|
||||
This specification outlines several standard event types, all of which are
|
||||
prefixed with ``m.``
|
||||
|
||||
State messages
|
||||
--------------
|
||||
- m.room.name
|
||||
- m.room.topic
|
||||
- m.room.member
|
||||
- m.room.config
|
||||
- m.room.invite_join
|
||||
|
||||
What are they, when are they used, what do they contain, how should they be used
|
||||
|
||||
Non-state messages
|
||||
------------------
|
||||
- m.room.message
|
||||
- m.room.message.feedback (and compressed format)
|
||||
|
||||
What are they, when are they used, what do they contain, how should they be used
|
||||
|
||||
m.room.message msgtypes
|
||||
-----------------------
|
||||
Each ``m.room.message`` MUST have a ``msgtype`` key which identifies the type of
|
||||
message being sent. Each type has their own required and optional keys, as outlined
|
||||
below:
|
||||
|
||||
``m.text``
|
||||
Required keys:
|
||||
- ``body`` : "string" - The body of the message.
|
||||
Optional keys:
|
||||
None.
|
||||
Example:
|
||||
``{ "msgtype": "m.text", "body": "I am a fish" }``
|
||||
|
||||
``m.emote``
|
||||
Required keys:
|
||||
- ``body`` : "string" - The emote action to perform.
|
||||
Optional keys:
|
||||
None.
|
||||
Example:
|
||||
``{ "msgtype": "m.emote", "body": "tries to come up with a witty explanation" }``
|
||||
|
||||
``m.image``
|
||||
Required keys:
|
||||
- ``url`` : "string" - The URL to the image.
|
||||
Optional keys:
|
||||
- ``info`` : "string" - info : JSON object (ImageInfo) - The image info for image
|
||||
referred to in ``url``.
|
||||
- ``thumbnail_url`` : "string" - The URL to the thumbnail.
|
||||
- ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
|
||||
referred to in ``thumbnail_url``.
|
||||
- ``body`` : "string" - The alt text of the image, or some kind of content
|
||||
description for accessibility e.g. "image attachment".
|
||||
|
||||
ImageInfo:
|
||||
Information about an image::
|
||||
|
||||
{
|
||||
"size" : integer (size of image in bytes),
|
||||
"w" : integer (width of image in pixels),
|
||||
"h" : integer (height of image in pixels),
|
||||
"mimetype" : "string (e.g. image/jpeg)",
|
||||
}
|
||||
|
||||
``m.audio``
|
||||
Required keys:
|
||||
- ``url`` : "string" - The URL to the audio.
|
||||
Optional keys:
|
||||
- ``info`` : JSON object (AudioInfo) - The audio info for the audio referred to in
|
||||
``url``.
|
||||
- ``body`` : "string" - A description of the audio e.g. "Bee Gees -
|
||||
Stayin' Alive", or some kind of content description for accessibility e.g.
|
||||
"audio attachment".
|
||||
AudioInfo:
|
||||
Information about a piece of audio::
|
||||
|
||||
{
|
||||
"mimetype" : "string (e.g. audio/aac)",
|
||||
"size" : integer (size of audio in bytes),
|
||||
"duration" : integer (duration of audio in milliseconds),
|
||||
}
|
||||
|
||||
``m.video``
|
||||
Required keys:
|
||||
- ``url`` : "string" - The URL to the video.
|
||||
Optional keys:
|
||||
- ``info`` : JSON object (VideoInfo) - The video info for the video referred to in
|
||||
``url``.
|
||||
- ``body`` : "string" - A description of the video e.g. "Gangnam style",
|
||||
or some kind of content description for accessibility e.g. "video attachment".
|
||||
|
||||
VideoInfo:
|
||||
Information about a video::
|
||||
|
||||
{
|
||||
"mimetype" : "string (e.g. video/mp4)",
|
||||
"size" : integer (size of video in bytes),
|
||||
"duration" : integer (duration of video in milliseconds),
|
||||
"w" : integer (width of video in pixels),
|
||||
"h" : integer (height of video in pixels),
|
||||
"thumbnail_url" : "string (URL to image)",
|
||||
"thumbanil_info" : JSON object (ImageInfo)
|
||||
}
|
||||
|
||||
``m.location``
|
||||
Required keys:
|
||||
- ``geo_uri`` : "string" - The geo URI representing the location.
|
||||
Optional keys:
|
||||
- ``thumbnail_url`` : "string" - The URL to a thumnail of the location being
|
||||
represented.
|
||||
- ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
|
||||
referred to in ``thumbnail_url``.
|
||||
- ``body`` : "string" - A description of the location e.g. "Big Ben,
|
||||
London, UK", or some kind of content description for accessibility e.g.
|
||||
"location attachment".
|
||||
|
||||
The following keys can be attached to any ``m.room.message``:
|
||||
|
||||
Optional keys:
|
||||
- ``sender_ts`` : integer - A timestamp (ms resolution) representing the
|
||||
wall-clock time when the message was sent from the client.
|
||||
|
||||
Presence
|
||||
========
|
||||
|
||||
Each user has the concept of presence information. This encodes the
|
||||
"availability" of that user, suitable for display on other user's clients. This
|
||||
is transmitted as an ``m.presence`` event and is one of the few events which
|
||||
are sent *outside the context of a room*. The basic piece of presence information
|
||||
is represented by the ``state`` key, which is an enum of one of the following:
|
||||
|
||||
- ``online`` : The default state when the user is connected to an event stream.
|
||||
- ``unavailable`` : The user is not reachable at this time.
|
||||
- ``offline`` : The user is not connected to an event stream.
|
||||
- ``free_for_chat`` : The user is generally willing to receive messages
|
||||
moreso than default.
|
||||
- ``hidden`` : TODO. Behaves as offline, but allows the user to see the client
|
||||
state anyway and generally interact with client features.
|
||||
|
||||
This basic ``state`` field applies to the user as a whole, regardless of how many
|
||||
client devices they have connected. The home server should synchronise this
|
||||
status choice among multiple devices to ensure the user gets a consistent
|
||||
experience.
|
||||
|
||||
Idle Time
|
||||
---------
|
||||
As well as the basic ``state`` field, the presence information can also show a sense
|
||||
of an "idle timer". This should be maintained individually by the user's
|
||||
clients, and the home server can take the highest reported time as that to
|
||||
report. When a user is offline, the home server can still report when the user was last
|
||||
seen online.
|
||||
|
||||
Transmission
|
||||
------------
|
||||
- Transmitted as an EDU.
|
||||
- Presence lists determine who to send to.
|
||||
|
||||
Presence List
|
||||
-------------
|
||||
Each user's home server stores a "presence list" for that user. This stores a
|
||||
list of other user IDs the user has chosen to add to it. To be added to this
|
||||
list, the user being added must receive permission from the list owner. Once
|
||||
granted, both user's HS(es) store this information. Since such subscriptions
|
||||
are likely to be bidirectional, HSes may wish to automatically accept requests
|
||||
when a reverse subscription already exists.
|
||||
|
||||
Presence and Permissions
|
||||
------------------------
|
||||
For a viewing user to be allowed to see the presence information of a target
|
||||
user, either:
|
||||
|
||||
- The target user has allowed the viewing user to add them to their presence
|
||||
list, or
|
||||
- The two users share at least one room in common
|
||||
|
||||
In the latter case, this allows for clients to display some minimal sense of
|
||||
presence information in a user list for a room.
|
||||
|
||||
Typing notifications
|
||||
====================
|
||||
|
||||
TODO : Leo
|
||||
|
||||
Voice over IP
|
||||
=============
|
||||
|
||||
TODO : Dave
|
||||
|
||||
Profiles
|
||||
========
|
||||
|
||||
Internally within Matrix users are referred to by their user ID, which is not a
|
||||
human-friendly string. Profiles grant users the ability to see human-readable
|
||||
names for other users that are in some way meaningful to them. Additionally,
|
||||
profiles can publish additional information, such as the user's age or location.
|
||||
|
||||
A Profile consists of a display name, an avatar picture, and a set of other
|
||||
metadata fields that the user may wish to publish (email address, phone
|
||||
numbers, website URLs, etc...). This specification puts no requirements on the
|
||||
display name other than it being a valid unicode string.
|
||||
|
||||
- Metadata extensibility
|
||||
- Bundled with which events? e.g. m.room.member
|
||||
- Generate own events? What type?
|
||||
|
||||
Registration and login
|
||||
======================
|
||||
|
||||
Clients must register with a home server in order to use Matrix. After
|
||||
registering, the client will be given an access token which must be used in ALL
|
||||
requests to that home server as a query parameter 'access_token'.
|
||||
|
||||
- TODO Kegan : Make registration like login (just omit the "user" key on the
|
||||
initial request?)
|
||||
|
||||
If the client has already registered, they need to be able to login to their
|
||||
account. The home server may provide many different ways of logging in, such
|
||||
as user/password auth, login via a social network (OAuth2), login by confirming
|
||||
a token sent to their email address, etc. This specification does not define how
|
||||
home servers should authorise their users who want to login to their existing
|
||||
accounts, but instead defines the standard interface which implementations
|
||||
should follow so that ANY client can login to ANY home server.
|
||||
|
||||
The login process breaks down into the following:
|
||||
1. Determine the requirements for logging in.
|
||||
2. Submit the login stage credentials.
|
||||
3. Get credentials or be told the next stage in the login process and repeat
|
||||
step 2.
|
||||
|
||||
As each home server may have different ways of logging in, the client needs to know how
|
||||
they should login. All distinct login stages MUST have a corresponding ``type``.
|
||||
A ``type`` is a namespaced string which details the mechanism for logging in.
|
||||
|
||||
A client may be able to login via multiple valid login flows, and should choose a single
|
||||
flow when logging in. A flow is a series of login stages. The home server MUST respond
|
||||
with all the valid login flows when requested::
|
||||
|
||||
The client can login via 3 paths: 1a and 1b, 2a and 2b, or 3. The client should
|
||||
select one of these paths.
|
||||
|
||||
{
|
||||
"flows": [
|
||||
{
|
||||
"type": "<login type1a>",
|
||||
"stages": [ "<login type 1a>", "<login type 1b>" ]
|
||||
},
|
||||
{
|
||||
"type": "<login type2a>",
|
||||
"stages": [ "<login type 2a>", "<login type 2b>" ]
|
||||
},
|
||||
{
|
||||
"type": "<login type3>"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
After the login is completed, the client's fully-qualified user ID and a new access
|
||||
token MUST be returned::
|
||||
|
||||
{
|
||||
"user_id": "@user:matrix.org",
|
||||
"access_token": "abcdef0123456789"
|
||||
}
|
||||
|
||||
The ``user_id`` key is particularly useful if the home server wishes to support
|
||||
localpart entry of usernames (e.g. "user" rather than "@user:matrix.org"), as the
|
||||
client may not be able to determine its ``user_id`` in this case.
|
||||
|
||||
If a login has multiple requests, the home server may wish to create a session. If
|
||||
a home server responds with a 'session' key to a request, clients MUST submit it in
|
||||
subsequent requests until the login is completed::
|
||||
|
||||
{
|
||||
"session": "<session id>"
|
||||
}
|
||||
|
||||
This specification defines the following login types:
|
||||
- ``m.login.password``
|
||||
- ``m.login.oauth2``
|
||||
- ``m.login.email.code``
|
||||
- ``m.login.email.url``
|
||||
|
||||
|
||||
Password-based
|
||||
--------------
|
||||
:Type:
|
||||
m.login.password
|
||||
:Description:
|
||||
Login is supported via a username and password.
|
||||
|
||||
To respond to this type, reply with::
|
||||
|
||||
{
|
||||
"type": "m.login.password",
|
||||
"user": "<user_id or user localpart>",
|
||||
"password": "<password>"
|
||||
}
|
||||
|
||||
The home server MUST respond with either new credentials, the next stage of the login
|
||||
process, or a standard error response.
|
||||
|
||||
OAuth2-based
|
||||
------------
|
||||
:Type:
|
||||
m.login.oauth2
|
||||
:Description:
|
||||
Login is supported via OAuth2 URLs. This login consists of multiple requests.
|
||||
|
||||
To respond to this type, reply with::
|
||||
|
||||
{
|
||||
"type": "m.login.oauth2",
|
||||
"user": "<user_id or user localpart>"
|
||||
}
|
||||
|
||||
The server MUST respond with::
|
||||
|
||||
{
|
||||
"uri": <Authorization Request URI OR service selection URI>
|
||||
}
|
||||
|
||||
The home server acts as a 'confidential' client for the purposes of OAuth2.
|
||||
If the uri is a ``sevice selection URI``, it MUST point to a webpage which prompts the
|
||||
user to choose which service to authorize with. On selection of a service, this
|
||||
MUST link through to an ``Authorization Request URI``. If there is only 1 service which the
|
||||
home server accepts when logging in, this indirection can be skipped and the
|
||||
"uri" key can be the ``Authorization Request URI``.
|
||||
|
||||
The client then visits the ``Authorization Request URI``, which then shows the OAuth2
|
||||
Allow/Deny prompt. Hitting 'Allow' returns the ``redirect URI`` with the auth code.
|
||||
Home servers can choose any path for the ``redirect URI``. The client should visit
|
||||
the ``redirect URI``, which will then finish the OAuth2 login process, granting the
|
||||
home server an access token for the chosen service. When the home server gets
|
||||
this access token, it verifies that the cilent has authorised with the 3rd party, and
|
||||
can now complete the login. The OAuth2 ``redirect URI`` (with auth code) MUST respond
|
||||
with either new credentials, the next stage of the login process, or a standard error
|
||||
response.
|
||||
|
||||
For example, if a home server accepts OAuth2 from Google, it would return the
|
||||
Authorization Request URI for Google::
|
||||
|
||||
{
|
||||
"uri": "https://accounts.google.com/o/oauth2/auth?response_type=code&
|
||||
client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&scope=photos"
|
||||
}
|
||||
|
||||
The client then visits this URI and authorizes the home server. The client then
|
||||
visits the REDIRECT_URI with the auth code= query parameter which returns::
|
||||
|
||||
{
|
||||
"user_id": "@user:matrix.org",
|
||||
"access_token": "0123456789abcdef"
|
||||
}
|
||||
|
||||
Email-based (code)
|
||||
------------------
|
||||
:Type:
|
||||
m.login.email.code
|
||||
:Description:
|
||||
Login is supported by typing in a code which is sent in an email. This login
|
||||
consists of multiple requests.
|
||||
|
||||
To respond to this type, reply with::
|
||||
|
||||
{
|
||||
"type": "m.login.email.code",
|
||||
"user": "<user_id or user localpart>",
|
||||
"email": "<email address>"
|
||||
}
|
||||
|
||||
After validating the email address, the home server MUST send an email containing
|
||||
an authentication code and return::
|
||||
|
||||
{
|
||||
"type": "m.login.email.code",
|
||||
"session": "<session id>"
|
||||
}
|
||||
|
||||
The second request in this login stage involves sending this authentication code::
|
||||
|
||||
{
|
||||
"type": "m.login.email.code",
|
||||
"session": "<session id>",
|
||||
"code": "<code in email sent>"
|
||||
}
|
||||
|
||||
The home server MUST respond to this with either new credentials, the next stage of
|
||||
the login process, or a standard error response.
|
||||
|
||||
Email-based (url)
|
||||
-----------------
|
||||
:Type:
|
||||
m.login.email.url
|
||||
:Description:
|
||||
Login is supported by clicking on a URL in an email. This login consists of
|
||||
multiple requests.
|
||||
|
||||
To respond to this type, reply with::
|
||||
|
||||
{
|
||||
"type": "m.login.email.url",
|
||||
"user": "<user_id or user localpart>",
|
||||
"email": "<email address>"
|
||||
}
|
||||
|
||||
After validating the email address, the home server MUST send an email containing
|
||||
an authentication URL and return::
|
||||
|
||||
{
|
||||
"type": "m.login.email.url",
|
||||
"session": "<session id>"
|
||||
}
|
||||
|
||||
The email contains a URL which must be clicked. After it has been clicked, the
|
||||
client should perform another request::
|
||||
|
||||
{
|
||||
"type": "m.login.email.url",
|
||||
"session": "<session id>"
|
||||
}
|
||||
|
||||
The home server MUST respond to this with either new credentials, the next stage of
|
||||
the login process, or a standard error response.
|
||||
|
||||
A common client implementation will be to periodically poll until the link is clicked.
|
||||
If the link has not been visited yet, a standard error response with an errcode of
|
||||
``M_LOGIN_EMAIL_URL_NOT_YET`` should be returned.
|
||||
|
||||
|
||||
N-Factor Authentication
|
||||
-----------------------
|
||||
Multiple login stages can be combined to create N-factor authentication during login.
|
||||
|
||||
This can be achieved by responding with the ``next`` login type on completion of a
|
||||
previous login stage::
|
||||
|
||||
{
|
||||
"next": "<next login type>"
|
||||
}
|
||||
|
||||
If a home server implements N-factor authentication, it MUST respond with all
|
||||
``stages`` when initially queried for their login requirements::
|
||||
|
||||
{
|
||||
"type": "<1st login type>",
|
||||
"stages": [ <1st login type>, <2nd login type>, ... , <Nth login type> ]
|
||||
}
|
||||
|
||||
This can be represented conceptually as::
|
||||
|
||||
_______________________
|
||||
| Login Stage 1 |
|
||||
| type: "<login type1>" |
|
||||
| ___________________ |
|
||||
| |_Request_1_________| | <-- Returns "session" key which is used throughout.
|
||||
| ___________________ |
|
||||
| |_Request_2_________| | <-- Returns a "next" value of "login type2"
|
||||
|_______________________|
|
||||
|
|
||||
|
|
||||
_________V_____________
|
||||
| Login Stage 2 |
|
||||
| type: "<login type2>" |
|
||||
| ___________________ |
|
||||
| |_Request_1_________| |
|
||||
| ___________________ |
|
||||
| |_Request_2_________| |
|
||||
| ___________________ |
|
||||
| |_Request_3_________| | <-- Returns a "next" value of "login type3"
|
||||
|_______________________|
|
||||
|
|
||||
|
|
||||
_________V_____________
|
||||
| Login Stage 3 |
|
||||
| type: "<login type3>" |
|
||||
| ___________________ |
|
||||
| |_Request_1_________| | <-- Returns user credentials
|
||||
|_______________________|
|
||||
|
||||
Fallback
|
||||
--------
|
||||
Clients cannot be expected to be able to know how to process every single
|
||||
login type. If a client determines it does not know how to handle a given
|
||||
login type, it should request a login fallback page::
|
||||
|
||||
GET matrix/client/api/v1/login/fallback
|
||||
|
||||
This MUST return an HTML page which can perform the entire login process.
|
||||
|
||||
Identity
|
||||
========
|
||||
|
||||
TODO : Dave
|
||||
- 3PIDs and identity server, functions
|
||||
|
||||
Federation
|
||||
==========
|
||||
|
||||
Federation is the term used to describe how to communicate between Matrix home
|
||||
servers. Federation is a mechanism by which two home servers can exchange
|
||||
Matrix event messages, both as a real-time push of current events, and as a
|
||||
historic fetching mechanism to synchronise past history for clients to view. It
|
||||
uses HTTP connections between each pair of servers involved as the underlying
|
||||
transport. Messages are exchanged between servers in real-time by active pushing
|
||||
from each server's HTTP client into the server of the other. Queries to fetch
|
||||
historic data for the purpose of back-filling scrollback buffers and the like
|
||||
can also be performed.
|
||||
|
||||
There are three main kinds of communication that occur between home servers:
|
||||
|
||||
:Queries:
|
||||
These are single request/response interactions between a given pair of
|
||||
servers, initiated by one side sending an HTTP GET request to obtain some
|
||||
information, and responded by the other. They are not persisted and contain
|
||||
no long-term significant history. They simply request a snapshot state at the
|
||||
instant the query is made.
|
||||
|
||||
:Ephemeral Data Units (EDUs):
|
||||
These are notifications of events that are pushed from one home server to
|
||||
another. They are not persisted and contain no long-term significant history,
|
||||
nor does the receiving home server have to reply to them.
|
||||
|
||||
:Persisted Data Units (PDUs):
|
||||
These are notifications of events that are broadcast from one home server to
|
||||
any others that are interested in the same "context" (namely, a Room ID).
|
||||
They are persisted to long-term storage and form the record of history for
|
||||
that context.
|
||||
|
||||
EDUs and PDUs are further wrapped in an envelope called a Transaction, which is
|
||||
transferred from the origin to the destination home server using an HTTP PUT request.
|
||||
|
||||
|
||||
Transactions
|
||||
------------
|
||||
The transfer of EDUs and PDUs between home servers is performed by an exchange
|
||||
of Transaction messages, which are encoded as JSON objects, passed over an
|
||||
HTTP PUT request. A Transaction is meaningful only to the pair of home servers that
|
||||
exchanged it; they are not globally-meaningful.
|
||||
|
||||
Each transaction has:
|
||||
- An opaque transaction ID.
|
||||
- A timestamp (UNIX epoch time in milliseconds) generated by its origin server.
|
||||
- An origin and destination server name.
|
||||
- A list of "previous IDs".
|
||||
- A list of PDUs and EDUs - the actual message payload that the Transaction carries.
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"transaction_id":"916d630ea616342b42e98a3be0b74113",
|
||||
"ts":1404835423000,
|
||||
"origin":"red",
|
||||
"destination":"blue",
|
||||
"prev_ids":["e1da392e61898be4d2009b9fecce5325"],
|
||||
"pdus":[...],
|
||||
"edus":[...]
|
||||
}
|
||||
|
||||
The ``prev_ids`` field contains a list of previous transaction IDs that
|
||||
the ``origin`` server has sent to this ``destination``. Its purpose is to act as a
|
||||
sequence checking mechanism - the destination server can check whether it has
|
||||
successfully received that Transaction, or ask for a retransmission if not.
|
||||
|
||||
The ``pdus`` field of a transaction is a list, containing zero or more PDUs.[*]
|
||||
Each PDU is itself a JSON object containing a number of keys, the exact details of
|
||||
which will vary depending on the type of PDU. Similarly, the ``edus`` field is
|
||||
another list containing the EDUs. This key may be entirely absent if there are
|
||||
no EDUs to transfer.
|
||||
|
||||
(* Normally the PDU list will be non-empty, but the server should cope with
|
||||
receiving an "empty" transaction, as this is useful for informing peers of other
|
||||
transaction IDs they should be aware of. This effectively acts as a push
|
||||
mechanism to encourage peers to continue to replicate content.)
|
||||
|
||||
PDUs and EDUs
|
||||
-------------
|
||||
|
||||
All PDUs have:
|
||||
- An ID
|
||||
- A context
|
||||
- A declaration of their type
|
||||
- A list of other PDU IDs that have been seen recently on that context (regardless of which origin
|
||||
sent them)
|
||||
|
||||
[[TODO(paul): Update this structure so that 'pdu_id' is a two-element
|
||||
[origin,ref] pair like the prev_pdus are]]
|
||||
|
||||
::
|
||||
|
||||
{
|
||||
"pdu_id":"a4ecee13e2accdadf56c1025af232176",
|
||||
"context":"#example.green",
|
||||
"origin":"green",
|
||||
"ts":1404838188000,
|
||||
"pdu_type":"m.text",
|
||||
"prev_pdus":[["blue","99d16afbc857975916f1d73e49e52b65"]],
|
||||
"content":...
|
||||
"is_state":false
|
||||
}
|
||||
|
||||
In contrast to Transactions, it is important to note that the ``prev_pdus``
|
||||
field of a PDU refers to PDUs that any origin server has sent, rather than
|
||||
previous IDs that this ``origin`` has sent. This list may refer to other PDUs sent
|
||||
by the same origin as the current one, or other origins.
|
||||
|
||||
Because of the distributed nature of participants in a Matrix conversation, it
|
||||
is impossible to establish a globally-consistent total ordering on the events.
|
||||
However, by annotating each outbound PDU at its origin with IDs of other PDUs it
|
||||
has received, a partial ordering can be constructed allowing causallity
|
||||
relationships to be preserved. A client can then display these messages to the
|
||||
end-user in some order consistent with their content and ensure that no message
|
||||
that is semantically in reply of an earlier one is ever displayed before it.
|
||||
|
||||
PDUs fall into two main categories: those that deliver Events, and those that
|
||||
synchronise State. For PDUs that relate to State synchronisation, additional
|
||||
keys exist to support this:
|
||||
|
||||
::
|
||||
|
||||
{...,
|
||||
"is_state":true,
|
||||
"state_key":TODO
|
||||
"power_level":TODO
|
||||
"prev_state_id":TODO
|
||||
"prev_state_origin":TODO}
|
||||
|
||||
[[TODO(paul): At this point we should probably have a long description of how
|
||||
State management works, with descriptions of clobbering rules, power levels, etc
|
||||
etc... But some of that detail is rather up-in-the-air, on the whiteboard, and
|
||||
so on. This part needs refining. And writing in its own document as the details
|
||||
relate to the server/system as a whole, not specifically to server-server
|
||||
federation.]]
|
||||
|
||||
EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
|
||||
"previous" IDs. The only mandatory fields for these are the type, origin and
|
||||
destination home server names, and the actual nested content.
|
||||
|
||||
::
|
||||
|
||||
{"edu_type":"m.presence",
|
||||
"origin":"blue",
|
||||
"destination":"orange",
|
||||
"content":...}
|
||||
|
||||
Backfilling
|
||||
-----------
|
||||
- What it is, when is it used, how is it done
|
||||
|
||||
SRV Records
|
||||
-----------
|
||||
- Why it is needed
|
||||
|
||||
Security
|
||||
========
|
||||
- rate limiting
|
||||
- crypto (s-s auth)
|
||||
- E2E
|
||||
- Lawful intercept + Key Escrow
|
||||
|
||||
TODO Mark
|
||||
|
||||
Policy Servers
|
||||
==============
|
||||
TODO
|
||||
|
||||
Content repository
|
||||
==================
|
||||
- thumbnail paths
|
||||
|
||||
Address book repository
|
||||
=======================
|
||||
- format
|
||||
|
||||
|
||||
Glossary
|
||||
========
|
||||
- domain specific words/acronyms with definitions
|
||||
|
||||
User ID:
|
||||
An opaque ID which identifies an end-user, which consists of some opaque
|
||||
localpart combined with the domain name of their home server.
|
||||
2
setup.py
Normal file → Executable file
2
setup.py
Normal file → Executable file
@@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
""" This is a reference implementation of a synapse home server.
|
||||
"""
|
||||
|
||||
__version__ = "0.0.1"
|
||||
__version__ = "0.1.0"
|
||||
|
||||
@@ -19,8 +19,7 @@ from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.errors import AuthError, StoreError, Codes
|
||||
from synapse.api.events.room import (RoomTopicEvent, RoomMemberEvent,
|
||||
MessageEvent, FeedbackEvent)
|
||||
from synapse.api.events.room import RoomMemberEvent
|
||||
|
||||
import logging
|
||||
|
||||
@@ -34,7 +33,7 @@ class Auth(object):
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def check(self, event, raises=False):
|
||||
def check(self, event, snapshot, raises=False):
|
||||
""" Checks if this event is correctly authed.
|
||||
|
||||
Returns:
|
||||
@@ -44,15 +43,19 @@ class Auth(object):
|
||||
be raised only if raises=True.
|
||||
"""
|
||||
try:
|
||||
if event.type in [RoomTopicEvent.TYPE, MessageEvent.TYPE,
|
||||
FeedbackEvent.TYPE]:
|
||||
yield self.check_joined_room(event.room_id, event.user_id)
|
||||
defer.returnValue(True)
|
||||
elif event.type == RoomMemberEvent.TYPE:
|
||||
allowed = yield self.is_membership_change_allowed(event)
|
||||
defer.returnValue(allowed)
|
||||
if hasattr(event, "room_id"):
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
allowed = yield self.is_membership_change_allowed(event)
|
||||
defer.returnValue(allowed)
|
||||
else:
|
||||
self._check_joined_room(
|
||||
member=snapshot.membership_state,
|
||||
user_id=snapshot.user_id,
|
||||
room_id=snapshot.room_id,
|
||||
)
|
||||
defer.returnValue(True)
|
||||
else:
|
||||
raise AuthError(500, "Unknown event type %s" % event.type)
|
||||
raise AuthError(500, "Unknown event: %s" % event)
|
||||
except AuthError as e:
|
||||
logger.info("Event auth check failed on event %s with msg: %s",
|
||||
event, e.msg)
|
||||
@@ -67,16 +70,22 @@ class Auth(object):
|
||||
room_id=room_id,
|
||||
user_id=user_id
|
||||
)
|
||||
if not member or member.membership != Membership.JOIN:
|
||||
raise AuthError(403, "User %s not in room %s" %
|
||||
(user_id, room_id))
|
||||
self._check_joined_room(member, user_id, room_id)
|
||||
defer.returnValue(member)
|
||||
except AttributeError:
|
||||
pass
|
||||
defer.returnValue(None)
|
||||
|
||||
def _check_joined_room(self, member, user_id, room_id):
|
||||
if not member or member.membership != Membership.JOIN:
|
||||
raise AuthError(403, "User %s not in room %s (%s)" % (
|
||||
user_id, room_id, repr(member)
|
||||
))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def is_membership_change_allowed(self, event):
|
||||
target_user_id = event.state_key
|
||||
|
||||
# does this room even exist
|
||||
room = yield self.store.get_room(event.room_id)
|
||||
if not room:
|
||||
@@ -94,7 +103,7 @@ class Auth(object):
|
||||
# get info about the target
|
||||
try:
|
||||
target = yield self.store.get_room_member(
|
||||
user_id=event.target_user_id,
|
||||
user_id=target_user_id,
|
||||
room_id=event.room_id)
|
||||
except:
|
||||
target = None
|
||||
@@ -108,12 +117,12 @@ class Auth(object):
|
||||
raise AuthError(403, "You are not in room %s." % event.room_id)
|
||||
elif target_in_room: # the target is already in the room.
|
||||
raise AuthError(403, "%s is already in the room." %
|
||||
event.target_user_id)
|
||||
target_user_id)
|
||||
elif Membership.JOIN == membership:
|
||||
# Joins are valid iff caller == target and they were:
|
||||
# invited: They are accepting the invitation
|
||||
# joined: It's a NOOP
|
||||
if event.user_id != event.target_user_id:
|
||||
if event.user_id != target_user_id:
|
||||
raise AuthError(403, "Cannot force another user to join.")
|
||||
elif room.is_public:
|
||||
pass # anyone can join public rooms.
|
||||
@@ -123,10 +132,10 @@ class Auth(object):
|
||||
elif Membership.LEAVE == membership:
|
||||
if not caller_in_room: # trying to leave a room you aren't joined
|
||||
raise AuthError(403, "You are not in room %s." % event.room_id)
|
||||
elif event.target_user_id != event.user_id:
|
||||
elif target_user_id != event.user_id:
|
||||
# trying to force another user to leave
|
||||
raise AuthError(403, "Cannot force %s to leave." %
|
||||
event.target_user_id)
|
||||
target_user_id)
|
||||
else:
|
||||
raise AuthError(500, "Unknown membership %s" % membership)
|
||||
|
||||
@@ -161,6 +170,8 @@ class Auth(object):
|
||||
"""
|
||||
try:
|
||||
user_id = yield self.store.get_user_by_token(token=token)
|
||||
if not user_id:
|
||||
raise StoreError()
|
||||
defer.returnValue(self.hs.parse_userid(user_id))
|
||||
except StoreError:
|
||||
raise AuthError(403, "Unrecognised access token.",
|
||||
|
||||
@@ -23,6 +23,7 @@ class Membership(object):
|
||||
JOIN = u"join"
|
||||
KNOCK = u"knock"
|
||||
LEAVE = u"leave"
|
||||
LIST = (INVITE, JOIN, KNOCK, LEAVE)
|
||||
|
||||
|
||||
class Feedback(object):
|
||||
@@ -30,8 +31,8 @@ class Feedback(object):
|
||||
"""Represents the types of feedback a user can send in response to a
|
||||
message."""
|
||||
|
||||
DELIVERED = u"d"
|
||||
READ = u"r"
|
||||
DELIVERED = u"delivered"
|
||||
READ = u"read"
|
||||
LIST = (DELIVERED, READ)
|
||||
|
||||
|
||||
|
||||
@@ -41,11 +41,11 @@ class SynapseEvent(JsonEncodedObject):
|
||||
"room_id",
|
||||
"user_id", # sender/initiator
|
||||
"content", # HTTP body, JSON
|
||||
"state_key",
|
||||
]
|
||||
|
||||
internal_keys = [
|
||||
"is_state",
|
||||
"state_key",
|
||||
"prev_events",
|
||||
"prev_state",
|
||||
"depth",
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.api.constants import Feedback, Membership
|
||||
from synapse.api.errors import SynapseError
|
||||
from . import SynapseEvent
|
||||
|
||||
|
||||
@@ -59,15 +61,15 @@ class RoomMemberEvent(SynapseEvent):
|
||||
TYPE = "m.room.member"
|
||||
|
||||
valid_keys = SynapseEvent.valid_keys + [
|
||||
"target_user_id", # target
|
||||
# target is the state_key
|
||||
"membership", # action
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if "target_user_id" in kwargs:
|
||||
kwargs["state_key"] = kwargs["target_user_id"]
|
||||
if "membership" not in kwargs:
|
||||
kwargs["membership"] = kwargs.get("content", {}).get("membership")
|
||||
if not kwargs["membership"] in Membership.LIST:
|
||||
raise SynapseError(400, "Bad membership value.")
|
||||
super(RoomMemberEvent, self).__init__(**kwargs)
|
||||
|
||||
def get_content_template(self):
|
||||
@@ -91,24 +93,26 @@ class MessageEvent(SynapseEvent):
|
||||
class FeedbackEvent(SynapseEvent):
|
||||
TYPE = "m.room.message.feedback"
|
||||
|
||||
valid_keys = SynapseEvent.valid_keys + [
|
||||
"msg_id", # the message ID being acknowledged
|
||||
"msg_sender_id", # person who is sending the feedback is 'user_id'
|
||||
"feedback_type", # the type of feedback (delivery, read, etc)
|
||||
]
|
||||
valid_keys = SynapseEvent.valid_keys
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(FeedbackEvent, self).__init__(**kwargs)
|
||||
if not kwargs["content"]["type"] in Feedback.LIST:
|
||||
raise SynapseError(400, "Bad feedback value.")
|
||||
|
||||
def get_content_template(self):
|
||||
return {}
|
||||
return {
|
||||
"type": u"string",
|
||||
"target_event_id": u"string",
|
||||
"msg_sender_id": u"string"
|
||||
}
|
||||
|
||||
|
||||
class InviteJoinEvent(SynapseEvent):
|
||||
TYPE = "m.room.invite_join"
|
||||
|
||||
valid_keys = SynapseEvent.valid_keys + [
|
||||
"target_user_id",
|
||||
# target_user_id is the state_key
|
||||
"target_host",
|
||||
]
|
||||
|
||||
|
||||
@@ -1,196 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.events.room import RoomMemberEvent
|
||||
from synapse.api.streams.event import EventsStreamData
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet import reactor
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Notifier(object):
|
||||
|
||||
def __init__(self, hs):
|
||||
self.store = hs.get_datastore()
|
||||
self.hs = hs
|
||||
self.stored_event_listeners = {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_new_room_event(self, event, store_id):
|
||||
"""Called when there is a new room event which may potentially be sent
|
||||
down listening users' event streams.
|
||||
|
||||
This function looks for interested *users* who may want to be notified
|
||||
for this event. This is different to users requesting from the event
|
||||
stream which looks for interested *events* for this user.
|
||||
|
||||
Args:
|
||||
event (SynapseEvent): The new event, which must have a room_id
|
||||
store_id (int): The ID of this event after it was stored with the
|
||||
data store.
|
||||
'"""
|
||||
member_list = yield self.store.get_room_members(room_id=event.room_id,
|
||||
membership="join")
|
||||
if not member_list:
|
||||
member_list = []
|
||||
|
||||
member_list = [u.user_id for u in member_list]
|
||||
|
||||
# invites MUST prod the person being invited, who won't be in the room.
|
||||
if (event.type == RoomMemberEvent.TYPE and
|
||||
event.content["membership"] == Membership.INVITE):
|
||||
member_list.append(event.target_user_id)
|
||||
# similarly, LEAVEs must be sent to the person leaving
|
||||
if (event.type == RoomMemberEvent.TYPE and
|
||||
event.content["membership"] == Membership.LEAVE):
|
||||
member_list.append(event.target_user_id)
|
||||
|
||||
for user_id in member_list:
|
||||
if user_id in self.stored_event_listeners:
|
||||
self._notify_and_callback(
|
||||
user_id=user_id,
|
||||
event_data=event.get_dict(),
|
||||
stream_type=EventsStreamData.EVENT_TYPE,
|
||||
store_id=store_id)
|
||||
|
||||
def on_new_user_event(self, user_id, event_data, stream_type, store_id):
|
||||
if user_id in self.stored_event_listeners:
|
||||
self._notify_and_callback(
|
||||
user_id=user_id,
|
||||
event_data=event_data,
|
||||
stream_type=stream_type,
|
||||
store_id=store_id
|
||||
)
|
||||
|
||||
def _notify_and_callback(self, user_id, event_data, stream_type, store_id):
|
||||
logger.debug(
|
||||
"Notifying %s of a new event.",
|
||||
user_id
|
||||
)
|
||||
|
||||
stream_ids = list(self.stored_event_listeners[user_id])
|
||||
for stream_id in stream_ids:
|
||||
self._notify_and_callback_stream(user_id, stream_id, event_data,
|
||||
stream_type, store_id)
|
||||
|
||||
if not self.stored_event_listeners[user_id]:
|
||||
del self.stored_event_listeners[user_id]
|
||||
|
||||
def _notify_and_callback_stream(self, user_id, stream_id, event_data,
|
||||
stream_type, store_id):
|
||||
|
||||
event_listener = self.stored_event_listeners[user_id].pop(stream_id)
|
||||
return_event_object = {
|
||||
k: event_listener[k] for k in ["start", "chunk", "end"]
|
||||
}
|
||||
|
||||
# work out the new end token
|
||||
token = event_listener["start"]
|
||||
end = self._next_token(stream_type, store_id, token)
|
||||
return_event_object["end"] = end
|
||||
|
||||
# add the event to the chunk
|
||||
chunk = event_listener["chunk"]
|
||||
chunk.append(event_data)
|
||||
|
||||
# callback the defer. We know this can't have been resolved before as
|
||||
# we always remove the event_listener from the map before resolving.
|
||||
event_listener["defer"].callback(return_event_object)
|
||||
|
||||
def _next_token(self, stream_type, store_id, current_token):
|
||||
stream_handler = self.hs.get_handlers().event_stream_handler
|
||||
return stream_handler.get_event_stream_token(
|
||||
stream_type,
|
||||
store_id,
|
||||
current_token
|
||||
)
|
||||
|
||||
def store_events_for(self, user_id=None, stream_id=None, from_tok=None):
|
||||
"""Store all incoming events for this user. This should be paired with
|
||||
get_events_for to return chunked data.
|
||||
|
||||
Args:
|
||||
user_id (str): The user to monitor incoming events for.
|
||||
stream (object): The stream that is receiving events
|
||||
from_tok (str): The token to monitor incoming events from.
|
||||
"""
|
||||
event_listener = {
|
||||
"start": from_tok,
|
||||
"chunk": [],
|
||||
"end": from_tok,
|
||||
"defer": defer.Deferred(),
|
||||
}
|
||||
|
||||
if user_id not in self.stored_event_listeners:
|
||||
self.stored_event_listeners[user_id] = {stream_id: event_listener}
|
||||
else:
|
||||
self.stored_event_listeners[user_id][stream_id] = event_listener
|
||||
|
||||
def purge_events_for(self, user_id=None, stream_id=None):
|
||||
"""Purges any stored events for this user.
|
||||
|
||||
Args:
|
||||
user_id (str): The user to purge stored events for.
|
||||
"""
|
||||
try:
|
||||
del self.stored_event_listeners[user_id][stream_id]
|
||||
if not self.stored_event_listeners[user_id]:
|
||||
del self.stored_event_listeners[user_id]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
def get_events_for(self, user_id=None, stream_id=None, timeout=0):
|
||||
"""Retrieve stored events for this user, waiting if necessary.
|
||||
|
||||
It is advisable to wrap this call in a maybeDeferred.
|
||||
|
||||
Args:
|
||||
user_id (str): The user to get events for.
|
||||
timeout (int): The time in seconds to wait before giving up.
|
||||
Returns:
|
||||
A Deferred or a dict containing the chunk data, depending on if
|
||||
there was data to return yet. The Deferred callback may be None if
|
||||
there were no events before the timeout expired.
|
||||
"""
|
||||
logger.debug("%s is listening for events.", user_id)
|
||||
|
||||
try:
|
||||
streams = self.stored_event_listeners[user_id][stream_id]["chunk"]
|
||||
if streams:
|
||||
logger.debug("%s returning existing chunk.", user_id)
|
||||
return streams
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
reactor.callLater(
|
||||
(timeout / 1000.0), self._timeout, user_id, stream_id
|
||||
)
|
||||
return self.stored_event_listeners[user_id][stream_id]["defer"]
|
||||
|
||||
def _timeout(self, user_id, stream_id):
|
||||
try:
|
||||
# We remove the event_listener from the map so that we can't
|
||||
# resolve the deferred twice.
|
||||
event_listeners = self.stored_event_listeners[user_id]
|
||||
event_listener = event_listeners.pop(stream_id)
|
||||
event_listener["defer"].callback(None)
|
||||
logger.debug("%s event listening timed out.", user_id)
|
||||
except KeyError:
|
||||
pass
|
||||
@@ -1,103 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
|
||||
|
||||
class PaginationConfig(object):
|
||||
|
||||
"""A configuration object which stores pagination parameters."""
|
||||
|
||||
def __init__(self, from_tok=None, to_tok=None, direction='f', limit=0):
|
||||
self.from_tok = from_tok
|
||||
self.to_tok = to_tok
|
||||
self.direction = direction
|
||||
self.limit = limit
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, request, raise_invalid_params=True):
|
||||
params = {
|
||||
"from_tok": "END",
|
||||
"direction": 'f',
|
||||
}
|
||||
|
||||
query_param_mappings = [ # 3-tuple of qp_key, attribute, rules
|
||||
("from", "from_tok", lambda x: type(x) == str),
|
||||
("to", "to_tok", lambda x: type(x) == str),
|
||||
("limit", "limit", lambda x: x.isdigit()),
|
||||
("dir", "direction", lambda x: x == 'f' or x == 'b'),
|
||||
]
|
||||
|
||||
for qp, attr, is_valid in query_param_mappings:
|
||||
if qp in request.args:
|
||||
if is_valid(request.args[qp][0]):
|
||||
params[attr] = request.args[qp][0]
|
||||
elif raise_invalid_params:
|
||||
raise SynapseError(400, "%s parameter is invalid." % qp)
|
||||
|
||||
return PaginationConfig(**params)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
"<PaginationConfig from_tok=%s, to_tok=%s, "
|
||||
"direction=%s, limit=%s>"
|
||||
) % (self.from_tok, self.to_tok, self.direction, self.limit)
|
||||
|
||||
|
||||
class PaginationStream(object):
|
||||
|
||||
""" An interface for streaming data as chunks. """
|
||||
|
||||
TOK_END = "END"
|
||||
|
||||
def get_chunk(self, config=None):
|
||||
""" Return the next chunk in the stream.
|
||||
|
||||
Args:
|
||||
config (PaginationConfig): The config to aid which chunk to get.
|
||||
Returns:
|
||||
A dict containing the new start token "start", the new end token
|
||||
"end" and the data "chunk" as a list.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class StreamData(object):
|
||||
|
||||
""" An interface for obtaining streaming data from a table. """
|
||||
|
||||
def __init__(self, hs):
|
||||
self.hs = hs
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
def get_rows(self, user_id, from_pkey, to_pkey, limit, direction):
|
||||
""" Get event stream data between the specified pkeys.
|
||||
|
||||
Args:
|
||||
user_id : The user's ID
|
||||
from_pkey : The starting pkey.
|
||||
to_pkey : The end pkey. May be -1 to mean "latest".
|
||||
limit: The max number of results to return.
|
||||
Returns:
|
||||
A tuple containing the list of event stream data and the last pkey.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def max_token(self):
|
||||
""" Get the latest currently-valid token.
|
||||
|
||||
Returns:
|
||||
The latest token."""
|
||||
raise NotImplementedError()
|
||||
@@ -1,197 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module contains classes for streaming from the event stream: /events.
|
||||
"""
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import EventStreamError
|
||||
from synapse.api.events import SynapseEvent
|
||||
from synapse.api.events.room import (
|
||||
RoomMemberEvent, MessageEvent, FeedbackEvent, RoomTopicEvent
|
||||
)
|
||||
from synapse.api.streams import PaginationStream, StreamData
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventsStreamData(StreamData):
|
||||
EVENT_TYPE = "EventsStream"
|
||||
|
||||
def __init__(self, hs, room_id=None, feedback=False):
|
||||
super(EventsStreamData, self).__init__(hs)
|
||||
self.room_id = room_id
|
||||
self.with_feedback = feedback
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_rows(self, user_id, from_key, to_key, limit, direction):
|
||||
data, latest_ver = yield self.store.get_room_events(
|
||||
user_id=user_id,
|
||||
from_key=from_key,
|
||||
to_key=to_key,
|
||||
limit=limit,
|
||||
room_id=self.room_id,
|
||||
with_feedback=self.with_feedback
|
||||
)
|
||||
defer.returnValue((data, latest_ver))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def max_token(self):
|
||||
val = yield self.store.get_room_events_max_id()
|
||||
defer.returnValue(val)
|
||||
|
||||
|
||||
class EventStream(PaginationStream):
|
||||
|
||||
SEPARATOR = '_'
|
||||
|
||||
def __init__(self, user_id, stream_data_list):
|
||||
super(EventStream, self).__init__()
|
||||
self.user_id = user_id
|
||||
self.stream_data = stream_data_list
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def fix_tokens(self, pagination_config):
|
||||
pagination_config.from_tok = yield self.fix_token(
|
||||
pagination_config.from_tok)
|
||||
pagination_config.to_tok = yield self.fix_token(
|
||||
pagination_config.to_tok)
|
||||
|
||||
if (
|
||||
not pagination_config.to_tok
|
||||
and pagination_config.direction == 'f'
|
||||
):
|
||||
pagination_config.to_tok = yield self.get_current_max_token()
|
||||
|
||||
logger.debug("pagination_config: %s", pagination_config)
|
||||
|
||||
defer.returnValue(pagination_config)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def fix_token(self, token):
|
||||
"""Fixes unknown values in a token to known values.
|
||||
|
||||
Args:
|
||||
token (str): The token to fix up.
|
||||
Returns:
|
||||
The fixed-up token, which may == token.
|
||||
"""
|
||||
if token == PaginationStream.TOK_END:
|
||||
new_token = yield self.get_current_max_token()
|
||||
|
||||
logger.debug("fix_token: From %s to %s", token, new_token)
|
||||
|
||||
token = new_token
|
||||
|
||||
defer.returnValue(token)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_current_max_token(self):
|
||||
new_token_parts = []
|
||||
for s in self.stream_data:
|
||||
mx = yield s.max_token()
|
||||
new_token_parts.append(str(mx))
|
||||
|
||||
new_token = EventStream.SEPARATOR.join(new_token_parts)
|
||||
|
||||
logger.debug("get_current_max_token: %s", new_token)
|
||||
|
||||
defer.returnValue(new_token)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_chunk(self, config):
|
||||
# no support for limit on >1 streams, makes no sense.
|
||||
if config.limit and len(self.stream_data) > 1:
|
||||
raise EventStreamError(
|
||||
400, "Limit not supported on multiplexed streams."
|
||||
)
|
||||
|
||||
chunk_data, next_tok = yield self._get_chunk_data(
|
||||
config.from_tok,
|
||||
config.to_tok,
|
||||
config.limit,
|
||||
config.direction,
|
||||
)
|
||||
|
||||
defer.returnValue({
|
||||
"chunk": chunk_data,
|
||||
"start": config.from_tok,
|
||||
"end": next_tok
|
||||
})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _get_chunk_data(self, from_tok, to_tok, limit, direction):
|
||||
""" Get event data between the two tokens.
|
||||
|
||||
Tokens are SEPARATOR separated values representing pkey values of
|
||||
certain tables, and the position determines the StreamData invoked
|
||||
according to the STREAM_DATA list.
|
||||
|
||||
The magic value '-1' can be used to get the latest value.
|
||||
|
||||
Args:
|
||||
from_tok - The token to start from.
|
||||
to_tok - The token to end at. Must have values > from_tok or be -1.
|
||||
Returns:
|
||||
A list of event data.
|
||||
Raises:
|
||||
EventStreamError if something went wrong.
|
||||
"""
|
||||
# sanity check
|
||||
if to_tok is not None:
|
||||
if (from_tok.count(EventStream.SEPARATOR) !=
|
||||
to_tok.count(EventStream.SEPARATOR) or
|
||||
(from_tok.count(EventStream.SEPARATOR) + 1) !=
|
||||
len(self.stream_data)):
|
||||
raise EventStreamError(400, "Token lengths don't match.")
|
||||
|
||||
chunk = []
|
||||
next_ver = []
|
||||
for i, (from_pkey, to_pkey) in enumerate(zip(
|
||||
self._split_token(from_tok),
|
||||
self._split_token(to_tok)
|
||||
)):
|
||||
if from_pkey == to_pkey:
|
||||
# tokens are the same, we have nothing to do.
|
||||
next_ver.append(str(to_pkey))
|
||||
continue
|
||||
|
||||
(event_chunk, max_pkey) = yield self.stream_data[i].get_rows(
|
||||
self.user_id, from_pkey, to_pkey, limit, direction,
|
||||
)
|
||||
|
||||
chunk.extend([
|
||||
e.get_dict() if isinstance(e, SynapseEvent) else e
|
||||
for e in event_chunk
|
||||
])
|
||||
next_ver.append(str(max_pkey))
|
||||
|
||||
defer.returnValue((chunk, EventStream.SEPARATOR.join(next_ver)))
|
||||
|
||||
def _split_token(self, token):
|
||||
"""Splits the given token into a list of pkeys.
|
||||
|
||||
Args:
|
||||
token (str): The token with SEPARATOR values.
|
||||
Returns:
|
||||
A list of ints.
|
||||
"""
|
||||
if token:
|
||||
segments = token.split(EventStream.SEPARATOR)
|
||||
else:
|
||||
segments = [None] * len(self.stream_data)
|
||||
return segments
|
||||
@@ -31,16 +31,34 @@ from synapse.api.urls import (
|
||||
)
|
||||
|
||||
from daemonize import Daemonize
|
||||
import twisted.manhole.telnet
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import logging.config
|
||||
import sqlite3
|
||||
import os
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
SCHEMAS = [
|
||||
"transactions",
|
||||
"pdu",
|
||||
"users",
|
||||
"profiles",
|
||||
"presence",
|
||||
"im",
|
||||
"room_aliases",
|
||||
]
|
||||
|
||||
|
||||
# Remember to update this number every time an incompatible change is made to
|
||||
# database schema files, so the users will be informed on server restarts.
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
|
||||
class SynapseHomeServer(HomeServer):
|
||||
|
||||
def build_http_client(self):
|
||||
@@ -63,31 +81,39 @@ class SynapseHomeServer(HomeServer):
|
||||
don't have to worry about overwriting existing content.
|
||||
"""
|
||||
logging.info("Preparing database: %s...", self.db_name)
|
||||
|
||||
with sqlite3.connect(self.db_name) as db_conn:
|
||||
c = db_conn.cursor()
|
||||
c.execute("PRAGMA user_version")
|
||||
row = c.fetchone()
|
||||
|
||||
if row and row[0]:
|
||||
user_version = row[0]
|
||||
|
||||
if user_version < SCHEMA_VERSION:
|
||||
# TODO(paul): add some kind of intelligent fixup here
|
||||
raise ValueError("Cannot use this database as the " +
|
||||
"schema version (%d) does not match (%d)" %
|
||||
(user_version, SCHEMA_VERSION)
|
||||
)
|
||||
|
||||
else:
|
||||
for sql_loc in SCHEMAS:
|
||||
sql_script = read_schema(sql_loc)
|
||||
|
||||
c.executescript(sql_script)
|
||||
db_conn.commit()
|
||||
|
||||
c.execute("PRAGMA user_version = %d" % SCHEMA_VERSION)
|
||||
|
||||
c.close()
|
||||
|
||||
logging.info("Database prepared in %s.", self.db_name)
|
||||
|
||||
pool = adbapi.ConnectionPool(
|
||||
'sqlite3', self.db_name, check_same_thread=False,
|
||||
cp_min=1, cp_max=1)
|
||||
|
||||
schemas = [
|
||||
"transactions",
|
||||
"pdu",
|
||||
"users",
|
||||
"profiles",
|
||||
"presence",
|
||||
"im",
|
||||
"room_aliases",
|
||||
]
|
||||
|
||||
for sql_loc in schemas:
|
||||
sql_script = read_schema(sql_loc)
|
||||
|
||||
with sqlite3.connect(self.db_name) as db_conn:
|
||||
c = db_conn.cursor()
|
||||
c.executescript(sql_script)
|
||||
c.close()
|
||||
db_conn.commit()
|
||||
|
||||
logging.info("Database prepared in %s.", self.db_name)
|
||||
|
||||
return pool
|
||||
|
||||
def create_resource_tree(self, web_client, redirect_root_to_web_client):
|
||||
@@ -182,6 +208,7 @@ class SynapseHomeServer(HomeServer):
|
||||
|
||||
def start_listening(self, port):
|
||||
reactor.listenTCP(port, Site(self.root_resource))
|
||||
logger.info("Synapse now listening on port %d", port)
|
||||
|
||||
|
||||
def setup_logging(verbosity=0, filename=None, config_path=None):
|
||||
@@ -237,6 +264,8 @@ def setup():
|
||||
default="hs.pid")
|
||||
parser.add_argument("-W", "--webclient", dest="webclient", default=True,
|
||||
action="store_false", help="Don't host a web client.")
|
||||
parser.add_argument("--manhole", dest="manhole", type=int, default=None,
|
||||
help="Turn on the twisted telnet manhole service.")
|
||||
args = parser.parse_args()
|
||||
|
||||
verbosity = int(args.verbose) if args.verbose else None
|
||||
@@ -255,16 +284,18 @@ def setup():
|
||||
|
||||
logger.info("Server hostname: %s", args.host)
|
||||
|
||||
if re.search(":[0-9]+$", args.host):
|
||||
domain_with_port = args.host
|
||||
else:
|
||||
domain_with_port = "%s:%s" % (args.host, args.port)
|
||||
|
||||
hs = SynapseHomeServer(
|
||||
args.host,
|
||||
domain_with_port=domain_with_port,
|
||||
upload_dir=os.path.abspath("uploads"),
|
||||
db_name=db_name,
|
||||
)
|
||||
|
||||
# This object doesn't need to be saved because it's set as the handler for
|
||||
# the replication layer
|
||||
hs.get_federation()
|
||||
|
||||
hs.register_servlets()
|
||||
|
||||
hs.create_resource_tree(
|
||||
@@ -272,7 +303,14 @@ def setup():
|
||||
redirect_root_to_web_client=True)
|
||||
hs.start_listening(args.port)
|
||||
|
||||
hs.build_db_pool()
|
||||
hs.get_db_pool()
|
||||
|
||||
if args.manhole:
|
||||
f = twisted.manhole.telnet.ShellFactory()
|
||||
f.username = "matrix"
|
||||
f.password = "rabbithole"
|
||||
f.namespace['hs'] = hs
|
||||
reactor.listenTCP(args.manhole, f, interface='127.0.0.1')
|
||||
|
||||
if args.daemonize:
|
||||
daemon = Daemonize(
|
||||
|
||||
@@ -1,157 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from .pdu_codec import PduCodec
|
||||
|
||||
from synapse.api.errors import AuthError
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FederationEventHandler(object):
|
||||
""" Responsible for:
|
||||
a) handling received Pdus before handing them on as Events to the rest
|
||||
of the home server (including auth and state conflict resoultion)
|
||||
b) converting events that were produced by local clients that may need
|
||||
to be sent to remote home servers.
|
||||
"""
|
||||
|
||||
def __init__(self, hs):
|
||||
self.store = hs.get_datastore()
|
||||
self.replication_layer = hs.get_replication_layer()
|
||||
self.state_handler = hs.get_state_handler()
|
||||
# self.auth_handler = gs.get_auth_handler()
|
||||
self.event_handler = hs.get_handlers().federation_handler
|
||||
self.server_name = hs.hostname
|
||||
|
||||
self.lock_manager = hs.get_room_lock_manager()
|
||||
|
||||
self.replication_layer.set_handler(self)
|
||||
|
||||
self.pdu_codec = PduCodec(hs)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def handle_new_event(self, event):
|
||||
""" 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
|
||||
remote home servers that may be interested.
|
||||
|
||||
Args:
|
||||
event
|
||||
|
||||
Returns:
|
||||
Deferred: Resolved when it has successfully been queued for
|
||||
processing.
|
||||
"""
|
||||
yield self.fill_out_prev_events(event)
|
||||
|
||||
pdu = self.pdu_codec.pdu_from_event(event)
|
||||
|
||||
if not hasattr(pdu, "destinations") or not pdu.destinations:
|
||||
pdu.destinations = []
|
||||
|
||||
yield self.replication_layer.send_pdu(pdu)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def backfill(self, dest, room_id, limit):
|
||||
pdus = yield self.replication_layer.backfill(dest, room_id, limit)
|
||||
|
||||
if not pdus:
|
||||
defer.returnValue([])
|
||||
|
||||
events = [
|
||||
self.pdu_codec.event_from_pdu(pdu)
|
||||
for pdu in pdus
|
||||
]
|
||||
|
||||
defer.returnValue(events)
|
||||
|
||||
@log_function
|
||||
def get_state_for_room(self, destination, room_id):
|
||||
return self.replication_layer.get_state_for_context(
|
||||
destination, room_id
|
||||
)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def on_receive_pdu(self, pdu, backfilled):
|
||||
""" Called by the ReplicationLayer when we have a new pdu. We need to
|
||||
do auth checks and put it throught the StateHandler.
|
||||
"""
|
||||
event = self.pdu_codec.event_from_pdu(pdu)
|
||||
|
||||
try:
|
||||
with (yield self.lock_manager.lock(pdu.context)):
|
||||
if event.is_state and not backfilled:
|
||||
is_new_state = yield self.state_handler.handle_new_state(
|
||||
pdu
|
||||
)
|
||||
if not is_new_state:
|
||||
return
|
||||
else:
|
||||
is_new_state = False
|
||||
|
||||
yield self.event_handler.on_receive(event, is_new_state, backfilled)
|
||||
|
||||
except AuthError:
|
||||
# TODO: Implement something in federation that allows us to
|
||||
# respond to PDU.
|
||||
raise
|
||||
|
||||
return
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _on_new_state(self, pdu, new_state_event):
|
||||
# TODO: Do any store stuff here. Notifiy C2S about this new
|
||||
# state.
|
||||
|
||||
yield self.store.update_current_state(
|
||||
pdu_id=pdu.pdu_id,
|
||||
origin=pdu.origin,
|
||||
context=pdu.context,
|
||||
pdu_type=pdu.pdu_type,
|
||||
state_key=pdu.state_key
|
||||
)
|
||||
|
||||
yield self.event_handler.on_receive(new_state_event)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def fill_out_prev_events(self, event):
|
||||
if hasattr(event, "prev_events"):
|
||||
return
|
||||
|
||||
results = yield self.store.get_latest_pdus_in_context(
|
||||
event.room_id
|
||||
)
|
||||
|
||||
es = [
|
||||
"%s@%s" % (p_id, origin) for p_id, origin, _ in results
|
||||
]
|
||||
|
||||
event.prev_events = [e for e in es if e != event.event_id]
|
||||
|
||||
if results:
|
||||
event.depth = max([int(v) for _, _, v in results]) + 1
|
||||
else:
|
||||
event.depth = 0
|
||||
@@ -25,7 +25,6 @@ from .units import Pdu
|
||||
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
|
||||
@@ -40,28 +39,6 @@ class PduActions(object):
|
||||
def __init__(self, datastore):
|
||||
self.store = datastore
|
||||
|
||||
@log_function
|
||||
def persist_received(self, pdu):
|
||||
""" Persists the given `Pdu` that was received from a remote home
|
||||
server.
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
"""
|
||||
return self._persist(pdu)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def persist_outgoing(self, pdu):
|
||||
""" Persists the given `Pdu` that this home server created.
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
"""
|
||||
ret = yield self._persist(pdu)
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
@log_function
|
||||
def mark_as_processed(self, pdu):
|
||||
""" Persist the fact that we have fully processed the given `Pdu`
|
||||
@@ -71,25 +48,6 @@ class PduActions(object):
|
||||
"""
|
||||
return self.store.mark_pdu_as_processed(pdu.pdu_id, pdu.origin)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def populate_previous_pdus(self, pdu):
|
||||
""" Given an outgoing `Pdu` fill out its `prev_ids` key with the `Pdu`s
|
||||
that we have received.
|
||||
|
||||
Returns:
|
||||
Deferred
|
||||
"""
|
||||
results = yield self.store.get_latest_pdus_in_context(pdu.context)
|
||||
|
||||
pdu.prev_pdus = [(p_id, origin) for p_id, origin, _ in results]
|
||||
|
||||
vs = [int(v) for _, _, v in results]
|
||||
if vs:
|
||||
pdu.depth = max(vs) + 1
|
||||
else:
|
||||
pdu.depth = 0
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def after_transaction(self, transaction_id, destination, origin):
|
||||
@@ -143,28 +101,6 @@ class PduActions(object):
|
||||
depth=pdu.depth
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def _persist(self, pdu):
|
||||
kwargs = copy.copy(pdu.__dict__)
|
||||
unrec_keys = copy.copy(pdu.unrecognized_keys)
|
||||
del kwargs["content"]
|
||||
kwargs["content_json"] = json.dumps(pdu.content)
|
||||
kwargs["unrecognized_keys"] = json.dumps(unrec_keys)
|
||||
|
||||
logger.debug("Persisting: %s", repr(kwargs))
|
||||
|
||||
if pdu.is_state:
|
||||
ret = yield self.store.persist_state(**kwargs)
|
||||
else:
|
||||
ret = yield self.store.persist_pdu(**kwargs)
|
||||
|
||||
yield self.store.update_min_depth_for_context(
|
||||
pdu.context, pdu.depth
|
||||
)
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
|
||||
class TransactionActions(object):
|
||||
""" Defines persistence actions that relate to handling Transactions.
|
||||
|
||||
@@ -134,10 +134,8 @@ class ReplicationLayer(object):
|
||||
|
||||
logger.debug("[%s] Persisting PDU", pdu.pdu_id)
|
||||
|
||||
#yield self.pdu_actions.populate_previous_pdus(pdu)
|
||||
|
||||
# Save *before* trying to send
|
||||
yield self.pdu_actions.persist_outgoing(pdu)
|
||||
yield self.store.persist_event(pdu=pdu)
|
||||
|
||||
logger.debug("[%s] Persisted PDU", pdu.pdu_id)
|
||||
logger.debug("[%s] transaction_layer.enqueue_pdu... ", pdu.pdu_id)
|
||||
@@ -450,7 +448,7 @@ class ReplicationLayer(object):
|
||||
logger.exception("Failed to get PDU")
|
||||
|
||||
# Persist the Pdu, but don't mark it as processed yet.
|
||||
yield self.pdu_actions.persist_received(pdu)
|
||||
yield self.store.persist_event(pdu=pdu)
|
||||
|
||||
if not backfilled:
|
||||
ret = yield self.handler.on_receive_pdu(pdu, backfilled=backfilled)
|
||||
@@ -509,10 +507,10 @@ class _TransactionQueue(object):
|
||||
# a transaction in progress. If we do, stick it in the pending_pdus
|
||||
# table and we'll get back to it later.
|
||||
|
||||
destinations = [
|
||||
destinations = set([
|
||||
d for d in pdu.destinations
|
||||
if d != self.server_name
|
||||
]
|
||||
])
|
||||
|
||||
logger.debug("Sending to: %s", str(destinations))
|
||||
|
||||
@@ -543,7 +541,10 @@ class _TransactionQueue(object):
|
||||
)
|
||||
|
||||
def eb(failure):
|
||||
deferred.errback(failure)
|
||||
if not deferred.called:
|
||||
deferred.errback(failure)
|
||||
else:
|
||||
logger.exception("Failed to send edu", failure)
|
||||
self._attempt_new_transaction(destination).addErrback(eb)
|
||||
|
||||
return deferred
|
||||
|
||||
@@ -15,14 +15,16 @@
|
||||
|
||||
from .register import RegistrationHandler
|
||||
from .room import (
|
||||
MessageHandler, RoomCreationHandler, RoomMemberHandler, RoomListHandler
|
||||
RoomCreationHandler, RoomMemberHandler, RoomListHandler
|
||||
)
|
||||
from .events import EventStreamHandler
|
||||
from .message import MessageHandler
|
||||
from .events import EventStreamHandler, EventHandler
|
||||
from .federation import FederationHandler
|
||||
from .login import LoginHandler
|
||||
from .profile import ProfileHandler
|
||||
from .presence import PresenceHandler
|
||||
from .directory import DirectoryHandler
|
||||
from .typing import TypingNotificationHandler
|
||||
|
||||
|
||||
class Handlers(object):
|
||||
@@ -39,9 +41,11 @@ class Handlers(object):
|
||||
self.room_creation_handler = RoomCreationHandler(hs)
|
||||
self.room_member_handler = RoomMemberHandler(hs)
|
||||
self.event_stream_handler = EventStreamHandler(hs)
|
||||
self.event_handler = EventHandler(hs)
|
||||
self.federation_handler = FederationHandler(hs)
|
||||
self.profile_handler = ProfileHandler(hs)
|
||||
self.presence_handler = PresenceHandler(hs)
|
||||
self.room_list_handler = RoomListHandler(hs)
|
||||
self.login_handler = LoginHandler(hs)
|
||||
self.directory_handler = DirectoryHandler(hs)
|
||||
self.typing_notification_handler = TypingNotificationHandler(hs)
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
class BaseHandler(object):
|
||||
|
||||
@@ -26,3 +26,25 @@ class BaseHandler(object):
|
||||
self.state_handler = hs.get_state_handler()
|
||||
self.distributor = hs.get_distributor()
|
||||
self.hs = hs
|
||||
|
||||
|
||||
class BaseRoomHandler(BaseHandler):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _on_new_room_event(self, event, snapshot, extra_destinations=[],
|
||||
extra_users=[]):
|
||||
snapshot.fill_out_prev_events(event)
|
||||
|
||||
yield self.store.persist_event(event)
|
||||
|
||||
destinations = set(extra_destinations)
|
||||
# Send a PDU to all hosts who have joined the room.
|
||||
destinations.update((yield self.store.get_joined_hosts_for_room(
|
||||
event.room_id
|
||||
)))
|
||||
event.destinations = list(destinations)
|
||||
|
||||
self.notifier.on_new_room_event(event, extra_users=extra_users)
|
||||
|
||||
federation_handler = self.hs.get_handlers().federation_handler
|
||||
yield federation_handler.handle_new_event(event, snapshot)
|
||||
|
||||
@@ -15,20 +15,19 @@
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.events import SynapseEvent
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
from ._base import BaseHandler
|
||||
from synapse.api.streams.event import (
|
||||
EventStream, EventsStreamData
|
||||
)
|
||||
from synapse.handlers.presence import PresenceStreamData
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EventStreamHandler(BaseHandler):
|
||||
|
||||
stream_data_classes = [
|
||||
EventsStreamData,
|
||||
PresenceStreamData,
|
||||
]
|
||||
|
||||
def __init__(self, hs):
|
||||
super(EventStreamHandler, self).__init__(hs)
|
||||
|
||||
@@ -43,45 +42,13 @@ class EventStreamHandler(BaseHandler):
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
def get_event_stream_token(self, stream_type, store_id, start_token):
|
||||
"""Return the next token after this event.
|
||||
|
||||
Args:
|
||||
stream_type (str): The StreamData.EVENT_TYPE
|
||||
store_id (int): The new storage ID assigned from the data store.
|
||||
start_token (str): The token the user started with.
|
||||
Returns:
|
||||
str: The end token.
|
||||
"""
|
||||
for i, stream_cls in enumerate(EventStreamHandler.stream_data_classes):
|
||||
if stream_cls.EVENT_TYPE == stream_type:
|
||||
# this is the stream for this event, so replace this part of
|
||||
# the token
|
||||
store_ids = start_token.split(EventStream.SEPARATOR)
|
||||
store_ids[i] = str(store_id)
|
||||
return EventStream.SEPARATOR.join(store_ids)
|
||||
raise RuntimeError("Didn't find a stream type %s" % stream_type)
|
||||
self.notifier = hs.get_notifier()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def get_stream(self, auth_user_id, pagin_config, timeout=0):
|
||||
"""Gets events as an event stream for this user.
|
||||
|
||||
This function looks for interesting *events* for this user. This is
|
||||
different from the notifier, which looks for interested *users* who may
|
||||
want to know about a single event.
|
||||
|
||||
Args:
|
||||
auth_user_id (str): The user requesting their event stream.
|
||||
pagin_config (synapse.api.streams.PaginationConfig): The config to
|
||||
use when obtaining the stream.
|
||||
timeout (int): The max time to wait for an incoming event in ms.
|
||||
Returns:
|
||||
A pagination stream API dict
|
||||
"""
|
||||
auth_user = self.hs.parse_userid(auth_user_id)
|
||||
|
||||
stream_id = object()
|
||||
|
||||
try:
|
||||
if auth_user not in self._streams_per_user:
|
||||
self._streams_per_user[auth_user] = 0
|
||||
@@ -94,41 +61,30 @@ class EventStreamHandler(BaseHandler):
|
||||
)
|
||||
self._streams_per_user[auth_user] += 1
|
||||
|
||||
# construct an event stream with the correct data ordering
|
||||
stream_data_list = []
|
||||
for stream_class in EventStreamHandler.stream_data_classes:
|
||||
stream_data_list.append(stream_class(self.hs))
|
||||
event_stream = EventStream(auth_user_id, stream_data_list)
|
||||
if pagin_config.from_token is None:
|
||||
pagin_config.from_token = None
|
||||
|
||||
# fix unknown tokens to known tokens
|
||||
pagin_config = yield event_stream.fix_tokens(pagin_config)
|
||||
rm_handler = self.hs.get_handlers().room_member_handler
|
||||
room_ids = yield rm_handler.get_rooms_for_user(auth_user)
|
||||
|
||||
# register interest in receiving new events
|
||||
self.notifier.store_events_for(user_id=auth_user_id,
|
||||
stream_id=stream_id,
|
||||
from_tok=pagin_config.from_tok)
|
||||
events, tokens = yield self.notifier.get_events_for(
|
||||
auth_user, room_ids, pagin_config, timeout
|
||||
)
|
||||
|
||||
# see if we can grab a chunk now
|
||||
data_chunk = yield event_stream.get_chunk(config=pagin_config)
|
||||
chunks = [
|
||||
e.get_dict() if isinstance(e, SynapseEvent) else e
|
||||
for e in events
|
||||
]
|
||||
|
||||
# if there are previous events, return those. If not, wait on the
|
||||
# new events for 'timeout' seconds.
|
||||
if len(data_chunk["chunk"]) == 0 and timeout != 0:
|
||||
results = yield defer.maybeDeferred(
|
||||
self.notifier.get_events_for,
|
||||
user_id=auth_user_id,
|
||||
stream_id=stream_id,
|
||||
timeout=timeout
|
||||
)
|
||||
if results:
|
||||
defer.returnValue(results)
|
||||
chunk = {
|
||||
"chunk": chunks,
|
||||
"start": tokens[0].to_string(),
|
||||
"end": tokens[1].to_string(),
|
||||
}
|
||||
|
||||
defer.returnValue(chunk)
|
||||
|
||||
defer.returnValue(data_chunk)
|
||||
finally:
|
||||
# cleanup
|
||||
self.notifier.purge_events_for(user_id=auth_user_id,
|
||||
stream_id=stream_id)
|
||||
|
||||
self._streams_per_user[auth_user] -= 1
|
||||
if not self._streams_per_user[auth_user]:
|
||||
del self._streams_per_user[auth_user]
|
||||
@@ -136,11 +92,39 @@ class EventStreamHandler(BaseHandler):
|
||||
# 10 seconds of grace to allow the client to reconnect again
|
||||
# before we think they're gone
|
||||
def _later():
|
||||
logger.debug("_later stopped_user_eventstream %s", auth_user)
|
||||
self.distributor.fire(
|
||||
"stopped_user_eventstream", auth_user
|
||||
)
|
||||
del self._stop_timer_per_user[auth_user]
|
||||
|
||||
logger.debug("Scheduling _later: for %s", auth_user)
|
||||
self._stop_timer_per_user[auth_user] = (
|
||||
self.clock.call_later(5, _later)
|
||||
self.clock.call_later(30, _later)
|
||||
)
|
||||
|
||||
|
||||
class EventHandler(BaseHandler):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_event(self, user, event_id):
|
||||
"""Retrieve a single specified event.
|
||||
|
||||
Args:
|
||||
user (synapse.types.UserID): The user requesting the event
|
||||
event_id (str): The event ID to obtain.
|
||||
Returns:
|
||||
dict: An event, or None if there is no event matching this ID.
|
||||
Raises:
|
||||
SynapseError if there was a problem retrieving this event, or
|
||||
AuthError if the user does not have the rights to inspect this
|
||||
event.
|
||||
"""
|
||||
event = yield self.store.get_event(event_id)
|
||||
|
||||
if not event:
|
||||
defer.returnValue(None)
|
||||
return
|
||||
|
||||
yield self.auth.check(event, raises=True)
|
||||
defer.returnValue(event)
|
||||
|
||||
@@ -20,6 +20,7 @@ from ._base import BaseHandler
|
||||
from synapse.api.events.room import InviteJoinEvent, RoomMemberEvent
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.util.logutils import log_function
|
||||
from synapse.federation.pdu_codec import PduCodec
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
@@ -30,8 +31,14 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FederationHandler(BaseHandler):
|
||||
"""Handles events that originated from federation.
|
||||
Responsible for:
|
||||
a) handling received Pdus before handing them on as Events to the rest
|
||||
of the home server (including auth and state conflict resoultion)
|
||||
b) converting events that were produced by local clients that may need
|
||||
to be sent to remote home servers.
|
||||
"""
|
||||
|
||||
"""Handles events that originated from federation."""
|
||||
def __init__(self, hs):
|
||||
super(FederationHandler, self).__init__(hs)
|
||||
|
||||
@@ -42,9 +49,61 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
self.waiting_for_join_list = {}
|
||||
|
||||
self.store = hs.get_datastore()
|
||||
self.replication_layer = hs.get_replication_layer()
|
||||
self.state_handler = hs.get_state_handler()
|
||||
# self.auth_handler = gs.get_auth_handler()
|
||||
self.server_name = hs.hostname
|
||||
|
||||
self.lock_manager = hs.get_room_lock_manager()
|
||||
|
||||
self.replication_layer.set_handler(self)
|
||||
|
||||
self.pdu_codec = PduCodec(hs)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def on_receive(self, event, is_new_state, backfilled):
|
||||
def handle_new_event(self, event, snapshot):
|
||||
""" 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
|
||||
remote home servers that may be interested.
|
||||
|
||||
Args:
|
||||
event
|
||||
snapshot (.storage.Snapshot): THe snapshot the event happened after
|
||||
|
||||
Returns:
|
||||
Deferred: Resolved when it has successfully been queued for
|
||||
processing.
|
||||
"""
|
||||
|
||||
pdu = self.pdu_codec.pdu_from_event(event)
|
||||
|
||||
if not hasattr(pdu, "destinations") or not pdu.destinations:
|
||||
pdu.destinations = []
|
||||
|
||||
yield self.replication_layer.send_pdu(pdu)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def on_receive_pdu(self, pdu, backfilled):
|
||||
""" Called by the ReplicationLayer when we have a new pdu. We need to
|
||||
do auth checks and put it throught the StateHandler.
|
||||
"""
|
||||
event = self.pdu_codec.event_from_pdu(pdu)
|
||||
|
||||
with (yield self.lock_manager.lock(pdu.context)):
|
||||
if event.is_state and not backfilled:
|
||||
is_new_state = yield self.state_handler.handle_new_state(
|
||||
pdu
|
||||
)
|
||||
if not is_new_state:
|
||||
return
|
||||
else:
|
||||
is_new_state = False
|
||||
# TODO: Implement something in federation that allows us to
|
||||
# respond to PDU.
|
||||
|
||||
if hasattr(event, "state_key") and not is_new_state:
|
||||
logger.debug("Ignoring old state.")
|
||||
return
|
||||
@@ -65,7 +124,7 @@ class FederationHandler(BaseHandler):
|
||||
content.update({"membership": Membership.JOIN})
|
||||
new_event = self.event_factory.create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
target_user_id=event.user_id,
|
||||
state_key=event.user_id,
|
||||
room_id=event.room_id,
|
||||
user_id=event.user_id,
|
||||
membership=Membership.JOIN,
|
||||
@@ -74,20 +133,19 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
yield self.hs.get_handlers().room_member_handler.change_membership(
|
||||
new_event,
|
||||
True
|
||||
do_auth=True
|
||||
)
|
||||
|
||||
else:
|
||||
with (yield self.room_lock.lock(event.room_id)):
|
||||
store_id = yield self.store.persist_event(event, backfilled)
|
||||
yield self.store.persist_event(event, backfilled)
|
||||
|
||||
room = yield self.store.get_room(event.room_id)
|
||||
|
||||
if not room:
|
||||
# Huh, let's try and get the current state
|
||||
try:
|
||||
federation = self.hs.get_federation()
|
||||
yield federation.get_state_for_room(
|
||||
yield self.replication_layer.get_state_for_context(
|
||||
event.origin, event.room_id
|
||||
)
|
||||
|
||||
@@ -97,9 +155,9 @@ class FederationHandler(BaseHandler):
|
||||
if self.hs.hostname in hosts:
|
||||
try:
|
||||
yield self.store.store_room(
|
||||
event.room_id,
|
||||
"",
|
||||
is_public=False
|
||||
room_id=event.room_id,
|
||||
room_creator_user_id="",
|
||||
is_public=False,
|
||||
)
|
||||
except:
|
||||
pass
|
||||
@@ -110,33 +168,32 @@ class FederationHandler(BaseHandler):
|
||||
)
|
||||
|
||||
if not backfilled:
|
||||
yield self.notifier.on_new_room_event(event, store_id)
|
||||
yield self.notifier.on_new_room_event(event)
|
||||
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
if event.membership == Membership.JOIN:
|
||||
user = self.hs.parse_userid(event.target_user_id)
|
||||
user = self.hs.parse_userid(event.state_key)
|
||||
self.distributor.fire(
|
||||
"user_joined_room", user=user, room_id=event.room_id
|
||||
)
|
||||
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def backfill(self, dest, room_id, limit):
|
||||
events = yield self.hs.get_federation().backfill(dest, room_id, limit)
|
||||
pdus = yield self.replication_layer.backfill(dest, room_id, limit)
|
||||
|
||||
for event in events:
|
||||
try:
|
||||
yield self.store.persist_event(event, backfilled=True)
|
||||
except:
|
||||
logger.exception("Failed to persist event: %s", event)
|
||||
events = []
|
||||
|
||||
for pdu in pdus:
|
||||
event = self.pdu_codec.event_from_pdu(pdu)
|
||||
events.append(event)
|
||||
yield self.store.persist_event(event, backfilled=True)
|
||||
|
||||
defer.returnValue(events)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def do_invite_join(self, target_host, room_id, joinee, content):
|
||||
federation = self.hs.get_federation()
|
||||
def do_invite_join(self, target_host, room_id, joinee, content, snapshot):
|
||||
|
||||
hosts = yield self.store.get_joined_hosts_for_room(room_id)
|
||||
if self.hs.hostname in hosts:
|
||||
@@ -146,7 +203,9 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
# First get current state to see if we are already joined.
|
||||
try:
|
||||
yield federation.get_state_for_room(target_host, room_id)
|
||||
yield self.replication_layer.get_state_for_context(
|
||||
target_host, room_id
|
||||
)
|
||||
|
||||
hosts = yield self.store.get_joined_hosts_for_room(room_id)
|
||||
if self.hs.hostname in hosts:
|
||||
@@ -166,7 +225,8 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
new_event.destinations = [target_host]
|
||||
|
||||
yield federation.handle_new_event(new_event)
|
||||
snapshot.fill_out_prev_events(new_event)
|
||||
yield self.handle_new_event(new_event, snapshot)
|
||||
|
||||
# TODO (erikj): Time out here.
|
||||
d = defer.Deferred()
|
||||
@@ -175,8 +235,8 @@ class FederationHandler(BaseHandler):
|
||||
|
||||
try:
|
||||
yield self.store.store_room(
|
||||
event.room_id,
|
||||
"",
|
||||
room_id=room_id,
|
||||
room_creator_user_id="",
|
||||
is_public=False
|
||||
)
|
||||
except:
|
||||
|
||||
304
synapse/handlers/message.py
Normal file
304
synapse/handlers/message.py
Normal file
@@ -0,0 +1,304 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.events.room import RoomTopicEvent
|
||||
from synapse.api.errors import RoomError
|
||||
from synapse.streams.config import PaginationConfig
|
||||
from ._base import BaseRoomHandler
|
||||
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
||||
class MessageHandler(BaseRoomHandler):
|
||||
|
||||
def __init__(self, hs):
|
||||
super(MessageHandler, self).__init__(hs)
|
||||
self.hs = hs
|
||||
self.clock = hs.get_clock()
|
||||
self.event_factory = hs.get_event_factory()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_message(self, msg_id=None, room_id=None, sender_id=None,
|
||||
user_id=None):
|
||||
""" Retrieve a message.
|
||||
|
||||
Args:
|
||||
msg_id (str): The message ID to obtain.
|
||||
room_id (str): The room where the message resides.
|
||||
sender_id (str): The user ID of the user who sent the message.
|
||||
user_id (str): The user ID of the user making this request.
|
||||
Returns:
|
||||
The message, or None if no message exists.
|
||||
Raises:
|
||||
SynapseError if something went wrong.
|
||||
"""
|
||||
yield self.auth.check_joined_room(room_id, user_id)
|
||||
|
||||
# Pull out the message from the db
|
||||
# msg = yield self.store.get_message(
|
||||
# room_id=room_id,
|
||||
# msg_id=msg_id,
|
||||
# user_id=sender_id
|
||||
# )
|
||||
|
||||
# TODO (erikj): Once we work out the correct c-s api we need to think on how to do this.
|
||||
|
||||
defer.returnValue(None)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_message(self, event=None, suppress_auth=False, stamp_event=True):
|
||||
""" Send a message.
|
||||
|
||||
Args:
|
||||
event : The message event to store.
|
||||
suppress_auth (bool) : True to suppress auth for this message. This
|
||||
is primarily so the home server can inject messages into rooms at
|
||||
will.
|
||||
stamp_event (bool) : True to stamp event content with server keys.
|
||||
Raises:
|
||||
SynapseError if something went wrong.
|
||||
"""
|
||||
if stamp_event:
|
||||
event.content["hsob_ts"] = int(self.clock.time_msec())
|
||||
|
||||
snapshot = yield self.store.snapshot_room(event.room_id, event.user_id)
|
||||
|
||||
if not suppress_auth:
|
||||
yield self.auth.check(event, snapshot, raises=True)
|
||||
|
||||
yield self._on_new_room_event(event, snapshot)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
|
||||
feedback=False):
|
||||
"""Get messages in a room.
|
||||
|
||||
Args:
|
||||
user_id (str): The user requesting messages.
|
||||
room_id (str): The room they want messages from.
|
||||
pagin_config (synapse.api.streams.PaginationConfig): The pagination
|
||||
config rules to apply, if any.
|
||||
feedback (bool): True to get compressed feedback with the messages
|
||||
Returns:
|
||||
dict: Pagination API results
|
||||
"""
|
||||
yield self.auth.check_joined_room(room_id, user_id)
|
||||
|
||||
data_source = self.hs.get_event_sources().sources["room"]
|
||||
|
||||
if not pagin_config.from_token:
|
||||
pagin_config.from_token = yield self.hs.get_event_sources().get_current_token()
|
||||
|
||||
user = self.hs.parse_userid(user_id)
|
||||
|
||||
events, next_token = yield data_source.get_pagination_rows(
|
||||
user, pagin_config, room_id
|
||||
)
|
||||
|
||||
chunk = {
|
||||
"chunk": [e.get_dict() for e in events],
|
||||
"start": pagin_config.from_token.to_string(),
|
||||
"end": next_token.to_string(),
|
||||
}
|
||||
|
||||
defer.returnValue(chunk)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def store_room_data(self, event=None, stamp_event=True):
|
||||
""" Stores data for a room.
|
||||
|
||||
Args:
|
||||
event : The room path event
|
||||
stamp_event (bool) : True to stamp event content with server keys.
|
||||
Raises:
|
||||
SynapseError if something went wrong.
|
||||
"""
|
||||
|
||||
snapshot = yield self.store.snapshot_room(event.room_id, event.user_id)
|
||||
|
||||
yield self.auth.check(event, snapshot, raises=True)
|
||||
|
||||
if stamp_event:
|
||||
event.content["hsob_ts"] = int(self.clock.time_msec())
|
||||
|
||||
yield self.state_handler.handle_new_event(event, snapshot)
|
||||
|
||||
yield self._on_new_room_event(event, snapshot)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_room_data(self, user_id=None, room_id=None,
|
||||
event_type=None, state_key="",
|
||||
public_room_rules=[],
|
||||
private_room_rules=["join"]):
|
||||
""" Get data from a room.
|
||||
|
||||
Args:
|
||||
event : The room path event
|
||||
public_room_rules : A list of membership states the user can be in,
|
||||
in order to read this data IN A PUBLIC ROOM. An empty list means
|
||||
'any state'.
|
||||
private_room_rules : A list of membership states the user can be
|
||||
in, in order to read this data IN A PRIVATE ROOM. An empty list
|
||||
means 'any state'.
|
||||
Returns:
|
||||
The path data content.
|
||||
Raises:
|
||||
SynapseError if something went wrong.
|
||||
"""
|
||||
if event_type == RoomTopicEvent.TYPE:
|
||||
# anyone invited/joined can read the topic
|
||||
private_room_rules = ["invite", "join"]
|
||||
|
||||
# does this room exist
|
||||
room = yield self.store.get_room(room_id)
|
||||
if not room:
|
||||
raise RoomError(403, "Room does not exist.")
|
||||
|
||||
# does this user exist in this room
|
||||
member = yield self.store.get_room_member(
|
||||
room_id=room_id,
|
||||
user_id="" if not user_id else user_id)
|
||||
|
||||
member_state = member.membership if member else None
|
||||
|
||||
if room.is_public and public_room_rules:
|
||||
# make sure the user meets public room rules
|
||||
if member_state not in public_room_rules:
|
||||
raise RoomError(403, "Member does not meet public room rules.")
|
||||
elif not room.is_public and private_room_rules:
|
||||
# make sure the user meets private room rules
|
||||
if member_state not in private_room_rules:
|
||||
raise RoomError(
|
||||
403, "Member does not meet private room rules.")
|
||||
|
||||
data = yield self.store.get_current_state(
|
||||
room_id, event_type, state_key
|
||||
)
|
||||
defer.returnValue(data)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_feedback(self, event_id):
|
||||
# yield self.auth.check_joined_room(room_id, user_id)
|
||||
|
||||
# Pull out the feedback from the db
|
||||
fb = yield self.store.get_feedback(event_id)
|
||||
|
||||
if fb:
|
||||
defer.returnValue(fb)
|
||||
defer.returnValue(None)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_feedback(self, event, stamp_event=True):
|
||||
if stamp_event:
|
||||
event.content["hsob_ts"] = int(self.clock.time_msec())
|
||||
|
||||
snapshot = yield self.store.snapshot_room(event.room_id, event.user_id)
|
||||
|
||||
yield self.auth.check(event, snapshot, raises=True)
|
||||
|
||||
# store message in db
|
||||
yield self._on_new_room_event(event, snapshot)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def snapshot_all_rooms(self, user_id=None, pagin_config=None,
|
||||
feedback=False):
|
||||
"""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
|
||||
joined, depending on the pagination config.
|
||||
|
||||
Args:
|
||||
user_id (str): The ID of the user making the request.
|
||||
pagin_config (synapse.api.streams.PaginationConfig): The pagination
|
||||
config used to determine how many messages *PER ROOM* to return.
|
||||
feedback (bool): True to get feedback along with these messages.
|
||||
Returns:
|
||||
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
|
||||
is joined on, may return a "messages" key with messages, depending
|
||||
on the specified PaginationConfig.
|
||||
"""
|
||||
room_list = yield self.store.get_rooms_for_user_where_membership_is(
|
||||
user_id=user_id,
|
||||
membership_list=[Membership.INVITE, Membership.JOIN]
|
||||
)
|
||||
|
||||
user = self.hs.parse_userid(user_id)
|
||||
|
||||
rooms_ret = []
|
||||
|
||||
now_token = yield self.hs.get_event_sources().get_current_token()
|
||||
|
||||
presence_stream = self.hs.get_event_sources().sources["presence"]
|
||||
pagination_config = PaginationConfig(from_token=now_token)
|
||||
presence, _ = yield presence_stream.get_pagination_rows(
|
||||
user, pagination_config, None
|
||||
)
|
||||
|
||||
limit = pagin_config.limit
|
||||
if not limit:
|
||||
limit = 10
|
||||
|
||||
for event in room_list:
|
||||
d = {
|
||||
"room_id": event.room_id,
|
||||
"membership": event.membership,
|
||||
}
|
||||
|
||||
if event.membership == Membership.INVITE:
|
||||
d["inviter"] = event.user_id
|
||||
|
||||
rooms_ret.append(d)
|
||||
|
||||
if event.membership != Membership.JOIN:
|
||||
continue
|
||||
try:
|
||||
messages, token = yield self.store.get_recent_events_for_room(
|
||||
event.room_id,
|
||||
limit=limit,
|
||||
end_token=now_token.events_key,
|
||||
)
|
||||
|
||||
start_token = now_token.copy_and_replace("events_key", token[0])
|
||||
end_token = now_token.copy_and_replace("events_key", token[1])
|
||||
|
||||
d["messages"] = {
|
||||
"chunk": [m.get_dict() for m in messages],
|
||||
"start": start_token.to_string(),
|
||||
"end": end_token.to_string(),
|
||||
}
|
||||
|
||||
current_state = yield self.store.get_current_state(
|
||||
event.room_id
|
||||
)
|
||||
d["state"] = [c.get_dict() for c in current_state]
|
||||
except:
|
||||
logger.exception("Failed to get snapshot")
|
||||
|
||||
ret = {
|
||||
"rooms": rooms_ret,
|
||||
"presence": presence,
|
||||
"end": now_token.to_string()
|
||||
}
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import SynapseError, AuthError
|
||||
from synapse.api.constants import PresenceState
|
||||
from synapse.api.streams import StreamData
|
||||
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
from ._base import BaseHandler
|
||||
|
||||
@@ -143,7 +144,7 @@ class PresenceHandler(BaseHandler):
|
||||
@defer.inlineCallbacks
|
||||
def is_presence_visible(self, observer_user, observed_user):
|
||||
defer.returnValue(True)
|
||||
return
|
||||
# return
|
||||
# FIXME (erikj): This code path absolutely kills the database.
|
||||
|
||||
assert(observed_user.is_mine)
|
||||
@@ -159,12 +160,11 @@ class PresenceHandler(BaseHandler):
|
||||
if allowed_by_subscription:
|
||||
defer.returnValue(True)
|
||||
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
for room_id in (yield rm_handler.get_rooms_for_user(observer_user)):
|
||||
if observed_user in (yield rm_handler.get_room_members(room_id)):
|
||||
defer.returnValue(True)
|
||||
share_room = yield self.store.do_users_share_a_room(
|
||||
[observer_user, observed_user]
|
||||
)
|
||||
|
||||
defer.returnValue(False)
|
||||
defer.returnValue(share_room)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_state(self, target_user, auth_user):
|
||||
@@ -190,8 +190,9 @@ class PresenceHandler(BaseHandler):
|
||||
defer.returnValue(state)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def set_state(self, target_user, auth_user, state):
|
||||
return
|
||||
# return
|
||||
# TODO (erikj): Turn this back on. Why did we end up sending EDUs
|
||||
# everywhere?
|
||||
|
||||
@@ -247,33 +248,42 @@ class PresenceHandler(BaseHandler):
|
||||
|
||||
self.push_presence(user, statuscache=statuscache)
|
||||
|
||||
@log_function
|
||||
def started_user_eventstream(self, user):
|
||||
# TODO(paul): Use "last online" state
|
||||
self.set_state(user, user, {"state": PresenceState.ONLINE})
|
||||
|
||||
@log_function
|
||||
def stopped_user_eventstream(self, user):
|
||||
# TODO(paul): Save current state as "last online" state
|
||||
self.set_state(user, user, {"state": PresenceState.OFFLINE})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def user_joined_room(self, user, room_id):
|
||||
localusers = set()
|
||||
remotedomains = set()
|
||||
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
yield rm_handler.fetch_room_distributions_into(room_id,
|
||||
localusers=localusers, remotedomains=remotedomains,
|
||||
ignore_user=user)
|
||||
|
||||
if user.is_mine:
|
||||
yield self._send_presence_to_distribution(srcuser=user,
|
||||
localusers=localusers, remotedomains=remotedomains,
|
||||
self.push_update_to_local_and_remote(
|
||||
observed_user=user,
|
||||
room_ids=[room_id],
|
||||
statuscache=self._get_or_offline_usercache(user),
|
||||
)
|
||||
|
||||
for srcuser in localusers:
|
||||
yield self._send_presence(srcuser=srcuser, destuser=user,
|
||||
statuscache=self._get_or_offline_usercache(srcuser),
|
||||
else:
|
||||
self.push_update_to_clients(
|
||||
observed_user=user,
|
||||
room_ids=[room_id],
|
||||
statuscache=self._get_or_offline_usercache(user),
|
||||
)
|
||||
|
||||
# We also want to tell them about current presence of people.
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
curr_users = yield rm_handler.get_room_members(room_id)
|
||||
|
||||
for local_user in [c for c in curr_users if c.is_mine]:
|
||||
self.push_update_to_local_and_remote(
|
||||
observed_user=local_user,
|
||||
users_to_push=[user],
|
||||
statuscache=self._get_or_offline_usercache(local_user),
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@@ -384,11 +394,13 @@ class PresenceHandler(BaseHandler):
|
||||
defer.returnValue(presence)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def start_polling_presence(self, user, target_user=None, state=None):
|
||||
logger.debug("Start polling for presence from %s", user)
|
||||
|
||||
if target_user:
|
||||
target_users = set([target_user])
|
||||
room_ids = []
|
||||
else:
|
||||
presence = yield self.store.get_presence_list(
|
||||
user.localpart, accepted=True
|
||||
@@ -402,23 +414,37 @@ class PresenceHandler(BaseHandler):
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
room_ids = yield rm_handler.get_rooms_for_user(user)
|
||||
|
||||
for room_id in room_ids:
|
||||
for member in (yield rm_handler.get_room_members(room_id)):
|
||||
target_users.add(member)
|
||||
|
||||
if state is None:
|
||||
state = yield self.store.get_presence_state(user.localpart)
|
||||
else:
|
||||
# statuscache = self._get_or_make_usercache(user)
|
||||
# self._user_cachemap_latest_serial += 1
|
||||
# statuscache.update(state, self._user_cachemap_latest_serial)
|
||||
pass
|
||||
|
||||
localusers, remoteusers = partitionbool(
|
||||
target_users,
|
||||
lambda u: u.is_mine
|
||||
yield self.push_update_to_local_and_remote(
|
||||
observed_user=user,
|
||||
users_to_push=target_users,
|
||||
room_ids=room_ids,
|
||||
statuscache=self._get_or_make_usercache(user),
|
||||
)
|
||||
|
||||
for target_user in localusers:
|
||||
self._start_polling_local(user, target_user)
|
||||
for target_user in target_users:
|
||||
if target_user.is_mine:
|
||||
self._start_polling_local(user, target_user)
|
||||
|
||||
# We want to tell the person that just came online
|
||||
# presence state of people they are interested in?
|
||||
self.push_update_to_clients(
|
||||
observed_user=target_user,
|
||||
users_to_push=[user],
|
||||
statuscache=self._get_or_offline_usercache(target_user),
|
||||
)
|
||||
|
||||
deferreds = []
|
||||
remoteusers_by_domain = partition(remoteusers, lambda u: u.domain)
|
||||
remote_users = [u for u in target_users if not u.is_mine]
|
||||
remoteusers_by_domain = partition(remote_users, lambda u: u.domain)
|
||||
# Only poll for people in our get_presence_list
|
||||
for domain in remoteusers_by_domain:
|
||||
remoteusers = remoteusers_by_domain[domain]
|
||||
|
||||
@@ -440,25 +466,26 @@ class PresenceHandler(BaseHandler):
|
||||
|
||||
self._local_pushmap[target_localpart].add(user)
|
||||
|
||||
self.push_update_to_clients(
|
||||
observer_user=user,
|
||||
observed_user=target_user,
|
||||
statuscache=self._get_or_offline_usercache(target_user),
|
||||
)
|
||||
|
||||
def _start_polling_remote(self, user, domain, remoteusers):
|
||||
to_poll = set()
|
||||
|
||||
for u in remoteusers:
|
||||
if u not in self._remote_recvmap:
|
||||
self._remote_recvmap[u] = set()
|
||||
to_poll.add(u)
|
||||
|
||||
self._remote_recvmap[u].add(user)
|
||||
|
||||
if not to_poll:
|
||||
return defer.succeed(None)
|
||||
|
||||
return self.federation.send_edu(
|
||||
destination=domain,
|
||||
edu_type="m.presence",
|
||||
content={"poll": [u.to_string() for u in remoteusers]}
|
||||
content={"poll": [u.to_string() for u in to_poll]}
|
||||
)
|
||||
|
||||
@log_function
|
||||
def stop_polling_presence(self, user, target_user=None):
|
||||
logger.debug("Stop polling for presence from %s", user)
|
||||
|
||||
@@ -498,20 +525,28 @@ class PresenceHandler(BaseHandler):
|
||||
if not self._local_pushmap[localpart]:
|
||||
del self._local_pushmap[localpart]
|
||||
|
||||
@log_function
|
||||
def _stop_polling_remote(self, user, domain, remoteusers):
|
||||
to_unpoll = set()
|
||||
|
||||
for u in remoteusers:
|
||||
self._remote_recvmap[u].remove(user)
|
||||
|
||||
if not self._remote_recvmap[u]:
|
||||
del self._remote_recvmap[u]
|
||||
to_unpoll.add(u)
|
||||
|
||||
if not to_unpoll:
|
||||
return defer.succeed(None)
|
||||
|
||||
return self.federation.send_edu(
|
||||
destination=domain,
|
||||
edu_type="m.presence",
|
||||
content={"unpoll": [u.to_string() for u in remoteusers]}
|
||||
content={"unpoll": [u.to_string() for u in to_unpoll]}
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def push_presence(self, user, statuscache):
|
||||
assert(user.is_mine)
|
||||
|
||||
@@ -527,53 +562,17 @@ class PresenceHandler(BaseHandler):
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
room_ids = yield rm_handler.get_rooms_for_user(user)
|
||||
|
||||
for room_id in room_ids:
|
||||
yield rm_handler.fetch_room_distributions_into(
|
||||
room_id, localusers=localusers, remotedomains=remotedomains,
|
||||
ignore_user=user,
|
||||
)
|
||||
|
||||
if not localusers and not remotedomains:
|
||||
if not localusers and not room_ids:
|
||||
defer.returnValue(None)
|
||||
|
||||
yield self._send_presence_to_distribution(user,
|
||||
localusers=localusers, remotedomains=remotedomains,
|
||||
statuscache=statuscache
|
||||
yield self.push_update_to_local_and_remote(
|
||||
observed_user=user,
|
||||
users_to_push=localusers,
|
||||
remote_domains=remotedomains,
|
||||
room_ids=room_ids,
|
||||
statuscache=statuscache,
|
||||
)
|
||||
|
||||
def _send_presence(self, srcuser, destuser, statuscache):
|
||||
if destuser.is_mine:
|
||||
self.push_update_to_clients(
|
||||
observer_user=destuser,
|
||||
observed_user=srcuser,
|
||||
statuscache=statuscache)
|
||||
return defer.succeed(None)
|
||||
else:
|
||||
return self._push_presence_remote(srcuser, destuser.domain,
|
||||
state=statuscache.get_state()
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _send_presence_to_distribution(self, srcuser, localusers=set(),
|
||||
remotedomains=set(), statuscache=None):
|
||||
|
||||
for u in localusers:
|
||||
logger.debug(" | push to local user %s", u)
|
||||
self.push_update_to_clients(
|
||||
observer_user=u,
|
||||
observed_user=srcuser,
|
||||
statuscache=statuscache,
|
||||
)
|
||||
|
||||
deferreds = []
|
||||
for domain in remotedomains:
|
||||
logger.debug(" | push to remote domain %s", domain)
|
||||
deferreds.append(self._push_presence_remote(srcuser, domain,
|
||||
state=statuscache.get_state())
|
||||
)
|
||||
|
||||
yield defer.DeferredList(deferreds)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _push_presence_remote(self, user, destination, state=None):
|
||||
if state is None:
|
||||
@@ -589,12 +588,17 @@ class PresenceHandler(BaseHandler):
|
||||
self.clock.time_msec() - state.pop("mtime")
|
||||
)
|
||||
|
||||
user_state = {
|
||||
"user_id": user.to_string(),
|
||||
}
|
||||
user_state.update(**state)
|
||||
|
||||
yield self.federation.send_edu(
|
||||
destination=destination,
|
||||
edu_type="m.presence",
|
||||
content={
|
||||
"push": [
|
||||
dict(user_id=user.to_string(), **state),
|
||||
user_state,
|
||||
],
|
||||
}
|
||||
)
|
||||
@@ -613,12 +617,7 @@ class PresenceHandler(BaseHandler):
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
room_ids = yield rm_handler.get_rooms_for_user(user)
|
||||
|
||||
for room_id in room_ids:
|
||||
yield rm_handler.fetch_room_distributions_into(
|
||||
room_id, localusers=observers, ignore_user=user
|
||||
)
|
||||
|
||||
if not observers:
|
||||
if not observers and not room_ids:
|
||||
break
|
||||
|
||||
state = dict(push)
|
||||
@@ -634,12 +633,12 @@ class PresenceHandler(BaseHandler):
|
||||
self._user_cachemap_latest_serial += 1
|
||||
statuscache.update(state, serial=self._user_cachemap_latest_serial)
|
||||
|
||||
for observer_user in observers:
|
||||
self.push_update_to_clients(
|
||||
observer_user=observer_user,
|
||||
observed_user=user,
|
||||
statuscache=statuscache,
|
||||
)
|
||||
self.push_update_to_clients(
|
||||
observed_user=user,
|
||||
users_to_push=observers,
|
||||
room_ids=room_ids,
|
||||
statuscache=statuscache,
|
||||
)
|
||||
|
||||
if state["state"] == PresenceState.OFFLINE:
|
||||
del self._user_cachemap[user]
|
||||
@@ -673,49 +672,54 @@ class PresenceHandler(BaseHandler):
|
||||
|
||||
yield defer.DeferredList(deferreds)
|
||||
|
||||
def push_update_to_clients(self, observer_user, observed_user,
|
||||
statuscache):
|
||||
state = statuscache.make_event(user=observed_user, clock=self.clock)
|
||||
@defer.inlineCallbacks
|
||||
def push_update_to_local_and_remote(self, observed_user,
|
||||
users_to_push=[], room_ids=[],
|
||||
remote_domains=[],
|
||||
statuscache=None):
|
||||
|
||||
self.notifier.on_new_user_event(
|
||||
observer_user.to_string(),
|
||||
event_data=statuscache.make_event(
|
||||
user=observed_user,
|
||||
clock=self.clock
|
||||
),
|
||||
stream_type=PresenceStreamData,
|
||||
store_id=statuscache.serial
|
||||
localusers, remoteusers = partitionbool(
|
||||
users_to_push,
|
||||
lambda u: u.is_mine
|
||||
)
|
||||
|
||||
localusers = set(localusers)
|
||||
|
||||
class PresenceStreamData(StreamData):
|
||||
def __init__(self, hs):
|
||||
super(PresenceStreamData, self).__init__(hs)
|
||||
self.presence = hs.get_handlers().presence_handler
|
||||
self.push_update_to_clients(
|
||||
observed_user=observed_user,
|
||||
users_to_push=localusers,
|
||||
room_ids=room_ids,
|
||||
statuscache=statuscache,
|
||||
)
|
||||
|
||||
def get_rows(self, user_id, from_key, to_key, limit, direction):
|
||||
from_key = int(from_key)
|
||||
to_key = int(to_key)
|
||||
remote_domains = set(remote_domains)
|
||||
remote_domains |= set([r.domain for r in remoteusers])
|
||||
for room_id in room_ids:
|
||||
remote_domains.update(
|
||||
(yield self.store.get_joined_hosts_for_room(room_id))
|
||||
)
|
||||
|
||||
cachemap = self.presence._user_cachemap
|
||||
remote_domains.discard(self.hs.hostname)
|
||||
|
||||
# TODO(paul): limit, and filter by visibility
|
||||
updates = [(k, cachemap[k]) for k in cachemap
|
||||
if from_key < cachemap[k].serial <= to_key]
|
||||
deferreds = []
|
||||
for domain in remote_domains:
|
||||
logger.debug(" | push to remote domain %s", domain)
|
||||
deferreds.append(
|
||||
self._push_presence_remote(
|
||||
observed_user, domain, state=statuscache.get_state()
|
||||
)
|
||||
)
|
||||
|
||||
if updates:
|
||||
clock = self.presence.clock
|
||||
yield defer.DeferredList(deferreds)
|
||||
|
||||
latest_serial = max([x[1].serial for x in updates])
|
||||
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
|
||||
return ((data, latest_serial))
|
||||
else:
|
||||
return (([], self.presence._user_cachemap_latest_serial))
|
||||
defer.returnValue((localusers, remote_domains))
|
||||
|
||||
def max_token(self):
|
||||
return self.presence._user_cachemap_latest_serial
|
||||
|
||||
PresenceStreamData.EVENT_TYPE = PresenceStreamData
|
||||
def push_update_to_clients(self, observed_user, users_to_push=[],
|
||||
room_ids=[], statuscache=None):
|
||||
self.notifier.on_new_user_event(
|
||||
users_to_push,
|
||||
room_ids,
|
||||
)
|
||||
|
||||
|
||||
class UserPresenceCache(object):
|
||||
|
||||
@@ -18,301 +18,19 @@ from twisted.internet import defer
|
||||
|
||||
from synapse.types import UserID, RoomAlias, RoomID
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.errors import RoomError, StoreError, SynapseError
|
||||
from synapse.api.errors import StoreError, SynapseError
|
||||
from synapse.api.events.room import (
|
||||
RoomTopicEvent, MessageEvent, InviteJoinEvent, RoomMemberEvent,
|
||||
RoomConfigEvent
|
||||
RoomMemberEvent, RoomConfigEvent
|
||||
)
|
||||
from synapse.api.streams.event import EventStream, EventsStreamData
|
||||
from synapse.handlers.presence import PresenceStreamData
|
||||
from synapse.util import stringutils
|
||||
from ._base import BaseHandler
|
||||
from ._base import BaseRoomHandler
|
||||
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MessageHandler(BaseHandler):
|
||||
|
||||
def __init__(self, hs):
|
||||
super(MessageHandler, self).__init__(hs)
|
||||
self.hs = hs
|
||||
self.clock = hs.get_clock()
|
||||
self.event_factory = hs.get_event_factory()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_message(self, msg_id=None, room_id=None, sender_id=None,
|
||||
user_id=None):
|
||||
""" Retrieve a message.
|
||||
|
||||
Args:
|
||||
msg_id (str): The message ID to obtain.
|
||||
room_id (str): The room where the message resides.
|
||||
sender_id (str): The user ID of the user who sent the message.
|
||||
user_id (str): The user ID of the user making this request.
|
||||
Returns:
|
||||
The message, or None if no message exists.
|
||||
Raises:
|
||||
SynapseError if something went wrong.
|
||||
"""
|
||||
yield self.auth.check_joined_room(room_id, user_id)
|
||||
|
||||
# Pull out the message from the db
|
||||
# msg = yield self.store.get_message(
|
||||
# room_id=room_id,
|
||||
# msg_id=msg_id,
|
||||
# user_id=sender_id
|
||||
# )
|
||||
|
||||
# TODO (erikj): Once we work out the correct c-s api we need to think on how to do this.
|
||||
|
||||
defer.returnValue(None)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_message(self, event=None, suppress_auth=False, stamp_event=True):
|
||||
""" Send a message.
|
||||
|
||||
Args:
|
||||
event : The message event to store.
|
||||
suppress_auth (bool) : True to suppress auth for this message. This
|
||||
is primarily so the home server can inject messages into rooms at
|
||||
will.
|
||||
stamp_event (bool) : True to stamp event content with server keys.
|
||||
Raises:
|
||||
SynapseError if something went wrong.
|
||||
"""
|
||||
if stamp_event:
|
||||
event.content["hsob_ts"] = int(self.clock.time_msec())
|
||||
|
||||
with (yield self.room_lock.lock(event.room_id)):
|
||||
if not suppress_auth:
|
||||
yield self.auth.check(event, raises=True)
|
||||
|
||||
# store message in db
|
||||
store_id = yield self.store.persist_event(event)
|
||||
|
||||
event.destinations = yield self.store.get_joined_hosts_for_room(
|
||||
event.room_id
|
||||
)
|
||||
|
||||
self.notifier.on_new_room_event(event, store_id)
|
||||
|
||||
yield self.hs.get_federation().handle_new_event(event)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
|
||||
feedback=False):
|
||||
"""Get messages in a room.
|
||||
|
||||
Args:
|
||||
user_id (str): The user requesting messages.
|
||||
room_id (str): The room they want messages from.
|
||||
pagin_config (synapse.api.streams.PaginationConfig): The pagination
|
||||
config rules to apply, if any.
|
||||
feedback (bool): True to get compressed feedback with the messages
|
||||
Returns:
|
||||
dict: Pagination API results
|
||||
"""
|
||||
yield self.auth.check_joined_room(room_id, user_id)
|
||||
|
||||
data_source = [
|
||||
EventsStreamData(self.hs, room_id=room_id, feedback=feedback)
|
||||
]
|
||||
event_stream = EventStream(user_id, data_source)
|
||||
pagin_config = yield event_stream.fix_tokens(pagin_config)
|
||||
data_chunk = yield event_stream.get_chunk(config=pagin_config)
|
||||
defer.returnValue(data_chunk)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def store_room_data(self, event=None, stamp_event=True):
|
||||
""" Stores data for a room.
|
||||
|
||||
Args:
|
||||
event : The room path event
|
||||
stamp_event (bool) : True to stamp event content with server keys.
|
||||
Raises:
|
||||
SynapseError if something went wrong.
|
||||
"""
|
||||
|
||||
with (yield self.room_lock.lock(event.room_id)):
|
||||
yield self.auth.check(event, raises=True)
|
||||
|
||||
if stamp_event:
|
||||
event.content["hsob_ts"] = int(self.clock.time_msec())
|
||||
|
||||
yield self.state_handler.handle_new_event(event)
|
||||
|
||||
# store in db
|
||||
store_id = yield self.store.persist_event(event)
|
||||
|
||||
event.destinations = yield self.store.get_joined_hosts_for_room(
|
||||
event.room_id
|
||||
)
|
||||
self.notifier.on_new_room_event(event, store_id)
|
||||
|
||||
yield self.hs.get_federation().handle_new_event(event)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_room_data(self, user_id=None, room_id=None,
|
||||
event_type=None, state_key="",
|
||||
public_room_rules=[],
|
||||
private_room_rules=["join"]):
|
||||
""" Get data from a room.
|
||||
|
||||
Args:
|
||||
event : The room path event
|
||||
public_room_rules : A list of membership states the user can be in,
|
||||
in order to read this data IN A PUBLIC ROOM. An empty list means
|
||||
'any state'.
|
||||
private_room_rules : A list of membership states the user can be
|
||||
in, in order to read this data IN A PRIVATE ROOM. An empty list
|
||||
means 'any state'.
|
||||
Returns:
|
||||
The path data content.
|
||||
Raises:
|
||||
SynapseError if something went wrong.
|
||||
"""
|
||||
if event_type == RoomTopicEvent.TYPE:
|
||||
# anyone invited/joined can read the topic
|
||||
private_room_rules = ["invite", "join"]
|
||||
|
||||
# does this room exist
|
||||
room = yield self.store.get_room(room_id)
|
||||
if not room:
|
||||
raise RoomError(403, "Room does not exist.")
|
||||
|
||||
# does this user exist in this room
|
||||
member = yield self.store.get_room_member(
|
||||
room_id=room_id,
|
||||
user_id="" if not user_id else user_id)
|
||||
|
||||
member_state = member.membership if member else None
|
||||
|
||||
if room.is_public and public_room_rules:
|
||||
# make sure the user meets public room rules
|
||||
if member_state not in public_room_rules:
|
||||
raise RoomError(403, "Member does not meet public room rules.")
|
||||
elif not room.is_public and private_room_rules:
|
||||
# make sure the user meets private room rules
|
||||
if member_state not in private_room_rules:
|
||||
raise RoomError(
|
||||
403, "Member does not meet private room rules.")
|
||||
|
||||
data = yield self.store.get_current_state(
|
||||
room_id, event_type, state_key
|
||||
)
|
||||
defer.returnValue(data)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_feedback(self, event_id):
|
||||
# yield self.auth.check_joined_room(room_id, user_id)
|
||||
|
||||
# Pull out the feedback from the db
|
||||
fb = yield self.store.get_feedback(event_id)
|
||||
|
||||
if fb:
|
||||
defer.returnValue(fb)
|
||||
defer.returnValue(None)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send_feedback(self, event, stamp_event=True):
|
||||
if stamp_event:
|
||||
event.content["hsob_ts"] = int(self.clock.time_msec())
|
||||
|
||||
with (yield self.room_lock.lock(event.room_id)):
|
||||
yield self.auth.check(event, raises=True)
|
||||
|
||||
# store message in db
|
||||
store_id = yield self.store.persist_event(event)
|
||||
|
||||
event.destinations = yield self.store.get_joined_hosts_for_room(
|
||||
event.room_id
|
||||
)
|
||||
yield self.hs.get_federation().handle_new_event(event)
|
||||
|
||||
self.notifier.on_new_room_event(event, store_id)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def snapshot_all_rooms(self, user_id=None, pagin_config=None,
|
||||
feedback=False):
|
||||
"""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
|
||||
joined, depending on the pagination config.
|
||||
|
||||
Args:
|
||||
user_id (str): The ID of the user making the request.
|
||||
pagin_config (synapse.api.streams.PaginationConfig): The pagination
|
||||
config used to determine how many messages *PER ROOM* to return.
|
||||
feedback (bool): True to get feedback along with these messages.
|
||||
Returns:
|
||||
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
|
||||
is joined on, may return a "messages" key with messages, depending
|
||||
on the specified PaginationConfig.
|
||||
"""
|
||||
room_list = yield self.store.get_rooms_for_user_where_membership_is(
|
||||
user_id=user_id,
|
||||
membership_list=[Membership.INVITE, Membership.JOIN]
|
||||
)
|
||||
|
||||
rooms_ret = []
|
||||
|
||||
now_rooms_token = yield self.store.get_room_events_max_id()
|
||||
|
||||
# FIXME (erikj): Fix this.
|
||||
presence_stream = PresenceStreamData(self.hs)
|
||||
now_presence_token = yield presence_stream.max_token()
|
||||
presence = yield presence_stream.get_rows(
|
||||
user_id, 0, now_presence_token, None, None
|
||||
)
|
||||
|
||||
# FIXME (erikj): We need to not generate this token,
|
||||
now_token = "%s_%s" % (now_rooms_token, now_presence_token)
|
||||
|
||||
for event in room_list:
|
||||
d = {
|
||||
"room_id": event.room_id,
|
||||
"membership": event.membership,
|
||||
}
|
||||
|
||||
if event.membership == Membership.INVITE:
|
||||
d["inviter"] = event.user_id
|
||||
|
||||
rooms_ret.append(d)
|
||||
|
||||
if event.membership != Membership.JOIN:
|
||||
continue
|
||||
try:
|
||||
messages, token = yield self.store.get_recent_events_for_room(
|
||||
event.room_id,
|
||||
limit=10,
|
||||
end_token=now_rooms_token,
|
||||
)
|
||||
|
||||
d["messages"] = {
|
||||
"chunk": [m.get_dict() for m in messages],
|
||||
"start": token[0],
|
||||
"end": token[1],
|
||||
}
|
||||
|
||||
current_state = yield self.store.get_current_state(event.room_id)
|
||||
d["state"] = [c.get_dict() for c in current_state]
|
||||
except:
|
||||
logger.exception("Failed to get snapshot")
|
||||
|
||||
user = self.hs.parse_userid(user_id)
|
||||
|
||||
ret = {"rooms": rooms_ret, "presence": presence[0], "end": now_token}
|
||||
|
||||
# logger.debug("snapshot_all_rooms returning: %s", ret)
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
|
||||
class RoomCreationHandler(BaseHandler):
|
||||
class RoomCreationHandler(BaseRoomHandler):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def create_room(self, user_id, room_id, config):
|
||||
@@ -383,6 +101,13 @@ class RoomCreationHandler(BaseHandler):
|
||||
content=config,
|
||||
)
|
||||
|
||||
snapshot = yield self.store.snapshot_room(
|
||||
room_id=room_id,
|
||||
user_id=user_id,
|
||||
state_type=RoomConfigEvent.TYPE,
|
||||
state_key="",
|
||||
)
|
||||
|
||||
if room_alias:
|
||||
yield self.store.create_room_alias_association(
|
||||
room_id=room_id,
|
||||
@@ -390,16 +115,16 @@ class RoomCreationHandler(BaseHandler):
|
||||
servers=[self.hs.hostname],
|
||||
)
|
||||
|
||||
yield self.state_handler.handle_new_event(config_event)
|
||||
yield self.state_handler.handle_new_event(config_event, snapshot)
|
||||
# store_id = persist...
|
||||
|
||||
yield self.hs.get_federation().handle_new_event(config_event)
|
||||
# self.notifier.on_new_room_event(event, store_id)
|
||||
federation_handler = self.hs.get_handlers().federation_handler
|
||||
yield federation_handler.handle_new_event(config_event, snapshot)
|
||||
|
||||
content = {"membership": Membership.JOIN}
|
||||
join_event = self.event_factory.create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
target_user_id=user_id,
|
||||
state_key=user_id,
|
||||
room_id=room_id,
|
||||
user_id=user_id,
|
||||
membership=Membership.JOIN,
|
||||
@@ -418,7 +143,7 @@ class RoomCreationHandler(BaseHandler):
|
||||
defer.returnValue(result)
|
||||
|
||||
|
||||
class RoomMemberHandler(BaseHandler):
|
||||
class RoomMemberHandler(BaseRoomHandler):
|
||||
# TODO(paul): This handler currently contains a messy conflation of
|
||||
# low-level API that works on UserID objects and so on, and REST-level
|
||||
# API that takes ID strings and returns pagination chunks. These concerns
|
||||
@@ -490,7 +215,7 @@ class RoomMemberHandler(BaseHandler):
|
||||
for entry in member_list
|
||||
]
|
||||
chunk_data = {
|
||||
"start": "START", # FIXME (erikj): START is no longer a valid value
|
||||
"start": "START", # FIXME (erikj): START is no longer valid
|
||||
"end": "END",
|
||||
"chunk": event_list
|
||||
}
|
||||
@@ -527,9 +252,15 @@ class RoomMemberHandler(BaseHandler):
|
||||
Raises:
|
||||
SynapseError if there was a problem changing the membership.
|
||||
"""
|
||||
target_user_id = event.state_key
|
||||
|
||||
snapshot = yield self.store.snapshot_room(
|
||||
event.room_id, event.user_id,
|
||||
RoomMemberEvent.TYPE, target_user_id
|
||||
)
|
||||
## TODO(markjh): get prev state from snapshot.
|
||||
prev_state = yield self.store.get_room_member(
|
||||
event.target_user_id, event.room_id
|
||||
target_user_id, event.room_id
|
||||
)
|
||||
|
||||
if prev_state:
|
||||
@@ -548,24 +279,22 @@ class RoomMemberHandler(BaseHandler):
|
||||
# if this HS is not currently in the room, i.e. we have to do the
|
||||
# invite/join dance.
|
||||
if event.membership == Membership.JOIN:
|
||||
yield self._do_join(event, do_auth=do_auth)
|
||||
yield self._do_join(event, snapshot, do_auth=do_auth)
|
||||
else:
|
||||
# This is not a JOIN, so we can handle it normally.
|
||||
if do_auth:
|
||||
yield self.auth.check(event, raises=True)
|
||||
yield self.auth.check(event, snapshot, raises=True)
|
||||
|
||||
prev_state = yield self.store.get_room_member(
|
||||
event.target_user_id, event.room_id
|
||||
)
|
||||
if prev_state and prev_state.membership == event.membership:
|
||||
# double same action, treat this event as a NOOP.
|
||||
defer.returnValue({})
|
||||
return
|
||||
|
||||
yield self.state_handler.handle_new_event(event)
|
||||
yield self.state_handler.handle_new_event(event, snapshot)
|
||||
yield self._do_local_membership_update(
|
||||
event,
|
||||
membership=event.content["membership"],
|
||||
snapshot=snapshot,
|
||||
)
|
||||
|
||||
defer.returnValue({"room_id": room_id})
|
||||
@@ -588,20 +317,25 @@ class RoomMemberHandler(BaseHandler):
|
||||
content.update({"membership": Membership.JOIN})
|
||||
new_event = self.event_factory.create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
target_user_id=joinee.to_string(),
|
||||
state_key=joinee.to_string(),
|
||||
room_id=room_id,
|
||||
user_id=joinee.to_string(),
|
||||
membership=Membership.JOIN,
|
||||
content=content,
|
||||
)
|
||||
|
||||
yield self._do_join(new_event, room_host=host, do_auth=True)
|
||||
snapshot = yield self.store.snapshot_room(
|
||||
room_id, joinee.to_string(), RoomMemberEvent.TYPE,
|
||||
joinee.to_string()
|
||||
)
|
||||
|
||||
yield self._do_join(new_event, snapshot, room_host=host, do_auth=True)
|
||||
|
||||
defer.returnValue({"room_id": room_id})
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_join(self, event, room_host=None, do_auth=True):
|
||||
joinee = self.hs.parse_userid(event.target_user_id)
|
||||
def _do_join(self, event, snapshot, room_host=None, do_auth=True):
|
||||
joinee = self.hs.parse_userid(event.state_key)
|
||||
# room_id = RoomID.from_string(event.room_id, self.hs)
|
||||
room_id = event.room_id
|
||||
|
||||
@@ -622,6 +356,7 @@ class RoomMemberHandler(BaseHandler):
|
||||
elif room_host:
|
||||
should_do_dance = True
|
||||
else:
|
||||
# TODO(markjh): get prev_state from snapshot
|
||||
prev_state = yield self.store.get_room_member(
|
||||
joinee.to_string(), room_id
|
||||
)
|
||||
@@ -641,7 +376,7 @@ class RoomMemberHandler(BaseHandler):
|
||||
if should_do_dance:
|
||||
handler = self.hs.get_handlers().federation_handler
|
||||
have_joined = yield handler.do_invite_join(
|
||||
room_host, room_id, event.user_id, event.content
|
||||
room_host, room_id, event.user_id, event.content, snapshot
|
||||
)
|
||||
|
||||
# We want to do the _do_update inside the room lock.
|
||||
@@ -649,12 +384,13 @@ class RoomMemberHandler(BaseHandler):
|
||||
logger.debug("Doing normal join")
|
||||
|
||||
if do_auth:
|
||||
yield self.auth.check(event, raises=True)
|
||||
yield self.auth.check(event, snapshot, raises=True)
|
||||
|
||||
yield self.state_handler.handle_new_event(event)
|
||||
yield self.state_handler.handle_new_event(event, snapshot)
|
||||
yield self._do_local_membership_update(
|
||||
event,
|
||||
membership=event.content["membership"],
|
||||
snapshot=snapshot,
|
||||
)
|
||||
|
||||
user = self.hs.parse_userid(event.user_id)
|
||||
@@ -698,38 +434,28 @@ class RoomMemberHandler(BaseHandler):
|
||||
|
||||
defer.returnValue([r.room_id for r in rooms])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_local_membership_update(self, event, membership):
|
||||
# store membership
|
||||
store_id = yield self.store.persist_event(event)
|
||||
|
||||
# Send a PDU to all hosts who have joined the room.
|
||||
destinations = yield self.store.get_joined_hosts_for_room(
|
||||
event.room_id
|
||||
)
|
||||
def _do_local_membership_update(self, event, membership, snapshot):
|
||||
destinations = []
|
||||
|
||||
# If we're inviting someone, then we should also send it to that
|
||||
# HS.
|
||||
target_user_id = event.state_key
|
||||
target_user = self.hs.parse_userid(target_user_id)
|
||||
if membership == Membership.INVITE:
|
||||
host = UserID.from_string(
|
||||
event.target_user_id, self.hs
|
||||
).domain
|
||||
host = target_user.domain
|
||||
destinations.append(host)
|
||||
|
||||
# If we are joining a remote HS, include that.
|
||||
if membership == Membership.JOIN:
|
||||
host = UserID.from_string(
|
||||
event.target_user_id, self.hs
|
||||
).domain
|
||||
host = target_user.domain
|
||||
destinations.append(host)
|
||||
|
||||
event.destinations = list(set(destinations))
|
||||
return self._on_new_room_event(
|
||||
event, snapshot, extra_destinations=destinations,
|
||||
extra_users=[target_user]
|
||||
)
|
||||
|
||||
yield self.hs.get_federation().handle_new_event(event)
|
||||
self.notifier.on_new_room_event(event, store_id)
|
||||
|
||||
|
||||
class RoomListHandler(BaseHandler):
|
||||
class RoomListHandler(BaseRoomHandler):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_public_room_list(self):
|
||||
|
||||
147
synapse/handlers/typing.py
Normal file
147
synapse/handlers/typing.py
Normal file
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from ._base import BaseHandler
|
||||
|
||||
from synapse.api.errors import SynapseError, AuthError
|
||||
|
||||
import logging
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# A tiny object useful for storing a user's membership in a room, as a mapping
|
||||
# key
|
||||
RoomMember = namedtuple("RoomMember", ("room_id", "user"))
|
||||
|
||||
|
||||
class TypingNotificationHandler(BaseHandler):
|
||||
def __init__(self, hs):
|
||||
super(TypingNotificationHandler, self).__init__(hs)
|
||||
|
||||
self.homeserver = hs
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
self.federation = hs.get_replication_layer()
|
||||
|
||||
self.federation.register_edu_handler("m.typing", self._recv_edu)
|
||||
|
||||
self._member_typing_until = {}
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def started_typing(self, target_user, auth_user, room_id, timeout):
|
||||
if not target_user.is_mine:
|
||||
raise SynapseError(400, "User is not hosted on this Home Server")
|
||||
|
||||
if target_user != auth_user:
|
||||
raise AuthError(400, "Cannot set another user's typing state")
|
||||
|
||||
until = self.clock.time_msec() + timeout
|
||||
member = RoomMember(room_id=room_id, user=target_user)
|
||||
|
||||
was_present = member in self._member_typing_until
|
||||
|
||||
self._member_typing_until[member] = until
|
||||
|
||||
if was_present:
|
||||
# No point sending another notification
|
||||
defer.returnValue(None)
|
||||
|
||||
yield self._push_update(
|
||||
room_id=room_id,
|
||||
user=target_user,
|
||||
typing=True,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def stopped_typing(self, target_user, auth_user, room_id):
|
||||
if not target_user.is_mine:
|
||||
raise SynapseError(400, "User is not hosted on this Home Server")
|
||||
|
||||
if target_user != auth_user:
|
||||
raise AuthError(400, "Cannot set another user's typing state")
|
||||
|
||||
member = RoomMember(room_id=room_id, user=target_user)
|
||||
|
||||
if member not in self._member_typing_until:
|
||||
# No point
|
||||
defer.returnValue(None)
|
||||
|
||||
yield self._push_update(
|
||||
room_id=room_id,
|
||||
user=target_user,
|
||||
typing=False,
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _push_update(self, room_id, user, typing):
|
||||
localusers = set()
|
||||
remotedomains = set()
|
||||
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
yield rm_handler.fetch_room_distributions_into(room_id,
|
||||
localusers=localusers, remotedomains=remotedomains,
|
||||
ignore_user=user)
|
||||
|
||||
for u in localusers:
|
||||
self.push_update_to_clients(
|
||||
room_id=room_id,
|
||||
observer_user=u,
|
||||
observed_user=user,
|
||||
typing=typing,
|
||||
)
|
||||
|
||||
deferreds = []
|
||||
for domain in remotedomains:
|
||||
deferreds.append(self.federation.send_edu(
|
||||
destination=domain,
|
||||
edu_type="m.typing",
|
||||
content={
|
||||
"room_id": room_id,
|
||||
"user_id": user.to_string(),
|
||||
"typing": typing,
|
||||
},
|
||||
))
|
||||
|
||||
yield defer.DeferredList(deferreds, consumeErrors=False)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _recv_edu(self, origin, content):
|
||||
room_id = content["room_id"]
|
||||
user = self.homeserver.parse_userid(content["user_id"])
|
||||
|
||||
localusers = set()
|
||||
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
yield rm_handler.fetch_room_distributions_into(room_id,
|
||||
localusers=localusers)
|
||||
|
||||
for u in localusers:
|
||||
self.push_update_to_clients(
|
||||
room_id=room_id,
|
||||
observer_user=u,
|
||||
observed_user=user,
|
||||
typing=content["typing"]
|
||||
)
|
||||
|
||||
def push_update_to_clients(self, room_id, observer_user, observed_user,
|
||||
typing):
|
||||
# TODO(paul) steal this from presence.py
|
||||
pass
|
||||
@@ -325,7 +325,9 @@ class ContentRepoResource(resource.Resource):
|
||||
|
||||
# FIXME (erikj): These should use constants.
|
||||
file_name = os.path.basename(fname)
|
||||
url = "http://%s/matrix/content/%s" % (self.hs.hostname, file_name)
|
||||
url = "http://%s/matrix/content/%s" % (
|
||||
self.hs.domain_with_port, file_name
|
||||
)
|
||||
|
||||
respond_with_json_bytes(request, 200,
|
||||
json.dumps({"content_token": url}),
|
||||
|
||||
241
synapse/notifier.py
Normal file
241
synapse/notifier.py
Normal file
@@ -0,0 +1,241 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from twisted.internet import defer, reactor
|
||||
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class _NotificationListener(object):
|
||||
""" This represents a single client connection to the events stream.
|
||||
|
||||
The events stream handler will have yielded to the deferred, so to
|
||||
notify the handler it is sufficient to resolve the deferred.
|
||||
|
||||
This listener will also keep track of which rooms it is listening in
|
||||
so that it can remove itself from the indexes in the Notifier class.
|
||||
"""
|
||||
|
||||
def __init__(self, user, rooms, from_token, limit, timeout, deferred):
|
||||
self.user = user
|
||||
self.from_token = from_token
|
||||
self.limit = limit
|
||||
self.timeout = timeout
|
||||
self.deferred = deferred
|
||||
|
||||
self.rooms = rooms
|
||||
|
||||
self.pending_notifications = []
|
||||
|
||||
def notify(self, notifier, events, start_token, end_token):
|
||||
""" Inform whoever is listening about the new events. This will
|
||||
also remove this listener from all the indexes in the Notifier
|
||||
it knows about.
|
||||
"""
|
||||
|
||||
result = (events, (start_token, end_token))
|
||||
|
||||
try:
|
||||
self.deferred.callback(result)
|
||||
except defer.AlreadyCalledError:
|
||||
pass
|
||||
|
||||
for room in self.rooms:
|
||||
lst = notifier.rooms_to_listeners.get(room, set())
|
||||
lst.discard(self)
|
||||
|
||||
notifier.user_to_listeners.get(self.user, set()).discard(self)
|
||||
|
||||
|
||||
class Notifier(object):
|
||||
""" This class is responsible for notifying any listeners when there are
|
||||
new events available for it.
|
||||
|
||||
Primarily used from the /events stream.
|
||||
"""
|
||||
|
||||
def __init__(self, hs):
|
||||
self.hs = hs
|
||||
|
||||
self.rooms_to_listeners = {}
|
||||
self.user_to_listeners = {}
|
||||
|
||||
self.event_sources = hs.get_event_sources()
|
||||
|
||||
hs.get_distributor().observe(
|
||||
"user_joined_room", self._user_joined_room
|
||||
)
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
def on_new_room_event(self, event, extra_users=[]):
|
||||
""" Used by handlers to inform the notifier something has happened
|
||||
in the room, room event wise.
|
||||
|
||||
This triggers the notifier to wake up any listeners that are
|
||||
listening to the room, and any listeners for the users in the
|
||||
`extra_users` param.
|
||||
"""
|
||||
room_id = event.room_id
|
||||
|
||||
source = self.event_sources.sources["room"]
|
||||
|
||||
listeners = self.rooms_to_listeners.get(room_id, set()).copy()
|
||||
|
||||
for user in extra_users:
|
||||
listeners |= self.user_to_listeners.get(user, set()).copy()
|
||||
|
||||
logger.debug("on_new_room_event listeners %s", listeners)
|
||||
|
||||
# TODO (erikj): Can we make this more efficient by hitting the
|
||||
# db once?
|
||||
for listener in listeners:
|
||||
events, end_token = yield source.get_new_events_for_user(
|
||||
listener.user,
|
||||
listener.from_token,
|
||||
listener.limit,
|
||||
)
|
||||
|
||||
if events:
|
||||
listener.notify(
|
||||
self, events, listener.from_token, end_token
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def on_new_user_event(self, users=[], rooms=[]):
|
||||
""" Used to inform listeners that something has happend
|
||||
presence/user event wise.
|
||||
|
||||
Will wake up all listeners for the given users and rooms.
|
||||
"""
|
||||
source = self.event_sources.sources["presence"]
|
||||
|
||||
listeners = set()
|
||||
|
||||
for user in users:
|
||||
listeners |= self.user_to_listeners.get(user, set()).copy()
|
||||
|
||||
for room in rooms:
|
||||
listeners |= self.rooms_to_listeners.get(room, set()).copy()
|
||||
|
||||
for listener in listeners:
|
||||
events, end_token = yield source.get_new_events_for_user(
|
||||
listener.user,
|
||||
listener.from_token,
|
||||
listener.limit,
|
||||
)
|
||||
|
||||
if events:
|
||||
listener.notify(
|
||||
self, events, listener.from_token, end_token
|
||||
)
|
||||
|
||||
def get_events_for(self, user, rooms, pagination_config, timeout):
|
||||
""" For the given user and rooms, return any new events for them. If
|
||||
there are no new events wait for up to `timeout` milliseconds for any
|
||||
new events to happen before returning.
|
||||
"""
|
||||
deferred = defer.Deferred()
|
||||
|
||||
self._get_events(
|
||||
deferred, user, rooms, pagination_config.from_token,
|
||||
pagination_config.limit, timeout
|
||||
).addErrback(deferred.errback)
|
||||
|
||||
return deferred
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _get_events(self, deferred, user, rooms, from_token, limit, timeout):
|
||||
if not from_token:
|
||||
from_token = yield self.event_sources.get_current_token()
|
||||
|
||||
listener = _NotificationListener(
|
||||
user,
|
||||
rooms,
|
||||
from_token,
|
||||
limit,
|
||||
timeout,
|
||||
deferred,
|
||||
)
|
||||
|
||||
if timeout:
|
||||
reactor.callLater(timeout/1000, self._timeout_listener, listener)
|
||||
|
||||
self._register_with_keys(listener)
|
||||
|
||||
yield self._check_for_updates(listener)
|
||||
|
||||
if not timeout:
|
||||
self._timeout_listener(listener)
|
||||
|
||||
return
|
||||
|
||||
def _timeout_listener(self, listener):
|
||||
# TODO (erikj): We should probably set to_token to the current max
|
||||
# rather than reusing from_token.
|
||||
listener.notify(
|
||||
self,
|
||||
[],
|
||||
listener.from_token,
|
||||
listener.from_token,
|
||||
)
|
||||
|
||||
@log_function
|
||||
def _register_with_keys(self, listener):
|
||||
for room in listener.rooms:
|
||||
s = self.rooms_to_listeners.setdefault(room, set())
|
||||
s.add(listener)
|
||||
|
||||
self.user_to_listeners.setdefault(listener.user, set()).add(listener)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def _check_for_updates(self, listener):
|
||||
# TODO (erikj): We need to think about limits across multiple sources
|
||||
events = []
|
||||
|
||||
from_token = listener.from_token
|
||||
limit = listener.limit
|
||||
|
||||
# TODO (erikj): DeferredList?
|
||||
for source in self.event_sources.sources.values():
|
||||
stuff, new_token = yield source.get_new_events_for_user(
|
||||
listener.user,
|
||||
from_token,
|
||||
limit,
|
||||
)
|
||||
|
||||
events.extend(stuff)
|
||||
|
||||
from_token = new_token
|
||||
|
||||
end_token = from_token
|
||||
|
||||
if events:
|
||||
listener.notify(self, events, listener.from_token, end_token)
|
||||
|
||||
defer.returnValue(listener)
|
||||
|
||||
def _user_joined_room(self, user, room_id):
|
||||
new_listeners = self.user_to_listeners.get(user, set())
|
||||
|
||||
listeners = self.rooms_to_listeners.setdefault(room_id, set())
|
||||
listeners |= new_listeners
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
|
||||
from . import (
|
||||
room, events, register, login, profile, public, presence, im, directory
|
||||
room, events, register, login, profile, presence, initial_sync, directory
|
||||
)
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ class RestServletFactory(object):
|
||||
register.register_servlets(hs, client_resource)
|
||||
login.register_servlets(hs, client_resource)
|
||||
profile.register_servlets(hs, client_resource)
|
||||
public.register_servlets(hs, client_resource)
|
||||
presence.register_servlets(hs, client_resource)
|
||||
im.register_servlets(hs, client_resource)
|
||||
initial_sync.register_servlets(hs, client_resource)
|
||||
directory.register_servlets(hs, client_resource)
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
|
||||
""" This module contains base REST classes for constructing REST servlets. """
|
||||
from synapse.api.urls import CLIENT_PREFIX
|
||||
from synapse.rest.transactions import HttpTransactionStore
|
||||
import re
|
||||
|
||||
|
||||
@@ -59,6 +60,7 @@ class RestServlet(object):
|
||||
self.handlers = hs.get_handlers()
|
||||
self.event_factory = hs.get_event_factory()
|
||||
self.auth = hs.get_auth()
|
||||
self.txns = HttpTransactionStore()
|
||||
|
||||
def register(self, http_server):
|
||||
""" Register this servlet with the given HTTP server. """
|
||||
|
||||
@@ -31,7 +31,7 @@ def register_servlets(hs, http_server):
|
||||
|
||||
|
||||
class ClientDirectoryServer(RestServlet):
|
||||
PATTERN = client_path_pattern("/ds/room/(?P<room_alias>[^/]*)$")
|
||||
PATTERN = client_path_pattern("/directory/room/(?P<room_alias>[^/]*)$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_alias):
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.api.streams import PaginationConfig
|
||||
from synapse.streams.config import PaginationConfig
|
||||
from synapse.rest.base import RestServlet, client_path_pattern
|
||||
|
||||
|
||||
@@ -41,11 +41,29 @@ class EventStreamRestServlet(RestServlet):
|
||||
|
||||
chunk = yield handler.get_stream(auth_user.to_string(), pagin_config,
|
||||
timeout=timeout)
|
||||
|
||||
defer.returnValue((200, chunk))
|
||||
|
||||
def on_OPTIONS(self, request):
|
||||
return (200, {})
|
||||
|
||||
|
||||
# TODO: Unit test gets, with and without auth, with different kinds of events.
|
||||
class EventRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/events/(?P<event_id>[^/]*)$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, event_id):
|
||||
auth_user = yield self.auth.get_user_by_req(request)
|
||||
handler = self.handlers.event_handler
|
||||
event = yield handler.get_event(auth_user, event_id)
|
||||
|
||||
if event:
|
||||
defer.returnValue((200, event.get_dict()))
|
||||
else:
|
||||
defer.returnValue((404, "Event not found."))
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
EventStreamRestServlet(hs).register(http_server)
|
||||
EventRestServlet(hs).register(http_server)
|
||||
|
||||
@@ -15,12 +15,13 @@
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.streams import PaginationConfig
|
||||
from synapse.streams.config import PaginationConfig
|
||||
from base import RestServlet, client_path_pattern
|
||||
|
||||
|
||||
class ImSyncRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/im/sync$")
|
||||
# TODO: Needs unit testing
|
||||
class InitialSyncRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/initialSync$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
@@ -37,4 +38,4 @@ class ImSyncRestServlet(RestServlet):
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
ImSyncRestServlet(hs).register(http_server)
|
||||
InitialSyncRestServlet(hs).register(http_server)
|
||||
@@ -27,7 +27,7 @@ class LoginRestServlet(RestServlet):
|
||||
PASS_TYPE = "m.login.password"
|
||||
|
||||
def on_GET(self, request):
|
||||
return (200, {"type": LoginRestServlet.PASS_TYPE})
|
||||
return (200, {"flows": [{"type": LoginRestServlet.PASS_TYPE}]})
|
||||
|
||||
def on_OPTIONS(self, request):
|
||||
return (200, {})
|
||||
|
||||
@@ -68,7 +68,7 @@ class PresenceStatusRestServlet(RestServlet):
|
||||
|
||||
|
||||
class PresenceListRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/presence_list/(?P<user_id>[^/]*)")
|
||||
PATTERN = client_path_pattern("/presence/list/(?P<user_id>[^/]*)")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, user_id):
|
||||
|
||||
@@ -33,10 +33,10 @@ class RegisterRestServlet(RestServlet):
|
||||
try:
|
||||
register_json = json.loads(request.content.read())
|
||||
if "password" in register_json:
|
||||
password = register_json["password"]
|
||||
password = register_json["password"].encode("utf-8")
|
||||
|
||||
if type(register_json["user_id"]) == unicode:
|
||||
desired_user_id = register_json["user_id"]
|
||||
desired_user_id = register_json["user_id"].encode("utf-8")
|
||||
if urllib.quote(desired_user_id) != desired_user_id:
|
||||
raise SynapseError(
|
||||
400,
|
||||
|
||||
@@ -18,10 +18,9 @@ from twisted.internet import defer
|
||||
|
||||
from base import RestServlet, client_path_pattern
|
||||
from synapse.api.errors import SynapseError, Codes
|
||||
from synapse.api.events.room import (RoomTopicEvent, MessageEvent,
|
||||
RoomMemberEvent, FeedbackEvent)
|
||||
from synapse.api.constants import Feedback, Membership
|
||||
from synapse.api.streams import PaginationConfig
|
||||
from synapse.streams.config import PaginationConfig
|
||||
from synapse.api.events.room import RoomMemberEvent
|
||||
from synapse.api.constants import Membership
|
||||
|
||||
import json
|
||||
import logging
|
||||
@@ -35,31 +34,28 @@ class RoomCreateRestServlet(RestServlet):
|
||||
# No PATTERN; we have custom dispatch rules here
|
||||
|
||||
def register(self, http_server):
|
||||
# /rooms OR /rooms/<roomid>
|
||||
http_server.register_path("POST",
|
||||
client_path_pattern("/rooms$"),
|
||||
self.on_POST)
|
||||
http_server.register_path("PUT",
|
||||
client_path_pattern(
|
||||
"/rooms/(?P<room_id>[^/]*)$"),
|
||||
self.on_PUT)
|
||||
PATTERN = "/createRoom"
|
||||
register_txn_path(self, PATTERN, http_server)
|
||||
# define CORS for all of /rooms in RoomCreateRestServlet for simplicity
|
||||
http_server.register_path("OPTIONS",
|
||||
client_path_pattern("/rooms(?:/.*)?$"),
|
||||
self.on_OPTIONS)
|
||||
# define CORS for /createRoom[/txnid]
|
||||
http_server.register_path("OPTIONS",
|
||||
client_path_pattern("/createRoom(?:/.*)?$"),
|
||||
self.on_OPTIONS)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id):
|
||||
room_id = urllib.unquote(room_id)
|
||||
auth_user = yield self.auth.get_user_by_req(request)
|
||||
def on_PUT(self, request, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not room_id:
|
||||
raise SynapseError(400, "PUT must specify a room ID")
|
||||
response = yield self.on_POST(request)
|
||||
|
||||
room_config = self.get_room_config(request)
|
||||
info = yield self.make_room(room_config, auth_user, room_id)
|
||||
room_config.update(info)
|
||||
defer.returnValue((200, info))
|
||||
self.txns.store_client_transaction(request, txn_id, response)
|
||||
defer.returnValue(response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request):
|
||||
@@ -95,254 +91,193 @@ class RoomCreateRestServlet(RestServlet):
|
||||
return (200, {})
|
||||
|
||||
|
||||
class RoomTopicRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/topic$")
|
||||
# TODO: Needs unit testing for generic events
|
||||
class RoomStateEventRestServlet(RestServlet):
|
||||
def register(self, http_server):
|
||||
# /room/$roomid/state/$eventtype
|
||||
no_state_key = "/rooms/(?P<room_id>[^/]*)/state/(?P<event_type>[^/]*)$"
|
||||
|
||||
def get_event_type(self):
|
||||
return RoomTopicEvent.TYPE
|
||||
# /room/$roomid/state/$eventtype/$statekey
|
||||
state_key = ("/rooms/(?P<room_id>[^/]*)/state/" +
|
||||
"(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$")
|
||||
|
||||
http_server.register_path("GET",
|
||||
client_path_pattern(state_key),
|
||||
self.on_GET)
|
||||
http_server.register_path("PUT",
|
||||
client_path_pattern(state_key),
|
||||
self.on_PUT)
|
||||
http_server.register_path("GET",
|
||||
client_path_pattern(no_state_key),
|
||||
self.on_GET_no_state_key)
|
||||
http_server.register_path("PUT",
|
||||
client_path_pattern(no_state_key),
|
||||
self.on_PUT_no_state_key)
|
||||
|
||||
def on_GET_no_state_key(self, request, room_id, event_type):
|
||||
return self.on_GET(request, room_id, event_type, "")
|
||||
|
||||
def on_PUT_no_state_key(self, request, room_id, event_type):
|
||||
return self.on_PUT(request, room_id, event_type, "")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id):
|
||||
def on_GET(self, request, room_id, event_type, state_key):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
msg_handler = self.handlers.message_handler
|
||||
data = yield msg_handler.get_room_data(
|
||||
user_id=user.to_string(),
|
||||
room_id=urllib.unquote(room_id),
|
||||
event_type=RoomTopicEvent.TYPE,
|
||||
state_key="",
|
||||
event_type=urllib.unquote(event_type),
|
||||
state_key=urllib.unquote(state_key),
|
||||
)
|
||||
|
||||
if not data:
|
||||
raise SynapseError(404, "Topic not found.", errcode=Codes.NOT_FOUND)
|
||||
defer.returnValue((200, data.content))
|
||||
raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)
|
||||
defer.returnValue((200, data[0].get_dict()["content"]))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id):
|
||||
def on_PUT(self, request, room_id, event_type, state_key):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
event_type = urllib.unquote(event_type)
|
||||
|
||||
content = _parse_json(request)
|
||||
|
||||
event = self.event_factory.create_event(
|
||||
etype=self.get_event_type(),
|
||||
etype=event_type,
|
||||
content=content,
|
||||
room_id=urllib.unquote(room_id),
|
||||
user_id=user.to_string(),
|
||||
state_key=urllib.unquote(state_key)
|
||||
)
|
||||
|
||||
msg_handler = self.handlers.message_handler
|
||||
yield msg_handler.store_room_data(
|
||||
event=event
|
||||
)
|
||||
defer.returnValue((200, ""))
|
||||
|
||||
|
||||
class JoinRoomAliasServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/join/(?P<room_alias>[^/]+)$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_alias):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
if not user:
|
||||
defer.returnValue((403, "Unrecognized user"))
|
||||
|
||||
logger.debug("room_alias: %s", room_alias)
|
||||
|
||||
room_alias = self.hs.parse_roomalias(urllib.unquote(room_alias))
|
||||
|
||||
handler = self.handlers.room_member_handler
|
||||
ret_dict = yield handler.join_room_alias(user, room_alias)
|
||||
|
||||
defer.returnValue((200, ret_dict))
|
||||
|
||||
|
||||
class RoomMemberRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members/"
|
||||
+ "(?P<target_user_id>[^/]*)/state$")
|
||||
|
||||
def get_event_type(self):
|
||||
return RoomMemberEvent.TYPE
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id, target_user_id):
|
||||
room_id = urllib.unquote(room_id)
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
handler = self.handlers.room_member_handler
|
||||
member = yield handler.get_room_member(
|
||||
room_id,
|
||||
urllib.unquote(target_user_id),
|
||||
user.to_string())
|
||||
if not member:
|
||||
raise SynapseError(404, "Member not found.",
|
||||
errcode=Codes.NOT_FOUND)
|
||||
defer.returnValue((200, member.content))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_DELETE(self, request, roomid, target_user_id):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
event = self.event_factory.create_event(
|
||||
etype=self.get_event_type(),
|
||||
target_user_id=urllib.unquote(target_user_id),
|
||||
room_id=urllib.unquote(roomid),
|
||||
user_id=user.to_string(),
|
||||
membership=Membership.LEAVE,
|
||||
content={"membership": Membership.LEAVE}
|
||||
if event_type == RoomMemberEvent.TYPE:
|
||||
# membership events are special
|
||||
handler = self.handlers.room_member_handler
|
||||
yield handler.change_membership(event)
|
||||
defer.returnValue((200, ""))
|
||||
else:
|
||||
# store random bits of state
|
||||
msg_handler = self.handlers.message_handler
|
||||
yield msg_handler.store_room_data(
|
||||
event=event
|
||||
)
|
||||
defer.returnValue((200, ""))
|
||||
|
||||
handler = self.handlers.room_member_handler
|
||||
yield handler.change_membership(event)
|
||||
defer.returnValue((200, ""))
|
||||
|
||||
# TODO: Needs unit testing for generic events + feedback
|
||||
class RoomSendEventRestServlet(RestServlet):
|
||||
|
||||
def register(self, http_server):
|
||||
# /rooms/$roomid/send/$event_type[/$txn_id]
|
||||
PATTERN = ("/rooms/(?P<room_id>[^/]*)/send/(?P<event_type>[^/]*)")
|
||||
register_txn_path(self, PATTERN, http_server, with_get=True)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, roomid, target_user_id):
|
||||
def on_POST(self, request, room_id, event_type):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
content = _parse_json(request)
|
||||
if "membership" not in content:
|
||||
raise SynapseError(400, "No membership key.",
|
||||
errcode=Codes.BAD_JSON)
|
||||
|
||||
valid_membership_values = [Membership.JOIN, Membership.INVITE]
|
||||
if (content["membership"] not in valid_membership_values):
|
||||
raise SynapseError(400, "Membership value must be %s." % (
|
||||
valid_membership_values,), errcode=Codes.BAD_JSON)
|
||||
|
||||
event = self.event_factory.create_event(
|
||||
etype=self.get_event_type(),
|
||||
target_user_id=urllib.unquote(target_user_id),
|
||||
room_id=urllib.unquote(roomid),
|
||||
user_id=user.to_string(),
|
||||
membership=content["membership"],
|
||||
content=content
|
||||
)
|
||||
|
||||
handler = self.handlers.room_member_handler
|
||||
yield handler.change_membership(event)
|
||||
defer.returnValue((200, ""))
|
||||
|
||||
|
||||
class MessageRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages/"
|
||||
+ "(?P<sender_id>[^/]*)/(?P<msg_id>[^/]*)$")
|
||||
|
||||
def get_event_type(self):
|
||||
return MessageEvent.TYPE
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id, sender_id, msg_id):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
msg_handler = self.handlers.message_handler
|
||||
msg = yield msg_handler.get_message(room_id=urllib.unquote(room_id),
|
||||
sender_id=urllib.unquote(sender_id),
|
||||
msg_id=msg_id,
|
||||
user_id=user.to_string(),
|
||||
)
|
||||
|
||||
if not msg:
|
||||
raise SynapseError(404, "Message not found.",
|
||||
errcode=Codes.NOT_FOUND)
|
||||
|
||||
defer.returnValue((200, json.loads(msg.content)))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id, sender_id, msg_id):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
if user.to_string() != urllib.unquote(sender_id):
|
||||
raise SynapseError(403, "Must send messages as yourself.",
|
||||
errcode=Codes.FORBIDDEN)
|
||||
|
||||
content = _parse_json(request)
|
||||
|
||||
event = self.event_factory.create_event(
|
||||
etype=self.get_event_type(),
|
||||
etype=event_type,
|
||||
room_id=urllib.unquote(room_id),
|
||||
user_id=user.to_string(),
|
||||
msg_id=msg_id,
|
||||
content=content
|
||||
)
|
||||
)
|
||||
|
||||
msg_handler = self.handlers.message_handler
|
||||
yield msg_handler.send_message(event)
|
||||
|
||||
defer.returnValue((200, ""))
|
||||
defer.returnValue((200, {"event_id": event.event_id}))
|
||||
|
||||
|
||||
class FeedbackRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern(
|
||||
"/rooms/(?P<room_id>[^/]*)/messages/" +
|
||||
"(?P<msg_sender_id>[^/]*)/(?P<msg_id>[^/]*)/feedback/" +
|
||||
"(?P<sender_id>[^/]*)/(?P<feedback_type>[^/]*)$"
|
||||
)
|
||||
|
||||
def get_event_type(self):
|
||||
return FeedbackEvent.TYPE
|
||||
def on_GET(self, request, room_id, event_type, txn_id):
|
||||
return (200, "Not implemented")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id, msg_sender_id, msg_id, fb_sender_id,
|
||||
feedback_type):
|
||||
user = yield (self.auth.get_user_by_req(request))
|
||||
def on_PUT(self, request, room_id, event_type, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# TODO (erikj): Implement this?
|
||||
raise NotImplementedError("Getting feedback is not supported")
|
||||
response = yield self.on_POST(request, room_id, event_type)
|
||||
|
||||
# if feedback_type not in Feedback.LIST:
|
||||
# raise SynapseError(400, "Bad feedback type.",
|
||||
# errcode=Codes.BAD_JSON)
|
||||
#
|
||||
# msg_handler = self.handlers.message_handler
|
||||
# feedback = yield msg_handler.get_feedback(
|
||||
# room_id=urllib.unquote(room_id),
|
||||
# msg_sender_id=msg_sender_id,
|
||||
# msg_id=msg_id,
|
||||
# user_id=user.to_string(),
|
||||
# fb_sender_id=fb_sender_id,
|
||||
# fb_type=feedback_type
|
||||
# )
|
||||
#
|
||||
# if not feedback:
|
||||
# raise SynapseError(404, "Feedback not found.",
|
||||
# errcode=Codes.NOT_FOUND)
|
||||
#
|
||||
# defer.returnValue((200, json.loads(feedback.content)))
|
||||
self.txns.store_client_transaction(request, txn_id, response)
|
||||
defer.returnValue(response)
|
||||
|
||||
|
||||
# TODO: Needs unit testing for room ID + alias joins
|
||||
class JoinRoomAliasServlet(RestServlet):
|
||||
|
||||
def register(self, http_server):
|
||||
# /join/$room_identifier[/$txn_id]
|
||||
PATTERN = ("/join/(?P<room_identifier>[^/]*)")
|
||||
register_txn_path(self, PATTERN, http_server)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id, sender_id, msg_id, fb_sender_id,
|
||||
feedback_type):
|
||||
user = yield (self.auth.get_user_by_req(request))
|
||||
def on_POST(self, request, room_identifier):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
if user.to_string() != fb_sender_id:
|
||||
raise SynapseError(403, "Must send feedback as yourself.",
|
||||
errcode=Codes.FORBIDDEN)
|
||||
# the identifier could be a room alias or a room id. Try one then the
|
||||
# other if it fails to parse, without swallowing other valid
|
||||
# SynapseErrors.
|
||||
|
||||
if feedback_type not in Feedback.LIST:
|
||||
raise SynapseError(400, "Bad feedback type.",
|
||||
errcode=Codes.BAD_JSON)
|
||||
|
||||
content = _parse_json(request)
|
||||
|
||||
event = self.event_factory.create_event(
|
||||
etype=self.get_event_type(),
|
||||
room_id=urllib.unquote(room_id),
|
||||
msg_sender_id=sender_id,
|
||||
msg_id=msg_id,
|
||||
user_id=user.to_string(), # user sending the feedback
|
||||
feedback_type=feedback_type,
|
||||
content=content
|
||||
identifier = None
|
||||
is_room_alias = False
|
||||
try:
|
||||
identifier = self.hs.parse_roomalias(
|
||||
urllib.unquote(room_identifier)
|
||||
)
|
||||
is_room_alias = True
|
||||
except SynapseError:
|
||||
identifier = self.hs.parse_roomid(
|
||||
urllib.unquote(room_identifier)
|
||||
)
|
||||
|
||||
msg_handler = self.handlers.message_handler
|
||||
yield msg_handler.send_feedback(event)
|
||||
# TODO: Support for specifying the home server to join with?
|
||||
|
||||
defer.returnValue((200, ""))
|
||||
if is_room_alias:
|
||||
handler = self.handlers.room_member_handler
|
||||
ret_dict = yield handler.join_room_alias(user, identifier)
|
||||
defer.returnValue((200, ret_dict))
|
||||
else: # room id
|
||||
event = self.event_factory.create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
content={"membership": Membership.JOIN},
|
||||
room_id=urllib.unquote(identifier.to_string()),
|
||||
user_id=user.to_string(),
|
||||
state_key=user.to_string()
|
||||
)
|
||||
handler = self.handlers.room_member_handler
|
||||
yield handler.change_membership(event)
|
||||
defer.returnValue((200, ""))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_identifier, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
response = yield self.on_POST(request, room_identifier)
|
||||
|
||||
self.txns.store_client_transaction(request, txn_id, response)
|
||||
defer.returnValue(response)
|
||||
|
||||
|
||||
# TODO: Needs unit testing
|
||||
class PublicRoomListRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/publicRooms$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
handler = self.handlers.room_list_handler
|
||||
data = yield handler.get_public_room_list()
|
||||
defer.returnValue((200, data))
|
||||
|
||||
|
||||
# TODO: Needs unit testing
|
||||
class RoomMemberListRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members/list$")
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id):
|
||||
@@ -354,7 +289,8 @@ class RoomMemberListRestServlet(RestServlet):
|
||||
user_id=user.to_string())
|
||||
|
||||
for event in members["chunk"]:
|
||||
target_user = self.hs.parse_userid(event["target_user_id"])
|
||||
# FIXME: should probably be state_key here, not user_id
|
||||
target_user = self.hs.parse_userid(event["user_id"])
|
||||
# Presence is an optional cache; don't fail if we can't fetch it
|
||||
try:
|
||||
presence_state = yield self.handlers.presence_handler.get_state(
|
||||
@@ -367,8 +303,9 @@ class RoomMemberListRestServlet(RestServlet):
|
||||
defer.returnValue((200, members))
|
||||
|
||||
|
||||
# TODO: Needs unit testing
|
||||
class RoomMessageListRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages/list$")
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id):
|
||||
@@ -385,6 +322,50 @@ class RoomMessageListRestServlet(RestServlet):
|
||||
defer.returnValue((200, msgs))
|
||||
|
||||
|
||||
# TODO: Needs unit testing
|
||||
class RoomStateRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/state$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
# TODO: Get all the current state for this room and return in the same
|
||||
# format as initial sync, that is:
|
||||
# [
|
||||
# { state event }, { state event }
|
||||
# ]
|
||||
defer.returnValue((200, []))
|
||||
|
||||
|
||||
# TODO: Needs unit testing
|
||||
class RoomInitialSyncRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/initialSync$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
# TODO: Get all the initial sync data for this room and return in the
|
||||
# same format as initial sync, that is:
|
||||
# {
|
||||
# membership: join,
|
||||
# messages: [
|
||||
# chunk: [ msg events ],
|
||||
# start: s_tok,
|
||||
# end: e_tok
|
||||
# ],
|
||||
# room_id: foo,
|
||||
# state: [
|
||||
# { state event } , { state event }
|
||||
# ]
|
||||
# }
|
||||
# Probably worth keeping the keys room_id and membership for parity with
|
||||
# /initialSync even though they must be joined to sync this and know the
|
||||
# room ID, so clients can reuse the same code (room_id and membership
|
||||
# are MANDATORY for /initialSync, so the code will expect it to be
|
||||
# there)
|
||||
defer.returnValue((200, {}))
|
||||
|
||||
|
||||
class RoomTriggerBackfill(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/backfill$")
|
||||
|
||||
@@ -400,6 +381,53 @@ class RoomTriggerBackfill(RestServlet):
|
||||
res = [event.get_dict() for event in events]
|
||||
defer.returnValue((200, res))
|
||||
|
||||
|
||||
# TODO: Needs unit testing
|
||||
class RoomMembershipRestServlet(RestServlet):
|
||||
|
||||
def register(self, http_server):
|
||||
# /rooms/$roomid/[invite|join|leave]
|
||||
PATTERN = ("/rooms/(?P<room_id>[^/]*)/" +
|
||||
"(?P<membership_action>join|invite|leave)")
|
||||
register_txn_path(self, PATTERN, http_server)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request, room_id, membership_action):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
content = _parse_json(request)
|
||||
|
||||
# target user is you unless it is an invite
|
||||
state_key = user.to_string()
|
||||
if membership_action == "invite":
|
||||
if "user_id" not in content:
|
||||
raise SynapseError(400, "Missing user_id key.")
|
||||
state_key = content["user_id"]
|
||||
|
||||
event = self.event_factory.create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
content={"membership": unicode(membership_action)},
|
||||
room_id=urllib.unquote(room_id),
|
||||
user_id=user.to_string(),
|
||||
state_key=state_key
|
||||
)
|
||||
handler = self.handlers.room_member_handler
|
||||
yield handler.change_membership(event)
|
||||
defer.returnValue((200, ""))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id, membership_action, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
response = yield self.on_POST(request, room_id, membership_action)
|
||||
|
||||
self.txns.store_client_transaction(request, txn_id, response)
|
||||
defer.returnValue(response)
|
||||
|
||||
|
||||
def _parse_json(request):
|
||||
try:
|
||||
content = json.loads(request.content.read())
|
||||
@@ -411,13 +439,46 @@ def _parse_json(request):
|
||||
raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
|
||||
|
||||
|
||||
def register_txn_path(servlet, regex_string, http_server, with_get=False):
|
||||
"""Registers a transaction-based path.
|
||||
|
||||
This registers two paths:
|
||||
PUT regex_string/$txnid
|
||||
POST regex_string
|
||||
|
||||
Args:
|
||||
regex_string (str): The regex string to register. Must NOT have a
|
||||
trailing $ as this string will be appended to.
|
||||
http_server : The http_server to register paths with.
|
||||
with_get: True to also register respective GET paths for the PUTs.
|
||||
"""
|
||||
http_server.register_path(
|
||||
"POST",
|
||||
client_path_pattern(regex_string + "$"),
|
||||
servlet.on_POST
|
||||
)
|
||||
http_server.register_path(
|
||||
"PUT",
|
||||
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"),
|
||||
servlet.on_PUT
|
||||
)
|
||||
if with_get:
|
||||
http_server.register_path(
|
||||
"GET",
|
||||
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"),
|
||||
servlet.on_GET
|
||||
)
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
RoomTopicRestServlet(hs).register(http_server)
|
||||
RoomMemberRestServlet(hs).register(http_server)
|
||||
MessageRestServlet(hs).register(http_server)
|
||||
FeedbackRestServlet(hs).register(http_server)
|
||||
RoomStateEventRestServlet(hs).register(http_server)
|
||||
RoomCreateRestServlet(hs).register(http_server)
|
||||
RoomMemberListRestServlet(hs).register(http_server)
|
||||
RoomMessageListRestServlet(hs).register(http_server)
|
||||
JoinRoomAliasServlet(hs).register(http_server)
|
||||
RoomTriggerBackfill(hs).register(http_server)
|
||||
RoomMembershipRestServlet(hs).register(http_server)
|
||||
RoomSendEventRestServlet(hs).register(http_server)
|
||||
PublicRoomListRestServlet(hs).register(http_server)
|
||||
RoomStateRestServlet(hs).register(http_server)
|
||||
RoomInitialSyncRestServlet(hs).register(http_server)
|
||||
|
||||
96
synapse/rest/transactions.py
Normal file
96
synapse/rest/transactions.py
Normal file
@@ -0,0 +1,96 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module contains logic for storing HTTP PUT transactions. This is used
|
||||
to ensure idempotency when performing PUTs using the REST API."""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HttpTransactionStore(object):
|
||||
|
||||
def __init__(self):
|
||||
# { key : (txn_id, response) }
|
||||
self.transactions = {}
|
||||
|
||||
def get_response(self, key, txn_id):
|
||||
"""Retrieve a response for this request.
|
||||
|
||||
Args:
|
||||
key (str): A transaction-independent key for this request. Typically
|
||||
this is a combination of the path (without the transaction id) and
|
||||
the user's access token.
|
||||
txn_id (str): The transaction ID for this request
|
||||
Returns:
|
||||
A tuple of (HTTP response code, response content) or None.
|
||||
"""
|
||||
try:
|
||||
logger.debug("get_response Key: %s TxnId: %s", key, txn_id)
|
||||
(last_txn_id, response) = self.transactions[key]
|
||||
if txn_id == last_txn_id:
|
||||
logger.info("get_response: Returning a response for %s", key)
|
||||
return response
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def store_response(self, key, txn_id, response):
|
||||
"""Stores an HTTP response tuple.
|
||||
|
||||
Args:
|
||||
key (str): A transaction-independent key for this request. Typically
|
||||
this is a combination of the path (without the transaction id) and
|
||||
the user's access token.
|
||||
txn_id (str): The transaction ID for this request.
|
||||
response (tuple): A tuple of (HTTP response code, response content)
|
||||
"""
|
||||
logger.debug("store_response Key: %s TxnId: %s", key, txn_id)
|
||||
self.transactions[key] = (txn_id, response)
|
||||
|
||||
def store_client_transaction(self, request, txn_id, response):
|
||||
"""Stores the request/response pair of an HTTP transaction.
|
||||
|
||||
Args:
|
||||
request (twisted.web.http.Request): The twisted HTTP request. This
|
||||
request must have the transaction ID as the last path segment.
|
||||
response (tuple): A tuple of (response code, response dict)
|
||||
txn_id (str): The transaction ID for this request.
|
||||
"""
|
||||
self.store_response(self._get_key(request), txn_id, response)
|
||||
|
||||
def get_client_transaction(self, request, txn_id):
|
||||
"""Retrieves a stored response if there was one.
|
||||
|
||||
Args:
|
||||
request (twisted.web.http.Request): The twisted HTTP request. This
|
||||
request must have the transaction ID as the last path segment.
|
||||
txn_id (str): The transaction ID for this request.
|
||||
Returns:
|
||||
The response tuple.
|
||||
Raises:
|
||||
KeyError if the transaction was not found.
|
||||
"""
|
||||
response = self.get_response(self._get_key(request), txn_id)
|
||||
if response is None:
|
||||
raise KeyError("Transaction not found.")
|
||||
return response
|
||||
|
||||
def _get_key(self, request):
|
||||
token = request.args["access_token"][0]
|
||||
path_without_txn_id = request.path.rsplit("/", 1)[0]
|
||||
return path_without_txn_id + "/" + token
|
||||
|
||||
|
||||
@@ -20,18 +20,18 @@
|
||||
|
||||
# Imports required for the default HomeServer() implementation
|
||||
from synapse.federation import initialize_http_replication
|
||||
from synapse.federation.handler import FederationEventHandler
|
||||
from synapse.api.events.factory import EventFactory
|
||||
from synapse.api.notifier import Notifier
|
||||
from synapse.notifier import Notifier
|
||||
from synapse.api.auth import Auth
|
||||
from synapse.handlers import Handlers
|
||||
from synapse.rest import RestServletFactory
|
||||
from synapse.state import StateHandler
|
||||
from synapse.storage import DataStore
|
||||
from synapse.types import UserID, RoomAlias
|
||||
from synapse.types import UserID, RoomAlias, RoomID
|
||||
from synapse.util import Clock
|
||||
from synapse.util.distributor import Distributor
|
||||
from synapse.util.lockutils import LockManager
|
||||
from synapse.streams.events import EventSources
|
||||
|
||||
|
||||
class BaseHomeServer(object):
|
||||
@@ -58,7 +58,6 @@ class BaseHomeServer(object):
|
||||
'http_client',
|
||||
'db_pool',
|
||||
'persistence_service',
|
||||
'federation',
|
||||
'replication_layer',
|
||||
'datastore',
|
||||
'event_factory',
|
||||
@@ -73,6 +72,7 @@ class BaseHomeServer(object):
|
||||
'resource_for_federation',
|
||||
'resource_for_web_client',
|
||||
'resource_for_content_repo',
|
||||
'event_sources',
|
||||
]
|
||||
|
||||
def __init__(self, hostname, **kwargs):
|
||||
@@ -117,6 +117,9 @@ class BaseHomeServer(object):
|
||||
|
||||
setattr(BaseHomeServer, "get_%s" % (depname), _get)
|
||||
|
||||
# TODO: Why are these parse_ methods so high up along with other globals?
|
||||
# Surely these should be in a util package or in the api package?
|
||||
|
||||
# Other utility methods
|
||||
def parse_userid(self, s):
|
||||
"""Parse the string given by 's' as a User ID and return a UserID
|
||||
@@ -128,6 +131,11 @@ class BaseHomeServer(object):
|
||||
object."""
|
||||
return RoomAlias.from_string(s, hs=self)
|
||||
|
||||
def parse_roomid(self, s):
|
||||
"""Parse the string given by 's' as a Room ID and return a RoomID
|
||||
object."""
|
||||
return RoomID.from_string(s, hs=self)
|
||||
|
||||
# Build magic accessors for every dependency
|
||||
for depname in BaseHomeServer.DEPENDENCIES:
|
||||
BaseHomeServer._make_dependency_method(depname)
|
||||
@@ -152,9 +160,6 @@ class HomeServer(BaseHomeServer):
|
||||
def build_replication_layer(self):
|
||||
return initialize_http_replication(self)
|
||||
|
||||
def build_federation(self):
|
||||
return FederationEventHandler(self)
|
||||
|
||||
def build_datastore(self):
|
||||
return DataStore(self)
|
||||
|
||||
@@ -182,6 +187,9 @@ class HomeServer(BaseHomeServer):
|
||||
def build_distributor(self):
|
||||
return Distributor()
|
||||
|
||||
def build_event_sources(self):
|
||||
return EventSources(self)
|
||||
|
||||
def register_servlets(self):
|
||||
""" Register all servlets associated with this HomeServer.
|
||||
"""
|
||||
|
||||
@@ -45,7 +45,7 @@ class StateHandler(object):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def handle_new_event(self, event):
|
||||
def handle_new_event(self, event, snapshot):
|
||||
""" Given an event this works out if a) we have sufficient power level
|
||||
to update the state and b) works out what the prev_state should be.
|
||||
|
||||
@@ -70,25 +70,13 @@ class StateHandler(object):
|
||||
# Now I need to fill out the prev state and work out if it has auth
|
||||
# (w.r.t. to power levels)
|
||||
|
||||
results = yield self.store.get_latest_pdus_in_context(
|
||||
event.room_id
|
||||
)
|
||||
snapshot.fill_out_prev_events(event)
|
||||
|
||||
event.prev_events = [
|
||||
encode_event_id(p_id, origin) for p_id, origin, _ in results
|
||||
]
|
||||
event.prev_events = [
|
||||
e for e in event.prev_events if e != event.event_id
|
||||
]
|
||||
|
||||
if results:
|
||||
event.depth = max([int(v) for _, _, v in results]) + 1
|
||||
else:
|
||||
event.depth = 0
|
||||
|
||||
current_state = yield self.store.get_current_state_pdu(
|
||||
key.context, key.type, key.state_key
|
||||
)
|
||||
current_state = snapshot.prev_state_pdu
|
||||
|
||||
if current_state:
|
||||
event.prev_state = encode_event_id(
|
||||
|
||||
@@ -16,8 +16,9 @@
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.events.room import (
|
||||
RoomMemberEvent, MessageEvent, RoomTopicEvent, FeedbackEvent,
|
||||
RoomConfigEvent, RoomNameEvent,
|
||||
RoomMemberEvent, RoomTopicEvent, FeedbackEvent,
|
||||
# RoomConfigEvent,
|
||||
RoomNameEvent,
|
||||
)
|
||||
|
||||
from synapse.util.logutils import log_function
|
||||
@@ -56,20 +57,22 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@log_function
|
||||
def persist_event(self, event, backfilled=False):
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
yield self._store_room_member(event)
|
||||
elif event.type == FeedbackEvent.TYPE:
|
||||
yield self._store_feedback(event)
|
||||
# elif event.type == RoomConfigEvent.TYPE:
|
||||
# yield self._store_room_config(event)
|
||||
elif event.type == RoomNameEvent.TYPE:
|
||||
yield self._store_room_name(event)
|
||||
elif event.type == RoomTopicEvent.TYPE:
|
||||
yield self._store_room_topic(event)
|
||||
def persist_event(self, event=None, backfilled=False, pdu=None):
|
||||
stream_ordering = None
|
||||
if backfilled:
|
||||
if not self.min_token_deferred.called:
|
||||
yield self.min_token_deferred
|
||||
self.min_token -= 1
|
||||
stream_ordering = self.min_token
|
||||
|
||||
ret = yield self._store_event(event, backfilled)
|
||||
defer.returnValue(ret)
|
||||
latest = yield self._db_pool.runInteraction(
|
||||
self._persist_pdu_event_txn,
|
||||
pdu=pdu,
|
||||
event=event,
|
||||
backfilled=backfilled,
|
||||
stream_ordering=stream_ordering,
|
||||
)
|
||||
defer.returnValue(latest)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_event(self, event_id):
|
||||
@@ -79,7 +82,6 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||
[
|
||||
"event_id",
|
||||
"type",
|
||||
"sender",
|
||||
"room_id",
|
||||
"content",
|
||||
"unrecognized_keys"
|
||||
@@ -89,12 +91,44 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||
event = self._parse_event_from_row(events_dict)
|
||||
defer.returnValue(event)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _persist_pdu_event_txn(self, txn, pdu=None, event=None,
|
||||
backfilled=False, stream_ordering=None):
|
||||
if pdu is not None:
|
||||
self._persist_event_pdu_txn(txn, pdu)
|
||||
if event is not None:
|
||||
return self._persist_event_txn(
|
||||
txn, event, backfilled, stream_ordering
|
||||
)
|
||||
|
||||
def _persist_event_pdu_txn(self, txn, pdu):
|
||||
cols = dict(pdu.__dict__)
|
||||
unrec_keys = dict(pdu.unrecognized_keys)
|
||||
del cols["content"]
|
||||
del cols["prev_pdus"]
|
||||
cols["content_json"] = json.dumps(pdu.content)
|
||||
cols["unrecognized_keys"] = json.dumps(unrec_keys)
|
||||
|
||||
logger.debug("Persisting: %s", repr(cols))
|
||||
|
||||
if pdu.is_state:
|
||||
self._persist_state_txn(txn, pdu.prev_pdus, cols)
|
||||
else:
|
||||
self._persist_pdu_txn(txn, pdu.prev_pdus, cols)
|
||||
|
||||
self._update_min_depth_for_context_txn(txn, pdu.context, pdu.depth)
|
||||
|
||||
@log_function
|
||||
def _store_event(self, event, backfilled):
|
||||
# FIXME (erikj): This should be removed when we start amalgamating
|
||||
# event and pdu storage
|
||||
yield self.hs.get_federation().fill_out_prev_events(event)
|
||||
def _persist_event_txn(self, txn, event, backfilled, stream_ordering=None):
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
self._store_room_member_txn(txn, event)
|
||||
elif event.type == FeedbackEvent.TYPE:
|
||||
self._store_feedback_txn(txn,event)
|
||||
# elif event.type == RoomConfigEvent.TYPE:
|
||||
# self._store_room_config_txn(txn, event)
|
||||
elif event.type == RoomNameEvent.TYPE:
|
||||
self._store_room_name_txn(txn, event)
|
||||
elif event.type == RoomTopicEvent.TYPE:
|
||||
self._store_room_topic_txn(txn, event)
|
||||
|
||||
vals = {
|
||||
"topological_ordering": event.depth,
|
||||
@@ -105,17 +139,14 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||
"processed": True,
|
||||
}
|
||||
|
||||
if stream_ordering is not None:
|
||||
vals["stream_ordering"] = stream_ordering
|
||||
|
||||
if hasattr(event, "outlier"):
|
||||
vals["outlier"] = event.outlier
|
||||
else:
|
||||
vals["outlier"] = False
|
||||
|
||||
if backfilled:
|
||||
if not self.min_token_deferred.called:
|
||||
yield self.min_token_deferred
|
||||
self.min_token -= 1
|
||||
vals["stream_ordering"] = self.min_token
|
||||
|
||||
unrec = {
|
||||
k: v
|
||||
for k, v in event.get_full_dict().items()
|
||||
@@ -124,7 +155,7 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||
vals["unrecognized_keys"] = json.dumps(unrec)
|
||||
|
||||
try:
|
||||
yield self._simple_insert("events", vals)
|
||||
self._simple_insert_txn(txn, "events", vals)
|
||||
except:
|
||||
logger.exception(
|
||||
"Failed to persist, probably duplicate: %s",
|
||||
@@ -143,9 +174,10 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||
if hasattr(event, "prev_state"):
|
||||
vals["prev_state"] = event.prev_state
|
||||
|
||||
yield self._simple_insert("state_events", vals)
|
||||
self._simple_insert_txn(txn, "state_events", vals)
|
||||
|
||||
yield self._simple_insert(
|
||||
self._simple_insert_txn(
|
||||
txn,
|
||||
"current_state_events",
|
||||
{
|
||||
"event_id": event.event_id,
|
||||
@@ -155,8 +187,7 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||
}
|
||||
)
|
||||
|
||||
latest = yield self.get_room_events_max_id()
|
||||
defer.returnValue(latest)
|
||||
return self._get_room_events_max_id_txn(txn)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_current_state(self, room_id, event_type=None, state_key=""):
|
||||
@@ -192,6 +223,85 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||
defer.returnValue(self.min_token)
|
||||
|
||||
|
||||
def snapshot_room(self, room_id, user_id, state_type=None, state_key=None):
|
||||
"""Snapshot the room for an update by a user
|
||||
Args:
|
||||
room_id (synapse.types.RoomId): The room to snapshot.
|
||||
user_id (synapse.types.UserId): The user to snapshot the room for.
|
||||
state_type (str): Optional state type to snapshot.
|
||||
state_key (str): Optional state key to snapshot.
|
||||
Returns:
|
||||
synapse.storage.Snapshot: A snapshot of the state of the room.
|
||||
"""
|
||||
def _snapshot(txn):
|
||||
membership_state = self._get_room_member(txn, user_id, room_id)
|
||||
prev_pdus = self._get_latest_pdus_in_context(
|
||||
txn, room_id
|
||||
)
|
||||
if state_type is not None and state_key is not None:
|
||||
prev_state_pdu = self._get_current_state_pdu(
|
||||
txn, room_id, state_type, state_key
|
||||
)
|
||||
else:
|
||||
prev_state_pdu = None
|
||||
|
||||
return Snapshot(
|
||||
store=self,
|
||||
room_id=room_id,
|
||||
user_id=user_id,
|
||||
prev_pdus=prev_pdus,
|
||||
membership_state=membership_state,
|
||||
state_type=state_type,
|
||||
state_key=state_key,
|
||||
prev_state_pdu=prev_state_pdu,
|
||||
)
|
||||
|
||||
return self._db_pool.runInteraction(_snapshot)
|
||||
|
||||
|
||||
class Snapshot(object):
|
||||
"""Snapshot of the state of a room
|
||||
Args:
|
||||
store (DataStore): The datastore.
|
||||
room_id (RoomId): The room of the snapshot.
|
||||
user_id (UserId): The user this snapshot is for.
|
||||
prev_pdus (list): The list of PDU ids this snapshot is after.
|
||||
membership_state (RoomMemberEvent): The current state of the user in
|
||||
the room.
|
||||
state_type (str, optional): State type captured by the snapshot
|
||||
state_key (str, optional): State key captured by the snapshot
|
||||
prev_state_pdu (PduEntry, optional): pdu id of
|
||||
the previous value of the state type and key in the room.
|
||||
"""
|
||||
|
||||
def __init__(self, store, room_id, user_id, prev_pdus,
|
||||
membership_state, state_type=None, state_key=None,
|
||||
prev_state_pdu=None):
|
||||
self.store = store
|
||||
self.room_id = room_id
|
||||
self.user_id = user_id
|
||||
self.prev_pdus = prev_pdus
|
||||
self.membership_state = membership_state
|
||||
self.state_type = state_type
|
||||
self.state_key = state_key
|
||||
self.prev_state_pdu = prev_state_pdu
|
||||
|
||||
def fill_out_prev_events(self, event):
|
||||
if hasattr(event, "prev_events"):
|
||||
return
|
||||
|
||||
es = [
|
||||
"%s@%s" % (p_id, origin) for p_id, origin, _ in self.prev_pdus
|
||||
]
|
||||
|
||||
event.prev_events = [e for e in es if e != event.event_id]
|
||||
|
||||
if self.prev_pdus:
|
||||
event.depth = max([int(v) for _, _, v in self.prev_pdus]) + 1
|
||||
else:
|
||||
event.depth = 0
|
||||
|
||||
|
||||
def schema_path(schema):
|
||||
""" Get a filesystem path for the named database schema
|
||||
|
||||
|
||||
@@ -86,16 +86,18 @@ class SQLBaseStore(object):
|
||||
table : string giving the table name
|
||||
values : dict of new column names and values for them
|
||||
"""
|
||||
return self._db_pool.runInteraction(
|
||||
self._simple_insert_txn, table, values,
|
||||
)
|
||||
|
||||
def _simple_insert_txn(self, txn, table, values):
|
||||
sql = "INSERT INTO %s (%s) VALUES(%s)" % (
|
||||
table,
|
||||
", ".join(k for k in values),
|
||||
", ".join("?" for k in values)
|
||||
)
|
||||
|
||||
def func(txn):
|
||||
txn.execute(sql, values.values())
|
||||
return txn.lastrowid
|
||||
return self._db_pool.runInteraction(func)
|
||||
txn.execute(sql, values.values())
|
||||
return txn.lastrowid
|
||||
|
||||
def _simple_select_one(self, table, keyvalues, retcols,
|
||||
allow_none=False):
|
||||
|
||||
@@ -15,21 +15,17 @@
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from ._base import SQLBaseStore, Table
|
||||
from synapse.api.events.room import FeedbackEvent
|
||||
|
||||
import collections
|
||||
import json
|
||||
from ._base import SQLBaseStore
|
||||
|
||||
|
||||
class FeedbackStore(SQLBaseStore):
|
||||
|
||||
def _store_feedback(self, event):
|
||||
return self._simple_insert("feedback", {
|
||||
def _store_feedback_txn(self, txn, event):
|
||||
self._simple_insert_txn(txn, "feedback", {
|
||||
"event_id": event.event_id,
|
||||
"feedback_type": event.feedback_type,
|
||||
"feedback_type": event.content["type"],
|
||||
"room_id": event.room_id,
|
||||
"target_event_id": event.target_event,
|
||||
"target_event_id": event.content["target_event_id"],
|
||||
"sender": event.user_id,
|
||||
})
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ class PduStore(SQLBaseStore):
|
||||
|
||||
return self._get_pdu_tuples(txn, res)
|
||||
|
||||
def persist_pdu(self, prev_pdus, **cols):
|
||||
def _persist_pdu_txn(self, txn, prev_pdus, cols):
|
||||
"""Inserts a (non-state) PDU into the database.
|
||||
|
||||
Args:
|
||||
@@ -122,11 +122,6 @@ class PduStore(SQLBaseStore):
|
||||
prev_pdus (list)
|
||||
**cols: The columns to insert into the PdusTable.
|
||||
"""
|
||||
return self._db_pool.runInteraction(
|
||||
self._persist_pdu, prev_pdus, cols
|
||||
)
|
||||
|
||||
def _persist_pdu(self, txn, prev_pdus, cols):
|
||||
entry = PdusTable.EntryType(
|
||||
**{k: cols.get(k, None) for k in PdusTable.fields}
|
||||
)
|
||||
@@ -262,7 +257,7 @@ class PduStore(SQLBaseStore):
|
||||
|
||||
return row[0] if row else None
|
||||
|
||||
def update_min_depth_for_context(self, context, depth):
|
||||
def _update_min_depth_for_context_txn(self, txn, context, depth):
|
||||
"""Update the minimum `depth` of the given context, which is the line
|
||||
on which we stop backfilling backwards.
|
||||
|
||||
@@ -270,11 +265,6 @@ class PduStore(SQLBaseStore):
|
||||
context (str)
|
||||
depth (int)
|
||||
"""
|
||||
return self._db_pool.runInteraction(
|
||||
self._update_min_depth_for_context, context, depth
|
||||
)
|
||||
|
||||
def _update_min_depth_for_context(self, txn, context, depth):
|
||||
min_depth = self._get_min_depth_interaction(txn, context)
|
||||
|
||||
do_insert = depth < min_depth if min_depth else True
|
||||
@@ -286,7 +276,7 @@ class PduStore(SQLBaseStore):
|
||||
(context, depth)
|
||||
)
|
||||
|
||||
def get_latest_pdus_in_context(self, context):
|
||||
def _get_latest_pdus_in_context(self, txn, context):
|
||||
"""Get's a list of the most current pdus for a given context. This is
|
||||
used when we are sending a Pdu and need to fill out the `prev_pdus`
|
||||
key
|
||||
@@ -295,11 +285,6 @@ class PduStore(SQLBaseStore):
|
||||
txn
|
||||
context
|
||||
"""
|
||||
return self._db_pool.runInteraction(
|
||||
self._get_latest_pdus_in_context, context
|
||||
)
|
||||
|
||||
def _get_latest_pdus_in_context(self, txn, context):
|
||||
query = (
|
||||
"SELECT p.pdu_id, p.origin, p.depth FROM %(pdus)s as p "
|
||||
"INNER JOIN %(forward)s as f ON p.pdu_id = f.pdu_id "
|
||||
@@ -485,7 +470,7 @@ class StatePduStore(SQLBaseStore):
|
||||
"""A collection of queries for handling state PDUs.
|
||||
"""
|
||||
|
||||
def persist_state(self, prev_pdus, **cols):
|
||||
def _persist_state_txn(self, txn, prev_pdus, cols):
|
||||
"""Inserts a state PDU into the database
|
||||
|
||||
Args:
|
||||
@@ -493,12 +478,6 @@ class StatePduStore(SQLBaseStore):
|
||||
prev_pdus (list)
|
||||
**cols: The columns to insert into the PdusTable and StatePdusTable
|
||||
"""
|
||||
|
||||
return self._db_pool.runInteraction(
|
||||
self._persist_state, prev_pdus, cols
|
||||
)
|
||||
|
||||
def _persist_state(self, txn, prev_pdus, cols):
|
||||
pdu_entry = PdusTable.EntryType(
|
||||
**{k: cols.get(k, None) for k in PdusTable.fields}
|
||||
)
|
||||
|
||||
@@ -18,12 +18,10 @@ from twisted.internet import defer
|
||||
from sqlite3 import IntegrityError
|
||||
|
||||
from synapse.api.errors import StoreError
|
||||
from synapse.api.events.room import RoomTopicEvent
|
||||
|
||||
from ._base import SQLBaseStore, Table
|
||||
|
||||
import collections
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -131,8 +129,9 @@ class RoomStore(SQLBaseStore):
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
def _store_room_topic(self, event):
|
||||
return self._simple_insert(
|
||||
def _store_room_topic_txn(self, txn, event):
|
||||
self._simple_insert_txn(
|
||||
txn,
|
||||
"topics",
|
||||
{
|
||||
"event_id": event.event_id,
|
||||
@@ -141,8 +140,9 @@ class RoomStore(SQLBaseStore):
|
||||
}
|
||||
)
|
||||
|
||||
def _store_room_name(self, event):
|
||||
return self._simple_insert(
|
||||
def _store_room_name_txn(self, txn, event):
|
||||
self._simple_insert_txn(
|
||||
txn,
|
||||
"room_names",
|
||||
{
|
||||
"event_id": event.event_id,
|
||||
|
||||
@@ -15,15 +15,10 @@
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.types import UserID
|
||||
from ._base import SQLBaseStore
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.events.room import RoomMemberEvent
|
||||
|
||||
from ._base import SQLBaseStore, Table
|
||||
|
||||
|
||||
import collections
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -31,17 +26,18 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class RoomMemberStore(SQLBaseStore):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _store_room_member(self, event):
|
||||
def _store_room_member_txn(self, txn, event):
|
||||
"""Store a room member in the database.
|
||||
"""
|
||||
domain = self.hs.parse_userid(event.target_user_id).domain
|
||||
target_user_id = event.state_key
|
||||
domain = self.hs.parse_userid(target_user_id).domain
|
||||
|
||||
yield self._simple_insert(
|
||||
self._simple_insert_txn(
|
||||
txn,
|
||||
"room_memberships",
|
||||
{
|
||||
"event_id": event.event_id,
|
||||
"user_id": event.target_user_id,
|
||||
"user_id": target_user_id,
|
||||
"sender": event.user_id,
|
||||
"room_id": event.room_id,
|
||||
"membership": event.membership,
|
||||
@@ -54,13 +50,13 @@ class RoomMemberStore(SQLBaseStore):
|
||||
"INSERT OR IGNORE INTO room_hosts (room_id, host) "
|
||||
"VALUES (?, ?)"
|
||||
)
|
||||
yield self._execute(None, sql, event.room_id, domain)
|
||||
txn.execute(sql, (event.room_id, domain))
|
||||
else:
|
||||
sql = (
|
||||
"DELETE FROM room_hosts WHERE room_id = ? AND host = ?"
|
||||
)
|
||||
|
||||
yield self._execute(None, sql, event.room_id, domain)
|
||||
txn.execute(sql, (event.room_id, domain))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_room_member(self, user_id, room_id):
|
||||
@@ -79,6 +75,24 @@ class RoomMemberStore(SQLBaseStore):
|
||||
|
||||
defer.returnValue(rows[0] if rows else None)
|
||||
|
||||
def _get_room_member(self, txn, user_id, room_id):
|
||||
sql = (
|
||||
"SELECT e.* FROM events as e"
|
||||
" INNER JOIN room_memberships as m"
|
||||
" ON e.event_id = m.event_id"
|
||||
" INNER JOIN current_state_events as c"
|
||||
" ON m.event_id = c.event_id"
|
||||
" WHERE m.user_id = ? and e.room_id = ?"
|
||||
" LIMIT 1"
|
||||
)
|
||||
txn.execute(sql, (user_id, room_id))
|
||||
rows = self.cursor_to_dict(txn)
|
||||
if rows:
|
||||
return self._parse_event_from_row(rows[0])
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_room_members(self, room_id, membership=None):
|
||||
"""Retrieve the current room member list for a room.
|
||||
|
||||
@@ -149,3 +163,24 @@ class RoomMemberStore(SQLBaseStore):
|
||||
|
||||
results = [self._parse_event_from_row(r) for r in rows]
|
||||
defer.returnValue(results)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def do_users_share_a_room(self, user_list):
|
||||
""" Checks whether a list of users share a room.
|
||||
"""
|
||||
user_list_clause = " OR ".join(["m.user_id = ?"] * len(user_list))
|
||||
sql = (
|
||||
"SELECT m.room_id FROM room_memberships as m "
|
||||
"INNER JOIN current_state_events as c "
|
||||
"ON m.event_id = c.event_id "
|
||||
"WHERE m.membership = 'join' "
|
||||
"AND (%(clause)s) "
|
||||
"GROUP BY m.room_id HAVING COUNT(m.room_id) = ?"
|
||||
) % {"clause": user_list_clause}
|
||||
|
||||
args = user_list
|
||||
args.append(len(user_list))
|
||||
|
||||
rows = yield self._execute(None, sql, *args)
|
||||
|
||||
defer.returnValue(len(rows) > 0)
|
||||
|
||||
@@ -98,5 +98,6 @@ CREATE TABLE IF NOT EXISTS rooms(
|
||||
|
||||
CREATE TABLE IF NOT EXISTS room_hosts(
|
||||
room_id TEXT NOT NULL,
|
||||
host TEXT NOT NULL
|
||||
host TEXT NOT NULL,
|
||||
CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE
|
||||
);
|
||||
|
||||
@@ -37,10 +37,8 @@ from twisted.internet import defer
|
||||
|
||||
from ._base import SQLBaseStore
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
||||
@@ -176,7 +174,7 @@ class StreamStore(SQLBaseStore):
|
||||
"SELECT * FROM events as e WHERE "
|
||||
"((room_id IN (%(current)s)) OR "
|
||||
"(event_id IN (%(invites)s))) "
|
||||
"AND e.stream_ordering > ? AND e.stream_ordering < ? "
|
||||
"AND e.stream_ordering > ? AND e.stream_ordering <= ? "
|
||||
"AND e.outlier = 0 "
|
||||
"ORDER BY stream_ordering ASC LIMIT %(limit)d "
|
||||
) % {
|
||||
@@ -207,8 +205,11 @@ class StreamStore(SQLBaseStore):
|
||||
with_feedback=False):
|
||||
# TODO (erikj): Handle compressed feedback
|
||||
|
||||
from_comp = '<' if direction =='b' else '>'
|
||||
to_comp = '>' if direction =='b' else '<'
|
||||
# Tokens really represent positions between elements, but we use
|
||||
# the convention of pointing to the event before the gap. Hence
|
||||
# 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]
|
||||
@@ -257,7 +258,7 @@ class StreamStore(SQLBaseStore):
|
||||
sql = (
|
||||
"SELECT * FROM events "
|
||||
"WHERE room_id = ? AND stream_ordering <= ? "
|
||||
"ORDER BY topological_ordering, stream_ordering DESC LIMIT ? "
|
||||
"ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? "
|
||||
)
|
||||
|
||||
rows = yield self._execute_and_decode(
|
||||
@@ -283,17 +284,20 @@ class StreamStore(SQLBaseStore):
|
||||
)
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_room_events_max_id(self):
|
||||
res = yield self._execute_and_decode(
|
||||
return self._db_pool.runInteraction(self._get_room_events_max_id_txn)
|
||||
|
||||
def _get_room_events_max_id_txn(self, txn):
|
||||
txn.execute(
|
||||
"SELECT MAX(stream_ordering) as m FROM events"
|
||||
)
|
||||
|
||||
res = self.cursor_to_dict(txn)
|
||||
|
||||
logger.debug("get_room_events_max_id: %s", res)
|
||||
|
||||
if not res or not res[0] or not res[0]["m"]:
|
||||
defer.returnValue("s1")
|
||||
return
|
||||
return "s0"
|
||||
|
||||
key = res[0]["m"] + 1
|
||||
defer.returnValue("s%d" % (key,))
|
||||
key = res[0]["m"]
|
||||
return "s%d" % (key,)
|
||||
|
||||
@@ -12,22 +12,3 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module contains REST servlets to do with public paths: /public"""
|
||||
from twisted.internet import defer
|
||||
|
||||
from base import RestServlet, client_path_pattern
|
||||
|
||||
|
||||
class PublicRoomListRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/public/rooms$")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request):
|
||||
handler = self.handlers.room_list_handler
|
||||
data = yield handler.get_public_room_list()
|
||||
defer.returnValue((200, data))
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
PublicRoomListRestServlet(hs).register(http_server)
|
||||
84
synapse/streams/config.py
Normal file
84
synapse/streams/config.py
Normal file
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.types import StreamToken
|
||||
|
||||
import logging
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaginationConfig(object):
|
||||
|
||||
"""A configuration object which stores pagination parameters."""
|
||||
|
||||
def __init__(self, from_token=None, to_token=None, direction='f',
|
||||
limit=0):
|
||||
self.from_token = from_token
|
||||
self.to_token = to_token
|
||||
self.direction = 'f' if direction == 'f' else 'b'
|
||||
self.limit = int(limit)
|
||||
|
||||
@classmethod
|
||||
def from_request(cls, request, raise_invalid_params=True):
|
||||
def get_param(name, default=None):
|
||||
lst = request.args.get(name, [])
|
||||
if len(lst) > 1:
|
||||
raise SynapseError(
|
||||
400, "%s must be specified only once" % (name,)
|
||||
)
|
||||
elif len(lst) == 1:
|
||||
return lst[0]
|
||||
else:
|
||||
return default
|
||||
|
||||
direction = get_param("dir", 'f')
|
||||
if direction not in ['f', 'b']:
|
||||
raise SynapseError(400, "'dir' parameter is invalid.")
|
||||
|
||||
from_tok = get_param("from")
|
||||
to_tok = get_param("to")
|
||||
|
||||
try:
|
||||
if from_tok == "END":
|
||||
from_tok = None # For backwards compat.
|
||||
elif from_tok:
|
||||
from_tok = StreamToken.from_string(from_tok)
|
||||
except:
|
||||
raise SynapseError(400, "'from' paramater is invalid")
|
||||
|
||||
try:
|
||||
if to_tok:
|
||||
to_tok = StreamToken.from_string(to_tok)
|
||||
except:
|
||||
raise SynapseError(400, "'to' paramater is invalid")
|
||||
|
||||
limit = get_param("limit", "0")
|
||||
if not limit.isdigit():
|
||||
raise SynapseError(400, "'limit' parameter must be an integer.")
|
||||
|
||||
try:
|
||||
return PaginationConfig(from_tok, to_tok, direction, limit)
|
||||
except:
|
||||
logger.exception("Failed to create pagination config")
|
||||
raise SynapseError(400, "Invalid request.")
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
"<PaginationConfig from_tok=%s, to_tok=%s, "
|
||||
"direction=%s, limit=%s>"
|
||||
) % (self.from_token, self.to_token, self.direction, self.limit)
|
||||
195
synapse/streams/events.py
Normal file
195
synapse/streams/events.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.types import StreamToken
|
||||
|
||||
|
||||
class NullSource(object):
|
||||
"""This event source never yields any events and its token remains at
|
||||
zero. It may be useful for unit-testing."""
|
||||
def __init__(self, hs):
|
||||
pass
|
||||
|
||||
def get_new_events_for_user(self, user, from_token, limit):
|
||||
return defer.succeed(([], from_token))
|
||||
|
||||
def get_current_token_part(self):
|
||||
return defer.succeed(0)
|
||||
|
||||
def get_pagination_rows(self, user, pagination_config, key):
|
||||
return defer.succeed(([], pagination_config.from_token))
|
||||
|
||||
|
||||
class RoomEventSource(object):
|
||||
def __init__(self, hs):
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_new_events_for_user(self, user, from_token, limit):
|
||||
# We just ignore the key for now.
|
||||
|
||||
to_key = yield self.get_current_token_part()
|
||||
|
||||
events, end_key = yield self.store.get_room_events_stream(
|
||||
user_id=user.to_string(),
|
||||
from_key=from_token.events_key,
|
||||
to_key=to_key,
|
||||
room_id=None,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
end_token = from_token.copy_and_replace("events_key", end_key)
|
||||
|
||||
defer.returnValue((events, end_token))
|
||||
|
||||
def get_current_token_part(self):
|
||||
return self.store.get_room_events_max_id()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_pagination_rows(self, user, pagination_config, key):
|
||||
from_token = pagination_config.from_token
|
||||
to_token = pagination_config.to_token
|
||||
limit = pagination_config.limit
|
||||
direction = pagination_config.direction
|
||||
|
||||
to_key = to_token.events_key if to_token else None
|
||||
|
||||
events, next_key = yield self.store.paginate_room_events(
|
||||
room_id=key,
|
||||
from_key=from_token.events_key,
|
||||
to_key=to_key,
|
||||
direction=direction,
|
||||
limit=limit,
|
||||
with_feedback=True
|
||||
)
|
||||
|
||||
next_token = from_token.copy_and_replace("events_key", next_key)
|
||||
|
||||
defer.returnValue((events, next_token))
|
||||
|
||||
|
||||
class PresenceSource(object):
|
||||
def __init__(self, hs):
|
||||
self.hs = hs
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
def get_new_events_for_user(self, user, from_token, limit):
|
||||
from_key = int(from_token.presence_key)
|
||||
|
||||
presence = self.hs.get_handlers().presence_handler
|
||||
cachemap = presence._user_cachemap
|
||||
|
||||
# TODO(paul): limit, and filter by visibility
|
||||
updates = [(k, cachemap[k]) for k in cachemap
|
||||
if from_key < cachemap[k].serial]
|
||||
|
||||
if updates:
|
||||
clock = self.clock
|
||||
|
||||
latest_serial = max([x[1].serial for x in updates])
|
||||
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
|
||||
|
||||
end_token = from_token.copy_and_replace(
|
||||
"presence_key", latest_serial
|
||||
)
|
||||
return ((data, end_token))
|
||||
else:
|
||||
end_token = from_token.copy_and_replace(
|
||||
"presence_key", presence._user_cachemap_latest_serial
|
||||
)
|
||||
return (([], end_token))
|
||||
|
||||
def get_current_token_part(self):
|
||||
presence = self.hs.get_handlers().presence_handler
|
||||
return presence._user_cachemap_latest_serial
|
||||
|
||||
def get_pagination_rows(self, user, pagination_config, key):
|
||||
# TODO (erikj): Does this make sense? Ordering?
|
||||
|
||||
from_token = pagination_config.from_token
|
||||
to_token = pagination_config.to_token
|
||||
|
||||
from_key = int(from_token.presence_key)
|
||||
|
||||
if to_token:
|
||||
to_key = int(to_token.presence_key)
|
||||
else:
|
||||
to_key = -1
|
||||
|
||||
presence = self.hs.get_handlers().presence_handler
|
||||
cachemap = presence._user_cachemap
|
||||
|
||||
# TODO(paul): limit, and filter by visibility
|
||||
updates = [(k, cachemap[k]) for k in cachemap
|
||||
if to_key < cachemap[k].serial < from_key]
|
||||
|
||||
if updates:
|
||||
clock = self.clock
|
||||
|
||||
earliest_serial = max([x[1].serial for x in updates])
|
||||
data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
|
||||
|
||||
if to_token:
|
||||
next_token = to_token
|
||||
else:
|
||||
next_token = from_token
|
||||
|
||||
next_token = next_token.copy_and_replace(
|
||||
"presence_key", earliest_serial
|
||||
)
|
||||
return ((data, next_token))
|
||||
else:
|
||||
if not to_token:
|
||||
to_token = from_token.copy_and_replace(
|
||||
"presence_key", 0
|
||||
)
|
||||
return (([], to_token))
|
||||
|
||||
|
||||
class EventSources(object):
|
||||
SOURCE_TYPES = {
|
||||
"room": RoomEventSource,
|
||||
"presence": PresenceSource,
|
||||
}
|
||||
|
||||
def __init__(self, hs):
|
||||
self.sources = {
|
||||
name: cls(hs)
|
||||
for name, cls in EventSources.SOURCE_TYPES.items()
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def create_token(events_key, presence_key):
|
||||
return StreamToken(events_key=events_key, presence_key=presence_key)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_current_token(self):
|
||||
events_key = yield self.sources["room"].get_current_token_part()
|
||||
presence_key = yield self.sources["presence"].get_current_token_part()
|
||||
token = EventSources.create_token(events_key, presence_key)
|
||||
defer.returnValue(token)
|
||||
|
||||
|
||||
class StreamSource(object):
|
||||
def get_new_events_for_user(self, user, from_token, limit):
|
||||
raise NotImplementedError("get_new_events_for_user")
|
||||
|
||||
def get_current_token_part(self):
|
||||
raise NotImplementedError("get_current_token_part")
|
||||
|
||||
def get_pagination_rows(self, user, pagination_config, key):
|
||||
raise NotImplementedError("get_rows")
|
||||
@@ -92,3 +92,36 @@ class RoomAlias(DomainSpecificString):
|
||||
class RoomID(DomainSpecificString):
|
||||
"""Structure representing a room id. """
|
||||
SIGIL = "!"
|
||||
|
||||
|
||||
class StreamToken(
|
||||
namedtuple(
|
||||
"Token",
|
||||
("events_key", "presence_key")
|
||||
)
|
||||
):
|
||||
_SEPARATOR = "_"
|
||||
|
||||
@classmethod
|
||||
def from_string(cls, string):
|
||||
try:
|
||||
events_key, presence_key = string.split(cls._SEPARATOR)
|
||||
|
||||
return cls(
|
||||
events_key=events_key,
|
||||
presence_key=presence_key,
|
||||
)
|
||||
except:
|
||||
raise SynapseError(400, "Invalid Token")
|
||||
|
||||
def to_string(self):
|
||||
return "".join([
|
||||
str(self.events_key),
|
||||
self._SEPARATOR,
|
||||
str(self.presence_key),
|
||||
])
|
||||
|
||||
def copy_and_replace(self, key, new_value):
|
||||
d = self._asdict()
|
||||
d[key] = new_value
|
||||
return StreamToken(**d)
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
|
||||
|
||||
from inspect import getcallargs
|
||||
from functools import wraps
|
||||
|
||||
import logging
|
||||
import inspect
|
||||
import traceback
|
||||
|
||||
|
||||
def log_function(f):
|
||||
@@ -26,6 +29,7 @@ def log_function(f):
|
||||
lineno = f.func_code.co_firstlineno
|
||||
pathname = f.func_code.co_filename
|
||||
|
||||
@wraps(f)
|
||||
def wrapped(*args, **kwargs):
|
||||
name = f.__module__
|
||||
logger = logging.getLogger(name)
|
||||
@@ -63,4 +67,55 @@ def log_function(f):
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
wrapped.__name__ = func_name
|
||||
return wrapped
|
||||
|
||||
|
||||
def trace_function(f):
|
||||
func_name = f.__name__
|
||||
linenum = f.func_code.co_firstlineno
|
||||
pathname = f.func_code.co_filename
|
||||
|
||||
def wrapped(*args, **kwargs):
|
||||
name = f.__module__
|
||||
logger = logging.getLogger(name)
|
||||
level = logging.DEBUG
|
||||
|
||||
s = inspect.currentframe().f_back
|
||||
|
||||
to_print = [
|
||||
"\t%s:%s %s. Args: args=%s, kwargs=%s" % (
|
||||
pathname, linenum, func_name, args, kwargs
|
||||
)
|
||||
]
|
||||
while s:
|
||||
if True or s.f_globals["__name__"].startswith("synapse"):
|
||||
filename, lineno, function, _, _ = inspect.getframeinfo(s)
|
||||
args_string = inspect.formatargvalues(*inspect.getargvalues(s))
|
||||
|
||||
to_print.append(
|
||||
"\t%s:%d %s. Args: %s" % (
|
||||
filename, lineno, function, args_string
|
||||
)
|
||||
)
|
||||
|
||||
s = s.f_back
|
||||
|
||||
msg = "\nTraceback for %s:\n" % (func_name,) + "\n".join(to_print)
|
||||
|
||||
record = logging.LogRecord(
|
||||
name=name,
|
||||
level=level,
|
||||
pathname=pathname,
|
||||
lineno=lineno,
|
||||
msg=msg,
|
||||
args=None,
|
||||
exc_info=None
|
||||
)
|
||||
|
||||
logger.handle(record)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
|
||||
wrapped.__name__ = func_name
|
||||
return wrapped
|
||||
|
||||
@@ -58,7 +58,7 @@ class FederationTestCase(unittest.TestCase):
|
||||
self.mock_persistence = Mock(spec=[
|
||||
"get_current_state_for_context",
|
||||
"get_pdu",
|
||||
"persist_pdu",
|
||||
"persist_event",
|
||||
"update_min_depth_for_context",
|
||||
"prep_send_transaction",
|
||||
"delivered_txn",
|
||||
|
||||
@@ -22,11 +22,14 @@ from synapse.api.events.room import (
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.handlers.federation import FederationHandler
|
||||
from synapse.server import HomeServer
|
||||
from synapse.federation.units import Pdu
|
||||
|
||||
from mock import NonCallableMock
|
||||
from mock import NonCallableMock, ANY
|
||||
|
||||
import logging
|
||||
|
||||
from ..utils import get_mock_call_args
|
||||
|
||||
logging.getLogger().addHandler(logging.NullHandler())
|
||||
|
||||
|
||||
@@ -60,46 +63,53 @@ class FederationTestCase(unittest.TestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_msg(self):
|
||||
event = self.hs.get_event_factory().create_event(
|
||||
etype=MessageEvent.TYPE,
|
||||
msg_id="bob",
|
||||
room_id="foo",
|
||||
pdu = Pdu(
|
||||
pdu_type=MessageEvent.TYPE,
|
||||
context="foo",
|
||||
content={"msgtype": u"fooo"},
|
||||
ts=0,
|
||||
pdu_id="a",
|
||||
origin="b",
|
||||
)
|
||||
|
||||
store_id = "ASD"
|
||||
self.datastore.persist_event.return_value = defer.succeed(store_id)
|
||||
self.datastore.get_room.return_value = defer.succeed(True)
|
||||
|
||||
yield self.handlers.federation_handler.on_receive(event, False, False)
|
||||
yield self.handlers.federation_handler.on_receive_pdu(pdu, False)
|
||||
|
||||
self.datastore.persist_event.assert_called_once_with(event, False)
|
||||
self.notifier.on_new_room_event.assert_called_once_with(
|
||||
event, store_id)
|
||||
self.datastore.persist_event.assert_called_once_with(ANY, False)
|
||||
self.notifier.on_new_room_event.assert_called_once_with(ANY)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_invite_join_target_this(self):
|
||||
room_id = "foo"
|
||||
user_id = "@bob:red"
|
||||
|
||||
event = self.hs.get_event_factory().create_event(
|
||||
etype=InviteJoinEvent.TYPE,
|
||||
pdu = Pdu(
|
||||
pdu_type=InviteJoinEvent.TYPE,
|
||||
user_id=user_id,
|
||||
target_host=self.hostname,
|
||||
room_id=room_id,
|
||||
context=room_id,
|
||||
content={},
|
||||
ts=0,
|
||||
pdu_id="a",
|
||||
origin="b",
|
||||
)
|
||||
|
||||
yield self.handlers.federation_handler.on_receive(event, False, False)
|
||||
yield self.handlers.federation_handler.on_receive_pdu(pdu, False)
|
||||
|
||||
mem_handler = self.handlers.room_member_handler
|
||||
self.assertEquals(1, mem_handler.change_membership.call_count)
|
||||
self.assertEquals(True, mem_handler.change_membership.call_args[0][1])
|
||||
call_args = get_mock_call_args(
|
||||
lambda event, do_auth: None,
|
||||
mem_handler.change_membership
|
||||
)
|
||||
self.assertEquals(True, call_args["do_auth"])
|
||||
|
||||
new_event = mem_handler.change_membership.call_args[0][0]
|
||||
new_event = call_args["event"]
|
||||
self.assertEquals(RoomMemberEvent.TYPE, new_event.type)
|
||||
self.assertEquals(room_id, new_event.room_id)
|
||||
self.assertEquals(user_id, new_event.target_user_id)
|
||||
self.assertEquals(user_id, new_event.state_key)
|
||||
self.assertEquals(Membership.JOIN, new_event.membership)
|
||||
|
||||
@@ -108,15 +118,18 @@ class FederationTestCase(unittest.TestCase):
|
||||
room_id = "foo"
|
||||
user_id = "@bob:red"
|
||||
|
||||
event = self.hs.get_event_factory().create_event(
|
||||
etype=InviteJoinEvent.TYPE,
|
||||
pdu = Pdu(
|
||||
pdu_type=InviteJoinEvent.TYPE,
|
||||
user_id=user_id,
|
||||
target_user_id="@red:not%s" % self.hostname,
|
||||
room_id=room_id,
|
||||
state_key="@red:not%s" % self.hostname,
|
||||
context=room_id,
|
||||
content={},
|
||||
ts=0,
|
||||
pdu_id="a",
|
||||
origin="b",
|
||||
)
|
||||
|
||||
yield self.handlers.federation_handler.on_receive(event, False, False)
|
||||
yield self.handlers.federation_handler.on_receive_pdu(pdu, False)
|
||||
|
||||
mem_handler = self.handlers.room_member_handler
|
||||
self.assertEquals(0, mem_handler.change_membership.call_count)
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import defer
|
||||
from twisted.internet import defer, reactor
|
||||
|
||||
from mock import Mock, call, ANY
|
||||
import logging
|
||||
@@ -92,10 +92,7 @@ class PresenceStateTestCase(unittest.TestCase):
|
||||
self.datastore.is_presence_visible = is_presence_visible
|
||||
|
||||
# Mock the RoomMemberHandler
|
||||
room_member_handler = Mock(spec=[
|
||||
"get_rooms_for_user",
|
||||
"get_room_members",
|
||||
])
|
||||
room_member_handler = Mock(spec=[])
|
||||
hs.handlers.room_member_handler = room_member_handler
|
||||
logging.getLogger().debug("Mocking room_member_handler=%r", room_member_handler)
|
||||
|
||||
@@ -122,6 +119,11 @@ class PresenceStateTestCase(unittest.TestCase):
|
||||
return defer.succeed([])
|
||||
room_member_handler.get_room_members = get_room_members
|
||||
|
||||
def do_users_share_a_room(userlist):
|
||||
shared = all(map(lambda u: u in self.room_members, userlist))
|
||||
return defer.succeed(shared)
|
||||
self.datastore.do_users_share_a_room = do_users_share_a_room
|
||||
|
||||
self.mock_start = Mock()
|
||||
self.mock_stop = Mock()
|
||||
|
||||
@@ -190,7 +192,8 @@ class PresenceStateTestCase(unittest.TestCase):
|
||||
),
|
||||
SynapseError
|
||||
)
|
||||
test_get_disallowed_state.skip = "Presence polling is disabled"
|
||||
|
||||
test_get_disallowed_state.skip = "Presence permissions are disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_set_my_state(self):
|
||||
@@ -215,7 +218,6 @@ class PresenceStateTestCase(unittest.TestCase):
|
||||
state={"state": OFFLINE})
|
||||
|
||||
self.mock_stop.assert_called_with(self.u_apple)
|
||||
test_set_my_state.skip = "Presence polling is disabled"
|
||||
|
||||
|
||||
class PresenceInvitesTestCase(unittest.TestCase):
|
||||
@@ -497,6 +499,7 @@ class PresencePushTestCase(unittest.TestCase):
|
||||
db_pool=None,
|
||||
datastore=Mock(spec=[
|
||||
"set_presence_state",
|
||||
"get_joined_hosts_for_room",
|
||||
|
||||
# Bits that Federation needs
|
||||
"prep_send_transaction",
|
||||
@@ -511,8 +514,12 @@ class PresencePushTestCase(unittest.TestCase):
|
||||
)
|
||||
hs.handlers = JustPresenceHandlers(hs)
|
||||
|
||||
def update(*args,**kwargs):
|
||||
# print "mock_update_client: Args=%s, kwargs=%s" %(args, kwargs,)
|
||||
return defer.succeed(None)
|
||||
|
||||
self.mock_update_client = Mock()
|
||||
self.mock_update_client.return_value = defer.succeed(None)
|
||||
self.mock_update_client.side_effect = update
|
||||
|
||||
self.datastore = hs.get_datastore()
|
||||
|
||||
@@ -546,6 +553,14 @@ class PresencePushTestCase(unittest.TestCase):
|
||||
return defer.succeed([])
|
||||
self.room_member_handler.get_room_members = get_room_members
|
||||
|
||||
def get_room_hosts(room_id):
|
||||
if room_id == "a-room":
|
||||
hosts = set([u.domain for u in self.room_members])
|
||||
return defer.succeed(hosts)
|
||||
else:
|
||||
return defer.succeed([])
|
||||
self.datastore.get_joined_hosts_for_room = get_room_hosts
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def fetch_room_distributions_into(room_id, localusers=None,
|
||||
remotedomains=None, ignore_user=None):
|
||||
@@ -611,18 +626,10 @@ class PresencePushTestCase(unittest.TestCase):
|
||||
{"state": ONLINE})
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_apple,
|
||||
call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]),
|
||||
room_ids=["a-room"],
|
||||
observed_user=self.u_apple,
|
||||
statuscache=ANY), # self-reflection
|
||||
call(observer_user=self.u_banana,
|
||||
observed_user=self.u_apple,
|
||||
statuscache=ANY),
|
||||
call(observer_user=self.u_clementine,
|
||||
observed_user=self.u_apple,
|
||||
statuscache=ANY),
|
||||
call(observer_user=self.u_elderberry,
|
||||
observed_user=self.u_apple,
|
||||
statuscache=ANY),
|
||||
], any_order=True)
|
||||
self.mock_update_client.reset_mock()
|
||||
|
||||
@@ -651,30 +658,30 @@ class PresencePushTestCase(unittest.TestCase):
|
||||
], presence)
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_banana,
|
||||
call(users_to_push=set([self.u_banana]),
|
||||
room_ids=[],
|
||||
observed_user=self.u_banana,
|
||||
statuscache=ANY), # self-reflection
|
||||
]) # and no others...
|
||||
test_push_local.skip = "Presence polling is disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_push_remote(self):
|
||||
put_json = self.mock_http_client.put_json
|
||||
put_json.expect_call_and_return(
|
||||
call("remote",
|
||||
path=ANY, # Can't guarantee which txn ID will be which
|
||||
data=_expect_edu("remote", "m.presence",
|
||||
content={
|
||||
"push": [
|
||||
{"user_id": "@apple:test",
|
||||
"state": "online",
|
||||
"mtime_age": 0},
|
||||
],
|
||||
}
|
||||
)
|
||||
),
|
||||
defer.succeed((200, "OK"))
|
||||
)
|
||||
# put_json.expect_call_and_return(
|
||||
# call("remote",
|
||||
# path=ANY, # Can't guarantee which txn ID will be which
|
||||
# data=_expect_edu("remote", "m.presence",
|
||||
# content={
|
||||
# "push": [
|
||||
# {"user_id": "@apple:test",
|
||||
# "state": "online",
|
||||
# "mtime_age": 0},
|
||||
# ],
|
||||
# }
|
||||
# )
|
||||
# ),
|
||||
# defer.succeed((200, "OK"))
|
||||
# )
|
||||
put_json.expect_call_and_return(
|
||||
call("farm",
|
||||
path=ANY, # Can't guarantee which txn ID will be which
|
||||
@@ -682,7 +689,7 @@ class PresencePushTestCase(unittest.TestCase):
|
||||
content={
|
||||
"push": [
|
||||
{"user_id": "@apple:test",
|
||||
"state": "online",
|
||||
"state": u"online",
|
||||
"mtime_age": 0},
|
||||
],
|
||||
}
|
||||
@@ -707,7 +714,6 @@ class PresencePushTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
yield put_json.await_calls()
|
||||
test_push_remote.skip = "Presence polling is disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_recv_remote(self):
|
||||
@@ -732,10 +738,8 @@ class PresencePushTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_apple,
|
||||
observed_user=self.u_potato,
|
||||
statuscache=ANY),
|
||||
call(observer_user=self.u_banana,
|
||||
call(users_to_push=set([self.u_apple]),
|
||||
room_ids=["a-room"],
|
||||
observed_user=self.u_potato,
|
||||
statuscache=ANY),
|
||||
], any_order=True)
|
||||
@@ -755,19 +759,17 @@ class PresencePushTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
# Apple and Elderberry see each other
|
||||
call(observer_user=self.u_apple,
|
||||
call(room_ids=["a-room"],
|
||||
observed_user=self.u_elderberry,
|
||||
users_to_push=set(),
|
||||
statuscache=ANY),
|
||||
call(observer_user=self.u_elderberry,
|
||||
call(users_to_push=set([self.u_elderberry]),
|
||||
observed_user=self.u_apple,
|
||||
room_ids=[],
|
||||
statuscache=ANY),
|
||||
# Banana and Elderberry see each other
|
||||
call(observer_user=self.u_banana,
|
||||
observed_user=self.u_elderberry,
|
||||
statuscache=ANY),
|
||||
call(observer_user=self.u_elderberry,
|
||||
call(users_to_push=set([self.u_elderberry]),
|
||||
observed_user=self.u_banana,
|
||||
room_ids=[],
|
||||
statuscache=ANY),
|
||||
], any_order=True)
|
||||
|
||||
@@ -855,6 +857,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
'apple': [ "@banana:test", "@clementine:test" ],
|
||||
'banana': [ "@apple:test" ],
|
||||
'clementine': [ "@apple:test", "@potato:remote" ],
|
||||
'fig': [ "@potato:remote" ],
|
||||
}
|
||||
|
||||
|
||||
@@ -888,7 +891,12 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
self.datastore.get_received_txn_response = get_received_txn_response
|
||||
|
||||
self.mock_update_client = Mock()
|
||||
self.mock_update_client.return_value = defer.succeed(None)
|
||||
|
||||
def update(*args,**kwargs):
|
||||
# print "mock_update_client: Args=%s, kwargs=%s" %(args, kwargs,)
|
||||
return defer.succeed(None)
|
||||
|
||||
self.mock_update_client.side_effect = update
|
||||
|
||||
self.handler = hs.get_handlers().presence_handler
|
||||
self.handler.push_update_to_clients = self.mock_update_client
|
||||
@@ -904,9 +912,10 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
# Mocked database state
|
||||
# Local users always start offline
|
||||
self.current_user_state = {
|
||||
"apple": OFFLINE,
|
||||
"banana": OFFLINE,
|
||||
"clementine": OFFLINE,
|
||||
"apple": OFFLINE,
|
||||
"banana": OFFLINE,
|
||||
"clementine": OFFLINE,
|
||||
"fig": OFFLINE,
|
||||
}
|
||||
|
||||
def get_presence_state(user_localpart):
|
||||
@@ -936,6 +945,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
self.u_apple = hs.parse_userid("@apple:test")
|
||||
self.u_banana = hs.parse_userid("@banana:test")
|
||||
self.u_clementine = hs.parse_userid("@clementine:test")
|
||||
self.u_fig = hs.parse_userid("@fig:test")
|
||||
|
||||
# Remote users
|
||||
self.u_potato = hs.parse_userid("@potato:remote")
|
||||
@@ -950,10 +960,10 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
|
||||
# apple should see both banana and clementine currently offline
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_apple,
|
||||
call(users_to_push=[self.u_apple],
|
||||
observed_user=self.u_banana,
|
||||
statuscache=ANY),
|
||||
call(observer_user=self.u_apple,
|
||||
call(users_to_push=[self.u_apple],
|
||||
observed_user=self.u_clementine,
|
||||
statuscache=ANY),
|
||||
], any_order=True)
|
||||
@@ -973,10 +983,11 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
|
||||
# apple and banana should now both see each other online
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_apple,
|
||||
call(users_to_push=set([self.u_apple]),
|
||||
observed_user=self.u_banana,
|
||||
room_ids=[],
|
||||
statuscache=ANY),
|
||||
call(observer_user=self.u_banana,
|
||||
call(users_to_push=[self.u_banana],
|
||||
observed_user=self.u_apple,
|
||||
statuscache=ANY),
|
||||
], any_order=True)
|
||||
@@ -993,14 +1004,14 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
|
||||
# banana should now be told apple is offline
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_banana,
|
||||
call(users_to_push=set([self.u_banana, self.u_apple]),
|
||||
observed_user=self.u_apple,
|
||||
room_ids=[],
|
||||
statuscache=ANY),
|
||||
], any_order=True)
|
||||
|
||||
self.assertFalse("banana" in self.handler._local_pushmap)
|
||||
self.assertFalse("clementine" in self.handler._local_pushmap)
|
||||
test_push_local.skip = "Presence polling is disabled"
|
||||
|
||||
|
||||
@defer.inlineCallbacks
|
||||
@@ -1008,7 +1019,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
put_json = self.mock_http_client.put_json
|
||||
put_json.expect_call_and_return(
|
||||
call("remote",
|
||||
path="/matrix/federation/v1/send/1000000/",
|
||||
path=ANY,
|
||||
data=_expect_edu("remote", "m.presence",
|
||||
content={
|
||||
"poll": [ "@potato:remote" ],
|
||||
@@ -1018,6 +1029,18 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
defer.succeed((200, "OK"))
|
||||
)
|
||||
|
||||
put_json.expect_call_and_return(
|
||||
call("remote",
|
||||
path=ANY,
|
||||
data=_expect_edu("remote", "m.presence",
|
||||
content={
|
||||
"push": [ {"user_id": "@clementine:test" }],
|
||||
},
|
||||
),
|
||||
),
|
||||
defer.succeed((200, "OK"))
|
||||
)
|
||||
|
||||
# clementine goes online
|
||||
yield self.handler.set_state(
|
||||
target_user=self.u_clementine, auth_user=self.u_clementine,
|
||||
@@ -1026,13 +1049,48 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
yield put_json.await_calls()
|
||||
|
||||
# Gut-wrenching tests
|
||||
self.assertTrue(self.u_potato in self.handler._remote_recvmap)
|
||||
self.assertTrue(self.u_potato in self.handler._remote_recvmap,
|
||||
msg="expected potato to be in _remote_recvmap"
|
||||
)
|
||||
self.assertTrue(self.u_clementine in
|
||||
self.handler._remote_recvmap[self.u_potato])
|
||||
|
||||
|
||||
put_json.expect_call_and_return(
|
||||
call("remote",
|
||||
path="/matrix/federation/v1/send/1000001/",
|
||||
path=ANY,
|
||||
data=_expect_edu("remote", "m.presence",
|
||||
content={
|
||||
"push": [ {"user_id": "@fig:test" }],
|
||||
},
|
||||
),
|
||||
),
|
||||
defer.succeed((200, "OK"))
|
||||
)
|
||||
|
||||
# fig goes online; shouldn't send a second poll
|
||||
yield self.handler.set_state(
|
||||
target_user=self.u_fig, auth_user=self.u_fig,
|
||||
state={"state": ONLINE}
|
||||
)
|
||||
|
||||
# reactor.iterate(delay=0)
|
||||
|
||||
yield put_json.await_calls()
|
||||
|
||||
# fig goes offline
|
||||
yield self.handler.set_state(
|
||||
target_user=self.u_fig, auth_user=self.u_fig,
|
||||
state={"state": OFFLINE}
|
||||
)
|
||||
|
||||
reactor.iterate(delay=0)
|
||||
|
||||
put_json.assert_had_no_calls()
|
||||
|
||||
put_json.expect_call_and_return(
|
||||
call("remote",
|
||||
path=ANY,
|
||||
data=_expect_edu("remote", "m.presence",
|
||||
content={
|
||||
"unpoll": [ "@potato:remote" ],
|
||||
@@ -1047,10 +1105,11 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||
target_user=self.u_clementine, auth_user=self.u_clementine,
|
||||
state={"state": OFFLINE})
|
||||
|
||||
put_json.await_calls()
|
||||
yield put_json.await_calls()
|
||||
|
||||
self.assertFalse(self.u_potato in self.handler._remote_recvmap)
|
||||
test_remote_poll_send.skip = "Presence polling is disabled"
|
||||
self.assertFalse(self.u_potato in self.handler._remote_recvmap,
|
||||
msg="expected potato not to be in _remote_recvmap"
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_remote_poll_receive(self):
|
||||
|
||||
@@ -81,7 +81,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||
|
||||
self.replication = hs.get_replication_layer()
|
||||
self.replication.send_edu = Mock()
|
||||
self.replication.send_edu.return_value = defer.succeed((200, "OK"))
|
||||
|
||||
def send_edu(*args, **kwargs):
|
||||
# print "send_edu: %s, %s" % (args, kwargs)
|
||||
return defer.succeed((200, "OK"))
|
||||
self.replication.send_edu.side_effect = send_edu
|
||||
|
||||
def get_profile_displayname(user_localpart):
|
||||
return defer.succeed("Frank")
|
||||
@@ -95,17 +99,25 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||
return defer.succeed("http://foo")
|
||||
self.datastore.get_profile_avatar_url = get_profile_avatar_url
|
||||
|
||||
self.presence_list = [
|
||||
{"observed_user_id": "@banana:test"},
|
||||
{"observed_user_id": "@clementine:test"},
|
||||
]
|
||||
def get_presence_list(user_localpart, accepted=None):
|
||||
return defer.succeed([
|
||||
{"observed_user_id": "@banana:test"},
|
||||
{"observed_user_id": "@clementine:test"},
|
||||
])
|
||||
return defer.succeed(self.presence_list)
|
||||
self.datastore.get_presence_list = get_presence_list
|
||||
|
||||
def do_users_share_a_room(userlist):
|
||||
return defer.succeed(False)
|
||||
self.datastore.do_users_share_a_room = do_users_share_a_room
|
||||
|
||||
self.handlers = hs.get_handlers()
|
||||
|
||||
self.mock_update_client = Mock()
|
||||
self.mock_update_client.return_value = defer.succeed(None)
|
||||
def update(*args, **kwargs):
|
||||
# print "mock_update_client: %s, %s" %(args, kwargs)
|
||||
return defer.succeed(None)
|
||||
self.mock_update_client.side_effect = update
|
||||
|
||||
self.handlers.presence_handler.push_update_to_clients = (
|
||||
self.mock_update_client)
|
||||
@@ -126,6 +138,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_set_my_state(self):
|
||||
self.presence_list = [
|
||||
{"observed_user_id": "@banana:test"},
|
||||
{"observed_user_id": "@clementine:test"},
|
||||
]
|
||||
|
||||
mocked_set = self.datastore.set_presence_state
|
||||
mocked_set.return_value = defer.succeed({"state": OFFLINE})
|
||||
|
||||
@@ -135,10 +152,14 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||
|
||||
mocked_set.assert_called_with("apple",
|
||||
{"state": UNAVAILABLE, "status_msg": "Away"})
|
||||
test_set_my_state.skip = "Presence polling is disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_push_local(self):
|
||||
self.presence_list = [
|
||||
{"observed_user_id": "@banana:test"},
|
||||
{"observed_user_id": "@clementine:test"},
|
||||
]
|
||||
|
||||
self.datastore.set_presence_state.return_value = defer.succeed(
|
||||
{"state": ONLINE})
|
||||
|
||||
@@ -170,12 +191,10 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||
presence)
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_apple,
|
||||
call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]),
|
||||
room_ids=[],
|
||||
observed_user=self.u_apple,
|
||||
statuscache=ANY), # self-reflection
|
||||
call(observer_user=self.u_banana,
|
||||
observed_user=self.u_apple,
|
||||
statuscache=ANY),
|
||||
], any_order=True)
|
||||
|
||||
statuscache = self.mock_update_client.call_args[1]["statuscache"]
|
||||
@@ -195,12 +214,10 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||
self.u_apple, "I am an Apple")
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_apple,
|
||||
call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]),
|
||||
room_ids=[],
|
||||
observed_user=self.u_apple,
|
||||
statuscache=ANY), # self-reflection
|
||||
call(observer_user=self.u_banana,
|
||||
observed_user=self.u_apple,
|
||||
statuscache=ANY),
|
||||
], any_order=True)
|
||||
|
||||
statuscache = self.mock_update_client.call_args[1]["statuscache"]
|
||||
@@ -210,11 +227,14 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||
"displayname": "I am an Apple",
|
||||
"avatar_url": "http://foo",
|
||||
}, statuscache.state)
|
||||
test_push_local.skip = "Presence polling is disabled"
|
||||
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_push_remote(self):
|
||||
self.presence_list = [
|
||||
{"observed_user_id": "@potato:remote"},
|
||||
]
|
||||
|
||||
self.datastore.set_presence_state.return_value = defer.succeed(
|
||||
{"state": ONLINE})
|
||||
|
||||
@@ -242,10 +262,14 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||
],
|
||||
},
|
||||
)
|
||||
test_push_remote.skip = "Presence polling is disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_recv_remote(self):
|
||||
self.presence_list = [
|
||||
{"observed_user_id": "@banana:test"},
|
||||
{"observed_user_id": "@clementine:test"},
|
||||
]
|
||||
|
||||
# TODO(paul): Gut-wrenching
|
||||
potato_set = self.handlers.presence_handler._remote_recvmap.setdefault(
|
||||
self.u_potato, set())
|
||||
@@ -263,7 +287,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
self.mock_update_client.assert_called_with(
|
||||
observer_user=self.u_apple,
|
||||
users_to_push=set([self.u_apple]),
|
||||
room_ids=[],
|
||||
observed_user=self.u_potato,
|
||||
statuscache=ANY)
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||
"get_room_member",
|
||||
"get_room",
|
||||
"store_room",
|
||||
"snapshot_room",
|
||||
]),
|
||||
resource_for_federation=NonCallableMock(),
|
||||
http_client=NonCallableMock(spec_set=[]),
|
||||
@@ -52,29 +53,36 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||
handlers=NonCallableMock(spec_set=[
|
||||
"room_member_handler",
|
||||
"profile_handler",
|
||||
"federation_handler",
|
||||
]),
|
||||
auth=NonCallableMock(spec_set=["check"]),
|
||||
federation=NonCallableMock(spec_set=[
|
||||
"handle_new_event",
|
||||
"get_state_for_room",
|
||||
]),
|
||||
state_handler=NonCallableMock(spec_set=["handle_new_event"]),
|
||||
)
|
||||
|
||||
self.federation = NonCallableMock(spec_set=[
|
||||
"handle_new_event",
|
||||
"get_state_for_room",
|
||||
])
|
||||
|
||||
self.datastore = hs.get_datastore()
|
||||
self.handlers = hs.get_handlers()
|
||||
self.notifier = hs.get_notifier()
|
||||
self.federation = hs.get_federation()
|
||||
self.state_handler = hs.get_state_handler()
|
||||
self.distributor = hs.get_distributor()
|
||||
self.hs = hs
|
||||
|
||||
self.handlers.federation_handler = self.federation
|
||||
|
||||
self.distributor.declare("collect_presencelike_data")
|
||||
|
||||
self.handlers.room_member_handler = RoomMemberHandler(self.hs)
|
||||
self.handlers.profile_handler = ProfileHandler(self.hs)
|
||||
self.room_member_handler = self.handlers.room_member_handler
|
||||
|
||||
self.snapshot = Mock()
|
||||
self.datastore.snapshot_room.return_value = self.snapshot
|
||||
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_invite(self):
|
||||
room_id = "!foo:red"
|
||||
@@ -85,7 +93,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||
event = self.hs.get_event_factory().create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
user_id=user_id,
|
||||
target_user_id=target_user_id,
|
||||
state_key=target_user_id,
|
||||
room_id=room_id,
|
||||
membership=Membership.INVITE,
|
||||
content=content,
|
||||
@@ -104,8 +112,12 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||
# Actual invocation
|
||||
yield self.room_member_handler.change_membership(event)
|
||||
|
||||
self.state_handler.handle_new_event.assert_called_once_with(event)
|
||||
self.federation.handle_new_event.assert_called_once_with(event)
|
||||
self.state_handler.handle_new_event.assert_called_once_with(
|
||||
event, self.snapshot,
|
||||
)
|
||||
self.federation.handle_new_event.assert_called_once_with(
|
||||
event, self.snapshot,
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
set(["blue", "red", "green"]),
|
||||
@@ -116,8 +128,8 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||
event
|
||||
)
|
||||
self.notifier.on_new_room_event.assert_called_once_with(
|
||||
event, store_id)
|
||||
|
||||
event, extra_users=[self.hs.parse_userid(target_user_id)]
|
||||
)
|
||||
self.assertFalse(self.datastore.get_room.called)
|
||||
self.assertFalse(self.datastore.store_room.called)
|
||||
self.assertFalse(self.federation.get_state_for_room.called)
|
||||
@@ -133,7 +145,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||
event = self.hs.get_event_factory().create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
user_id=user_id,
|
||||
target_user_id=target_user_id,
|
||||
state_key=target_user_id,
|
||||
room_id=room_id,
|
||||
membership=Membership.JOIN,
|
||||
content=content,
|
||||
@@ -148,6 +160,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||
|
||||
self.datastore.get_joined_hosts_for_room.side_effect = get_joined
|
||||
|
||||
|
||||
store_id = "store_id_fooo"
|
||||
self.datastore.persist_event.return_value = defer.succeed(store_id)
|
||||
self.datastore.get_room.return_value = defer.succeed(1) # Not None.
|
||||
@@ -163,8 +176,12 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||
# Actual invocation
|
||||
yield self.room_member_handler.change_membership(event)
|
||||
|
||||
self.state_handler.handle_new_event.assert_called_once_with(event)
|
||||
self.federation.handle_new_event.assert_called_once_with(event)
|
||||
self.state_handler.handle_new_event.assert_called_once_with(
|
||||
event, self.snapshot
|
||||
)
|
||||
self.federation.handle_new_event.assert_called_once_with(
|
||||
event, self.snapshot
|
||||
)
|
||||
|
||||
self.assertEquals(
|
||||
set(["red", "green"]),
|
||||
@@ -175,7 +192,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||
event
|
||||
)
|
||||
self.notifier.on_new_room_event.assert_called_once_with(
|
||||
event, store_id)
|
||||
event, extra_users=[user])
|
||||
|
||||
join_signal_observer.assert_called_with(
|
||||
user=user, room_id=room_id)
|
||||
@@ -312,27 +329,31 @@ class RoomCreationTest(unittest.TestCase):
|
||||
db_pool=None,
|
||||
datastore=NonCallableMock(spec_set=[
|
||||
"store_room",
|
||||
"snapshot_room",
|
||||
]),
|
||||
http_client=NonCallableMock(spec_set=[]),
|
||||
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
||||
handlers=NonCallableMock(spec_set=[
|
||||
"room_creation_handler",
|
||||
"room_member_handler",
|
||||
"federation_handler",
|
||||
]),
|
||||
auth=NonCallableMock(spec_set=["check"]),
|
||||
federation=NonCallableMock(spec_set=[
|
||||
"handle_new_event",
|
||||
]),
|
||||
state_handler=NonCallableMock(spec_set=["handle_new_event"]),
|
||||
)
|
||||
|
||||
self.federation = NonCallableMock(spec_set=[
|
||||
"handle_new_event",
|
||||
])
|
||||
|
||||
self.datastore = hs.get_datastore()
|
||||
self.handlers = hs.get_handlers()
|
||||
self.notifier = hs.get_notifier()
|
||||
self.federation = hs.get_federation()
|
||||
self.state_handler = hs.get_state_handler()
|
||||
self.hs = hs
|
||||
|
||||
self.handlers.federation_handler = self.federation
|
||||
|
||||
self.handlers.room_creation_handler = RoomCreationHandler(self.hs)
|
||||
self.room_creation_handler = self.handlers.room_creation_handler
|
||||
|
||||
@@ -359,7 +380,7 @@ class RoomCreationTest(unittest.TestCase):
|
||||
self.assertEquals(RoomMemberEvent.TYPE, join_event.type)
|
||||
self.assertEquals(room_id, join_event.room_id)
|
||||
self.assertEquals(user_id, join_event.user_id)
|
||||
self.assertEquals(user_id, join_event.target_user_id)
|
||||
self.assertEquals(user_id, join_event.state_key)
|
||||
|
||||
self.assertTrue(self.state_handler.handle_new_event.called)
|
||||
|
||||
|
||||
250
tests/handlers/test_typing.py
Normal file
250
tests/handlers/test_typing.py
Normal file
@@ -0,0 +1,250 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from twisted.trial import unittest
|
||||
from twisted.internet import defer
|
||||
|
||||
from mock import Mock, call, ANY
|
||||
import json
|
||||
import logging
|
||||
|
||||
from ..utils import MockHttpResource, MockClock, DeferredMockCallable
|
||||
|
||||
from synapse.server import HomeServer
|
||||
from synapse.handlers.typing import TypingNotificationHandler
|
||||
|
||||
|
||||
logging.getLogger().addHandler(logging.NullHandler())
|
||||
|
||||
|
||||
def _expect_edu(destination, edu_type, content, origin="test"):
|
||||
return {
|
||||
"origin": origin,
|
||||
"ts": 1000000,
|
||||
"pdus": [],
|
||||
"edus": [
|
||||
{
|
||||
"origin": origin,
|
||||
"destination": destination,
|
||||
"edu_type": edu_type,
|
||||
"content": content,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _make_edu_json(origin, edu_type, content):
|
||||
return json.dumps(_expect_edu("test", edu_type, content, origin=origin))
|
||||
|
||||
|
||||
class JustTypingNotificationHandlers(object):
|
||||
def __init__(self, hs):
|
||||
self.typing_notification_handler = TypingNotificationHandler(hs)
|
||||
|
||||
|
||||
class TypingNotificationsTestCase(unittest.TestCase):
|
||||
"""Tests typing notifications to rooms."""
|
||||
def setUp(self):
|
||||
self.clock = MockClock()
|
||||
|
||||
self.mock_http_client = Mock(spec=[])
|
||||
self.mock_http_client.put_json = DeferredMockCallable()
|
||||
|
||||
self.mock_federation_resource = MockHttpResource()
|
||||
|
||||
hs = HomeServer("test",
|
||||
clock=self.clock,
|
||||
db_pool=None,
|
||||
datastore=Mock(spec=[
|
||||
# Bits that Federation needs
|
||||
"prep_send_transaction",
|
||||
"delivered_txn",
|
||||
"get_received_txn_response",
|
||||
"set_received_txn_response",
|
||||
]),
|
||||
handlers=None,
|
||||
resource_for_client=Mock(),
|
||||
resource_for_federation=self.mock_federation_resource,
|
||||
http_client=self.mock_http_client,
|
||||
)
|
||||
hs.handlers = JustTypingNotificationHandlers(hs)
|
||||
|
||||
self.mock_update_client = Mock()
|
||||
self.mock_update_client.return_value = defer.succeed(None)
|
||||
|
||||
self.handler = hs.get_handlers().typing_notification_handler
|
||||
self.handler.push_update_to_clients = self.mock_update_client
|
||||
|
||||
self.datastore = hs.get_datastore()
|
||||
|
||||
def get_received_txn_response(*args):
|
||||
return defer.succeed(None)
|
||||
self.datastore.get_received_txn_response = get_received_txn_response
|
||||
|
||||
self.room_id = "a-room"
|
||||
|
||||
# Mock the RoomMemberHandler
|
||||
hs.handlers.room_member_handler = Mock(spec=[])
|
||||
self.room_member_handler = hs.handlers.room_member_handler
|
||||
|
||||
self.room_members = []
|
||||
|
||||
def get_rooms_for_user(user):
|
||||
if user in self.room_members:
|
||||
return defer.succeed([self.room_id])
|
||||
else:
|
||||
return defer.succeed([])
|
||||
self.room_member_handler.get_rooms_for_user = get_rooms_for_user
|
||||
|
||||
def get_room_members(room_id):
|
||||
if room_id == self.room_id:
|
||||
return defer.succeed(self.room_members)
|
||||
else:
|
||||
return defer.succeed([])
|
||||
self.room_member_handler.get_room_members = get_room_members
|
||||
|
||||
@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 member.is_mine:
|
||||
if localusers is not None:
|
||||
localusers.add(member)
|
||||
else:
|
||||
if remotedomains is not None:
|
||||
remotedomains.add(member.domain)
|
||||
self.room_member_handler.fetch_room_distributions_into = (
|
||||
fetch_room_distributions_into)
|
||||
|
||||
# Some local users to test with
|
||||
self.u_apple = hs.parse_userid("@apple:test")
|
||||
self.u_banana = hs.parse_userid("@banana:test")
|
||||
|
||||
# Remote user
|
||||
self.u_onion = hs.parse_userid("@onion:farm")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_started_typing_local(self):
|
||||
self.room_members = [self.u_apple, self.u_banana]
|
||||
|
||||
yield self.handler.started_typing(
|
||||
target_user=self.u_apple,
|
||||
auth_user=self.u_apple,
|
||||
room_id=self.room_id,
|
||||
timeout=20000,
|
||||
)
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_banana,
|
||||
observed_user=self.u_apple,
|
||||
room_id=self.room_id,
|
||||
typing=True),
|
||||
])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_started_typing_remote_send(self):
|
||||
self.room_members = [self.u_apple, self.u_onion]
|
||||
|
||||
put_json = self.mock_http_client.put_json
|
||||
put_json.expect_call_and_return(
|
||||
call("farm",
|
||||
path="/matrix/federation/v1/send/1000000/",
|
||||
data=_expect_edu("farm", "m.typing",
|
||||
content={
|
||||
"room_id": self.room_id,
|
||||
"user_id": self.u_apple.to_string(),
|
||||
"typing": True,
|
||||
}
|
||||
)
|
||||
),
|
||||
defer.succeed((200, "OK"))
|
||||
)
|
||||
|
||||
yield self.handler.started_typing(
|
||||
target_user=self.u_apple,
|
||||
auth_user=self.u_apple,
|
||||
room_id=self.room_id,
|
||||
timeout=20000,
|
||||
)
|
||||
|
||||
yield put_json.await_calls()
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_started_typing_remote_recv(self):
|
||||
self.room_members = [self.u_apple, self.u_onion]
|
||||
|
||||
yield self.mock_federation_resource.trigger("PUT",
|
||||
"/matrix/federation/v1/send/1000000/",
|
||||
_make_edu_json("farm", "m.typing",
|
||||
content={
|
||||
"room_id": self.room_id,
|
||||
"user_id": self.u_onion.to_string(),
|
||||
"typing": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_apple,
|
||||
observed_user=self.u_onion,
|
||||
room_id=self.room_id,
|
||||
typing=True),
|
||||
])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_stopped_typing(self):
|
||||
self.room_members = [self.u_apple, self.u_banana, self.u_onion]
|
||||
|
||||
put_json = self.mock_http_client.put_json
|
||||
put_json.expect_call_and_return(
|
||||
call("farm",
|
||||
path="/matrix/federation/v1/send/1000000/",
|
||||
data=_expect_edu("farm", "m.typing",
|
||||
content={
|
||||
"room_id": self.room_id,
|
||||
"user_id": self.u_apple.to_string(),
|
||||
"typing": False,
|
||||
}
|
||||
)
|
||||
),
|
||||
defer.succeed((200, "OK"))
|
||||
)
|
||||
|
||||
# Gut-wrenching
|
||||
from synapse.handlers.typing import RoomMember
|
||||
self.handler._member_typing_until[
|
||||
RoomMember(self.room_id, self.u_apple)
|
||||
] = 1002000
|
||||
|
||||
yield self.handler.stopped_typing(
|
||||
target_user=self.u_apple,
|
||||
auth_user=self.u_apple,
|
||||
room_id=self.room_id,
|
||||
)
|
||||
|
||||
self.mock_update_client.assert_has_calls([
|
||||
call(observer_user=self.u_banana,
|
||||
observed_user=self.u_apple,
|
||||
room_id=self.room_id,
|
||||
typing=False),
|
||||
])
|
||||
|
||||
yield put_json.await_calls()
|
||||
@@ -128,9 +128,9 @@ class EventStreamPermissionsTestCase(RestTestCase):
|
||||
"test",
|
||||
db_pool=None,
|
||||
http_client=None,
|
||||
federation=Mock(),
|
||||
replication_layer=Mock(),
|
||||
state_handler=state_handler,
|
||||
datastore=MemoryDataStore(),
|
||||
persistence_service=persistence_service,
|
||||
clock=Mock(spec=[
|
||||
"call_later",
|
||||
@@ -139,9 +139,10 @@ class EventStreamPermissionsTestCase(RestTestCase):
|
||||
]),
|
||||
)
|
||||
|
||||
hs.get_handlers().federation_handler = Mock()
|
||||
|
||||
hs.get_clock().time_msec.return_value = 1000000
|
||||
|
||||
hs.datastore = MemoryDataStore()
|
||||
synapse.rest.register.register_servlets(hs, self.mock_resource)
|
||||
synapse.rest.events.register_servlets(hs, self.mock_resource)
|
||||
synapse.rest.room.register_servlets(hs, self.mock_resource)
|
||||
@@ -178,10 +179,9 @@ class EventStreamPermissionsTestCase(RestTestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_stream_room_permissions(self):
|
||||
room_id = "!rid1:test"
|
||||
yield self.create_room_as(room_id, self.other_user,
|
||||
tok=self.other_token)
|
||||
yield self.send(room_id, self.other_user, tok=self.other_token)
|
||||
room_id = yield self.create_room_as(self.other_user,
|
||||
tok=self.other_token)
|
||||
yield self.send(room_id, tok=self.other_token)
|
||||
|
||||
# invited to room (expect no content for room)
|
||||
yield self.invite(room_id, src=self.other_user, targ=self.user_id,
|
||||
|
||||
@@ -114,7 +114,6 @@ class PresenceStateTestCase(unittest.TestCase):
|
||||
self.assertEquals(200, code)
|
||||
mocked_set.assert_called_with("apple",
|
||||
{"state": UNAVAILABLE, "status_msg": "Away"})
|
||||
test_set_my_status.skip = "Presence polling is disabled"
|
||||
|
||||
|
||||
class PresenceListTestCase(unittest.TestCase):
|
||||
@@ -171,7 +170,7 @@ class PresenceListTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("GET",
|
||||
"/presence_list/%s" % (myid), None)
|
||||
"/presence/list/%s" % (myid), None)
|
||||
|
||||
self.assertEquals(200, code)
|
||||
self.assertEquals(
|
||||
@@ -192,7 +191,7 @@ class PresenceListTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("POST",
|
||||
"/presence_list/%s" % (myid),
|
||||
"/presence/list/%s" % (myid),
|
||||
"""{"invite": ["@banana:test"]}"""
|
||||
)
|
||||
|
||||
@@ -212,7 +211,7 @@ class PresenceListTestCase(unittest.TestCase):
|
||||
)
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("POST",
|
||||
"/presence_list/%s" % (myid),
|
||||
"/presence/list/%s" % (myid),
|
||||
"""{"drop": ["@banana:test"]}"""
|
||||
)
|
||||
|
||||
@@ -229,11 +228,19 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
||||
|
||||
# HIDEOUS HACKERY
|
||||
# TODO(paul): This should be injected in via the HomeServer DI system
|
||||
from synapse.handlers.events import EventStreamHandler
|
||||
from synapse.handlers.presence import PresenceStreamData
|
||||
EventStreamHandler.stream_data_classes = [
|
||||
PresenceStreamData
|
||||
]
|
||||
from synapse.streams.events import (
|
||||
PresenceSource, NullSource, EventSources
|
||||
)
|
||||
|
||||
old_SOURCE_TYPES = EventSources.SOURCE_TYPES
|
||||
def tearDown():
|
||||
EventSources.SOURCE_TYPES = old_SOURCE_TYPES
|
||||
self.tearDown = tearDown
|
||||
|
||||
EventSources.SOURCE_TYPES = {
|
||||
k: NullSource for k in old_SOURCE_TYPES.keys()
|
||||
}
|
||||
EventSources.SOURCE_TYPES["presence"] = PresenceSource
|
||||
|
||||
hs = HomeServer("test",
|
||||
db_pool=None,
|
||||
@@ -288,7 +295,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
||||
# all be ours
|
||||
|
||||
# I'll already get my own presence state change
|
||||
self.assertEquals({"start": "1", "end": "1", "chunk": []}, response)
|
||||
self.assertEquals({"start": "0_1", "end": "0_1", "chunk": []}, response)
|
||||
|
||||
self.mock_datastore.set_presence_state.return_value = defer.succeed(
|
||||
{"state": ONLINE})
|
||||
@@ -299,10 +306,10 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
||||
state={"state": ONLINE})
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("GET",
|
||||
"/events?from=1&timeout=0", None)
|
||||
"/events?from=0_1&timeout=0", None)
|
||||
|
||||
self.assertEquals(200, code)
|
||||
self.assertEquals({"start": "1", "end": "2", "chunk": [
|
||||
self.assertEquals({"start": "0_1", "end": "0_2", "chunk": [
|
||||
{"type": "m.presence",
|
||||
"content": {
|
||||
"user_id": "@banana:test",
|
||||
@@ -310,4 +317,3 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
||||
"mtime_age": 0,
|
||||
}},
|
||||
]}, response)
|
||||
test_shortpoll.skip = "Presence polling is disabled"
|
||||
|
||||
@@ -54,12 +54,12 @@ class RoomPermissionsTestCase(RestTestCase):
|
||||
"test",
|
||||
db_pool=None,
|
||||
http_client=None,
|
||||
federation=Mock(),
|
||||
datastore=MemoryDataStore(),
|
||||
replication_layer=Mock(),
|
||||
state_handler=state_handler,
|
||||
persistence_service=persistence_service,
|
||||
)
|
||||
hs.get_handlers().federation_handler = Mock()
|
||||
|
||||
def _get_user_by_token(token=None):
|
||||
return hs.parse_userid(self.auth_user_id)
|
||||
@@ -74,17 +74,15 @@ class RoomPermissionsTestCase(RestTestCase):
|
||||
# create some rooms under the name rmcreator_id
|
||||
self.uncreated_rmid = "!aa:test"
|
||||
|
||||
self.created_rmid = "!abc:test"
|
||||
yield self.create_room_as(self.created_rmid, self.rmcreator_id,
|
||||
is_public=False)
|
||||
self.created_rmid = yield self.create_room_as(self.rmcreator_id,
|
||||
is_public=False)
|
||||
|
||||
self.created_public_rmid = "!def1234ghi:test"
|
||||
yield self.create_room_as(self.created_public_rmid, self.rmcreator_id,
|
||||
is_public=True)
|
||||
self.created_public_rmid = yield self.create_room_as(self.rmcreator_id,
|
||||
is_public=True)
|
||||
|
||||
# send a message in one of the rooms
|
||||
self.created_rmid_msg_path = ("/rooms/%s/messages/%s/midaaa1" %
|
||||
(self.created_rmid, self.rmcreator_id))
|
||||
self.created_rmid_msg_path = ("/rooms/%s/send/m.room.message/a1" %
|
||||
(self.created_rmid))
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT",
|
||||
self.created_rmid_msg_path,
|
||||
@@ -94,7 +92,7 @@ class RoomPermissionsTestCase(RestTestCase):
|
||||
# set topic for public room
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT",
|
||||
"/rooms/%s/topic" % self.created_public_rmid,
|
||||
"/rooms/%s/state/m.room.topic" % self.created_public_rmid,
|
||||
'{"topic":"Public Room Topic"}')
|
||||
self.assertEquals(200, code, msg=str(response))
|
||||
|
||||
@@ -138,14 +136,14 @@ class RoomPermissionsTestCase(RestTestCase):
|
||||
@defer.inlineCallbacks
|
||||
def test_send_message(self):
|
||||
msg_content = '{"msgtype":"m.text","body":"hello"}'
|
||||
send_msg_path = ("/rooms/%s/messages/%s/mid1" %
|
||||
(self.created_rmid, self.user_id))
|
||||
send_msg_path = ("/rooms/%s/send/m.room.message/mid1" %
|
||||
(self.created_rmid))
|
||||
|
||||
# send message in uncreated room, expect 403
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT",
|
||||
"/rooms/%s/messages/%s/mid1" %
|
||||
(self.uncreated_rmid, self.user_id), msg_content)
|
||||
"/rooms/%s/send/m.room.message/mid2" %
|
||||
(self.uncreated_rmid), msg_content)
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
|
||||
# send message in created room not joined (no state), expect 403
|
||||
@@ -175,15 +173,15 @@ class RoomPermissionsTestCase(RestTestCase):
|
||||
@defer.inlineCallbacks
|
||||
def test_topic_perms(self):
|
||||
topic_content = '{"topic":"My Topic Name"}'
|
||||
topic_path = "/rooms/%s/topic" % self.created_rmid
|
||||
topic_path = "/rooms/%s/state/m.room.topic" % self.created_rmid
|
||||
|
||||
# set/get topic in uncreated room, expect 403
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%s/topic" % self.uncreated_rmid,
|
||||
"PUT", "/rooms/%s/state/m.room.topic" % self.uncreated_rmid,
|
||||
topic_content)
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
"/rooms/%s/topic" % self.uncreated_rmid)
|
||||
"/rooms/%s/state/m.room.topic" % self.uncreated_rmid)
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
|
||||
# set/get topic in created PRIVATE room not joined, expect 403
|
||||
@@ -223,19 +221,19 @@ class RoomPermissionsTestCase(RestTestCase):
|
||||
|
||||
# get topic in PUBLIC room, not joined, expect 200 (or 404)
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
"/rooms/%s/topic" % self.created_public_rmid)
|
||||
"/rooms/%s/state/m.room.topic" % self.created_public_rmid)
|
||||
self.assertEquals(200, code, msg=str(response))
|
||||
|
||||
# set topic in PUBLIC room, not joined, expect 403
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT",
|
||||
"/rooms/%s/topic" % self.created_public_rmid,
|
||||
"/rooms/%s/state/m.room.topic" % self.created_public_rmid,
|
||||
topic_content)
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _test_get_membership(self, room=None, members=[], expect_code=None):
|
||||
path = "/rooms/%s/members/%s/state"
|
||||
path = "/rooms/%s/state/m.room.member/%s"
|
||||
for member in members:
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
path %
|
||||
@@ -291,12 +289,12 @@ class RoomPermissionsTestCase(RestTestCase):
|
||||
def test_membership_public_room_perms(self):
|
||||
room = self.created_public_rmid
|
||||
# get membership of self, get membership of other, public room + invite
|
||||
# expect all 403s
|
||||
# expect all 200s - public rooms, you can see who is in them.
|
||||
yield self.invite(room=room, src=self.rmcreator_id,
|
||||
targ=self.user_id)
|
||||
yield self._test_get_membership(
|
||||
members=[self.user_id, self.rmcreator_id],
|
||||
room=room, expect_code=403)
|
||||
room=room, expect_code=200)
|
||||
|
||||
# get membership of self, get membership of other, public room + joined
|
||||
# expect all 200s
|
||||
@@ -306,11 +304,11 @@ class RoomPermissionsTestCase(RestTestCase):
|
||||
room=room, expect_code=200)
|
||||
|
||||
# get membership of self, get membership of other, public room + left
|
||||
# expect all 403s
|
||||
# expect all 200s - public rooms, you can always see who is in them.
|
||||
yield self.leave(room=room, user=self.user_id)
|
||||
yield self._test_get_membership(
|
||||
members=[self.user_id, self.rmcreator_id],
|
||||
room=room, expect_code=403)
|
||||
room=room, expect_code=200)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_invited_permissions(self):
|
||||
@@ -403,12 +401,12 @@ class RoomsMemberListTestCase(RestTestCase):
|
||||
"test",
|
||||
db_pool=None,
|
||||
http_client=None,
|
||||
federation=Mock(),
|
||||
datastore=MemoryDataStore(),
|
||||
replication_layer=Mock(),
|
||||
state_handler=state_handler,
|
||||
persistence_service=persistence_service,
|
||||
)
|
||||
hs.get_handlers().federation_handler = Mock()
|
||||
|
||||
self.auth_user_id = self.user_id
|
||||
|
||||
@@ -423,32 +421,29 @@ class RoomsMemberListTestCase(RestTestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_member_list(self):
|
||||
room_id = "!aa:test"
|
||||
yield self.create_room_as(room_id, self.user_id)
|
||||
room_id = yield self.create_room_as(self.user_id)
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
"/rooms/%s/members/list" % room_id)
|
||||
"/rooms/%s/members" % room_id)
|
||||
self.assertEquals(200, code, msg=str(response))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_member_list_no_room(self):
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
"/rooms/roomdoesnotexist/members/list")
|
||||
"/rooms/roomdoesnotexist/members")
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_member_list_no_permission(self):
|
||||
room_id = "!bb:test"
|
||||
yield self.create_room_as(room_id, "@some_other_guy:red")
|
||||
room_id = yield self.create_room_as("@some_other_guy:red")
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
"/rooms/%s/members/list" % room_id)
|
||||
"/rooms/%s/members" % room_id)
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_member_list_mixed_memberships(self):
|
||||
room_id = "!bb:test"
|
||||
room_creator = "@some_other_guy:blue"
|
||||
room_path = "/rooms/%s/members/list" % room_id
|
||||
yield self.create_room_as(room_id, room_creator)
|
||||
room_id = yield self.create_room_as(room_creator)
|
||||
room_path = "/rooms/%s/members" % room_id
|
||||
yield self.invite(room=room_id, src=room_creator,
|
||||
targ=self.user_id)
|
||||
# can't see list if you're just invited.
|
||||
@@ -484,12 +479,12 @@ class RoomsCreateTestCase(RestTestCase):
|
||||
"test",
|
||||
db_pool=None,
|
||||
http_client=None,
|
||||
federation=Mock(),
|
||||
datastore=MemoryDataStore(),
|
||||
replication_layer=Mock(),
|
||||
state_handler=state_handler,
|
||||
persistence_service=persistence_service,
|
||||
)
|
||||
hs.get_handlers().federation_handler = Mock()
|
||||
|
||||
def _get_user_by_token(token=None):
|
||||
return hs.parse_userid(self.auth_user_id)
|
||||
@@ -503,107 +498,57 @@ class RoomsCreateTestCase(RestTestCase):
|
||||
@defer.inlineCallbacks
|
||||
def test_post_room_no_keys(self):
|
||||
# POST with no config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
"{}")
|
||||
(code, response) = yield self.mock_resource.trigger("POST",
|
||||
"/createRoom",
|
||||
"{}")
|
||||
self.assertEquals(200, code, response)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_post_room_visibility_key(self):
|
||||
# POST with visibility config key, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
'{"visibility":"private"}')
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"POST",
|
||||
"/createRoom",
|
||||
'{"visibility":"private"}')
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_post_room_custom_key(self):
|
||||
# POST with custom config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
'{"custom":"stuff"}')
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"POST",
|
||||
"/createRoom",
|
||||
'{"custom":"stuff"}')
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_post_room_known_and_unknown_keys(self):
|
||||
# POST with custom + known config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
'{"visibility":"private","custom":"things"}')
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"POST",
|
||||
"/createRoom",
|
||||
'{"visibility":"private","custom":"things"}')
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_post_room_invalid_content(self):
|
||||
# POST with invalid content / paths, expect 400
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
'{"visibili')
|
||||
self.assertEquals(400, code)
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("POST", "/rooms",
|
||||
'["hello"]')
|
||||
self.assertEquals(400, code)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_no_keys(self):
|
||||
# PUT with no config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%21aa%3Atest", "{}"
|
||||
)
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_visibility_key(self):
|
||||
# PUT with known config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%21bb%3Atest", '{"visibility":"private"}'
|
||||
)
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_custom_key(self):
|
||||
# PUT with custom config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%21cc%3Atest", '{"custom":"stuff"}'
|
||||
)
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_known_and_unknown_keys(self):
|
||||
# PUT with custom + known config keys, expect new room id
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%21dd%3Atest",
|
||||
'{"visibility":"private","custom":"things"}'
|
||||
)
|
||||
self.assertEquals(200, code)
|
||||
self.assertTrue("room_id" in response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_invalid_content(self):
|
||||
# PUT with invalid content / room names, expect 400
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/ee", '{"sdf"'
|
||||
)
|
||||
"POST",
|
||||
"/createRoom",
|
||||
'{"visibili')
|
||||
self.assertEquals(400, code)
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/ee", '["hello"]'
|
||||
)
|
||||
"POST",
|
||||
"/createRoom",
|
||||
'["hello"]')
|
||||
self.assertEquals(400, code)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_put_room_conflict(self):
|
||||
yield self.create_room_as("!aa:test", self.user_id)
|
||||
|
||||
# PUT with conflicting room ID, expect 409
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%21aa%3Atest", "{}"
|
||||
)
|
||||
self.assertEquals(409, code)
|
||||
|
||||
|
||||
class RoomTopicTestCase(RestTestCase):
|
||||
""" Tests /rooms/$room_id/topic REST events. """
|
||||
@@ -613,8 +558,6 @@ class RoomTopicTestCase(RestTestCase):
|
||||
def setUp(self):
|
||||
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
|
||||
self.auth_user_id = self.user_id
|
||||
self.room_id = "!rid1:test"
|
||||
self.path = "/rooms/%s/topic" % self.room_id
|
||||
|
||||
state_handler = Mock(spec=["handle_new_event"])
|
||||
state_handler.handle_new_event.return_value = True
|
||||
@@ -626,12 +569,12 @@ class RoomTopicTestCase(RestTestCase):
|
||||
"test",
|
||||
db_pool=None,
|
||||
http_client=None,
|
||||
federation=Mock(),
|
||||
datastore=MemoryDataStore(),
|
||||
replication_layer=Mock(),
|
||||
state_handler=state_handler,
|
||||
persistence_service=persistence_service,
|
||||
)
|
||||
hs.get_handlers().federation_handler = Mock()
|
||||
|
||||
def _get_user_by_token(token=None):
|
||||
return hs.parse_userid(self.auth_user_id)
|
||||
@@ -640,7 +583,8 @@ class RoomTopicTestCase(RestTestCase):
|
||||
synapse.rest.room.register_servlets(hs, self.mock_resource)
|
||||
|
||||
# create the room
|
||||
yield self.create_room_as(self.room_id, self.user_id)
|
||||
self.room_id = yield self.create_room_as(self.user_id)
|
||||
self.path = "/rooms/%s/state/m.room.topic" % self.room_id
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
@@ -717,7 +661,6 @@ class RoomMemberStateTestCase(RestTestCase):
|
||||
def setUp(self):
|
||||
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
|
||||
self.auth_user_id = self.user_id
|
||||
self.room_id = "!rid1:test"
|
||||
|
||||
state_handler = Mock(spec=["handle_new_event"])
|
||||
state_handler.handle_new_event.return_value = True
|
||||
@@ -729,12 +672,12 @@ class RoomMemberStateTestCase(RestTestCase):
|
||||
"test",
|
||||
db_pool=None,
|
||||
http_client=None,
|
||||
federation=Mock(),
|
||||
datastore=MemoryDataStore(),
|
||||
replication_layer=Mock(),
|
||||
state_handler=state_handler,
|
||||
persistence_service=persistence_service,
|
||||
)
|
||||
hs.get_handlers().federation_handler = Mock()
|
||||
|
||||
def _get_user_by_token(token=None):
|
||||
return hs.parse_userid(self.auth_user_id)
|
||||
@@ -742,14 +685,14 @@ class RoomMemberStateTestCase(RestTestCase):
|
||||
|
||||
synapse.rest.room.register_servlets(hs, self.mock_resource)
|
||||
|
||||
yield self.create_room_as(self.room_id, self.user_id)
|
||||
self.room_id = yield self.create_room_as(self.user_id)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_invalid_puts(self):
|
||||
path = "/rooms/%s/members/%s/state" % (self.room_id, self.user_id)
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (self.room_id, self.user_id)
|
||||
# missing keys or invalid json
|
||||
(code, response) = yield self.mock_resource.trigger("PUT",
|
||||
path, '{}')
|
||||
@@ -783,7 +726,7 @@ class RoomMemberStateTestCase(RestTestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_rooms_members_self(self):
|
||||
path = "/rooms/%s/members/%s/state" % (
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (
|
||||
urllib.quote(self.room_id), self.user_id
|
||||
)
|
||||
|
||||
@@ -804,7 +747,7 @@ class RoomMemberStateTestCase(RestTestCase):
|
||||
@defer.inlineCallbacks
|
||||
def test_rooms_members_other(self):
|
||||
self.other_id = "@zzsid1:red"
|
||||
path = "/rooms/%s/members/%s/state" % (
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (
|
||||
urllib.quote(self.room_id), self.other_id
|
||||
)
|
||||
|
||||
@@ -820,7 +763,7 @@ class RoomMemberStateTestCase(RestTestCase):
|
||||
@defer.inlineCallbacks
|
||||
def test_rooms_members_other_custom_keys(self):
|
||||
self.other_id = "@zzsid1:red"
|
||||
path = "/rooms/%s/members/%s/state" % (
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (
|
||||
urllib.quote(self.room_id), self.other_id
|
||||
)
|
||||
|
||||
@@ -843,7 +786,6 @@ class RoomMessagesTestCase(RestTestCase):
|
||||
def setUp(self):
|
||||
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
|
||||
self.auth_user_id = self.user_id
|
||||
self.room_id = "!rid1:test"
|
||||
|
||||
state_handler = Mock(spec=["handle_new_event"])
|
||||
state_handler.handle_new_event.return_value = True
|
||||
@@ -855,12 +797,12 @@ class RoomMessagesTestCase(RestTestCase):
|
||||
"test",
|
||||
db_pool=None,
|
||||
http_client=None,
|
||||
federation=Mock(),
|
||||
datastore=MemoryDataStore(),
|
||||
replication_layer=Mock(),
|
||||
state_handler=state_handler,
|
||||
persistence_service=persistence_service,
|
||||
)
|
||||
hs.get_handlers().federation_handler = Mock()
|
||||
|
||||
def _get_user_by_token(token=None):
|
||||
return hs.parse_userid(self.auth_user_id)
|
||||
@@ -868,16 +810,15 @@ class RoomMessagesTestCase(RestTestCase):
|
||||
|
||||
synapse.rest.room.register_servlets(hs, self.mock_resource)
|
||||
|
||||
yield self.create_room_as(self.room_id, self.user_id)
|
||||
self.room_id = yield self.create_room_as(self.user_id)
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_invalid_puts(self):
|
||||
path = "/rooms/%s/messages/%s/mid1" % (
|
||||
urllib.quote(self.room_id), self.user_id
|
||||
)
|
||||
path = "/rooms/%s/send/m.room.message/mid1" % (
|
||||
urllib.quote(self.room_id))
|
||||
# missing keys or invalid json
|
||||
(code, response) = yield self.mock_resource.trigger("PUT",
|
||||
path, '{}')
|
||||
@@ -905,9 +846,8 @@ class RoomMessagesTestCase(RestTestCase):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_rooms_messages_sent(self):
|
||||
path = "/rooms/%s/messages/%s/mid1" % (
|
||||
urllib.quote(self.room_id), self.user_id
|
||||
)
|
||||
path = "/rooms/%s/send/m.room.message/mid1" % (
|
||||
urllib.quote(self.room_id))
|
||||
|
||||
content = '{"body":"test","msgtype":{"type":"a"}}'
|
||||
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
|
||||
@@ -923,9 +863,8 @@ class RoomMessagesTestCase(RestTestCase):
|
||||
# self.assert_dict(json.loads(content), response)
|
||||
|
||||
# m.text message type
|
||||
path = "/rooms/%s/messages/%s/mid2" % (
|
||||
urllib.quote(self.room_id), self.user_id
|
||||
)
|
||||
path = "/rooms/%s/send/m.room.message/mid2" % (
|
||||
urllib.quote(self.room_id))
|
||||
content = '{"body":"test2","msgtype":"m.text"}'
|
||||
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
|
||||
self.assertEquals(200, code, msg=str(response))
|
||||
@@ -933,11 +872,3 @@ class RoomMessagesTestCase(RestTestCase):
|
||||
# (code, response) = yield self.mock_resource.trigger("GET", path, None)
|
||||
# self.assertEquals(200, code, msg=str(response))
|
||||
# self.assert_dict(json.loads(content), response)
|
||||
|
||||
# trying to send message in different user path
|
||||
path = "/rooms/%s/messages/%s/mid2" % (
|
||||
urllib.quote(self.room_id), "invalid" + self.user_id
|
||||
)
|
||||
content = '{"body":"test2","msgtype":"m.text"}'
|
||||
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
|
||||
@@ -21,8 +21,10 @@ from twisted.trial import unittest
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
|
||||
class RestTestCase(unittest.TestCase):
|
||||
"""Contains extra helper functions to quickly and clearly perform a given
|
||||
REST action, which isn't the focus of the test.
|
||||
@@ -39,18 +41,19 @@ class RestTestCase(unittest.TestCase):
|
||||
return self.auth_user_id
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def create_room_as(self, room_id, room_creator, is_public=True, tok=None):
|
||||
def create_room_as(self, room_creator, is_public=True, tok=None):
|
||||
temp_id = self.auth_user_id
|
||||
self.auth_user_id = room_creator
|
||||
path = "/rooms/%s" % room_id
|
||||
path = "/createRoom"
|
||||
content = "{}"
|
||||
if not is_public:
|
||||
content = '{"visibility":"private"}'
|
||||
if tok:
|
||||
path = path + "?access_token=%s" % tok
|
||||
(code, response) = yield self.mock_resource.trigger("PUT", path, content)
|
||||
(code, response) = yield self.mock_resource.trigger("POST", path, content)
|
||||
self.assertEquals(200, code, msg=str(response))
|
||||
self.auth_user_id = temp_id
|
||||
defer.returnValue(response["room_id"])
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def invite(self, room=None, src=None, targ=None, expect_code=200, tok=None):
|
||||
@@ -71,23 +74,22 @@ class RestTestCase(unittest.TestCase):
|
||||
expect_code=expect_code)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def change_membership(self, room=None, src=None, targ=None,
|
||||
membership=None, expect_code=200, tok=None):
|
||||
def change_membership(self, room, src, targ, membership, tok=None,
|
||||
expect_code=200):
|
||||
temp_id = self.auth_user_id
|
||||
self.auth_user_id = src
|
||||
|
||||
path = "/rooms/%s/members/%s/state" % (room, targ)
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (room, targ)
|
||||
if tok:
|
||||
path = path + "?access_token=%s" % tok
|
||||
|
||||
if membership == Membership.LEAVE:
|
||||
(code, response) = yield self.mock_resource.trigger("DELETE", path,
|
||||
None)
|
||||
self.assertEquals(expect_code, code, msg=str(response))
|
||||
else:
|
||||
(code, response) = yield self.mock_resource.trigger("PUT", path,
|
||||
'{"membership":"%s"}' % membership)
|
||||
self.assertEquals(expect_code, code, msg=str(response))
|
||||
data = {
|
||||
"membership": membership
|
||||
}
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("PUT", path,
|
||||
json.dumps(data))
|
||||
self.assertEquals(expect_code, code, msg=str(response))
|
||||
|
||||
self.auth_user_id = temp_id
|
||||
|
||||
@@ -99,14 +101,14 @@ class RestTestCase(unittest.TestCase):
|
||||
defer.returnValue(response)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def send(self, room_id, sender_id, body=None, msg_id=None, tok=None,
|
||||
def send(self, room_id, body=None, txn_id=None, tok=None,
|
||||
expect_code=200):
|
||||
if msg_id is None:
|
||||
msg_id = "m%s" % (str(time.time()))
|
||||
if txn_id is None:
|
||||
txn_id = "m%s" % (str(time.time()))
|
||||
if body is None:
|
||||
body = "body_text_here"
|
||||
|
||||
path = "/rooms/%s/messages/%s/%s" % (room_id, sender_id, msg_id)
|
||||
path = "/rooms/%s/send/m.room.message/%s" % (room_id, txn_id)
|
||||
content = '{"msgtype":"m.text","body":"%s"}' % body
|
||||
if tok:
|
||||
path = path + "?access_token=%s" % tok
|
||||
|
||||
@@ -243,21 +243,24 @@ class StateTestCase(unittest.TestCase):
|
||||
|
||||
state_pdu = new_fake_pdu_entry("C", "test", "mem", "x", "A", 20)
|
||||
|
||||
tup = ("pdu_id", "origin.com", 5)
|
||||
pdus = [tup]
|
||||
snapshot = Mock()
|
||||
snapshot.prev_state_pdu = state_pdu
|
||||
event_id = "pdu_id@origin.com"
|
||||
|
||||
self.persistence.get_latest_pdus_in_context.return_value = pdus
|
||||
self.persistence.get_current_state_pdu.return_value = state_pdu
|
||||
def fill_out_prev_events(event):
|
||||
event.prev_events = [event_id]
|
||||
event.depth = 6
|
||||
snapshot.fill_out_prev_events = fill_out_prev_events
|
||||
|
||||
yield self.state.handle_new_event(event)
|
||||
yield self.state.handle_new_event(event, snapshot)
|
||||
|
||||
self.assertLess(tup[2], event.depth)
|
||||
self.assertLess(5, event.depth)
|
||||
|
||||
self.assertEquals(1, len(event.prev_events))
|
||||
|
||||
prev_id = event.prev_events[0]
|
||||
|
||||
self.assertEqual(encode_event_id(tup[0], tup[1]), prev_id)
|
||||
self.assertEqual(event_id, prev_id)
|
||||
|
||||
self.assertEqual(
|
||||
encode_event_id(state_pdu.pdu_id, state_pdu.origin),
|
||||
|
||||
@@ -21,13 +21,23 @@ from synapse.api.events.room import (
|
||||
RoomMemberEvent, MessageEvent
|
||||
)
|
||||
|
||||
from twisted.internet import defer
|
||||
from twisted.internet import defer, reactor
|
||||
|
||||
from collections import namedtuple
|
||||
from mock import patch, Mock
|
||||
import json
|
||||
import urlparse
|
||||
|
||||
from inspect import getcallargs
|
||||
|
||||
|
||||
def get_mock_call_args(pattern_func, mock_func):
|
||||
""" Return the arguments the mock function was called with interpreted
|
||||
by the pattern functions argument list.
|
||||
"""
|
||||
invoked_args, invoked_kargs = mock_func.call_args
|
||||
return getcallargs(pattern_func, *invoked_args, **invoked_kargs)
|
||||
|
||||
|
||||
# This is a mock /resource/ not an entire server
|
||||
class MockHttpResource(HttpServer):
|
||||
@@ -127,6 +137,15 @@ class MemoryDataStore(object):
|
||||
self.current_state = {}
|
||||
self.events = []
|
||||
|
||||
class Snapshot(namedtuple("Snapshot", "room_id user_id membership_state")):
|
||||
def fill_out_prev_events(self, event):
|
||||
pass
|
||||
|
||||
def snapshot_room(self, room_id, user_id, state_type=None, state_key=None):
|
||||
return self.Snapshot(
|
||||
room_id, user_id, self.get_room_member(user_id, room_id)
|
||||
)
|
||||
|
||||
def register(self, user_id, token, password_hash):
|
||||
if user_id in self.tokens_to_users.values():
|
||||
raise StoreError(400, "User in use.")
|
||||
@@ -183,7 +202,7 @@ class MemoryDataStore(object):
|
||||
def persist_event(self, event):
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
room_id = event.room_id
|
||||
user = event.target_user_id
|
||||
user = event.state_key
|
||||
membership = event.membership
|
||||
self.members.setdefault(room_id, {})[user] = event
|
||||
|
||||
@@ -196,7 +215,9 @@ class MemoryDataStore(object):
|
||||
def get_current_state(self, room_id, event_type=None, state_key=""):
|
||||
if event_type:
|
||||
key = (room_id, event_type, state_key)
|
||||
return self.current_state.get(key)
|
||||
if self.current_state.get(key):
|
||||
return [self.current_state.get(key)]
|
||||
return None
|
||||
else:
|
||||
return [
|
||||
e for e in self.current_state
|
||||
@@ -214,7 +235,7 @@ class MemoryDataStore(object):
|
||||
|
||||
def _format_call(args, kwargs):
|
||||
return ", ".join(
|
||||
["%r" % (a) for a in args] +
|
||||
["%r" % (a) for a in args] +
|
||||
["%s=%r" % (k, v) for k, v in kwargs.items()]
|
||||
)
|
||||
|
||||
@@ -227,8 +248,11 @@ class DeferredMockCallable(object):
|
||||
|
||||
def __init__(self):
|
||||
self.expectations = []
|
||||
self.calls = []
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.calls.append((args, kwargs))
|
||||
|
||||
if not self.expectations:
|
||||
raise ValueError("%r has no pending calls to handle call(%s)" % (
|
||||
self, _format_call(args, kwargs))
|
||||
@@ -239,15 +263,52 @@ class DeferredMockCallable(object):
|
||||
d.callback(None)
|
||||
return result
|
||||
|
||||
raise AssertionError("Was not expecting call(%s)" %
|
||||
failure = AssertionError("Was not expecting call(%s)" %
|
||||
_format_call(args, kwargs)
|
||||
)
|
||||
|
||||
for _, _, d in self.expectations:
|
||||
try:
|
||||
d.errback(failure)
|
||||
except:
|
||||
pass
|
||||
|
||||
raise failure
|
||||
|
||||
def expect_call_and_return(self, call, result):
|
||||
self.expectations.append((call, result, defer.Deferred()))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def await_calls(self):
|
||||
while self.expectations:
|
||||
(_, _, d) = self.expectations.pop(0)
|
||||
yield d
|
||||
def await_calls(self, timeout=1000):
|
||||
deferred = defer.DeferredList(
|
||||
[d for _, _, d in self.expectations],
|
||||
fireOnOneErrback=True
|
||||
)
|
||||
|
||||
timer = reactor.callLater(
|
||||
timeout/1000,
|
||||
deferred.errback,
|
||||
AssertionError(
|
||||
"%d pending calls left: %s"% (
|
||||
len([e for e in self.expectations if not e[2].called]),
|
||||
[e for e in self.expectations if not e[2].called]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
yield deferred
|
||||
|
||||
timer.cancel()
|
||||
|
||||
self.calls = []
|
||||
|
||||
def assert_had_no_calls(self):
|
||||
if self.calls:
|
||||
calls = self.calls
|
||||
self.calls = []
|
||||
|
||||
raise AssertionError("Expected not to received any calls, got:\n" +
|
||||
"\n".join([
|
||||
"call(%s)" % _format_call(c[0], c[1]) for c in calls
|
||||
])
|
||||
)
|
||||
|
||||
@@ -20,9 +20,9 @@ limitations under the License.
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('MatrixWebClientController', ['matrixService'])
|
||||
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'eventStreamService',
|
||||
function($scope, $location, $rootScope, matrixService, eventStreamService) {
|
||||
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
|
||||
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService',
|
||||
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService) {
|
||||
|
||||
// Check current URL to avoid to display the logout button on the login page
|
||||
$scope.location = $location.path();
|
||||
@@ -31,36 +31,29 @@ angular.module('MatrixWebClientController', ['matrixService'])
|
||||
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
|
||||
$scope.location = $location.path();
|
||||
});
|
||||
|
||||
|
||||
// Manage the display of the current config
|
||||
$scope.config;
|
||||
|
||||
// Toggles the config display
|
||||
$scope.showConfig = function() {
|
||||
if ($scope.config) {
|
||||
$scope.config = undefined;
|
||||
}
|
||||
else {
|
||||
$scope.config = matrixService.config();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.closeConfig = function() {
|
||||
if ($scope.config) {
|
||||
$scope.config = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (matrixService.isUserLoggedIn()) {
|
||||
// eventStreamService.resume();
|
||||
eventStreamService.resume();
|
||||
mPresence.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a given page.
|
||||
* @param {String} url url of the page
|
||||
*/
|
||||
$scope.goToPage = function(url) {
|
||||
$location.url(url);
|
||||
};
|
||||
|
||||
// Logs the user out
|
||||
$scope.logout = function() {
|
||||
|
||||
// kill the event stream
|
||||
eventStreamService.stop();
|
||||
|
||||
|
||||
// Do not update presence anymore
|
||||
mPresence.stop();
|
||||
|
||||
// Clean permanent data
|
||||
matrixService.setConfig({});
|
||||
matrixService.saveConfig();
|
||||
@@ -83,7 +76,6 @@ angular.module('MatrixWebClientController', ['matrixService'])
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,81 @@
|
||||
/*** Mobile voodoo ***/
|
||||
@media all and (max-device-width: 640px) {
|
||||
|
||||
#messageTableWrapper {
|
||||
margin-right: 0px ! important;
|
||||
}
|
||||
|
||||
.leftBlock {
|
||||
width: 8em ! important;
|
||||
font-size: 8px ! important;
|
||||
}
|
||||
|
||||
.rightBlock {
|
||||
width: 0px ! important;
|
||||
display: none ! important;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 36px ! important;
|
||||
}
|
||||
|
||||
#header,
|
||||
#messageTable,
|
||||
#wrapper,
|
||||
#roomName,
|
||||
#controls {
|
||||
max-width: 640px ! important;
|
||||
}
|
||||
|
||||
#userIdCell,
|
||||
#usersTableWrapper,
|
||||
#extraControls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#buttonsCell {
|
||||
width: 60px ! important;
|
||||
padding-left: 20px ! important;
|
||||
}
|
||||
|
||||
#roomLogo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#roomName {
|
||||
text-align: left ! important;
|
||||
top: -35px ! important;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
font-size: 12px ! important;
|
||||
min-height: 20px ! important;
|
||||
}
|
||||
|
||||
#page {
|
||||
top: 35px ! important;
|
||||
bottom: 70px ! important;
|
||||
}
|
||||
|
||||
#header,
|
||||
#page {
|
||||
margin: 5px ! important;
|
||||
}
|
||||
|
||||
#header {
|
||||
padding: 5px ! important;
|
||||
}
|
||||
|
||||
/* stop zoom on select */
|
||||
select:focus,
|
||||
textarea,
|
||||
input
|
||||
{
|
||||
font-size: 16px ! important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
|
||||
font-size: 12pt;
|
||||
@@ -17,7 +95,6 @@ h1 {
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
margin: 20px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
@@ -32,8 +109,7 @@ h1 {
|
||||
text-align: right;
|
||||
top: -40px;
|
||||
position: absolute;
|
||||
font-size: 16pt;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
@@ -50,6 +126,10 @@ h1 {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#buttonsCell {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#inputBarTable {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -111,13 +191,13 @@ h1 {
|
||||
color: #fff;
|
||||
margin: 2px;
|
||||
bottom: 0px;
|
||||
font-size: 8pt;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.userPresence {
|
||||
text-align: center;
|
||||
font-size: 8pt;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background-color: #aaa;
|
||||
border-bottom: 1px #ddd solid;
|
||||
@@ -159,7 +239,7 @@ h1 {
|
||||
background-color: #fff;
|
||||
color: #888;
|
||||
font-weight: medium;
|
||||
font-size: 8pt;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
border-top: 1px #ddd solid;
|
||||
}
|
||||
@@ -272,12 +352,70 @@ h1 {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/*** Recents ***/
|
||||
.recentsTable {
|
||||
max-width: 480px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
.recentsTable tr {
|
||||
width: 100%;
|
||||
}
|
||||
.recentsTable td {
|
||||
vertical-align: text-top;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.recentsRoom {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.recentsRoom:hover {
|
||||
background-color: #f8f8ff;
|
||||
}
|
||||
|
||||
.recentsRoomSelected {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.recentsRoomName {
|
||||
font-size: 16px;
|
||||
padding-top: 7px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.recentsRoomSummaryTS {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
width: 7em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.recentsRoomSummary {
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
/*** Recents in the room page ***/
|
||||
#roomRecentsTableWrapper {
|
||||
float: left;
|
||||
max-width: 320px;
|
||||
margin-right: 20px;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/*** Profile ***/
|
||||
|
||||
.profile-avatar {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
display:table-cell;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -293,13 +431,19 @@ h1 {
|
||||
}
|
||||
|
||||
#user-displayname {
|
||||
font-size: 16pt;
|
||||
font-size: 24px;
|
||||
}
|
||||
/******************************/
|
||||
|
||||
#header {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
#header
|
||||
{
|
||||
padding: 20px;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#logo,
|
||||
#roomLogo {
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
}
|
||||
@@ -308,18 +452,6 @@ h1 {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#config {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
width: 500px;
|
||||
margin-left: -250px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.text_entry_section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
||||
@@ -19,9 +19,13 @@ var matrixWebClient = angular.module('matrixWebClient', [
|
||||
'MatrixWebClientController',
|
||||
'LoginController',
|
||||
'RoomController',
|
||||
'RoomsController',
|
||||
'HomeController',
|
||||
'RecentsController',
|
||||
'SettingsController',
|
||||
'UserController',
|
||||
'matrixService',
|
||||
'matrixPhoneService',
|
||||
'MatrixCall',
|
||||
'eventStreamService',
|
||||
'eventHandlerService',
|
||||
'infinite-scroll'
|
||||
@@ -44,16 +48,20 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
|
||||
templateUrl: 'room/room.html',
|
||||
controller: 'RoomController'
|
||||
}).
|
||||
when('/rooms', {
|
||||
templateUrl: 'rooms/rooms.html',
|
||||
controller: 'RoomsController'
|
||||
when('/', {
|
||||
templateUrl: 'home/home.html',
|
||||
controller: 'HomeController'
|
||||
}).
|
||||
when('/settings', {
|
||||
templateUrl: 'settings/settings.html',
|
||||
controller: 'SettingsController'
|
||||
}).
|
||||
when('/user/:user_matrix_id', {
|
||||
templateUrl: 'user/user.html',
|
||||
controller: 'UserController'
|
||||
}).
|
||||
otherwise({
|
||||
redirectTo: '/rooms'
|
||||
redirectTo: '/'
|
||||
});
|
||||
|
||||
$provide.factory('AccessTokenInterceptor', ['$q', '$rootScope',
|
||||
@@ -73,13 +81,11 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
|
||||
$httpProvider.interceptors.push('AccessTokenInterceptor');
|
||||
}]);
|
||||
|
||||
matrixWebClient.run(['$location', 'matrixService', 'eventStreamService', function($location, matrixService, eventStreamService) {
|
||||
matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) {
|
||||
|
||||
// If user auth details are not in cache, go to the login page
|
||||
if (!matrixService.isUserLoggedIn()) {
|
||||
eventStreamService.stop();
|
||||
$location.path("login");
|
||||
}
|
||||
else {
|
||||
// eventStreamService.resume();
|
||||
}
|
||||
|
||||
}]);
|
||||
|
||||
@@ -27,13 +27,16 @@ Typically, this service will store events or broadcast them to any listeners
|
||||
if typically all the $on method would do is update its own $scope.
|
||||
*/
|
||||
angular.module('eventHandlerService', [])
|
||||
.factory('eventHandlerService', ['matrixService', '$rootScope', function(matrixService, $rootScope) {
|
||||
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
|
||||
var MSG_EVENT = "MSG_EVENT";
|
||||
var MEMBER_EVENT = "MEMBER_EVENT";
|
||||
var PRESENCE_EVENT = "PRESENCE_EVENT";
|
||||
var CALL_EVENT = "CALL_EVENT";
|
||||
|
||||
var InitialSyncDeferred = $q.defer();
|
||||
|
||||
$rootScope.events = {
|
||||
rooms: {}, // will contain roomId: { messages:[], members:{userid1: event} }
|
||||
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
|
||||
};
|
||||
|
||||
$rootScope.presence = {};
|
||||
@@ -47,17 +50,13 @@ angular.module('eventHandlerService', [])
|
||||
}
|
||||
}
|
||||
|
||||
var reInitRoom = function(room_id) {
|
||||
$rootScope.events.rooms[room_id] = {};
|
||||
$rootScope.events.rooms[room_id].messages = [];
|
||||
$rootScope.events.rooms[room_id].members = {};
|
||||
}
|
||||
var resetRoomMessages = function(room_id) {
|
||||
if ($rootScope.events.rooms[room_id]) {
|
||||
$rootScope.events.rooms[room_id].messages = [];
|
||||
}
|
||||
};
|
||||
|
||||
var handleMessage = function(event, isLiveEvent) {
|
||||
if ("membership_target" in event.content) {
|
||||
event.user_id = event.content.membership_target;
|
||||
}
|
||||
|
||||
initRoom(event.room_id);
|
||||
|
||||
if (isLiveEvent) {
|
||||
@@ -96,12 +95,16 @@ angular.module('eventHandlerService', [])
|
||||
$rootScope.presence[event.content.user_id] = event;
|
||||
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
|
||||
};
|
||||
|
||||
|
||||
var handleCallEvent = function(event, isLiveEvent) {
|
||||
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
|
||||
};
|
||||
|
||||
return {
|
||||
MSG_EVENT: MSG_EVENT,
|
||||
MEMBER_EVENT: MEMBER_EVENT,
|
||||
PRESENCE_EVENT: PRESENCE_EVENT,
|
||||
CALL_EVENT: CALL_EVENT,
|
||||
|
||||
|
||||
handleEvent: function(event, isLiveEvent) {
|
||||
@@ -119,6 +122,9 @@ angular.module('eventHandlerService', [])
|
||||
console.log("Unable to handle event type " + event.type);
|
||||
break;
|
||||
}
|
||||
if (event.type.indexOf('m.call.') == 0) {
|
||||
handleCallEvent(event, isLiveEvent);
|
||||
}
|
||||
},
|
||||
|
||||
// isLiveEvents determines whether notifications should be shown, whether
|
||||
@@ -129,8 +135,18 @@ angular.module('eventHandlerService', [])
|
||||
}
|
||||
},
|
||||
|
||||
reInitRoom: function(room_id) {
|
||||
reInitRoom(room_id);
|
||||
handleInitialSyncDone: function() {
|
||||
console.log("# handleInitialSyncDone");
|
||||
InitialSyncDeferred.resolve($rootScope.events, $rootScope.presence);
|
||||
},
|
||||
|
||||
// Returns a promise that resolves when the initialSync request has been processed
|
||||
waitForInitialSyncCompletion: function() {
|
||||
return InitialSyncDeferred.promise;
|
||||
},
|
||||
|
||||
resetRoomMessages: function(room_id) {
|
||||
resetRoomMessages(room_id);
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
@@ -25,7 +25,8 @@ the eventHandlerService.
|
||||
angular.module('eventStreamService', [])
|
||||
.factory('eventStreamService', ['$q', '$timeout', 'matrixService', 'eventHandlerService', function($q, $timeout, matrixService, eventHandlerService) {
|
||||
var END = "END";
|
||||
var TIMEOUT_MS = 30000;
|
||||
var SERVER_TIMEOUT_MS = 30000;
|
||||
var CLIENT_TIMEOUT_MS = 40000;
|
||||
var ERR_TIMEOUT_MS = 5000;
|
||||
|
||||
var settings = {
|
||||
@@ -55,7 +56,7 @@ angular.module('eventStreamService', [])
|
||||
deferred = deferred || $q.defer();
|
||||
|
||||
// run the stream from the latest token
|
||||
matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
|
||||
matrixService.getEventStream(settings.from, SERVER_TIMEOUT_MS, CLIENT_TIMEOUT_MS).then(
|
||||
function(response) {
|
||||
if (!settings.isActive) {
|
||||
console.log("[EventStream] Got response but now inactive. Dropping data.");
|
||||
@@ -80,7 +81,7 @@ angular.module('eventStreamService', [])
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
if (error.status == 403) {
|
||||
if (error.status === 403) {
|
||||
settings.shouldPoll = false;
|
||||
}
|
||||
|
||||
@@ -96,7 +97,7 @@ angular.module('eventStreamService', [])
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
};
|
||||
|
||||
var startEventStream = function() {
|
||||
settings.shouldPoll = true;
|
||||
@@ -110,18 +111,17 @@ angular.module('eventStreamService', [])
|
||||
for (var i = 0; i < rooms.length; ++i) {
|
||||
var room = rooms[i];
|
||||
if ("state" in room) {
|
||||
for (var j = 0; j < room.state.length; ++j) {
|
||||
eventHandlerService.handleEvents(room.state[j], false);
|
||||
}
|
||||
eventHandlerService.handleEvents(room.state, false);
|
||||
}
|
||||
}
|
||||
|
||||
var presence = response.data.presence;
|
||||
for (var i = 0; i < presence.length; ++i) {
|
||||
eventHandlerService.handleEvent(presence[i], false);
|
||||
}
|
||||
eventHandlerService.handleEvents(presence, false);
|
||||
|
||||
settings.from = response.data.end
|
||||
// Initial sync is done
|
||||
eventHandlerService.handleInitialSyncDone();
|
||||
|
||||
settings.from = response.data.end;
|
||||
doEventStream(deferred);
|
||||
},
|
||||
function(error) {
|
||||
|
||||
268
webclient/components/matrix/matrix-call.js
Normal file
268
webclient/components/matrix/matrix-call.js
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
var forAllVideoTracksOnStream = function(s, f) {
|
||||
var tracks = s.getVideoTracks();
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
f(tracks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var forAllAudioTracksOnStream = function(s, f) {
|
||||
var tracks = s.getAudioTracks();
|
||||
for (var i = 0; i < tracks.length; i++) {
|
||||
f(tracks[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var forAllTracksOnStream = function(s, f) {
|
||||
forAllVideoTracksOnStream(s, f);
|
||||
forAllAudioTracksOnStream(s, f);
|
||||
}
|
||||
|
||||
angular.module('MatrixCall', [])
|
||||
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', function MatrixCallFactory(matrixService, matrixPhoneService) {
|
||||
var MatrixCall = function(room_id) {
|
||||
this.room_id = room_id;
|
||||
this.call_id = "c" + new Date().getTime();
|
||||
this.state = 'fledgling';
|
||||
}
|
||||
|
||||
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
||||
|
||||
window.RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
|
||||
|
||||
MatrixCall.prototype.placeCall = function() {
|
||||
self = this;
|
||||
matrixPhoneService.callPlaced(this);
|
||||
navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); });
|
||||
self.state = 'wait_local_media';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.initWithInvite = function(msg) {
|
||||
this.msg = msg;
|
||||
this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]})
|
||||
self= this;
|
||||
this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
|
||||
this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
|
||||
this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
|
||||
this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
|
||||
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
|
||||
this.state = 'ringing';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.answer = function() {
|
||||
console.trace("Answering call "+this.call_id);
|
||||
self = this;
|
||||
navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForAnswer(s); }, function(e) { self.getUserMediaFailed(e); });
|
||||
this.state = 'wait_local_media';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.hangup = function() {
|
||||
console.trace("Ending call "+this.call_id);
|
||||
|
||||
if (this.localAVStream) {
|
||||
forAllTracksOnStream(this.localAVStream, function(t) {
|
||||
t.stop();
|
||||
});
|
||||
}
|
||||
if (this.remoteAVStream) {
|
||||
forAllTracksOnStream(this.remoteAVStream, function(t) {
|
||||
t.stop();
|
||||
});
|
||||
}
|
||||
|
||||
var content = {
|
||||
version: 0,
|
||||
call_id: this.call_id,
|
||||
};
|
||||
matrixService.sendEvent(this.room_id, 'm.call.hangup', undefined, content).then(this.messageSent, this.messageSendFailed);
|
||||
this.state = 'ended';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.gotUserMediaForInvite = function(stream) {
|
||||
this.localAVStream = stream;
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
for (var i = 0; i < audioTracks.length; i++) {
|
||||
audioTracks[i].enabled = true;
|
||||
}
|
||||
this.peerConn = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]})
|
||||
self = this;
|
||||
this.peerConn.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
|
||||
this.peerConn.onsignalingstatechange = function() { self.onSignallingStateChanged(); };
|
||||
this.peerConn.onicecandidate = function(c) { self.gotLocalIceCandidate(c); };
|
||||
this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
|
||||
this.peerConn.addStream(stream);
|
||||
this.peerConn.createOffer(function(d) {
|
||||
self.gotLocalOffer(d);
|
||||
}, function(e) {
|
||||
self.getLocalOfferFailed(e);
|
||||
});
|
||||
this.state = 'create_offer';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.gotUserMediaForAnswer = function(stream) {
|
||||
this.localAVStream = stream;
|
||||
var audioTracks = stream.getAudioTracks();
|
||||
for (var i = 0; i < audioTracks.length; i++) {
|
||||
audioTracks[i].enabled = true;
|
||||
}
|
||||
this.peerConn.addStream(stream);
|
||||
self = this;
|
||||
var constraints = {
|
||||
'mandatory': {
|
||||
'OfferToReceiveAudio': true,
|
||||
'OfferToReceiveVideo': false
|
||||
},
|
||||
};
|
||||
this.peerConn.createAnswer(function(d) { self.createdAnswer(d); }, function(e) {}, constraints);
|
||||
this.state = 'create_answer';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.gotLocalIceCandidate = function(event) {
|
||||
console.trace(event);
|
||||
if (event.candidate) {
|
||||
var content = {
|
||||
version: 0,
|
||||
call_id: this.call_id,
|
||||
candidate: event.candidate
|
||||
};
|
||||
matrixService.sendEvent(this.room_id, 'm.call.candidate', undefined, content).then(this.messageSent, this.messageSendFailed);
|
||||
}
|
||||
}
|
||||
|
||||
MatrixCall.prototype.gotRemoteIceCandidate = function(cand) {
|
||||
console.trace("Got ICE candidate from remote: "+cand);
|
||||
var candidateObject = new RTCIceCandidate({
|
||||
sdpMLineIndex: cand.label,
|
||||
candidate: cand.candidate
|
||||
});
|
||||
this.peerConn.addIceCandidate(candidateObject, function() {}, function(e) {});
|
||||
};
|
||||
|
||||
MatrixCall.prototype.receivedAnswer = function(msg) {
|
||||
this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
|
||||
this.state = 'connecting';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.gotLocalOffer = function(description) {
|
||||
console.trace("Created offer: "+description);
|
||||
this.peerConn.setLocalDescription(description);
|
||||
|
||||
var content = {
|
||||
version: 0,
|
||||
call_id: this.call_id,
|
||||
offer: description
|
||||
};
|
||||
matrixService.sendEvent(this.room_id, 'm.call.invite', undefined, content).then(this.messageSent, this.messageSendFailed);
|
||||
this.state = 'invite_sent';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.createdAnswer = function(description) {
|
||||
console.trace("Created answer: "+description);
|
||||
this.peerConn.setLocalDescription(description);
|
||||
var content = {
|
||||
version: 0,
|
||||
call_id: this.call_id,
|
||||
answer: description
|
||||
};
|
||||
matrixService.sendEvent(this.room_id, 'm.call.answer', undefined, content).then(this.messageSent, this.messageSendFailed);
|
||||
this.state = 'connecting';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.messageSent = function() {
|
||||
};
|
||||
|
||||
MatrixCall.prototype.messageSendFailed = function(error) {
|
||||
};
|
||||
|
||||
MatrixCall.prototype.getLocalOfferFailed = function(error) {
|
||||
this.onError("Failed to start audio for call!");
|
||||
};
|
||||
|
||||
MatrixCall.prototype.getUserMediaFailed = function() {
|
||||
this.onError("Couldn't start capturing audio! Is your microphone set up?");
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onIceConnectionStateChanged = function() {
|
||||
console.trace("Ice connection state changed to: "+this.peerConn.iceConnectionState);
|
||||
// ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet
|
||||
if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
|
||||
this.state = 'connected';
|
||||
}
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onSignallingStateChanged = function() {
|
||||
console.trace("Signalling state changed to: "+this.peerConn.signalingState);
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onSetRemoteDescriptionSuccess = function() {
|
||||
console.trace("Set remote description");
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onSetRemoteDescriptionError = function(e) {
|
||||
console.trace("Failed to set remote description"+e);
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onAddStream = function(event) {
|
||||
console.trace("Stream added"+event);
|
||||
|
||||
var s = event.stream;
|
||||
|
||||
this.remoteAVStream = s;
|
||||
|
||||
var self = this;
|
||||
forAllTracksOnStream(s, function(t) {
|
||||
// not currently implemented in chrome
|
||||
t.onstarted = self.onRemoteStreamTrackStarted;
|
||||
});
|
||||
|
||||
// not currently implemented in chrome
|
||||
event.stream.onstarted = this.onRemoteStreamStarted;
|
||||
var player = new Audio();
|
||||
player.src = URL.createObjectURL(s);
|
||||
player.play();
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onRemoteStreamStarted = function(event) {
|
||||
this.state = 'connected';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) {
|
||||
this.state = 'connected';
|
||||
};
|
||||
|
||||
MatrixCall.prototype.onHangupReceived = function() {
|
||||
this.state = 'ended';
|
||||
|
||||
if (this.localAVStream) {
|
||||
forAllTracksOnStream(this.localAVStream, function(t) {
|
||||
t.stop();
|
||||
});
|
||||
}
|
||||
if (this.remoteAVStream) {
|
||||
forAllTracksOnStream(this.remoteAVStream, function(t) {
|
||||
t.stop();
|
||||
});
|
||||
}
|
||||
|
||||
this.onHangup();
|
||||
};
|
||||
|
||||
return MatrixCall;
|
||||
}]);
|
||||
68
webclient/components/matrix/matrix-phone-service.js
Normal file
68
webclient/components/matrix/matrix-phone-service.js
Normal file
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('matrixPhoneService', [])
|
||||
.factory('matrixPhoneService', ['$rootScope', '$injector', 'matrixService', 'eventHandlerService', function MatrixPhoneService($rootScope, $injector, matrixService, eventHandlerService) {
|
||||
var matrixPhoneService = function() {
|
||||
};
|
||||
|
||||
matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT";
|
||||
matrixPhoneService.allCalls = {};
|
||||
|
||||
matrixPhoneService.callPlaced = function(call) {
|
||||
matrixPhoneService.allCalls[call.call_id] = call;
|
||||
};
|
||||
|
||||
$rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
|
||||
if (!isLive) return; // until matrix supports expiring messages
|
||||
if (event.user_id == matrixService.config().user_id) return;
|
||||
var msg = event.content;
|
||||
if (event.type == 'm.call.invite') {
|
||||
var MatrixCall = $injector.get('MatrixCall');
|
||||
var call = new MatrixCall(event.room_id);
|
||||
call.call_id = msg.call_id;
|
||||
call.initWithInvite(msg);
|
||||
matrixPhoneService.allCalls[call.call_id] = call;
|
||||
$rootScope.$broadcast(matrixPhoneService.INCOMING_CALL_EVENT, call);
|
||||
} else if (event.type == 'm.call.answer') {
|
||||
var call = matrixPhoneService.allCalls[msg.call_id];
|
||||
if (!call) {
|
||||
console.trace("Got answer for unknown call ID "+msg.call_id);
|
||||
return;
|
||||
}
|
||||
call.receivedAnswer(msg);
|
||||
} else if (event.type == 'm.call.candidate') {
|
||||
var call = matrixPhoneService.allCalls[msg.call_id];
|
||||
if (!call) {
|
||||
console.trace("Got candidate for unknown call ID "+msg.call_id);
|
||||
return;
|
||||
}
|
||||
call.gotRemoteIceCandidate(msg.candidate);
|
||||
} else if (event.type == 'm.call.hangup') {
|
||||
var call = matrixPhoneService.allCalls[msg.call_id];
|
||||
if (!call) {
|
||||
console.trace("Got hangup for unknown call ID "+msg.call_id);
|
||||
return;
|
||||
}
|
||||
call.onHangupReceived();
|
||||
matrixPhoneService.allCalls[msg.call_id] = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
return matrixPhoneService;
|
||||
}]);
|
||||
@@ -41,7 +41,7 @@ angular.module('matrixService', [])
|
||||
var prefixPath = "/matrix/client/api/v1";
|
||||
var MAPPING_PREFIX = "alias_for_";
|
||||
|
||||
var doRequest = function(method, path, params, data) {
|
||||
var doRequest = function(method, path, params, data, $httpParams) {
|
||||
if (!config) {
|
||||
console.warn("No config exists. Cannot perform request to "+path);
|
||||
return;
|
||||
@@ -58,7 +58,7 @@ angular.module('matrixService', [])
|
||||
path = prefixPath + path;
|
||||
}
|
||||
|
||||
return doBaseRequest(config.homeserver, method, path, params, data, undefined);
|
||||
return doBaseRequest(config.homeserver, method, path, params, data, undefined, $httpParams);
|
||||
};
|
||||
|
||||
var doBaseRequest = function(baseUrl, method, path, params, data, headers, $httpParams) {
|
||||
@@ -97,7 +97,7 @@ angular.module('matrixService', [])
|
||||
// Create a room
|
||||
create: function(room_id, visibility) {
|
||||
// The REST path spec
|
||||
var path = "/rooms";
|
||||
var path = "/createRoom";
|
||||
|
||||
return doRequest("POST", path, undefined, {
|
||||
visibility: visibility,
|
||||
@@ -106,28 +106,25 @@ angular.module('matrixService', [])
|
||||
},
|
||||
|
||||
// List all rooms joined or been invited to
|
||||
rooms: function(from, to, limit) {
|
||||
rooms: function(limit, feedback) {
|
||||
// The REST path spec
|
||||
var path = "/im/sync";
|
||||
|
||||
return doRequest("GET", path);
|
||||
var path = "/initialSync";
|
||||
|
||||
var params = {};
|
||||
if (limit) {
|
||||
params.limit = limit;
|
||||
}
|
||||
if (feedback) {
|
||||
params.feedback = feedback;
|
||||
}
|
||||
|
||||
return doRequest("GET", path, params);
|
||||
},
|
||||
|
||||
// Joins a room
|
||||
join: function(room_id) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/members/$user_id/state";
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$user_id", config.user_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, {
|
||||
membership: "join"
|
||||
});
|
||||
return this.membershipChange(room_id, undefined, "join");
|
||||
},
|
||||
|
||||
joinAlias: function(room_alias) {
|
||||
@@ -136,44 +133,38 @@ angular.module('matrixService', [])
|
||||
|
||||
path = path.replace("$room_alias", room_alias);
|
||||
|
||||
return doRequest("PUT", path, undefined, {});
|
||||
// TODO: PUT with txn ID
|
||||
return doRequest("POST", path, undefined, {});
|
||||
},
|
||||
|
||||
// Invite a user to a room
|
||||
invite: function(room_id, user_id) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/members/$user_id/state";
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$user_id", user_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, {
|
||||
membership: "invite"
|
||||
});
|
||||
return this.membershipChange(room_id, user_id, "invite");
|
||||
},
|
||||
|
||||
// Leaves a room
|
||||
leave: function(room_id) {
|
||||
return this.membershipChange(room_id, undefined, "leave");
|
||||
},
|
||||
|
||||
membershipChange: function(room_id, user_id, membershipValue) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/members/$user_id/state";
|
||||
var path = "/rooms/$room_id/$membership";
|
||||
path = path.replace("$room_id", encodeURIComponent(room_id));
|
||||
path = path.replace("$membership", encodeURIComponent(membershipValue));
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
var data = {};
|
||||
if (user_id !== undefined) {
|
||||
data = { user_id: user_id };
|
||||
}
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$user_id", config.user_id);
|
||||
|
||||
return doRequest("DELETE", path, undefined, undefined);
|
||||
// TODO: Use PUT with transaction IDs
|
||||
return doRequest("POST", path, undefined, data);
|
||||
},
|
||||
|
||||
// Retrieves the room ID corresponding to a room alias
|
||||
resolveRoomAlias:function(room_alias) {
|
||||
var path = "/matrix/client/api/v1/ds/room/$room_alias";
|
||||
var path = "/matrix/client/api/v1/directory/room/$room_alias";
|
||||
room_alias = encodeURIComponent(room_alias);
|
||||
|
||||
path = path.replace("$room_alias", room_alias);
|
||||
@@ -181,12 +172,12 @@ angular.module('matrixService', [])
|
||||
return doRequest("GET", path, undefined, {});
|
||||
},
|
||||
|
||||
sendMessage: function(room_id, msg_id, content) {
|
||||
sendEvent: function(room_id, eventType, txn_id, content) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/messages/$from/$msg_id";
|
||||
var path = "/rooms/$room_id/send/"+eventType+"/$txn_id";
|
||||
|
||||
if (!msg_id) {
|
||||
msg_id = "m" + new Date().getTime();
|
||||
if (!txn_id) {
|
||||
txn_id = "m" + new Date().getTime();
|
||||
}
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
@@ -194,12 +185,15 @@ angular.module('matrixService', [])
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$from", config.user_id);
|
||||
path = path.replace("$msg_id", msg_id);
|
||||
path = path.replace("$txn_id", txn_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, content);
|
||||
},
|
||||
|
||||
sendMessage: function(room_id, txn_id, content) {
|
||||
return this.sendEvent(room_id, 'm.room.message', txn_id, content);
|
||||
},
|
||||
|
||||
// Send a text message
|
||||
sendTextMessage: function(room_id, body, msg_id) {
|
||||
var content = {
|
||||
@@ -236,13 +230,13 @@ angular.module('matrixService', [])
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
|
||||
var path = "/rooms/$room_id/members/list";
|
||||
var path = "/rooms/$room_id/members";
|
||||
path = path.replace("$room_id", room_id);
|
||||
return doRequest("GET", path);
|
||||
},
|
||||
|
||||
paginateBackMessages: function(room_id, from_token, limit) {
|
||||
var path = "/rooms/$room_id/messages/list";
|
||||
var path = "/rooms/$room_id/messages";
|
||||
path = path.replace("$room_id", room_id);
|
||||
var params = {
|
||||
from: from_token,
|
||||
@@ -254,7 +248,7 @@ angular.module('matrixService', [])
|
||||
|
||||
// get a list of public rooms on your home server
|
||||
publicRooms: function() {
|
||||
var path = "/public/rooms"
|
||||
var path = "/publicRooms"
|
||||
return doRequest("GET", path);
|
||||
},
|
||||
|
||||
@@ -353,15 +347,31 @@ angular.module('matrixService', [])
|
||||
|
||||
return doBaseRequest(config.homeserver, "POST", path, params, file, headers, $httpParams);
|
||||
},
|
||||
|
||||
// start listening on /events
|
||||
getEventStream: function(from, timeout) {
|
||||
|
||||
/**
|
||||
* Start listening on /events
|
||||
* @param {String} from the token from which to listen events to
|
||||
* @param {Integer} serverTimeout the time in ms the server will hold open the connection
|
||||
* @param {Integer} clientTimeout the timeout in ms used at the client HTTP request level
|
||||
* @returns a promise
|
||||
*/
|
||||
getEventStream: function(from, serverTimeout, clientTimeout) {
|
||||
var path = "/events";
|
||||
var params = {
|
||||
from: from,
|
||||
timeout: timeout
|
||||
timeout: serverTimeout
|
||||
};
|
||||
return doRequest("GET", path, params);
|
||||
|
||||
var $httpParams;
|
||||
if (clientTimeout) {
|
||||
// If the Internet connection is lost, this timeout is used to be able to
|
||||
// cancel the current request and notify the client so that it can retry with a new request.
|
||||
$httpParams = {
|
||||
timeout: clientTimeout
|
||||
};
|
||||
}
|
||||
|
||||
return doRequest("GET", path, params, undefined, $httpParams);
|
||||
},
|
||||
|
||||
// Indicates if user authentications details are stored in cache
|
||||
@@ -377,6 +387,23 @@ angular.module('matrixService', [])
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Enum of presence state
|
||||
presence: {
|
||||
offline: "offline",
|
||||
unavailable: "unavailable",
|
||||
online: "online",
|
||||
free_for_chat: "free_for_chat"
|
||||
},
|
||||
|
||||
// Set the logged in user presence state
|
||||
setUserPresence: function(presence) {
|
||||
var path = "/presence/$user_id/status";
|
||||
path = path.replace("$user_id", config.user_id);
|
||||
return doRequest("PUT", path, undefined, {
|
||||
state: presence
|
||||
});
|
||||
},
|
||||
|
||||
/****** Permanent storage of user information ******/
|
||||
|
||||
@@ -408,6 +435,44 @@ angular.module('matrixService', [])
|
||||
config.version = configVersion;
|
||||
localStorage.setItem("config", JSON.stringify(config));
|
||||
},
|
||||
|
||||
|
||||
/****** Room aliases management ******/
|
||||
|
||||
/**
|
||||
* Get the room_alias & room_display_name which are computed from data
|
||||
* already retrieved from the server.
|
||||
* @param {Room object} room one element of the array returned by the response
|
||||
* of rooms() and publicRooms()
|
||||
* @returns {Object} {room_alias: "...", room_display_name: "..."}
|
||||
*/
|
||||
getRoomAliasAndDisplayName: function(room) {
|
||||
var result = {
|
||||
room_alias: undefined,
|
||||
room_display_name: undefined
|
||||
};
|
||||
|
||||
var alias = this.getRoomIdToAliasMapping(room.room_id);
|
||||
if (alias) {
|
||||
// use the existing alias from storage
|
||||
result.room_alias = alias;
|
||||
result.room_display_name = alias;
|
||||
}
|
||||
else if (room.aliases && room.aliases[0]) {
|
||||
// save the mapping
|
||||
// TODO: select the smarter alias from the array
|
||||
this.createRoomIdToAliasMapping(room.room_id, room.aliases[0]);
|
||||
result.room_display_name = room.aliases[0];
|
||||
}
|
||||
else if (room.membership === "invite" && "inviter" in room) {
|
||||
result.room_display_name = room.inviter + "'s room";
|
||||
}
|
||||
else {
|
||||
// last resort use the room id
|
||||
result.room_display_name = room.room_id;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
createRoomIdToAliasMapping: function(roomId, alias) {
|
||||
localStorage.setItem(MAPPING_PREFIX+roomId, alias);
|
||||
|
||||
113
webclient/components/matrix/presence-service.js
Normal file
113
webclient/components/matrix/presence-service.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* This service tracks user activity on the page to determine his presence state.
|
||||
* Any state change will be sent to the Home Server.
|
||||
*/
|
||||
angular.module('mPresence', [])
|
||||
.service('mPresence', ['$timeout', 'matrixService', function ($timeout, matrixService) {
|
||||
|
||||
// Time in ms after that a user is considered as unavailable/away
|
||||
var UNAVAILABLE_TIME = 5 * 60000; // 5 mins
|
||||
|
||||
// The current presence state
|
||||
var state = undefined;
|
||||
|
||||
var self =this;
|
||||
var timer;
|
||||
|
||||
/**
|
||||
* Start listening the user activity to evaluate his presence state.
|
||||
* Any state change will be sent to the Home Server.
|
||||
*/
|
||||
this.start = function() {
|
||||
if (undefined === state) {
|
||||
// The user is online if he moves the mouser or press a key
|
||||
document.onmousemove = resetTimer;
|
||||
document.onkeypress = resetTimer;
|
||||
|
||||
resetTimer();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop tracking user activity
|
||||
*/
|
||||
this.stop = function() {
|
||||
if (timer) {
|
||||
$timeout.cancel(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
state = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current presence state.
|
||||
* @returns {matrixService.presence} the presence state
|
||||
*/
|
||||
this.getState = function() {
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the presence state.
|
||||
* If the state has changed, the Home Server will be notified.
|
||||
* @param {matrixService.presence} newState the new presence state
|
||||
*/
|
||||
this.setState = function(newState) {
|
||||
if (newState !== state) {
|
||||
console.log("mPresence - New state: " + newState);
|
||||
|
||||
state = newState;
|
||||
|
||||
// Inform the HS on the new user state
|
||||
matrixService.setUserPresence(state).then(
|
||||
function() {
|
||||
|
||||
},
|
||||
function(error) {
|
||||
console.log("mPresence - Failed to send new presence state: " + JSON.stringify(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback called when the user made no action on the page for UNAVAILABLE_TIME ms.
|
||||
* @private
|
||||
*/
|
||||
function onUnvailableTimerFire() {
|
||||
self.setState(matrixService.presence.unavailable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the user made an action on the page
|
||||
* @private
|
||||
*/
|
||||
function resetTimer() {
|
||||
// User is still here
|
||||
self.setState(matrixService.presence.online);
|
||||
|
||||
// Re-arm the timer
|
||||
$timeout.cancel(timer);
|
||||
timer = $timeout(onUnvailableTimerFire, UNAVAILABLE_TIME);
|
||||
}
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
113
webclient/home/home-controller.js
Normal file
113
webclient/home/home-controller.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController'])
|
||||
.controller('HomeController', ['$scope', '$location', 'matrixService',
|
||||
function($scope, $location, matrixService) {
|
||||
|
||||
$scope.config = matrixService.config();
|
||||
$scope.public_rooms = [];
|
||||
$scope.newRoomId = "";
|
||||
$scope.feedback = "";
|
||||
|
||||
$scope.newRoom = {
|
||||
room_id: "",
|
||||
private: false
|
||||
};
|
||||
|
||||
$scope.goToRoom = {
|
||||
room_id: ""
|
||||
};
|
||||
|
||||
$scope.joinAlias = {
|
||||
room_alias: ""
|
||||
};
|
||||
|
||||
var refresh = function() {
|
||||
|
||||
matrixService.publicRooms().then(
|
||||
function(response) {
|
||||
$scope.public_rooms = response.data.chunk;
|
||||
for (var i = 0; i < $scope.public_rooms.length; i++) {
|
||||
var room = $scope.public_rooms[i];
|
||||
|
||||
// Add room_alias & room_display_name members
|
||||
angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.createNewRoom = function(room_id, isPrivate) {
|
||||
|
||||
var visibility = "public";
|
||||
if (isPrivate) {
|
||||
visibility = "private";
|
||||
}
|
||||
|
||||
matrixService.create(room_id, visibility).then(
|
||||
function(response) {
|
||||
// This room has been created. Refresh the rooms list
|
||||
console.log("Created room " + response.data.room_alias + " with id: "+
|
||||
response.data.room_id);
|
||||
matrixService.createRoomIdToAliasMapping(
|
||||
response.data.room_id, response.data.room_alias);
|
||||
refresh();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
});
|
||||
};
|
||||
|
||||
// Go to a room
|
||||
$scope.goToRoom = function(room_id) {
|
||||
// Simply open the room page on this room id
|
||||
//$location.url("room/" + room_id);
|
||||
matrixService.join(room_id).then(
|
||||
function(response) {
|
||||
if (response.data.hasOwnProperty("room_id")) {
|
||||
if (response.data.room_id != room_id) {
|
||||
$location.url("room/" + response.data.room_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$location.url("room/" + room_id);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't join room: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.joinAlias = function(room_alias) {
|
||||
matrixService.joinAlias(room_alias).then(
|
||||
function(response) {
|
||||
// Go to this room
|
||||
$location.url("room/" + room_alias);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't join room: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.onInit = function() {
|
||||
refresh();
|
||||
};
|
||||
}]);
|
||||
58
webclient/home/home.html
Normal file
58
webclient/home/home.html
Normal file
@@ -0,0 +1,58 @@
|
||||
<div ng-controller="HomeController" data-ng-init="onInit()">
|
||||
|
||||
<div id="page">
|
||||
<div id="wrapper">
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="profile-avatar">
|
||||
<img ng-src="{{ config.avatarUrl || 'img/default-profile.jpg' }}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="user-ids">
|
||||
<div id="user-displayname">{{ config.displayName }}</div>
|
||||
<div>{{ config.user_id }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h3>Recents</h3>
|
||||
<div ng-include="'recents/recents.html'"></div>
|
||||
<br/>
|
||||
|
||||
<h3>Public rooms</h3>
|
||||
|
||||
<div class="public_rooms" ng-repeat="room in public_rooms">
|
||||
<div>
|
||||
<a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/>
|
||||
<input type="checkbox" ng-model="newRoom.private">private
|
||||
<button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo_channel:example.org)"/>
|
||||
<button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
{{ feedback }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2,10 +2,12 @@
|
||||
<html xmlns:ng="http://angularjs.org" ng-app="matrixWebClient" ng-controller="MatrixWebClientController">
|
||||
<head>
|
||||
<title>[matrix]</title>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<link rel="icon" href="favicon.ico">
|
||||
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<script type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
|
||||
<script src="js/angular.min.js"></script>
|
||||
<script src="js/angular-route.min.js"></script>
|
||||
@@ -15,14 +17,20 @@
|
||||
<script src="app-controller.js"></script>
|
||||
<script src="app-directive.js"></script>
|
||||
<script src="app-filter.js"></script>
|
||||
<script src="home/home-controller.js"></script>
|
||||
<script src="login/login-controller.js"></script>
|
||||
<script src="recents/recents-controller.js"></script>
|
||||
<script src="recents/recents-filter.js"></script>
|
||||
<script src="room/room-controller.js"></script>
|
||||
<script src="room/room-directive.js"></script>
|
||||
<script src="rooms/rooms-controller.js"></script>
|
||||
<script src="settings/settings-controller.js"></script>
|
||||
<script src="user/user-controller.js"></script>
|
||||
<script src="components/matrix/matrix-service.js"></script>
|
||||
<script src="components/matrix/matrix-call.js"></script>
|
||||
<script src="components/matrix/matrix-phone-service.js"></script>
|
||||
<script src="components/matrix/event-stream-service.js"></script>
|
||||
<script src="components/matrix/event-handler-service.js"></script>
|
||||
<script src="components/matrix/presence-service.js"></script>
|
||||
<script src="components/fileInput/file-input-directive.js"></script>
|
||||
<script src="components/fileUpload/file-upload-service.js"></script>
|
||||
<script src="components/utilities/utilities-service.js"></script>
|
||||
@@ -33,22 +41,11 @@
|
||||
<header id="header">
|
||||
<!-- Do not show buttons on the login page -->
|
||||
<div id="header-buttons" ng-hide="'/login' == location ">
|
||||
<button ng-click="showConfig()">Config</button>
|
||||
<button ng-click='goToPage("settings")'>Settings</button>
|
||||
<button ng-click="logout()">Log out</button>
|
||||
</div>
|
||||
|
||||
<h1>[matrix]</h1>
|
||||
</header>
|
||||
|
||||
<div id="config" ng-hide="!config">
|
||||
<div>Home server: {{ config.homeserver }} </div>
|
||||
<div>User ID: {{ config.user_id }} </div>
|
||||
<div>Access token: {{ config.access_token }} </div>
|
||||
<div><button ng-click="requestNotifications()">Request notifications</button></div>
|
||||
<div><button ng-click="closeConfig()">Close</button></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</body>
|
||||
|
||||
@@ -53,7 +53,7 @@ angular.module('LoginController', ['matrixService'])
|
||||
matrixService.saveConfig();
|
||||
eventStreamService.resume();
|
||||
// Go to the user's rooms list page
|
||||
$location.url("rooms");
|
||||
$location.url("home");
|
||||
},
|
||||
function(error) {
|
||||
if (error.data) {
|
||||
@@ -86,7 +86,7 @@ angular.module('LoginController', ['matrixService'])
|
||||
});
|
||||
matrixService.saveConfig();
|
||||
eventStreamService.resume();
|
||||
$location.url("rooms");
|
||||
$location.url("home");
|
||||
}
|
||||
else {
|
||||
$scope.feedback = "Failed to login: " + JSON.stringify(response.data);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div ng-controller="LoginController" class="login">
|
||||
<div ng-controller="LoginController" class="login">
|
||||
<h1 id="logo">[matrix]</h1>
|
||||
|
||||
<div id="page">
|
||||
<div id="wrapper">
|
||||
|
||||
|
||||
90
webclient/recents/recents-controller.js
Normal file
90
webclient/recents/recents-controller.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
|
||||
.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService',
|
||||
function($scope, matrixService, eventHandlerService) {
|
||||
$scope.rooms = {};
|
||||
|
||||
// $scope of the parent where the recents component is included can override this value
|
||||
// in order to highlight a specific room in the list
|
||||
$scope.recentsSelectedRoomID;
|
||||
|
||||
var listenToEventStream = function() {
|
||||
// Refresh the list on matrix invitation and message event
|
||||
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
||||
var config = matrixService.config();
|
||||
if (isLive && event.state_key === config.user_id && event.content.membership === "invite") {
|
||||
console.log("Invited to room " + event.room_id);
|
||||
// FIXME push membership to top level key to match /im/sync
|
||||
event.membership = event.content.membership;
|
||||
// FIXME bodge a nicer name than the room ID for this invite.
|
||||
event.room_display_name = event.user_id + "'s room";
|
||||
$scope.rooms[event.room_id] = event;
|
||||
}
|
||||
});
|
||||
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
|
||||
if (isLive) {
|
||||
$scope.rooms[event.room_id].lastMsg = event;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
var refresh = function() {
|
||||
// List all rooms joined or been invited to
|
||||
// TODO: This is a pity that event-stream-service.js makes the same call
|
||||
// We should be able to reuse event-stream-service.js fetched data
|
||||
matrixService.rooms(1, false).then(
|
||||
function(response) {
|
||||
// Reset data
|
||||
$scope.rooms = {};
|
||||
|
||||
var rooms = response.data.rooms;
|
||||
for (var i=0; i<rooms.length; i++) {
|
||||
var room = rooms[i];
|
||||
|
||||
// Add room_alias & room_display_name members
|
||||
$scope.rooms[room.room_id] = angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
|
||||
|
||||
// Create a shortcut for the last message of this room
|
||||
if (room.messages && room.messages.chunk && room.messages.chunk[0]) {
|
||||
$scope.rooms[room.room_id].lastMsg = room.messages.chunk[0];
|
||||
}
|
||||
}
|
||||
|
||||
var presence = response.data.presence;
|
||||
for (var i = 0; i < presence.length; ++i) {
|
||||
eventHandlerService.handleEvent(presence[i], false);
|
||||
}
|
||||
|
||||
// From now, update recents from the stream
|
||||
listenToEventStream();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.onInit = function() {
|
||||
refresh();
|
||||
};
|
||||
|
||||
}]);
|
||||
|
||||
47
webclient/recents/recents-filter.js
Normal file
47
webclient/recents/recents-filter.js
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('RecentsController')
|
||||
.filter('orderRecents', function() {
|
||||
return function(rooms) {
|
||||
|
||||
// Transform the dict into an array
|
||||
// The key, room_id, is already in value objects
|
||||
var filtered = [];
|
||||
angular.forEach(rooms, function(value, key) {
|
||||
filtered.push( value );
|
||||
});
|
||||
|
||||
// And time sort them
|
||||
// The room with the lastest message at first
|
||||
filtered.sort(function (a, b) {
|
||||
// Invite message does not have a body message nor ts
|
||||
// Puth them at the top of the list
|
||||
if (undefined === a.lastMsg) {
|
||||
return -1;
|
||||
}
|
||||
else if (undefined === b.lastMsg) {
|
||||
return 1;
|
||||
}
|
||||
else {
|
||||
return b.lastMsg.ts - a.lastMsg.ts;
|
||||
}
|
||||
});
|
||||
return filtered;
|
||||
};
|
||||
});
|
||||
61
webclient/recents/recents.html
Normal file
61
webclient/recents/recents.html
Normal file
@@ -0,0 +1,61 @@
|
||||
<div ng-controller="RecentsController" data-ng-init="onInit()">
|
||||
<table class="recentsTable">
|
||||
<tbody ng-repeat="(rm_id, room) in rooms | orderRecents"
|
||||
ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )"
|
||||
class ="recentsRoom"
|
||||
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
|
||||
<tr>
|
||||
<td class="recentsRoomName">
|
||||
{{ room.room_display_name }}
|
||||
</td>
|
||||
<td class="recentsRoomSummaryTS">
|
||||
{{ (room.lastMsg.ts) | date:'MMM d HH:mm' }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td colspan="2" class="recentsRoomSummary">
|
||||
|
||||
<div ng-show="room.membership === 'invite'" >
|
||||
{{ room.inviter }} invited you
|
||||
</div>
|
||||
|
||||
<div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type" >
|
||||
<div ng-switch-when="m.room.member">
|
||||
{{ room.lastMsg.user_id }}
|
||||
{{ {"join": "joined", "leave": "left", "invite": "invited"}[room.lastMsg.content.membership] }}
|
||||
{{ room.lastMsg.content.membership === "invite" ? (room.lastMsg.state_key || '') : '' }}
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="m.room.message">
|
||||
<div ng-switch="room.lastMsg.content.msgtype">
|
||||
<div ng-switch-when="m.text">
|
||||
{{ room.lastMsg.user_id }} :
|
||||
<span ng-bind-html="(room.lastMsg.content.body) | linky:'_blank'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="m.image">
|
||||
{{ room.lastMsg.user_id }} sent an image
|
||||
</div>
|
||||
|
||||
<div ng-switch-when="m.emote">
|
||||
<span ng-bind-html="'* ' + (room.lastMsg.user_id) + ' ' + room.lastMsg.content.body | linky:'_blank'">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div ng-switch-default>
|
||||
{{ room.lastMsg.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-switch-default>
|
||||
{{ room.lastMsg }}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'mFileUpload', 'mUtilities', '$rootScope',
|
||||
function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, mFileUpload, mUtilities, $rootScope) {
|
||||
angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
||||
.controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mPresence', 'matrixPhoneService', 'MatrixCall',
|
||||
function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, mPresence, matrixPhoneService, MatrixCall) {
|
||||
'use strict';
|
||||
var MESSAGES_PER_PAGINATION = 30;
|
||||
var THUMBNAIL_SIZE = 320;
|
||||
@@ -51,21 +51,20 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
objDiv.scrollTop = objDiv.scrollHeight;
|
||||
}, 0);
|
||||
};
|
||||
|
||||
|
||||
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
|
||||
if (isLive && event.room_id === $scope.room_id) {
|
||||
scrollToBottom();
|
||||
|
||||
if (window.Notification) {
|
||||
// FIXME: we should also notify based on a timer or other heuristics
|
||||
// rather than the window being minimised
|
||||
if (document.hidden) {
|
||||
// Show notification when the user is idle
|
||||
if (matrixService.presence.offline === mPresence.getState()) {
|
||||
var notification = new window.Notification(
|
||||
($scope.members[event.user_id].displayname || event.user_id) +
|
||||
" (" + ($scope.room_alias || $scope.room_id) + ")", // FIXME: don't leak room_ids here
|
||||
{
|
||||
"body": event.content.body,
|
||||
"icon": $scope.members[event.user_id].avatar_url,
|
||||
"icon": $scope.members[event.user_id].avatar_url
|
||||
});
|
||||
$timeout(function() {
|
||||
notification.close();
|
||||
@@ -82,6 +81,17 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
$scope.$on(eventHandlerService.PRESENCE_EVENT, function(ngEvent, event, isLive) {
|
||||
updatePresence(event);
|
||||
});
|
||||
|
||||
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
|
||||
console.trace("incoming call");
|
||||
call.onError = $scope.onCallError;
|
||||
call.onHangup = $scope.onCallHangup;
|
||||
$scope.currentCall = call;
|
||||
});
|
||||
|
||||
$scope.memberCount = function() {
|
||||
return Object.keys($scope.members).length;
|
||||
};
|
||||
|
||||
$scope.paginateMore = function() {
|
||||
if ($scope.state.can_paginate) {
|
||||
@@ -89,6 +99,15 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
paginate(MESSAGES_PER_PAGINATION);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.answerCall = function() {
|
||||
$scope.currentCall.answer();
|
||||
};
|
||||
|
||||
$scope.hangupCall = function() {
|
||||
$scope.currentCall.hangup();
|
||||
$scope.currentCall = undefined;
|
||||
};
|
||||
|
||||
var paginate = function(numItems) {
|
||||
// console.log("paginate " + numItems);
|
||||
@@ -154,7 +173,10 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
var updateMemberList = function(chunk) {
|
||||
if (chunk.room_id != $scope.room_id) return;
|
||||
|
||||
var isNewMember = !(chunk.target_user_id in $scope.members);
|
||||
// set target_user_id to keep things clear
|
||||
var target_user_id = chunk.state_key;
|
||||
|
||||
var isNewMember = !(target_user_id in $scope.members);
|
||||
if (isNewMember) {
|
||||
// FIXME: why are we copying these fields around inside chunk?
|
||||
if ("state" in chunk.content) {
|
||||
@@ -172,7 +194,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
if ("avatar_url" in chunk.content) {
|
||||
chunk.avatar_url = chunk.content.avatar_url;
|
||||
}
|
||||
$scope.members[chunk.target_user_id] = chunk;
|
||||
$scope.members[target_user_id] = chunk;
|
||||
|
||||
/*
|
||||
// Stale code for explicitly hammering the homeserver for every displayname & avatar_url
|
||||
@@ -202,16 +224,16 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
});
|
||||
*/
|
||||
|
||||
if (chunk.target_user_id in $rootScope.presence) {
|
||||
updatePresence($rootScope.presence[chunk.target_user_id]);
|
||||
if (target_user_id in $rootScope.presence) {
|
||||
updatePresence($rootScope.presence[target_user_id]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// selectively update membership else it will nuke the picture and displayname too :/
|
||||
var member = $scope.members[chunk.target_user_id];
|
||||
var member = $scope.members[target_user_id];
|
||||
member.content.membership = chunk.content.membership;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var updatePresence = function(chunk) {
|
||||
if (!(chunk.content.user_id in $scope.members)) {
|
||||
@@ -238,10 +260,10 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
if ("avatar_url" in chunk.content) {
|
||||
member.avatar_url = chunk.content.avatar_url;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.send = function() {
|
||||
if ($scope.textInput == "") {
|
||||
if ($scope.textInput === "") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,7 +272,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
// Send the text message
|
||||
var promise;
|
||||
// FIXME: handle other commands too
|
||||
if ($scope.textInput.indexOf("/me") == 0) {
|
||||
if ($scope.textInput.indexOf("/me") === 0) {
|
||||
promise = matrixService.sendEmoteMessage($scope.room_id, $scope.textInput.substr(4));
|
||||
}
|
||||
else {
|
||||
@@ -279,7 +301,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
}
|
||||
|
||||
if (room_id_or_alias && '!' === room_id_or_alias[0]) {
|
||||
// Yes. We can start right now
|
||||
// Yes. We can go on right now
|
||||
$scope.room_id = room_id_or_alias;
|
||||
$scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
|
||||
onInit2();
|
||||
@@ -310,7 +332,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
$scope.room_id = response.data.room_id;
|
||||
console.log(" -> Room ID: " + $scope.room_id);
|
||||
|
||||
// Now, we can start
|
||||
// Now, we can go on
|
||||
onInit2();
|
||||
},
|
||||
function () {
|
||||
@@ -320,34 +342,61 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
var onInit2 = function() {
|
||||
eventHandlerService.reInitRoom($scope.room_id);
|
||||
|
||||
// Join the room
|
||||
matrixService.join($scope.room_id).then(
|
||||
console.log("onInit2");
|
||||
|
||||
// Make sure the initialSync has been before going further
|
||||
eventHandlerService.waitForInitialSyncCompletion().then(
|
||||
function() {
|
||||
console.log("Joined room "+$scope.room_id);
|
||||
|
||||
// Get the current member list
|
||||
matrixService.getMemberList($scope.room_id).then(
|
||||
function(response) {
|
||||
for (var i = 0; i < response.data.chunk.length; i++) {
|
||||
var chunk = response.data.chunk[i];
|
||||
updateMemberList(chunk);
|
||||
}
|
||||
eventStreamService.resume();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failed get member list: " + error.data.error;
|
||||
}
|
||||
);
|
||||
var needsToJoin = true;
|
||||
|
||||
paginate(MESSAGES_PER_PAGINATION);
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Can't join room: " + reason;
|
||||
});
|
||||
// The room members is available in the data fetched by initialSync
|
||||
if ($rootScope.events.rooms[$scope.room_id]) {
|
||||
var members = $rootScope.events.rooms[$scope.room_id].members;
|
||||
|
||||
// Update the member list
|
||||
for (var i in members) {
|
||||
var member = members[i];
|
||||
updateMemberList(member);
|
||||
}
|
||||
|
||||
// Check if the user has already join the room
|
||||
if ($scope.state.user_id in members) {
|
||||
if ("join" === members[$scope.state.user_id].membership) {
|
||||
needsToJoin = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Do we to join the room before starting?
|
||||
if (needsToJoin) {
|
||||
matrixService.join($scope.room_id).then(
|
||||
function() {
|
||||
console.log("Joined room "+$scope.room_id);
|
||||
onInit3();
|
||||
},
|
||||
function(reason) {
|
||||
$scope.feedback = "Can't join room: " + reason;
|
||||
});
|
||||
}
|
||||
else {
|
||||
onInit3();
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var onInit3 = function() {
|
||||
console.log("onInit3");
|
||||
|
||||
// TODO: We should be able to keep them
|
||||
eventHandlerService.resetRoomMessages($scope.room_id);
|
||||
|
||||
// Make recents highlight the current room
|
||||
$scope.recentsSelectedRoomID = $scope.room_id;
|
||||
|
||||
paginate(MESSAGES_PER_PAGINATION);
|
||||
};
|
||||
|
||||
$scope.inviteUser = function(user_id) {
|
||||
@@ -372,7 +421,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
matrixService.leave($scope.room_id).then(
|
||||
function(response) {
|
||||
console.log("Left room ");
|
||||
$location.url("rooms");
|
||||
$location.url("home");
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failed to leave room: " + error.data.error;
|
||||
@@ -424,4 +473,21 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
$scope.loadMoreHistory = function() {
|
||||
paginate(MESSAGES_PER_PAGINATION);
|
||||
};
|
||||
|
||||
$scope.startVoiceCall = function() {
|
||||
var call = new MatrixCall($scope.room_id);
|
||||
call.onError = $scope.onCallError;
|
||||
call.onHangup = $scope.onCallHangup;
|
||||
call.placeCall();
|
||||
$scope.currentCall = call;
|
||||
}
|
||||
|
||||
$scope.onCallError = function(errStr) {
|
||||
$scope.feedback = errStr;
|
||||
}
|
||||
|
||||
$scope.onCallHangup = function() {
|
||||
$scope.feedback = "Call ended";
|
||||
$scope.currentCall = undefined;
|
||||
}
|
||||
}]);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<div ng-controller="RoomController" data-ng-init="onInit()" class="room">
|
||||
<h1 id="roomLogo">[matrix]</h1>
|
||||
|
||||
<div id="page">
|
||||
<div id="wrapper">
|
||||
@@ -6,7 +7,11 @@
|
||||
<div id="roomName">
|
||||
{{ room_alias || room_id }}
|
||||
</div>
|
||||
|
||||
|
||||
<div id="roomRecentsTableWrapper">
|
||||
<div ng-include="'recents/recents.html'"></div>
|
||||
</div>
|
||||
|
||||
<div id="usersTableWrapper">
|
||||
<table id="usersTable">
|
||||
<tr ng-repeat="member in members | orderMembersList">
|
||||
@@ -32,7 +37,7 @@
|
||||
ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
|
||||
<td class="leftBlock">
|
||||
<div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
|
||||
<div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm:ss' }}</div>
|
||||
<div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}</div>
|
||||
</td>
|
||||
<td class="avatar">
|
||||
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
|
||||
@@ -40,13 +45,13 @@
|
||||
</td>
|
||||
<td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
|
||||
<div class="bubble">
|
||||
<span ng-hide='msg.type !== "m.room.member"'>
|
||||
<span ng-show='msg.type === "m.room.member"'>
|
||||
{{ members[msg.user_id].displayname || msg.user_id }}
|
||||
{{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }}
|
||||
{{ msg.content.target_id || '' }}
|
||||
{{ msg.content.membership === "invite" ? (msg.state_key || '') : '' }}
|
||||
</span>
|
||||
<span ng-hide='msg.content.msgtype !== "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
|
||||
<span ng-hide='msg.content.msgtype !== "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
|
||||
<span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
|
||||
<span ng-show='msg.content.msgtype === "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
|
||||
<div ng-show='msg.content.msgtype === "m.image"'>
|
||||
<div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
|
||||
<img class="image" ng-src="{{ msg.content.url }}"/>
|
||||
@@ -73,31 +78,38 @@
|
||||
<div id="controls">
|
||||
<table id="inputBarTable">
|
||||
<tr>
|
||||
<td width="1">
|
||||
<td id="userIdCell" width="1px">
|
||||
{{ state.user_id }}
|
||||
</td>
|
||||
<td width="*" style="min-width: 100px">
|
||||
<td width="*">
|
||||
<input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" autocomplete="off" tab-complete/>
|
||||
</td>
|
||||
<td width="150px">
|
||||
<td id="buttonsCell">
|
||||
<button ng-click="send()">Send</button>
|
||||
<button m-file-input="imageFileToSend">Send Image</button>
|
||||
</td>
|
||||
<td width="1">
|
||||
|
||||
<button m-file-input="imageFileToSend">Image</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<span>
|
||||
Invite a user:
|
||||
<input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>
|
||||
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
|
||||
</span>
|
||||
<button ng-click="leaveRoom()">Leave</button>
|
||||
<button ng-click="loadMoreHistory()" ng-disabled="!state.can_paginate">Load more history</button>
|
||||
<div id="extraControls">
|
||||
<span>
|
||||
Invite a user:
|
||||
<input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>
|
||||
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
|
||||
</span>
|
||||
<button ng-click="leaveRoom()">Leave</button>
|
||||
<button ng-click="startVoiceCall()" ng-show="currentCall == undefined && memberCount() == 2">Voice Call</button>
|
||||
<div ng-show="currentCall.state == 'ringing'">
|
||||
Incoming call from {{ currentCall.user_id }}
|
||||
<button ng-click="answerCall()">Answer</button>
|
||||
<button ng-click="hangupCall()">Reject</button>
|
||||
</div>
|
||||
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing'">Hang up</button>
|
||||
<span style="display: none; ">{{ currentCall.state }}</span>
|
||||
</div>
|
||||
|
||||
{{ feedback }}
|
||||
<div ng-hide="!state.stream_failure">
|
||||
<div ng-show="state.stream_failure">
|
||||
{{ state.stream_failure.data.error || "Connection failure" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,300 +0,0 @@
|
||||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload', 'eventHandlerService'])
|
||||
.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService', 'eventStreamService',
|
||||
function($scope, $location, matrixService, mFileUpload, eventHandlerService, eventStreamService) {
|
||||
|
||||
$scope.rooms = {};
|
||||
$scope.public_rooms = [];
|
||||
$scope.newRoomId = "";
|
||||
$scope.feedback = "";
|
||||
|
||||
$scope.newRoom = {
|
||||
room_id: "",
|
||||
private: false
|
||||
};
|
||||
|
||||
$scope.goToRoom = {
|
||||
room_id: "",
|
||||
};
|
||||
|
||||
$scope.joinAlias = {
|
||||
room_alias: "",
|
||||
};
|
||||
|
||||
$scope.newProfileInfo = {
|
||||
name: matrixService.config().displayName,
|
||||
avatar: matrixService.config().avatarUrl,
|
||||
avatarFile: undefined
|
||||
};
|
||||
|
||||
$scope.linkedEmails = {
|
||||
linkNewEmail: "", // the email entry box
|
||||
emailBeingAuthed: undefined, // to populate verification text
|
||||
authTokenId: undefined, // the token id from the IS
|
||||
clientSecret: undefined, // our client secret
|
||||
sendAttempt: 1,
|
||||
emailCode: "", // the code entry box
|
||||
linkedEmailList: matrixService.config().emailList // linked email list
|
||||
};
|
||||
|
||||
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
||||
var config = matrixService.config();
|
||||
if (event.target_user_id === config.user_id && event.content.membership === "invite") {
|
||||
console.log("Invited to room " + event.room_id);
|
||||
// FIXME push membership to top level key to match /im/sync
|
||||
event.membership = event.content.membership;
|
||||
// FIXME bodge a nicer name than the room ID for this invite.
|
||||
event.room_display_name = event.user_id + "'s room";
|
||||
$scope.rooms[event.room_id] = event;
|
||||
}
|
||||
});
|
||||
|
||||
var assignRoomAliases = function(data) {
|
||||
for (var i=0; i<data.length; i++) {
|
||||
var alias = matrixService.getRoomIdToAliasMapping(data[i].room_id);
|
||||
if (alias) {
|
||||
// use the existing alias from storage
|
||||
data[i].room_alias = alias;
|
||||
data[i].room_display_name = alias;
|
||||
}
|
||||
else if (data[i].aliases && data[i].aliases[0]) {
|
||||
// save the mapping
|
||||
// TODO: select the smarter alias from the array
|
||||
matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
|
||||
data[i].room_display_name = data[i].aliases[0];
|
||||
}
|
||||
else if (data[i].membership == "invite" && "inviter" in data[i]) {
|
||||
data[i].room_display_name = data[i].inviter + "'s room"
|
||||
}
|
||||
else {
|
||||
// last resort use the room id
|
||||
data[i].room_display_name = data[i].room_id;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
$scope.refresh = function() {
|
||||
// List all rooms joined or been invited to
|
||||
matrixService.rooms().then(
|
||||
function(response) {
|
||||
var data = assignRoomAliases(response.data.rooms);
|
||||
$scope.feedback = "Success";
|
||||
for (var i=0; i<data.length; i++) {
|
||||
$scope.rooms[data[i].room_id] = data[i];
|
||||
}
|
||||
|
||||
var presence = response.data.presence;
|
||||
for (var i = 0; i < presence.length; ++i) {
|
||||
eventHandlerService.handleEvent(presence[i], false);
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
});
|
||||
|
||||
matrixService.publicRooms().then(
|
||||
function(response) {
|
||||
$scope.public_rooms = assignRoomAliases(response.data.chunk);
|
||||
}
|
||||
);
|
||||
|
||||
eventStreamService.resume();
|
||||
};
|
||||
|
||||
$scope.createNewRoom = function(room_id, isPrivate) {
|
||||
|
||||
var visibility = "public";
|
||||
if (isPrivate) {
|
||||
visibility = "private";
|
||||
}
|
||||
|
||||
matrixService.create(room_id, visibility).then(
|
||||
function(response) {
|
||||
// This room has been created. Refresh the rooms list
|
||||
console.log("Created room " + response.data.room_alias + " with id: "+
|
||||
response.data.room_id);
|
||||
matrixService.createRoomIdToAliasMapping(
|
||||
response.data.room_id, response.data.room_alias);
|
||||
$scope.refresh();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
});
|
||||
};
|
||||
|
||||
// Go to a room
|
||||
$scope.goToRoom = function(room_id) {
|
||||
// Simply open the room page on this room id
|
||||
//$location.url("room/" + room_id);
|
||||
matrixService.join(room_id).then(
|
||||
function(response) {
|
||||
if (response.data.hasOwnProperty("room_id")) {
|
||||
if (response.data.room_id != room_id) {
|
||||
$location.url("room/" + response.data.room_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$location.url("room/" + room_id);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't join room: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.joinAlias = function(room_alias) {
|
||||
matrixService.joinAlias(room_alias).then(
|
||||
function(response) {
|
||||
// Go to this room
|
||||
$location.url("room/" + room_alias);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't join room: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.setDisplayName = function(newName) {
|
||||
matrixService.setDisplayName(newName).then(
|
||||
function(response) {
|
||||
$scope.feedback = "Updated display name.";
|
||||
var config = matrixService.config();
|
||||
config.displayName = newName;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't update display name: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
$scope.$watch("newProfileInfo.avatarFile", function(newValue, oldValue) {
|
||||
if ($scope.newProfileInfo.avatarFile) {
|
||||
console.log("Uploading new avatar file...");
|
||||
mFileUpload.uploadFile($scope.newProfileInfo.avatarFile).then(
|
||||
function(url) {
|
||||
$scope.newProfileInfo.avatar = url;
|
||||
$scope.setAvatar($scope.newProfileInfo.avatar);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't upload image";
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.setAvatar = function(newUrl) {
|
||||
console.log("Updating avatar to "+newUrl);
|
||||
matrixService.setProfilePictureUrl(newUrl).then(
|
||||
function(response) {
|
||||
console.log("Updated avatar");
|
||||
$scope.feedback = "Updated avatar.";
|
||||
var config = matrixService.config();
|
||||
config.avatarUrl = newUrl;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't update avatar: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var generateClientSecret = function() {
|
||||
var ret = "";
|
||||
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
|
||||
for (var i = 0; i < 32; i++) {
|
||||
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
|
||||
return ret;
|
||||
};
|
||||
|
||||
|
||||
$scope.linkEmail = function(email) {
|
||||
if (email != $scope.linkedEmails.emailBeingAuthed) {
|
||||
$scope.linkedEmails.clientSecret = generateClientSecret();
|
||||
$scope.linkedEmails.sendAttempt = 1;
|
||||
}
|
||||
matrixService.linkEmail(email, $scope.linkedEmails.clientSecret, $scope.linkedEmails.sendAttempt).then(
|
||||
function(response) {
|
||||
if (response.data.success === true) {
|
||||
$scope.linkedEmails.authTokenId = response.data.sid;
|
||||
$scope.emailFeedback = "You have been sent an email.";
|
||||
$scope.linkedEmails.emailBeingAuthed = email;
|
||||
}
|
||||
else {
|
||||
$scope.emailFeedback = "Failed to send email.";
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.emailFeedback = "Can't send email: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.submitEmailCode = function(code) {
|
||||
var tokenId = $scope.linkedEmails.authTokenId;
|
||||
if (tokenId === undefined) {
|
||||
$scope.emailFeedback = "You have not requested a code with this email.";
|
||||
return;
|
||||
}
|
||||
matrixService.authEmail(matrixService.config().user_id, tokenId, code, $scope.linkedEmails.clientSecret).then(
|
||||
function(response) {
|
||||
if ("success" in response.data && response.data.success === false) {
|
||||
$scope.emailFeedback = "Failed to authenticate email.";
|
||||
return;
|
||||
}
|
||||
matrixService.bindEmail(matrixService.config().user_id, tokenId, $scope.linkedEmails.clientSecret).then(
|
||||
function(response) {
|
||||
var config = matrixService.config();
|
||||
var emailList = {};
|
||||
if ("emailList" in config) {
|
||||
emailList = config.emailList;
|
||||
}
|
||||
emailList[$scope.linkedEmails.emailBeingAuthed] = response;
|
||||
// save the new email list
|
||||
config.emailList = emailList;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
// invalidate the email being authed and update UI.
|
||||
$scope.linkedEmails.emailBeingAuthed = undefined;
|
||||
$scope.emailFeedback = "";
|
||||
$scope.linkedEmails.linkedEmailList = emailList;
|
||||
$scope.linkedEmails.linkNewEmail = "";
|
||||
$scope.linkedEmails.emailCode = "";
|
||||
}, function(reason) {
|
||||
$scope.emailFeedback = "Failed to link email: " + reason;
|
||||
}
|
||||
);
|
||||
},
|
||||
function(reason) {
|
||||
$scope.emailFeedback = "Failed to auth email: " + reason;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.refresh();
|
||||
}]);
|
||||
@@ -1,101 +0,0 @@
|
||||
<div ng-controller="RoomsController" class="rooms">
|
||||
|
||||
<div id="page">
|
||||
<div id="wrapper">
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="profile-avatar">
|
||||
<img ng-src="{{ newProfileInfo.avatar || 'img/default-profile.jpg' }}" m-file-input="newProfileInfo.avatarFile"/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<!-- TODO: To enable once we have an upload server
|
||||
<button m-file-input="newProfileInfo.avatarFile">Upload new Avatar</button>
|
||||
or use an existing image URL:
|
||||
-->
|
||||
<div>
|
||||
<input size="40" ng-model="newProfileInfo.avatar" ng-enter="setAvatar(newProfileInfo.avatar)" placeholder="Image URL"/>
|
||||
<button ng-disabled="!newProfileInfo.avatar" ng-click="setAvatar(newProfileInfo.avatar)">Update Avatar URL</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newProfileInfo.name" ng-enter="setDisplayName(newProfileInfo.name)" />
|
||||
<button ng-disabled="!newProfileInfo.name" ng-click="setDisplayName(newProfileInfo.name)">Update Name</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
|
||||
<button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
|
||||
Link Email
|
||||
</button>
|
||||
{{ emailFeedback }}
|
||||
</form>
|
||||
<form ng-hide="!linkedEmails.emailBeingAuthed">
|
||||
Enter validation token for {{ linkedEmails.emailBeingAuthed }}:
|
||||
<br />
|
||||
<input size="20" ng-model="linkedEmails.emailCode" ng-enter="submitEmailCode(linkedEmails.emailCode)" />
|
||||
<button ng-disabled="!linkedEmails.emailCode || !linkedEmails.linkNewEmail" ng-click="submitEmailCode(linkedEmails.emailCode)">
|
||||
Submit Code
|
||||
</button>
|
||||
</form>
|
||||
Linked emails:
|
||||
<table>
|
||||
<tr ng-repeat="(address, info) in linkedEmails.linkedEmailList">
|
||||
<td>{{address}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>My rooms</h3>
|
||||
|
||||
<div class="rooms" ng-repeat="(rm_id, room) in rooms">
|
||||
<div>
|
||||
<a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_display_name }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>Public rooms</h3>
|
||||
|
||||
<div class="public_rooms" ng-repeat="room in public_rooms">
|
||||
<div>
|
||||
<a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/>
|
||||
<input type="checkbox" ng-model="newRoom.private">private
|
||||
<button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo_channel:example.org)"/>
|
||||
<button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
{{ feedback }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user