mirror of
https://github.com/element-hq/synapse.git
synced 2025-12-17 02:10:27 +00:00
Compare commits
154 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8a921f6a6 | ||
|
|
47c7dd590d | ||
|
|
9a7f7cb74f | ||
|
|
18f0247491 | ||
|
|
731c33dd97 | ||
|
|
c2c3ee8e4a | ||
|
|
211a8b288a | ||
|
|
e166e29e87 | ||
|
|
235f686da9 | ||
|
|
464e1fcfa5 | ||
|
|
e565a4bfc4 | ||
|
|
d5da6b0cef | ||
|
|
aa337f588c | ||
|
|
4b8244fbf8 | ||
|
|
4ac80b8570 | ||
|
|
5539251d82 | ||
|
|
5c778f2f15 | ||
|
|
fdcb876495 | ||
|
|
d9d6fbb085 | ||
|
|
7b56a7a3cb | ||
|
|
10e7821461 | ||
|
|
cf890e9d43 | ||
|
|
a808c06a10 | ||
|
|
db02021aba | ||
|
|
ed25abe05f | ||
|
|
08d2f902dd | ||
|
|
0393e87519 | ||
|
|
45570e4695 | ||
|
|
64b341cc10 | ||
|
|
828101dd51 | ||
|
|
7e22afbc7c | ||
|
|
30572e28c2 | ||
|
|
d45f89c95b | ||
|
|
ab0637c2c3 | ||
|
|
1bc05aef20 | ||
|
|
772c117e68 | ||
|
|
040d985908 | ||
|
|
15a7312273 | ||
|
|
3122ff2433 | ||
|
|
027857b261 | ||
|
|
94bb4031f3 | ||
|
|
07d609cbc2 | ||
|
|
68a04b9282 | ||
|
|
399e004884 | ||
|
|
79650f795f | ||
|
|
270d302834 | ||
|
|
32fdf8efd6 | ||
|
|
61e28cdb6f | ||
|
|
118b250473 | ||
|
|
6fd730c96b | ||
|
|
48142a01dd | ||
|
|
8b69468e5f | ||
|
|
bcfaaf7da6 | ||
|
|
a85612baf8 | ||
|
|
6d28560626 | ||
|
|
86fa1138be | ||
|
|
f452899fe2 | ||
|
|
3f5ebccbff | ||
|
|
ff79437d9b | ||
|
|
5452a8ee29 | ||
|
|
00b042a3eb | ||
|
|
a53946a8a1 | ||
|
|
b8ab9f1c0a | ||
|
|
3faa2ae78c | ||
|
|
0271e8e692 | ||
|
|
74cffcf51a | ||
|
|
6d07a28a29 | ||
|
|
6200630904 | ||
|
|
7d99cee3ef | ||
|
|
99ce820cc8 | ||
|
|
ab8de33c76 | ||
|
|
32bfd567ac | ||
|
|
57f047a05a | ||
|
|
5a11a8ef69 | ||
|
|
9b61076d42 | ||
|
|
46dcb0d890 | ||
|
|
ef6a8e4f32 | ||
|
|
b9172b982f | ||
|
|
1c6ab2d759 | ||
|
|
59d3955db1 | ||
|
|
db7109c43b | ||
|
|
fd696f1243 | ||
|
|
401c16559d | ||
|
|
fa6b3490e2 | ||
|
|
4e14e38bd5 | ||
|
|
f5755bcadf | ||
|
|
9ea1de432d | ||
|
|
468d94c920 | ||
|
|
26a95988da | ||
|
|
c9ee9b45c7 | ||
|
|
02f4e3b3ff | ||
|
|
f500dd627a | ||
|
|
865469f233 | ||
|
|
67ffc00d48 | ||
|
|
389ee3624c | ||
|
|
51b0b5c5ab | ||
|
|
10efca1a74 | ||
|
|
a9512d0994 | ||
|
|
fad58dbd08 | ||
|
|
0b01c8560d | ||
|
|
3bb93abb34 | ||
|
|
f81002df60 | ||
|
|
df752a15ce | ||
|
|
d27e1ab148 | ||
|
|
3eb45eba0e | ||
|
|
d9ebe531ed | ||
|
|
7ca6d4e8f7 | ||
|
|
2b7918bd6f | ||
|
|
8fe912d95c | ||
|
|
820ef6e9d8 | ||
|
|
0a65a2384c | ||
|
|
1bc036a12d | ||
|
|
b040bd6157 | ||
|
|
3ef312fb95 | ||
|
|
91753655b7 | ||
|
|
17a4bc10bc | ||
|
|
885e0c8b76 | ||
|
|
2a0e79bbfa | ||
|
|
f64ce52305 | ||
|
|
c715660cb8 | ||
|
|
93407cf7cf | ||
|
|
a8e8d1d06c | ||
|
|
eec67a675f | ||
|
|
56424eca5c | ||
|
|
6797c7f1b1 | ||
|
|
4bfdec1eb2 | ||
|
|
7b79c0f08f | ||
|
|
8c36179d35 | ||
|
|
490f142d73 | ||
|
|
26766c22eb | ||
|
|
e006f101c3 | ||
|
|
74cc722b96 | ||
|
|
6dd50da54e | ||
|
|
f85a3757cf | ||
|
|
95cbd026cc | ||
|
|
e1f249ce20 | ||
|
|
67f42b2f26 | ||
|
|
b86d2a2d4f | ||
|
|
d4145abd33 | ||
|
|
20d0db6cfb | ||
|
|
ca025c2b1d | ||
|
|
c3a774e414 | ||
|
|
0ef54caa28 | ||
|
|
1118f02689 | ||
|
|
1cdc29e260 | ||
|
|
339dd3dc6c | ||
|
|
27047d8f51 | ||
|
|
7c4b47652e | ||
|
|
8e2d4c6da5 | ||
|
|
e76cd252fe | ||
|
|
fbdacce3fe | ||
|
|
898dde8812 | ||
|
|
d7ae9b90a0 | ||
|
|
d2bc5d6f29 |
28
CHANGES.rst
28
CHANGES.rst
@@ -1,3 +1,31 @@
|
|||||||
|
Changes in synapse 0.2.0 (2014-09-02)
|
||||||
|
=====================================
|
||||||
|
This update changes many configuration options, updates the
|
||||||
|
database schema and mandates SSL for server-server connections.
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* Require SSL for server-server connections.
|
||||||
|
* Add SSL listener for client-server connections.
|
||||||
|
* Add ability to use config files.
|
||||||
|
* Add support for kicking/banning and power levels.
|
||||||
|
* Allow setting of room names and topics on creation.
|
||||||
|
* Change presence to include last seen time of the user.
|
||||||
|
* Change url path prefix to /_matrix/...
|
||||||
|
* Bug fixes to presence.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Reskin the CSS for registration and login.
|
||||||
|
* Various improvements to rooms CSS.
|
||||||
|
* Support changes in client-server API.
|
||||||
|
* Bug fixes to VOIP UI.
|
||||||
|
* Various bug fixes to handling of changes to room member list.
|
||||||
|
|
||||||
|
Changes in synapse 0.1.2 (2014-08-29)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Add basic call state UI for VoIP calls.
|
||||||
|
|
||||||
Changes in synapse 0.1.1 (2014-08-29)
|
Changes in synapse 0.1.1 (2014-08-29)
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|||||||
21
README.rst
21
README.rst
@@ -1,5 +1,5 @@
|
|||||||
Quick Start
|
Introduction
|
||||||
===========
|
============
|
||||||
|
|
||||||
Matrix is an ambitious new ecosystem for open federated Instant Messaging and
|
Matrix is an ambitious new ecosystem for open federated Instant Messaging and
|
||||||
VoIP[1]. The basics you need to know to get up and running are:
|
VoIP[1]. The basics you need to know to get up and running are:
|
||||||
@@ -17,6 +17,9 @@ The overall architecture is::
|
|||||||
client <----> homeserver <=================> homeserver <-----> client
|
client <----> homeserver <=================> homeserver <-----> client
|
||||||
e.g. matrix.org:8080 e.g. mydomain.net:8080
|
e.g. matrix.org:8080 e.g. mydomain.net:8080
|
||||||
|
|
||||||
|
Quick Start
|
||||||
|
===========
|
||||||
|
|
||||||
To get up and running:
|
To get up and running:
|
||||||
|
|
||||||
- To simply play with an **existing** homeserver you can
|
- To simply play with an **existing** homeserver you can
|
||||||
@@ -173,7 +176,11 @@ IDs:
|
|||||||
For the first form, simply pass the required hostname (of the machine) as the
|
For the first form, simply pass the required hostname (of the machine) as the
|
||||||
--host parameter::
|
--host parameter::
|
||||||
|
|
||||||
$ python synapse/app/homeserver.py --host machine.my.domain.name
|
$ python synapse/app/homeserver.py \
|
||||||
|
--server-name machine.my.domain.name \
|
||||||
|
--config-path homeserver.config \
|
||||||
|
--generate-config
|
||||||
|
$ python synapse/app/homeserver.py --config-path homeserver.config
|
||||||
|
|
||||||
For the second form, first create your SRV record and publish it in DNS. This
|
For the second form, first create your SRV record and publish it in DNS. This
|
||||||
needs to be named _matrix._tcp.YOURDOMAIN, and point at at least one hostname
|
needs to be named _matrix._tcp.YOURDOMAIN, and point at at least one hostname
|
||||||
@@ -186,7 +193,13 @@ record would then look something like::
|
|||||||
At this point, you should then run the homeserver with the hostname of this
|
At this point, you should then run the homeserver with the hostname of this
|
||||||
SRV record, as that is the name other machines will expect it to have::
|
SRV record, as that is the name other machines will expect it to have::
|
||||||
|
|
||||||
$ python synapse/app/homeserver.py --host my.domain.name --port 8448
|
$ python synapse/app/homeserver.py \
|
||||||
|
--server-name YOURDOMAIN \
|
||||||
|
--bind-port 8448 \
|
||||||
|
--config-path homeserver.config \
|
||||||
|
--generate-config
|
||||||
|
$ python synapse/app/homeserver.py --config-path homeserver.config
|
||||||
|
|
||||||
|
|
||||||
You may additionally want to pass one or more "-v" options, in order to
|
You may additionally want to pass one or more "-v" options, in order to
|
||||||
increase the verbosity of logging output; at least for initial testing.
|
increase the verbosity of logging output; at least for initial testing.
|
||||||
|
|||||||
31
UPGRADE.rst
31
UPGRADE.rst
@@ -1,3 +1,34 @@
|
|||||||
|
Upgrading to v0.2.0
|
||||||
|
===================
|
||||||
|
|
||||||
|
To upgrade the database schema, run::
|
||||||
|
|
||||||
|
./database-prepare-for-0.2.0.sh "<database>.db"
|
||||||
|
|
||||||
|
|
||||||
|
The home server now requires setting up of SSL config before it can run. To
|
||||||
|
automatically generate default config use::
|
||||||
|
|
||||||
|
$ python synapse/app/homeserver.py \
|
||||||
|
--server-name machine.my.domain.name \
|
||||||
|
--bind-port 8448 \
|
||||||
|
--config-path homeserver.config \
|
||||||
|
--generate-config
|
||||||
|
|
||||||
|
This config can be edited if desired, for example to specify a different SSL
|
||||||
|
certificate to use. Once done you can run the home server using::
|
||||||
|
|
||||||
|
$ python synapse/app/homeserver.py --config-path homeserver.config
|
||||||
|
|
||||||
|
See the README.rst for more information.
|
||||||
|
|
||||||
|
Also note that some config options have been renamed, including:
|
||||||
|
|
||||||
|
- "host" to "server-name"
|
||||||
|
- "database" to "database-path"
|
||||||
|
- "port" to "bind-port" and "unsecure-port"
|
||||||
|
|
||||||
|
|
||||||
Upgrading to v0.0.1
|
Upgrading to v0.0.1
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
|||||||
7
WISHLIST.rst
Normal file
7
WISHLIST.rst
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Broad-sweeping stuff which would be nice to have
|
||||||
|
================================================
|
||||||
|
|
||||||
|
- Additional SQL backends beyond sqlite
|
||||||
|
- homeserver implementation in go
|
||||||
|
- homeserver implementation in node.js
|
||||||
|
- client SDKs
|
||||||
@@ -60,7 +60,7 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
"complete_usernames": "on",
|
"complete_usernames": "on",
|
||||||
"send_delivery_receipts": "on"
|
"send_delivery_receipts": "on"
|
||||||
}
|
}
|
||||||
self.path_prefix = "/matrix/client/api/v1"
|
self.path_prefix = "/_matrix/client/api/v1"
|
||||||
self.event_stream_token = "END"
|
self.event_stream_token = "END"
|
||||||
self.prompt = ">>> "
|
self.prompt = ">>> "
|
||||||
|
|
||||||
@@ -252,7 +252,7 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_emailrequest(self, args):
|
def _do_emailrequest(self, args):
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/requestToken"
|
url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/requestToken"
|
||||||
|
|
||||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||||
@@ -274,7 +274,7 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_emailvalidate(self, args):
|
def _do_emailvalidate(self, args):
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/validate/email/submitToken"
|
url = self._identityServerUrl()+"/_matrix/identity/api/v1/validate/email/submitToken"
|
||||||
|
|
||||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||||
@@ -294,7 +294,7 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_3pidbind(self, args):
|
def _do_3pidbind(self, args):
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/3pid/bind"
|
url = self._identityServerUrl()+"/_matrix/identity/api/v1/3pid/bind"
|
||||||
|
|
||||||
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
json_res = yield self.http_client.do_request("POST", url, data=urllib.urlencode(args), jsonreq=False,
|
||||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||||
@@ -360,14 +360,14 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
def _do_invite(self, roomid, userstring):
|
def _do_invite(self, roomid, userstring):
|
||||||
if (not userstring.startswith('@') and
|
if (not userstring.startswith('@') and
|
||||||
self._is_on("complete_usernames")):
|
self._is_on("complete_usernames")):
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/lookup"
|
url = self._identityServerUrl()+"/_matrix/identity/api/v1/lookup"
|
||||||
|
|
||||||
json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring})
|
json_res = yield self.http_client.do_request("GET", url, qparams={'medium':'email','address':userstring})
|
||||||
|
|
||||||
mxid = None
|
mxid = None
|
||||||
|
|
||||||
if 'mxid' in json_res and 'signatures' in json_res:
|
if 'mxid' in json_res and 'signatures' in json_res:
|
||||||
url = self._identityServerUrl()+"/matrix/identity/api/v1/pubkey/ed25519"
|
url = self._identityServerUrl()+"/_matrix/identity/api/v1/pubkey/ed25519"
|
||||||
|
|
||||||
pubKey = None
|
pubKey = None
|
||||||
pubKeyObj = yield self.http_client.do_request("GET", url)
|
pubKeyObj = yield self.http_client.do_request("GET", url)
|
||||||
|
|||||||
10
database-prepare-for-0.2.0.sh
Executable file
10
database-prepare-for-0.2.0.sh
Executable file
@@ -0,0 +1,10 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This is will prepare a synapse database for running with v0.2.0 of synapse.
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cp "$1" "$1.bak"
|
||||||
|
|
||||||
|
sqlite3 "$1" < "synapse/storage/schema/im.sql"
|
||||||
|
sqlite3 "$1" <<< "PRAGMA user_version = 2;"
|
||||||
9
demo/demo.tls.dh
Normal file
9
demo/demo.tls.dh
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
2048-bit DH parameters taken from rfc3526
|
||||||
|
-----BEGIN DH PARAMETERS-----
|
||||||
|
MIIBCAKCAQEA///////////JD9qiIWjCNMTGYouA3BzRKQJOCIpnzHQCC76mOxOb
|
||||||
|
IlFKCHmONATd75UZs806QxswKwpt8l8UN0/hNW1tUcJF5IW1dmJefsb0TELppjft
|
||||||
|
awv/XLb0Brft7jhr+1qJn6WunyQRfEsf5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT
|
||||||
|
mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVSu57VKQdwlpZtZww1Tkq8mATxdGwIyhgh
|
||||||
|
fDKQXkYuNs474553LBgOhgObJ4Oi7Aeij7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq
|
||||||
|
5RXSJhiY+gUQFXKOWoqsqmj//////////wIBAg==
|
||||||
|
-----END DH PARAMETERS-----
|
||||||
@@ -6,17 +6,29 @@ CWD=$(pwd)
|
|||||||
|
|
||||||
cd "$DIR/.."
|
cd "$DIR/.."
|
||||||
|
|
||||||
|
mkdir -p demo/etc
|
||||||
|
|
||||||
for port in 8080 8081 8082; do
|
for port in 8080 8081 8082; do
|
||||||
echo "Starting server on port $port... "
|
echo "Starting server on port $port... "
|
||||||
|
|
||||||
|
https_port=$((port + 400))
|
||||||
|
|
||||||
python -m synapse.app.homeserver \
|
python -m synapse.app.homeserver \
|
||||||
-p "$port" \
|
--generate-config \
|
||||||
-H "localhost:$port" \
|
--config-path "demo/etc/$port.config" \
|
||||||
|
-p "$https_port" \
|
||||||
|
--unsecure-port "$port" \
|
||||||
|
-H "localhost:$https_port" \
|
||||||
-f "$DIR/$port.log" \
|
-f "$DIR/$port.log" \
|
||||||
-d "$DIR/$port.db" \
|
-d "$DIR/$port.db" \
|
||||||
-vv \
|
|
||||||
-D --pid-file "$DIR/$port.pid" \
|
-D --pid-file "$DIR/$port.pid" \
|
||||||
--manhole $((port + 1000))
|
--manhole $((port + 1000)) \
|
||||||
|
--tls-dh-params-path "demo/demo.tls.dh"
|
||||||
|
|
||||||
|
python -m synapse.app.homeserver \
|
||||||
|
--config-path "demo/etc/$port.config" \
|
||||||
|
-vv \
|
||||||
|
|
||||||
done
|
done
|
||||||
|
|
||||||
echo "Starting webclient on port 8000..."
|
echo "Starting webclient on port 8000..."
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Registration
|
|||||||
The aim of registration is to get a user ID and access token which you will need
|
The aim of registration is to get a user ID and access token which you will need
|
||||||
when accessing other APIs::
|
when accessing other APIs::
|
||||||
|
|
||||||
curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/register"
|
curl -XPOST -d '{"user_id":"example", "password":"wordpass"}' "http://localhost:8080/_matrix/client/api/v1/register"
|
||||||
|
|
||||||
{
|
{
|
||||||
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc",
|
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.AqdSzFmFYrLrTmteXc",
|
||||||
@@ -51,13 +51,13 @@ Login
|
|||||||
-----
|
-----
|
||||||
The aim when logging in is to get an access token for your existing user ID::
|
The aim when logging in is to get an access token for your existing user ID::
|
||||||
|
|
||||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/login"
|
curl -XGET "http://localhost:8080/_matrix/client/api/v1/login"
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "m.login.password"
|
"type": "m.login.password"
|
||||||
}
|
}
|
||||||
|
|
||||||
curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/matrix/client/api/v1/login"
|
curl -XPOST -d '{"type":"m.login.password", "user":"example", "password":"wordpass"}' "http://localhost:8080/_matrix/client/api/v1/login"
|
||||||
|
|
||||||
{
|
{
|
||||||
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd",
|
"access_token": "QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd",
|
||||||
@@ -87,7 +87,7 @@ Creating a room
|
|||||||
If you want to send a message to someone, you have to be in a room with them. To
|
If you want to send a message to someone, you have to be in a room with them. To
|
||||||
create a room::
|
create a room::
|
||||||
|
|
||||||
curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
curl -XPOST -d '{"room_alias_name":"tutorial"}' "http://localhost:8080/_matrix/client/api/v1/rooms?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
||||||
|
|
||||||
{
|
{
|
||||||
"room_alias": "#tutorial:localhost",
|
"room_alias": "#tutorial:localhost",
|
||||||
@@ -105,7 +105,7 @@ Sending messages
|
|||||||
----------------
|
----------------
|
||||||
You can now send messages to this room::
|
You can now send messages to this room::
|
||||||
|
|
||||||
curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
curl -XPUT -d '{"msgtype":"m.text", "body":"hello"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/messages/%40example%3Alocalhost/msgid1?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
||||||
|
|
||||||
NB: There are no limitations to the types of messages which can be exchanged.
|
NB: There are no limitations to the types of messages which can be exchanged.
|
||||||
The only requirement is that ``"msgtype"`` is specified.
|
The only requirement is that ``"msgtype"`` is specified.
|
||||||
@@ -127,7 +127,7 @@ Inviting a user to a room
|
|||||||
-------------------------
|
-------------------------
|
||||||
You can directly invite a user to a room like so::
|
You can directly invite a user to a room like so::
|
||||||
|
|
||||||
curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
curl -XPUT -d '{"membership":"invite"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QGV4YW1wbGU6bG9jYWxob3N0.vRDLTgxefmKWQEtgGd"
|
||||||
|
|
||||||
This informs ``@myfriend:localhost`` of the room ID
|
This informs ``@myfriend:localhost`` of the room ID
|
||||||
``!CvcvRuDYDzTOzfKKgh:localhost`` and allows them to join the room.
|
``!CvcvRuDYDzTOzfKKgh:localhost`` and allows them to join the room.
|
||||||
@@ -137,7 +137,7 @@ Joining a room via an invite
|
|||||||
If you receive an invite, you can join the room by changing the membership to
|
If you receive an invite, you can join the room by changing the membership to
|
||||||
join::
|
join::
|
||||||
|
|
||||||
curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
curl -XPUT -d '{"membership":"join"}' "http://localhost:8080/_matrix/client/api/v1/rooms/%21CvcvRuDYDzTOzfKKgh:localhost/members/%40myfriend%3Alocalhost/state?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
||||||
|
|
||||||
NB: Only the person invited (``@myfriend:localhost``) can change the membership
|
NB: Only the person invited (``@myfriend:localhost``) can change the membership
|
||||||
state to ``"join"``.
|
state to ``"join"``.
|
||||||
@@ -147,7 +147,7 @@ Joining a room via an alias
|
|||||||
Alternatively, if you know the room alias for this room and the room config
|
Alternatively, if you know the room alias for this room and the room config
|
||||||
allows it, you can directly join a room via the alias::
|
allows it, you can directly join a room via the alias::
|
||||||
|
|
||||||
curl -XPUT -d '{}' "http://localhost:8080/matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
curl -XPUT -d '{}' "http://localhost:8080/_matrix/client/api/v1/join/%23tutorial%3Alocalhost?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
||||||
|
|
||||||
{
|
{
|
||||||
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
|
"room_id": "!CvcvRuDYDzTOzfKKgh:localhost"
|
||||||
@@ -173,7 +173,7 @@ Getting all state
|
|||||||
If the client doesn't know any information on the rooms the user is
|
If the client doesn't know any information on the rooms the user is
|
||||||
invited/joined on, they can get all the user's state for all rooms::
|
invited/joined on, they can get all the user's state for all rooms::
|
||||||
|
|
||||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
curl -XGET "http://localhost:8080/_matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK"
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -236,7 +236,7 @@ all of the messages and feedback for these rooms. This can be a LOT of data. You
|
|||||||
may just want the most recent message for each room. This can be achieved by
|
may just want the most recent message for each room. This can be achieved by
|
||||||
applying pagination stream parameters to this request::
|
applying pagination stream parameters to this request::
|
||||||
|
|
||||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1"
|
curl -XGET "http://localhost:8080/_matrix/client/api/v1/im/sync?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END&to=START&limit=1"
|
||||||
|
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -271,7 +271,7 @@ Getting live state
|
|||||||
Once you know which rooms the client has previously interacted with, you need to
|
Once you know which rooms the client has previously interacted with, you need to
|
||||||
listen for incoming events. This can be done like so::
|
listen for incoming events. This can be done like so::
|
||||||
|
|
||||||
curl -XGET "http://localhost:8080/matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END"
|
curl -XGET "http://localhost:8080/_matrix/client/api/v1/events?access_token=QG15ZnJpZW5kOmxvY2FsaG9zdA...XKuGdVsovHmwMyDDvK&from=END"
|
||||||
|
|
||||||
{
|
{
|
||||||
"chunk": [],
|
"chunk": [],
|
||||||
|
|||||||
@@ -306,11 +306,11 @@ POST requests MUST be submitted as application/json.
|
|||||||
All paths MUST be namespaced by the version of the API being used. This should
|
All paths MUST be namespaced by the version of the API being used. This should
|
||||||
be:
|
be:
|
||||||
|
|
||||||
/matrix/client/api/v1
|
/_matrix/client/api/v1
|
||||||
|
|
||||||
All REST paths in this section MUST be prefixed with this. E.g.
|
All REST paths in this section MUST be prefixed with this. E.g.
|
||||||
REST Path: /rooms/$room_id
|
REST Path: /rooms/$room_id
|
||||||
Absolute Path: /matrix/client/api/v1/rooms/$room_id
|
Absolute Path: /_matrix/client/api/v1/rooms/$room_id
|
||||||
|
|
||||||
Registration
|
Registration
|
||||||
============
|
============
|
||||||
@@ -1007,26 +1007,15 @@ for users from other servers entirely.
|
|||||||
Presence
|
Presence
|
||||||
========
|
========
|
||||||
|
|
||||||
In the following messages, the presence state is an integer enumeration of the
|
In the following messages, the presence state is a presence string as described in
|
||||||
following states:
|
the main specification document.
|
||||||
0 : OFFLINE
|
|
||||||
1 : BUSY
|
|
||||||
2 : ONLINE
|
|
||||||
3 : FREE_TO_CHAT
|
|
||||||
|
|
||||||
Aside from OFFLINE, the protocol doesn't assign any special meaning to these
|
|
||||||
states; they are provided as an approximate signal for users to give to other
|
|
||||||
users and for clients to present them in some way that may be useful. Clients
|
|
||||||
could have different behaviours for different states of the user's presence, for
|
|
||||||
example to decide how much prominence or sound to use for incoming event
|
|
||||||
notifications.
|
|
||||||
|
|
||||||
Getting/Setting your own presence state
|
Getting/Setting your own presence state
|
||||||
---------------------------------------
|
---------------------------------------
|
||||||
REST Path: /presence/$user_id/status
|
REST Path: /presence/$user_id/status
|
||||||
Valid methods: GET/PUT
|
Valid methods: GET/PUT
|
||||||
Required keys:
|
Required keys:
|
||||||
state : [0|1|2|3] - The user's new presence state
|
presence : <string> - The user's new presence state
|
||||||
Optional keys:
|
Optional keys:
|
||||||
status_msg : text string provided by the user to explain their status
|
status_msg : text string provided by the user to explain their status
|
||||||
|
|
||||||
@@ -1039,7 +1028,7 @@ Fetching your presence list
|
|||||||
following keys:
|
following keys:
|
||||||
{
|
{
|
||||||
"user_id" : string giving the observed user's ID
|
"user_id" : string giving the observed user's ID
|
||||||
"state" : int giving their status
|
"presence" : int giving their status
|
||||||
"status_msg" : optional text string
|
"status_msg" : optional text string
|
||||||
"displayname" : optional text string from the user's profile
|
"displayname" : optional text string from the user's profile
|
||||||
"avatar_url" : optional text string from the user's profile
|
"avatar_url" : optional text string from the user's profile
|
||||||
|
|||||||
@@ -3,31 +3,31 @@
|
|||||||
"swaggerVersion": "1.2",
|
"swaggerVersion": "1.2",
|
||||||
"apis": [
|
"apis": [
|
||||||
{
|
{
|
||||||
"path": "/login",
|
"path": "-login",
|
||||||
"description": "Login operations"
|
"description": "Login operations"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/registration",
|
"path": "-registration",
|
||||||
"description": "Registration operations"
|
"description": "Registration operations"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/rooms",
|
"path": "-rooms",
|
||||||
"description": "Room operations"
|
"description": "Room operations"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/profile",
|
"path": "-profile",
|
||||||
"description": "Profile operations"
|
"description": "Profile operations"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/presence",
|
"path": "-presence",
|
||||||
"description": "Presence operations"
|
"description": "Presence operations"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/events",
|
"path": "-events",
|
||||||
"description": "Event operations"
|
"description": "Event operations"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/directory",
|
"path": "-directory",
|
||||||
"description": "Directory operations"
|
"description": "Directory operations"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"apiVersion": "1.0.0",
|
"apiVersion": "1.0.0",
|
||||||
"swaggerVersion": "1.2",
|
"swaggerVersion": "1.2",
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
"basePath": "http://localhost:8080/_matrix/client/api/v1",
|
||||||
"resourcePath": "/directory",
|
"resourcePath": "/directory",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"apiVersion": "1.0.0",
|
"apiVersion": "1.0.0",
|
||||||
"swaggerVersion": "1.2",
|
"swaggerVersion": "1.2",
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
"basePath": "http://localhost:8080/_matrix/client/api/v1",
|
||||||
"resourcePath": "/events",
|
"resourcePath": "/events",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -15,9 +15,7 @@
|
|||||||
"summary": "Listen on the event stream",
|
"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.",
|
"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",
|
"type": "PaginationChunk",
|
||||||
"nickname": "get_event_stream"
|
"nickname": "get_event_stream",
|
||||||
}
|
|
||||||
],
|
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "from",
|
"name": "from",
|
||||||
@@ -33,7 +31,10 @@
|
|||||||
"type": "integer",
|
"type": "integer",
|
||||||
"paramType": "query"
|
"paramType": "query"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
"responseMessages": [
|
"responseMessages": [
|
||||||
{
|
{
|
||||||
"code": 400,
|
"code": 400,
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"path": "/login"
|
"path": "/login"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
"basePath": "http://localhost:8080/_matrix/client/api/v1",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"apiVersion": "1.0.0",
|
"apiVersion": "1.0.0",
|
||||||
"swaggerVersion": "1.2",
|
"swaggerVersion": "1.2",
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
"basePath": "http://localhost:8080/_matrix/client/api/v1",
|
||||||
"resourcePath": "/presence",
|
"resourcePath": "/presence",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"apiVersion": "1.0.0",
|
"apiVersion": "1.0.0",
|
||||||
"swaggerVersion": "1.2",
|
"swaggerVersion": "1.2",
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
"basePath": "http://localhost:8080/_matrix/client/api/v1",
|
||||||
"resourcePath": "/profile",
|
"resourcePath": "/profile",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"path": "/register"
|
"path": "/register"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
"basePath": "http://localhost:8080/_matrix/client/api/v1",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"apiVersion": "1.0.0",
|
"apiVersion": "1.0.0",
|
||||||
"swaggerVersion": "1.2",
|
"swaggerVersion": "1.2",
|
||||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
"basePath": "http://localhost:8080/_matrix/client/api/v1",
|
||||||
"resourcePath": "/rooms",
|
"resourcePath": "/rooms",
|
||||||
"produces": [
|
"produces": [
|
||||||
"application/json"
|
"application/json"
|
||||||
@@ -14,12 +14,12 @@
|
|||||||
},
|
},
|
||||||
"apis": [
|
"apis": [
|
||||||
{
|
{
|
||||||
"path": "/rooms/{roomId}/send/{eventType}/{txnId}",
|
"path": "/rooms/{roomId}/send/{eventType}",
|
||||||
"operations": [
|
"operations": [
|
||||||
{
|
{
|
||||||
"method": "PUT",
|
"method": "POST",
|
||||||
"summary": "Send a generic non-state event to this room.",
|
"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}",
|
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||||
"type": "EventId",
|
"type": "EventId",
|
||||||
"nickname": "send_non_state_event",
|
"nickname": "send_non_state_event",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -46,13 +46,6 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"paramType": "path"
|
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -104,12 +97,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/rooms/{roomId}/send/m.room.message/{txnId}",
|
"path": "/rooms/{roomId}/send/m.room.message",
|
||||||
"operations": [
|
"operations": [
|
||||||
{
|
{
|
||||||
"method": "PUT",
|
"method": "POST",
|
||||||
"summary": "Send a message in this room.",
|
"summary": "Send a message in this room.",
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message",
|
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||||
"type": "EventId",
|
"type": "EventId",
|
||||||
"nickname": "send_message",
|
"nickname": "send_message",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -129,13 +122,6 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"paramType": "path"
|
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -195,12 +181,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/rooms/{roomId}/send/m.room.message.feedback/{txnId}",
|
"path": "/rooms/{roomId}/send/m.room.message.feedback",
|
||||||
"operations": [
|
"operations": [
|
||||||
{
|
{
|
||||||
"method": "PUT",
|
"method": "POST",
|
||||||
"summary": "Send feedback to a message.",
|
"summary": "Send feedback to a message.",
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/send/m.room.message.feedback",
|
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||||
"type": "EventId",
|
"type": "EventId",
|
||||||
"nickname": "send_feedback",
|
"nickname": "send_feedback",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -220,13 +206,6 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"paramType": "path"
|
"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"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responseMessages": [
|
"responseMessages": [
|
||||||
@@ -239,12 +218,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/rooms/{roomId}/invite/{txnId}",
|
"path": "/rooms/{roomId}/invite",
|
||||||
"operations": [
|
"operations": [
|
||||||
{
|
{
|
||||||
"method": "PUT",
|
"method": "POST",
|
||||||
"summary": "Invite a user to this room.",
|
"summary": "Invite a user to this room.",
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/invite",
|
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||||
"type": "void",
|
"type": "void",
|
||||||
"nickname": "invite",
|
"nickname": "invite",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -258,13 +237,6 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"paramType": "path"
|
"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",
|
"name": "body",
|
||||||
"description": "The user to invite.",
|
"description": "The user to invite.",
|
||||||
@@ -277,12 +249,12 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"path": "/rooms/{roomId}/join/{txnId}",
|
"path": "/rooms/{roomId}/join",
|
||||||
"operations": [
|
"operations": [
|
||||||
{
|
{
|
||||||
"method": "PUT",
|
"method": "POST",
|
||||||
"summary": "Join this room.",
|
"summary": "Join this room.",
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/join",
|
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||||
"type": "void",
|
"type": "void",
|
||||||
"nickname": "join_room",
|
"nickname": "join_room",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -295,25 +267,18 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"paramType": "path"
|
"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}",
|
"path": "/rooms/{roomId}/leave",
|
||||||
"operations": [
|
"operations": [
|
||||||
{
|
{
|
||||||
"method": "PUT",
|
"method": "POST",
|
||||||
"summary": "Leave this room.",
|
"summary": "Leave this room.",
|
||||||
"notes": "This operation can also be done as a POST to /rooms/{roomId}/leave",
|
"notes": "This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||||
"type": "void",
|
"type": "void",
|
||||||
"nickname": "leave",
|
"nickname": "leave",
|
||||||
"consumes": [
|
"consumes": [
|
||||||
@@ -326,13 +291,37 @@
|
|||||||
"required": true,
|
"required": true,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"paramType": "path"
|
"paramType": "path"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "txnId",
|
"path": "/rooms/{roomId}/ban",
|
||||||
"description": "A client transaction ID for this PUT to ensure idempotency. This can only be omitted if the HTTP method becomes a POST. ",
|
"operations": [
|
||||||
"required": false,
|
{
|
||||||
|
"method": "POST",
|
||||||
|
"summary": "Ban a user in the room.",
|
||||||
|
"notes": "This operation can also be done as a PUT by suffixing /{txnId}. The caller must have the required power level to do this operation.",
|
||||||
|
"type": "void",
|
||||||
|
"nickname": "ban",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "roomId",
|
||||||
|
"description": "The room which has the user to ban.",
|
||||||
|
"required": true,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"paramType": "path"
|
"paramType": "path"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "body",
|
||||||
|
"description": "The user to ban.",
|
||||||
|
"required": true,
|
||||||
|
"type": "BanRequest",
|
||||||
|
"paramType": "body"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -476,7 +465,7 @@
|
|||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"name": "body",
|
"name": "body",
|
||||||
"description": "The desired configuration for the room.",
|
"description": "The desired configuration for the room. This operation can also be done as a PUT by suffixing /{txnId}.",
|
||||||
"required": true,
|
"required": true,
|
||||||
"type": "RoomConfig",
|
"type": "RoomConfig",
|
||||||
"paramType": "body"
|
"paramType": "body"
|
||||||
@@ -699,6 +688,16 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "The alias to give the new room.",
|
"description": "The alias to give the new room.",
|
||||||
"required": false
|
"required": false
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Sets the name of the room. Send a m.room.name event after creating the room with the 'name' key specified.",
|
||||||
|
"required": false
|
||||||
|
},
|
||||||
|
"topic": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Sets the topic for the room. Send a m.room.topic event after creating the room with the 'topic' key specified.",
|
||||||
|
"required": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -831,6 +830,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"BanRequest": {
|
||||||
|
"id": "BanRequest",
|
||||||
|
"properties": {
|
||||||
|
"user_id": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The fully-qualified user ID."
|
||||||
|
},
|
||||||
|
"reason": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "The reason for the ban."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"InitialSyncRoomData": {
|
"InitialSyncRoomData": {
|
||||||
"id": "InitialSyncRoomData",
|
"id": "InitialSyncRoomData",
|
||||||
"properties": {
|
"properties": {
|
||||||
5
docs/client-server/web/README
Normal file
5
docs/client-server/web/README
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
To get this running:
|
||||||
|
ln -s ../swagger_matrix
|
||||||
|
python -m SimpleHTTPServer
|
||||||
|
|
||||||
|
Go to http://localhost:8000/swagger.html
|
||||||
38
docs/client-server/web/files/backbone-min.js
vendored
Normal file
38
docs/client-server/web/files/backbone-min.js
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Backbone.js 0.9.2
|
||||||
|
|
||||||
|
// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||||
|
// Backbone may be freely distributed under the MIT license.
|
||||||
|
// For all details and documentation:
|
||||||
|
// http://backbonejs.org
|
||||||
|
(function(){var l=this,y=l.Backbone,z=Array.prototype.slice,A=Array.prototype.splice,g;g="undefined"!==typeof exports?exports:l.Backbone={};g.VERSION="0.9.2";var f=l._;!f&&"undefined"!==typeof require&&(f=require("underscore"));var i=l.jQuery||l.Zepto||l.ender;g.setDomLibrary=function(a){i=a};g.noConflict=function(){l.Backbone=y;return this};g.emulateHTTP=!1;g.emulateJSON=!1;var p=/\s+/,k=g.Events={on:function(a,b,c){var d,e,f,g,j;if(!b)return this;a=a.split(p);for(d=this._callbacks||(this._callbacks=
|
||||||
|
{});e=a.shift();)f=(j=d[e])?j.tail:{},f.next=g={},f.context=c,f.callback=b,d[e]={tail:g,next:j?j.next:f};return this},off:function(a,b,c){var d,e,h,g,j,q;if(e=this._callbacks){if(!a&&!b&&!c)return delete this._callbacks,this;for(a=a?a.split(p):f.keys(e);d=a.shift();)if(h=e[d],delete e[d],h&&(b||c))for(g=h.tail;(h=h.next)!==g;)if(j=h.callback,q=h.context,b&&j!==b||c&&q!==c)this.on(d,j,q);return this}},trigger:function(a){var b,c,d,e,f,g;if(!(d=this._callbacks))return this;f=d.all;a=a.split(p);for(g=
|
||||||
|
z.call(arguments,1);b=a.shift();){if(c=d[b])for(e=c.tail;(c=c.next)!==e;)c.callback.apply(c.context||this,g);if(c=f){e=c.tail;for(b=[b].concat(g);(c=c.next)!==e;)c.callback.apply(c.context||this,b)}}return this}};k.bind=k.on;k.unbind=k.off;var o=g.Model=function(a,b){var c;a||(a={});b&&b.parse&&(a=this.parse(a));if(c=n(this,"defaults"))a=f.extend({},c,a);b&&b.collection&&(this.collection=b.collection);this.attributes={};this._escapedAttributes={};this.cid=f.uniqueId("c");this.changed={};this._silent=
|
||||||
|
{};this._pending={};this.set(a,{silent:!0});this.changed={};this._silent={};this._pending={};this._previousAttributes=f.clone(this.attributes);this.initialize.apply(this,arguments)};f.extend(o.prototype,k,{changed:null,_silent:null,_pending:null,idAttribute:"id",initialize:function(){},toJSON:function(){return f.clone(this.attributes)},get:function(a){return this.attributes[a]},escape:function(a){var b;if(b=this._escapedAttributes[a])return b;b=this.get(a);return this._escapedAttributes[a]=f.escape(null==
|
||||||
|
b?"":""+b)},has:function(a){return null!=this.get(a)},set:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c||(c={});if(!d)return this;d instanceof o&&(d=d.attributes);if(c.unset)for(e in d)d[e]=void 0;if(!this._validate(d,c))return!1;this.idAttribute in d&&(this.id=d[this.idAttribute]);var b=c.changes={},h=this.attributes,g=this._escapedAttributes,j=this._previousAttributes||{};for(e in d){a=d[e];if(!f.isEqual(h[e],a)||c.unset&&f.has(h,e))delete g[e],(c.silent?this._silent:
|
||||||
|
b)[e]=!0;c.unset?delete h[e]:h[e]=a;!f.isEqual(j[e],a)||f.has(h,e)!=f.has(j,e)?(this.changed[e]=a,c.silent||(this._pending[e]=!0)):(delete this.changed[e],delete this._pending[e])}c.silent||this.change(c);return this},unset:function(a,b){(b||(b={})).unset=!0;return this.set(a,null,b)},clear:function(a){(a||(a={})).unset=!0;return this.set(f.clone(this.attributes),a)},fetch:function(a){var a=a?f.clone(a):{},b=this,c=a.success;a.success=function(d,e,f){if(!b.set(b.parse(d,f),a))return!1;c&&c(b,d)};
|
||||||
|
a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},save:function(a,b,c){var d,e;f.isObject(a)||null==a?(d=a,c=b):(d={},d[a]=b);c=c?f.clone(c):{};if(c.wait){if(!this._validate(d,c))return!1;e=f.clone(this.attributes)}a=f.extend({},c,{silent:!0});if(d&&!this.set(d,c.wait?a:c))return!1;var h=this,i=c.success;c.success=function(a,b,e){b=h.parse(a,e);if(c.wait){delete c.wait;b=f.extend(d||{},b)}if(!h.set(b,c))return false;i?i(h,a):h.trigger("sync",h,a,c)};c.error=g.wrapError(c.error,
|
||||||
|
h,c);b=this.isNew()?"create":"update";b=(this.sync||g.sync).call(this,b,this,c);c.wait&&this.set(e,a);return b},destroy:function(a){var a=a?f.clone(a):{},b=this,c=a.success,d=function(){b.trigger("destroy",b,b.collection,a)};if(this.isNew())return d(),!1;a.success=function(e){a.wait&&d();c?c(b,e):b.trigger("sync",b,e,a)};a.error=g.wrapError(a.error,b,a);var e=(this.sync||g.sync).call(this,"delete",this,a);a.wait||d();return e},url:function(){var a=n(this,"urlRoot")||n(this.collection,"url")||t();
|
||||||
|
return this.isNew()?a:a+("/"==a.charAt(a.length-1)?"":"/")+encodeURIComponent(this.id)},parse:function(a){return a},clone:function(){return new this.constructor(this.attributes)},isNew:function(){return null==this.id},change:function(a){a||(a={});var b=this._changing;this._changing=!0;for(var c in this._silent)this._pending[c]=!0;var d=f.extend({},a.changes,this._silent);this._silent={};for(c in d)this.trigger("change:"+c,this,this.get(c),a);if(b)return this;for(;!f.isEmpty(this._pending);){this._pending=
|
||||||
|
{};this.trigger("change",this,a);for(c in this.changed)!this._pending[c]&&!this._silent[c]&&delete this.changed[c];this._previousAttributes=f.clone(this.attributes)}this._changing=!1;return this},hasChanged:function(a){return!arguments.length?!f.isEmpty(this.changed):f.has(this.changed,a)},changedAttributes:function(a){if(!a)return this.hasChanged()?f.clone(this.changed):!1;var b,c=!1,d=this._previousAttributes,e;for(e in a)if(!f.isEqual(d[e],b=a[e]))(c||(c={}))[e]=b;return c},previous:function(a){return!arguments.length||
|
||||||
|
!this._previousAttributes?null:this._previousAttributes[a]},previousAttributes:function(){return f.clone(this._previousAttributes)},isValid:function(){return!this.validate(this.attributes)},_validate:function(a,b){if(b.silent||!this.validate)return!0;var a=f.extend({},this.attributes,a),c=this.validate(a,b);if(!c)return!0;b&&b.error?b.error(this,c,b):this.trigger("error",this,c,b);return!1}});var r=g.Collection=function(a,b){b||(b={});b.model&&(this.model=b.model);b.comparator&&(this.comparator=b.comparator);
|
||||||
|
this._reset();this.initialize.apply(this,arguments);a&&this.reset(a,{silent:!0,parse:b.parse})};f.extend(r.prototype,k,{model:o,initialize:function(){},toJSON:function(a){return this.map(function(b){return b.toJSON(a)})},add:function(a,b){var c,d,e,g,i,j={},k={},l=[];b||(b={});a=f.isArray(a)?a.slice():[a];c=0;for(d=a.length;c<d;c++){if(!(e=a[c]=this._prepareModel(a[c],b)))throw Error("Can't add an invalid model to a collection");g=e.cid;i=e.id;j[g]||this._byCid[g]||null!=i&&(k[i]||this._byId[i])?
|
||||||
|
l.push(c):j[g]=k[i]=e}for(c=l.length;c--;)a.splice(l[c],1);c=0;for(d=a.length;c<d;c++)(e=a[c]).on("all",this._onModelEvent,this),this._byCid[e.cid]=e,null!=e.id&&(this._byId[e.id]=e);this.length+=d;A.apply(this.models,[null!=b.at?b.at:this.models.length,0].concat(a));this.comparator&&this.sort({silent:!0});if(b.silent)return this;c=0;for(d=this.models.length;c<d;c++)if(j[(e=this.models[c]).cid])b.index=c,e.trigger("add",e,this,b);return this},remove:function(a,b){var c,d,e,g;b||(b={});a=f.isArray(a)?
|
||||||
|
a.slice():[a];c=0;for(d=a.length;c<d;c++)if(g=this.getByCid(a[c])||this.get(a[c]))delete this._byId[g.id],delete this._byCid[g.cid],e=this.indexOf(g),this.models.splice(e,1),this.length--,b.silent||(b.index=e,g.trigger("remove",g,this,b)),this._removeReference(g);return this},push:function(a,b){a=this._prepareModel(a,b);this.add(a,b);return a},pop:function(a){var b=this.at(this.length-1);this.remove(b,a);return b},unshift:function(a,b){a=this._prepareModel(a,b);this.add(a,f.extend({at:0},b));return a},
|
||||||
|
shift:function(a){var b=this.at(0);this.remove(b,a);return b},get:function(a){return null==a?void 0:this._byId[null!=a.id?a.id:a]},getByCid:function(a){return a&&this._byCid[a.cid||a]},at:function(a){return this.models[a]},where:function(a){return f.isEmpty(a)?[]:this.filter(function(b){for(var c in a)if(a[c]!==b.get(c))return!1;return!0})},sort:function(a){a||(a={});if(!this.comparator)throw Error("Cannot sort a set without a comparator");var b=f.bind(this.comparator,this);1==this.comparator.length?
|
||||||
|
this.models=this.sortBy(b):this.models.sort(b);a.silent||this.trigger("reset",this,a);return this},pluck:function(a){return f.map(this.models,function(b){return b.get(a)})},reset:function(a,b){a||(a=[]);b||(b={});for(var c=0,d=this.models.length;c<d;c++)this._removeReference(this.models[c]);this._reset();this.add(a,f.extend({silent:!0},b));b.silent||this.trigger("reset",this,b);return this},fetch:function(a){a=a?f.clone(a):{};void 0===a.parse&&(a.parse=!0);var b=this,c=a.success;a.success=function(d,
|
||||||
|
e,f){b[a.add?"add":"reset"](b.parse(d,f),a);c&&c(b,d)};a.error=g.wrapError(a.error,b,a);return(this.sync||g.sync).call(this,"read",this,a)},create:function(a,b){var c=this,b=b?f.clone(b):{},a=this._prepareModel(a,b);if(!a)return!1;b.wait||c.add(a,b);var d=b.success;b.success=function(e,f){b.wait&&c.add(e,b);d?d(e,f):e.trigger("sync",a,f,b)};a.save(null,b);return a},parse:function(a){return a},chain:function(){return f(this.models).chain()},_reset:function(){this.length=0;this.models=[];this._byId=
|
||||||
|
{};this._byCid={}},_prepareModel:function(a,b){b||(b={});a instanceof o?a.collection||(a.collection=this):(b.collection=this,a=new this.model(a,b),a._validate(a.attributes,b)||(a=!1));return a},_removeReference:function(a){this==a.collection&&delete a.collection;a.off("all",this._onModelEvent,this)},_onModelEvent:function(a,b,c,d){("add"==a||"remove"==a)&&c!=this||("destroy"==a&&this.remove(b,d),b&&a==="change:"+b.idAttribute&&(delete this._byId[b.previous(b.idAttribute)],this._byId[b.id]=b),this.trigger.apply(this,
|
||||||
|
arguments))}});f.each("forEach,each,map,reduce,reduceRight,find,detect,filter,select,reject,every,all,some,any,include,contains,invoke,max,min,sortBy,sortedIndex,toArray,size,first,initial,rest,last,without,indexOf,shuffle,lastIndexOf,isEmpty,groupBy".split(","),function(a){r.prototype[a]=function(){return f[a].apply(f,[this.models].concat(f.toArray(arguments)))}});var u=g.Router=function(a){a||(a={});a.routes&&(this.routes=a.routes);this._bindRoutes();this.initialize.apply(this,arguments)},B=/:\w+/g,
|
||||||
|
C=/\*\w+/g,D=/[-[\]{}()+?.,\\^$|#\s]/g;f.extend(u.prototype,k,{initialize:function(){},route:function(a,b,c){g.history||(g.history=new m);f.isRegExp(a)||(a=this._routeToRegExp(a));c||(c=this[b]);g.history.route(a,f.bind(function(d){d=this._extractParameters(a,d);c&&c.apply(this,d);this.trigger.apply(this,["route:"+b].concat(d));g.history.trigger("route",this,b,d)},this));return this},navigate:function(a,b){g.history.navigate(a,b)},_bindRoutes:function(){if(this.routes){var a=[],b;for(b in this.routes)a.unshift([b,
|
||||||
|
this.routes[b]]);b=0;for(var c=a.length;b<c;b++)this.route(a[b][0],a[b][1],this[a[b][1]])}},_routeToRegExp:function(a){a=a.replace(D,"\\$&").replace(B,"([^/]+)").replace(C,"(.*?)");return RegExp("^"+a+"$")},_extractParameters:function(a,b){return a.exec(b).slice(1)}});var m=g.History=function(){this.handlers=[];f.bindAll(this,"checkUrl")},s=/^[#\/]/,E=/msie [\w.]+/;m.started=!1;f.extend(m.prototype,k,{interval:50,getHash:function(a){return(a=(a?a.location:window.location).href.match(/#(.*)$/))?a[1]:
|
||||||
|
""},getFragment:function(a,b){if(null==a)if(this._hasPushState||b){var a=window.location.pathname,c=window.location.search;c&&(a+=c)}else a=this.getHash();a.indexOf(this.options.root)||(a=a.substr(this.options.root.length));return a.replace(s,"")},start:function(a){if(m.started)throw Error("Backbone.history has already been started");m.started=!0;this.options=f.extend({},{root:"/"},this.options,a);this._wantsHashChange=!1!==this.options.hashChange;this._wantsPushState=!!this.options.pushState;this._hasPushState=
|
||||||
|
!(!this.options.pushState||!window.history||!window.history.pushState);var a=this.getFragment(),b=document.documentMode;if(b=E.exec(navigator.userAgent.toLowerCase())&&(!b||7>=b))this.iframe=i('<iframe src="javascript:0" tabindex="-1" />').hide().appendTo("body")[0].contentWindow,this.navigate(a);this._hasPushState?i(window).bind("popstate",this.checkUrl):this._wantsHashChange&&"onhashchange"in window&&!b?i(window).bind("hashchange",this.checkUrl):this._wantsHashChange&&(this._checkUrlInterval=setInterval(this.checkUrl,
|
||||||
|
this.interval));this.fragment=a;a=window.location;b=a.pathname==this.options.root;if(this._wantsHashChange&&this._wantsPushState&&!this._hasPushState&&!b)return this.fragment=this.getFragment(null,!0),window.location.replace(this.options.root+"#"+this.fragment),!0;this._wantsPushState&&this._hasPushState&&b&&a.hash&&(this.fragment=this.getHash().replace(s,""),window.history.replaceState({},document.title,a.protocol+"//"+a.host+this.options.root+this.fragment));if(!this.options.silent)return this.loadUrl()},
|
||||||
|
stop:function(){i(window).unbind("popstate",this.checkUrl).unbind("hashchange",this.checkUrl);clearInterval(this._checkUrlInterval);m.started=!1},route:function(a,b){this.handlers.unshift({route:a,callback:b})},checkUrl:function(){var a=this.getFragment();a==this.fragment&&this.iframe&&(a=this.getFragment(this.getHash(this.iframe)));if(a==this.fragment)return!1;this.iframe&&this.navigate(a);this.loadUrl()||this.loadUrl(this.getHash())},loadUrl:function(a){var b=this.fragment=this.getFragment(a);return f.any(this.handlers,
|
||||||
|
function(a){if(a.route.test(b))return a.callback(b),!0})},navigate:function(a,b){if(!m.started)return!1;if(!b||!0===b)b={trigger:b};var c=(a||"").replace(s,"");this.fragment!=c&&(this._hasPushState?(0!=c.indexOf(this.options.root)&&(c=this.options.root+c),this.fragment=c,window.history[b.replace?"replaceState":"pushState"]({},document.title,c)):this._wantsHashChange?(this.fragment=c,this._updateHash(window.location,c,b.replace),this.iframe&&c!=this.getFragment(this.getHash(this.iframe))&&(b.replace||
|
||||||
|
this.iframe.document.open().close(),this._updateHash(this.iframe.location,c,b.replace))):window.location.assign(this.options.root+a),b.trigger&&this.loadUrl(a))},_updateHash:function(a,b,c){c?a.replace(a.toString().replace(/(javascript:|#).*$/,"")+"#"+b):a.hash=b}});var v=g.View=function(a){this.cid=f.uniqueId("view");this._configure(a||{});this._ensureElement();this.initialize.apply(this,arguments);this.delegateEvents()},F=/^(\S+)\s*(.*)$/,w="model,collection,el,id,attributes,className,tagName".split(",");
|
||||||
|
f.extend(v.prototype,k,{tagName:"div",$:function(a){return this.$el.find(a)},initialize:function(){},render:function(){return this},remove:function(){this.$el.remove();return this},make:function(a,b,c){a=document.createElement(a);b&&i(a).attr(b);c&&i(a).html(c);return a},setElement:function(a,b){this.$el&&this.undelegateEvents();this.$el=a instanceof i?a:i(a);this.el=this.$el[0];!1!==b&&this.delegateEvents();return this},delegateEvents:function(a){if(a||(a=n(this,"events"))){this.undelegateEvents();
|
||||||
|
for(var b in a){var c=a[b];f.isFunction(c)||(c=this[a[b]]);if(!c)throw Error('Method "'+a[b]+'" does not exist');var d=b.match(F),e=d[1],d=d[2],c=f.bind(c,this),e=e+(".delegateEvents"+this.cid);""===d?this.$el.bind(e,c):this.$el.delegate(d,e,c)}}},undelegateEvents:function(){this.$el.unbind(".delegateEvents"+this.cid)},_configure:function(a){this.options&&(a=f.extend({},this.options,a));for(var b=0,c=w.length;b<c;b++){var d=w[b];a[d]&&(this[d]=a[d])}this.options=a},_ensureElement:function(){if(this.el)this.setElement(this.el,
|
||||||
|
!1);else{var a=n(this,"attributes")||{};this.id&&(a.id=this.id);this.className&&(a["class"]=this.className);this.setElement(this.make(this.tagName,a),!1)}}});o.extend=r.extend=u.extend=v.extend=function(a,b){var c=G(this,a,b);c.extend=this.extend;return c};var H={create:"POST",update:"PUT","delete":"DELETE",read:"GET"};g.sync=function(a,b,c){var d=H[a];c||(c={});var e={type:d,dataType:"json"};c.url||(e.url=n(b,"url")||t());if(!c.data&&b&&("create"==a||"update"==a))e.contentType="application/json",
|
||||||
|
e.data=JSON.stringify(b.toJSON());g.emulateJSON&&(e.contentType="application/x-www-form-urlencoded",e.data=e.data?{model:e.data}:{});if(g.emulateHTTP&&("PUT"===d||"DELETE"===d))g.emulateJSON&&(e.data._method=d),e.type="POST",e.beforeSend=function(a){a.setRequestHeader("X-HTTP-Method-Override",d)};"GET"!==e.type&&!g.emulateJSON&&(e.processData=!1);return i.ajax(f.extend(e,c))};g.wrapError=function(a,b,c){return function(d,e){e=d===b?e:d;a?a(b,e,c):b.trigger("error",b,e,c)}};var x=function(){},G=function(a,
|
||||||
|
b,c){var d;d=b&&b.hasOwnProperty("constructor")?b.constructor:function(){a.apply(this,arguments)};f.extend(d,a);x.prototype=a.prototype;d.prototype=new x;b&&f.extend(d.prototype,b);c&&f.extend(d,c);d.prototype.constructor=d;d.__super__=a.prototype;return d},n=function(a,b){return!a||!a[b]?null:f.isFunction(a[b])?a[b]():a[b]},t=function(){throw Error('A "url" property or function must be specified');}}).call(this);
|
||||||
16
docs/client-server/web/files/css
Normal file
16
docs/client-server/web/files/css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Droid Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Droid Sans'), local('DroidSans'), url(http://fonts.gstatic.com/s/droidsans/v5/s-BiyweUPV0v-yRb-cjciPk_vArhqVIZ0nv9q090hN8.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Droid Sans';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: local('Droid Sans Bold'), local('DroidSans-Bold'), url(http://fonts.gstatic.com/s/droidsans/v5/EFpQQyG9GqCrobXxL-KRMYWiMMZ7xLd792ULpGE4W_Y.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2212, U+2215, U+E0FF, U+EFFD, U+F000;
|
||||||
|
}
|
||||||
2278
docs/client-server/web/files/handlebars-1.0.0.js
Normal file
2278
docs/client-server/web/files/handlebars-1.0.0.js
Normal file
File diff suppressed because it is too large
Load Diff
1
docs/client-server/web/files/highlight.7.3.pack.js
Normal file
1
docs/client-server/web/files/highlight.7.3.pack.js
Normal file
File diff suppressed because one or more lines are too long
2
docs/client-server/web/files/jquery-1.8.0.min.js
vendored
Normal file
2
docs/client-server/web/files/jquery-1.8.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
18
docs/client-server/web/files/jquery.ba-bbq.min.js
vendored
Normal file
18
docs/client-server/web/files/jquery.ba-bbq.min.js
vendored
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* jQuery BBQ: Back Button & Query Library - v1.2.1 - 2/17/2010
|
||||||
|
* http://benalman.com/projects/jquery-bbq-plugin/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2010 "Cowboy" Ben Alman
|
||||||
|
* Dual licensed under the MIT and GPL licenses.
|
||||||
|
* http://benalman.com/about/license/
|
||||||
|
*/
|
||||||
|
(function($,p){var i,m=Array.prototype.slice,r=decodeURIComponent,a=$.param,c,l,v,b=$.bbq=$.bbq||{},q,u,j,e=$.event.special,d="hashchange",A="querystring",D="fragment",y="elemUrlAttr",g="location",k="href",t="src",x=/^.*\?|#.*$/g,w=/^.*\#/,h,C={};function E(F){return typeof F==="string"}function B(G){var F=m.call(arguments,1);return function(){return G.apply(this,F.concat(m.call(arguments)))}}function n(F){return F.replace(/^[^#]*#?(.*)$/,"$1")}function o(F){return F.replace(/(?:^[^?#]*\?([^#]*).*$)?.*/,"$1")}function f(H,M,F,I,G){var O,L,K,N,J;if(I!==i){K=F.match(H?/^([^#]*)\#?(.*)$/:/^([^#?]*)\??([^#]*)(#?.*)/);J=K[3]||"";if(G===2&&E(I)){L=I.replace(H?w:x,"")}else{N=l(K[2]);I=E(I)?l[H?D:A](I):I;L=G===2?I:G===1?$.extend({},I,N):$.extend({},N,I);L=a(L);if(H){L=L.replace(h,r)}}O=K[1]+(H?"#":L||!K[1]?"?":"")+L+J}else{O=M(F!==i?F:p[g][k])}return O}a[A]=B(f,0,o);a[D]=c=B(f,1,n);c.noEscape=function(G){G=G||"";var F=$.map(G.split(""),encodeURIComponent);h=new RegExp(F.join("|"),"g")};c.noEscape(",/");$.deparam=l=function(I,F){var H={},G={"true":!0,"false":!1,"null":null};$.each(I.replace(/\+/g," ").split("&"),function(L,Q){var K=Q.split("="),P=r(K[0]),J,O=H,M=0,R=P.split("]["),N=R.length-1;if(/\[/.test(R[0])&&/\]$/.test(R[N])){R[N]=R[N].replace(/\]$/,"");R=R.shift().split("[").concat(R);N=R.length-1}else{N=0}if(K.length===2){J=r(K[1]);if(F){J=J&&!isNaN(J)?+J:J==="undefined"?i:G[J]!==i?G[J]:J}if(N){for(;M<=N;M++){P=R[M]===""?O.length:R[M];O=O[P]=M<N?O[P]||(R[M+1]&&isNaN(R[M+1])?{}:[]):J}}else{if($.isArray(H[P])){H[P].push(J)}else{if(H[P]!==i){H[P]=[H[P],J]}else{H[P]=J}}}}else{if(P){H[P]=F?i:""}}});return H};function z(H,F,G){if(F===i||typeof F==="boolean"){G=F;F=a[H?D:A]()}else{F=E(F)?F.replace(H?w:x,""):F}return l(F,G)}l[A]=B(z,0);l[D]=v=B(z,1);$[y]||($[y]=function(F){return $.extend(C,F)})({a:k,base:k,iframe:t,img:t,input:t,form:"action",link:k,script:t});j=$[y];function s(I,G,H,F){if(!E(H)&&typeof H!=="object"){F=H;H=G;G=i}return this.each(function(){var L=$(this),J=G||j()[(this.nodeName||"").toLowerCase()]||"",K=J&&L.attr(J)||"";L.attr(J,a[I](K,H,F))})}$.fn[A]=B(s,A);$.fn[D]=B(s,D);b.pushState=q=function(I,F){if(E(I)&&/^#/.test(I)&&F===i){F=2}var H=I!==i,G=c(p[g][k],H?I:{},H?F:2);p[g][k]=G+(/#/.test(G)?"":"#")};b.getState=u=function(F,G){return F===i||typeof F==="boolean"?v(F):v(G)[F]};b.removeState=function(F){var G={};if(F!==i){G=u();$.each($.isArray(F)?F:arguments,function(I,H){delete G[H]})}q(G,2)};e[d]=$.extend(e[d],{add:function(F){var H;function G(J){var I=J[D]=c();J.getState=function(K,L){return K===i||typeof K==="boolean"?l(I,K):l(I,L)[K]};H.apply(this,arguments)}if($.isFunction(F)){H=F;return G}else{H=F.handler;F.handler=G}}})})(jQuery,this);
|
||||||
|
/*
|
||||||
|
* jQuery hashchange event - v1.2 - 2/11/2010
|
||||||
|
* http://benalman.com/projects/jquery-hashchange-plugin/
|
||||||
|
*
|
||||||
|
* Copyright (c) 2010 "Cowboy" Ben Alman
|
||||||
|
* Dual licensed under the MIT and GPL licenses.
|
||||||
|
* http://benalman.com/about/license/
|
||||||
|
*/
|
||||||
|
(function($,i,b){var j,k=$.event.special,c="location",d="hashchange",l="href",f=$.browser,g=document.documentMode,h=f.msie&&(g===b||g<8),e="on"+d in i&&!h;function a(m){m=m||i[c][l];return m.replace(/^[^#]*#?(.*)$/,"$1")}$[d+"Delay"]=100;k[d]=$.extend(k[d],{setup:function(){if(e){return false}$(j.start)},teardown:function(){if(e){return false}$(j.stop)}});j=(function(){var m={},r,n,o,q;function p(){o=q=function(s){return s};if(h){n=$('<iframe src="javascript:0"/>').hide().insertAfter("body")[0].contentWindow;q=function(){return a(n.document[c][l])};o=function(u,s){if(u!==s){var t=n.document;t.open().close();t[c].hash="#"+u}};o(a())}}m.start=function(){if(r){return}var t=a();o||p();(function s(){var v=a(),u=q(t);if(v!==t){o(t=v,u);$(i).trigger(d)}else{if(u!==t){i[c][l]=i[c][l].replace(/#.*/,"")+"#"+u}}r=setTimeout(s,$[d+"Delay"])})()};m.stop=function(){if(!n){r&&clearTimeout(r);r=0}};return m})()})(jQuery,this);
|
||||||
1
docs/client-server/web/files/jquery.slideto.min.js
vendored
Normal file
1
docs/client-server/web/files/jquery.slideto.min.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
(function(b){b.fn.slideto=function(a){a=b.extend({slide_duration:"slow",highlight_duration:3E3,highlight:true,highlight_color:"#FFFF99"},a);return this.each(function(){obj=b(this);b("body").animate({scrollTop:obj.offset().top},a.slide_duration,function(){a.highlight&&b.ui.version&&obj.effect("highlight",{color:a.highlight_color},a.highlight_duration)})})}})(jQuery);
|
||||||
8
docs/client-server/web/files/jquery.wiggle.min.js
vendored
Normal file
8
docs/client-server/web/files/jquery.wiggle.min.js
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
jQuery Wiggle
|
||||||
|
Author: WonderGroup, Jordan Thomas
|
||||||
|
URL: http://labs.wondergroup.com/demos/mini-ui/index.html
|
||||||
|
License: MIT (http://en.wikipedia.org/wiki/MIT_License)
|
||||||
|
*/
|
||||||
|
jQuery.fn.wiggle=function(o){var d={speed:50,wiggles:3,travel:5,callback:null};var o=jQuery.extend(d,o);return this.each(function(){var cache=this;var wrap=jQuery(this).wrap('<div class="wiggle-wrap"></div>').css("position","relative");var calls=0;for(i=1;i<=o.wiggles;i++){jQuery(this).animate({left:"-="+o.travel},o.speed).animate({left:"+="+o.travel*2},o.speed*2).animate({left:"-="+o.travel},o.speed,function(){calls++;if(jQuery(cache).parent().hasClass('wiggle-wrap')){jQuery(cache).parent().replaceWith(cache);}
|
||||||
|
if(calls==o.wiggles&&jQuery.isFunction(o.callback)){o.callback();}});}});};
|
||||||
125
docs/client-server/web/files/reset.css
Normal file
125
docs/client-server/web/files/reset.css
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/* http://meyerweb.com/eric/tools/css/reset/ v2.0 | 20110126 */
|
||||||
|
html,
|
||||||
|
body,
|
||||||
|
div,
|
||||||
|
span,
|
||||||
|
applet,
|
||||||
|
object,
|
||||||
|
iframe,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
p,
|
||||||
|
blockquote,
|
||||||
|
pre,
|
||||||
|
a,
|
||||||
|
abbr,
|
||||||
|
acronym,
|
||||||
|
address,
|
||||||
|
big,
|
||||||
|
cite,
|
||||||
|
code,
|
||||||
|
del,
|
||||||
|
dfn,
|
||||||
|
em,
|
||||||
|
img,
|
||||||
|
ins,
|
||||||
|
kbd,
|
||||||
|
q,
|
||||||
|
s,
|
||||||
|
samp,
|
||||||
|
small,
|
||||||
|
strike,
|
||||||
|
strong,
|
||||||
|
sub,
|
||||||
|
sup,
|
||||||
|
tt,
|
||||||
|
var,
|
||||||
|
b,
|
||||||
|
u,
|
||||||
|
i,
|
||||||
|
center,
|
||||||
|
dl,
|
||||||
|
dt,
|
||||||
|
dd,
|
||||||
|
ol,
|
||||||
|
ul,
|
||||||
|
li,
|
||||||
|
fieldset,
|
||||||
|
form,
|
||||||
|
label,
|
||||||
|
legend,
|
||||||
|
table,
|
||||||
|
caption,
|
||||||
|
tbody,
|
||||||
|
tfoot,
|
||||||
|
thead,
|
||||||
|
tr,
|
||||||
|
th,
|
||||||
|
td,
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
canvas,
|
||||||
|
details,
|
||||||
|
embed,
|
||||||
|
figure,
|
||||||
|
figcaption,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
output,
|
||||||
|
ruby,
|
||||||
|
section,
|
||||||
|
summary,
|
||||||
|
time,
|
||||||
|
mark,
|
||||||
|
audio,
|
||||||
|
video {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-size: 100%;
|
||||||
|
font: inherit;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
blockquote,
|
||||||
|
q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
blockquote:before,
|
||||||
|
blockquote:after,
|
||||||
|
q:before,
|
||||||
|
q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
1221
docs/client-server/web/files/screen.css
Normal file
1221
docs/client-server/web/files/screen.css
Normal file
File diff suppressed because it is too large
Load Diff
2765
docs/client-server/web/files/shred.bundle.js
Normal file
2765
docs/client-server/web/files/shred.bundle.js
Normal file
File diff suppressed because it is too large
Load Diff
211
docs/client-server/web/files/swagger-oauth.js
Normal file
211
docs/client-server/web/files/swagger-oauth.js
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
var appName;
|
||||||
|
var popupMask;
|
||||||
|
var popupDialog;
|
||||||
|
var clientId;
|
||||||
|
var realm;
|
||||||
|
|
||||||
|
function handleLogin() {
|
||||||
|
var scopes = [];
|
||||||
|
|
||||||
|
if(window.swaggerUi.api.authSchemes
|
||||||
|
&& window.swaggerUi.api.authSchemes.oauth2
|
||||||
|
&& window.swaggerUi.api.authSchemes.oauth2.scopes) {
|
||||||
|
scopes = window.swaggerUi.api.authSchemes.oauth2.scopes;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(window.swaggerUi.api
|
||||||
|
&& window.swaggerUi.api.info) {
|
||||||
|
appName = window.swaggerUi.api.info.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(popupDialog.length > 0)
|
||||||
|
popupDialog = popupDialog.last();
|
||||||
|
else {
|
||||||
|
popupDialog = $(
|
||||||
|
[
|
||||||
|
'<div class="api-popup-dialog">',
|
||||||
|
'<div class="api-popup-title">Select OAuth2.0 Scopes</div>',
|
||||||
|
'<div class="api-popup-content">',
|
||||||
|
'<p>Scopes are used to grant an application different levels of access to data on behalf of the end user. Each API may declare one or more scopes.',
|
||||||
|
'<a href="#">Learn how to use</a>',
|
||||||
|
'</p>',
|
||||||
|
'<p><strong>' + appName + '</strong> API requires the following scopes. Select which ones you want to grant to Swagger UI.</p>',
|
||||||
|
'<ul class="api-popup-scopes">',
|
||||||
|
'</ul>',
|
||||||
|
'<p class="error-msg"></p>',
|
||||||
|
'<div class="api-popup-actions"><button class="api-popup-authbtn api-button green" type="button">Authorize</button><button class="api-popup-cancel api-button gray" type="button">Cancel</button></div>',
|
||||||
|
'</div>',
|
||||||
|
'</div>'].join(''));
|
||||||
|
$(document.body).append(popupDialog);
|
||||||
|
|
||||||
|
popup = popupDialog.find('ul.api-popup-scopes').empty();
|
||||||
|
for (i = 0; i < scopes.length; i ++) {
|
||||||
|
scope = scopes[i];
|
||||||
|
str = '<li><input type="checkbox" id="scope_' + i + '" scope="' + scope.scope + '"/>' + '<label for="scope_' + i + '">' + scope.scope;
|
||||||
|
if (scope.description) {
|
||||||
|
str += '<br/><span class="api-scope-desc">' + scope.description + '</span>';
|
||||||
|
}
|
||||||
|
str += '</label></li>';
|
||||||
|
popup.append(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var $win = $(window),
|
||||||
|
dw = $win.width(),
|
||||||
|
dh = $win.height(),
|
||||||
|
st = $win.scrollTop(),
|
||||||
|
dlgWd = popupDialog.outerWidth(),
|
||||||
|
dlgHt = popupDialog.outerHeight(),
|
||||||
|
top = (dh -dlgHt)/2 + st,
|
||||||
|
left = (dw - dlgWd)/2;
|
||||||
|
|
||||||
|
popupDialog.css({
|
||||||
|
top: (top < 0? 0 : top) + 'px',
|
||||||
|
left: (left < 0? 0 : left) + 'px'
|
||||||
|
});
|
||||||
|
|
||||||
|
popupDialog.find('button.api-popup-cancel').click(function() {
|
||||||
|
popupMask.hide();
|
||||||
|
popupDialog.hide();
|
||||||
|
});
|
||||||
|
popupDialog.find('button.api-popup-authbtn').click(function() {
|
||||||
|
popupMask.hide();
|
||||||
|
popupDialog.hide();
|
||||||
|
|
||||||
|
var authSchemes = window.swaggerUi.api.authSchemes;
|
||||||
|
var host = window.location;
|
||||||
|
var redirectUrl = host.protocol + '//' + host.host + "/o2c.html";
|
||||||
|
var url = null;
|
||||||
|
|
||||||
|
var p = window.swaggerUi.api.authSchemes;
|
||||||
|
for (var key in p) {
|
||||||
|
if (p.hasOwnProperty(key)) {
|
||||||
|
var o = p[key].grantTypes;
|
||||||
|
for(var t in o) {
|
||||||
|
if(o.hasOwnProperty(t) && t === 'implicit') {
|
||||||
|
var dets = o[t];
|
||||||
|
url = dets.loginEndpoint.url + "?response_type=token";
|
||||||
|
window.swaggerUi.tokenName = dets.tokenName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var scopes = []
|
||||||
|
var o = $('.api-popup-scopes').find('input:checked');
|
||||||
|
|
||||||
|
for(k =0; k < o.length; k++) {
|
||||||
|
scopes.push($(o[k]).attr("scope"));
|
||||||
|
}
|
||||||
|
|
||||||
|
window.enabledScopes=scopes;
|
||||||
|
|
||||||
|
url += '&redirect_uri=' + encodeURIComponent(redirectUrl);
|
||||||
|
url += '&realm=' + encodeURIComponent(realm);
|
||||||
|
url += '&client_id=' + encodeURIComponent(clientId);
|
||||||
|
url += '&scope=' + encodeURIComponent(scopes);
|
||||||
|
|
||||||
|
window.open(url);
|
||||||
|
});
|
||||||
|
|
||||||
|
popupMask.show();
|
||||||
|
popupDialog.show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function handleLogout() {
|
||||||
|
for(key in window.authorizations.authz){
|
||||||
|
window.authorizations.remove(key)
|
||||||
|
}
|
||||||
|
window.enabledScopes = null;
|
||||||
|
$('.api-ic.ic-on').addClass('ic-off');
|
||||||
|
$('.api-ic.ic-on').removeClass('ic-on');
|
||||||
|
|
||||||
|
// set the info box
|
||||||
|
$('.api-ic.ic-warning').addClass('ic-error');
|
||||||
|
$('.api-ic.ic-warning').removeClass('ic-warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
function initOAuth(opts) {
|
||||||
|
var o = (opts||{});
|
||||||
|
var errors = [];
|
||||||
|
|
||||||
|
appName = (o.appName||errors.push("missing appName"));
|
||||||
|
popupMask = (o.popupMask||$('#api-common-mask'));
|
||||||
|
popupDialog = (o.popupDialog||$('.api-popup-dialog'));
|
||||||
|
clientId = (o.clientId||errors.push("missing client id"));
|
||||||
|
realm = (o.realm||errors.push("missing realm"));
|
||||||
|
|
||||||
|
if(errors.length > 0){
|
||||||
|
log("auth unable initialize oauth: " + errors);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('pre code').each(function(i, e) {hljs.highlightBlock(e)});
|
||||||
|
$('.api-ic').click(function(s) {
|
||||||
|
if($(s.target).hasClass('ic-off'))
|
||||||
|
handleLogin();
|
||||||
|
else {
|
||||||
|
handleLogout();
|
||||||
|
}
|
||||||
|
false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onOAuthComplete(token) {
|
||||||
|
if(token) {
|
||||||
|
if(token.error) {
|
||||||
|
var checkbox = $('input[type=checkbox],.secured')
|
||||||
|
checkbox.each(function(pos){
|
||||||
|
checkbox[pos].checked = false;
|
||||||
|
});
|
||||||
|
alert(token.error);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
var b = token[window.swaggerUi.tokenName];
|
||||||
|
if(b){
|
||||||
|
// if all roles are satisfied
|
||||||
|
var o = null;
|
||||||
|
$.each($('.auth #api_information_panel'), function(k, v) {
|
||||||
|
var children = v;
|
||||||
|
if(children && children.childNodes) {
|
||||||
|
var requiredScopes = [];
|
||||||
|
$.each((children.childNodes), function (k1, v1){
|
||||||
|
var inner = v1.innerHTML;
|
||||||
|
if(inner)
|
||||||
|
requiredScopes.push(inner);
|
||||||
|
});
|
||||||
|
var diff = [];
|
||||||
|
for(var i=0; i < requiredScopes.length; i++) {
|
||||||
|
var s = requiredScopes[i];
|
||||||
|
if(window.enabledScopes && window.enabledScopes.indexOf(s) == -1) {
|
||||||
|
diff.push(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(diff.length > 0){
|
||||||
|
o = v.parentNode;
|
||||||
|
$(o.parentNode).find('.api-ic.ic-on').addClass('ic-off');
|
||||||
|
$(o.parentNode).find('.api-ic.ic-on').removeClass('ic-on');
|
||||||
|
|
||||||
|
// sorry, not all scopes are satisfied
|
||||||
|
$(o).find('.api-ic').addClass('ic-warning');
|
||||||
|
$(o).find('.api-ic').removeClass('ic-error');
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
o = v.parentNode;
|
||||||
|
$(o.parentNode).find('.api-ic.ic-off').addClass('ic-on');
|
||||||
|
$(o.parentNode).find('.api-ic.ic-off').removeClass('ic-off');
|
||||||
|
|
||||||
|
// all scopes are satisfied
|
||||||
|
$(o).find('.api-ic').addClass('ic-info');
|
||||||
|
$(o).find('.api-ic').removeClass('ic-warning');
|
||||||
|
$(o).find('.api-ic').removeClass('ic-error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.authorizations.add("oauth2", new ApiKeyAuthorization("Authorization", "Bearer " + b, "header"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2315
docs/client-server/web/files/swagger-ui.js
Normal file
2315
docs/client-server/web/files/swagger-ui.js
Normal file
File diff suppressed because it is too large
Load Diff
1604
docs/client-server/web/files/swagger.js
Normal file
1604
docs/client-server/web/files/swagger.js
Normal file
File diff suppressed because it is too large
Load Diff
32
docs/client-server/web/files/underscore-min.js
vendored
Normal file
32
docs/client-server/web/files/underscore-min.js
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Underscore.js 1.3.3
|
||||||
|
// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
|
||||||
|
// Underscore is freely distributable under the MIT license.
|
||||||
|
// Portions of Underscore are inspired or borrowed from Prototype,
|
||||||
|
// Oliver Steele's Functional, and John Resig's Micro-Templating.
|
||||||
|
// For all details and documentation:
|
||||||
|
// http://documentcloud.github.com/underscore
|
||||||
|
(function(){function r(a,c,d){if(a===c)return 0!==a||1/a==1/c;if(null==a||null==c)return a===c;a._chain&&(a=a._wrapped);c._chain&&(c=c._wrapped);if(a.isEqual&&b.isFunction(a.isEqual))return a.isEqual(c);if(c.isEqual&&b.isFunction(c.isEqual))return c.isEqual(a);var e=l.call(a);if(e!=l.call(c))return!1;switch(e){case "[object String]":return a==""+c;case "[object Number]":return a!=+a?c!=+c:0==a?1/a==1/c:a==+c;case "[object Date]":case "[object Boolean]":return+a==+c;case "[object RegExp]":return a.source==
|
||||||
|
c.source&&a.global==c.global&&a.multiline==c.multiline&&a.ignoreCase==c.ignoreCase}if("object"!=typeof a||"object"!=typeof c)return!1;for(var f=d.length;f--;)if(d[f]==a)return!0;d.push(a);var f=0,g=!0;if("[object Array]"==e){if(f=a.length,g=f==c.length)for(;f--&&(g=f in a==f in c&&r(a[f],c[f],d)););}else{if("constructor"in a!="constructor"in c||a.constructor!=c.constructor)return!1;for(var h in a)if(b.has(a,h)&&(f++,!(g=b.has(c,h)&&r(a[h],c[h],d))))break;if(g){for(h in c)if(b.has(c,h)&&!f--)break;
|
||||||
|
g=!f}}d.pop();return g}var s=this,I=s._,o={},k=Array.prototype,p=Object.prototype,i=k.slice,J=k.unshift,l=p.toString,K=p.hasOwnProperty,y=k.forEach,z=k.map,A=k.reduce,B=k.reduceRight,C=k.filter,D=k.every,E=k.some,q=k.indexOf,F=k.lastIndexOf,p=Array.isArray,L=Object.keys,t=Function.prototype.bind,b=function(a){return new m(a)};"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(exports=module.exports=b),exports._=b):s._=b;b.VERSION="1.3.3";var j=b.each=b.forEach=function(a,
|
||||||
|
c,d){if(a!=null)if(y&&a.forEach===y)a.forEach(c,d);else if(a.length===+a.length)for(var e=0,f=a.length;e<f;e++){if(e in a&&c.call(d,a[e],e,a)===o)break}else for(e in a)if(b.has(a,e)&&c.call(d,a[e],e,a)===o)break};b.map=b.collect=function(a,c,b){var e=[];if(a==null)return e;if(z&&a.map===z)return a.map(c,b);j(a,function(a,g,h){e[e.length]=c.call(b,a,g,h)});if(a.length===+a.length)e.length=a.length;return e};b.reduce=b.foldl=b.inject=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(A&&
|
||||||
|
a.reduce===A){e&&(c=b.bind(c,e));return f?a.reduce(c,d):a.reduce(c)}j(a,function(a,b,i){if(f)d=c.call(e,d,a,b,i);else{d=a;f=true}});if(!f)throw new TypeError("Reduce of empty array with no initial value");return d};b.reduceRight=b.foldr=function(a,c,d,e){var f=arguments.length>2;a==null&&(a=[]);if(B&&a.reduceRight===B){e&&(c=b.bind(c,e));return f?a.reduceRight(c,d):a.reduceRight(c)}var g=b.toArray(a).reverse();e&&!f&&(c=b.bind(c,e));return f?b.reduce(g,c,d,e):b.reduce(g,c)};b.find=b.detect=function(a,
|
||||||
|
c,b){var e;G(a,function(a,g,h){if(c.call(b,a,g,h)){e=a;return true}});return e};b.filter=b.select=function(a,c,b){var e=[];if(a==null)return e;if(C&&a.filter===C)return a.filter(c,b);j(a,function(a,g,h){c.call(b,a,g,h)&&(e[e.length]=a)});return e};b.reject=function(a,c,b){var e=[];if(a==null)return e;j(a,function(a,g,h){c.call(b,a,g,h)||(e[e.length]=a)});return e};b.every=b.all=function(a,c,b){var e=true;if(a==null)return e;if(D&&a.every===D)return a.every(c,b);j(a,function(a,g,h){if(!(e=e&&c.call(b,
|
||||||
|
a,g,h)))return o});return!!e};var G=b.some=b.any=function(a,c,d){c||(c=b.identity);var e=false;if(a==null)return e;if(E&&a.some===E)return a.some(c,d);j(a,function(a,b,h){if(e||(e=c.call(d,a,b,h)))return o});return!!e};b.include=b.contains=function(a,c){var b=false;if(a==null)return b;if(q&&a.indexOf===q)return a.indexOf(c)!=-1;return b=G(a,function(a){return a===c})};b.invoke=function(a,c){var d=i.call(arguments,2);return b.map(a,function(a){return(b.isFunction(c)?c||a:a[c]).apply(a,d)})};b.pluck=
|
||||||
|
function(a,c){return b.map(a,function(a){return a[c]})};b.max=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.max.apply(Math,a);if(!c&&b.isEmpty(a))return-Infinity;var e={computed:-Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b>=e.computed&&(e={value:a,computed:b})});return e.value};b.min=function(a,c,d){if(!c&&b.isArray(a)&&a[0]===+a[0])return Math.min.apply(Math,a);if(!c&&b.isEmpty(a))return Infinity;var e={computed:Infinity};j(a,function(a,b,h){b=c?c.call(d,a,b,h):a;b<e.computed&&
|
||||||
|
(e={value:a,computed:b})});return e.value};b.shuffle=function(a){var b=[],d;j(a,function(a,f){d=Math.floor(Math.random()*(f+1));b[f]=b[d];b[d]=a});return b};b.sortBy=function(a,c,d){var e=b.isFunction(c)?c:function(a){return a[c]};return b.pluck(b.map(a,function(a,b,c){return{value:a,criteria:e.call(d,a,b,c)}}).sort(function(a,b){var c=a.criteria,d=b.criteria;return c===void 0?1:d===void 0?-1:c<d?-1:c>d?1:0}),"value")};b.groupBy=function(a,c){var d={},e=b.isFunction(c)?c:function(a){return a[c]};
|
||||||
|
j(a,function(a,b){var c=e(a,b);(d[c]||(d[c]=[])).push(a)});return d};b.sortedIndex=function(a,c,d){d||(d=b.identity);for(var e=0,f=a.length;e<f;){var g=e+f>>1;d(a[g])<d(c)?e=g+1:f=g}return e};b.toArray=function(a){return!a?[]:b.isArray(a)||b.isArguments(a)?i.call(a):a.toArray&&b.isFunction(a.toArray)?a.toArray():b.values(a)};b.size=function(a){return b.isArray(a)?a.length:b.keys(a).length};b.first=b.head=b.take=function(a,b,d){return b!=null&&!d?i.call(a,0,b):a[0]};b.initial=function(a,b,d){return i.call(a,
|
||||||
|
0,a.length-(b==null||d?1:b))};b.last=function(a,b,d){return b!=null&&!d?i.call(a,Math.max(a.length-b,0)):a[a.length-1]};b.rest=b.tail=function(a,b,d){return i.call(a,b==null||d?1:b)};b.compact=function(a){return b.filter(a,function(a){return!!a})};b.flatten=function(a,c){return b.reduce(a,function(a,e){if(b.isArray(e))return a.concat(c?e:b.flatten(e));a[a.length]=e;return a},[])};b.without=function(a){return b.difference(a,i.call(arguments,1))};b.uniq=b.unique=function(a,c,d){var d=d?b.map(a,d):a,
|
||||||
|
e=[];a.length<3&&(c=true);b.reduce(d,function(d,g,h){if(c?b.last(d)!==g||!d.length:!b.include(d,g)){d.push(g);e.push(a[h])}return d},[]);return e};b.union=function(){return b.uniq(b.flatten(arguments,true))};b.intersection=b.intersect=function(a){var c=i.call(arguments,1);return b.filter(b.uniq(a),function(a){return b.every(c,function(c){return b.indexOf(c,a)>=0})})};b.difference=function(a){var c=b.flatten(i.call(arguments,1),true);return b.filter(a,function(a){return!b.include(c,a)})};b.zip=function(){for(var a=
|
||||||
|
i.call(arguments),c=b.max(b.pluck(a,"length")),d=Array(c),e=0;e<c;e++)d[e]=b.pluck(a,""+e);return d};b.indexOf=function(a,c,d){if(a==null)return-1;var e;if(d){d=b.sortedIndex(a,c);return a[d]===c?d:-1}if(q&&a.indexOf===q)return a.indexOf(c);d=0;for(e=a.length;d<e;d++)if(d in a&&a[d]===c)return d;return-1};b.lastIndexOf=function(a,b){if(a==null)return-1;if(F&&a.lastIndexOf===F)return a.lastIndexOf(b);for(var d=a.length;d--;)if(d in a&&a[d]===b)return d;return-1};b.range=function(a,b,d){if(arguments.length<=
|
||||||
|
1){b=a||0;a=0}for(var d=arguments[2]||1,e=Math.max(Math.ceil((b-a)/d),0),f=0,g=Array(e);f<e;){g[f++]=a;a=a+d}return g};var H=function(){};b.bind=function(a,c){var d,e;if(a.bind===t&&t)return t.apply(a,i.call(arguments,1));if(!b.isFunction(a))throw new TypeError;e=i.call(arguments,2);return d=function(){if(!(this instanceof d))return a.apply(c,e.concat(i.call(arguments)));H.prototype=a.prototype;var b=new H,g=a.apply(b,e.concat(i.call(arguments)));return Object(g)===g?g:b}};b.bindAll=function(a){var c=
|
||||||
|
i.call(arguments,1);c.length==0&&(c=b.functions(a));j(c,function(c){a[c]=b.bind(a[c],a)});return a};b.memoize=function(a,c){var d={};c||(c=b.identity);return function(){var e=c.apply(this,arguments);return b.has(d,e)?d[e]:d[e]=a.apply(this,arguments)}};b.delay=function(a,b){var d=i.call(arguments,2);return setTimeout(function(){return a.apply(null,d)},b)};b.defer=function(a){return b.delay.apply(b,[a,1].concat(i.call(arguments,1)))};b.throttle=function(a,c){var d,e,f,g,h,i,j=b.debounce(function(){h=
|
||||||
|
g=false},c);return function(){d=this;e=arguments;f||(f=setTimeout(function(){f=null;h&&a.apply(d,e);j()},c));g?h=true:i=a.apply(d,e);j();g=true;return i}};b.debounce=function(a,b,d){var e;return function(){var f=this,g=arguments;d&&!e&&a.apply(f,g);clearTimeout(e);e=setTimeout(function(){e=null;d||a.apply(f,g)},b)}};b.once=function(a){var b=false,d;return function(){if(b)return d;b=true;return d=a.apply(this,arguments)}};b.wrap=function(a,b){return function(){var d=[a].concat(i.call(arguments,0));
|
||||||
|
return b.apply(this,d)}};b.compose=function(){var a=arguments;return function(){for(var b=arguments,d=a.length-1;d>=0;d--)b=[a[d].apply(this,b)];return b[0]}};b.after=function(a,b){return a<=0?b():function(){if(--a<1)return b.apply(this,arguments)}};b.keys=L||function(a){if(a!==Object(a))throw new TypeError("Invalid object");var c=[],d;for(d in a)b.has(a,d)&&(c[c.length]=d);return c};b.values=function(a){return b.map(a,b.identity)};b.functions=b.methods=function(a){var c=[],d;for(d in a)b.isFunction(a[d])&&
|
||||||
|
c.push(d);return c.sort()};b.extend=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]=b[d]});return a};b.pick=function(a){var c={};j(b.flatten(i.call(arguments,1)),function(b){b in a&&(c[b]=a[b])});return c};b.defaults=function(a){j(i.call(arguments,1),function(b){for(var d in b)a[d]==null&&(a[d]=b[d])});return a};b.clone=function(a){return!b.isObject(a)?a:b.isArray(a)?a.slice():b.extend({},a)};b.tap=function(a,b){b(a);return a};b.isEqual=function(a,b){return r(a,b,[])};b.isEmpty=
|
||||||
|
function(a){if(a==null)return true;if(b.isArray(a)||b.isString(a))return a.length===0;for(var c in a)if(b.has(a,c))return false;return true};b.isElement=function(a){return!!(a&&a.nodeType==1)};b.isArray=p||function(a){return l.call(a)=="[object Array]"};b.isObject=function(a){return a===Object(a)};b.isArguments=function(a){return l.call(a)=="[object Arguments]"};b.isArguments(arguments)||(b.isArguments=function(a){return!(!a||!b.has(a,"callee"))});b.isFunction=function(a){return l.call(a)=="[object Function]"};
|
||||||
|
b.isString=function(a){return l.call(a)=="[object String]"};b.isNumber=function(a){return l.call(a)=="[object Number]"};b.isFinite=function(a){return b.isNumber(a)&&isFinite(a)};b.isNaN=function(a){return a!==a};b.isBoolean=function(a){return a===true||a===false||l.call(a)=="[object Boolean]"};b.isDate=function(a){return l.call(a)=="[object Date]"};b.isRegExp=function(a){return l.call(a)=="[object RegExp]"};b.isNull=function(a){return a===null};b.isUndefined=function(a){return a===void 0};b.has=function(a,
|
||||||
|
b){return K.call(a,b)};b.noConflict=function(){s._=I;return this};b.identity=function(a){return a};b.times=function(a,b,d){for(var e=0;e<a;e++)b.call(d,e)};b.escape=function(a){return(""+a).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'").replace(/\//g,"/")};b.result=function(a,c){if(a==null)return null;var d=a[c];return b.isFunction(d)?d.call(a):d};b.mixin=function(a){j(b.functions(a),function(c){M(c,b[c]=a[c])})};var N=0;b.uniqueId=
|
||||||
|
function(a){var b=N++;return a?a+b:b};b.templateSettings={evaluate:/<%([\s\S]+?)%>/g,interpolate:/<%=([\s\S]+?)%>/g,escape:/<%-([\s\S]+?)%>/g};var u=/.^/,n={"\\":"\\","'":"'",r:"\r",n:"\n",t:"\t",u2028:"\u2028",u2029:"\u2029"},v;for(v in n)n[n[v]]=v;var O=/\\|'|\r|\n|\t|\u2028|\u2029/g,P=/\\(\\|'|r|n|t|u2028|u2029)/g,w=function(a){return a.replace(P,function(a,b){return n[b]})};b.template=function(a,c,d){d=b.defaults(d||{},b.templateSettings);a="__p+='"+a.replace(O,function(a){return"\\"+n[a]}).replace(d.escape||
|
||||||
|
u,function(a,b){return"'+\n_.escape("+w(b)+")+\n'"}).replace(d.interpolate||u,function(a,b){return"'+\n("+w(b)+")+\n'"}).replace(d.evaluate||u,function(a,b){return"';\n"+w(b)+"\n;__p+='"})+"';\n";d.variable||(a="with(obj||{}){\n"+a+"}\n");var a="var __p='';var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n"+a+"return __p;\n",e=new Function(d.variable||"obj","_",a);if(c)return e(c,b);c=function(a){return e.call(this,a,b)};c.source="function("+(d.variable||"obj")+"){\n"+a+"}";return c};
|
||||||
|
b.chain=function(a){return b(a).chain()};var m=function(a){this._wrapped=a};b.prototype=m.prototype;var x=function(a,c){return c?b(a).chain():a},M=function(a,c){m.prototype[a]=function(){var a=i.call(arguments);J.call(a,this._wrapped);return x(c.apply(b,a),this._chain)}};b.mixin(b);j("pop,push,reverse,shift,sort,splice,unshift".split(","),function(a){var b=k[a];m.prototype[a]=function(){var d=this._wrapped;b.apply(d,arguments);var e=d.length;(a=="shift"||a=="splice")&&e===0&&delete d[0];return x(d,
|
||||||
|
this._chain)}});j(["concat","join","slice"],function(a){var b=k[a];m.prototype[a]=function(){return x(b.apply(this._wrapped,arguments),this._chain)}});m.prototype.chain=function(){this._chain=true;return this};m.prototype.value=function(){return this._wrapped}}).call(this);
|
||||||
78
docs/client-server/web/swagger.html
Normal file
78
docs/client-server/web/swagger.html
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html><head><meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
|
||||||
|
<title>Matrix Client-Server API Documentation</title>
|
||||||
|
<link href="./files/css" rel="stylesheet" type="text/css">
|
||||||
|
<link href="./files/reset.css" media="screen" rel="stylesheet" type="text/css">
|
||||||
|
<link href="./files/screen.css" media="screen" rel="stylesheet" type="text/css">
|
||||||
|
<link href="./files/reset.css" media="print" rel="stylesheet" type="text/css">
|
||||||
|
<link href="./files/screen.css" media="print" rel="stylesheet" type="text/css">
|
||||||
|
<script type="text/javascript" src="./files/shred.bundle.js"></script>
|
||||||
|
<script src="./files/jquery-1.8.0.min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/jquery.slideto.min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/jquery.wiggle.min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/jquery.ba-bbq.min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/handlebars-1.0.0.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/underscore-min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/backbone-min.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/swagger.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/swagger-ui.js" type="text/javascript"></script>
|
||||||
|
<script src="./files/highlight.7.3.pack.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<!-- enabling this will enable oauth2 implicit scope support -->
|
||||||
|
<script src="./files/swagger-oauth.js" type="text/javascript"></script>
|
||||||
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
$(function () {
|
||||||
|
window.swaggerUi = new SwaggerUi({
|
||||||
|
url: "http://localhost:8000/swagger_matrix/api-docs",
|
||||||
|
dom_id: "swagger-ui-container",
|
||||||
|
supportedSubmitMethods: ['get', 'post', 'put', 'delete'],
|
||||||
|
onComplete: function(swaggerApi, swaggerUi){
|
||||||
|
log("Loaded SwaggerUI");
|
||||||
|
|
||||||
|
if(typeof initOAuth == "function") {
|
||||||
|
initOAuth({
|
||||||
|
clientId: "your-client-id",
|
||||||
|
realm: "your-realms",
|
||||||
|
appName: "your-app-name"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
$('pre code').each(function(i, e) {
|
||||||
|
hljs.highlightBlock(e)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onFailure: function(data) {
|
||||||
|
log("Unable to Load SwaggerUI");
|
||||||
|
},
|
||||||
|
docExpansion: "none"
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#input_apiKey').change(function() {
|
||||||
|
var key = $('#input_apiKey')[0].value;
|
||||||
|
log("key: " + key);
|
||||||
|
if(key && key.trim() != "") {
|
||||||
|
log("added key " + key);
|
||||||
|
window.authorizations.add("key", new ApiKeyAuthorization("access_token", key, "query"));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.swaggerUi.load();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="swagger-section">
|
||||||
|
<div id="header">
|
||||||
|
<div class="swagger-ui-wrap">
|
||||||
|
<a id="logo" href="http://swagger.wordnik.com/">swagger</a>
|
||||||
|
<form id="api_selector">
|
||||||
|
<div class="input"><input placeholder="http://example.com/api" id="input_baseUrl" name="baseUrl" type="text"></div>
|
||||||
|
<div class="input"><input placeholder="access_token" id="input_apiKey" name="apiKey" type="text"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="message-bar" class="swagger-ui-wrap message-fail">Can't read from server. It may not have the appropriate access-control-origin settings.</div>
|
||||||
|
<div id="swagger-ui-container" class="swagger-ui-wrap"></div>
|
||||||
|
|
||||||
|
|
||||||
|
</body></html>
|
||||||
@@ -155,7 +155,7 @@ Protocol URLs
|
|||||||
|
|
||||||
All these URLs are namespaced within a prefix of
|
All these URLs are namespaced within a prefix of
|
||||||
|
|
||||||
/matrix/federation/v1/...
|
/_matrix/federation/v1/...
|
||||||
|
|
||||||
For active pushing of messages representing live activity "as it happens":
|
For active pushing of messages representing live activity "as it happens":
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ namespaced to the home server which allocated the account and looks like::
|
|||||||
|
|
||||||
@localpart:domain
|
@localpart:domain
|
||||||
|
|
||||||
The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user.
|
The ``localpart`` of a user ID may be a user name, or an opaque ID identifying this user. They are
|
||||||
|
case-insensitive.
|
||||||
|
|
||||||
A "Home Server" is a server which provides C-S APIs and has the ability to federate with other HSes.
|
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
|
It is typically responsible for multiple clients. "Federation" is the term used to describe the
|
||||||
@@ -60,7 +60,8 @@ identified via a "Room ID", which look like::
|
|||||||
|
|
||||||
There is exactly one room ID for each room. Whilst the room ID does contain a
|
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, 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.
|
domain specified. Room IDs are not meant to be human readable. They ARE
|
||||||
|
case-sensitive.
|
||||||
|
|
||||||
The following diagram shows an ``m.room.message`` event being sent in the room
|
The following diagram shows an ``m.room.message`` event being sent in the room
|
||||||
``!qporfwt:matrix.org``::
|
``!qporfwt:matrix.org``::
|
||||||
@@ -102,10 +103,10 @@ Each room can also have multiple "Room Aliases", which looks like::
|
|||||||
|
|
||||||
A room alias "points" to a room ID. The room ID the alias is pointing to can be obtained
|
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
|
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
|
which can be used to publicise rooms. They are case-insensitive. Note that the mapping
|
||||||
room ID is not fixed, and may change over time to point to a different room ID. For this
|
from a room alias to a room ID is not fixed, and may change over time to point to a
|
||||||
reason, Clients SHOULD resolve the room alias to a room ID once and then use that ID on
|
different room ID. For this reason, Clients SHOULD resolve the room alias to a room ID
|
||||||
subsequent requests.
|
once and then use that ID on subsequent requests.
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
@@ -132,49 +133,567 @@ Identity
|
|||||||
|
|
||||||
API Standards
|
API Standards
|
||||||
-------------
|
-------------
|
||||||
- All HTTP[S]
|
All communication in Matrix is performed over HTTP[S] using a Content-Type of ``application/json``.
|
||||||
- Uses JSON as HTTP bodies
|
Any errors which occur on the Matrix API level MUST return a "standard error response". This is a
|
||||||
- Standard error response format { errcode: M_WHATEVER, error: "some message" }
|
JSON object which looks like::
|
||||||
- C-S API provides POST for operations, or PUT with txn IDs. Explain txn IDs.
|
|
||||||
|
{
|
||||||
|
"errcode": "<error code>",
|
||||||
|
"error": "<error message>"
|
||||||
|
}
|
||||||
|
|
||||||
|
The ``error`` string will be a human-readable error message, usually a sentence
|
||||||
|
explaining what went wrong. The ``errcode`` string will be a unique string which can be
|
||||||
|
used to handle an error message e.g. ``M_FORBIDDEN``. These error codes should have their
|
||||||
|
namespace first in ALL CAPS, followed by a single _. For example, if there was a custom
|
||||||
|
namespace ``com.mydomain.here``, and a ``FORBIDDEN`` code, the error code should look
|
||||||
|
like ``COM.MYDOMAIN.HERE_FORBIDDEN``. There may be additional keys depending on
|
||||||
|
the error, but the keys ``error`` and ``errcode`` MUST always be present.
|
||||||
|
|
||||||
|
Some standard error codes are below:
|
||||||
|
|
||||||
|
:``M_FORBIDDEN``:
|
||||||
|
Forbidden access, e.g. joining a room without permission, failed login.
|
||||||
|
|
||||||
|
:``M_UNKNOWN_TOKEN``:
|
||||||
|
The access token specified was not recognised.
|
||||||
|
|
||||||
|
:``M_BAD_JSON``:
|
||||||
|
Request contained valid JSON, but it was malformed in some way, e.g. missing
|
||||||
|
required keys, invalid values for keys.
|
||||||
|
|
||||||
|
:``M_NOT_JSON``:
|
||||||
|
Request did not contain valid JSON.
|
||||||
|
|
||||||
|
:``M_NOT_FOUND``:
|
||||||
|
No resource was found for this request.
|
||||||
|
|
||||||
|
:``M_LIMIT_EXCEEDED``:
|
||||||
|
Too many requests have been sent in a short period of time. Wait a while then
|
||||||
|
try again.
|
||||||
|
|
||||||
|
Some requests have unique error codes:
|
||||||
|
|
||||||
|
:``M_USER_IN_USE``:
|
||||||
|
Encountered when trying to register a user ID which has been taken.
|
||||||
|
|
||||||
|
:``M_ROOM_IN_USE``:
|
||||||
|
Encountered when trying to create a room which has been taken.
|
||||||
|
|
||||||
|
:``M_BAD_PAGINATION``:
|
||||||
|
Encountered when specifying bad pagination query parameters.
|
||||||
|
|
||||||
|
:``M_LOGIN_EMAIL_URL_NOT_YET``:
|
||||||
|
Encountered when polling for an email link which has not been clicked yet.
|
||||||
|
|
||||||
|
The C-S API typically uses ``HTTP POST`` to submit requests. This means these requests
|
||||||
|
are not idempotent. The C-S API also allows ``HTTP PUT`` to make requests idempotent.
|
||||||
|
In order to use a ``PUT``, paths should be suffixed with ``/{txnId}``. ``{txnId}`` is a
|
||||||
|
client-generated transaction ID which identifies the request. Crucially, it **only**
|
||||||
|
serves to identify new requests from retransmits. After the request has finished, the
|
||||||
|
``{txnId}`` value should be changed (how is not specified, it could be a monotonically
|
||||||
|
increasing integer, etc). It is preferable to use ``HTTP PUT`` to make sure requests to
|
||||||
|
send messages do not get sent more than once should clients need to retransmit requests.
|
||||||
|
|
||||||
|
Valid requests look like::
|
||||||
|
|
||||||
|
POST /some/path/here
|
||||||
|
{
|
||||||
|
"key": "This is a post."
|
||||||
|
}
|
||||||
|
|
||||||
|
PUT /some/path/here/11
|
||||||
|
{
|
||||||
|
"key": "This is a put with a txnId of 11."
|
||||||
|
}
|
||||||
|
|
||||||
|
In contrast, these are invalid requests::
|
||||||
|
|
||||||
|
POST /some/path/here/11
|
||||||
|
{
|
||||||
|
"key": "This is a post, but it has a txnId."
|
||||||
|
}
|
||||||
|
|
||||||
|
PUT /some/path/here
|
||||||
|
{
|
||||||
|
"key": "This is a put but it is missing a txnId."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
- TODO: All strings everywhere are UTF-8
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Receiving live updates on a client
|
Receiving live updates on a client
|
||||||
----------------------------------
|
----------------------------------
|
||||||
- C-S longpoll event stream
|
Clients can receive new events by long-polling the home server. This will hold open the
|
||||||
- Concept of start/end tokens.
|
HTTP connection for a short period of time waiting for new events, returning early if an
|
||||||
- Mention /initialSync to get token.
|
event occurs. This is called the "Event Stream". All events which the client is authorised
|
||||||
|
to view will appear in the event stream. When the stream is closed, an ``end`` token is
|
||||||
|
returned. This token can be used in the next request to continue where the client left off.
|
||||||
|
|
||||||
|
When the client first logs in, they will need to initially synchronise with their home
|
||||||
|
server. This is achieved via the ``/initialSync`` API. This API also returns an ``end``
|
||||||
|
token which can be used with the event stream.
|
||||||
|
|
||||||
Rooms
|
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.
|
Creation
|
||||||
|
--------
|
||||||
|
To create a room, a client has to use the ``/createRoom`` API. There are various options
|
||||||
|
which can be set when creating a room:
|
||||||
|
|
||||||
Messages
|
``visibility``
|
||||||
========
|
Type:
|
||||||
|
String
|
||||||
|
Optional:
|
||||||
|
Yes
|
||||||
|
Value:
|
||||||
|
Either ``public`` or ``private``.
|
||||||
|
Description:
|
||||||
|
A ``public`` visibility indicates that the room will be shown in the public room list. A
|
||||||
|
``private`` visibility will hide the room from the public room list. Rooms default to
|
||||||
|
``public`` visibility if this key is not included.
|
||||||
|
|
||||||
|
``room_alias_name``
|
||||||
|
Type:
|
||||||
|
String
|
||||||
|
Optional:
|
||||||
|
Yes
|
||||||
|
Value:
|
||||||
|
The room alias localpart.
|
||||||
|
Description:
|
||||||
|
If this is included, a room alias will be created and mapped to the newly created room.
|
||||||
|
The alias will belong on the same home server which created the room, e.g.
|
||||||
|
``!qadnasoi:domain.com >>> #room_alias_name:domain.com``
|
||||||
|
|
||||||
|
``name``
|
||||||
|
Type:
|
||||||
|
String
|
||||||
|
Optional:
|
||||||
|
Yes
|
||||||
|
Value:
|
||||||
|
The ``name`` value for the ``m.room.name`` state event.
|
||||||
|
Description:
|
||||||
|
If this is included, an ``m.room.name`` event will be sent into the room to indicate the
|
||||||
|
name of the room. See "Room Events" for more information on ``m.room.name``.
|
||||||
|
|
||||||
|
``topic``
|
||||||
|
Type:
|
||||||
|
String
|
||||||
|
Optional:
|
||||||
|
Yes
|
||||||
|
Value:
|
||||||
|
The ``topic`` value for the ``m.room.topic`` state event.
|
||||||
|
Description:
|
||||||
|
If this is included, an ``m.room.topic`` event will be sent into the room to indicate the
|
||||||
|
topic for the room. See "Room Events" for more information on ``m.room.topic``.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
{
|
||||||
|
"visibility": "public",
|
||||||
|
"room_alias_name": "the pub",
|
||||||
|
"name": "The Grand Duke Pub",
|
||||||
|
"topic": "All about happy hour"
|
||||||
|
}
|
||||||
|
|
||||||
|
- TODO: This creates a room creation event which serves as the root of the PDU graph for this room.
|
||||||
|
- TODO: Keys for speccing a room name / room topic / invite these users?
|
||||||
|
|
||||||
|
Modifying aliases
|
||||||
|
-----------------
|
||||||
|
- path to edit aliases
|
||||||
|
- format when retrieving list of aliases. NOT complete list.
|
||||||
|
- format for adding aliases.
|
||||||
|
|
||||||
|
Permissions
|
||||||
|
-----------
|
||||||
|
- TODO: What is a power level? How do they work? Defaults / required levels for X. How do they change
|
||||||
|
as people join and leave rooms? What do you do if you get a clash? Examples.
|
||||||
|
- TODO: List all actions which use power levels (sending msgs, inviting users, banning people, etc...)
|
||||||
|
- TODO: Room config - what is the event and what are the keys/values and explanations for them.
|
||||||
|
Link through to respective sections where necessary. How does this tie in with permissions, e.g.
|
||||||
|
give example of creating a read-only room.
|
||||||
|
|
||||||
|
|
||||||
|
Joining rooms
|
||||||
|
-------------
|
||||||
|
- TODO: What does the home server have to do to join a user to a room?
|
||||||
|
|
||||||
|
Users need to join a room in order to send and receive events in that room. A user can join a
|
||||||
|
room by making a request to ``/join/<room alias or id>`` with::
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
Alternatively, a user can make a request to ``/rooms/<room id>/join`` with the same request content.
|
||||||
|
This is only provided for symmetry with the other membership APIs: ``/rooms/<room id>/invite`` and
|
||||||
|
``/rooms/<room id>/leave``. If a room alias was specified, it will be automatically resolved to
|
||||||
|
a room ID, which will then be joined. The room ID that was joined will be returned in response::
|
||||||
|
|
||||||
|
{
|
||||||
|
"room_id": "!roomid:domain"
|
||||||
|
}
|
||||||
|
|
||||||
|
The membership state for the joining user can also be modified directly to be ``join``
|
||||||
|
by sending the following request to
|
||||||
|
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
|
||||||
|
|
||||||
|
{
|
||||||
|
"membership": "join"
|
||||||
|
}
|
||||||
|
|
||||||
|
See the "Room events" section for more information on ``m.room.member``.
|
||||||
|
|
||||||
|
After the user has joined a room, they will receive subsequent events in that room. This room
|
||||||
|
will now appear as an entry in the ``/initialSync`` API.
|
||||||
|
|
||||||
|
Some rooms enforce that a user is *invited* to a room before they can join that room. Other
|
||||||
|
rooms will allow anyone to join the room even if they have not received an invite.
|
||||||
|
|
||||||
|
Inviting users
|
||||||
|
--------------
|
||||||
|
- Can invite users to a room if the room config key TODO is set to TODO. Must have required power level.
|
||||||
|
- Outline invite join dance. What is it? Why is it required? How does it work?
|
||||||
|
- What does the home server have to do?
|
||||||
|
|
||||||
|
The purpose of inviting users to a room is to notify them that the room exists
|
||||||
|
so they can choose to become a member of that room. Some rooms require that all
|
||||||
|
users who join a room are previously invited to it (an "invite-only" room).
|
||||||
|
Whether a given room is an "invite-only" room is determined by the room config
|
||||||
|
key ``TODO``. It can have one of the following values:
|
||||||
|
|
||||||
|
- TODO Room config invite only value explanation
|
||||||
|
- TODO Room config free-to-join value explanation
|
||||||
|
|
||||||
|
Only users who have a membership state of ``join`` in a room can invite new
|
||||||
|
users to said room. The person being invited must not be in the ``join`` state
|
||||||
|
in the room. The fully-qualified user ID must be specified when inviting a user,
|
||||||
|
as the user may reside on a different home server. To invite a user, send the
|
||||||
|
following request to ``/rooms/<room id>/invite``, which will manage the
|
||||||
|
entire invitation process::
|
||||||
|
|
||||||
|
{
|
||||||
|
"user_id": "<user id to invite>"
|
||||||
|
}
|
||||||
|
|
||||||
|
Alternatively, the membership state for this user in this room can be modified
|
||||||
|
directly by sending the following request to
|
||||||
|
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
|
||||||
|
|
||||||
|
{
|
||||||
|
"membership": "invite"
|
||||||
|
}
|
||||||
|
|
||||||
|
See the "Room events" section for more information on ``m.room.member``.
|
||||||
|
|
||||||
|
- TODO: In what circumstances will this NOT be equivalent to ``/invite``?
|
||||||
|
|
||||||
|
Leaving rooms
|
||||||
|
-------------
|
||||||
|
A user can leave a room to stop receiving events for that room. A user must have
|
||||||
|
joined the room before they are eligible to leave the room. If the room is an
|
||||||
|
"invite-only" room, they will need to be re-invited before they can re-join the room.
|
||||||
|
To leave a room, a request should be made to ``/rooms/<room id>/leave`` with::
|
||||||
|
|
||||||
|
{}
|
||||||
|
|
||||||
|
Alternatively, the membership state for this user in this room can be modified
|
||||||
|
directly by sending the following request to
|
||||||
|
``/rooms/<room id>/state/m.room.member/<url encoded user id>``::
|
||||||
|
|
||||||
|
{
|
||||||
|
"membership": "leave"
|
||||||
|
}
|
||||||
|
|
||||||
|
See the "Room events" section for more information on ``m.room.member``.
|
||||||
|
|
||||||
|
Once a user has left a room, that room will no longer appear on the ``/initialSync``
|
||||||
|
API. Be aware that leaving a room is not equivalent to have never been
|
||||||
|
in that room. A user who has previously left a room still maintains some residual state in
|
||||||
|
that room. Their membership state will be marked as ``leave``. This contrasts with
|
||||||
|
a user who has *never been invited or joined to that room* who will not have any
|
||||||
|
membership state for that room.
|
||||||
|
|
||||||
|
If all members in a room leave, that room becomes eligible for deletion.
|
||||||
|
- TODO: Grace period before deletion?
|
||||||
|
- TODO: Under what conditions should a room NOT be purged?
|
||||||
|
|
||||||
|
Banning users in a room
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
A user may decide to ban another user in a room. 'Banning' forces the target user
|
||||||
|
to leave the room and prevents them from re-joining the room. A banned user will
|
||||||
|
not be treated as a joined user, and so will not be able to send or receive events
|
||||||
|
in the room. In order to ban someone, the user performing the ban MUST have the
|
||||||
|
required power level. To ban a user, a request should be made to
|
||||||
|
``/rooms/<room id>/ban`` with::
|
||||||
|
|
||||||
|
{
|
||||||
|
"user_id": "<user id to ban"
|
||||||
|
"reason": "string: <reason for the ban>"
|
||||||
|
}
|
||||||
|
|
||||||
|
Banning a user adjusts the banned member's membership state to ``ban`` and adjusts
|
||||||
|
the power level of this event to a level higher than the banned person. Like
|
||||||
|
with other membership changes, a user can directly adjust the target member's
|
||||||
|
state, by making a request to ``/rooms/<room id>/state/m.room.member/<user id>``::
|
||||||
|
|
||||||
|
{
|
||||||
|
"membership": "ban"
|
||||||
|
}
|
||||||
|
|
||||||
|
Events in a room
|
||||||
|
----------------
|
||||||
|
Room events can be split into two categories:
|
||||||
|
|
||||||
|
:State Events:
|
||||||
|
These are events which replace events that came before it, depending on a set of unique keys.
|
||||||
|
These keys are the event ``type`` and a ``state_key``. Events with the same set of keys will
|
||||||
|
be overwritten. Typically, state events are used to store state, hence their name.
|
||||||
|
|
||||||
|
:Non-state events:
|
||||||
|
These are events which cannot be overwritten after sending. The list of events continues
|
||||||
|
to grow as more events are sent. As this list grows, it becomes necessary to
|
||||||
|
provide a mechanism for navigating this list. Pagination APIs are used to view the list
|
||||||
|
of historical non-state events. Typically, non-state events are used to send messages.
|
||||||
|
|
||||||
|
This specification outlines several events, all with the event type prefix ``m.``. However,
|
||||||
|
applications may wish to add their own type of event, and this can be achieved using the
|
||||||
|
REST API detailed in the following sections. If new events are added, the event ``type``
|
||||||
|
key SHOULD follow the Java package naming convention, e.g. ``com.example.myapp.event``.
|
||||||
|
This ensures event types are suitably namespaced for each application and reduces the
|
||||||
|
risk of clashes.
|
||||||
|
|
||||||
|
State events
|
||||||
|
------------
|
||||||
|
State events can be sent by ``PUT`` ing to ``/rooms/<room id>/state/<event type>/<state key>``.
|
||||||
|
These events will be overwritten if ``<room id>``, ``<event type>`` and ``<state key>`` all match.
|
||||||
|
If the state event has no ``state_key``, it can be omitted from the path. These requests
|
||||||
|
**cannot use transaction IDs** like other ``PUT`` paths because they cannot be differentiated
|
||||||
|
from the ``state_key``. Furthermore, ``POST`` is unsupported on state paths. Valid requests
|
||||||
|
look like::
|
||||||
|
|
||||||
|
PUT /rooms/!roomid:domain/state/m.example.event
|
||||||
|
{ "key" : "without a state key" }
|
||||||
|
|
||||||
|
PUT /rooms/!roomid:domain/state/m.another.example.event/foo
|
||||||
|
{ "key" : "with 'foo' as the state key" }
|
||||||
|
|
||||||
|
In contrast, these requests are invalid::
|
||||||
|
|
||||||
|
POST /rooms/!roomid:domain/state/m.example.event/
|
||||||
|
{ "key" : "cannot use POST here" }
|
||||||
|
|
||||||
|
PUT /rooms/!roomid:domain/state/m.another.example.event/foo/11
|
||||||
|
{ "key" : "txnIds are not supported" }
|
||||||
|
|
||||||
|
Care should be taken to avoid setting the wrong ``state key``::
|
||||||
|
|
||||||
|
PUT /rooms/!roomid:domain/state/m.another.example.event/11
|
||||||
|
{ "key" : "with '11' as the state key, but was probably intended to be a txnId" }
|
||||||
|
|
||||||
|
The ``state_key`` is often used to store state about individual users, by using the user ID as the
|
||||||
|
``state_key`` value. For example::
|
||||||
|
|
||||||
|
PUT /rooms/!roomid:domain/state/m.favorite.animal.event/%40my_user%3Adomain.com
|
||||||
|
{ "animal" : "cat", "reason": "fluffy" }
|
||||||
|
|
||||||
|
In some cases, there may be no need for a ``state_key``, so it can be omitted::
|
||||||
|
|
||||||
|
PUT /rooms/!roomid:domain/state/m.room.bgd.color
|
||||||
|
{ "color": "red", "hex": "#ff0000" }
|
||||||
|
|
||||||
|
See "Room Events" for the ``m.`` event specification.
|
||||||
|
|
||||||
|
Non-state events
|
||||||
|
----------------
|
||||||
|
Non-state events can be sent by sending a request to ``/rooms/<room id>/send/<event type>``.
|
||||||
|
These requests *can* use transaction IDs and ``PUT``/``POST`` methods. Non-state events
|
||||||
|
allow access to historical events and pagination, making it best suited for sending messages.
|
||||||
|
For example::
|
||||||
|
|
||||||
|
POST /rooms/!roomid:domain/send/m.custom.example.message
|
||||||
|
{ "text": "Hello world!" }
|
||||||
|
|
||||||
|
PUT /rooms/!roomid:domain/send/m.custom.example.message/11
|
||||||
|
{ "text": "Goodbye world!" }
|
||||||
|
|
||||||
|
See "Room Events" for the ``m.`` event specification.
|
||||||
|
|
||||||
|
Syncing rooms
|
||||||
|
-------------
|
||||||
|
When a client logs in, they may have a list of rooms which they have already joined. These rooms
|
||||||
|
may also have a list of events associated with them. The purpose of 'syncing' is to present the
|
||||||
|
current room and event information in a convenient, compact manner. The events returned are not
|
||||||
|
limited to room events; presence events will also be returned. There are two APIs provided:
|
||||||
|
|
||||||
|
- ``/initialSync`` : A global sync which will present room and event information for all rooms
|
||||||
|
the user has joined.
|
||||||
|
|
||||||
|
- ``/rooms/<room id>/initialSync`` : A sync scoped to a single room. Presents room and event
|
||||||
|
information for this room only.
|
||||||
|
|
||||||
|
- TODO: JSON response format for both types
|
||||||
|
- TODO: when would you use global? when would you use scoped?
|
||||||
|
|
||||||
|
Getting events for a room
|
||||||
|
-------------------------
|
||||||
|
There are several APIs provided to ``GET`` events for a room:
|
||||||
|
|
||||||
|
``/rooms/<room id>/state/<event type>/<state key>``
|
||||||
|
Description:
|
||||||
|
Get the state event identified.
|
||||||
|
Response format:
|
||||||
|
A JSON object representing the state event **content**.
|
||||||
|
Example:
|
||||||
|
``/rooms/!room:domain.com/state/m.room.name`` returns ``{ "name": "Room name" }``
|
||||||
|
|
||||||
|
``/rooms/<room id>/state``
|
||||||
|
Description:
|
||||||
|
Get all state events for a room.
|
||||||
|
Response format:
|
||||||
|
``[ { state event }, { state event }, ... ]``
|
||||||
|
Example:
|
||||||
|
TODO
|
||||||
|
|
||||||
|
|
||||||
|
``/rooms/<room id>/members``
|
||||||
|
Description:
|
||||||
|
Get all ``m.room.member`` state events.
|
||||||
|
Response format:
|
||||||
|
``{ "start": "token", "end": "token", "chunk": [ { m.room.member event }, ... ] }``
|
||||||
|
Example:
|
||||||
|
TODO
|
||||||
|
|
||||||
|
``/rooms/<room id>/messages``
|
||||||
|
Description:
|
||||||
|
Get all ``m.room.message`` events.
|
||||||
|
Response format:
|
||||||
|
``{ TODO }``
|
||||||
|
Example:
|
||||||
|
TODO
|
||||||
|
|
||||||
|
``/rooms/<room id>/initialSync``
|
||||||
|
Description:
|
||||||
|
Get all relevant events for a room. This includes state events, paginated non-state
|
||||||
|
events and presence events.
|
||||||
|
Response format:
|
||||||
|
`` { TODO } ``
|
||||||
|
Example:
|
||||||
|
TODO
|
||||||
|
|
||||||
|
|
||||||
|
Room Events
|
||||||
|
===========
|
||||||
|
- voip events?
|
||||||
|
|
||||||
This specification outlines several standard event types, all of which are
|
This specification outlines several standard event types, all of which are
|
||||||
prefixed with ``m.``
|
prefixed with ``m.``
|
||||||
|
|
||||||
State messages
|
``m.room.name``
|
||||||
--------------
|
Summary:
|
||||||
- m.room.name
|
Set the human-readable name for the room.
|
||||||
- m.room.topic
|
Type:
|
||||||
- m.room.member
|
State event
|
||||||
- m.room.config
|
JSON format:
|
||||||
- m.room.invite_join
|
``{ "name" : "string" }``
|
||||||
|
Example:
|
||||||
|
``{ "name" : "My Room" }``
|
||||||
|
Description:
|
||||||
|
A room has an opaque room ID which is not human-friendly to read. A room alias is
|
||||||
|
human-friendly, but not all rooms have room aliases. The room name is a human-friendly
|
||||||
|
string designed to be displayed to the end-user. The room name is not *unique*, as
|
||||||
|
multiple rooms can have the same room name set. The room name can also be set when
|
||||||
|
creating a room using ``/createRoom`` with the ``name`` key.
|
||||||
|
|
||||||
What are they, when are they used, what do they contain, how should they be used
|
``m.room.topic``
|
||||||
|
Summary:
|
||||||
|
Set a topic for the room.
|
||||||
|
Type:
|
||||||
|
State event
|
||||||
|
JSON format:
|
||||||
|
``{ "topic" : "string" }``
|
||||||
|
Example:
|
||||||
|
``{ "topic" : "Welcome to the real world." }``
|
||||||
|
Description:
|
||||||
|
A topic is a short message detailing what is currently being discussed in the room.
|
||||||
|
It can also be used as a way to display extra information about the room, which may
|
||||||
|
not be suitable for the room name.
|
||||||
|
|
||||||
Non-state messages
|
``m.room.member``
|
||||||
------------------
|
Summary:
|
||||||
- m.room.message
|
The current membership state of a user in the room.
|
||||||
- m.room.message.feedback (and compressed format)
|
Type:
|
||||||
|
State event
|
||||||
|
JSON format:
|
||||||
|
``{ "membership" : "enum[ invite|join|leave|ban ]" }``
|
||||||
|
Example:
|
||||||
|
``{ "membership" : "join" }``
|
||||||
|
Description:
|
||||||
|
Adjusts the membership state for a user in a room. It is preferable to use the
|
||||||
|
membership APIs (``/rooms/<room id>/invite`` etc) when performing membership actions
|
||||||
|
rather than adjusting the state directly as there are a restricted set of valid
|
||||||
|
transformations. For example, user A cannot force user B to join a room, and trying
|
||||||
|
to force this state change directly will fail. See the "Rooms" section for how to
|
||||||
|
use the membership APIs.
|
||||||
|
|
||||||
What are they, when are they used, what do they contain, how should they be used
|
``m.room.config``
|
||||||
|
Summary:
|
||||||
|
The room config.
|
||||||
|
Type:
|
||||||
|
State event
|
||||||
|
JSON format:
|
||||||
|
TODO
|
||||||
|
Example:
|
||||||
|
TODO
|
||||||
|
Description:
|
||||||
|
TODO
|
||||||
|
|
||||||
|
``m.room.invite_join``
|
||||||
|
Summary:
|
||||||
|
TODO.
|
||||||
|
Type:
|
||||||
|
State event
|
||||||
|
JSON format:
|
||||||
|
TODO
|
||||||
|
Example:
|
||||||
|
TODO
|
||||||
|
Description:
|
||||||
|
TODO
|
||||||
|
|
||||||
|
``m.room.message``
|
||||||
|
Summary:
|
||||||
|
A message.
|
||||||
|
Type:
|
||||||
|
Non-state event
|
||||||
|
JSON format:
|
||||||
|
``{ "msgtype": "string" }``
|
||||||
|
Example:
|
||||||
|
``{ "msgtype": "m.text", "body": "Testing" }``
|
||||||
|
Description:
|
||||||
|
This event is used when sending messages in a room. Messages are not limited to be text.
|
||||||
|
The ``msgtype`` key outlines the type of message, e.g. text, audio, image, video, etc.
|
||||||
|
Whilst not required, the ``body`` key SHOULD be used with every kind of ``msgtype`` as
|
||||||
|
a fallback mechanism when a client cannot render the message. For more information on
|
||||||
|
the types of messages which can be sent, see "m.room.message msgtypes".
|
||||||
|
|
||||||
|
``m.room.message.feedback``
|
||||||
|
Summary:
|
||||||
|
A receipt for a message.
|
||||||
|
Type:
|
||||||
|
Non-state event
|
||||||
|
JSON format:
|
||||||
|
``{ "type": "enum [ delivered|read ]", "target_event_id": "string" }``
|
||||||
|
Example:
|
||||||
|
``{ "type": "delivered", "target_event_id": "e3b2icys" }``
|
||||||
|
Description:
|
||||||
|
Feedback events are events sent to acknowledge a message in some way. There are two
|
||||||
|
supported acknowledgements: ``delivered`` (sent when the event has been received) and
|
||||||
|
``read`` (sent when the event has been observed by the end-user). The ``target_event_id``
|
||||||
|
should reference the ``m.room.message`` event being acknowledged.
|
||||||
|
|
||||||
m.room.message msgtypes
|
m.room.message msgtypes
|
||||||
-----------------------
|
-----------------------
|
||||||
@@ -285,7 +804,7 @@ Each user has the concept of presence information. This encodes the
|
|||||||
"availability" of that user, suitable for display on other user's clients. This
|
"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
|
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
|
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:
|
is represented by the ``presence`` key, which is an enum of one of the following:
|
||||||
|
|
||||||
- ``online`` : The default state when the user is connected to an event stream.
|
- ``online`` : The default state when the user is connected to an event stream.
|
||||||
- ``unavailable`` : The user is not reachable at this time.
|
- ``unavailable`` : The user is not reachable at this time.
|
||||||
@@ -295,18 +814,26 @@ is represented by the ``state`` key, which is an enum of one of the following:
|
|||||||
- ``hidden`` : TODO. Behaves as offline, but allows the user to see the client
|
- ``hidden`` : TODO. Behaves as offline, but allows the user to see the client
|
||||||
state anyway and generally interact with client features.
|
state anyway and generally interact with client features.
|
||||||
|
|
||||||
This basic ``state`` field applies to the user as a whole, regardless of how many
|
This basic ``presence`` field applies to the user as a whole, regardless of how many
|
||||||
client devices they have connected. The home server should synchronise this
|
client devices they have connected. The home server should synchronise this
|
||||||
status choice among multiple devices to ensure the user gets a consistent
|
status choice among multiple devices to ensure the user gets a consistent
|
||||||
experience.
|
experience.
|
||||||
|
|
||||||
|
In addition, the server maintains a timestamp of the last time it saw an active
|
||||||
|
action from the user; either sending a message to a room, or changing presence
|
||||||
|
state from a lower to a higher level of availability (thus: changing state from
|
||||||
|
``unavailable`` to ``online`` will count as an action for being active, whereas
|
||||||
|
in the other direction will not). This timestamp is presented via a key called
|
||||||
|
``last_active_ago``, which gives the relative number of miliseconds since the
|
||||||
|
message is generated/emitted, that the user was last seen active.
|
||||||
|
|
||||||
Idle Time
|
Idle Time
|
||||||
---------
|
---------
|
||||||
As well as the basic ``state`` field, the presence information can also show a sense
|
As well as the basic ``presence`` field, the presence information can also show
|
||||||
of an "idle timer". This should be maintained individually by the user's
|
a sense of an "idle timer". This should be maintained individually by the
|
||||||
clients, and the home server can take the highest reported time as that to
|
user's clients, and the home server can take the highest reported time as that
|
||||||
report. When a user is offline, the home server can still report when the user was last
|
to report. When a user is offline, the home server can still report when the
|
||||||
seen online.
|
user was last seen online.
|
||||||
|
|
||||||
Transmission
|
Transmission
|
||||||
------------
|
------------
|
||||||
@@ -336,11 +863,23 @@ presence information in a user list for a room.
|
|||||||
|
|
||||||
Typing notifications
|
Typing notifications
|
||||||
====================
|
====================
|
||||||
|
- what is the event type. Are they bundled with other event types? If so, which.
|
||||||
|
- what are the valid keys / values. What do they represent. Any gotchas?
|
||||||
|
- Timeouts. How do they work, who sets them and how do they expire. Does one
|
||||||
|
have priority over another? Give examples.
|
||||||
|
|
||||||
TODO : Leo
|
TODO : Leo
|
||||||
|
|
||||||
Voice over IP
|
Voice over IP
|
||||||
=============
|
=============
|
||||||
|
- what are the event types.
|
||||||
|
- what are the valid keys/values. What do they represent. Any gotchas?
|
||||||
|
- In what sequence should the events be sent?
|
||||||
|
- How do you accept / decline inbound calls? How do you make outbound calls?
|
||||||
|
Give examples.
|
||||||
|
- How does negotiation work? Give examples.
|
||||||
|
- How do you hang up?
|
||||||
|
- What does call log information look like e.g. duration of call?
|
||||||
|
|
||||||
TODO : Dave
|
TODO : Dave
|
||||||
|
|
||||||
@@ -358,8 +897,12 @@ numbers, website URLs, etc...). This specification puts no requirements on the
|
|||||||
display name other than it being a valid unicode string.
|
display name other than it being a valid unicode string.
|
||||||
|
|
||||||
- Metadata extensibility
|
- Metadata extensibility
|
||||||
- Bundled with which events? e.g. m.room.member
|
- Changing profile info generates m.presence events ("presencelike")
|
||||||
- Generate own events? What type?
|
- keys on m.presence are optional, except presence which is required
|
||||||
|
- m.room.member is populated with the current displayname at that point in time.
|
||||||
|
- That is added by the HS, not you.
|
||||||
|
- Display name changes also generates m.room.member with displayname key f.e. room
|
||||||
|
the user is in.
|
||||||
|
|
||||||
Registration and login
|
Registration and login
|
||||||
======================
|
======================
|
||||||
@@ -823,11 +1366,16 @@ TODO
|
|||||||
|
|
||||||
Content repository
|
Content repository
|
||||||
==================
|
==================
|
||||||
- thumbnail paths
|
- path to upload
|
||||||
|
- format for thumbnail paths, mention what it is protecting against.
|
||||||
|
- content size limit and associated M_ERROR.
|
||||||
|
|
||||||
Address book repository
|
Address book repository
|
||||||
=======================
|
=======================
|
||||||
- format
|
- format: POST(?) wodges of json, some possible processing, then return wodges of json on GET.
|
||||||
|
- processing may remove dupes, merge contacts, pepper with extra info (e.g. matrix-ability of
|
||||||
|
contacts), etc.
|
||||||
|
- Standard json format for contacts? Piggy back off vcards?
|
||||||
|
|
||||||
|
|
||||||
Glossary
|
Glossary
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ def make_graph(pdus, room, filename_prefix):
|
|||||||
def get_pdus(host, room):
|
def get_pdus(host, room):
|
||||||
transaction = json.loads(
|
transaction = json.loads(
|
||||||
urllib2.urlopen(
|
urllib2.urlopen(
|
||||||
"http://%s/matrix/federation/v1/context/%s/" % (host, room)
|
"http://%s/_matrix/federation/v1/context/%s/" % (host, room)
|
||||||
).read()
|
).read()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ $('.login').live('click', function() {
|
|||||||
var user = $("#userLogin").val();
|
var user = $("#userLogin").val();
|
||||||
var password = $("#passwordLogin").val();
|
var password = $("#passwordLogin").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
url: "http://localhost:8080/_matrix/client/api/v1/login",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
@@ -25,11 +25,12 @@ $('.login').live('click', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var getCurrentRoomList = function() {
|
var getCurrentRoomList = function() {
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=0; i<data.length; ++i) {
|
var rooms = data.rooms;
|
||||||
data[i].latest_message = data[i].messages.chunk[0].content.body;
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
addRoom(data[i]);
|
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||||
|
addRoom(rooms[i]);
|
||||||
}
|
}
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
@@ -43,7 +44,7 @@ $('.createRoom').live('click', function() {
|
|||||||
data.room_alias_name = roomAlias;
|
data.room_alias_name = roomAlias;
|
||||||
}
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
|
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
@@ -78,11 +79,9 @@ $('.sendMessage').live('click', function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
|
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
|
||||||
url = url.replace("$msgid", msgId);
|
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
@@ -91,7 +90,7 @@ $('.sendMessage').live('click', function() {
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ var eventStreamInfo = {
|
|||||||
var roomInfo = [];
|
var roomInfo = [];
|
||||||
|
|
||||||
var longpollEventStream = function() {
|
var longpollEventStream = function() {
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from";
|
var url = "http://localhost:8080/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$from", eventStreamInfo.from);
|
url = url.replace("$from", eventStreamInfo.from);
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ $('.login').live('click', function() {
|
|||||||
var user = $("#userLogin").val();
|
var user = $("#userLogin").val();
|
||||||
var password = $("#passwordLogin").val();
|
var password = $("#passwordLogin").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
url: "http://localhost:8080/_matrix/client/api/v1/login",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
@@ -65,14 +65,15 @@ $('.login').live('click', function() {
|
|||||||
|
|
||||||
var getCurrentRoomList = function() {
|
var getCurrentRoomList = function() {
|
||||||
$("#roomId").val("");
|
$("#roomId").val("");
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=0; i<data.length; ++i) {
|
var rooms = data.rooms;
|
||||||
if ("messages" in data[i]) {
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
data[i].latest_message = data[i].messages.chunk[0].content.body;
|
if ("messages" in rooms[i]) {
|
||||||
|
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomInfo = data;
|
roomInfo = rooms;
|
||||||
setRooms(roomInfo);
|
setRooms(roomInfo);
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
@@ -92,17 +93,14 @@ $('.sendMessage').live('click', function() {
|
|||||||
|
|
||||||
var sendMessage = function(roomId) {
|
var sendMessage = function(roomId) {
|
||||||
var body = "jsfiddle message @" + $.now();
|
var body = "jsfiddle message @" + $.now();
|
||||||
var msgId = $.now();
|
|
||||||
|
|
||||||
if (roomId.length === 0) {
|
if (roomId.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
|
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
|
||||||
url = url.replace("$msgid", msgId);
|
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
@@ -111,7 +109,7 @@ var sendMessage = function(roomId) {
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ var viewingRoomId;
|
|||||||
|
|
||||||
// ************** Event Streaming **************
|
// ************** Event Streaming **************
|
||||||
var longpollEventStream = function() {
|
var longpollEventStream = function() {
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/events?access_token=$token&from=$from";
|
var url = "http://localhost:8080/_matrix/client/api/v1/events?access_token=$token&from=$from";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$from", eventStreamInfo.from);
|
url = url.replace("$from", eventStreamInfo.from);
|
||||||
|
|
||||||
@@ -38,8 +38,9 @@ var longpollEventStream = function() {
|
|||||||
else if (data.chunk[i].type === "m.room.member") {
|
else if (data.chunk[i].type === "m.room.member") {
|
||||||
if (viewingRoomId === data.chunk[i].room_id) {
|
if (viewingRoomId === data.chunk[i].room_id) {
|
||||||
console.log("Got new member: " + JSON.stringify(data.chunk[i]));
|
console.log("Got new member: " + JSON.stringify(data.chunk[i]));
|
||||||
|
addMessage(data.chunk[i]);
|
||||||
for (j=0; j<memberInfo.length; ++j) {
|
for (j=0; j<memberInfo.length; ++j) {
|
||||||
if (memberInfo[j].target_user_id === data.chunk[i].target_user_id) {
|
if (memberInfo[j].state_key === data.chunk[i].state_key) {
|
||||||
memberInfo[j] = data.chunk[i];
|
memberInfo[j] = data.chunk[i];
|
||||||
updatedMemberList = true;
|
updatedMemberList = true;
|
||||||
break;
|
break;
|
||||||
@@ -50,7 +51,7 @@ var longpollEventStream = function() {
|
|||||||
updatedMemberList = true;
|
updatedMemberList = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (data.chunk[i].target_user_id === accountInfo.user_id) {
|
if (data.chunk[i].state_key === accountInfo.user_id) {
|
||||||
getCurrentRoomList(); // update our join/invite list
|
getCurrentRoomList(); // update our join/invite list
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -88,7 +89,7 @@ $('.login').live('click', function() {
|
|||||||
var user = $("#userLogin").val();
|
var user = $("#userLogin").val();
|
||||||
var password = $("#passwordLogin").val();
|
var password = $("#passwordLogin").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
url: "http://localhost:8080/_matrix/client/api/v1/login",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
@@ -106,7 +107,7 @@ $('.register').live('click', function() {
|
|||||||
var user = $("#userReg").val();
|
var user = $("#userReg").val();
|
||||||
var password = $("#passwordReg").val();
|
var password = $("#passwordReg").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/register",
|
url: "http://localhost:8080/_matrix/client/api/v1/register",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user_id: user, password: password }),
|
data: JSON.stringify({ user_id: user, password: password }),
|
||||||
@@ -133,7 +134,7 @@ $('.createRoom').live('click', function() {
|
|||||||
data.room_alias_name = roomAlias;
|
data.room_alias_name = roomAlias;
|
||||||
}
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
|
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
@@ -154,14 +155,15 @@ $('.createRoom').live('click', function() {
|
|||||||
|
|
||||||
// ************** Getting current state **************
|
// ************** Getting current state **************
|
||||||
var getCurrentRoomList = function() {
|
var getCurrentRoomList = function() {
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=0; i<data.length; ++i) {
|
var rooms = data.rooms;
|
||||||
if ("messages" in data[i]) {
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
data[i].latest_message = data[i].messages.chunk[0].content.body;
|
if ("messages" in rooms[i]) {
|
||||||
|
rooms[i].latest_message = rooms[i].messages.chunk[0].content.body;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomInfo = data;
|
roomInfo = rooms;
|
||||||
setRooms(roomInfo);
|
setRooms(roomInfo);
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
@@ -179,7 +181,8 @@ var loadRoomContent = function(roomId) {
|
|||||||
|
|
||||||
var getMessages = function(roomId) {
|
var getMessages = function(roomId) {
|
||||||
$("#messages").empty();
|
$("#messages").empty();
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/messages/list?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=10";
|
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" +
|
||||||
|
encodeURIComponent(roomId) + "/messages?access_token=" + accountInfo.access_token + "&from=END&dir=b&limit=10";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=data.chunk.length-1; i>=0; --i) {
|
for (var i=data.chunk.length-1; i>=0; --i) {
|
||||||
addMessage(data.chunk[i]);
|
addMessage(data.chunk[i]);
|
||||||
@@ -190,7 +193,8 @@ var getMessages = function(roomId) {
|
|||||||
var getMemberList = function(roomId) {
|
var getMemberList = function(roomId) {
|
||||||
$("#members").empty();
|
$("#members").empty();
|
||||||
memberInfo = [];
|
memberInfo = [];
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/" + roomId + "/members/list?access_token=" + accountInfo.access_token;
|
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/" +
|
||||||
|
encodeURIComponent(roomId) + "/members?access_token=" + accountInfo.access_token;
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=0; i<data.chunk.length; ++i) {
|
for (var i=0; i<data.chunk.length; ++i) {
|
||||||
memberInfo.push(data.chunk[i]);
|
memberInfo.push(data.chunk[i]);
|
||||||
@@ -212,11 +216,9 @@ $('.sendMessage').live('click', function() {
|
|||||||
var sendMessage = function(roomId, body) {
|
var sendMessage = function(roomId, body) {
|
||||||
var msgId = $.now();
|
var msgId = $.now();
|
||||||
|
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/messages/$user/$msgid?access_token=$token";
|
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/send/m.room.message?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
|
||||||
url = url.replace("$msgid", msgId);
|
|
||||||
|
|
||||||
var data = {
|
var data = {
|
||||||
msgtype: "m.text",
|
msgtype: "m.text",
|
||||||
@@ -225,7 +227,7 @@ var sendMessage = function(roomId, body) {
|
|||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
@@ -260,13 +262,12 @@ var setRooms = function(roomList) {
|
|||||||
var membership = $(this).find('td:eq(1)').text();
|
var membership = $(this).find('td:eq(1)').text();
|
||||||
if (membership !== "join") {
|
if (membership !== "join") {
|
||||||
console.log("Joining room " + roomId);
|
console.log("Joining room " + roomId);
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/members/$user/state?access_token=$token";
|
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/join?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
url = url.replace("$user", encodeURIComponent(accountInfo.user_id));
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({membership: "join"}),
|
data: JSON.stringify({membership: "join"}),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
@@ -286,16 +287,33 @@ var setRooms = function(roomList) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var addMessage = function(data) {
|
var addMessage = function(data) {
|
||||||
|
|
||||||
|
var msg = data.content.body;
|
||||||
|
if (data.type === "m.room.member") {
|
||||||
|
if (data.content.membership === "invite") {
|
||||||
|
msg = "<em>invited " + data.state_key + " to the room</em>";
|
||||||
|
}
|
||||||
|
else if (data.content.membership === "join") {
|
||||||
|
msg = "<em>joined the room</em>";
|
||||||
|
}
|
||||||
|
else if (data.content.membership === "leave") {
|
||||||
|
msg = "<em>left the room</em>";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
msg = "<em>" + data.content.membership + "</em>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var row = "<tr>" +
|
var row = "<tr>" +
|
||||||
"<td>"+data.user_id+"</td>" +
|
"<td>"+data.user_id+"</td>" +
|
||||||
"<td>"+data.content.body+"</td>" +
|
"<td>"+msg+"</td>" +
|
||||||
"</tr>";
|
"</tr>";
|
||||||
$("#messages").append(row);
|
$("#messages").append(row);
|
||||||
};
|
};
|
||||||
|
|
||||||
var addMember = function(data) {
|
var addMember = function(data) {
|
||||||
var row = "<tr>" +
|
var row = "<tr>" +
|
||||||
"<td>"+data.target_user_id+"</td>" +
|
"<td>"+data.state_key+"</td>" +
|
||||||
"<td>"+data.content.membership+"</td>" +
|
"<td>"+data.content.membership+"</td>" +
|
||||||
"</tr>";
|
"</tr>";
|
||||||
$("#members").append(row);
|
$("#members").append(row);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ $('.register').live('click', function() {
|
|||||||
var user = $("#user").val();
|
var user = $("#user").val();
|
||||||
var password = $("#password").val();
|
var password = $("#password").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/register",
|
url: "http://localhost:8080/_matrix/client/api/v1/register",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user_id: user, password: password }),
|
data: JSON.stringify({ user_id: user, password: password }),
|
||||||
@@ -27,7 +27,7 @@ $('.register').live('click', function() {
|
|||||||
|
|
||||||
var login = function(user, password) {
|
var login = function(user, password) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
url: "http://localhost:8080/_matrix/client/api/v1/login",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
@@ -44,8 +44,8 @@ var login = function(user, password) {
|
|||||||
$('.login').live('click', function() {
|
$('.login').live('click', function() {
|
||||||
var user = $("#userLogin").val();
|
var user = $("#userLogin").val();
|
||||||
var password = $("#passwordLogin").val();
|
var password = $("#passwordLogin").val();
|
||||||
$.getJSON("http://localhost:8080/matrix/client/api/v1/login", function(data) {
|
$.getJSON("http://localhost:8080/_matrix/client/api/v1/login", function(data) {
|
||||||
if (data.type !== "m.login.password") {
|
if (data.flows[0].type !== "m.login.password") {
|
||||||
alert("I don't know how to login with this type: " + data.type);
|
alert("I don't know how to login with this type: " + data.type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -60,7 +60,7 @@ $('.logout').live('click', function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
$('.testToken').live('click', function() {
|
$('.testToken').live('click', function() {
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
$("#imSyncText").text(JSON.stringify(data, undefined, 2));
|
$("#imSyncText").text(JSON.stringify(data, undefined, 2));
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
<input type="text" id="roomId" placeholder="Room ID"></input>
|
<input type="text" id="roomId" placeholder="Room ID"></input>
|
||||||
<input type="text" id="targetUser" placeholder="Target User ID"></input>
|
<input type="text" id="targetUser" placeholder="Target User ID"></input>
|
||||||
<select id="membership">
|
<select id="membership">
|
||||||
<option value="invite">Invite</option>
|
<option value="invite">invite</option>
|
||||||
<option value="join">Join</option>
|
<option value="join">join</option>
|
||||||
<option value="leave">Leave</option>
|
<option value="leave">leave</option>
|
||||||
</select>
|
</select>
|
||||||
<input type="button" class="changeMembership" value="Change Membership"></input>
|
<input type="button" class="changeMembership" value="Change Membership"></input>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -4,13 +4,21 @@ var showLoggedIn = function(data) {
|
|||||||
accountInfo = data;
|
accountInfo = data;
|
||||||
getCurrentRoomList();
|
getCurrentRoomList();
|
||||||
$(".loggedin").css({visibility: "visible"});
|
$(".loggedin").css({visibility: "visible"});
|
||||||
|
$("#membership").change(function() {
|
||||||
|
if ($("#membership").val() === "invite") {
|
||||||
|
$("#targetUser").css({visibility: "visible"});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$("#targetUser").css({visibility: "hidden"});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$('.login').live('click', function() {
|
$('.login').live('click', function() {
|
||||||
var user = $("#userLogin").val();
|
var user = $("#userLogin").val();
|
||||||
var password = $("#passwordLogin").val();
|
var password = $("#passwordLogin").val();
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/login",
|
url: "http://localhost:8080/_matrix/client/api/v1/login",
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
data: JSON.stringify({ user: user, password: password, type: "m.login.password" }),
|
||||||
@@ -31,10 +39,11 @@ var getCurrentRoomList = function() {
|
|||||||
// solution but that is out of scope of this fiddle.
|
// solution but that is out of scope of this fiddle.
|
||||||
$("#rooms").find("tr:gt(0)").remove();
|
$("#rooms").find("tr:gt(0)").remove();
|
||||||
|
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/im/sync?access_token=" + accountInfo.access_token + "&from=END&to=START&limit=1";
|
var url = "http://localhost:8080/_matrix/client/api/v1/initialSync?access_token=" + accountInfo.access_token + "&limit=1";
|
||||||
$.getJSON(url, function(data) {
|
$.getJSON(url, function(data) {
|
||||||
for (var i=0; i<data.length; ++i) {
|
var rooms = data.rooms;
|
||||||
addRoom(data[i]);
|
for (var i=0; i<rooms.length; ++i) {
|
||||||
|
addRoom(rooms[i]);
|
||||||
}
|
}
|
||||||
}).fail(function(err) {
|
}).fail(function(err) {
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
@@ -44,7 +53,7 @@ var getCurrentRoomList = function() {
|
|||||||
$('.createRoom').live('click', function() {
|
$('.createRoom').live('click', function() {
|
||||||
var data = {};
|
var data = {};
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "http://localhost:8080/matrix/client/api/v1/rooms?access_token="+accountInfo.access_token,
|
url: "http://localhost:8080/_matrix/client/api/v1/createRoom?access_token="+accountInfo.access_token,
|
||||||
type: "POST",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
@@ -78,33 +87,22 @@ $('.changeMembership').live('click', function() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/rooms/$roomid/members/$user/state?access_token=$token";
|
var url = "http://localhost:8080/_matrix/client/api/v1/rooms/$roomid/$membership?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomid", encodeURIComponent(roomId));
|
url = url.replace("$roomid", encodeURIComponent(roomId));
|
||||||
url = url.replace("$user", encodeURIComponent(member));
|
url = url.replace("$membership", membership);
|
||||||
|
|
||||||
if (membership === "leave") {
|
var data = {};
|
||||||
$.ajax({
|
|
||||||
url: url,
|
if (membership === "invite") {
|
||||||
type: "DELETE",
|
data = {
|
||||||
contentType: "application/json; charset=utf-8",
|
user_id: member
|
||||||
dataType: "json",
|
|
||||||
success: function(data) {
|
|
||||||
getCurrentRoomList();
|
|
||||||
},
|
|
||||||
error: function(err) {
|
|
||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var data = {
|
|
||||||
membership: membership
|
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify(data),
|
data: JSON.stringify(data),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
@@ -115,17 +113,16 @@ $('.changeMembership').live('click', function() {
|
|||||||
alert(JSON.stringify($.parseJSON(err.responseText)));
|
alert(JSON.stringify($.parseJSON(err.responseText)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$('.joinAlias').live('click', function() {
|
$('.joinAlias').live('click', function() {
|
||||||
var roomAlias = $("#roomAlias").val();
|
var roomAlias = $("#roomAlias").val();
|
||||||
var url = "http://localhost:8080/matrix/client/api/v1/join/$roomalias?access_token=$token";
|
var url = "http://localhost:8080/_matrix/client/api/v1/join/$roomalias?access_token=$token";
|
||||||
url = url.replace("$token", accountInfo.access_token);
|
url = url.replace("$token", accountInfo.access_token);
|
||||||
url = url.replace("$roomalias", encodeURIComponent(roomAlias));
|
url = url.replace("$roomalias", encodeURIComponent(roomAlias));
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: "PUT",
|
type: "POST",
|
||||||
contentType: "application/json; charset=utf-8",
|
contentType: "application/json; charset=utf-8",
|
||||||
data: JSON.stringify({}),
|
data: JSON.stringify({}),
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
|
|||||||
1
setup.py
1
setup.py
@@ -34,6 +34,7 @@ setup(
|
|||||||
"syutil==0.0.1",
|
"syutil==0.0.1",
|
||||||
"Twisted>=14.0.0",
|
"Twisted>=14.0.0",
|
||||||
"service_identity>=1.0.0",
|
"service_identity>=1.0.0",
|
||||||
|
"pyyaml",
|
||||||
"pyasn1",
|
"pyasn1",
|
||||||
"pynacl",
|
"pynacl",
|
||||||
"daemonize",
|
"daemonize",
|
||||||
|
|||||||
@@ -16,4 +16,4 @@
|
|||||||
""" This is a reference implementation of a synapse home server.
|
""" This is a reference implementation of a synapse home server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.1.1"
|
__version__ = "0.2.0"
|
||||||
|
|||||||
@@ -17,9 +17,10 @@
|
|||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import Membership
|
from synapse.api.constants import Membership, JoinRules
|
||||||
from synapse.api.errors import AuthError, StoreError, Codes
|
from synapse.api.errors import AuthError, StoreError, Codes
|
||||||
from synapse.api.events.room import RoomMemberEvent
|
from synapse.api.events.room import RoomMemberEvent
|
||||||
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@@ -44,15 +45,28 @@ class Auth(object):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if hasattr(event, "room_id"):
|
if hasattr(event, "room_id"):
|
||||||
|
is_state = hasattr(event, "state_key")
|
||||||
|
|
||||||
if event.type == RoomMemberEvent.TYPE:
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
|
yield self._can_replace_state(event)
|
||||||
allowed = yield self.is_membership_change_allowed(event)
|
allowed = yield self.is_membership_change_allowed(event)
|
||||||
defer.returnValue(allowed)
|
defer.returnValue(allowed)
|
||||||
else:
|
return
|
||||||
|
|
||||||
self._check_joined_room(
|
self._check_joined_room(
|
||||||
member=snapshot.membership_state,
|
member=snapshot.membership_state,
|
||||||
user_id=snapshot.user_id,
|
user_id=snapshot.user_id,
|
||||||
room_id=snapshot.room_id,
|
room_id=snapshot.room_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if is_state:
|
||||||
|
# TODO (erikj): This really only should be called for *new*
|
||||||
|
# state
|
||||||
|
yield self._can_add_state(event)
|
||||||
|
yield self._can_replace_state(event)
|
||||||
|
else:
|
||||||
|
yield self._can_send_event(event)
|
||||||
|
|
||||||
defer.returnValue(True)
|
defer.returnValue(True)
|
||||||
else:
|
else:
|
||||||
raise AuthError(500, "Unknown event: %s" % event)
|
raise AuthError(500, "Unknown event: %s" % event)
|
||||||
@@ -111,7 +125,14 @@ class Auth(object):
|
|||||||
|
|
||||||
membership = event.content["membership"]
|
membership = event.content["membership"]
|
||||||
|
|
||||||
|
join_rule = yield self.store.get_room_join_rule(event.room_id)
|
||||||
|
if not join_rule:
|
||||||
|
join_rule = JoinRules.INVITE
|
||||||
|
|
||||||
if Membership.INVITE == membership:
|
if Membership.INVITE == membership:
|
||||||
|
# TODO (erikj): We should probably handle this more intelligently
|
||||||
|
# PRIVATE join rules.
|
||||||
|
|
||||||
# Invites are valid iff caller is in the room and target isn't.
|
# Invites are valid iff caller is in the room and target isn't.
|
||||||
if not caller_in_room: # caller isn't joined
|
if not caller_in_room: # caller isn't joined
|
||||||
raise AuthError(403, "You are not in room %s." % event.room_id)
|
raise AuthError(403, "You are not in room %s." % event.room_id)
|
||||||
@@ -124,18 +145,54 @@ class Auth(object):
|
|||||||
# joined: It's a NOOP
|
# joined: It's a NOOP
|
||||||
if event.user_id != target_user_id:
|
if event.user_id != target_user_id:
|
||||||
raise AuthError(403, "Cannot force another user to join.")
|
raise AuthError(403, "Cannot force another user to join.")
|
||||||
elif room.is_public:
|
elif join_rule == JoinRules.PUBLIC or room.is_public:
|
||||||
pass # anyone can join public rooms.
|
pass
|
||||||
elif (not caller or caller.membership not in
|
elif join_rule == JoinRules.INVITE:
|
||||||
[Membership.INVITE, Membership.JOIN]):
|
if (
|
||||||
|
not caller or caller.membership not in
|
||||||
|
[Membership.INVITE, Membership.JOIN]
|
||||||
|
):
|
||||||
raise AuthError(403, "You are not invited to this room.")
|
raise AuthError(403, "You are not invited to this room.")
|
||||||
|
else:
|
||||||
|
# TODO (erikj): may_join list
|
||||||
|
# TODO (erikj): private rooms
|
||||||
|
raise AuthError(403, "You are not allowed to join this room")
|
||||||
elif Membership.LEAVE == membership:
|
elif Membership.LEAVE == membership:
|
||||||
|
# TODO (erikj): Implement kicks.
|
||||||
|
|
||||||
if not caller_in_room: # trying to leave a room you aren't joined
|
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)
|
raise AuthError(403, "You are not in room %s." % event.room_id)
|
||||||
elif target_user_id != event.user_id:
|
elif target_user_id != event.user_id:
|
||||||
# trying to force another user to leave
|
user_level = yield self.store.get_power_level(
|
||||||
raise AuthError(403, "Cannot force %s to leave." %
|
event.room_id,
|
||||||
target_user_id)
|
event.user_id,
|
||||||
|
)
|
||||||
|
_, kick_level = yield self.store.get_ops_levels(event.room_id)
|
||||||
|
|
||||||
|
if kick_level:
|
||||||
|
kick_level = int(kick_level)
|
||||||
|
else:
|
||||||
|
kick_level = 5
|
||||||
|
|
||||||
|
if user_level < kick_level:
|
||||||
|
raise AuthError(
|
||||||
|
403, "You cannot kick user %s." % target_user_id
|
||||||
|
)
|
||||||
|
elif Membership.BAN == membership:
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
ban_level, _ = yield self.store.get_ops_levels(event.room_id)
|
||||||
|
|
||||||
|
if ban_level:
|
||||||
|
ban_level = int(ban_level)
|
||||||
|
else:
|
||||||
|
ban_level = 5 # FIXME (erikj): What should we do here?
|
||||||
|
|
||||||
|
if user_level < ban_level:
|
||||||
|
raise AuthError(403, "You don't have permission to ban")
|
||||||
else:
|
else:
|
||||||
raise AuthError(500, "Unknown membership %s" % membership)
|
raise AuthError(500, "Unknown membership %s" % membership)
|
||||||
|
|
||||||
@@ -176,3 +233,85 @@ class Auth(object):
|
|||||||
except StoreError:
|
except StoreError:
|
||||||
raise AuthError(403, "Unrecognised access token.",
|
raise AuthError(403, "Unrecognised access token.",
|
||||||
errcode=Codes.UNKNOWN_TOKEN)
|
errcode=Codes.UNKNOWN_TOKEN)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
@log_function
|
||||||
|
def _can_send_event(self, event):
|
||||||
|
send_level = yield self.store.get_send_event_level(event.room_id)
|
||||||
|
|
||||||
|
if send_level:
|
||||||
|
send_level = int(send_level)
|
||||||
|
else:
|
||||||
|
send_level = 0
|
||||||
|
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_level:
|
||||||
|
user_level = int(user_level)
|
||||||
|
else:
|
||||||
|
user_level = 0
|
||||||
|
|
||||||
|
if user_level < send_level:
|
||||||
|
raise AuthError(
|
||||||
|
403, "You don't have permission to post to the room"
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _can_add_state(self, event):
|
||||||
|
add_level = yield self.store.get_add_state_level(event.room_id)
|
||||||
|
|
||||||
|
if not add_level:
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
add_level = int(add_level)
|
||||||
|
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
user_level = int(user_level)
|
||||||
|
|
||||||
|
if user_level < add_level:
|
||||||
|
raise AuthError(
|
||||||
|
403, "You don't have permission to add state to the room"
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(True)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _can_replace_state(self, event):
|
||||||
|
current_state = yield self.store.get_current_state(
|
||||||
|
event.room_id,
|
||||||
|
event.type,
|
||||||
|
event.state_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
if current_state:
|
||||||
|
current_state = current_state[0]
|
||||||
|
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_level:
|
||||||
|
user_level = int(user_level)
|
||||||
|
else:
|
||||||
|
user_level = 0
|
||||||
|
|
||||||
|
logger.debug("Checking power level for %s, %s", event.user_id, user_level)
|
||||||
|
if current_state and hasattr(current_state, "required_power_level"):
|
||||||
|
req = current_state.required_power_level
|
||||||
|
|
||||||
|
logger.debug("Checked power level for %s, %s", event.user_id, req)
|
||||||
|
if user_level < req:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to change that state"
|
||||||
|
)
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ class Membership(object):
|
|||||||
JOIN = u"join"
|
JOIN = u"join"
|
||||||
KNOCK = u"knock"
|
KNOCK = u"knock"
|
||||||
LEAVE = u"leave"
|
LEAVE = u"leave"
|
||||||
LIST = (INVITE, JOIN, KNOCK, LEAVE)
|
BAN = u"ban"
|
||||||
|
LIST = (INVITE, JOIN, KNOCK, LEAVE, BAN)
|
||||||
|
|
||||||
|
|
||||||
class Feedback(object):
|
class Feedback(object):
|
||||||
@@ -42,3 +43,10 @@ class PresenceState(object):
|
|||||||
UNAVAILABLE = u"unavailable"
|
UNAVAILABLE = u"unavailable"
|
||||||
ONLINE = u"online"
|
ONLINE = u"online"
|
||||||
FREE_FOR_CHAT = u"free_for_chat"
|
FREE_FOR_CHAT = u"free_for_chat"
|
||||||
|
|
||||||
|
|
||||||
|
class JoinRules(object):
|
||||||
|
PUBLIC = u"public"
|
||||||
|
KNOCK = u"knock"
|
||||||
|
INVITE = u"invite"
|
||||||
|
PRIVATE = u"private"
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
"user_id", # sender/initiator
|
"user_id", # sender/initiator
|
||||||
"content", # HTTP body, JSON
|
"content", # HTTP body, JSON
|
||||||
"state_key",
|
"state_key",
|
||||||
|
"required_power_level",
|
||||||
]
|
]
|
||||||
|
|
||||||
internal_keys = [
|
internal_keys = [
|
||||||
@@ -52,6 +53,7 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
"destinations",
|
"destinations",
|
||||||
"origin",
|
"origin",
|
||||||
"outlier",
|
"outlier",
|
||||||
|
"power_level",
|
||||||
]
|
]
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
@@ -152,3 +154,10 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
msg = self._check_json(entry, template[key][0])
|
msg = self._check_json(entry, template[key][0])
|
||||||
if msg:
|
if msg:
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
class SynapseStateEvent(SynapseEvent):
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
if "state_key" not in kwargs:
|
||||||
|
kwargs["state_key"] = ""
|
||||||
|
super(SynapseStateEvent, self).__init__(**kwargs)
|
||||||
|
|||||||
@@ -16,6 +16,8 @@
|
|||||||
from synapse.api.events.room import (
|
from synapse.api.events.room import (
|
||||||
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
||||||
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
||||||
|
RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent,
|
||||||
|
RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent
|
||||||
)
|
)
|
||||||
|
|
||||||
from synapse.util.stringutils import random_string
|
from synapse.util.stringutils import random_string
|
||||||
@@ -30,7 +32,13 @@ class EventFactory(object):
|
|||||||
RoomMemberEvent,
|
RoomMemberEvent,
|
||||||
FeedbackEvent,
|
FeedbackEvent,
|
||||||
InviteJoinEvent,
|
InviteJoinEvent,
|
||||||
RoomConfigEvent
|
RoomConfigEvent,
|
||||||
|
RoomPowerLevelsEvent,
|
||||||
|
RoomJoinRulesEvent,
|
||||||
|
RoomCreateEvent,
|
||||||
|
RoomAddStateLevelEvent,
|
||||||
|
RoomSendEventLevelEvent,
|
||||||
|
RoomOpsPowerLevelsEvent,
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
from synapse.api.constants import Feedback, Membership
|
from synapse.api.constants import Feedback, Membership
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from . import SynapseEvent
|
from . import SynapseEvent, SynapseStateEvent
|
||||||
|
|
||||||
|
|
||||||
class GenericEvent(SynapseEvent):
|
class GenericEvent(SynapseEvent):
|
||||||
@@ -132,3 +132,45 @@ class RoomConfigEvent(SynapseEvent):
|
|||||||
|
|
||||||
def get_content_template(self):
|
def get_content_template(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomCreateEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.create"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomJoinRulesEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.join_rules"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomPowerLevelsEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.power_levels"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomAddStateLevelEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.add_state_level"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomSendEventLevelEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.send_event_level"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomOpsPowerLevelsEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.ops_levels"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
"""Contains the URL paths to prefix various aspects of the server with. """
|
"""Contains the URL paths to prefix various aspects of the server with. """
|
||||||
|
|
||||||
CLIENT_PREFIX = "/matrix/client/api/v1"
|
CLIENT_PREFIX = "/_matrix/client/api/v1"
|
||||||
FEDERATION_PREFIX = "/matrix/federation/v1"
|
FEDERATION_PREFIX = "/_matrix/federation/v1"
|
||||||
WEB_CLIENT_PREFIX = "/matrix/client"
|
WEB_CLIENT_PREFIX = "/_matrix/client"
|
||||||
CONTENT_REPO_PREFIX = "/matrix/content"
|
CONTENT_REPO_PREFIX = "/_matrix/content"
|
||||||
@@ -20,7 +20,6 @@ from synapse.server import HomeServer
|
|||||||
|
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from twisted.enterprise import adbapi
|
from twisted.enterprise import adbapi
|
||||||
from twisted.python.log import PythonLoggingObserver
|
|
||||||
from twisted.web.resource import Resource
|
from twisted.web.resource import Resource
|
||||||
from twisted.web.static import File
|
from twisted.web.static import File
|
||||||
from twisted.web.server import Site
|
from twisted.web.server import Site
|
||||||
@@ -29,16 +28,17 @@ from synapse.http.client import TwistedHttpClient
|
|||||||
from synapse.api.urls import (
|
from synapse.api.urls import (
|
||||||
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
|
CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
|
||||||
)
|
)
|
||||||
|
from synapse.config.homeserver import HomeServerConfig
|
||||||
|
from synapse.crypto import context_factory
|
||||||
|
|
||||||
from daemonize import Daemonize
|
from daemonize import Daemonize
|
||||||
import twisted.manhole.telnet
|
import twisted.manhole.telnet
|
||||||
|
|
||||||
import argparse
|
|
||||||
import logging
|
import logging
|
||||||
import logging.config
|
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -56,13 +56,13 @@ SCHEMAS = [
|
|||||||
|
|
||||||
# Remember to update this number every time an incompatible change is made to
|
# 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.
|
# database schema files, so the users will be informed on server restarts.
|
||||||
SCHEMA_VERSION = 1
|
SCHEMA_VERSION = 2
|
||||||
|
|
||||||
|
|
||||||
class SynapseHomeServer(HomeServer):
|
class SynapseHomeServer(HomeServer):
|
||||||
|
|
||||||
def build_http_client(self):
|
def build_http_client(self):
|
||||||
return TwistedHttpClient()
|
return TwistedHttpClient(self)
|
||||||
|
|
||||||
def build_resource_for_client(self):
|
def build_resource_for_client(self):
|
||||||
return JsonResource()
|
return JsonResource()
|
||||||
@@ -206,37 +206,17 @@ class SynapseHomeServer(HomeServer):
|
|||||||
"""
|
"""
|
||||||
return "%s-%s" % (resource, path_seg)
|
return "%s-%s" % (resource, path_seg)
|
||||||
|
|
||||||
def start_listening(self, port):
|
def start_listening(self, secure_port, unsecure_port):
|
||||||
reactor.listenTCP(port, Site(self.root_resource))
|
if secure_port is not None:
|
||||||
logger.info("Synapse now listening on port %d", port)
|
reactor.listenSSL(
|
||||||
|
secure_port, Site(self.root_resource), self.tls_context_factory
|
||||||
|
|
||||||
def setup_logging(verbosity=0, filename=None, config_path=None):
|
|
||||||
""" Sets up logging with verbosity levels.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
verbosity: The verbosity level.
|
|
||||||
filename: Log to the given file rather than to the console.
|
|
||||||
config_path: Path to a python logging config file.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if config_path is None:
|
|
||||||
log_format = (
|
|
||||||
'%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
|
|
||||||
)
|
)
|
||||||
|
logger.info("Synapse now listening on port %d", secure_port)
|
||||||
level = logging.INFO
|
if unsecure_port is not None:
|
||||||
if verbosity:
|
reactor.listenTCP(
|
||||||
level = logging.DEBUG
|
unsecure_port, Site(self.root_resource)
|
||||||
|
)
|
||||||
# FIXME: we need a logging.WARN for a -q quiet option
|
logger.info("Synapse now listening on port %d", unsecure_port)
|
||||||
|
|
||||||
logging.basicConfig(level=level, filename=filename, format=log_format)
|
|
||||||
else:
|
|
||||||
logging.config.fileConfig(config_path)
|
|
||||||
|
|
||||||
observer = PythonLoggingObserver()
|
|
||||||
observer.start()
|
|
||||||
|
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
@@ -244,78 +224,53 @@ def run():
|
|||||||
|
|
||||||
|
|
||||||
def setup():
|
def setup():
|
||||||
parser = argparse.ArgumentParser()
|
config = HomeServerConfig.load_config(
|
||||||
parser.add_argument("-p", "--port", dest="port", type=int, default=8080,
|
"Synapse Homeserver",
|
||||||
help="The port to listen on.")
|
sys.argv[1:],
|
||||||
parser.add_argument("-d", "--database", dest="db", default="homeserver.db",
|
generate_section="Homeserver"
|
||||||
help="The database name.")
|
|
||||||
parser.add_argument("-H", "--host", dest="host", default="localhost",
|
|
||||||
help="The hostname of the server.")
|
|
||||||
parser.add_argument('-v', '--verbose', dest="verbose", action='count',
|
|
||||||
help="The verbosity level.")
|
|
||||||
parser.add_argument('-f', '--log-file', dest="log_file", default=None,
|
|
||||||
help="File to log to.")
|
|
||||||
parser.add_argument('--log-config', dest="log_config", default=None,
|
|
||||||
help="Python logging config")
|
|
||||||
parser.add_argument('-D', '--daemonize', action='store_true',
|
|
||||||
default=False, help="Daemonize the home server")
|
|
||||||
parser.add_argument('--pid-file', dest="pid", help="When running as a "
|
|
||||||
"daemon, the file to store the pid in",
|
|
||||||
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
|
|
||||||
|
|
||||||
# Because if/when we daemonize we change to root dir.
|
|
||||||
db_name = os.path.abspath(args.db)
|
|
||||||
log_file = args.log_file
|
|
||||||
if log_file:
|
|
||||||
log_file = os.path.abspath(log_file)
|
|
||||||
|
|
||||||
setup_logging(
|
|
||||||
verbosity=verbosity,
|
|
||||||
filename=log_file,
|
|
||||||
config_path=args.log_config,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info("Server hostname: %s", args.host)
|
config.setup_logging()
|
||||||
|
|
||||||
if re.search(":[0-9]+$", args.host):
|
logger.info("Server hostname: %s", config.server_name)
|
||||||
domain_with_port = args.host
|
|
||||||
|
if re.search(":[0-9]+$", config.server_name):
|
||||||
|
domain_with_port = config.server_name
|
||||||
else:
|
else:
|
||||||
domain_with_port = "%s:%s" % (args.host, args.port)
|
domain_with_port = "%s:%s" % (config.server_name, config.bind_port)
|
||||||
|
|
||||||
|
tls_context_factory = context_factory.ServerContextFactory(config)
|
||||||
|
|
||||||
hs = SynapseHomeServer(
|
hs = SynapseHomeServer(
|
||||||
args.host,
|
config.server_name,
|
||||||
domain_with_port=domain_with_port,
|
domain_with_port=domain_with_port,
|
||||||
upload_dir=os.path.abspath("uploads"),
|
upload_dir=os.path.abspath("uploads"),
|
||||||
db_name=db_name,
|
db_name=config.database_path,
|
||||||
|
tls_context_factory=tls_context_factory,
|
||||||
)
|
)
|
||||||
|
|
||||||
hs.register_servlets()
|
hs.register_servlets()
|
||||||
|
|
||||||
hs.create_resource_tree(
|
hs.create_resource_tree(
|
||||||
web_client=args.webclient,
|
web_client=config.webclient,
|
||||||
redirect_root_to_web_client=True)
|
redirect_root_to_web_client=True,
|
||||||
hs.start_listening(args.port)
|
)
|
||||||
|
hs.start_listening(config.bind_port, config.unsecure_port)
|
||||||
|
|
||||||
hs.get_db_pool()
|
hs.get_db_pool()
|
||||||
|
|
||||||
if args.manhole:
|
if config.manhole:
|
||||||
f = twisted.manhole.telnet.ShellFactory()
|
f = twisted.manhole.telnet.ShellFactory()
|
||||||
f.username = "matrix"
|
f.username = "matrix"
|
||||||
f.password = "rabbithole"
|
f.password = "rabbithole"
|
||||||
f.namespace['hs'] = hs
|
f.namespace['hs'] = hs
|
||||||
reactor.listenTCP(args.manhole, f, interface='127.0.0.1')
|
reactor.listenTCP(config.manhole, f, interface='127.0.0.1')
|
||||||
|
|
||||||
if args.daemonize:
|
if config.daemonize:
|
||||||
|
print config.pid_file
|
||||||
daemon = Daemonize(
|
daemon = Daemonize(
|
||||||
app="synapse-homeserver",
|
app="synapse-homeserver",
|
||||||
pid=args.pid,
|
pid=config.pid_file,
|
||||||
action=run,
|
action=run,
|
||||||
auto_close_fds=False,
|
auto_close_fds=False,
|
||||||
verbose=True,
|
verbose=True,
|
||||||
|
|||||||
14
synapse/config/__init__.py
Normal file
14
synapse/config/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# -*- 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.
|
||||||
130
synapse/config/_base.py
Normal file
130
synapse/config/_base.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
|
||||||
|
import ConfigParser as configparser
|
||||||
|
import argparse
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Config(object):
|
||||||
|
def __init__(self, args):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def abspath(file_path):
|
||||||
|
return os.path.abspath(file_path) if file_path else file_path
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def check_file(cls, file_path, config_name):
|
||||||
|
if file_path is None:
|
||||||
|
raise ConfigError(
|
||||||
|
"Missing config for %s."
|
||||||
|
" Try running again with --generate-config"
|
||||||
|
% (config_name,)
|
||||||
|
)
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
raise ConfigError(
|
||||||
|
"File % config for %s doesn't exist."
|
||||||
|
" Try running again with --generate-config"
|
||||||
|
% (config_name,)
|
||||||
|
)
|
||||||
|
return cls.abspath(file_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def read_file(cls, file_path, config_name):
|
||||||
|
cls.check_file(file_path, config_name)
|
||||||
|
with open(file_path) as file_stream:
|
||||||
|
return file_stream.read()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_config_file(file_path):
|
||||||
|
with open(file_path) as file_stream:
|
||||||
|
return yaml.load(file_stream)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_config(cls, args, config_dir_path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load_config(cls, description, argv, generate_section=None):
|
||||||
|
config_parser = argparse.ArgumentParser(add_help=False)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"-c", "--config-path",
|
||||||
|
metavar="CONFIG_FILE",
|
||||||
|
help="Specify config file"
|
||||||
|
)
|
||||||
|
config_parser.add_argument(
|
||||||
|
"--generate-config",
|
||||||
|
action="store_true",
|
||||||
|
help="Generate config file"
|
||||||
|
)
|
||||||
|
config_args, remaining_args = config_parser.parse_known_args(argv)
|
||||||
|
|
||||||
|
if config_args.generate_config:
|
||||||
|
if not config_args.config_path:
|
||||||
|
config_parser.error(
|
||||||
|
"Must specify where to generate the config file"
|
||||||
|
)
|
||||||
|
config_dir_path = os.path.dirname(config_args.config_path)
|
||||||
|
if os.path.exists(config_args.config_path):
|
||||||
|
defaults = cls.read_config_file(config_args.config_path)
|
||||||
|
else:
|
||||||
|
defaults = {}
|
||||||
|
else:
|
||||||
|
if config_args.config_path:
|
||||||
|
defaults = cls.read_config_file(config_args.config_path)
|
||||||
|
else:
|
||||||
|
defaults = {}
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
parents=[config_parser],
|
||||||
|
description=description,
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
)
|
||||||
|
cls.add_arguments(parser)
|
||||||
|
parser.set_defaults(**defaults)
|
||||||
|
|
||||||
|
args = parser.parse_args(remaining_args)
|
||||||
|
|
||||||
|
if config_args.generate_config:
|
||||||
|
config_dir_path = os.path.dirname(config_args.config_path)
|
||||||
|
config_dir_path = os.path.abspath(config_dir_path)
|
||||||
|
if not os.path.exists(config_dir_path):
|
||||||
|
os.makedirs(config_dir_path)
|
||||||
|
cls.generate_config(args, config_dir_path)
|
||||||
|
config = {}
|
||||||
|
for key, value in vars(args).items():
|
||||||
|
if (key not in set(["config_path", "generate_config"])
|
||||||
|
and value is not None):
|
||||||
|
config[key] = value
|
||||||
|
with open(config_args.config_path, "w") as config_file:
|
||||||
|
yaml.dump(config, config_file, default_flow_style=False)
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
return cls(args)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
37
synapse/config/database.py
Normal file
37
synapse/config/database.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# -*- 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 ._base import Config
|
||||||
|
import os
|
||||||
|
|
||||||
|
class DatabaseConfig(Config):
|
||||||
|
def __init__(self, args):
|
||||||
|
super(DatabaseConfig, self).__init__(args)
|
||||||
|
self.database_path = self.abspath(args.database_path)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(DatabaseConfig, cls).add_arguments(parser)
|
||||||
|
db_group = parser.add_argument_group("database")
|
||||||
|
db_group.add_argument(
|
||||||
|
"-d", "--database-path", default="homeserver.db",
|
||||||
|
help="The database name."
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_config(cls, args, config_dir_path):
|
||||||
|
super(DatabaseConfig, cls).generate_config(args, config_dir_path)
|
||||||
|
args.database_path = os.path.abspath(args.database_path)
|
||||||
|
|
||||||
26
synapse/config/homeserver.py
Normal file
26
synapse/config/homeserver.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# -*- 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 .tls import TlsConfig
|
||||||
|
from .server import ServerConfig
|
||||||
|
from .logger import LoggingConfig
|
||||||
|
from .database import DatabaseConfig
|
||||||
|
|
||||||
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig):
|
||||||
|
pass
|
||||||
|
|
||||||
|
if __name__=='__main__':
|
||||||
|
import sys
|
||||||
|
HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
|
||||||
67
synapse/config/logger.py
Normal file
67
synapse/config/logger.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# -*- 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 ._base import Config
|
||||||
|
|
||||||
|
from twisted.python.log import PythonLoggingObserver
|
||||||
|
import logging
|
||||||
|
import logging.config
|
||||||
|
|
||||||
|
class LoggingConfig(Config):
|
||||||
|
def __init__(self, args):
|
||||||
|
super(LoggingConfig, self).__init__(args)
|
||||||
|
self.verbosity = int(args.verbose) if args.verbose else None
|
||||||
|
self.log_config = self.abspath(args.log_config)
|
||||||
|
self.log_file = self.abspath(args.log_file)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(LoggingConfig, cls).add_arguments(parser)
|
||||||
|
logging_group = parser.add_argument_group("logging")
|
||||||
|
logging_group.add_argument(
|
||||||
|
'-v', '--verbose', dest="verbose", action='count',
|
||||||
|
help="The verbosity level."
|
||||||
|
)
|
||||||
|
logging_group.add_argument(
|
||||||
|
'-f', '--log-file', dest="log_file", default=None,
|
||||||
|
help="File to log to."
|
||||||
|
)
|
||||||
|
logging_group.add_argument(
|
||||||
|
'--log-config', dest="log_config", default=None,
|
||||||
|
help="Python logging config file"
|
||||||
|
)
|
||||||
|
|
||||||
|
def setup_logging(self):
|
||||||
|
log_format = (
|
||||||
|
'%(asctime)s - %(name)s - %(lineno)d - %(levelname)s - %(message)s'
|
||||||
|
)
|
||||||
|
if self.log_config is None:
|
||||||
|
|
||||||
|
level = logging.INFO
|
||||||
|
if self.verbosity:
|
||||||
|
level = logging.DEBUG
|
||||||
|
|
||||||
|
# FIXME: we need a logging.WARN for a -q quiet option
|
||||||
|
|
||||||
|
logging.basicConfig(
|
||||||
|
level=level,
|
||||||
|
filename=self.log_file,
|
||||||
|
format=log_format
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
logging.config.fileConfig(self.log_config)
|
||||||
|
|
||||||
|
observer = PythonLoggingObserver()
|
||||||
|
observer.start()
|
||||||
79
synapse/config/server.py
Normal file
79
synapse/config/server.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
import nacl.signing
|
||||||
|
import os
|
||||||
|
from ._base import Config
|
||||||
|
from syutil.base64util import encode_base64, decode_base64
|
||||||
|
|
||||||
|
|
||||||
|
class ServerConfig(Config):
|
||||||
|
def __init__(self, args):
|
||||||
|
super(ServerConfig, self).__init__(args)
|
||||||
|
self.server_name = args.server_name
|
||||||
|
self.signing_key = self.read_signing_key(args.signing_key_path)
|
||||||
|
self.bind_port = args.bind_port
|
||||||
|
self.bind_host = args.bind_host
|
||||||
|
self.unsecure_port = args.unsecure_port
|
||||||
|
self.daemonize = args.daemonize
|
||||||
|
self.pid_file = self.abspath(args.pid_file)
|
||||||
|
self.webclient = True
|
||||||
|
self.manhole = args.manhole
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(ServerConfig, cls).add_arguments(parser)
|
||||||
|
server_group = parser.add_argument_group("server")
|
||||||
|
server_group.add_argument("-H", "--server-name", default="localhost",
|
||||||
|
help="The name of the server")
|
||||||
|
server_group.add_argument("--signing-key-path",
|
||||||
|
help="The signing key to sign messages with")
|
||||||
|
server_group.add_argument("-p", "--bind-port", metavar="PORT",
|
||||||
|
type=int, help="https port to listen on",
|
||||||
|
default=8448)
|
||||||
|
server_group.add_argument("--unsecure-port", metavar="PORT",
|
||||||
|
type=int, help="http port to listen on",
|
||||||
|
default=8008)
|
||||||
|
server_group.add_argument("--bind-host", default="",
|
||||||
|
help="Local interface to listen on")
|
||||||
|
server_group.add_argument("-D", "--daemonize", action='store_true',
|
||||||
|
help="Daemonize the home server")
|
||||||
|
server_group.add_argument('--pid-file', default="hs.pid",
|
||||||
|
help="When running as a daemon, the file to"
|
||||||
|
" store the pid in")
|
||||||
|
server_group.add_argument("--manhole", metavar="PORT", dest="manhole",
|
||||||
|
type=int,
|
||||||
|
help="Turn on the twisted telnet manhole"
|
||||||
|
" service on the given port.")
|
||||||
|
|
||||||
|
def read_signing_key(self, signing_key_path):
|
||||||
|
signing_key_base64 = self.read_file(signing_key_path, "signing_key")
|
||||||
|
signing_key_bytes = decode_base64(signing_key_base64)
|
||||||
|
return nacl.signing.SigningKey(signing_key_bytes)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_config(cls, args, config_dir_path):
|
||||||
|
super(ServerConfig, cls).generate_config(args, config_dir_path)
|
||||||
|
base_key_name = os.path.join(config_dir_path, args.server_name)
|
||||||
|
|
||||||
|
args.pid_file = os.path.abspath(args.pid_file)
|
||||||
|
|
||||||
|
if not args.signing_key_path:
|
||||||
|
args.signing_key_path = base_key_name + ".signing.key"
|
||||||
|
|
||||||
|
if not os.path.exists(args.signing_key_path):
|
||||||
|
with open(args.signing_key_path, "w") as signing_key_file:
|
||||||
|
key = nacl.signing.SigningKey.generate()
|
||||||
|
signing_key_file.write(encode_base64(key.encode()))
|
||||||
130
synapse/config/tls.py
Normal file
130
synapse/config/tls.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# -*- 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 ._base import Config
|
||||||
|
|
||||||
|
from OpenSSL import crypto
|
||||||
|
import subprocess
|
||||||
|
import os
|
||||||
|
|
||||||
|
GENERATE_DH_PARAMS=False
|
||||||
|
|
||||||
|
|
||||||
|
class TlsConfig(Config):
|
||||||
|
def __init__(self, args):
|
||||||
|
super(TlsConfig, self).__init__(args)
|
||||||
|
self.tls_certificate = self.read_tls_certificate(
|
||||||
|
args.tls_certificate_path
|
||||||
|
)
|
||||||
|
self.tls_private_key = self.read_tls_private_key(
|
||||||
|
args.tls_private_key_path
|
||||||
|
)
|
||||||
|
self.tls_dh_params_path = self.check_file(
|
||||||
|
args.tls_dh_params_path, "tls_dh_params"
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(TlsConfig, cls).add_arguments(parser)
|
||||||
|
tls_group = parser.add_argument_group("tls")
|
||||||
|
tls_group.add_argument("--tls-certificate-path",
|
||||||
|
help="PEM encoded X509 certificate for TLS")
|
||||||
|
tls_group.add_argument("--tls-private-key-path",
|
||||||
|
help="PEM encoded private key for TLS")
|
||||||
|
tls_group.add_argument("--tls-dh-params-path",
|
||||||
|
help="PEM dh parameters for ephemeral keys")
|
||||||
|
|
||||||
|
def read_tls_certificate(self, cert_path):
|
||||||
|
cert_pem = self.read_file(cert_path, "tls_certificate")
|
||||||
|
return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
|
||||||
|
|
||||||
|
def read_tls_private_key(self, private_key_path):
|
||||||
|
private_key_pem = self.read_file(private_key_path, "tls_private_key")
|
||||||
|
return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def generate_config(cls, args, config_dir_path):
|
||||||
|
super(TlsConfig, cls).generate_config(args, config_dir_path)
|
||||||
|
base_key_name = os.path.join(config_dir_path, args.server_name)
|
||||||
|
|
||||||
|
if args.tls_certificate_path is None:
|
||||||
|
args.tls_certificate_path = base_key_name + ".tls.crt"
|
||||||
|
|
||||||
|
if args.tls_private_key_path is None:
|
||||||
|
args.tls_private_key_path = base_key_name + ".tls.key"
|
||||||
|
|
||||||
|
if args.tls_dh_params_path is None:
|
||||||
|
args.tls_dh_params_path = base_key_name + ".tls.dh"
|
||||||
|
|
||||||
|
if not os.path.exists(args.tls_private_key_path):
|
||||||
|
with open(args.tls_private_key_path, "w") as private_key_file:
|
||||||
|
tls_private_key = crypto.PKey()
|
||||||
|
tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
|
||||||
|
private_key_pem = crypto.dump_privatekey(
|
||||||
|
crypto.FILETYPE_PEM, tls_private_key
|
||||||
|
)
|
||||||
|
private_key_file.write(private_key_pem)
|
||||||
|
else:
|
||||||
|
with open(args.tls_private_key_path) as private_key_file:
|
||||||
|
private_key_pem = private_key_file.read()
|
||||||
|
tls_private_key = crypto.load_privatekey(
|
||||||
|
crypto.FILETYPE_PEM, private_key_pem
|
||||||
|
)
|
||||||
|
|
||||||
|
if not os.path.exists(args.tls_certificate_path):
|
||||||
|
with open(args.tls_certificate_path, "w") as certifcate_file:
|
||||||
|
cert = crypto.X509()
|
||||||
|
subject = cert.get_subject()
|
||||||
|
subject.CN = args.server_name
|
||||||
|
|
||||||
|
cert.set_serial_number(1000)
|
||||||
|
cert.gmtime_adj_notBefore(0)
|
||||||
|
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
|
||||||
|
cert.set_issuer(cert.get_subject())
|
||||||
|
cert.set_pubkey(tls_private_key)
|
||||||
|
|
||||||
|
cert.sign(tls_private_key, 'sha256')
|
||||||
|
|
||||||
|
cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
||||||
|
|
||||||
|
certifcate_file.write(cert_pem)
|
||||||
|
|
||||||
|
if not os.path.exists(args.tls_dh_params_path):
|
||||||
|
if GENERATE_DH_PARAMS:
|
||||||
|
subprocess.check_call([
|
||||||
|
"openssl", "dhparam",
|
||||||
|
"-outform", "PEM",
|
||||||
|
"-out", args.tls_dh_params_path,
|
||||||
|
"2048"
|
||||||
|
])
|
||||||
|
else:
|
||||||
|
with open(args.tls_dh_params_path, "w") as dh_params_file:
|
||||||
|
dh_params_file.write(
|
||||||
|
"2048-bit DH parameters taken from rfc3526\n"
|
||||||
|
"-----BEGIN DH PARAMETERS-----\n"
|
||||||
|
"MIIBCAKCAQEA///////////JD9qiIWjC"
|
||||||
|
"NMTGYouA3BzRKQJOCIpnzHQCC76mOxOb\n"
|
||||||
|
"IlFKCHmONATd75UZs806QxswKwpt8l8U"
|
||||||
|
"N0/hNW1tUcJF5IW1dmJefsb0TELppjft\n"
|
||||||
|
"awv/XLb0Brft7jhr+1qJn6WunyQRfEsf"
|
||||||
|
"5kkoZlHs5Fs9wgB8uKFjvwWY2kg2HFXT\n"
|
||||||
|
"mmkWP6j9JM9fg2VdI9yjrZYcYvNWIIVS"
|
||||||
|
"u57VKQdwlpZtZww1Tkq8mATxdGwIyhgh\n"
|
||||||
|
"fDKQXkYuNs474553LBgOhgObJ4Oi7Aei"
|
||||||
|
"j7XFXfBvTFLJ3ivL9pVYFxg5lUl86pVq\n"
|
||||||
|
"5RXSJhiY+gUQFXKOWoqsqmj/////////"
|
||||||
|
"/wIBAg==\n"
|
||||||
|
"-----END DH PARAMETERS-----\n"
|
||||||
|
)
|
||||||
@@ -1,160 +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.
|
|
||||||
|
|
||||||
|
|
||||||
import ConfigParser as configparser
|
|
||||||
import argparse
|
|
||||||
import socket
|
|
||||||
import sys
|
|
||||||
import os
|
|
||||||
from OpenSSL import crypto
|
|
||||||
import nacl.signing
|
|
||||||
from syutil.base64util import encode_base64
|
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
|
||||||
def load_config(description, argv):
|
|
||||||
config_parser = argparse.ArgumentParser(add_help=False)
|
|
||||||
config_parser.add_argument("-c", "--config-path", metavar="CONFIG_FILE",
|
|
||||||
help="Specify config file")
|
|
||||||
config_args, remaining_args = config_parser.parse_known_args(argv)
|
|
||||||
if config_args.config_path:
|
|
||||||
config = configparser.SafeConfigParser()
|
|
||||||
config.read([config_args.config_path])
|
|
||||||
defaults = dict(config.items("KeyServer"))
|
|
||||||
else:
|
|
||||||
defaults = {}
|
|
||||||
parser = argparse.ArgumentParser(
|
|
||||||
parents=[config_parser],
|
|
||||||
description=description,
|
|
||||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
||||||
)
|
|
||||||
parser.set_defaults(**defaults)
|
|
||||||
parser.add_argument("--server-name", default=socket.getfqdn(),
|
|
||||||
help="The name of the server")
|
|
||||||
parser.add_argument("--signing-key-path",
|
|
||||||
help="The signing key to sign responses with")
|
|
||||||
parser.add_argument("--tls-certificate-path",
|
|
||||||
help="PEM encoded X509 certificate for TLS")
|
|
||||||
parser.add_argument("--tls-private-key-path",
|
|
||||||
help="PEM encoded private key for TLS")
|
|
||||||
parser.add_argument("--tls-dh-params-path",
|
|
||||||
help="PEM encoded dh parameters for ephemeral keys")
|
|
||||||
parser.add_argument("--bind-port", type=int,
|
|
||||||
help="TCP port to listen on")
|
|
||||||
parser.add_argument("--bind-host", default="",
|
|
||||||
help="Local interface to listen on")
|
|
||||||
|
|
||||||
args = parser.parse_args(remaining_args)
|
|
||||||
|
|
||||||
server_config = vars(args)
|
|
||||||
del server_config["config_path"]
|
|
||||||
return server_config
|
|
||||||
|
|
||||||
|
|
||||||
def generate_config(argv):
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("-c", "--config-path", help="Specify config file",
|
|
||||||
metavar="CONFIG_FILE", required=True)
|
|
||||||
parser.add_argument("--server-name", default=socket.getfqdn(),
|
|
||||||
help="The name of the server")
|
|
||||||
parser.add_argument("--signing-key-path",
|
|
||||||
help="The signing key to sign responses with")
|
|
||||||
parser.add_argument("--tls-certificate-path",
|
|
||||||
help="PEM encoded X509 certificate for TLS")
|
|
||||||
parser.add_argument("--tls-private-key-path",
|
|
||||||
help="PEM encoded private key for TLS")
|
|
||||||
parser.add_argument("--tls-dh-params-path",
|
|
||||||
help="PEM encoded dh parameters for ephemeral keys")
|
|
||||||
parser.add_argument("--bind-port", type=int, required=True,
|
|
||||||
help="TCP port to listen on")
|
|
||||||
parser.add_argument("--bind-host", default="",
|
|
||||||
help="Local interface to listen on")
|
|
||||||
|
|
||||||
args = parser.parse_args(argv)
|
|
||||||
|
|
||||||
dir_name = os.path.dirname(args.config_path)
|
|
||||||
base_key_name = os.path.join(dir_name, args.server_name)
|
|
||||||
|
|
||||||
if args.signing_key_path is None:
|
|
||||||
args.signing_key_path = base_key_name + ".signing.key"
|
|
||||||
|
|
||||||
if args.tls_certificate_path is None:
|
|
||||||
args.tls_certificate_path = base_key_name + ".tls.crt"
|
|
||||||
|
|
||||||
if args.tls_private_key_path is None:
|
|
||||||
args.tls_private_key_path = base_key_name + ".tls.key"
|
|
||||||
|
|
||||||
if args.tls_dh_params_path is None:
|
|
||||||
args.tls_dh_params_path = base_key_name + ".tls.dh"
|
|
||||||
|
|
||||||
if not os.path.exists(args.signing_key_path):
|
|
||||||
with open(args.signing_key_path, "w") as signing_key_file:
|
|
||||||
key = nacl.signing.SigningKey.generate()
|
|
||||||
signing_key_file.write(encode_base64(key.encode()))
|
|
||||||
|
|
||||||
if not os.path.exists(args.tls_private_key_path):
|
|
||||||
with open(args.tls_private_key_path, "w") as private_key_file:
|
|
||||||
tls_private_key = crypto.PKey()
|
|
||||||
tls_private_key.generate_key(crypto.TYPE_RSA, 2048)
|
|
||||||
private_key_pem = crypto.dump_privatekey(
|
|
||||||
crypto.FILETYPE_PEM, tls_private_key
|
|
||||||
)
|
|
||||||
private_key_file.write(private_key_pem)
|
|
||||||
else:
|
|
||||||
with open(args.tls_private_key_path) as private_key_file:
|
|
||||||
private_key_pem = private_key_file.read()
|
|
||||||
tls_private_key = crypto.load_privatekey(
|
|
||||||
crypto.FILETYPE_PEM, private_key_pem
|
|
||||||
)
|
|
||||||
|
|
||||||
if not os.path.exists(args.tls_certificate_path):
|
|
||||||
with open(args.tls_certificate_path, "w") as certifcate_file:
|
|
||||||
cert = crypto.X509()
|
|
||||||
subject = cert.get_subject()
|
|
||||||
subject.CN = args.server_name
|
|
||||||
|
|
||||||
cert.set_serial_number(1000)
|
|
||||||
cert.gmtime_adj_notBefore(0)
|
|
||||||
cert.gmtime_adj_notAfter(10 * 365 * 24 * 60 * 60)
|
|
||||||
cert.set_issuer(cert.get_subject())
|
|
||||||
cert.set_pubkey(tls_private_key)
|
|
||||||
|
|
||||||
cert.sign(tls_private_key, 'sha256')
|
|
||||||
|
|
||||||
cert_pem = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
|
|
||||||
|
|
||||||
certifcate_file.write(cert_pem)
|
|
||||||
|
|
||||||
if not os.path.exists(args.tls_dh_params_path):
|
|
||||||
subprocess.check_call([
|
|
||||||
"openssl", "dhparam",
|
|
||||||
"-outform", "PEM",
|
|
||||||
"-out", args.tls_dh_params_path,
|
|
||||||
"2048"
|
|
||||||
])
|
|
||||||
|
|
||||||
config = configparser.SafeConfigParser()
|
|
||||||
config.add_section("KeyServer")
|
|
||||||
for key, value in vars(args).items():
|
|
||||||
if key != "config_path":
|
|
||||||
config.set("KeyServer", key, str(value))
|
|
||||||
|
|
||||||
with open(args.config_path, "w") as config_file:
|
|
||||||
config.write(config_file)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
generate_config(sys.argv[1:])
|
|
||||||
29
synapse/crypto/context_factory.py
Normal file
29
synapse/crypto/context_factory.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from twisted.internet import reactor, ssl
|
||||||
|
from OpenSSL import SSL
|
||||||
|
from twisted.internet._sslverify import _OpenSSLECCurve, _defaultCurveName
|
||||||
|
|
||||||
|
|
||||||
|
class ServerContextFactory(ssl.ContextFactory):
|
||||||
|
"""Factory for PyOpenSSL SSL contexts that are used to handle incoming
|
||||||
|
connections and to make connections to remote servers."""
|
||||||
|
|
||||||
|
def __init__(self, config):
|
||||||
|
self._context = SSL.Context(SSL.SSLv23_METHOD)
|
||||||
|
self.configure_context(self._context, config)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def configure_context(context, config):
|
||||||
|
try:
|
||||||
|
_ecCurve = _OpenSSLECCurve(_defaultCurveName)
|
||||||
|
_ecCurve.addECKeyToContext(context)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
|
||||||
|
context.use_certificate(config.tls_certificate)
|
||||||
|
context.use_privatekey(config.tls_private_key)
|
||||||
|
context.load_tmp_dh(config.tls_dh_params_path)
|
||||||
|
context.set_cipher_list("!ADH:HIGH+kEDH:!AECDH:HIGH+kEECDH")
|
||||||
|
|
||||||
|
def getContext(self):
|
||||||
|
return self._context
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ class Pdu(JsonEncodedObject):
|
|||||||
"power_level",
|
"power_level",
|
||||||
"prev_state_id",
|
"prev_state_id",
|
||||||
"prev_state_origin",
|
"prev_state_origin",
|
||||||
|
"required_power_level",
|
||||||
]
|
]
|
||||||
|
|
||||||
internal_keys = [
|
internal_keys = [
|
||||||
|
|||||||
@@ -76,6 +76,10 @@ class MessageHandler(BaseRoomHandler):
|
|||||||
Raises:
|
Raises:
|
||||||
SynapseError if something went wrong.
|
SynapseError if something went wrong.
|
||||||
"""
|
"""
|
||||||
|
# TODO(paul): Why does 'event' not have a 'user' object?
|
||||||
|
user = self.hs.parse_userid(event.user_id)
|
||||||
|
assert user.is_mine, "User must be our own: %s" % (user,)
|
||||||
|
|
||||||
if stamp_event:
|
if stamp_event:
|
||||||
event.content["hsob_ts"] = int(self.clock.time_msec())
|
event.content["hsob_ts"] = int(self.clock.time_msec())
|
||||||
|
|
||||||
@@ -86,6 +90,10 @@ class MessageHandler(BaseRoomHandler):
|
|||||||
|
|
||||||
yield self._on_new_room_event(event, snapshot)
|
yield self._on_new_room_event(event, snapshot)
|
||||||
|
|
||||||
|
self.hs.get_handlers().presence_handler.bump_presence_active_time(
|
||||||
|
user
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
|
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
|
||||||
feedback=False):
|
feedback=False):
|
||||||
@@ -274,11 +282,11 @@ class MessageHandler(BaseRoomHandler):
|
|||||||
messages, token = yield self.store.get_recent_events_for_room(
|
messages, token = yield self.store.get_recent_events_for_room(
|
||||||
event.room_id,
|
event.room_id,
|
||||||
limit=limit,
|
limit=limit,
|
||||||
end_token=now_token.events_key,
|
end_token=now_token.room_key,
|
||||||
)
|
)
|
||||||
|
|
||||||
start_token = now_token.copy_and_replace("events_key", token[0])
|
start_token = now_token.copy_and_replace("room_key", token[0])
|
||||||
end_token = now_token.copy_and_replace("events_key", token[1])
|
end_token = now_token.copy_and_replace("room_key", token[1])
|
||||||
|
|
||||||
d["messages"] = {
|
d["messages"] = {
|
||||||
"chunk": [m.get_dict() for m in messages],
|
"chunk": [m.get_dict() for m in messages],
|
||||||
|
|||||||
@@ -52,6 +52,13 @@ def partitionbool(l, func):
|
|||||||
|
|
||||||
class PresenceHandler(BaseHandler):
|
class PresenceHandler(BaseHandler):
|
||||||
|
|
||||||
|
STATE_LEVELS = {
|
||||||
|
PresenceState.OFFLINE: 0,
|
||||||
|
PresenceState.UNAVAILABLE: 1,
|
||||||
|
PresenceState.ONLINE: 2,
|
||||||
|
PresenceState.FREE_FOR_CHAT: 3,
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(PresenceHandler, self).__init__(hs)
|
super(PresenceHandler, self).__init__(hs)
|
||||||
|
|
||||||
@@ -135,7 +142,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
return self._user_cachemap[user]
|
return self._user_cachemap[user]
|
||||||
else:
|
else:
|
||||||
statuscache = UserPresenceCache()
|
statuscache = UserPresenceCache()
|
||||||
statuscache.update({"state": PresenceState.OFFLINE}, user)
|
statuscache.update({"presence": PresenceState.OFFLINE}, user)
|
||||||
return statuscache
|
return statuscache
|
||||||
|
|
||||||
def registered_user(self, user):
|
def registered_user(self, user):
|
||||||
@@ -143,10 +150,6 @@ class PresenceHandler(BaseHandler):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def is_presence_visible(self, observer_user, observed_user):
|
def is_presence_visible(self, observer_user, observed_user):
|
||||||
defer.returnValue(True)
|
|
||||||
# return
|
|
||||||
# FIXME (erikj): This code path absolutely kills the database.
|
|
||||||
|
|
||||||
assert(observed_user.is_mine)
|
assert(observed_user.is_mine)
|
||||||
|
|
||||||
if observer_user == observed_user:
|
if observer_user == observed_user:
|
||||||
@@ -173,19 +176,24 @@ class PresenceHandler(BaseHandler):
|
|||||||
observed_user=target_user
|
observed_user=target_user
|
||||||
)
|
)
|
||||||
|
|
||||||
if visible:
|
if not visible:
|
||||||
state = yield self.store.get_presence_state(
|
|
||||||
target_user.localpart
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise SynapseError(404, "Presence information not visible")
|
raise SynapseError(404, "Presence information not visible")
|
||||||
|
state = yield self.store.get_presence_state(target_user.localpart)
|
||||||
|
if "mtime" in state:
|
||||||
|
del state["mtime"]
|
||||||
|
state["presence"] = state["state"]
|
||||||
|
|
||||||
|
if target_user in self._user_cachemap:
|
||||||
|
state["last_active"] = (
|
||||||
|
self._user_cachemap[target_user].get_state()["last_active"]
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# TODO(paul): Have remote server send us permissions set
|
# TODO(paul): Have remote server send us permissions set
|
||||||
state = self._get_or_offline_usercache(target_user).get_state()
|
state = self._get_or_offline_usercache(target_user).get_state()
|
||||||
|
|
||||||
if "mtime" in state and (state["mtime"] is not None):
|
if "last_active" in state:
|
||||||
state["mtime_age"] = int(
|
state["last_active_ago"] = int(
|
||||||
self.clock.time_msec() - state.pop("mtime")
|
self.clock.time_msec() - state.pop("last_active")
|
||||||
)
|
)
|
||||||
defer.returnValue(state)
|
defer.returnValue(state)
|
||||||
|
|
||||||
@@ -202,20 +210,33 @@ class PresenceHandler(BaseHandler):
|
|||||||
if target_user != auth_user:
|
if target_user != auth_user:
|
||||||
raise AuthError(400, "Cannot set another user's displayname")
|
raise AuthError(400, "Cannot set another user's displayname")
|
||||||
|
|
||||||
# TODO(paul): Sanity-check 'state'
|
|
||||||
if "status_msg" not in state:
|
if "status_msg" not in state:
|
||||||
state["status_msg"] = None
|
state["status_msg"] = None
|
||||||
|
|
||||||
for k in state.keys():
|
for k in state.keys():
|
||||||
if k not in ("state", "status_msg"):
|
if k not in ("presence", "state", "status_msg"):
|
||||||
raise SynapseError(
|
raise SynapseError(
|
||||||
400, "Unexpected presence state key '%s'" % (k,)
|
400, "Unexpected presence state key '%s'" % (k,)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Handle legacy "state" key for now
|
||||||
|
if "state" in state:
|
||||||
|
state["presence"] = state.pop("state")
|
||||||
|
|
||||||
|
if state["presence"] not in self.STATE_LEVELS:
|
||||||
|
raise SynapseError(400, "'%s' is not a valid presence state" %
|
||||||
|
state["presence"]
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug("Updating presence state of %s to %s",
|
logger.debug("Updating presence state of %s to %s",
|
||||||
target_user.localpart, state["state"])
|
target_user.localpart, state["presence"])
|
||||||
|
|
||||||
state_to_store = dict(state)
|
state_to_store = dict(state)
|
||||||
|
state_to_store["state"] = state_to_store.pop("presence")
|
||||||
|
|
||||||
|
statuscache=self._get_or_offline_usercache(target_user)
|
||||||
|
was_level = self.STATE_LEVELS[statuscache.get_state()["presence"]]
|
||||||
|
now_level = self.STATE_LEVELS[state["presence"]]
|
||||||
|
|
||||||
yield defer.DeferredList([
|
yield defer.DeferredList([
|
||||||
self.store.set_presence_state(
|
self.store.set_presence_state(
|
||||||
@@ -226,9 +247,10 @@ class PresenceHandler(BaseHandler):
|
|||||||
),
|
),
|
||||||
])
|
])
|
||||||
|
|
||||||
state["mtime"] = self.clock.time_msec()
|
if now_level > was_level:
|
||||||
|
state["last_active"] = self.clock.time_msec()
|
||||||
|
|
||||||
now_online = state["state"] != PresenceState.OFFLINE
|
now_online = state["presence"] != PresenceState.OFFLINE
|
||||||
was_polling = target_user in self._user_cachemap
|
was_polling = target_user in self._user_cachemap
|
||||||
|
|
||||||
if now_online and not was_polling:
|
if now_online and not was_polling:
|
||||||
@@ -240,6 +262,12 @@ class PresenceHandler(BaseHandler):
|
|||||||
# we don't have to do this all the time
|
# we don't have to do this all the time
|
||||||
self.changed_presencelike_data(target_user, state)
|
self.changed_presencelike_data(target_user, state)
|
||||||
|
|
||||||
|
def bump_presence_active_time(self, user, now=None):
|
||||||
|
if now is None:
|
||||||
|
now = self.clock.time_msec()
|
||||||
|
|
||||||
|
self.changed_presencelike_data(user, {"last_active": now})
|
||||||
|
|
||||||
def changed_presencelike_data(self, user, state):
|
def changed_presencelike_data(self, user, state):
|
||||||
statuscache = self._get_or_make_usercache(user)
|
statuscache = self._get_or_make_usercache(user)
|
||||||
|
|
||||||
@@ -251,28 +279,27 @@ class PresenceHandler(BaseHandler):
|
|||||||
@log_function
|
@log_function
|
||||||
def started_user_eventstream(self, user):
|
def started_user_eventstream(self, user):
|
||||||
# TODO(paul): Use "last online" state
|
# TODO(paul): Use "last online" state
|
||||||
self.set_state(user, user, {"state": PresenceState.ONLINE})
|
self.set_state(user, user, {"presence": PresenceState.ONLINE})
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
def stopped_user_eventstream(self, user):
|
def stopped_user_eventstream(self, user):
|
||||||
# TODO(paul): Save current state as "last online" state
|
# TODO(paul): Save current state as "last online" state
|
||||||
self.set_state(user, user, {"state": PresenceState.OFFLINE})
|
self.set_state(user, user, {"presence": PresenceState.OFFLINE})
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def user_joined_room(self, user, room_id):
|
def user_joined_room(self, user, room_id):
|
||||||
|
|
||||||
if user.is_mine:
|
if user.is_mine:
|
||||||
|
statuscache = self._get_or_make_usercache(user)
|
||||||
|
|
||||||
|
# No actual update but we need to bump the serial anyway for the
|
||||||
|
# event source
|
||||||
|
self._user_cachemap_latest_serial += 1
|
||||||
|
statuscache.update({}, serial=self._user_cachemap_latest_serial)
|
||||||
|
|
||||||
self.push_update_to_local_and_remote(
|
self.push_update_to_local_and_remote(
|
||||||
observed_user=user,
|
observed_user=user,
|
||||||
room_ids=[room_id],
|
room_ids=[room_id],
|
||||||
statuscache=self._get_or_offline_usercache(user),
|
statuscache=statuscache,
|
||||||
)
|
|
||||||
|
|
||||||
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.
|
# We also want to tell them about current presence of people.
|
||||||
@@ -386,9 +413,9 @@ class PresenceHandler(BaseHandler):
|
|||||||
observed_user = self.hs.parse_userid(p.pop("observed_user_id"))
|
observed_user = self.hs.parse_userid(p.pop("observed_user_id"))
|
||||||
p["observed_user"] = observed_user
|
p["observed_user"] = observed_user
|
||||||
p.update(self._get_or_offline_usercache(observed_user).get_state())
|
p.update(self._get_or_offline_usercache(observed_user).get_state())
|
||||||
if "mtime" in p:
|
if "last_active" in p:
|
||||||
p["mtime_age"] = int(
|
p["last_active_ago"] = int(
|
||||||
self.clock.time_msec() - p.pop("mtime")
|
self.clock.time_msec() - p.pop("last_active")
|
||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue(presence)
|
defer.returnValue(presence)
|
||||||
@@ -457,10 +484,6 @@ class PresenceHandler(BaseHandler):
|
|||||||
def _start_polling_local(self, user, target_user):
|
def _start_polling_local(self, user, target_user):
|
||||||
target_localpart = target_user.localpart
|
target_localpart = target_user.localpart
|
||||||
|
|
||||||
if not self.is_presence_visible(observer_user=user,
|
|
||||||
observed_user=target_user):
|
|
||||||
return
|
|
||||||
|
|
||||||
if target_localpart not in self._local_pushmap:
|
if target_localpart not in self._local_pushmap:
|
||||||
self._local_pushmap[target_localpart] = set()
|
self._local_pushmap[target_localpart] = set()
|
||||||
|
|
||||||
@@ -577,21 +600,30 @@ class PresenceHandler(BaseHandler):
|
|||||||
def _push_presence_remote(self, user, destination, state=None):
|
def _push_presence_remote(self, user, destination, state=None):
|
||||||
if state is None:
|
if state is None:
|
||||||
state = yield self.store.get_presence_state(user.localpart)
|
state = yield self.store.get_presence_state(user.localpart)
|
||||||
|
del state["mtime"]
|
||||||
|
state["presence"] = state["state"]
|
||||||
|
|
||||||
|
if user in self._user_cachemap:
|
||||||
|
state["last_active"] = (
|
||||||
|
self._user_cachemap[user].get_state()["last_active"]
|
||||||
|
)
|
||||||
|
|
||||||
yield self.distributor.fire(
|
yield self.distributor.fire(
|
||||||
"collect_presencelike_data", user, state
|
"collect_presencelike_data", user, state
|
||||||
)
|
)
|
||||||
|
|
||||||
if "mtime" in state:
|
if "last_active" in state:
|
||||||
state = dict(state)
|
state = dict(state)
|
||||||
state["mtime_age"] = int(
|
state["last_active_ago"] = int(
|
||||||
self.clock.time_msec() - state.pop("mtime")
|
self.clock.time_msec() - state.pop("last_active")
|
||||||
)
|
)
|
||||||
|
|
||||||
user_state = {
|
user_state = {
|
||||||
"user_id": user.to_string(),
|
"user_id": user.to_string(),
|
||||||
}
|
}
|
||||||
user_state.update(**state)
|
user_state.update(**state)
|
||||||
|
if "state" in user_state and "presence" not in user_state:
|
||||||
|
user_state["presence"] = user_state["state"]
|
||||||
|
|
||||||
yield self.federation.send_edu(
|
yield self.federation.send_edu(
|
||||||
destination=destination,
|
destination=destination,
|
||||||
@@ -618,14 +650,29 @@ class PresenceHandler(BaseHandler):
|
|||||||
room_ids = yield rm_handler.get_rooms_for_user(user)
|
room_ids = yield rm_handler.get_rooms_for_user(user)
|
||||||
|
|
||||||
if not observers and not room_ids:
|
if not observers and not room_ids:
|
||||||
break
|
continue
|
||||||
|
|
||||||
state = dict(push)
|
state = dict(push)
|
||||||
del state["user_id"]
|
del state["user_id"]
|
||||||
|
|
||||||
if "mtime_age" in state:
|
if "presence" in state:
|
||||||
state["mtime"] = int(
|
# all is OK
|
||||||
self.clock.time_msec() - state.pop("mtime_age")
|
pass
|
||||||
|
elif "state" in state:
|
||||||
|
# Legacy handling
|
||||||
|
state["presence"] = state["state"]
|
||||||
|
else:
|
||||||
|
logger.warning("Received a presence 'push' EDU from %s without"
|
||||||
|
+ " either a 'presence' or 'state' key", origin
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "state" in state:
|
||||||
|
del state["state"]
|
||||||
|
|
||||||
|
if "last_active_ago" in state:
|
||||||
|
state["last_active"] = int(
|
||||||
|
self.clock.time_msec() - state.pop("last_active_ago")
|
||||||
)
|
)
|
||||||
|
|
||||||
statuscache = self._get_or_make_usercache(user)
|
statuscache = self._get_or_make_usercache(user)
|
||||||
@@ -640,7 +687,7 @@ class PresenceHandler(BaseHandler):
|
|||||||
statuscache=statuscache,
|
statuscache=statuscache,
|
||||||
)
|
)
|
||||||
|
|
||||||
if state["state"] == PresenceState.OFFLINE:
|
if state["presence"] == PresenceState.OFFLINE:
|
||||||
del self._user_cachemap[user]
|
del self._user_cachemap[user]
|
||||||
|
|
||||||
for poll in content.get("poll", []):
|
for poll in content.get("poll", []):
|
||||||
@@ -673,10 +720,9 @@ class PresenceHandler(BaseHandler):
|
|||||||
yield defer.DeferredList(deferreds)
|
yield defer.DeferredList(deferreds)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def push_update_to_local_and_remote(self, observed_user,
|
def push_update_to_local_and_remote(self, observed_user, statuscache,
|
||||||
users_to_push=[], room_ids=[],
|
users_to_push=[], room_ids=[],
|
||||||
remote_domains=[],
|
remote_domains=[]):
|
||||||
statuscache=None):
|
|
||||||
|
|
||||||
localusers, remoteusers = partitionbool(
|
localusers, remoteusers = partitionbool(
|
||||||
users_to_push,
|
users_to_push,
|
||||||
@@ -722,6 +768,78 @@ class PresenceHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PresenceEventSource(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.hs = hs
|
||||||
|
self.clock = hs.get_clock()
|
||||||
|
|
||||||
|
def get_new_events_for_user(self, user, from_key, limit):
|
||||||
|
from_key = int(from_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]
|
||||||
|
|
||||||
|
return ((data, latest_serial))
|
||||||
|
else:
|
||||||
|
return (([], presence._user_cachemap_latest_serial))
|
||||||
|
|
||||||
|
def get_current_key(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 UserPresenceCache(object):
|
class UserPresenceCache(object):
|
||||||
"""Store an observed user's state and status message.
|
"""Store an observed user's state and status message.
|
||||||
|
|
||||||
@@ -733,6 +851,7 @@ class UserPresenceCache(object):
|
|||||||
|
|
||||||
def update(self, state, serial):
|
def update(self, state, serial):
|
||||||
assert("mtime_age" not in state)
|
assert("mtime_age" not in state)
|
||||||
|
assert("state" not in state)
|
||||||
|
|
||||||
self.state.update(state)
|
self.state.update(state)
|
||||||
# Delete keys that are now 'None'
|
# Delete keys that are now 'None'
|
||||||
@@ -749,15 +868,21 @@ class UserPresenceCache(object):
|
|||||||
|
|
||||||
def get_state(self):
|
def get_state(self):
|
||||||
# clone it so caller can't break our cache
|
# clone it so caller can't break our cache
|
||||||
return dict(self.state)
|
state = dict(self.state)
|
||||||
|
|
||||||
|
# Legacy handling
|
||||||
|
if "presence" in state:
|
||||||
|
state["state"] = state["presence"]
|
||||||
|
|
||||||
|
return state
|
||||||
|
|
||||||
def make_event(self, user, clock):
|
def make_event(self, user, clock):
|
||||||
content = self.get_state()
|
content = self.get_state()
|
||||||
content["user_id"] = user.to_string()
|
content["user_id"] = user.to_string()
|
||||||
|
|
||||||
if "mtime" in content:
|
if "last_active" in content:
|
||||||
content["mtime_age"] = int(
|
content["last_active_ago"] = int(
|
||||||
clock.time_msec() - content.pop("mtime")
|
clock.time_msec() - content.pop("last_active")
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"type": "m.presence", "content": content}
|
return {"type": "m.presence", "content": content}
|
||||||
|
|||||||
@@ -17,10 +17,12 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.types import UserID, RoomAlias, RoomID
|
from synapse.types import UserID, RoomAlias, RoomID
|
||||||
from synapse.api.constants import Membership
|
from synapse.api.constants import Membership, JoinRules
|
||||||
from synapse.api.errors import StoreError, SynapseError
|
from synapse.api.errors import StoreError, SynapseError
|
||||||
from synapse.api.events.room import (
|
from synapse.api.events.room import (
|
||||||
RoomMemberEvent, RoomConfigEvent
|
RoomMemberEvent, RoomCreateEvent, RoomPowerLevelsEvent,
|
||||||
|
RoomJoinRulesEvent, RoomAddStateLevelEvent, RoomTopicEvent,
|
||||||
|
RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent, RoomNameEvent,
|
||||||
)
|
)
|
||||||
from synapse.util import stringutils
|
from synapse.util import stringutils
|
||||||
from ._base import BaseRoomHandler
|
from ._base import BaseRoomHandler
|
||||||
@@ -62,6 +64,8 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
else:
|
else:
|
||||||
room_alias = None
|
room_alias = None
|
||||||
|
|
||||||
|
is_public = config.get("visibility", None) == "public"
|
||||||
|
|
||||||
if room_id:
|
if room_id:
|
||||||
# Ensure room_id is the correct type
|
# Ensure room_id is the correct type
|
||||||
room_id_obj = RoomID.from_string(room_id, self.hs)
|
room_id_obj = RoomID.from_string(room_id, self.hs)
|
||||||
@@ -71,7 +75,7 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
yield self.store.store_room(
|
yield self.store.store_room(
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
room_creator_user_id=user_id,
|
room_creator_user_id=user_id,
|
||||||
is_public=config["visibility"] == "public"
|
is_public=is_public
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# autogen room IDs and try to create it. We may clash, so just
|
# autogen room IDs and try to create it. We may clash, so just
|
||||||
@@ -85,7 +89,7 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
yield self.store.store_room(
|
yield self.store.store_room(
|
||||||
room_id=gen_room_id.to_string(),
|
room_id=gen_room_id.to_string(),
|
||||||
room_creator_user_id=user_id,
|
room_creator_user_id=user_id,
|
||||||
is_public=config["visibility"] == "public"
|
is_public=is_public
|
||||||
)
|
)
|
||||||
room_id = gen_room_id.to_string()
|
room_id = gen_room_id.to_string()
|
||||||
break
|
break
|
||||||
@@ -94,18 +98,9 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
if not room_id:
|
if not room_id:
|
||||||
raise StoreError(500, "Couldn't generate a room ID.")
|
raise StoreError(500, "Couldn't generate a room ID.")
|
||||||
|
|
||||||
config_event = self.event_factory.create_event(
|
user = self.hs.parse_userid(user_id)
|
||||||
etype=RoomConfigEvent.TYPE,
|
creation_events = self._create_events_for_new_room(
|
||||||
room_id=room_id,
|
user, room_id, is_public=is_public
|
||||||
user_id=user_id,
|
|
||||||
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:
|
if room_alias:
|
||||||
@@ -115,11 +110,46 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
servers=[self.hs.hostname],
|
servers=[self.hs.hostname],
|
||||||
)
|
)
|
||||||
|
|
||||||
yield self.state_handler.handle_new_event(config_event, snapshot)
|
|
||||||
# store_id = persist...
|
|
||||||
|
|
||||||
federation_handler = self.hs.get_handlers().federation_handler
|
federation_handler = self.hs.get_handlers().federation_handler
|
||||||
yield federation_handler.handle_new_event(config_event, snapshot)
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def handle_event(event):
|
||||||
|
snapshot = yield self.store.snapshot_room(
|
||||||
|
room_id=room_id,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.debug("Event: %s", event)
|
||||||
|
|
||||||
|
yield self.state_handler.handle_new_event(event, snapshot)
|
||||||
|
yield self._on_new_room_event(event, snapshot, extra_users=[user])
|
||||||
|
|
||||||
|
for event in creation_events:
|
||||||
|
yield handle_event(event)
|
||||||
|
|
||||||
|
if "name" in config:
|
||||||
|
name = config["name"]
|
||||||
|
name_event = self.event_factory.create_event(
|
||||||
|
etype=RoomNameEvent.TYPE,
|
||||||
|
room_id=room_id,
|
||||||
|
user_id=user_id,
|
||||||
|
required_power_level=5,
|
||||||
|
content={"name": name},
|
||||||
|
)
|
||||||
|
|
||||||
|
yield handle_event(name_event)
|
||||||
|
|
||||||
|
if "topic" in config:
|
||||||
|
topic = config["topic"]
|
||||||
|
topic_event = self.event_factory.create_event(
|
||||||
|
etype=RoomTopicEvent.TYPE,
|
||||||
|
room_id=room_id,
|
||||||
|
user_id=user_id,
|
||||||
|
required_power_level=5,
|
||||||
|
content={"topic": topic},
|
||||||
|
)
|
||||||
|
|
||||||
|
yield handle_event(topic_event)
|
||||||
|
|
||||||
content = {"membership": Membership.JOIN}
|
content = {"membership": Membership.JOIN}
|
||||||
join_event = self.event_factory.create_event(
|
join_event = self.event_factory.create_event(
|
||||||
@@ -142,6 +172,62 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
|
|
||||||
defer.returnValue(result)
|
defer.returnValue(result)
|
||||||
|
|
||||||
|
def _create_events_for_new_room(self, creator, room_id, is_public=False):
|
||||||
|
event_keys = {
|
||||||
|
"room_id": room_id,
|
||||||
|
"user_id": creator.to_string(),
|
||||||
|
"required_power_level": 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
def create(etype, **content):
|
||||||
|
return self.event_factory.create_event(
|
||||||
|
etype=etype,
|
||||||
|
content=content,
|
||||||
|
**event_keys
|
||||||
|
)
|
||||||
|
|
||||||
|
creation_event = create(
|
||||||
|
etype=RoomCreateEvent.TYPE,
|
||||||
|
creator=creator.to_string(),
|
||||||
|
)
|
||||||
|
|
||||||
|
power_levels_event = self.event_factory.create_event(
|
||||||
|
etype=RoomPowerLevelsEvent.TYPE,
|
||||||
|
content={creator.to_string(): 10, "default": 0},
|
||||||
|
**event_keys
|
||||||
|
)
|
||||||
|
|
||||||
|
join_rule = JoinRules.PUBLIC if is_public else JoinRules.INVITE
|
||||||
|
join_rules_event = create(
|
||||||
|
etype=RoomJoinRulesEvent.TYPE,
|
||||||
|
join_rule=join_rule,
|
||||||
|
)
|
||||||
|
|
||||||
|
add_state_event = create(
|
||||||
|
etype=RoomAddStateLevelEvent.TYPE,
|
||||||
|
level=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
send_event = create(
|
||||||
|
etype=RoomSendEventLevelEvent.TYPE,
|
||||||
|
level=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
ops = create(
|
||||||
|
etype=RoomOpsPowerLevelsEvent.TYPE,
|
||||||
|
ban_level=5,
|
||||||
|
kick_level=5,
|
||||||
|
)
|
||||||
|
|
||||||
|
return [
|
||||||
|
creation_event,
|
||||||
|
power_levels_event,
|
||||||
|
join_rules_event,
|
||||||
|
add_state_event,
|
||||||
|
send_event,
|
||||||
|
ops,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class RoomMemberHandler(BaseRoomHandler):
|
class RoomMemberHandler(BaseRoomHandler):
|
||||||
# TODO(paul): This handler currently contains a messy conflation of
|
# TODO(paul): This handler currently contains a messy conflation of
|
||||||
@@ -285,6 +371,16 @@ class RoomMemberHandler(BaseRoomHandler):
|
|||||||
if do_auth:
|
if do_auth:
|
||||||
yield self.auth.check(event, snapshot, raises=True)
|
yield self.auth.check(event, snapshot, raises=True)
|
||||||
|
|
||||||
|
# If we're banning someone, set a req power level
|
||||||
|
if event.membership == Membership.BAN:
|
||||||
|
if not hasattr(event, "required_power_level") or event.required_power_level is None:
|
||||||
|
# Add some default required_power_level
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
event.required_power_level = user_level
|
||||||
|
|
||||||
if prev_state and prev_state.membership == event.membership:
|
if prev_state and prev_state.membership == event.membership:
|
||||||
# double same action, treat this event as a NOOP.
|
# double same action, treat this event as a NOOP.
|
||||||
defer.returnValue({})
|
defer.returnValue({})
|
||||||
@@ -445,8 +541,7 @@ class RoomMemberHandler(BaseRoomHandler):
|
|||||||
host = target_user.domain
|
host = target_user.domain
|
||||||
destinations.append(host)
|
destinations.append(host)
|
||||||
|
|
||||||
# If we are joining a remote HS, include that.
|
# Always include target domain
|
||||||
if membership == Membership.JOIN:
|
|
||||||
host = target_user.domain
|
host = target_user.domain
|
||||||
destinations.append(host)
|
destinations.append(host)
|
||||||
|
|
||||||
@@ -462,3 +557,49 @@ class RoomListHandler(BaseRoomHandler):
|
|||||||
chunk = yield self.store.get_rooms(is_public=True)
|
chunk = yield self.store.get_rooms(is_public=True)
|
||||||
# FIXME (erikj): START is no longer a valid value
|
# FIXME (erikj): START is no longer a valid value
|
||||||
defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
|
defer.returnValue({"start": "START", "end": "END", "chunk": chunk})
|
||||||
|
|
||||||
|
|
||||||
|
class RoomEventSource(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_new_events_for_user(self, user, from_key, limit):
|
||||||
|
# We just ignore the key for now.
|
||||||
|
|
||||||
|
to_key = yield self.get_current_key()
|
||||||
|
|
||||||
|
events, end_key = yield self.store.get_room_events_stream(
|
||||||
|
user_id=user.to_string(),
|
||||||
|
from_key=from_key,
|
||||||
|
to_key=to_key,
|
||||||
|
room_id=None,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((events, end_key))
|
||||||
|
|
||||||
|
def get_current_key(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.room_key if to_token else None
|
||||||
|
|
||||||
|
events, next_key = yield self.store.paginate_room_events(
|
||||||
|
room_id=key,
|
||||||
|
from_key=from_token.room_key,
|
||||||
|
to_key=to_key,
|
||||||
|
direction=direction,
|
||||||
|
limit=limit,
|
||||||
|
with_feedback=True
|
||||||
|
)
|
||||||
|
|
||||||
|
next_token = from_token.copy_and_replace("room_key", next_key)
|
||||||
|
|
||||||
|
defer.returnValue((events, next_token))
|
||||||
|
|||||||
@@ -145,3 +145,17 @@ class TypingNotificationHandler(BaseHandler):
|
|||||||
typing):
|
typing):
|
||||||
# TODO(paul) steal this from presence.py
|
# TODO(paul) steal this from presence.py
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TypingNotificationEventSource(object):
|
||||||
|
def __init__(self, hs):
|
||||||
|
self.hs = hs
|
||||||
|
|
||||||
|
def get_new_events_for_user(self, user, from_key, limit):
|
||||||
|
return ([], from_key)
|
||||||
|
|
||||||
|
def get_current_key(self):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_pagination_rows(self, user, pagination_config, key):
|
||||||
|
return ([], pagination_config.from_token)
|
||||||
|
|||||||
@@ -113,8 +113,9 @@ class TwistedHttpClient(HttpClient):
|
|||||||
requests.
|
requests.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, hs):
|
||||||
self.agent = MatrixHttpAgent(reactor)
|
self.agent = MatrixHttpAgent(reactor)
|
||||||
|
self.hs = hs
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def put_json(self, destination, path, data):
|
def put_json(self, destination, path, data):
|
||||||
@@ -177,7 +178,10 @@ class TwistedHttpClient(HttpClient):
|
|||||||
retries_left = 5
|
retries_left = 5
|
||||||
|
|
||||||
# TODO: setup and pass in an ssl_context to enable TLS
|
# TODO: setup and pass in an ssl_context to enable TLS
|
||||||
endpoint = matrix_endpoint(reactor, destination, timeout=10)
|
endpoint = matrix_endpoint(
|
||||||
|
reactor, destination, timeout=10,
|
||||||
|
ssl_context_factory=self.hs.tls_context_factory
|
||||||
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -50,11 +50,11 @@ def matrix_endpoint(reactor, destination, ssl_context_factory=None,
|
|||||||
|
|
||||||
if ssl_context_factory is None:
|
if ssl_context_factory is None:
|
||||||
transport_endpoint = TCP4ClientEndpoint
|
transport_endpoint = TCP4ClientEndpoint
|
||||||
default_port = 8080
|
default_port = 8008
|
||||||
else:
|
else:
|
||||||
transport_endpoint = SSL4ClientEndpoint
|
transport_endpoint = SSL4ClientEndpoint
|
||||||
endpoint_kw_args.update(ssl_context_factory=ssl_context_factory)
|
endpoint_kw_args.update(sslContextFactory=ssl_context_factory)
|
||||||
default_port = 443
|
default_port = 8448
|
||||||
|
|
||||||
if port is None:
|
if port is None:
|
||||||
return SRVClientEndpoint(
|
return SRVClientEndpoint(
|
||||||
|
|||||||
@@ -325,7 +325,11 @@ class ContentRepoResource(resource.Resource):
|
|||||||
|
|
||||||
# FIXME (erikj): These should use constants.
|
# FIXME (erikj): These should use constants.
|
||||||
file_name = os.path.basename(fname)
|
file_name = os.path.basename(fname)
|
||||||
url = "http://%s/matrix/content/%s" % (
|
# FIXME: we can't assume what the public mounted path of the repo is
|
||||||
|
# ...plus self-signed SSL won't work to remote clients anyway
|
||||||
|
# ...and we can't assume that it's SSL anyway, as we might want to
|
||||||
|
# server it via the non-SSL listener...
|
||||||
|
url = "https://%s/_matrix/content/%s" % (
|
||||||
self.hs.domain_with_port, file_name
|
self.hs.domain_with_port, file_name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class Notifier(object):
|
|||||||
"""
|
"""
|
||||||
room_id = event.room_id
|
room_id = event.room_id
|
||||||
|
|
||||||
source = self.event_sources.sources["room"]
|
room_source = self.event_sources.sources["room"]
|
||||||
|
|
||||||
listeners = self.rooms_to_listeners.get(room_id, set()).copy()
|
listeners = self.rooms_to_listeners.get(room_id, set()).copy()
|
||||||
|
|
||||||
@@ -109,13 +109,17 @@ class Notifier(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def notify(listener):
|
def notify(listener):
|
||||||
events, end_token = yield source.get_new_events_for_user(
|
events, end_key = yield room_source.get_new_events_for_user(
|
||||||
listener.user,
|
listener.user,
|
||||||
listener.from_token,
|
listener.from_token.room_key,
|
||||||
listener.limit,
|
listener.limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
if events:
|
if events:
|
||||||
|
end_token = listener.from_token.copy_and_replace(
|
||||||
|
"room_key", end_key
|
||||||
|
)
|
||||||
|
|
||||||
listener.notify(
|
listener.notify(
|
||||||
self, events, listener.from_token, end_token
|
self, events, listener.from_token, end_token
|
||||||
)
|
)
|
||||||
@@ -135,7 +139,7 @@ class Notifier(object):
|
|||||||
|
|
||||||
Will wake up all listeners for the given users and rooms.
|
Will wake up all listeners for the given users and rooms.
|
||||||
"""
|
"""
|
||||||
source = self.event_sources.sources["presence"]
|
presence_source = self.event_sources.sources["presence"]
|
||||||
|
|
||||||
listeners = set()
|
listeners = set()
|
||||||
|
|
||||||
@@ -147,13 +151,17 @@ class Notifier(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def notify(listener):
|
def notify(listener):
|
||||||
events, end_token = yield source.get_new_events_for_user(
|
events, end_key = yield presence_source.get_new_events_for_user(
|
||||||
listener.user,
|
listener.user,
|
||||||
listener.from_token,
|
listener.from_token.presence_key,
|
||||||
listener.limit,
|
listener.limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
if events:
|
if events:
|
||||||
|
end_token = listener.from_token.copy_and_replace(
|
||||||
|
"presence_key", end_key
|
||||||
|
)
|
||||||
|
|
||||||
listener.notify(
|
listener.notify(
|
||||||
self, events, listener.from_token, end_token
|
self, events, listener.from_token, end_token
|
||||||
)
|
)
|
||||||
@@ -233,16 +241,18 @@ class Notifier(object):
|
|||||||
limit = listener.limit
|
limit = listener.limit
|
||||||
|
|
||||||
# TODO (erikj): DeferredList?
|
# TODO (erikj): DeferredList?
|
||||||
for source in self.event_sources.sources.values():
|
for name, source in self.event_sources.sources.items():
|
||||||
stuff, new_token = yield source.get_new_events_for_user(
|
keyname = "%s_key" % name
|
||||||
|
|
||||||
|
stuff, new_key = yield source.get_new_events_for_user(
|
||||||
listener.user,
|
listener.user,
|
||||||
from_token,
|
getattr(from_token, keyname),
|
||||||
limit,
|
limit,
|
||||||
)
|
)
|
||||||
|
|
||||||
events.extend(stuff)
|
events.extend(stuff)
|
||||||
|
|
||||||
from_token = new_token
|
from_token = from_token.copy_and_replace(keyname, new_key)
|
||||||
|
|
||||||
end_token = from_token
|
end_token = from_token
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ class PresenceStatusRestServlet(RestServlet):
|
|||||||
try:
|
try:
|
||||||
content = json.loads(request.content.read())
|
content = json.loads(request.content.read())
|
||||||
|
|
||||||
state["state"] = content.pop("state")
|
# Legacy handling
|
||||||
|
if "state" in content:
|
||||||
|
state["presence"] = content.pop("state")
|
||||||
|
else:
|
||||||
|
state["presence"] = content.pop("presence")
|
||||||
|
|
||||||
if "status_msg" in content:
|
if "status_msg" in content:
|
||||||
state["status_msg"] = content.pop("status_msg")
|
state["status_msg"] = content.pop("status_msg")
|
||||||
|
|||||||
@@ -87,6 +87,27 @@ class ProfileAvatarURLRestServlet(RestServlet):
|
|||||||
return (200, {})
|
return (200, {})
|
||||||
|
|
||||||
|
|
||||||
|
class ProfileRestServlet(RestServlet):
|
||||||
|
PATTERN = client_path_pattern("/profile/(?P<user_id>[^/]*)")
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_GET(self, request, user_id):
|
||||||
|
user = self.hs.parse_userid(user_id)
|
||||||
|
|
||||||
|
displayname = yield self.handlers.profile_handler.get_displayname(
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
avatar_url = yield self.handlers.profile_handler.get_avatar_url(
|
||||||
|
user,
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue((200, {
|
||||||
|
"displayname": displayname,
|
||||||
|
"avatar_url": avatar_url
|
||||||
|
}))
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(hs, http_server):
|
def register_servlets(hs, http_server):
|
||||||
ProfileDisplaynameRestServlet(hs).register(http_server)
|
ProfileDisplaynameRestServlet(hs).register(http_server)
|
||||||
ProfileAvatarURLRestServlet(hs).register(http_server)
|
ProfileAvatarURLRestServlet(hs).register(http_server)
|
||||||
|
ProfileRestServlet(hs).register(http_server)
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ from synapse.api.events.room import (
|
|||||||
RoomMemberEvent, RoomTopicEvent, FeedbackEvent,
|
RoomMemberEvent, RoomTopicEvent, FeedbackEvent,
|
||||||
# RoomConfigEvent,
|
# RoomConfigEvent,
|
||||||
RoomNameEvent,
|
RoomNameEvent,
|
||||||
|
RoomJoinRulesEvent,
|
||||||
|
RoomPowerLevelsEvent,
|
||||||
|
RoomAddStateLevelEvent,
|
||||||
|
RoomSendEventLevelEvent,
|
||||||
|
RoomOpsPowerLevelsEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
@@ -33,6 +38,7 @@ from .roommember import RoomMemberStore
|
|||||||
from .stream import StreamStore
|
from .stream import StreamStore
|
||||||
from .pdu import StatePduStore, PduStore
|
from .pdu import StatePduStore, PduStore
|
||||||
from .transactions import TransactionStore
|
from .transactions import TransactionStore
|
||||||
|
from .keys import KeyStore
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
@@ -45,7 +51,7 @@ logger = logging.getLogger(__name__)
|
|||||||
class DataStore(RoomMemberStore, RoomStore,
|
class DataStore(RoomMemberStore, RoomStore,
|
||||||
RegistrationStore, StreamStore, ProfileStore, FeedbackStore,
|
RegistrationStore, StreamStore, ProfileStore, FeedbackStore,
|
||||||
PresenceStore, PduStore, StatePduStore, TransactionStore,
|
PresenceStore, PduStore, StatePduStore, TransactionStore,
|
||||||
DirectoryStore):
|
DirectoryStore, KeyStore):
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(DataStore, self).__init__(hs)
|
super(DataStore, self).__init__(hs)
|
||||||
@@ -123,12 +129,20 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||||||
self._store_room_member_txn(txn, event)
|
self._store_room_member_txn(txn, event)
|
||||||
elif event.type == FeedbackEvent.TYPE:
|
elif event.type == FeedbackEvent.TYPE:
|
||||||
self._store_feedback_txn(txn, event)
|
self._store_feedback_txn(txn, event)
|
||||||
# elif event.type == RoomConfigEvent.TYPE:
|
|
||||||
# self._store_room_config_txn(txn, event)
|
|
||||||
elif event.type == RoomNameEvent.TYPE:
|
elif event.type == RoomNameEvent.TYPE:
|
||||||
self._store_room_name_txn(txn, event)
|
self._store_room_name_txn(txn, event)
|
||||||
elif event.type == RoomTopicEvent.TYPE:
|
elif event.type == RoomTopicEvent.TYPE:
|
||||||
self._store_room_topic_txn(txn, event)
|
self._store_room_topic_txn(txn, event)
|
||||||
|
elif event.type == RoomJoinRulesEvent.TYPE:
|
||||||
|
self._store_join_rule(txn, event)
|
||||||
|
elif event.type == RoomPowerLevelsEvent.TYPE:
|
||||||
|
self._store_power_levels(txn, event)
|
||||||
|
elif event.type == RoomAddStateLevelEvent.TYPE:
|
||||||
|
self._store_add_state_level(txn, event)
|
||||||
|
elif event.type == RoomSendEventLevelEvent.TYPE:
|
||||||
|
self._store_send_event_level(txn, event)
|
||||||
|
elif event.type == RoomOpsPowerLevelsEvent.TYPE:
|
||||||
|
self._store_ops_level(txn, event)
|
||||||
|
|
||||||
vals = {
|
vals = {
|
||||||
"topological_ordering": event.depth,
|
"topological_ordering": event.depth,
|
||||||
@@ -222,7 +236,6 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||||||
|
|
||||||
defer.returnValue(self.min_token)
|
defer.returnValue(self.min_token)
|
||||||
|
|
||||||
|
|
||||||
def snapshot_room(self, room_id, user_id, state_type=None, state_key=None):
|
def snapshot_room(self, room_id, user_id, state_type=None, state_key=None):
|
||||||
"""Snapshot the room for an update by a user
|
"""Snapshot the room for an update by a user
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
103
synapse/storage/keys.py
Normal file
103
synapse/storage/keys.py
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
# -*- 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 _base import SQLBaseStore
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
import OpenSSL
|
||||||
|
import nacl.signing
|
||||||
|
|
||||||
|
class KeyStore(SQLBaseStore):
|
||||||
|
"""Persistence for signature verification keys and tls X.509 certificates
|
||||||
|
"""
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_server_certificate(self, server_name):
|
||||||
|
"""Retrieve the TLS X.509 certificate for the given server
|
||||||
|
Args:
|
||||||
|
server_name (bytes): The name of the server.
|
||||||
|
Returns:
|
||||||
|
(OpenSSL.crypto.X509): The tls certificate.
|
||||||
|
"""
|
||||||
|
tls_certificate_bytes, = yield self._simple_select_one(
|
||||||
|
table="server_tls_certificates",
|
||||||
|
keyvalues={"server_name": server_name},
|
||||||
|
retcols=("tls_certificate",),
|
||||||
|
)
|
||||||
|
tls_certificate = OpenSSL.crypto.load_certificate(
|
||||||
|
OpenSSL.crypto.FILETYPE_ASN1, tls_certificate_bytes,
|
||||||
|
)
|
||||||
|
defer.returnValue(tls_certificate)
|
||||||
|
|
||||||
|
def store_server_certificate(self, server_name, key_server, ts_now_ms,
|
||||||
|
tls_certificate):
|
||||||
|
"""Stores the TLS X.509 certificate for the given server
|
||||||
|
Args:
|
||||||
|
server_name (bytes): The name of the server.
|
||||||
|
key_server (bytes): Where the certificate was looked up
|
||||||
|
ts_now_ms (int): The time now in milliseconds
|
||||||
|
tls_certificate (OpenSSL.crypto.X509): The X.509 certificate.
|
||||||
|
"""
|
||||||
|
tls_certificate_bytes = OpenSSL.crypto.dump_certificate(
|
||||||
|
OpenSSL.crypto.FILETYPE_ASN1, tls_certificate
|
||||||
|
)
|
||||||
|
return self._simple_insert(
|
||||||
|
table="server_tls_certificates",
|
||||||
|
keyvalues={
|
||||||
|
"server_name": server_name,
|
||||||
|
"key_server": key_server,
|
||||||
|
"ts_added_ms": ts_now_ms,
|
||||||
|
"tls_certificate": tls_certificate_bytes,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_server_verification_key(self, server_name):
|
||||||
|
"""Retrieve the NACL verification key for a given server
|
||||||
|
Args:
|
||||||
|
server_name (bytes): The name of the server.
|
||||||
|
Returns:
|
||||||
|
(nacl.signing.VerifyKey): The verification key.
|
||||||
|
"""
|
||||||
|
verification_key_bytes, = yield self._simple_select_one(
|
||||||
|
table="server_signature_keys",
|
||||||
|
key_values={"server_name": server_name},
|
||||||
|
retcols=("tls_certificate",),
|
||||||
|
)
|
||||||
|
verification_key = nacl.signing.VerifyKey(verification_key_bytes)
|
||||||
|
defer.returnValue(verification_key)
|
||||||
|
|
||||||
|
def store_server_verification_key(self, server_name, key_version,
|
||||||
|
key_server, ts_now_ms, verification_key):
|
||||||
|
"""Stores a NACL verification key for the given server.
|
||||||
|
Args:
|
||||||
|
server_name (bytes): The name of the server.
|
||||||
|
key_version (bytes): The version of the key for the server.
|
||||||
|
key_server (bytes): Where the verification key was looked up
|
||||||
|
ts_now_ms (int): The time now in milliseconds
|
||||||
|
verification_key (nacl.signing.VerifyKey): The NACL verify key.
|
||||||
|
"""
|
||||||
|
verification_key_bytes = verification_key.encode()
|
||||||
|
return self._simple_insert(
|
||||||
|
table="server_signature_keys",
|
||||||
|
key_values={
|
||||||
|
"server_name": server_name,
|
||||||
|
"key_version": key_version,
|
||||||
|
"key_server": key_server,
|
||||||
|
"ts_added_ms": ts_now_ms,
|
||||||
|
"verification_key": verification_key_bytes,
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -27,6 +27,9 @@ import logging
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level"))
|
||||||
|
|
||||||
|
|
||||||
class RoomStore(SQLBaseStore):
|
class RoomStore(SQLBaseStore):
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -129,6 +132,98 @@ class RoomStore(SQLBaseStore):
|
|||||||
|
|
||||||
defer.returnValue(ret)
|
defer.returnValue(ret)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_room_join_rule(self, room_id):
|
||||||
|
sql = (
|
||||||
|
"SELECT join_rule FROM room_join_rules as r "
|
||||||
|
"INNER JOIN current_state_events as c "
|
||||||
|
"ON r.event_id = c.event_id "
|
||||||
|
"WHERE c.room_id = ? "
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = yield self._execute(None, sql, room_id)
|
||||||
|
|
||||||
|
if len(rows) == 1:
|
||||||
|
defer.returnValue(rows[0][0])
|
||||||
|
else:
|
||||||
|
defer.returnValue(None)
|
||||||
|
|
||||||
|
def get_power_level(self, room_id, user_id):
|
||||||
|
return self._db_pool.runInteraction(
|
||||||
|
self._get_power_level,
|
||||||
|
room_id, user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_power_level(self, txn, room_id, user_id):
|
||||||
|
sql = (
|
||||||
|
"SELECT level FROM room_power_levels as r "
|
||||||
|
"INNER JOIN current_state_events as c "
|
||||||
|
"ON r.event_id = c.event_id "
|
||||||
|
"WHERE c.room_id = ? AND r.user_id = ? "
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = txn.execute(sql, (room_id, user_id,)).fetchall()
|
||||||
|
|
||||||
|
if len(rows) == 1:
|
||||||
|
return rows[0][0]
|
||||||
|
|
||||||
|
sql = (
|
||||||
|
"SELECT level FROM room_default_levels as r "
|
||||||
|
"INNER JOIN current_state_events as c "
|
||||||
|
"ON r.event_id = c.event_id "
|
||||||
|
"WHERE c.room_id = ? "
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = txn.execute(sql, (room_id,)).fetchall()
|
||||||
|
|
||||||
|
if len(rows) == 1:
|
||||||
|
return rows[0][0]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_ops_levels(self, room_id):
|
||||||
|
return self._db_pool.runInteraction(
|
||||||
|
self._get_ops_levels,
|
||||||
|
room_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _get_ops_levels(self, txn, room_id):
|
||||||
|
sql = (
|
||||||
|
"SELECT ban_level, kick_level FROM room_ops_levels as r "
|
||||||
|
"INNER JOIN current_state_events as c "
|
||||||
|
"ON r.event_id = c.event_id "
|
||||||
|
"WHERE c.room_id = ? "
|
||||||
|
)
|
||||||
|
|
||||||
|
rows = txn.execute(sql, (room_id,)).fetchall()
|
||||||
|
|
||||||
|
if len(rows) == 1:
|
||||||
|
return OpsLevel(rows[0][0], rows[0][1])
|
||||||
|
else:
|
||||||
|
return OpsLevel(None, None)
|
||||||
|
|
||||||
|
def get_add_state_level(self, room_id):
|
||||||
|
return self._get_level_from_table("room_add_state_levels", room_id)
|
||||||
|
|
||||||
|
def get_send_event_level(self, room_id):
|
||||||
|
return self._get_level_from_table("room_send_event_levels", room_id)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _get_level_from_table(self, table, room_id):
|
||||||
|
sql = (
|
||||||
|
"SELECT level FROM %(table)s as r "
|
||||||
|
"INNER JOIN current_state_events as c "
|
||||||
|
"ON r.event_id = c.event_id "
|
||||||
|
"WHERE c.room_id = ? "
|
||||||
|
) % {"table": table}
|
||||||
|
|
||||||
|
rows = yield self._execute(None, sql, room_id)
|
||||||
|
|
||||||
|
if len(rows) == 1:
|
||||||
|
defer.returnValue(rows[0][0])
|
||||||
|
else:
|
||||||
|
defer.returnValue(None)
|
||||||
|
|
||||||
def _store_room_topic_txn(self, txn, event):
|
def _store_room_topic_txn(self, txn, event):
|
||||||
self._simple_insert_txn(
|
self._simple_insert_txn(
|
||||||
txn,
|
txn,
|
||||||
@@ -151,6 +246,92 @@ class RoomStore(SQLBaseStore):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _store_join_rule(self, txn, event):
|
||||||
|
self._simple_insert_txn(
|
||||||
|
txn,
|
||||||
|
"room_join_rules",
|
||||||
|
{
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"join_rule": event.content["join_rule"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_power_levels(self, txn, event):
|
||||||
|
for user_id, level in event.content.items():
|
||||||
|
if user_id == "default":
|
||||||
|
self._simple_insert_txn(
|
||||||
|
txn,
|
||||||
|
"room_default_levels",
|
||||||
|
{
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"level": level,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self._simple_insert_txn(
|
||||||
|
txn,
|
||||||
|
"room_power_levels",
|
||||||
|
{
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"level": level
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_default_level(self, txn, event):
|
||||||
|
self._simple_insert_txn(
|
||||||
|
txn,
|
||||||
|
"room_default_levels",
|
||||||
|
{
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"level": event.content["default_level"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_add_state_level(self, txn, event):
|
||||||
|
self._simple_insert_txn(
|
||||||
|
txn,
|
||||||
|
"room_add_state_levels",
|
||||||
|
{
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"level": event.content["level"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_send_event_level(self, txn, event):
|
||||||
|
self._simple_insert_txn(
|
||||||
|
txn,
|
||||||
|
"room_send_event_levels",
|
||||||
|
{
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
"level": event.content["level"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
def _store_ops_level(self, txn, event):
|
||||||
|
content = {
|
||||||
|
"event_id": event.event_id,
|
||||||
|
"room_id": event.room_id,
|
||||||
|
}
|
||||||
|
|
||||||
|
if "kick_level" in event.content:
|
||||||
|
content["kick_level"] = event.content["kick_level"]
|
||||||
|
|
||||||
|
if "ban_level" in event.content:
|
||||||
|
content["ban_level"] = event.content["ban_level"]
|
||||||
|
|
||||||
|
self._simple_insert_txn(
|
||||||
|
txn,
|
||||||
|
"room_ops_levels",
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class RoomsTable(Table):
|
class RoomsTable(Table):
|
||||||
table_name = "rooms"
|
table_name = "rooms"
|
||||||
|
|||||||
@@ -96,8 +96,71 @@ CREATE TABLE IF NOT EXISTS rooms(
|
|||||||
creator TEXT
|
creator TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS room_join_rules(
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
join_rule TEXT NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS room_join_rules_event_id ON room_join_rules(event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS room_join_rules_room_id ON room_join_rules(room_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS room_power_levels(
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
level INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS room_power_levels_event_id ON room_power_levels(event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS room_power_levels_room_id ON room_power_levels(room_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS room_power_levels_room_user ON room_power_levels(room_id, user_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS room_default_levels(
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
level INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS room_default_levels_event_id ON room_default_levels(event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS room_default_levels_room_id ON room_default_levels(room_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS room_add_state_levels(
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
level INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS room_add_state_levels_event_id ON room_add_state_levels(event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS room_add_state_levels_room_id ON room_add_state_levels(room_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS room_send_event_levels(
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
level INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS room_send_event_levels_event_id ON room_send_event_levels(event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS room_send_event_levels_room_id ON room_send_event_levels(room_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS room_ops_levels(
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
room_id TEXT NOT NULL,
|
||||||
|
ban_level INTEGER,
|
||||||
|
kick_level INTEGER
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS room_ops_levels_room_id ON room_ops_levels(room_id);
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS room_hosts(
|
CREATE TABLE IF NOT EXISTS room_hosts(
|
||||||
room_id TEXT NOT NULL,
|
room_id TEXT NOT NULL,
|
||||||
host TEXT NOT NULL,
|
host TEXT NOT NULL,
|
||||||
CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE
|
CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS room_hosts_room_id ON room_hosts (room_id);
|
||||||
|
|||||||
30
synapse/storage/schema/keys.sql
Normal file
30
synapse/storage/schema/keys.sql
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
/* 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.
|
||||||
|
*/
|
||||||
|
CREATE TABLE IF NOT EXISTS server_tls_certificates(
|
||||||
|
server_name TEXT, -- Server name.
|
||||||
|
key_server TEXT, -- Which key server the certificate was fetched from.
|
||||||
|
ts_added_ms INTEGER, -- When the certifcate was added.
|
||||||
|
tls_certificate BLOB, -- DER encoded x509 certificate.
|
||||||
|
CONSTRAINT uniqueness UNIQUE (server_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS server_signature_keys(
|
||||||
|
server_name TEXT, -- Server name.
|
||||||
|
key_version TEXT, -- Key version.
|
||||||
|
key_server TEXT, -- Which key server the key was fetched form.
|
||||||
|
ts_added_ms INTEGER, -- When the key was added.
|
||||||
|
verification_key BLOB, -- NACL verification key.
|
||||||
|
CONSTRAINT uniqueness UNIQUE (server_name, key_version)
|
||||||
|
);
|
||||||
@@ -17,6 +17,10 @@ from twisted.internet import defer
|
|||||||
|
|
||||||
from synapse.types import StreamToken
|
from synapse.types import StreamToken
|
||||||
|
|
||||||
|
from synapse.handlers.presence import PresenceEventSource
|
||||||
|
from synapse.handlers.room import RoomEventSource
|
||||||
|
from synapse.handlers.typing import TypingNotificationEventSource
|
||||||
|
|
||||||
|
|
||||||
class NullSource(object):
|
class NullSource(object):
|
||||||
"""This event source never yields any events and its token remains at
|
"""This event source never yields any events and its token remains at
|
||||||
@@ -24,146 +28,21 @@ class NullSource(object):
|
|||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def get_new_events_for_user(self, user, from_token, limit):
|
def get_new_events_for_user(self, user, from_key, limit):
|
||||||
return defer.succeed(([], from_token))
|
return defer.succeed(([], from_key))
|
||||||
|
|
||||||
def get_current_token_part(self):
|
def get_current_key(self):
|
||||||
return defer.succeed(0)
|
return defer.succeed(0)
|
||||||
|
|
||||||
def get_pagination_rows(self, user, pagination_config, key):
|
def get_pagination_rows(self, user, pagination_config, key):
|
||||||
return defer.succeed(([], pagination_config.from_token))
|
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):
|
class EventSources(object):
|
||||||
SOURCE_TYPES = {
|
SOURCE_TYPES = {
|
||||||
"room": RoomEventSource,
|
"room": RoomEventSource,
|
||||||
"presence": PresenceSource,
|
"presence": PresenceEventSource,
|
||||||
|
"typing": TypingNotificationEventSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
@@ -172,24 +51,29 @@ class EventSources(object):
|
|||||||
for name, cls in EventSources.SOURCE_TYPES.items()
|
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
|
@defer.inlineCallbacks
|
||||||
def get_current_token(self):
|
def get_current_token(self):
|
||||||
events_key = yield self.sources["room"].get_current_token_part()
|
token = StreamToken(
|
||||||
presence_key = yield self.sources["presence"].get_current_token_part()
|
room_key=(
|
||||||
token = EventSources.create_token(events_key, presence_key)
|
yield self.sources["room"].get_current_key()
|
||||||
|
),
|
||||||
|
presence_key=(
|
||||||
|
yield self.sources["presence"].get_current_key()
|
||||||
|
),
|
||||||
|
typing_key=(
|
||||||
|
yield self.sources["typing"].get_current_key()
|
||||||
|
)
|
||||||
|
)
|
||||||
defer.returnValue(token)
|
defer.returnValue(token)
|
||||||
|
|
||||||
|
|
||||||
class StreamSource(object):
|
class StreamSource(object):
|
||||||
def get_new_events_for_user(self, user, from_token, limit):
|
def get_new_events_for_user(self, user, from_key, limit):
|
||||||
|
"""from_key is the key within this event source."""
|
||||||
raise NotImplementedError("get_new_events_for_user")
|
raise NotImplementedError("get_new_events_for_user")
|
||||||
|
|
||||||
def get_current_token_part(self):
|
def get_current_key(self):
|
||||||
raise NotImplementedError("get_current_token_part")
|
raise NotImplementedError("get_current_key")
|
||||||
|
|
||||||
def get_pagination_rows(self, user, pagination_config, key):
|
def get_pagination_rows(self, user, pagination_config, key):
|
||||||
raise NotImplementedError("get_rows")
|
raise NotImplementedError("get_rows")
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class RoomID(DomainSpecificString):
|
|||||||
class StreamToken(
|
class StreamToken(
|
||||||
namedtuple(
|
namedtuple(
|
||||||
"Token",
|
"Token",
|
||||||
("events_key", "presence_key")
|
("room_key", "presence_key", "typing_key")
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
_SEPARATOR = "_"
|
_SEPARATOR = "_"
|
||||||
@@ -105,21 +105,14 @@ class StreamToken(
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_string(cls, string):
|
def from_string(cls, string):
|
||||||
try:
|
try:
|
||||||
events_key, presence_key = string.split(cls._SEPARATOR)
|
keys = string.split(cls._SEPARATOR)
|
||||||
|
|
||||||
return cls(
|
return cls(*keys)
|
||||||
events_key=events_key,
|
|
||||||
presence_key=presence_key,
|
|
||||||
)
|
|
||||||
except:
|
except:
|
||||||
raise SynapseError(400, "Invalid Token")
|
raise SynapseError(400, "Invalid Token")
|
||||||
|
|
||||||
def to_string(self):
|
def to_string(self):
|
||||||
return "".join([
|
return self._SEPARATOR.join([str(k) for k in self])
|
||||||
str(self.events_key),
|
|
||||||
self._SEPARATOR,
|
|
||||||
str(self.presence_key),
|
|
||||||
])
|
|
||||||
|
|
||||||
def copy_and_replace(self, key, new_value):
|
def copy_and_replace(self, key, new_value):
|
||||||
d = self._asdict()
|
d = self._asdict()
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
# Empty context initially
|
# Empty context initially
|
||||||
(code, response) = yield self.mock_resource.trigger("GET",
|
(code, response) = yield self.mock_resource.trigger("GET",
|
||||||
"/matrix/federation/v1/state/my-context/", None)
|
"/_matrix/federation/v1/state/my-context/", None)
|
||||||
self.assertEquals(200, code)
|
self.assertEquals(200, code)
|
||||||
self.assertFalse(response["pdus"])
|
self.assertFalse(response["pdus"])
|
||||||
|
|
||||||
@@ -112,7 +112,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
(code, response) = yield self.mock_resource.trigger("GET",
|
(code, response) = yield self.mock_resource.trigger("GET",
|
||||||
"/matrix/federation/v1/state/my-context/", None)
|
"/_matrix/federation/v1/state/my-context/", None)
|
||||||
self.assertEquals(200, code)
|
self.assertEquals(200, code)
|
||||||
self.assertEquals(1, len(response["pdus"]))
|
self.assertEquals(1, len(response["pdus"]))
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
(code, response) = yield self.mock_resource.trigger("GET",
|
(code, response) = yield self.mock_resource.trigger("GET",
|
||||||
"/matrix/federation/v1/pdu/red/abc123def456/", None)
|
"/_matrix/federation/v1/pdu/red/abc123def456/", None)
|
||||||
self.assertEquals(404, code)
|
self.assertEquals(404, code)
|
||||||
|
|
||||||
# Now insert such a PDU
|
# Now insert such a PDU
|
||||||
@@ -142,7 +142,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
(code, response) = yield self.mock_resource.trigger("GET",
|
(code, response) = yield self.mock_resource.trigger("GET",
|
||||||
"/matrix/federation/v1/pdu/red/abc123def456/", None)
|
"/_matrix/federation/v1/pdu/red/abc123def456/", None)
|
||||||
self.assertEquals(200, code)
|
self.assertEquals(200, code)
|
||||||
self.assertEquals(1, len(response["pdus"]))
|
self.assertEquals(1, len(response["pdus"]))
|
||||||
self.assertEquals("m.text", response["pdus"][0]["pdu_type"])
|
self.assertEquals("m.text", response["pdus"][0]["pdu_type"])
|
||||||
@@ -168,7 +168,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.mock_http_client.put_json.assert_called_with(
|
self.mock_http_client.put_json.assert_called_with(
|
||||||
"remote",
|
"remote",
|
||||||
path="/matrix/federation/v1/send/1000000/",
|
path="/_matrix/federation/v1/send/1000000/",
|
||||||
data={
|
data={
|
||||||
"ts": 1000000,
|
"ts": 1000000,
|
||||||
"origin": "test",
|
"origin": "test",
|
||||||
@@ -203,7 +203,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
# MockClock ensures we can guess these timestamps
|
# MockClock ensures we can guess these timestamps
|
||||||
self.mock_http_client.put_json.assert_called_with(
|
self.mock_http_client.put_json.assert_called_with(
|
||||||
"remote",
|
"remote",
|
||||||
path="/matrix/federation/v1/send/1000000/",
|
path="/_matrix/federation/v1/send/1000000/",
|
||||||
data={
|
data={
|
||||||
"origin": "test",
|
"origin": "test",
|
||||||
"ts": 1000000,
|
"ts": 1000000,
|
||||||
@@ -226,7 +226,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
self.federation.register_edu_handler("m.test", recv_observer)
|
self.federation.register_edu_handler("m.test", recv_observer)
|
||||||
|
|
||||||
yield self.mock_resource.trigger("PUT",
|
yield self.mock_resource.trigger("PUT",
|
||||||
"/matrix/federation/v1/send/1001000/",
|
"/_matrix/federation/v1/send/1001000/",
|
||||||
"""{
|
"""{
|
||||||
"origin": "remote",
|
"origin": "remote",
|
||||||
"ts": 1001000,
|
"ts": 1001000,
|
||||||
@@ -261,7 +261,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.mock_http_client.get_json.assert_called_with(
|
self.mock_http_client.get_json.assert_called_with(
|
||||||
destination="remote",
|
destination="remote",
|
||||||
path="/matrix/federation/v1/query/a-question",
|
path="/_matrix/federation/v1/query/a-question",
|
||||||
args={"one": "1", "two": "2"}
|
args={"one": "1", "two": "2"}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -273,7 +273,7 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
self.federation.register_query_handler("a-question", recv_handler)
|
self.federation.register_query_handler("a-question", recv_handler)
|
||||||
|
|
||||||
code, response = yield self.mock_resource.trigger("GET",
|
code, response = yield self.mock_resource.trigger("GET",
|
||||||
"/matrix/federation/v1/query/a-question?three=3&four=4", None)
|
"/_matrix/federation/v1/query/a-question?three=3&four=4", None)
|
||||||
|
|
||||||
self.assertEquals(200, code)
|
self.assertEquals(200, code)
|
||||||
self.assertEquals({"another": "response"}, response)
|
self.assertEquals({"another": "response"}, response)
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ ONLINE = PresenceState.ONLINE
|
|||||||
|
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
logging.getLogger().addHandler(logging.NullHandler())
|
||||||
#logging.getLogger().addHandler(logging.StreamHandler())
|
|
||||||
#logging.getLogger().setLevel(logging.DEBUG)
|
|
||||||
|
|
||||||
|
|
||||||
def _expect_edu(destination, edu_type, content, origin="test"):
|
def _expect_edu(destination, edu_type, content, origin="test"):
|
||||||
@@ -141,7 +139,8 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||||||
target_user=self.u_apple, auth_user=self.u_apple
|
target_user=self.u_apple, auth_user=self.u_apple
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals({"state": ONLINE, "status_msg": "Online"},
|
self.assertEquals(
|
||||||
|
{"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
mocked_get.assert_called_with("apple")
|
mocked_get.assert_called_with("apple")
|
||||||
@@ -157,7 +156,8 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||||||
target_user=self.u_apple, auth_user=self.u_banana
|
target_user=self.u_apple, auth_user=self.u_banana
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals({"state": ONLINE, "status_msg": "Online"},
|
self.assertEquals(
|
||||||
|
{"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
|
||||||
state
|
state
|
||||||
)
|
)
|
||||||
mocked_get.assert_called_with("apple")
|
mocked_get.assert_called_with("apple")
|
||||||
@@ -175,7 +175,10 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||||||
target_user=self.u_apple, auth_user=self.u_clementine
|
target_user=self.u_apple, auth_user=self.u_clementine
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEquals({"state": ONLINE, "status_msg": "Online"}, state)
|
self.assertEquals(
|
||||||
|
{"state": ONLINE, "presence": ONLINE, "status_msg": "Online"},
|
||||||
|
state
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_get_disallowed_state(self):
|
def test_get_disallowed_state(self):
|
||||||
@@ -193,8 +196,6 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||||||
SynapseError
|
SynapseError
|
||||||
)
|
)
|
||||||
|
|
||||||
test_get_disallowed_state.skip = "Presence permissions are disabled"
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_set_my_state(self):
|
def test_set_my_state(self):
|
||||||
mocked_set = self.datastore.set_presence_state
|
mocked_set = self.datastore.set_presence_state
|
||||||
@@ -202,20 +203,20 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
yield self.handler.set_state(
|
yield self.handler.set_state(
|
||||||
target_user=self.u_apple, auth_user=self.u_apple,
|
target_user=self.u_apple, auth_user=self.u_apple,
|
||||||
state={"state": UNAVAILABLE, "status_msg": "Away"})
|
state={"presence": UNAVAILABLE, "status_msg": "Away"})
|
||||||
|
|
||||||
mocked_set.assert_called_with("apple",
|
mocked_set.assert_called_with("apple",
|
||||||
{"state": UNAVAILABLE, "status_msg": "Away"})
|
{"state": UNAVAILABLE, "status_msg": "Away"})
|
||||||
self.mock_start.assert_called_with(self.u_apple,
|
self.mock_start.assert_called_with(self.u_apple,
|
||||||
state={
|
state={
|
||||||
"state": UNAVAILABLE,
|
"presence": UNAVAILABLE,
|
||||||
"status_msg": "Away",
|
"status_msg": "Away",
|
||||||
"mtime": 1000000, # MockClock
|
"last_active": 1000000, # MockClock
|
||||||
})
|
})
|
||||||
|
|
||||||
yield self.handler.set_state(
|
yield self.handler.set_state(
|
||||||
target_user=self.u_apple, auth_user=self.u_apple,
|
target_user=self.u_apple, auth_user=self.u_apple,
|
||||||
state={"state": OFFLINE})
|
state={"presence": OFFLINE})
|
||||||
|
|
||||||
self.mock_stop.assert_called_with(self.u_apple)
|
self.mock_stop.assert_called_with(self.u_apple)
|
||||||
|
|
||||||
@@ -314,7 +315,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
put_json = self.mock_http_client.put_json
|
put_json = self.mock_http_client.put_json
|
||||||
put_json.expect_call_and_return(
|
put_json.expect_call_and_return(
|
||||||
call("elsewhere",
|
call("elsewhere",
|
||||||
path="/matrix/federation/v1/send/1000000/",
|
path="/_matrix/federation/v1/send/1000000/",
|
||||||
data=_expect_edu("elsewhere", "m.presence_invite",
|
data=_expect_edu("elsewhere", "m.presence_invite",
|
||||||
content={
|
content={
|
||||||
"observer_user": "@apple:test",
|
"observer_user": "@apple:test",
|
||||||
@@ -340,7 +341,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
put_json = self.mock_http_client.put_json
|
put_json = self.mock_http_client.put_json
|
||||||
put_json.expect_call_and_return(
|
put_json.expect_call_and_return(
|
||||||
call("elsewhere",
|
call("elsewhere",
|
||||||
path="/matrix/federation/v1/send/1000000/",
|
path="/_matrix/federation/v1/send/1000000/",
|
||||||
data=_expect_edu("elsewhere", "m.presence_accept",
|
data=_expect_edu("elsewhere", "m.presence_accept",
|
||||||
content={
|
content={
|
||||||
"observer_user": "@cabbage:elsewhere",
|
"observer_user": "@cabbage:elsewhere",
|
||||||
@@ -352,7 +353,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
yield self.mock_federation_resource.trigger("PUT",
|
yield self.mock_federation_resource.trigger("PUT",
|
||||||
"/matrix/federation/v1/send/1000000/",
|
"/_matrix/federation/v1/send/1000000/",
|
||||||
_make_edu_json("elsewhere", "m.presence_invite",
|
_make_edu_json("elsewhere", "m.presence_invite",
|
||||||
content={
|
content={
|
||||||
"observer_user": "@cabbage:elsewhere",
|
"observer_user": "@cabbage:elsewhere",
|
||||||
@@ -371,7 +372,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
put_json = self.mock_http_client.put_json
|
put_json = self.mock_http_client.put_json
|
||||||
put_json.expect_call_and_return(
|
put_json.expect_call_and_return(
|
||||||
call("elsewhere",
|
call("elsewhere",
|
||||||
path="/matrix/federation/v1/send/1000000/",
|
path="/_matrix/federation/v1/send/1000000/",
|
||||||
data=_expect_edu("elsewhere", "m.presence_deny",
|
data=_expect_edu("elsewhere", "m.presence_deny",
|
||||||
content={
|
content={
|
||||||
"observer_user": "@cabbage:elsewhere",
|
"observer_user": "@cabbage:elsewhere",
|
||||||
@@ -383,7 +384,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
yield self.mock_federation_resource.trigger("PUT",
|
yield self.mock_federation_resource.trigger("PUT",
|
||||||
"/matrix/federation/v1/send/1000000/",
|
"/_matrix/federation/v1/send/1000000/",
|
||||||
_make_edu_json("elsewhere", "m.presence_invite",
|
_make_edu_json("elsewhere", "m.presence_invite",
|
||||||
content={
|
content={
|
||||||
"observer_user": "@cabbage:elsewhere",
|
"observer_user": "@cabbage:elsewhere",
|
||||||
@@ -397,7 +398,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_accepted_remote(self):
|
def test_accepted_remote(self):
|
||||||
yield self.mock_federation_resource.trigger("PUT",
|
yield self.mock_federation_resource.trigger("PUT",
|
||||||
"/matrix/federation/v1/send/1000000/",
|
"/_matrix/federation/v1/send/1000000/",
|
||||||
_make_edu_json("elsewhere", "m.presence_accept",
|
_make_edu_json("elsewhere", "m.presence_accept",
|
||||||
content={
|
content={
|
||||||
"observer_user": "@apple:test",
|
"observer_user": "@apple:test",
|
||||||
@@ -415,7 +416,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_denied_remote(self):
|
def test_denied_remote(self):
|
||||||
yield self.mock_federation_resource.trigger("PUT",
|
yield self.mock_federation_resource.trigger("PUT",
|
||||||
"/matrix/federation/v1/send/1000000/",
|
"/_matrix/federation/v1/send/1000000/",
|
||||||
_make_edu_json("elsewhere", "m.presence_deny",
|
_make_edu_json("elsewhere", "m.presence_deny",
|
||||||
content={
|
content={
|
||||||
"observer_user": "@apple:test",
|
"observer_user": "@apple:test",
|
||||||
@@ -455,22 +456,29 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
presence = yield self.handler.get_presence_list(
|
presence = yield self.handler.get_presence_list(
|
||||||
observer_user=self.u_apple)
|
observer_user=self.u_apple)
|
||||||
|
|
||||||
self.assertEquals([{"observed_user": self.u_banana,
|
self.assertEquals([
|
||||||
"state": OFFLINE}], presence)
|
{"observed_user": self.u_banana,
|
||||||
|
"presence": OFFLINE,
|
||||||
|
"state": OFFLINE},
|
||||||
|
], presence)
|
||||||
|
|
||||||
self.datastore.get_presence_list.assert_called_with("apple",
|
self.datastore.get_presence_list.assert_called_with("apple",
|
||||||
accepted=None)
|
accepted=None
|
||||||
|
)
|
||||||
|
|
||||||
self.datastore.get_presence_list.return_value = defer.succeed(
|
self.datastore.get_presence_list.return_value = defer.succeed(
|
||||||
[{"observed_user_id": "@banana:test"}]
|
[{"observed_user_id": "@banana:test"}]
|
||||||
)
|
)
|
||||||
|
|
||||||
presence = yield self.handler.get_presence_list(
|
presence = yield self.handler.get_presence_list(
|
||||||
observer_user=self.u_apple, accepted=True)
|
observer_user=self.u_apple, accepted=True
|
||||||
|
)
|
||||||
|
|
||||||
self.assertEquals([{"observed_user": self.u_banana,
|
self.assertEquals([
|
||||||
"state": OFFLINE}], presence)
|
{"observed_user": self.u_banana,
|
||||||
|
"presence": OFFLINE,
|
||||||
|
"state": OFFLINE},
|
||||||
|
], presence)
|
||||||
|
|
||||||
self.datastore.get_presence_list.assert_called_with("apple",
|
self.datastore.get_presence_list.assert_called_with("apple",
|
||||||
accepted=True)
|
accepted=True)
|
||||||
@@ -514,13 +522,6 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
hs.handlers = JustPresenceHandlers(hs)
|
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.side_effect = update
|
|
||||||
|
|
||||||
self.datastore = hs.get_datastore()
|
self.datastore = hs.get_datastore()
|
||||||
|
|
||||||
def get_received_txn_response(*args):
|
def get_received_txn_response(*args):
|
||||||
@@ -528,7 +529,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
self.datastore.get_received_txn_response = get_received_txn_response
|
self.datastore.get_received_txn_response = get_received_txn_response
|
||||||
|
|
||||||
self.handler = hs.get_handlers().presence_handler
|
self.handler = hs.get_handlers().presence_handler
|
||||||
self.handler.push_update_to_clients = self.mock_update_client
|
self.event_source = hs.get_event_sources().sources["presence"]
|
||||||
|
|
||||||
# Mock the RoomMemberHandler
|
# Mock the RoomMemberHandler
|
||||||
hs.handlers.room_member_handler = Mock(spec=[
|
hs.handlers.room_member_handler = Mock(spec=[
|
||||||
@@ -618,31 +619,54 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
# TODO(paul): Gut-wrenching
|
# TODO(paul): Gut-wrenching
|
||||||
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
|
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
|
||||||
|
self.handler._user_cachemap[self.u_apple].update(
|
||||||
|
{"presence": OFFLINE}, serial=0
|
||||||
|
)
|
||||||
apple_set = self.handler._local_pushmap.setdefault("apple", set())
|
apple_set = self.handler._local_pushmap.setdefault("apple", set())
|
||||||
apple_set.add(self.u_banana)
|
apple_set.add(self.u_banana)
|
||||||
apple_set.add(self.u_clementine)
|
apple_set.add(self.u_clementine)
|
||||||
|
|
||||||
yield self.handler.set_state(self.u_apple, self.u_apple,
|
self.assertEquals(self.event_source.get_current_key(), 0)
|
||||||
{"state": ONLINE})
|
|
||||||
|
|
||||||
self.mock_update_client.assert_has_calls([
|
yield self.handler.set_state(self.u_apple, self.u_apple,
|
||||||
call(users_to_push=set([self.u_apple, self.u_banana, self.u_clementine]),
|
{"presence": ONLINE}
|
||||||
room_ids=["a-room"],
|
)
|
||||||
observed_user=self.u_apple,
|
|
||||||
statuscache=ANY), # self-reflection
|
(events, _) = yield self.event_source.get_new_events_for_user(
|
||||||
], any_order=True)
|
self.u_apple, 0, None
|
||||||
self.mock_update_client.reset_mock()
|
)
|
||||||
|
|
||||||
|
self.assertEquals(self.event_source.get_current_key(), 1)
|
||||||
|
self.assertEquals(events,
|
||||||
|
[
|
||||||
|
{"type": "m.presence",
|
||||||
|
"content": {
|
||||||
|
"user_id": "@apple:test",
|
||||||
|
"presence": ONLINE,
|
||||||
|
"state": ONLINE,
|
||||||
|
"last_active_ago": 0,
|
||||||
|
}},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
presence = yield self.handler.get_presence_list(
|
presence = yield self.handler.get_presence_list(
|
||||||
observer_user=self.u_apple, accepted=True)
|
observer_user=self.u_apple, accepted=True)
|
||||||
|
|
||||||
self.assertEquals([
|
self.assertEquals(
|
||||||
{"observed_user": self.u_banana, "state": OFFLINE},
|
[
|
||||||
{"observed_user": self.u_clementine, "state": OFFLINE}],
|
{"observed_user": self.u_banana,
|
||||||
presence)
|
"presence": OFFLINE,
|
||||||
|
"state": OFFLINE},
|
||||||
|
{"observed_user": self.u_clementine,
|
||||||
|
"presence": OFFLINE,
|
||||||
|
"state": OFFLINE},
|
||||||
|
],
|
||||||
|
presence
|
||||||
|
)
|
||||||
|
|
||||||
yield self.handler.set_state(self.u_banana, self.u_banana,
|
yield self.handler.set_state(self.u_banana, self.u_banana,
|
||||||
{"state": ONLINE})
|
{"presence": ONLINE}
|
||||||
|
)
|
||||||
|
|
||||||
self.clock.advance_time(2)
|
self.clock.advance_time(2)
|
||||||
|
|
||||||
@@ -651,37 +675,34 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEquals([
|
self.assertEquals([
|
||||||
{"observed_user": self.u_banana,
|
{"observed_user": self.u_banana,
|
||||||
|
"presence": ONLINE,
|
||||||
"state": ONLINE,
|
"state": ONLINE,
|
||||||
"mtime_age": 2000},
|
"last_active_ago": 2000},
|
||||||
{"observed_user": self.u_clementine,
|
{"observed_user": self.u_clementine,
|
||||||
|
"presence": OFFLINE,
|
||||||
"state": OFFLINE},
|
"state": OFFLINE},
|
||||||
], presence)
|
], presence)
|
||||||
|
|
||||||
self.mock_update_client.assert_has_calls([
|
(events, _) = yield self.event_source.get_new_events_for_user(
|
||||||
call(users_to_push=set([self.u_banana]),
|
self.u_apple, 1, None
|
||||||
room_ids=[],
|
)
|
||||||
observed_user=self.u_banana,
|
|
||||||
statuscache=ANY), # self-reflection
|
self.assertEquals(self.event_source.get_current_key(), 2)
|
||||||
]) # and no others...
|
self.assertEquals(events,
|
||||||
|
[
|
||||||
|
{"type": "m.presence",
|
||||||
|
"content": {
|
||||||
|
"user_id": "@banana:test",
|
||||||
|
"presence": ONLINE,
|
||||||
|
"state": ONLINE,
|
||||||
|
"last_active_ago": 2000
|
||||||
|
}},
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_push_remote(self):
|
def test_push_remote(self):
|
||||||
put_json = self.mock_http_client.put_json
|
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(
|
put_json.expect_call_and_return(
|
||||||
call("farm",
|
call("farm",
|
||||||
path=ANY, # Can't guarantee which txn ID will be which
|
path=ANY, # Can't guarantee which txn ID will be which
|
||||||
@@ -689,8 +710,9 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
content={
|
content={
|
||||||
"push": [
|
"push": [
|
||||||
{"user_id": "@apple:test",
|
{"user_id": "@apple:test",
|
||||||
|
"presence": u"online",
|
||||||
"state": u"online",
|
"state": u"online",
|
||||||
"mtime_age": 0},
|
"last_active_ago": 0},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -706,11 +728,14 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
# TODO(paul): Gut-wrenching
|
# TODO(paul): Gut-wrenching
|
||||||
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
|
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
|
||||||
|
self.handler._user_cachemap[self.u_apple].update(
|
||||||
|
{"presence": OFFLINE}, serial=0
|
||||||
|
)
|
||||||
apple_set = self.handler._remote_sendmap.setdefault("apple", set())
|
apple_set = self.handler._remote_sendmap.setdefault("apple", set())
|
||||||
apple_set.add(self.u_potato.domain)
|
apple_set.add(self.u_potato.domain)
|
||||||
|
|
||||||
yield self.handler.set_state(self.u_apple, self.u_apple,
|
yield self.handler.set_state(self.u_apple, self.u_apple,
|
||||||
{"state": ONLINE}
|
{"presence": ONLINE}
|
||||||
)
|
)
|
||||||
|
|
||||||
yield put_json.await_calls()
|
yield put_json.await_calls()
|
||||||
@@ -724,54 +749,82 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.room_members = [self.u_banana, self.u_potato]
|
self.room_members = [self.u_banana, self.u_potato]
|
||||||
|
|
||||||
|
self.assertEquals(self.event_source.get_current_key(), 0)
|
||||||
|
|
||||||
yield self.mock_federation_resource.trigger("PUT",
|
yield self.mock_federation_resource.trigger("PUT",
|
||||||
"/matrix/federation/v1/send/1000000/",
|
"/_matrix/federation/v1/send/1000000/",
|
||||||
_make_edu_json("elsewhere", "m.presence",
|
_make_edu_json("elsewhere", "m.presence",
|
||||||
content={
|
content={
|
||||||
"push": [
|
"push": [
|
||||||
{"user_id": "@potato:remote",
|
{"user_id": "@potato:remote",
|
||||||
"state": "online",
|
"state": "online",
|
||||||
"mtime_age": 1000},
|
"last_active_ago": 1000},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.mock_update_client.assert_has_calls([
|
(events, _) = yield self.event_source.get_new_events_for_user(
|
||||||
call(users_to_push=set([self.u_apple]),
|
self.u_apple, 0, None
|
||||||
room_ids=["a-room"],
|
)
|
||||||
observed_user=self.u_potato,
|
|
||||||
statuscache=ANY),
|
self.assertEquals(self.event_source.get_current_key(), 1)
|
||||||
], any_order=True)
|
self.assertEquals(events,
|
||||||
|
[
|
||||||
|
{"type": "m.presence",
|
||||||
|
"content": {
|
||||||
|
"user_id": "@potato:remote",
|
||||||
|
"presence": ONLINE,
|
||||||
|
"state": ONLINE,
|
||||||
|
"last_active_ago": 1000,
|
||||||
|
}}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
self.clock.advance_time(2)
|
self.clock.advance_time(2)
|
||||||
|
|
||||||
state = yield self.handler.get_state(self.u_potato, self.u_apple)
|
state = yield self.handler.get_state(self.u_potato, self.u_apple)
|
||||||
|
|
||||||
self.assertEquals({"state": ONLINE, "mtime_age": 3000}, state)
|
self.assertEquals(
|
||||||
|
{"state": ONLINE, "presence": ONLINE, "last_active_ago": 3000},
|
||||||
|
state
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_join_room_local(self):
|
def test_join_room_local(self):
|
||||||
self.room_members = [self.u_apple, self.u_banana]
|
self.room_members = [self.u_apple, self.u_banana]
|
||||||
|
|
||||||
yield self.distributor.fire("user_joined_room", self.u_elderberry,
|
self.assertEquals(self.event_source.get_current_key(), 0)
|
||||||
|
|
||||||
|
# TODO(paul): Gut-wrenching
|
||||||
|
self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
|
||||||
|
self.handler._user_cachemap[self.u_clementine].update(
|
||||||
|
{
|
||||||
|
"presence": PresenceState.ONLINE,
|
||||||
|
"last_active": self.clock.time_msec(),
|
||||||
|
}, self.u_clementine
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.distributor.fire("user_joined_room", self.u_clementine,
|
||||||
"a-room"
|
"a-room"
|
||||||
)
|
)
|
||||||
|
|
||||||
self.mock_update_client.assert_has_calls([
|
(events, _) = yield self.event_source.get_new_events_for_user(
|
||||||
call(room_ids=["a-room"],
|
self.u_apple, 0, None
|
||||||
observed_user=self.u_elderberry,
|
)
|
||||||
users_to_push=set(),
|
|
||||||
statuscache=ANY),
|
self.assertEquals(self.event_source.get_current_key(), 1)
|
||||||
call(users_to_push=set([self.u_elderberry]),
|
self.assertEquals(events,
|
||||||
observed_user=self.u_apple,
|
[
|
||||||
room_ids=[],
|
{"type": "m.presence",
|
||||||
statuscache=ANY),
|
"content": {
|
||||||
call(users_to_push=set([self.u_elderberry]),
|
"user_id": "@clementine:test",
|
||||||
observed_user=self.u_banana,
|
"presence": ONLINE,
|
||||||
room_ids=[],
|
"state": ONLINE,
|
||||||
statuscache=ANY),
|
"last_active_ago": 0,
|
||||||
], any_order=True)
|
}}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_join_room_remote(self):
|
def test_join_room_remote(self):
|
||||||
@@ -784,6 +837,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
content={
|
content={
|
||||||
"push": [
|
"push": [
|
||||||
{"user_id": "@apple:test",
|
{"user_id": "@apple:test",
|
||||||
|
"presence": "online",
|
||||||
"state": "online"},
|
"state": "online"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -798,6 +852,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
content={
|
content={
|
||||||
"push": [
|
"push": [
|
||||||
{"user_id": "@banana:test",
|
{"user_id": "@banana:test",
|
||||||
|
"presence": "offline",
|
||||||
"state": "offline"},
|
"state": "offline"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -809,7 +864,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
# TODO(paul): Gut-wrenching
|
# TODO(paul): Gut-wrenching
|
||||||
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
|
self.handler._user_cachemap[self.u_apple] = UserPresenceCache()
|
||||||
self.handler._user_cachemap[self.u_apple].update(
|
self.handler._user_cachemap[self.u_apple].update(
|
||||||
{"state": PresenceState.ONLINE}, self.u_apple)
|
{"presence": PresenceState.ONLINE}, self.u_apple)
|
||||||
self.room_members = [self.u_apple, self.u_banana]
|
self.room_members = [self.u_apple, self.u_banana]
|
||||||
|
|
||||||
yield self.distributor.fire("user_joined_room", self.u_potato,
|
yield self.distributor.fire("user_joined_room", self.u_potato,
|
||||||
@@ -822,11 +877,12 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
put_json.expect_call_and_return(
|
put_json.expect_call_and_return(
|
||||||
call("remote",
|
call("remote",
|
||||||
path="/matrix/federation/v1/send/1000002/",
|
path="/_matrix/federation/v1/send/1000002/",
|
||||||
data=_expect_edu("remote", "m.presence",
|
data=_expect_edu("remote", "m.presence",
|
||||||
content={
|
content={
|
||||||
"push": [
|
"push": [
|
||||||
{"user_id": "@clementine:test",
|
{"user_id": "@clementine:test",
|
||||||
|
"presence": "online",
|
||||||
"state": "online"},
|
"state": "online"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
@@ -837,7 +893,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
|
self.handler._user_cachemap[self.u_clementine] = UserPresenceCache()
|
||||||
self.handler._user_cachemap[self.u_clementine].update(
|
self.handler._user_cachemap[self.u_clementine].update(
|
||||||
{"state": ONLINE}, self.u_clementine)
|
{"presence": ONLINE}, self.u_clementine)
|
||||||
self.room_members.append(self.u_potato)
|
self.room_members.append(self.u_potato)
|
||||||
|
|
||||||
yield self.distributor.fire("user_joined_room", self.u_clementine,
|
yield self.distributor.fire("user_joined_room", self.u_clementine,
|
||||||
@@ -921,7 +977,8 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
def get_presence_state(user_localpart):
|
def get_presence_state(user_localpart):
|
||||||
return defer.succeed(
|
return defer.succeed(
|
||||||
{"state": self.current_user_state[user_localpart],
|
{"state": self.current_user_state[user_localpart],
|
||||||
"status_msg": None}
|
"status_msg": None,
|
||||||
|
"mtime": 123456000}
|
||||||
)
|
)
|
||||||
self.datastore.get_presence_state = get_presence_state
|
self.datastore.get_presence_state = get_presence_state
|
||||||
|
|
||||||
@@ -955,7 +1012,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
# apple goes online
|
# apple goes online
|
||||||
yield self.handler.set_state(
|
yield self.handler.set_state(
|
||||||
target_user=self.u_apple, auth_user=self.u_apple,
|
target_user=self.u_apple, auth_user=self.u_apple,
|
||||||
state={"state": ONLINE}
|
state={"presence": ONLINE}
|
||||||
)
|
)
|
||||||
|
|
||||||
# apple should see both banana and clementine currently offline
|
# apple should see both banana and clementine currently offline
|
||||||
@@ -979,7 +1036,8 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
# banana goes online
|
# banana goes online
|
||||||
yield self.handler.set_state(
|
yield self.handler.set_state(
|
||||||
target_user=self.u_banana, auth_user=self.u_banana,
|
target_user=self.u_banana, auth_user=self.u_banana,
|
||||||
state={"state": ONLINE})
|
state={"presence": ONLINE}
|
||||||
|
)
|
||||||
|
|
||||||
# apple and banana should now both see each other online
|
# apple and banana should now both see each other online
|
||||||
self.mock_update_client.assert_has_calls([
|
self.mock_update_client.assert_has_calls([
|
||||||
@@ -1000,7 +1058,8 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
# apple goes offline
|
# apple goes offline
|
||||||
yield self.handler.set_state(
|
yield self.handler.set_state(
|
||||||
target_user=self.u_apple, auth_user=self.u_apple,
|
target_user=self.u_apple, auth_user=self.u_apple,
|
||||||
state={"state": OFFLINE})
|
state={"presence": OFFLINE}
|
||||||
|
)
|
||||||
|
|
||||||
# banana should now be told apple is offline
|
# banana should now be told apple is offline
|
||||||
self.mock_update_client.assert_has_calls([
|
self.mock_update_client.assert_has_calls([
|
||||||
@@ -1013,7 +1072,6 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
self.assertFalse("banana" in self.handler._local_pushmap)
|
self.assertFalse("banana" in self.handler._local_pushmap)
|
||||||
self.assertFalse("clementine" in self.handler._local_pushmap)
|
self.assertFalse("clementine" in self.handler._local_pushmap)
|
||||||
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_remote_poll_send(self):
|
def test_remote_poll_send(self):
|
||||||
put_json = self.mock_http_client.put_json
|
put_json = self.mock_http_client.put_json
|
||||||
@@ -1044,7 +1102,8 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
# clementine goes online
|
# clementine goes online
|
||||||
yield self.handler.set_state(
|
yield self.handler.set_state(
|
||||||
target_user=self.u_clementine, auth_user=self.u_clementine,
|
target_user=self.u_clementine, auth_user=self.u_clementine,
|
||||||
state={"state": ONLINE})
|
state={"presence": ONLINE}
|
||||||
|
)
|
||||||
|
|
||||||
yield put_json.await_calls()
|
yield put_json.await_calls()
|
||||||
|
|
||||||
@@ -1071,7 +1130,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
# fig goes online; shouldn't send a second poll
|
# fig goes online; shouldn't send a second poll
|
||||||
yield self.handler.set_state(
|
yield self.handler.set_state(
|
||||||
target_user=self.u_fig, auth_user=self.u_fig,
|
target_user=self.u_fig, auth_user=self.u_fig,
|
||||||
state={"state": ONLINE}
|
state={"presence": ONLINE}
|
||||||
)
|
)
|
||||||
|
|
||||||
# reactor.iterate(delay=0)
|
# reactor.iterate(delay=0)
|
||||||
@@ -1081,7 +1140,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
# fig goes offline
|
# fig goes offline
|
||||||
yield self.handler.set_state(
|
yield self.handler.set_state(
|
||||||
target_user=self.u_fig, auth_user=self.u_fig,
|
target_user=self.u_fig, auth_user=self.u_fig,
|
||||||
state={"state": OFFLINE}
|
state={"presence": OFFLINE}
|
||||||
)
|
)
|
||||||
|
|
||||||
reactor.iterate(delay=0)
|
reactor.iterate(delay=0)
|
||||||
@@ -1103,7 +1162,8 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
# clementine goes offline
|
# clementine goes offline
|
||||||
yield self.handler.set_state(
|
yield self.handler.set_state(
|
||||||
target_user=self.u_clementine, auth_user=self.u_clementine,
|
target_user=self.u_clementine, auth_user=self.u_clementine,
|
||||||
state={"state": OFFLINE})
|
state={"presence": OFFLINE}
|
||||||
|
)
|
||||||
|
|
||||||
yield put_json.await_calls()
|
yield put_json.await_calls()
|
||||||
|
|
||||||
@@ -1116,11 +1176,12 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
put_json = self.mock_http_client.put_json
|
put_json = self.mock_http_client.put_json
|
||||||
put_json.expect_call_and_return(
|
put_json.expect_call_and_return(
|
||||||
call("remote",
|
call("remote",
|
||||||
path="/matrix/federation/v1/send/1000000/",
|
path="/_matrix/federation/v1/send/1000000/",
|
||||||
data=_expect_edu("remote", "m.presence",
|
data=_expect_edu("remote", "m.presence",
|
||||||
content={
|
content={
|
||||||
"push": [
|
"push": [
|
||||||
{"user_id": "@banana:test",
|
{"user_id": "@banana:test",
|
||||||
|
"presence": "offline",
|
||||||
"state": "offline",
|
"state": "offline",
|
||||||
"status_msg": None},
|
"status_msg": None},
|
||||||
],
|
],
|
||||||
@@ -1131,7 +1192,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
yield self.mock_federation_resource.trigger("PUT",
|
yield self.mock_federation_resource.trigger("PUT",
|
||||||
"/matrix/federation/v1/send/1000000/",
|
"/_matrix/federation/v1/send/1000000/",
|
||||||
_make_edu_json("remote", "m.presence",
|
_make_edu_json("remote", "m.presence",
|
||||||
content={
|
content={
|
||||||
"poll": [ "@banana:test" ],
|
"poll": [ "@banana:test" ],
|
||||||
@@ -1145,7 +1206,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
self.assertTrue(self.u_banana in self.handler._remote_sendmap)
|
self.assertTrue(self.u_banana in self.handler._remote_sendmap)
|
||||||
|
|
||||||
yield self.mock_federation_resource.trigger("PUT",
|
yield self.mock_federation_resource.trigger("PUT",
|
||||||
"/matrix/federation/v1/send/1000001/",
|
"/_matrix/federation/v1/send/1000001/",
|
||||||
_make_edu_json("remote", "m.presence",
|
_make_edu_json("remote", "m.presence",
|
||||||
content={
|
content={
|
||||||
"unpoll": [ "@banana:test" ],
|
"unpoll": [ "@banana:test" ],
|
||||||
|
|||||||
@@ -166,7 +166,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
# TODO(paul): Gut-wrenching
|
# TODO(paul): Gut-wrenching
|
||||||
from synapse.handlers.presence import UserPresenceCache
|
from synapse.handlers.presence import UserPresenceCache
|
||||||
self.handlers.presence_handler._user_cachemap[self.u_apple] = (
|
self.handlers.presence_handler._user_cachemap[self.u_apple] = (
|
||||||
UserPresenceCache())
|
UserPresenceCache()
|
||||||
|
)
|
||||||
|
self.handlers.presence_handler._user_cachemap[self.u_apple].update(
|
||||||
|
{"presence": OFFLINE}, serial=0
|
||||||
|
)
|
||||||
apple_set = self.handlers.presence_handler._local_pushmap.setdefault(
|
apple_set = self.handlers.presence_handler._local_pushmap.setdefault(
|
||||||
"apple", set())
|
"apple", set())
|
||||||
apple_set.add(self.u_banana)
|
apple_set.add(self.u_banana)
|
||||||
@@ -182,11 +186,13 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEquals([
|
self.assertEquals([
|
||||||
{"observed_user": self.u_banana,
|
{"observed_user": self.u_banana,
|
||||||
|
"presence": ONLINE,
|
||||||
"state": ONLINE,
|
"state": ONLINE,
|
||||||
"mtime_age": 0,
|
"last_active_ago": 0,
|
||||||
"displayname": "Frank",
|
"displayname": "Frank",
|
||||||
"avatar_url": "http://foo"},
|
"avatar_url": "http://foo"},
|
||||||
{"observed_user": self.u_clementine,
|
{"observed_user": self.u_clementine,
|
||||||
|
"presence": OFFLINE,
|
||||||
"state": OFFLINE}],
|
"state": OFFLINE}],
|
||||||
presence)
|
presence)
|
||||||
|
|
||||||
@@ -199,8 +205,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
statuscache = self.mock_update_client.call_args[1]["statuscache"]
|
statuscache = self.mock_update_client.call_args[1]["statuscache"]
|
||||||
self.assertEquals({
|
self.assertEquals({
|
||||||
"state": ONLINE,
|
"presence": ONLINE,
|
||||||
"mtime": 1000000, # MockClock
|
"last_active": 1000000, # MockClock
|
||||||
"displayname": "Frank",
|
"displayname": "Frank",
|
||||||
"avatar_url": "http://foo",
|
"avatar_url": "http://foo",
|
||||||
}, statuscache.state)
|
}, statuscache.state)
|
||||||
@@ -222,8 +228,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
statuscache = self.mock_update_client.call_args[1]["statuscache"]
|
statuscache = self.mock_update_client.call_args[1]["statuscache"]
|
||||||
self.assertEquals({
|
self.assertEquals({
|
||||||
"state": ONLINE,
|
"presence": ONLINE,
|
||||||
"mtime": 1000000, # MockClock
|
"last_active": 1000000, # MockClock
|
||||||
"displayname": "I am an Apple",
|
"displayname": "I am an Apple",
|
||||||
"avatar_url": "http://foo",
|
"avatar_url": "http://foo",
|
||||||
}, statuscache.state)
|
}, statuscache.state)
|
||||||
@@ -241,7 +247,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
# TODO(paul): Gut-wrenching
|
# TODO(paul): Gut-wrenching
|
||||||
from synapse.handlers.presence import UserPresenceCache
|
from synapse.handlers.presence import UserPresenceCache
|
||||||
self.handlers.presence_handler._user_cachemap[self.u_apple] = (
|
self.handlers.presence_handler._user_cachemap[self.u_apple] = (
|
||||||
UserPresenceCache())
|
UserPresenceCache()
|
||||||
|
)
|
||||||
|
self.handlers.presence_handler._user_cachemap[self.u_apple].update(
|
||||||
|
{"presence": OFFLINE}, serial=0
|
||||||
|
)
|
||||||
apple_set = self.handlers.presence_handler._remote_sendmap.setdefault(
|
apple_set = self.handlers.presence_handler._remote_sendmap.setdefault(
|
||||||
"apple", set())
|
"apple", set())
|
||||||
apple_set.add(self.u_potato.domain)
|
apple_set.add(self.u_potato.domain)
|
||||||
@@ -255,8 +265,9 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
content={
|
content={
|
||||||
"push": [
|
"push": [
|
||||||
{"user_id": "@apple:test",
|
{"user_id": "@apple:test",
|
||||||
|
"presence": "online",
|
||||||
"state": "online",
|
"state": "online",
|
||||||
"mtime_age": 0,
|
"last_active_ago": 0,
|
||||||
"displayname": "Frank",
|
"displayname": "Frank",
|
||||||
"avatar_url": "http://foo"},
|
"avatar_url": "http://foo"},
|
||||||
],
|
],
|
||||||
@@ -293,14 +304,16 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
statuscache=ANY)
|
statuscache=ANY)
|
||||||
|
|
||||||
statuscache = self.mock_update_client.call_args[1]["statuscache"]
|
statuscache = self.mock_update_client.call_args[1]["statuscache"]
|
||||||
self.assertEquals({"state": ONLINE,
|
self.assertEquals({"presence": ONLINE,
|
||||||
"displayname": "Frank",
|
"displayname": "Frank",
|
||||||
"avatar_url": "http://foo"}, statuscache.state)
|
"avatar_url": "http://foo"}, statuscache.state)
|
||||||
|
|
||||||
state = yield self.handlers.presence_handler.get_state(self.u_potato,
|
state = yield self.handlers.presence_handler.get_state(self.u_potato,
|
||||||
self.u_apple)
|
self.u_apple)
|
||||||
|
|
||||||
self.assertEquals({"state": ONLINE,
|
self.assertEquals(
|
||||||
|
{"presence": ONLINE,
|
||||||
|
"state": ONLINE,
|
||||||
"displayname": "Frank",
|
"displayname": "Frank",
|
||||||
"avatar_url": "http://foo"},
|
"avatar_url": "http://foo"},
|
||||||
state)
|
state)
|
||||||
|
|||||||
@@ -330,6 +330,8 @@ class RoomCreationTest(unittest.TestCase):
|
|||||||
datastore=NonCallableMock(spec_set=[
|
datastore=NonCallableMock(spec_set=[
|
||||||
"store_room",
|
"store_room",
|
||||||
"snapshot_room",
|
"snapshot_room",
|
||||||
|
"persist_event",
|
||||||
|
"get_joined_hosts_for_room",
|
||||||
]),
|
]),
|
||||||
http_client=NonCallableMock(spec_set=[]),
|
http_client=NonCallableMock(spec_set=[]),
|
||||||
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
||||||
@@ -362,6 +364,10 @@ class RoomCreationTest(unittest.TestCase):
|
|||||||
])
|
])
|
||||||
self.room_member_handler = self.handlers.room_member_handler
|
self.room_member_handler = self.handlers.room_member_handler
|
||||||
|
|
||||||
|
def hosts(room):
|
||||||
|
return defer.succeed([])
|
||||||
|
self.datastore.get_joined_hosts_for_room.side_effect = hosts
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_room_creation(self):
|
def test_room_creation(self):
|
||||||
user_id = "@foo:red"
|
user_id = "@foo:red"
|
||||||
@@ -385,9 +391,3 @@ class RoomCreationTest(unittest.TestCase):
|
|||||||
self.assertTrue(self.state_handler.handle_new_event.called)
|
self.assertTrue(self.state_handler.handle_new_event.called)
|
||||||
|
|
||||||
self.assertTrue(self.federation.handle_new_event.called)
|
self.assertTrue(self.federation.handle_new_event.called)
|
||||||
config_event = self.federation.handle_new_event.call_args[0][0]
|
|
||||||
|
|
||||||
self.assertEquals(RoomConfigEvent.TYPE, config_event.type)
|
|
||||||
self.assertEquals(room_id, config_event.room_id)
|
|
||||||
self.assertEquals(user_id, config_event.user_id)
|
|
||||||
self.assertEquals(config, config_event.content)
|
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
|
|||||||
put_json = self.mock_http_client.put_json
|
put_json = self.mock_http_client.put_json
|
||||||
put_json.expect_call_and_return(
|
put_json.expect_call_and_return(
|
||||||
call("farm",
|
call("farm",
|
||||||
path="/matrix/federation/v1/send/1000000/",
|
path="/_matrix/federation/v1/send/1000000/",
|
||||||
data=_expect_edu("farm", "m.typing",
|
data=_expect_edu("farm", "m.typing",
|
||||||
content={
|
content={
|
||||||
"room_id": self.room_id,
|
"room_id": self.room_id,
|
||||||
@@ -192,7 +192,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
|
|||||||
self.room_members = [self.u_apple, self.u_onion]
|
self.room_members = [self.u_apple, self.u_onion]
|
||||||
|
|
||||||
yield self.mock_federation_resource.trigger("PUT",
|
yield self.mock_federation_resource.trigger("PUT",
|
||||||
"/matrix/federation/v1/send/1000000/",
|
"/_matrix/federation/v1/send/1000000/",
|
||||||
_make_edu_json("farm", "m.typing",
|
_make_edu_json("farm", "m.typing",
|
||||||
content={
|
content={
|
||||||
"room_id": self.room_id,
|
"room_id": self.room_id,
|
||||||
@@ -216,7 +216,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
|
|||||||
put_json = self.mock_http_client.put_json
|
put_json = self.mock_http_client.put_json
|
||||||
put_json.expect_call_and_return(
|
put_json.expect_call_and_return(
|
||||||
call("farm",
|
call("farm",
|
||||||
path="/matrix/federation/v1/send/1000000/",
|
path="/_matrix/federation/v1/send/1000000/",
|
||||||
data=_expect_edu("farm", "m.typing",
|
data=_expect_edu("farm", "m.typing",
|
||||||
content={
|
content={
|
||||||
"room_id": self.room_id,
|
"room_id": self.room_id,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ from mock import Mock
|
|||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
logging.getLogger().addHandler(logging.NullHandler())
|
||||||
|
|
||||||
PATH_PREFIX = "/matrix/client/api/v1"
|
PATH_PREFIX = "/_matrix/client/api/v1"
|
||||||
|
|
||||||
|
|
||||||
class EventStreamPaginationApiTestCase(unittest.TestCase):
|
class EventStreamPaginationApiTestCase(unittest.TestCase):
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ ONLINE = PresenceState.ONLINE
|
|||||||
|
|
||||||
|
|
||||||
myid = "@apple:test"
|
myid = "@apple:test"
|
||||||
PATH_PREFIX = "/matrix/client/api/v1"
|
PATH_PREFIX = "/_matrix/client/api/v1"
|
||||||
|
|
||||||
|
|
||||||
class JustPresenceHandlers(object):
|
class JustPresenceHandlers(object):
|
||||||
@@ -98,8 +98,10 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||||||
"/presence/%s/status" % (myid), None)
|
"/presence/%s/status" % (myid), None)
|
||||||
|
|
||||||
self.assertEquals(200, code)
|
self.assertEquals(200, code)
|
||||||
self.assertEquals({"state": ONLINE, "status_msg": "Available"},
|
self.assertEquals(
|
||||||
response)
|
{"presence": ONLINE, "state": ONLINE, "status_msg": "Available"},
|
||||||
|
response
|
||||||
|
)
|
||||||
mocked_get.assert_called_with("apple")
|
mocked_get.assert_called_with("apple")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@@ -109,7 +111,7 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
(code, response) = yield self.mock_resource.trigger("PUT",
|
(code, response) = yield self.mock_resource.trigger("PUT",
|
||||||
"/presence/%s/status" % (myid),
|
"/presence/%s/status" % (myid),
|
||||||
'{"state": "unavailable", "status_msg": "Away"}')
|
'{"presence": "unavailable", "status_msg": "Away"}')
|
||||||
|
|
||||||
self.assertEquals(200, code)
|
self.assertEquals(200, code)
|
||||||
mocked_set.assert_called_with("apple",
|
mocked_set.assert_called_with("apple",
|
||||||
@@ -173,9 +175,9 @@ class PresenceListTestCase(unittest.TestCase):
|
|||||||
"/presence/list/%s" % (myid), None)
|
"/presence/list/%s" % (myid), None)
|
||||||
|
|
||||||
self.assertEquals(200, code)
|
self.assertEquals(200, code)
|
||||||
self.assertEquals(
|
self.assertEquals([
|
||||||
[{"user_id": "@banana:test", "state": OFFLINE}], response
|
{"user_id": "@banana:test", "presence": OFFLINE, "state": OFFLINE},
|
||||||
)
|
], response)
|
||||||
|
|
||||||
self.datastore.get_presence_list.assert_called_with(
|
self.datastore.get_presence_list.assert_called_with(
|
||||||
"apple", accepted=True
|
"apple", accepted=True
|
||||||
@@ -229,7 +231,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
|||||||
# HIDEOUS HACKERY
|
# HIDEOUS HACKERY
|
||||||
# TODO(paul): This should be injected in via the HomeServer DI system
|
# TODO(paul): This should be injected in via the HomeServer DI system
|
||||||
from synapse.streams.events import (
|
from synapse.streams.events import (
|
||||||
PresenceSource, NullSource, EventSources
|
PresenceEventSource, NullSource, EventSources
|
||||||
)
|
)
|
||||||
|
|
||||||
old_SOURCE_TYPES = EventSources.SOURCE_TYPES
|
old_SOURCE_TYPES = EventSources.SOURCE_TYPES
|
||||||
@@ -240,7 +242,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
|||||||
EventSources.SOURCE_TYPES = {
|
EventSources.SOURCE_TYPES = {
|
||||||
k: NullSource for k in old_SOURCE_TYPES.keys()
|
k: NullSource for k in old_SOURCE_TYPES.keys()
|
||||||
}
|
}
|
||||||
EventSources.SOURCE_TYPES["presence"] = PresenceSource
|
EventSources.SOURCE_TYPES["presence"] = PresenceEventSource
|
||||||
|
|
||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
@@ -274,6 +276,15 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
|||||||
lambda u: defer.succeed([]))
|
lambda u: defer.succeed([]))
|
||||||
|
|
||||||
self.mock_datastore = hs.get_datastore()
|
self.mock_datastore = hs.get_datastore()
|
||||||
|
|
||||||
|
def get_profile_displayname(user_id):
|
||||||
|
return defer.succeed("Frank")
|
||||||
|
self.mock_datastore.get_profile_displayname = get_profile_displayname
|
||||||
|
|
||||||
|
def get_profile_avatar_url(user_id):
|
||||||
|
return defer.succeed(None)
|
||||||
|
self.mock_datastore.get_profile_avatar_url = get_profile_avatar_url
|
||||||
|
|
||||||
self.presence = hs.get_handlers().presence_handler
|
self.presence = hs.get_handlers().presence_handler
|
||||||
|
|
||||||
self.u_apple = hs.parse_userid("@apple:test")
|
self.u_apple = hs.parse_userid("@apple:test")
|
||||||
@@ -295,7 +306,9 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
|||||||
# all be ours
|
# all be ours
|
||||||
|
|
||||||
# I'll already get my own presence state change
|
# I'll already get my own presence state change
|
||||||
self.assertEquals({"start": "0_1", "end": "0_1", "chunk": []}, response)
|
self.assertEquals({"start": "0_1_0", "end": "0_1_0", "chunk": []},
|
||||||
|
response
|
||||||
|
)
|
||||||
|
|
||||||
self.mock_datastore.set_presence_state.return_value = defer.succeed(
|
self.mock_datastore.set_presence_state.return_value = defer.succeed(
|
||||||
{"state": ONLINE})
|
{"state": ONLINE})
|
||||||
@@ -303,17 +316,20 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
|||||||
[])
|
[])
|
||||||
|
|
||||||
yield self.presence.set_state(self.u_banana, self.u_banana,
|
yield self.presence.set_state(self.u_banana, self.u_banana,
|
||||||
state={"state": ONLINE})
|
state={"presence": ONLINE}
|
||||||
|
)
|
||||||
|
|
||||||
(code, response) = yield self.mock_resource.trigger("GET",
|
(code, response) = yield self.mock_resource.trigger("GET",
|
||||||
"/events?from=0_1&timeout=0", None)
|
"/events?from=0_1_0&timeout=0", None)
|
||||||
|
|
||||||
self.assertEquals(200, code)
|
self.assertEquals(200, code)
|
||||||
self.assertEquals({"start": "0_1", "end": "0_2", "chunk": [
|
self.assertEquals({"start": "0_1_0", "end": "0_2_0", "chunk": [
|
||||||
{"type": "m.presence",
|
{"type": "m.presence",
|
||||||
"content": {
|
"content": {
|
||||||
"user_id": "@banana:test",
|
"user_id": "@banana:test",
|
||||||
|
"presence": ONLINE,
|
||||||
"state": ONLINE,
|
"state": ONLINE,
|
||||||
"mtime_age": 0,
|
"displayname": "Frank",
|
||||||
|
"last_active_ago": 0,
|
||||||
}},
|
}},
|
||||||
]}, response)
|
]}, response)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ from synapse.api.errors import SynapseError, AuthError
|
|||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
myid = "@1234ABCD:test"
|
myid = "@1234ABCD:test"
|
||||||
PATH_PREFIX = "/matrix/client/api/v1"
|
PATH_PREFIX = "/_matrix/client/api/v1"
|
||||||
|
|
||||||
class ProfileTestCase(unittest.TestCase):
|
class ProfileTestCase(unittest.TestCase):
|
||||||
""" Tests profile management. """
|
""" Tests profile management. """
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ from .utils import RestTestCase
|
|||||||
|
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
|
|
||||||
PATH_PREFIX = "/matrix/client/api/v1"
|
PATH_PREFIX = "/_matrix/client/api/v1"
|
||||||
|
|
||||||
|
|
||||||
class RoomPermissionsTestCase(RestTestCase):
|
class RoomPermissionsTestCase(RestTestCase):
|
||||||
@@ -51,7 +51,7 @@ class RoomPermissionsTestCase(RestTestCase):
|
|||||||
persistence_service.get_latest_pdus_in_context.return_value = []
|
persistence_service.get_latest_pdus_in_context.return_value = []
|
||||||
|
|
||||||
hs = HomeServer(
|
hs = HomeServer(
|
||||||
"test",
|
"red",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
datastore=MemoryDataStore(),
|
datastore=MemoryDataStore(),
|
||||||
@@ -398,7 +398,7 @@ class RoomsMemberListTestCase(RestTestCase):
|
|||||||
persistence_service.get_latest_pdus_in_context.return_value = []
|
persistence_service.get_latest_pdus_in_context.return_value = []
|
||||||
|
|
||||||
hs = HomeServer(
|
hs = HomeServer(
|
||||||
"test",
|
"red",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
datastore=MemoryDataStore(),
|
datastore=MemoryDataStore(),
|
||||||
@@ -476,7 +476,7 @@ class RoomsCreateTestCase(RestTestCase):
|
|||||||
persistence_service.get_latest_pdus_in_context.return_value = []
|
persistence_service.get_latest_pdus_in_context.return_value = []
|
||||||
|
|
||||||
hs = HomeServer(
|
hs = HomeServer(
|
||||||
"test",
|
"red",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
datastore=MemoryDataStore(),
|
datastore=MemoryDataStore(),
|
||||||
@@ -566,7 +566,7 @@ class RoomTopicTestCase(RestTestCase):
|
|||||||
persistence_service.get_latest_pdus_in_context.return_value = []
|
persistence_service.get_latest_pdus_in_context.return_value = []
|
||||||
|
|
||||||
hs = HomeServer(
|
hs = HomeServer(
|
||||||
"test",
|
"red",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
datastore=MemoryDataStore(),
|
datastore=MemoryDataStore(),
|
||||||
@@ -669,7 +669,7 @@ class RoomMemberStateTestCase(RestTestCase):
|
|||||||
persistence_service.get_latest_pdus_in_context.return_value = []
|
persistence_service.get_latest_pdus_in_context.return_value = []
|
||||||
|
|
||||||
hs = HomeServer(
|
hs = HomeServer(
|
||||||
"test",
|
"red",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
datastore=MemoryDataStore(),
|
datastore=MemoryDataStore(),
|
||||||
@@ -794,7 +794,7 @@ class RoomMessagesTestCase(RestTestCase):
|
|||||||
persistence_service.get_latest_pdus_in_context.return_value = []
|
persistence_service.get_latest_pdus_in_context.return_value = []
|
||||||
|
|
||||||
hs = HomeServer(
|
hs = HomeServer(
|
||||||
"test",
|
"red",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
datastore=MemoryDataStore(),
|
datastore=MemoryDataStore(),
|
||||||
|
|||||||
@@ -188,8 +188,9 @@ class MemoryDataStore(object):
|
|||||||
|
|
||||||
def get_rooms_for_user_where_membership_is(self, user_id, membership_list):
|
def get_rooms_for_user_where_membership_is(self, user_id, membership_list):
|
||||||
return [
|
return [
|
||||||
r for r in self.members
|
self.members[r].get(user_id) for r in self.members
|
||||||
if self.members[r].get(user_id).membership in membership_list
|
if user_id in self.members[r] and
|
||||||
|
self.members[r][user_id].membership in membership_list
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_room_events_stream(self, user_id=None, from_key=None, to_key=None,
|
def get_room_events_stream(self, user_id=None, from_key=None, to_key=None,
|
||||||
@@ -233,6 +234,23 @@ class MemoryDataStore(object):
|
|||||||
def get_room_events_max_id(self):
|
def get_room_events_max_id(self):
|
||||||
return 0 # TODO (erikj)
|
return 0 # TODO (erikj)
|
||||||
|
|
||||||
|
def get_send_event_level(self, room_id):
|
||||||
|
return defer.succeed(0)
|
||||||
|
|
||||||
|
def get_power_level(self, room_id, user_id):
|
||||||
|
return defer.succeed(0)
|
||||||
|
|
||||||
|
def get_add_state_level(self, room_id):
|
||||||
|
return defer.succeed(0)
|
||||||
|
|
||||||
|
def get_room_join_rule(self, room_id):
|
||||||
|
# TODO (erikj): This should be configurable
|
||||||
|
return defer.succeed("invite")
|
||||||
|
|
||||||
|
def get_ops_levels(self, room_id):
|
||||||
|
return defer.succeed((5, 5))
|
||||||
|
|
||||||
|
|
||||||
def _format_call(args, kwargs):
|
def _format_call(args, kwargs):
|
||||||
return ", ".join(
|
return ", ".join(
|
||||||
["%r" % (a) for a in args] +
|
["%r" % (a) for a in args] +
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ limitations under the License.
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
|
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
|
||||||
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService',
|
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService', 'matrixPhoneService',
|
||||||
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService) {
|
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService, matrixPhoneService) {
|
||||||
|
|
||||||
// Check current URL to avoid to display the logout button on the login page
|
// Check current URL to avoid to display the logout button on the login page
|
||||||
$scope.location = $location.path();
|
$scope.location = $location.path();
|
||||||
@@ -37,6 +37,12 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
|
|||||||
mPresence.start();
|
mPresence.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$scope.user_id;
|
||||||
|
var config = matrixService.config();
|
||||||
|
if (config) {
|
||||||
|
$scope.user_id = matrixService.config().user_id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a given page.
|
* Open a given page.
|
||||||
* @param {String} url url of the page
|
* @param {String} url url of the page
|
||||||
@@ -45,6 +51,16 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
|
|||||||
$location.url(url);
|
$location.url(url);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Open the given user profile page
|
||||||
|
$scope.goToUserPage = function(user_id) {
|
||||||
|
if (user_id === $scope.user_id) {
|
||||||
|
$location.url("/settings");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$location.url("/user/" + user_id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Logs the user out
|
// Logs the user out
|
||||||
$scope.logout = function() {
|
$scope.logout = function() {
|
||||||
|
|
||||||
@@ -69,13 +85,30 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
|
|||||||
$scope.logout();
|
$scope.logout();
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.requestNotifications = function() {
|
$scope.updateHeader = function() {
|
||||||
if (window.Notification) {
|
$scope.user_id = matrixService.config().user_id;
|
||||||
console.log("Notification.permission: " + window.Notification.permission);
|
|
||||||
window.Notification.requestPermission(function(){});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
|
||||||
|
console.trace("incoming call");
|
||||||
|
call.onError = $scope.onCallError;
|
||||||
|
call.onHangup = $scope.onCallHangup;
|
||||||
|
$rootScope.currentCall = call;
|
||||||
|
});
|
||||||
|
|
||||||
|
$scope.answerCall = function() {
|
||||||
|
$scope.currentCall.answer();
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.hangupCall = function() {
|
||||||
|
$scope.currentCall.hangup();
|
||||||
|
$scope.currentCall = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
$rootScope.onCallError = function(errStr) {
|
||||||
|
$scope.feedback = errStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootScope.onCallHangup = function() {
|
||||||
|
}
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,12 @@ angular.module('matrixWebClient')
|
|||||||
.directive('ngFocus', ['$timeout', function($timeout) {
|
.directive('ngFocus', ['$timeout', function($timeout) {
|
||||||
return {
|
return {
|
||||||
link: function(scope, element, attr) {
|
link: function(scope, element, attr) {
|
||||||
|
// XXX: slightly evil hack to disable autofocus on iOS, as in general
|
||||||
|
// it causes more problems than it fixes, by bouncing the page
|
||||||
|
// around
|
||||||
|
if (!/(iPad|iPhone|iPod)/g.test(navigator.userAgent)) {
|
||||||
$timeout(function() { element[0].focus(); }, 0);
|
$timeout(function() { element[0].focus(); }, 0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
@@ -70,7 +70,7 @@ angular.module('matrixWebClient')
|
|||||||
});
|
});
|
||||||
|
|
||||||
filtered.sort(function (a, b) {
|
filtered.sort(function (a, b) {
|
||||||
return ((a["mtime_age"] || 10e10) > (b["mtime_age"] || 10e10) ? 1 : -1);
|
return ((a["last_active_ago"] || 10e10) > (b["last_active_ago"] || 10e10) ? 1 : -1);
|
||||||
});
|
});
|
||||||
return filtered;
|
return filtered;
|
||||||
};
|
};
|
||||||
@@ -79,4 +79,43 @@ angular.module('matrixWebClient')
|
|||||||
return function(text) {
|
return function(text) {
|
||||||
return $sce.trustAsHtml(text);
|
return $sce.trustAsHtml(text);
|
||||||
};
|
};
|
||||||
|
}])
|
||||||
|
|
||||||
|
// Compute the room name according to information we have
|
||||||
|
.filter('roomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) {
|
||||||
|
return function(room_id) {
|
||||||
|
var roomName;
|
||||||
|
|
||||||
|
// If there is an alias, use it
|
||||||
|
// TODO: only one alias is managed for now
|
||||||
|
var alias = matrixService.getRoomIdToAliasMapping(room_id);
|
||||||
|
if (alias) {
|
||||||
|
roomName = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefined === roomName) {
|
||||||
|
// Else, build the name from its users
|
||||||
|
var room = $rootScope.events.rooms[room_id];
|
||||||
|
if (room) {
|
||||||
|
if (room.members) {
|
||||||
|
// Limit the room renaming to 1:1 room
|
||||||
|
if (2 === Object.keys(room.members).length) {
|
||||||
|
for (var i in room.members) {
|
||||||
|
var member = room.members[i];
|
||||||
|
if (member.user_id !== matrixService.config().user_id) {
|
||||||
|
roomName = member.content.displayname ? member.content.displayname : member.user_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefined === roomName) {
|
||||||
|
// By default, use the room ID
|
||||||
|
roomName = room_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return roomName;
|
||||||
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|||||||
332
webclient/app.css
Normal file → Executable file
332
webclient/app.css
Normal file → Executable file
@@ -1,121 +1,198 @@
|
|||||||
/*** Mobile voodoo ***/
|
/** Common layout **/
|
||||||
@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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
html {
|
||||||
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
|
height: 100%;
|
||||||
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
|
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
|
||||||
font-size: 12pt;
|
font-size: 12pt;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-family: Helvetica, Arial, sans-serif;
|
font-size: 20pt;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** Overall page layout ***/
|
a:link { color: #666; }
|
||||||
|
a:visited { color: #666; }
|
||||||
|
a:hover { color: #000; }
|
||||||
|
a:active { color: #000; }
|
||||||
|
|
||||||
#page {
|
#page {
|
||||||
position: absolute;
|
min-height: 100%;
|
||||||
top: 80px;
|
margin-bottom: -32px; /* to make room for the footer */
|
||||||
bottom: 100px;
|
|
||||||
left: 0px;
|
|
||||||
right: 0px;
|
|
||||||
margin: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#wrapper {
|
#wrapper {
|
||||||
|
margin: auto;
|
||||||
|
max-width: 1280px;
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#header
|
||||||
|
{
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: #333;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#callBar {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#headerContent {
|
||||||
|
color: #ccc;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: auto;
|
||||||
|
text-align: right;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#headerContent a:link,
|
||||||
|
#headerContent a:visited,
|
||||||
|
#headerContent a:hover,
|
||||||
|
#headerContent a:active {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footer
|
||||||
|
{
|
||||||
|
width: 100%;
|
||||||
|
border-top: #666 1px solid;
|
||||||
|
background-color: #aaa;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#footerContent
|
||||||
|
{
|
||||||
|
font-size: 8pt;
|
||||||
|
color: #fff;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: auto;
|
||||||
|
text-align: center;
|
||||||
|
height: 32px;
|
||||||
|
line-height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#genericHeading
|
||||||
|
{
|
||||||
|
margin-top: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#feedback {
|
||||||
|
color: #800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mouse-pointer {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.invited {
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Login Pages ***/
|
||||||
|
|
||||||
|
.loginWrapper {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginForm {
|
||||||
|
text-align: left;
|
||||||
|
padding: 1em;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
-webkit-border-radius: 10px;
|
||||||
|
-moz-border-radius: 10px;
|
||||||
|
border-radius: 10px;
|
||||||
|
|
||||||
|
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
|
||||||
|
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
|
||||||
|
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.15);
|
||||||
|
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
border: 1px #ccc solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginForm input[type='radio'] {
|
||||||
|
margin-right: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#serverConfig {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#serverConfig,
|
||||||
|
#serverConfig input,
|
||||||
|
#serverConfig button
|
||||||
|
{
|
||||||
|
font-size: 10pt ! important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smallPrint {
|
||||||
|
color: #888;
|
||||||
|
font-size: 9pt ! important;
|
||||||
|
font-style: italic ! important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#serverConfig label {
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
width: 7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loginForm,
|
||||||
|
#loginForm input,
|
||||||
|
#loginForm button,
|
||||||
|
#loginForm select {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*** Room page ***/
|
||||||
|
|
||||||
|
#roomPage {
|
||||||
|
position: absolute;
|
||||||
|
top: 120px;
|
||||||
|
bottom: 120px;
|
||||||
|
left: 20px;
|
||||||
|
right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#roomWrapper {
|
||||||
margin: auto;
|
margin: auto;
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#roomName {
|
#roomName {
|
||||||
max-width: 1280px;
|
float: right;
|
||||||
width: 100%;
|
|
||||||
text-align: right;
|
|
||||||
top: -40px;
|
|
||||||
position: absolute;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#roomHeader {
|
||||||
|
margin: auto;
|
||||||
|
padding-left: 20px;
|
||||||
|
padding-right: 20px;
|
||||||
|
padding-top: 53px;
|
||||||
|
max-width: 1280px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#controlPanel {
|
#controlPanel {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0px;
|
bottom: 0px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
height: 100px;
|
||||||
background-color: #f8f8f8;
|
background-color: #f8f8f8;
|
||||||
border-top: #aaa 1px solid;
|
border-top: #aaa 1px solid;
|
||||||
}
|
}
|
||||||
@@ -146,10 +223,6 @@ h1 {
|
|||||||
background-color: #faa;
|
background-color: #faa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mouse-pointer {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** Participant list ***/
|
/*** Participant list ***/
|
||||||
|
|
||||||
#usersTableWrapper {
|
#usersTableWrapper {
|
||||||
@@ -195,6 +268,13 @@ h1 {
|
|||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.userAvatar .userPowerLevel {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
height: 1px;
|
||||||
|
background-color: red;
|
||||||
|
}
|
||||||
|
|
||||||
.userPresence {
|
.userPresence {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -300,7 +380,7 @@ h1 {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
max-width: 90%;
|
max-width: 90%;
|
||||||
font-size: 16px;
|
font-size: 14px;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
padding-top: 7px;
|
padding-top: 7px;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
@@ -310,6 +390,11 @@ h1 {
|
|||||||
-webkit-text-size-adjust:100%
|
-webkit-text-size-adjust:100%
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bubble img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
.differentUser td {
|
.differentUser td {
|
||||||
padding-bottom: 5px ! important;
|
padding-bottom: 5px ! important;
|
||||||
}
|
}
|
||||||
@@ -341,8 +426,8 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#room-fullscreen-image img {
|
#room-fullscreen-image img {
|
||||||
max-width: 100%;
|
max-width: 90%;
|
||||||
max-height: 100%;
|
max-height: 90%;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@@ -350,9 +435,14 @@ h1 {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
|
||||||
|
-webkit-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
|
||||||
|
-moz-box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
|
||||||
|
box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** Recents ***/
|
/*** Recents ***/
|
||||||
|
|
||||||
.recentsTable {
|
.recentsTable {
|
||||||
max-width: 480px;
|
max-width: 480px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -402,11 +492,14 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*** Recents in the room page ***/
|
/*** Recents in the room page ***/
|
||||||
|
|
||||||
#roomRecentsTableWrapper {
|
#roomRecentsTableWrapper {
|
||||||
float: left;
|
float: left;
|
||||||
max-width: 320px;
|
max-width: 320px;
|
||||||
margin-right: 20px;
|
padding-right: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
border-right: 1px solid #ddd;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,55 +514,14 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.profile-avatar img {
|
.profile-avatar img {
|
||||||
max-width: 100%;
|
width: 100%;
|
||||||
max-height: 100%;
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** User profile page ***/
|
/*** User profile page ***/
|
||||||
#user-ids {
|
|
||||||
padding-left: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
#user-displayname {
|
#user-displayname {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
}
|
}
|
||||||
/******************************/
|
|
||||||
|
|
||||||
#header
|
|
||||||
{
|
|
||||||
padding: 20px;
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#logo,
|
|
||||||
#roomLogo {
|
|
||||||
max-width: 1280px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#header-buttons {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text_entry_section {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
z-index: 100;
|
|
||||||
left: 0;
|
|
||||||
right: 10em;
|
|
||||||
width: 100%;
|
|
||||||
background: #e0e0e0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member_invited {
|
|
||||||
color: blue;
|
|
||||||
}
|
|
||||||
|
|
||||||
.member_joined {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.member_left {
|
|
||||||
color: gray;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ var matrixWebClient = angular.module('matrixWebClient', [
|
|||||||
'ngRoute',
|
'ngRoute',
|
||||||
'MatrixWebClientController',
|
'MatrixWebClientController',
|
||||||
'LoginController',
|
'LoginController',
|
||||||
|
'RegisterController',
|
||||||
'RoomController',
|
'RoomController',
|
||||||
'HomeController',
|
'HomeController',
|
||||||
'RecentsController',
|
'RecentsController',
|
||||||
@@ -35,30 +36,27 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
|
|||||||
function($routeProvider, $provide, $httpProvider) {
|
function($routeProvider, $provide, $httpProvider) {
|
||||||
$routeProvider.
|
$routeProvider.
|
||||||
when('/login', {
|
when('/login', {
|
||||||
templateUrl: 'login/login.html',
|
templateUrl: 'login/login.html'
|
||||||
controller: 'LoginController'
|
}).
|
||||||
|
when('/register', {
|
||||||
|
templateUrl: 'login/register.html'
|
||||||
}).
|
}).
|
||||||
when('/room/:room_id_or_alias', {
|
when('/room/:room_id_or_alias', {
|
||||||
templateUrl: 'room/room.html',
|
templateUrl: 'room/room.html'
|
||||||
controller: 'RoomController'
|
|
||||||
}).
|
}).
|
||||||
when('/room/', { // room URL with room alias in it (ex: http://127.0.0.1:8000/#/room/#public:localhost:8080) will come here.
|
when('/room/', { // room URL with room alias in it (ex: http://127.0.0.1:8000/#/room/#public:localhost:8080) will come here.
|
||||||
// The reason is that 2nd hash key breaks routeProvider parameters cutting so that the URL will not match with
|
// The reason is that 2nd hash key breaks routeProvider parameters cutting so that the URL will not match with
|
||||||
// the previous '/room/:room_id_or_alias' URL rule
|
// the previous '/room/:room_id_or_alias' URL rule
|
||||||
templateUrl: 'room/room.html',
|
templateUrl: 'room/room.html'
|
||||||
controller: 'RoomController'
|
|
||||||
}).
|
}).
|
||||||
when('/', {
|
when('/', {
|
||||||
templateUrl: 'home/home.html',
|
templateUrl: 'home/home.html'
|
||||||
controller: 'HomeController'
|
|
||||||
}).
|
}).
|
||||||
when('/settings', {
|
when('/settings', {
|
||||||
templateUrl: 'settings/settings.html',
|
templateUrl: 'settings/settings.html'
|
||||||
controller: 'SettingsController'
|
|
||||||
}).
|
}).
|
||||||
when('/user/:user_matrix_id', {
|
when('/user/:user_matrix_id', {
|
||||||
templateUrl: 'user/user.html',
|
templateUrl: 'user/user.html'
|
||||||
controller: 'UserController'
|
|
||||||
}).
|
}).
|
||||||
otherwise({
|
otherwise({
|
||||||
redirectTo: '/'
|
redirectTo: '/'
|
||||||
@@ -84,7 +82,10 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
|
|||||||
matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) {
|
matrixWebClient.run(['$location', 'matrixService', function($location, matrixService) {
|
||||||
|
|
||||||
// If user auth details are not in cache, go to the login page
|
// If user auth details are not in cache, go to the login page
|
||||||
if (!matrixService.isUserLoggedIn()) {
|
if (!matrixService.isUserLoggedIn() &&
|
||||||
|
$location.path() !== "/login" &&
|
||||||
|
$location.path() !== "/register")
|
||||||
|
{
|
||||||
$location.path("login");
|
$location.path("login");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ angular.module('mFileUpload', ['matrixService', 'mUtilities'])
|
|||||||
*/
|
*/
|
||||||
this.uploadFile = function(file) {
|
this.uploadFile = function(file) {
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
console.log("Uploading " + file.name + "... to /matrix/content");
|
console.log("Uploading " + file.name + "... to /_matrix/content");
|
||||||
matrixService.uploadContent(file).then(
|
matrixService.uploadContent(file).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
var content_url = response.data.content_token;
|
var content_url = response.data.content_token;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ if typically all the $on method would do is update its own $scope.
|
|||||||
*/
|
*/
|
||||||
angular.module('eventHandlerService', [])
|
angular.module('eventHandlerService', [])
|
||||||
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
|
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
|
||||||
|
var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
|
||||||
var MSG_EVENT = "MSG_EVENT";
|
var MSG_EVENT = "MSG_EVENT";
|
||||||
var MEMBER_EVENT = "MEMBER_EVENT";
|
var MEMBER_EVENT = "MEMBER_EVENT";
|
||||||
var PRESENCE_EVENT = "PRESENCE_EVENT";
|
var PRESENCE_EVENT = "PRESENCE_EVENT";
|
||||||
@@ -48,7 +49,7 @@ angular.module('eventHandlerService', [])
|
|||||||
$rootScope.events.rooms[room_id].messages = [];
|
$rootScope.events.rooms[room_id].messages = [];
|
||||||
$rootScope.events.rooms[room_id].members = {};
|
$rootScope.events.rooms[room_id].members = {};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
var resetRoomMessages = function(room_id) {
|
var resetRoomMessages = function(room_id) {
|
||||||
if ($rootScope.events.rooms[room_id]) {
|
if ($rootScope.events.rooms[room_id]) {
|
||||||
@@ -56,6 +57,13 @@ angular.module('eventHandlerService', [])
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var handleRoomCreate = function(event, isLiveEvent) {
|
||||||
|
initRoom(event.room_id);
|
||||||
|
|
||||||
|
// For now, we do not use the event data. Simply signal it to the app controllers
|
||||||
|
$rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent);
|
||||||
|
};
|
||||||
|
|
||||||
var handleMessage = function(event, isLiveEvent) {
|
var handleMessage = function(event, isLiveEvent) {
|
||||||
initRoom(event.room_id);
|
initRoom(event.room_id);
|
||||||
|
|
||||||
@@ -96,11 +104,21 @@ angular.module('eventHandlerService', [])
|
|||||||
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
|
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var handlePowerLevels = function(event, isLiveEvent) {
|
||||||
|
initRoom(event.room_id);
|
||||||
|
|
||||||
|
$rootScope.events.rooms[event.room_id][event.type] = event;
|
||||||
|
|
||||||
|
//TODO
|
||||||
|
//$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
|
||||||
|
};
|
||||||
|
|
||||||
var handleCallEvent = function(event, isLiveEvent) {
|
var handleCallEvent = function(event, isLiveEvent) {
|
||||||
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
|
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
|
||||||
MSG_EVENT: MSG_EVENT,
|
MSG_EVENT: MSG_EVENT,
|
||||||
MEMBER_EVENT: MEMBER_EVENT,
|
MEMBER_EVENT: MEMBER_EVENT,
|
||||||
PRESENCE_EVENT: PRESENCE_EVENT,
|
PRESENCE_EVENT: PRESENCE_EVENT,
|
||||||
@@ -109,6 +127,9 @@ angular.module('eventHandlerService', [])
|
|||||||
|
|
||||||
handleEvent: function(event, isLiveEvent) {
|
handleEvent: function(event, isLiveEvent) {
|
||||||
switch(event.type) {
|
switch(event.type) {
|
||||||
|
case "m.room.create":
|
||||||
|
handleRoomCreate(event, isLiveEvent);
|
||||||
|
break;
|
||||||
case "m.room.message":
|
case "m.room.message":
|
||||||
handleMessage(event, isLiveEvent);
|
handleMessage(event, isLiveEvent);
|
||||||
break;
|
break;
|
||||||
@@ -118,11 +139,20 @@ angular.module('eventHandlerService', [])
|
|||||||
case "m.presence":
|
case "m.presence":
|
||||||
handlePresence(event, isLiveEvent);
|
handlePresence(event, isLiveEvent);
|
||||||
break;
|
break;
|
||||||
|
case 'm.room.ops_levels':
|
||||||
|
case 'm.room.send_event_level':
|
||||||
|
case 'm.room.add_state_level':
|
||||||
|
case 'm.room.join_rules':
|
||||||
|
case 'm.room.power_levels':
|
||||||
|
handlePowerLevels(event, isLiveEvent);
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
console.log("Unable to handle event type " + event.type);
|
console.log("Unable to handle event type " + event.type);
|
||||||
|
console.log(JSON.stringify(event, undefined, 4));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (event.type.indexOf('m.call.') == 0) {
|
if (event.type.indexOf('m.call.') === 0) {
|
||||||
handleCallEvent(event, isLiveEvent);
|
handleCallEvent(event, isLiveEvent);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ var forAllTracksOnStream = function(s, f) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
angular.module('MatrixCall', [])
|
angular.module('MatrixCall', [])
|
||||||
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', function MatrixCallFactory(matrixService, matrixPhoneService) {
|
.factory('MatrixCall', ['matrixService', 'matrixPhoneService', '$rootScope', function MatrixCallFactory(matrixService, matrixPhoneService, $rootScope) {
|
||||||
var MatrixCall = function(room_id) {
|
var MatrixCall = function(room_id) {
|
||||||
this.room_id = room_id;
|
this.room_id = room_id;
|
||||||
this.call_id = "c" + new Date().getTime();
|
this.call_id = "c" + new Date().getTime();
|
||||||
@@ -73,9 +73,7 @@ angular.module('MatrixCall', [])
|
|||||||
this.state = 'wait_local_media';
|
this.state = 'wait_local_media';
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixCall.prototype.hangup = function() {
|
MatrixCall.prototype.stopAllMedia = function() {
|
||||||
console.trace("Ending call "+this.call_id);
|
|
||||||
|
|
||||||
if (this.localAVStream) {
|
if (this.localAVStream) {
|
||||||
forAllTracksOnStream(this.localAVStream, function(t) {
|
forAllTracksOnStream(this.localAVStream, function(t) {
|
||||||
t.stop();
|
t.stop();
|
||||||
@@ -86,6 +84,12 @@ angular.module('MatrixCall', [])
|
|||||||
t.stop();
|
t.stop();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
MatrixCall.prototype.hangup = function() {
|
||||||
|
console.trace("Ending call "+this.call_id);
|
||||||
|
|
||||||
|
this.stopAllMedia();
|
||||||
|
|
||||||
var content = {
|
var content = {
|
||||||
version: 0,
|
version: 0,
|
||||||
@@ -204,6 +208,7 @@ angular.module('MatrixCall', [])
|
|||||||
// ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet
|
// 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') {
|
if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
|
||||||
this.state = 'connected';
|
this.state = 'connected';
|
||||||
|
$rootScope.$apply();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -232,8 +237,9 @@ angular.module('MatrixCall', [])
|
|||||||
t.onstarted = self.onRemoteStreamTrackStarted;
|
t.onstarted = self.onRemoteStreamTrackStarted;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
event.stream.onended = function(e) { self.onRemoteStreamEnded(e); };
|
||||||
// not currently implemented in chrome
|
// not currently implemented in chrome
|
||||||
event.stream.onstarted = this.onRemoteStreamStarted;
|
event.stream.onstarted = function(e) { self.onRemoteStreamStarted(e); };
|
||||||
var player = new Audio();
|
var player = new Audio();
|
||||||
player.src = URL.createObjectURL(s);
|
player.src = URL.createObjectURL(s);
|
||||||
player.play();
|
player.play();
|
||||||
@@ -243,24 +249,19 @@ angular.module('MatrixCall', [])
|
|||||||
this.state = 'connected';
|
this.state = 'connected';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
MatrixCall.prototype.onRemoteStreamEnded = function(event) {
|
||||||
|
this.state = 'ended';
|
||||||
|
this.stopAllMedia();
|
||||||
|
this.onHangup();
|
||||||
|
};
|
||||||
|
|
||||||
MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) {
|
MatrixCall.prototype.onRemoteStreamTrackStarted = function(event) {
|
||||||
this.state = 'connected';
|
this.state = 'connected';
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixCall.prototype.onHangupReceived = function() {
|
MatrixCall.prototype.onHangupReceived = function() {
|
||||||
this.state = 'ended';
|
this.state = 'ended';
|
||||||
|
this.stopAllMedia();
|
||||||
if (this.localAVStream) {
|
|
||||||
forAllTracksOnStream(this.localAVStream, function(t) {
|
|
||||||
t.stop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (this.remoteAVStream) {
|
|
||||||
forAllTracksOnStream(this.remoteAVStream, function(t) {
|
|
||||||
t.stop();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.onHangup();
|
this.onHangup();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ angular.module('matrixService', [])
|
|||||||
|
|
||||||
// Current version of permanent storage
|
// Current version of permanent storage
|
||||||
var configVersion = 0;
|
var configVersion = 0;
|
||||||
var prefixPath = "/matrix/client/api/v1";
|
var prefixPath = "/_matrix/client/api/v1";
|
||||||
var MAPPING_PREFIX = "alias_for_";
|
var MAPPING_PREFIX = "alias_for_";
|
||||||
|
|
||||||
var doRequest = function(method, path, params, data, $httpParams) {
|
var doRequest = function(method, path, params, data, $httpParams) {
|
||||||
@@ -95,14 +95,18 @@ angular.module('matrixService', [])
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Create a room
|
// Create a room
|
||||||
create: function(room_id, visibility) {
|
create: function(room_alias, visibility) {
|
||||||
// The REST path spec
|
// The REST path spec
|
||||||
var path = "/createRoom";
|
var path = "/createRoom";
|
||||||
|
|
||||||
return doRequest("POST", path, undefined, {
|
var req = {
|
||||||
visibility: visibility,
|
"visibility": visibility
|
||||||
room_alias_name: room_id
|
};
|
||||||
});
|
if (room_alias) {
|
||||||
|
req.room_alias_name = room_alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
return doRequest("POST", path, undefined, req);
|
||||||
},
|
},
|
||||||
|
|
||||||
// List all rooms joined or been invited to
|
// List all rooms joined or been invited to
|
||||||
@@ -164,7 +168,7 @@ angular.module('matrixService', [])
|
|||||||
|
|
||||||
// Retrieves the room ID corresponding to a room alias
|
// Retrieves the room ID corresponding to a room alias
|
||||||
resolveRoomAlias:function(room_alias) {
|
resolveRoomAlias:function(room_alias) {
|
||||||
var path = "/matrix/client/api/v1/directory/room/$room_alias";
|
var path = "/_matrix/client/api/v1/directory/room/$room_alias";
|
||||||
room_alias = encodeURIComponent(room_alias);
|
room_alias = encodeURIComponent(room_alias);
|
||||||
|
|
||||||
path = path.replace("$room_alias", room_alias);
|
path = path.replace("$room_alias", room_alias);
|
||||||
@@ -304,7 +308,7 @@ angular.module('matrixService', [])
|
|||||||
|
|
||||||
// hit the Identity Server for a 3PID request.
|
// hit the Identity Server for a 3PID request.
|
||||||
linkEmail: function(email, clientSecret, sendAttempt) {
|
linkEmail: function(email, clientSecret, sendAttempt) {
|
||||||
var path = "/matrix/identity/api/v1/validate/email/requestToken"
|
var path = "/_matrix/identity/api/v1/validate/email/requestToken"
|
||||||
var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt;
|
var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt;
|
||||||
var headers = {};
|
var headers = {};
|
||||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||||
@@ -312,7 +316,7 @@ angular.module('matrixService', [])
|
|||||||
},
|
},
|
||||||
|
|
||||||
authEmail: function(clientSecret, tokenId, code) {
|
authEmail: function(clientSecret, tokenId, code) {
|
||||||
var path = "/matrix/identity/api/v1/validate/email/submitToken";
|
var path = "/_matrix/identity/api/v1/validate/email/submitToken";
|
||||||
var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret;
|
var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret;
|
||||||
var headers = {};
|
var headers = {};
|
||||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||||
@@ -320,7 +324,7 @@ angular.module('matrixService', [])
|
|||||||
},
|
},
|
||||||
|
|
||||||
bindEmail: function(userId, tokenId, clientSecret) {
|
bindEmail: function(userId, tokenId, clientSecret) {
|
||||||
var path = "/matrix/identity/api/v1/3pid/bind";
|
var path = "/_matrix/identity/api/v1/3pid/bind";
|
||||||
var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret;
|
var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret;
|
||||||
var headers = {};
|
var headers = {};
|
||||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||||
@@ -328,7 +332,7 @@ angular.module('matrixService', [])
|
|||||||
},
|
},
|
||||||
|
|
||||||
uploadContent: function(file) {
|
uploadContent: function(file) {
|
||||||
var path = "/matrix/content";
|
var path = "/_matrix/content";
|
||||||
var headers = {
|
var headers = {
|
||||||
"Content-Type": undefined // undefined means angular will figure it out
|
"Content-Type": undefined // undefined means angular will figure it out
|
||||||
};
|
};
|
||||||
@@ -480,6 +484,30 @@ angular.module('matrixService', [])
|
|||||||
|
|
||||||
getRoomIdToAliasMapping: function(roomId) {
|
getRoomIdToAliasMapping: function(roomId) {
|
||||||
return localStorage.getItem(MAPPING_PREFIX+roomId);
|
return localStorage.getItem(MAPPING_PREFIX+roomId);
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
/****** Power levels management ******/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the power level of an user in a particular room
|
||||||
|
* @param {String} room_id the room id
|
||||||
|
* @param {String} user_id the user id
|
||||||
|
* @returns {Number} a value between 0 and 10
|
||||||
|
*/
|
||||||
|
getUserPowerLevel: function(room_id, user_id) {
|
||||||
|
var powerLevel = 0;
|
||||||
|
var room = $rootScope.events.rooms[room_id];
|
||||||
|
if (room && room["m.room.power_levels"]) {
|
||||||
|
if (user_id in room["m.room.power_levels"].content) {
|
||||||
|
powerLevel = room["m.room.power_levels"].content[user_id];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Use the room default user power
|
||||||
|
powerLevel = room["m.room.power_levels"].content["default"];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return powerLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user