Merge branch 'develop' into storage_transactions
Conflicts: synapse/api/auth.py synapse/handlers/room.py synapse/storage/__init__.py
This commit is contained in:
commit
4b63b06cad
|
@ -0,0 +1,28 @@
|
|||
Changes in synapse 0.0.1 (2014-08-22)
|
||||
=====================================
|
||||
Presence has been disabled in this release due to a bug that caused the
|
||||
homeserver to spam other remote homeservers.
|
||||
|
||||
Homeserver:
|
||||
* Completely change the database schema to support generic event types.
|
||||
* Improve presence reliability.
|
||||
* Improve reliability of joining remote rooms.
|
||||
* Fix bug where room join events were duplicated.
|
||||
* Improve initial sync API to return more information to the client.
|
||||
* Stop generating fake messages for room membership events.
|
||||
|
||||
Webclient:
|
||||
* Add tab completion of names.
|
||||
* Add ability to upload and send images.
|
||||
* Add profile pages.
|
||||
* Improve CSS layout of room.
|
||||
* Disambiguate identical display names.
|
||||
* Don't get remote users display names and avatars individually.
|
||||
* Use the new initial sync API to reduce number of round trips to the homeserver.
|
||||
* Change url scheme to use room aliases instead of room ids where known.
|
||||
* Increase longpoll timeout.
|
||||
|
||||
Changes in synapse 0.0.0 (2014-08-13)
|
||||
=====================================
|
||||
|
||||
* Initial alpha release
|
27
README.rst
27
README.rst
|
@ -24,11 +24,8 @@ To get up and running:
|
|||
|
||||
- To run your own **private** homeserver on localhost:8080, install synapse
|
||||
with ``python setup.py develop --user`` and then run one with
|
||||
``python synapse/app/homeserver.py``
|
||||
|
||||
- To run your own webclient, add ``-w``:
|
||||
``python synapse/app/homeserver.py -w`` and hit http://localhost:8080/matrix/client
|
||||
in your web browser (a recent Chrome, Safari or Firefox for now,
|
||||
``python synapse/app/homeserver.py`` - you will find a webclient running
|
||||
at http://localhost:8080 (use a recent Chrome, Safari or Firefox for now,
|
||||
please...)
|
||||
|
||||
- To make the homeserver **public** and let it exchange messages with
|
||||
|
@ -36,7 +33,12 @@ To get up and running:
|
|||
up port 8080 and run ``python synapse/app/homeserver.py --host
|
||||
machine.my.domain.name``. Then come join ``#matrix:matrix.org`` and
|
||||
say hi! :)
|
||||
|
||||
|
||||
For more detailed setup instructions, please see further down this document.
|
||||
|
||||
[1] VoIP currently in development
|
||||
|
||||
|
||||
About Matrix
|
||||
============
|
||||
|
||||
|
@ -87,8 +89,6 @@ https://github.com/matrix-org/synapse/issues or at matrix@matrix.org.
|
|||
|
||||
Thanks for trying Matrix!
|
||||
|
||||
[1] VoIP currently in development
|
||||
|
||||
[2] Cryptographic signing of messages isn't turned on yet
|
||||
|
||||
[3] End-to-end encryption is currently in development
|
||||
|
@ -146,6 +146,13 @@ This should end with a 'PASSED' result::
|
|||
PASSED (successes=143)
|
||||
|
||||
|
||||
Upgrading an existing homeserver
|
||||
================================
|
||||
|
||||
Before upgrading an existing homeserver to a new version, please refer to
|
||||
UPGRADE.rst for any additional instructions.
|
||||
|
||||
|
||||
Setting up Federation
|
||||
=====================
|
||||
|
||||
|
@ -201,9 +208,7 @@ http://localhost:8080. Simply run::
|
|||
Running The Demo Web Client
|
||||
===========================
|
||||
|
||||
You can run the web client when you run the homeserver by adding ``-w`` to the
|
||||
command to run ``homeserver.py``. The web client can be accessed via
|
||||
http://localhost:8080/matrix/client
|
||||
The homeserver runs a web client by default at http://localhost:8080.
|
||||
|
||||
If this is the first time you have used the client from that browser (it uses
|
||||
HTML5 local storage to remember its config), you will need to log in to your
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
Upgrading to v0.0.1
|
||||
===================
|
||||
|
||||
This release completely changes the database schema and so requires upgrading
|
||||
it before starting the new version of the homeserver.
|
||||
|
||||
The script "database-prepare-for-0.0.1.sh" should be used to upgrade the
|
||||
database. This will save all user information, such as logins and profiles,
|
||||
but will otherwise purge the database. This includes messages, which
|
||||
rooms the home server was a member of and room alias mappings.
|
||||
|
||||
Before running the command the homeserver should be first completely
|
||||
shutdown. To run it, simply specify the location of the database, e.g.:
|
||||
|
||||
./database-prepare-for-0.0.1.sh "homeserver.db"
|
||||
|
||||
Once this has successfully completed it will be safe to restart the
|
||||
homeserver. You may notice that the homeserver takes a few seconds longer to
|
||||
restart than usual as it reinitializes the database.
|
||||
|
||||
On startup of the new version, users can either rejoin remote rooms using room
|
||||
aliases or by being reinvited. Alternatively, if any other homeserver sends a
|
||||
message to a room that the homeserver was previously in the local HS will
|
||||
automatically rejoin the room.
|
|
@ -61,7 +61,7 @@ class SynapseCmd(cmd.Cmd):
|
|||
"send_delivery_receipts": "on"
|
||||
}
|
||||
self.path_prefix = "/matrix/client/api/v1"
|
||||
self.event_stream_token = "START"
|
||||
self.event_stream_token = "END"
|
||||
self.prompt = ">>> "
|
||||
|
||||
def do_EOF(self, line): # allows CTRL+D quitting
|
||||
|
@ -233,56 +233,68 @@ class SynapseCmd(cmd.Cmd):
|
|||
defer.returnValue(False)
|
||||
defer.returnValue(True)
|
||||
|
||||
def do_3pidrequest(self, line):
|
||||
def do_emailrequest(self, line):
|
||||
"""Requests the association of a third party identifier
|
||||
<medium> The medium of the identifer (currently only 'email')
|
||||
<address> The address of the identifer (ie. the email address)
|
||||
<address> The email address)
|
||||
<clientSecret> A string of characters generated when requesting an email that you'll supply in subsequent calls to identify yourself
|
||||
<sendAttempt> The number of times the user has requested an email. Leave this the same between requests to retry the request at the transport level. Increment it to request that the email be sent again.
|
||||
"""
|
||||
args = self._parse(line, ['medium', 'address'])
|
||||
args = self._parse(line, ['address', 'clientSecret', 'sendAttempt'])
|
||||
|
||||
if not args['medium'] == 'email':
|
||||
print "Only email is supported currently"
|
||||
return
|
||||
postArgs = {'email': args['address'], 'clientSecret': args['clientSecret'], 'sendAttempt': args['sendAttempt']}
|
||||
|
||||
postArgs = {'email': args['address'], 'clientSecret': '____'}
|
||||
|
||||
reactor.callFromThread(self._do_3pidrequest, postArgs)
|
||||
reactor.callFromThread(self._do_emailrequest, postArgs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_3pidrequest(self, args):
|
||||
def _do_emailrequest(self, args):
|
||||
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,
|
||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||
print json_res
|
||||
if 'tokenId' in json_res:
|
||||
print "Token ID %s sent" % (json_res['tokenId'])
|
||||
if 'sid' in json_res:
|
||||
print "Token sent. Your session ID is %s" % (json_res['sid'])
|
||||
|
||||
def do_3pidvalidate(self, line):
|
||||
def do_emailvalidate(self, line):
|
||||
"""Validate and associate a third party ID
|
||||
<medium> The medium of the identifer (currently only 'email')
|
||||
<tokenId> The identifier iof the token given in 3pidrequest
|
||||
<sid> The session ID (sid) given to you in the response to requestToken
|
||||
<token> The token sent to your third party identifier address
|
||||
<clientSecret> The same clientSecret you supplied in requestToken
|
||||
"""
|
||||
args = self._parse(line, ['medium', 'tokenId', 'token'])
|
||||
args = self._parse(line, ['sid', 'token', 'clientSecret'])
|
||||
|
||||
if not args['medium'] == 'email':
|
||||
print "Only email is supported currently"
|
||||
return
|
||||
postArgs = { 'sid' : args['sid'], 'token' : args['token'], 'clientSecret': args['clientSecret'] }
|
||||
|
||||
postArgs = { 'tokenId' : args['tokenId'], 'token' : args['token'] }
|
||||
postArgs['mxId'] = self.config["user"]
|
||||
|
||||
reactor.callFromThread(self._do_3pidvalidate, postArgs)
|
||||
reactor.callFromThread(self._do_emailvalidate, postArgs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_3pidvalidate(self, args):
|
||||
def _do_emailvalidate(self, args):
|
||||
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,
|
||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||
print json_res
|
||||
|
||||
def do_3pidbind(self, line):
|
||||
"""Validate and associate a third party ID
|
||||
<sid> The session ID (sid) given to you in the response to requestToken
|
||||
<clientSecret> The same clientSecret you supplied in requestToken
|
||||
"""
|
||||
args = self._parse(line, ['sid', 'clientSecret'])
|
||||
|
||||
postArgs = { 'sid' : args['sid'], 'clientSecret': args['clientSecret'] }
|
||||
postArgs['mxid'] = self.config["user"]
|
||||
|
||||
reactor.callFromThread(self._do_3pidbind, postArgs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _do_3pidbind(self, args):
|
||||
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,
|
||||
headers={'Content-Type': ['application/x-www-form-urlencoded']})
|
||||
print json_res
|
||||
|
||||
def do_join(self, line):
|
||||
"""Joins a room: "join <roomid>" """
|
||||
try:
|
||||
|
@ -390,9 +402,7 @@ class SynapseCmd(cmd.Cmd):
|
|||
"""Leaves a room: "leave <roomid>" """
|
||||
try:
|
||||
args = self._parse(line, ["roomid"], force_keys=True)
|
||||
path = ("/rooms/%s/members/%s/state" %
|
||||
(urllib.quote(args["roomid"]), self._usr()))
|
||||
reactor.callFromThread(self._run_and_pprint, "DELETE", path)
|
||||
self._do_membership_change(args["roomid"], "leave", self._usr())
|
||||
except Exception as e:
|
||||
print e
|
||||
|
||||
|
@ -555,7 +565,7 @@ class SynapseCmd(cmd.Cmd):
|
|||
alt_text="Sent receipt for %s" % event["msg_id"])
|
||||
|
||||
def _do_membership_change(self, roomid, membership, userid):
|
||||
path = "/rooms/%s/members/%s/state" % (urllib.quote(roomid), userid)
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (urllib.quote(roomid), urllib.quote(userid))
|
||||
data = {
|
||||
"membership": membership
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This is will prepare a synapse database for running with v0.0.1 of synapse.
|
||||
# It will store all the user information, but will *delete* all messages and
|
||||
# room data.
|
||||
|
||||
set -e
|
||||
|
||||
cp "$1" "$1.bak"
|
||||
|
||||
DUMP=$(sqlite3 "$1" << 'EOF'
|
||||
.dump users
|
||||
.dump access_tokens
|
||||
.dump presence
|
||||
.dump profiles
|
||||
EOF
|
||||
)
|
||||
|
||||
rm "$1"
|
||||
|
||||
sqlite3 "$1" <<< "$DUMP"
|
|
@ -8,7 +8,7 @@
|
|||
#
|
||||
# $ sqlite3 homeserver.db < table-save.sql
|
||||
|
||||
sqlite3 homeserver.db <<'EOF' >table-save.sql
|
||||
sqlite3 "$1" <<'EOF' >table-save.sql
|
||||
.dump users
|
||||
.dump access_tokens
|
||||
.dump presence
|
||||
|
|
|
@ -6,7 +6,7 @@ CWD=$(pwd)
|
|||
|
||||
cd "$DIR/.."
|
||||
|
||||
for port in "8080" "8081" "8082"; do
|
||||
for port in 8080 8081 8082; do
|
||||
echo "Starting server on port $port... "
|
||||
|
||||
python -m synapse.app.homeserver \
|
||||
|
@ -15,7 +15,8 @@ for port in "8080" "8081" "8082"; do
|
|||
-f "$DIR/$port.log" \
|
||||
-d "$DIR/$port.db" \
|
||||
-vv \
|
||||
-D --pid-file "$DIR/$port.pid"
|
||||
-D --pid-file "$DIR/$port.pid" \
|
||||
--manhole $((port + 1000))
|
||||
done
|
||||
|
||||
echo "Starting webclient on port 8000..."
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"apis": [
|
||||
{
|
||||
"path": "/login",
|
||||
"description": "Login operations"
|
||||
},
|
||||
{
|
||||
"path": "/registration",
|
||||
"description": "Registration operations"
|
||||
},
|
||||
{
|
||||
"path": "/rooms",
|
||||
"description": "Room operations"
|
||||
},
|
||||
{
|
||||
"path": "/profile",
|
||||
"description": "Profile operations"
|
||||
},
|
||||
{
|
||||
"path": "/presence",
|
||||
"description": "Presence operations"
|
||||
}
|
||||
],
|
||||
"authorizations": {
|
||||
"token": {
|
||||
"scopes": []
|
||||
}
|
||||
},
|
||||
"info": {
|
||||
"title": "Matrix Client-Server API Reference",
|
||||
"description": "This contains the client-server API for the reference implementation of the home server",
|
||||
"termsOfServiceUrl": "http://matrix.org",
|
||||
"license": "Apache 2.0",
|
||||
"licenseUrl": "http://www.apache.org/licenses/LICENSE-2.0.html"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,299 @@
|
|||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://petstore.swagger.wordnik.com/api",
|
||||
"resourcePath": "/user",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"path": "/user",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Create user",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "createUser",
|
||||
"authorizations": {
|
||||
"oauth2": [
|
||||
{
|
||||
"scope": "test:anything",
|
||||
"description": "anything"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "Created user object",
|
||||
"required": true,
|
||||
"type": "User",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/logout",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Logs out current logged in user session",
|
||||
"notes": "",
|
||||
"type": "void",
|
||||
"nickname": "logoutUser",
|
||||
"authorizations": {},
|
||||
"parameters": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/createWithArray",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Creates list of users with given input array",
|
||||
"notes": "",
|
||||
"type": "void",
|
||||
"nickname": "createUsersWithArrayInput",
|
||||
"authorizations": {
|
||||
"oauth2": [
|
||||
{
|
||||
"scope": "test:anything",
|
||||
"description": "anything"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "List of user object",
|
||||
"required": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "User"
|
||||
},
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/createWithList",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Creates list of users with given list input",
|
||||
"notes": "",
|
||||
"type": "void",
|
||||
"nickname": "createUsersWithListInput",
|
||||
"authorizations": {
|
||||
"oauth2": [
|
||||
{
|
||||
"scope": "test:anything",
|
||||
"description": "anything"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "List of user object",
|
||||
"required": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "User"
|
||||
},
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/{username}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Updated user",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "updateUser",
|
||||
"authorizations": {
|
||||
"oauth2": [
|
||||
{
|
||||
"scope": "test:anything",
|
||||
"description": "anything"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"description": "name that need to be deleted",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "Updated user object",
|
||||
"required": true,
|
||||
"type": "User",
|
||||
"paramType": "body"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid username supplied"
|
||||
},
|
||||
{
|
||||
"code": 404,
|
||||
"message": "User not found"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"summary": "Delete user",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "deleteUser",
|
||||
"authorizations": {
|
||||
"oauth2": [
|
||||
{
|
||||
"scope": "test:anything",
|
||||
"description": "anything"
|
||||
}
|
||||
]
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"description": "The name that needs to be deleted",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid username supplied"
|
||||
},
|
||||
{
|
||||
"code": 404,
|
||||
"message": "User not found"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get user by user name",
|
||||
"notes": "",
|
||||
"type": "User",
|
||||
"nickname": "getUserByName",
|
||||
"authorizations": {},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"description": "The name that needs to be fetched. Use user1 for testing.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid username supplied"
|
||||
},
|
||||
{
|
||||
"code": 404,
|
||||
"message": "User not found"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/user/login",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Logs user into the system",
|
||||
"notes": "",
|
||||
"type": "string",
|
||||
"nickname": "loginUser",
|
||||
"authorizations": {},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "username",
|
||||
"description": "The user name for login",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "password",
|
||||
"description": "The password for login in clear text",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Invalid username and password combination"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"User": {
|
||||
"id": "User",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
},
|
||||
"username": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastName": {
|
||||
"type": "string"
|
||||
},
|
||||
"email": {
|
||||
"type": "string"
|
||||
},
|
||||
"password": {
|
||||
"type": "string"
|
||||
},
|
||||
"phone": {
|
||||
"type": "string"
|
||||
},
|
||||
"userStatus": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "User Status",
|
||||
"enum": [
|
||||
"1-registered",
|
||||
"2-active",
|
||||
"3-closed"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,102 @@
|
|||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"apis": [
|
||||
{
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"nickname": "get_login_info",
|
||||
"notes": "All login stages MUST be mentioned if there is >1 login type.",
|
||||
"summary": "Get the login mechanism to use when logging in.",
|
||||
"type": "LoginInfo"
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"nickname": "submit_login",
|
||||
"notes": "If this is part of a multi-stage login, there MUST be a 'session' key.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A login submission",
|
||||
"name": "body",
|
||||
"paramType": "body",
|
||||
"required": true,
|
||||
"type": "LoginSubmission"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad login type"
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Missing JSON keys"
|
||||
}
|
||||
],
|
||||
"summary": "Submit a login action.",
|
||||
"type": "LoginResult"
|
||||
}
|
||||
],
|
||||
"path": "/login"
|
||||
}
|
||||
],
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"models": {
|
||||
"LoginInfo": {
|
||||
"id": "LoginInfo",
|
||||
"properties": {
|
||||
"stages": {
|
||||
"description": "Multi-stage login only: An array of all the login types required to login.",
|
||||
"format": "string",
|
||||
"type": "array"
|
||||
},
|
||||
"type": {
|
||||
"description": "The login type that must be used when logging in.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoginResult": {
|
||||
"id": "LoginResult",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"description": "The access token for this user's login if this is the final stage of the login process.",
|
||||
"type": "string"
|
||||
},
|
||||
"next": {
|
||||
"description": "Multi-stage login only: The next login type to submit.",
|
||||
"type": "string"
|
||||
},
|
||||
"session": {
|
||||
"description": "Multi-stage login only: The session token to send when submitting the next login type.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoginSubmission": {
|
||||
"id": "LoginSubmission",
|
||||
"properties": {
|
||||
"type": {
|
||||
"description": "The type of login being submitted.",
|
||||
"type": "string"
|
||||
},
|
||||
"session": {
|
||||
"description": "Multi-stage login only: The session token from an earlier login stage.",
|
||||
"type": "string"
|
||||
},
|
||||
"_login_type_defined_keys_": {
|
||||
"description": "Keys as defined by the specified login type, e.g. \"user\", \"password\""
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"resourcePath": "/login",
|
||||
"swaggerVersion": "1.2"
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/presence",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"path": "/presence/{userId}/status",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Update this user's presence state.",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "update_presence",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The new presence state",
|
||||
"required": true,
|
||||
"type": "PresenceUpdate",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose presence to set.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get this user's presence state.",
|
||||
"notes": "Get this user's presence state.",
|
||||
"type": "PresenceUpdate",
|
||||
"nickname": "get_presence",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose presence to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/presence_list/{userId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Retrieve a list of presences for all of this user's friends.",
|
||||
"notes": "",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Presence"
|
||||
},
|
||||
"nickname": "get_presence_list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose presence list to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Add or remove users from this presence list.",
|
||||
"notes": "Add or remove users from this presence list.",
|
||||
"type": "void",
|
||||
"nickname": "modify_presence_list",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose presence list is being modified.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The modifications to make to this presence list.",
|
||||
"required": true,
|
||||
"type": "PresenceListModifications",
|
||||
"paramType": "body"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"PresenceUpdate": {
|
||||
"id": "PresenceUpdate",
|
||||
"properties": {
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "Enum: The presence state.",
|
||||
"enum": [
|
||||
"offline",
|
||||
"unavailable",
|
||||
"online",
|
||||
"free_for_chat"
|
||||
]
|
||||
},
|
||||
"status_msg": {
|
||||
"type": "string",
|
||||
"description": "The user-defined message associated with this presence state."
|
||||
}
|
||||
},
|
||||
"subTypes": [
|
||||
"Presence"
|
||||
]
|
||||
},
|
||||
"Presence": {
|
||||
"id": "Presence",
|
||||
"properties": {
|
||||
"mtime_age": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "The last time this user's presence state changed, in milliseconds."
|
||||
},
|
||||
"user_id": {
|
||||
"type": "string",
|
||||
"description": "The fully qualified user ID"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PresenceListModifications": {
|
||||
"id": "PresenceListModifications",
|
||||
"properties": {
|
||||
"invite": {
|
||||
"type": "array",
|
||||
"description": "A list of user IDs to add to the list.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "A fully qualified user ID."
|
||||
}
|
||||
},
|
||||
"drop": {
|
||||
"type": "array",
|
||||
"description": "A list of user IDs to remove from the list.",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"description": "A fully qualified user ID."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/profile",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"apis": [
|
||||
{
|
||||
"path": "/profile/{userId}/displayname",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Set a display name.",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "set_display_name",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The new display name for this user.",
|
||||
"required": true,
|
||||
"type": "DisplayName",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose display name to set.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a display name.",
|
||||
"notes": "This can be done by anyone.",
|
||||
"type": "DisplayName",
|
||||
"nickname": "get_display_name",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose display name to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/profile/{userId}/avatar_url",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Set an avatar URL.",
|
||||
"notes": "This can only be done by the logged in user.",
|
||||
"type": "void",
|
||||
"nickname": "set_avatar_url",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The new avatar url for this user.",
|
||||
"required": true,
|
||||
"type": "AvatarUrl",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose avatar url to set.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get an avatar url.",
|
||||
"notes": "This can be done by anyone.",
|
||||
"type": "AvatarUrl",
|
||||
"nickname": "get_avatar_url",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose avatar url to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"DisplayName": {
|
||||
"id": "DisplayName",
|
||||
"properties": {
|
||||
"displayname": {
|
||||
"type": "string",
|
||||
"description": "The textual display name"
|
||||
}
|
||||
}
|
||||
},
|
||||
"AvatarUrl": {
|
||||
"id": "AvatarUrl",
|
||||
"properties": {
|
||||
"avatar_url": {
|
||||
"type": "string",
|
||||
"description": "A url to an image representing an avatar."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"apis": [
|
||||
{
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"nickname": "register",
|
||||
"notes": "Volatile: This API is likely to change.",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "A registration request",
|
||||
"name": "body",
|
||||
"paramType": "body",
|
||||
"required": true,
|
||||
"type": "RegistrationRequest"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "No JSON object."
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "User ID must only contain characters which do not require url encoding."
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "User ID already taken."
|
||||
}
|
||||
],
|
||||
"summary": "Register with the home server.",
|
||||
"type": "RegistrationResponse"
|
||||
}
|
||||
],
|
||||
"path": "/register"
|
||||
}
|
||||
],
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"models": {
|
||||
"RegistrationResponse": {
|
||||
"id": "RegistrationResponse",
|
||||
"properties": {
|
||||
"access_token": {
|
||||
"description": "The access token for this user.",
|
||||
"type": "string"
|
||||
},
|
||||
"user_id": {
|
||||
"description": "The fully-qualified user ID.",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"RegistrationRequest": {
|
||||
"id": "RegistrationRequest",
|
||||
"properties": {
|
||||
"user_id": {
|
||||
"description": "The desired user ID. If not specified, a random user ID will be allocated.",
|
||||
"type": "string",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"resourcePath": "/register",
|
||||
"swaggerVersion": "1.2"
|
||||
}
|
||||
|
|
@ -0,0 +1,807 @@
|
|||
{
|
||||
"apiVersion": "1.0.0",
|
||||
"swaggerVersion": "1.2",
|
||||
"basePath": "http://localhost:8080/matrix/client/api/v1",
|
||||
"resourcePath": "/rooms",
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"authorizations": {
|
||||
"token": []
|
||||
},
|
||||
"apis": [
|
||||
{
|
||||
"path": "/rooms/{roomId}/messages/{userId}/{messageId}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send a message in this room.",
|
||||
"notes": "Send a message in this room.",
|
||||
"type": "void",
|
||||
"nickname": "send_message",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The message contents",
|
||||
"required": true,
|
||||
"type": "Message",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The fully qualified message sender's user ID.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "messageId",
|
||||
"description": "A message ID which is unique for each room and user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Must send messages as yourself."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a message from this room.",
|
||||
"notes": "Get a message from this room.",
|
||||
"type": "Message",
|
||||
"nickname": "get_message",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The fully qualified message sender's user ID.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "messageId",
|
||||
"description": "A message ID which is unique for each room and user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Message not found."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/topic",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Set the topic for this room.",
|
||||
"notes": "Set the topic for this room.",
|
||||
"type": "void",
|
||||
"nickname": "set_topic",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The topic contents",
|
||||
"required": true,
|
||||
"type": "Topic",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to set the topic in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Must send messages as yourself."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get the topic for this room.",
|
||||
"notes": "Get the topic for this room.",
|
||||
"type": "Topic",
|
||||
"nickname": "get_topic",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to get topic in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Topic not found."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/messages/{msgSenderId}/{messageId}/feedback/{senderId}/{feedbackType}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Send feedback to a message.",
|
||||
"notes": "Send feedback to a message.",
|
||||
"type": "void",
|
||||
"nickname": "send_feedback",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The feedback contents",
|
||||
"required": true,
|
||||
"type": "Feedback",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the feedback in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "msgSenderId",
|
||||
"description": "The fully qualified message sender's user ID.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "messageId",
|
||||
"description": "A message ID which is unique for each room and user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "senderId",
|
||||
"description": "The fully qualified feedback sender's user ID.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "feedbackType",
|
||||
"description": "The type of feedback being sent.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path",
|
||||
"enum": [
|
||||
"d",
|
||||
"r"
|
||||
]
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Must send feedback as yourself."
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad feedback type."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get feedback for a message.",
|
||||
"notes": "Get feedback for a message.",
|
||||
"type": "Feedback",
|
||||
"nickname": "get_feedback",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to send the message in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "msgSenderId",
|
||||
"description": "The fully qualified message sender's user ID.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "messageId",
|
||||
"description": "A message ID which is unique for each room and user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "senderId",
|
||||
"description": "The fully qualified feedback sender's user ID.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "feedbackType",
|
||||
"description": "Enum: The type of feedback being sent.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path",
|
||||
"enum": [
|
||||
"d",
|
||||
"r"
|
||||
]
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Feedback not found."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/members/{userId}/state",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Change the membership state for a user in a room.",
|
||||
"notes": "Change the membership state for a user in a room.",
|
||||
"type": "void",
|
||||
"nickname": "set_membership",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The new membership state",
|
||||
"required": true,
|
||||
"type": "Member",
|
||||
"paramType": "body"
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose membership is being changed.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has this user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "No membership key."
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad membership value."
|
||||
},
|
||||
{
|
||||
"code": 403,
|
||||
"message": "When inviting: You are not in the room."
|
||||
},
|
||||
{
|
||||
"code": 403,
|
||||
"message": "When inviting: <target> is already in the room."
|
||||
},
|
||||
{
|
||||
"code": 403,
|
||||
"message": "When joining: Cannot force another user to join."
|
||||
},
|
||||
{
|
||||
"code": 403,
|
||||
"message": "When joining: You are not invited to this room."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get the membership state of a user in a room.",
|
||||
"notes": "Get the membership state of a user in a room.",
|
||||
"type": "Member",
|
||||
"nickname": "get_membership",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user whose membership state you want to get.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has this user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 404,
|
||||
"message": "Member not found."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"method": "DELETE",
|
||||
"summary": "Leave a room.",
|
||||
"notes": "Leave a room.",
|
||||
"type": "void",
|
||||
"nickname": "remove_membership",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"description": "The user who is leaving.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room which has this user.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 403,
|
||||
"message": "You are not in the room."
|
||||
},
|
||||
{
|
||||
"code": 403,
|
||||
"message": "Cannot force another user to leave."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/join/{roomAlias}",
|
||||
"operations": [
|
||||
{
|
||||
"method": "PUT",
|
||||
"summary": "Join a room via a room alias.",
|
||||
"notes": "Join a room via a room alias.",
|
||||
"type": "RoomInfo",
|
||||
"nickname": "join_room_via_alias",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomAlias",
|
||||
"description": "The room alias to join.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Bad room alias."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms",
|
||||
"operations": [
|
||||
{
|
||||
"method": "POST",
|
||||
"summary": "Create a room.",
|
||||
"notes": "Create a room.",
|
||||
"type": "RoomInfo",
|
||||
"nickname": "create_room",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "body",
|
||||
"description": "The desired configuration for the room.",
|
||||
"required": true,
|
||||
"type": "RoomConfig",
|
||||
"paramType": "body"
|
||||
}
|
||||
],
|
||||
"responseMessages": [
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Body must be JSON."
|
||||
},
|
||||
{
|
||||
"code": 400,
|
||||
"message": "Room alias already taken."
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/messages/list",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a list of messages for this room.",
|
||||
"notes": "Get a list of messages for this room.",
|
||||
"type": "MessagePaginationChunk",
|
||||
"nickname": "get_messages",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to get messages in.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "from",
|
||||
"description": "The token to start getting results from.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "to",
|
||||
"description": "The token to stop getting results at.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "The maximum number of messages to return.",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"paramType": "query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"path": "/rooms/{roomId}/members/list",
|
||||
"operations": [
|
||||
{
|
||||
"method": "GET",
|
||||
"summary": "Get a list of members for this room.",
|
||||
"notes": "Get a list of members for this room.",
|
||||
"type": "MemberPaginationChunk",
|
||||
"nickname": "get_members",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "roomId",
|
||||
"description": "The room to get a list of members from.",
|
||||
"required": true,
|
||||
"type": "string",
|
||||
"paramType": "path"
|
||||
},
|
||||
{
|
||||
"name": "from",
|
||||
"description": "The token to start getting results from.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "to",
|
||||
"description": "The token to stop getting results at.",
|
||||
"required": false,
|
||||
"type": "string",
|
||||
"paramType": "query"
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"description": "The maximum number of members to return.",
|
||||
"required": false,
|
||||
"type": "integer",
|
||||
"paramType": "query"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"models": {
|
||||
"Topic": {
|
||||
"id": "Topic",
|
||||
"properties": {
|
||||
"topic": {
|
||||
"type": "string",
|
||||
"description": "The topic text"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Message": {
|
||||
"id": "Message",
|
||||
"properties": {
|
||||
"msgtype": {
|
||||
"type": "string",
|
||||
"description": "The type of message being sent, e.g. \"m.text\"",
|
||||
"required": true
|
||||
},
|
||||
"_msgtype_defined_keys_": {
|
||||
"description": "Additional keys as defined by the msgtype, e.g. \"body\""
|
||||
}
|
||||
}
|
||||
},
|
||||
"Feedback": {
|
||||
"id": "Feedback",
|
||||
"properties": {
|
||||
}
|
||||
},
|
||||
"Member": {
|
||||
"id": "Member",
|
||||
"properties": {
|
||||
"membership": {
|
||||
"type": "string",
|
||||
"description": "Enum: The membership state of this member.",
|
||||
"enum": [
|
||||
"invite",
|
||||
"join",
|
||||
"leave",
|
||||
"knock"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"RoomInfo": {
|
||||
"id": "RoomInfo",
|
||||
"properties": {
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The allocated room ID.",
|
||||
"required": true
|
||||
},
|
||||
"room_alias": {
|
||||
"type": "string",
|
||||
"description": "The alias for the room.",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"RoomConfig": {
|
||||
"id": "RoomConfig",
|
||||
"properties": {
|
||||
"visibility": {
|
||||
"type": "string",
|
||||
"description": "Enum: The room visibility.",
|
||||
"required": false,
|
||||
"enum": [
|
||||
"public",
|
||||
"private"
|
||||
]
|
||||
},
|
||||
"room_alias_name": {
|
||||
"type": "string",
|
||||
"description": "The alias to give the new room.",
|
||||
"required": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"PaginationRequest": {
|
||||
"id": "PaginationRequest",
|
||||
"properties": {
|
||||
"from": {
|
||||
"type": "string",
|
||||
"description": "The token to start getting results from."
|
||||
},
|
||||
"to": {
|
||||
"type": "string",
|
||||
"description": "The token to stop getting results at."
|
||||
},
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "The maximum number of entries to return."
|
||||
}
|
||||
}
|
||||
},
|
||||
"PaginationChunk": {
|
||||
"id": "PaginationChunk",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the first value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
},
|
||||
"end": {
|
||||
"type": "string",
|
||||
"description": "A token which correlates to the last value in \"chunk\" for paginating.",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"subTypes": [
|
||||
"MessagePaginationChunk"
|
||||
]
|
||||
},
|
||||
"MessagePaginationChunk": {
|
||||
"id": "MessagePaginationChunk",
|
||||
"properties": {
|
||||
"chunk": {
|
||||
"type": "array",
|
||||
"description": "A list of message events.",
|
||||
"items": {
|
||||
"$ref": "MessageEvent"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"MemberPaginationChunk": {
|
||||
"id": "MemberPaginationChunk",
|
||||
"properties": {
|
||||
"chunk": {
|
||||
"type": "array",
|
||||
"description": "A list of member events.",
|
||||
"items": {
|
||||
"$ref": "MemberEvent"
|
||||
},
|
||||
"required": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"Event": {
|
||||
"id": "Event",
|
||||
"properties": {
|
||||
"event_id": {
|
||||
"type": "string",
|
||||
"description": "An ID which uniquely identifies this event.",
|
||||
"required": true
|
||||
},
|
||||
"room_id": {
|
||||
"type": "string",
|
||||
"description": "The room in which this event occurred.",
|
||||
"required": true
|
||||
}
|
||||
},
|
||||
"subTypes": [
|
||||
"MessageEvent"
|
||||
]
|
||||
},
|
||||
"MessageEvent": {
|
||||
"id": "MessageEvent",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "Message"
|
||||
}
|
||||
}
|
||||
},
|
||||
"MemberEvent": {
|
||||
"id": "MemberEvent",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "Member"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Tag": {
|
||||
"id": "Tag",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pet": {
|
||||
"id": "Pet",
|
||||
"required": [
|
||||
"id",
|
||||
"name"
|
||||
],
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64",
|
||||
"description": "unique identifier for the pet",
|
||||
"minimum": "0.0",
|
||||
"maximum": "100.0"
|
||||
},
|
||||
"category": {
|
||||
"$ref": "Category"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"photoUrls": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "Tag"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"description": "pet status in the store",
|
||||
"enum": [
|
||||
"available",
|
||||
"pending",
|
||||
"sold"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"Category": {
|
||||
"id": "Category",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "integer",
|
||||
"format": "int64"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"pet": {
|
||||
"$ref": "Pet"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
2
setup.py
2
setup.py
|
@ -25,7 +25,7 @@ def read(fname):
|
|||
|
||||
setup(
|
||||
name="SynapseHomeServer",
|
||||
version="0.1",
|
||||
version="0.0.1",
|
||||
packages=find_packages(exclude=["tests"]),
|
||||
description="Reference Synapse Home Server",
|
||||
install_requires=[
|
||||
|
|
|
@ -15,3 +15,5 @@
|
|||
|
||||
""" This is a reference implementation of a synapse home server.
|
||||
"""
|
||||
|
||||
__version__ = "0.0.1"
|
||||
|
|
|
@ -19,8 +19,7 @@ from twisted.internet import defer
|
|||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.errors import AuthError, StoreError, Codes
|
||||
from synapse.api.events.room import (RoomTopicEvent, RoomMemberEvent,
|
||||
MessageEvent, FeedbackEvent)
|
||||
from synapse.api.events.room import RoomMemberEvent
|
||||
|
||||
import logging
|
||||
|
||||
|
@ -44,19 +43,19 @@ class Auth(object):
|
|||
be raised only if raises=True.
|
||||
"""
|
||||
try:
|
||||
if event.type in [RoomTopicEvent.TYPE, MessageEvent.TYPE,
|
||||
FeedbackEvent.TYPE]:
|
||||
self._check_joined_room(
|
||||
member=snapshot.membership_state,
|
||||
user_id=snapshot.user_id,
|
||||
room_id=snapshot.room_id,
|
||||
)
|
||||
defer.returnValue(True)
|
||||
elif event.type == RoomMemberEvent.TYPE:
|
||||
allowed = yield self.is_membership_change_allowed(event)
|
||||
defer.returnValue(allowed)
|
||||
if hasattr(event, "room_id"):
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
allowed = yield self.is_membership_change_allowed(event)
|
||||
defer.returnValue(allowed)
|
||||
else:
|
||||
self._check_joined_room(
|
||||
member=snapshot.membership_state,
|
||||
user_id=snapshot.user_id,
|
||||
room_id=snapshot.room_id,
|
||||
)
|
||||
defer.returnValue(True)
|
||||
else:
|
||||
raise AuthError(500, "Unknown event type %s" % event.type)
|
||||
raise AuthError(500, "Unknown event: %s" % event)
|
||||
except AuthError as e:
|
||||
logger.info("Event auth check failed on event %s with msg: %s",
|
||||
event, e.msg)
|
||||
|
@ -83,6 +82,8 @@ class Auth(object):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def is_membership_change_allowed(self, event):
|
||||
target_user_id = event.state_key
|
||||
|
||||
# does this room even exist
|
||||
room = yield self.store.get_room(event.room_id)
|
||||
if not room:
|
||||
|
@ -100,7 +101,7 @@ class Auth(object):
|
|||
# get info about the target
|
||||
try:
|
||||
target = yield self.store.get_room_member(
|
||||
user_id=event.target_user_id,
|
||||
user_id=target_user_id,
|
||||
room_id=event.room_id)
|
||||
except:
|
||||
target = None
|
||||
|
@ -114,12 +115,12 @@ class Auth(object):
|
|||
raise AuthError(403, "You are not in room %s." % event.room_id)
|
||||
elif target_in_room: # the target is already in the room.
|
||||
raise AuthError(403, "%s is already in the room." %
|
||||
event.target_user_id)
|
||||
target_user_id)
|
||||
elif Membership.JOIN == membership:
|
||||
# Joins are valid iff caller == target and they were:
|
||||
# invited: They are accepting the invitation
|
||||
# joined: It's a NOOP
|
||||
if event.user_id != event.target_user_id:
|
||||
if event.user_id != target_user_id:
|
||||
raise AuthError(403, "Cannot force another user to join.")
|
||||
elif room.is_public:
|
||||
pass # anyone can join public rooms.
|
||||
|
@ -129,10 +130,10 @@ class Auth(object):
|
|||
elif Membership.LEAVE == membership:
|
||||
if not caller_in_room: # trying to leave a room you aren't joined
|
||||
raise AuthError(403, "You are not in room %s." % event.room_id)
|
||||
elif event.target_user_id != event.user_id:
|
||||
elif target_user_id != event.user_id:
|
||||
# trying to force another user to leave
|
||||
raise AuthError(403, "Cannot force %s to leave." %
|
||||
event.target_user_id)
|
||||
target_user_id)
|
||||
else:
|
||||
raise AuthError(500, "Unknown membership %s" % membership)
|
||||
|
||||
|
|
|
@ -23,6 +23,7 @@ class Membership(object):
|
|||
JOIN = u"join"
|
||||
KNOCK = u"knock"
|
||||
LEAVE = u"leave"
|
||||
LIST = (INVITE, JOIN, KNOCK, LEAVE)
|
||||
|
||||
|
||||
class Feedback(object):
|
||||
|
|
|
@ -41,16 +41,17 @@ class SynapseEvent(JsonEncodedObject):
|
|||
"room_id",
|
||||
"user_id", # sender/initiator
|
||||
"content", # HTTP body, JSON
|
||||
"state_key",
|
||||
]
|
||||
|
||||
internal_keys = [
|
||||
"is_state",
|
||||
"state_key",
|
||||
"prev_events",
|
||||
"prev_state",
|
||||
"depth",
|
||||
"destinations",
|
||||
"origin",
|
||||
"outlier",
|
||||
]
|
||||
|
||||
required_keys = [
|
||||
|
|
|
@ -33,16 +33,21 @@ class EventFactory(object):
|
|||
RoomConfigEvent
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
def __init__(self, hs):
|
||||
self._event_list = {} # dict of TYPE to event class
|
||||
for event_class in EventFactory._event_classes:
|
||||
self._event_list[event_class.TYPE] = event_class
|
||||
|
||||
self.clock = hs.get_clock()
|
||||
|
||||
def create_event(self, etype=None, **kwargs):
|
||||
kwargs["type"] = etype
|
||||
if "event_id" not in kwargs:
|
||||
kwargs["event_id"] = random_string(10)
|
||||
|
||||
if "ts" not in kwargs:
|
||||
kwargs["ts"] = int(self.clock.time_msec())
|
||||
|
||||
if etype in self._event_list:
|
||||
handler = self._event_list[etype]
|
||||
else:
|
||||
|
|
|
@ -13,6 +13,8 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.errors import SynapseError
|
||||
from . import SynapseEvent
|
||||
|
||||
|
||||
|
@ -59,15 +61,15 @@ class RoomMemberEvent(SynapseEvent):
|
|||
TYPE = "m.room.member"
|
||||
|
||||
valid_keys = SynapseEvent.valid_keys + [
|
||||
"target_user_id", # target
|
||||
# target is the state_key
|
||||
"membership", # action
|
||||
]
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
if "target_user_id" in kwargs:
|
||||
kwargs["state_key"] = kwargs["target_user_id"]
|
||||
if "membership" not in kwargs:
|
||||
kwargs["membership"] = kwargs.get("content", {}).get("membership")
|
||||
if not kwargs["membership"] in Membership.LIST:
|
||||
raise SynapseError(400, "Bad membership value.")
|
||||
super(RoomMemberEvent, self).__init__(**kwargs)
|
||||
|
||||
def get_content_template(self):
|
||||
|
@ -108,7 +110,7 @@ class InviteJoinEvent(SynapseEvent):
|
|||
TYPE = "m.room.invite_join"
|
||||
|
||||
valid_keys = SynapseEvent.valid_keys + [
|
||||
"target_user_id",
|
||||
# target_user_id is the state_key
|
||||
"target_host",
|
||||
]
|
||||
|
||||
|
|
|
@ -56,11 +56,11 @@ class Notifier(object):
|
|||
# invites MUST prod the person being invited, who won't be in the room.
|
||||
if (event.type == RoomMemberEvent.TYPE and
|
||||
event.content["membership"] == Membership.INVITE):
|
||||
member_list.append(event.target_user_id)
|
||||
member_list.append(event.state_key)
|
||||
# similarly, LEAVEs must be sent to the person leaving
|
||||
if (event.type == RoomMemberEvent.TYPE and
|
||||
event.content["membership"] == Membership.LEAVE):
|
||||
member_list.append(event.target_user_id)
|
||||
member_list.append(event.state_key)
|
||||
|
||||
for user_id in member_list:
|
||||
if user_id in self.stored_event_listeners:
|
||||
|
|
|
@ -19,9 +19,6 @@ from twisted.internet import defer
|
|||
|
||||
from synapse.api.errors import EventStreamError
|
||||
from synapse.api.events import SynapseEvent
|
||||
from synapse.api.events.room import (
|
||||
RoomMemberEvent, MessageEvent, FeedbackEvent, RoomTopicEvent
|
||||
)
|
||||
from synapse.api.streams import PaginationStream, StreamData
|
||||
|
||||
import logging
|
||||
|
|
|
@ -31,12 +31,14 @@ from synapse.api.urls import (
|
|||
)
|
||||
|
||||
from daemonize import Daemonize
|
||||
import twisted.manhole.telnet
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import logging.config
|
||||
import sqlite3
|
||||
import os
|
||||
import re
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -56,7 +58,7 @@ class SynapseHomeServer(HomeServer):
|
|||
return File("webclient") # TODO configurable?
|
||||
|
||||
def build_resource_for_content_repo(self):
|
||||
return ContentRepoResource("uploads", self.auth)
|
||||
return ContentRepoResource(self, self.upload_dir, self.auth)
|
||||
|
||||
def build_db_pool(self):
|
||||
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
|
||||
|
@ -235,8 +237,10 @@ def setup():
|
|||
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",
|
||||
action="store_true", help="Host the web client.")
|
||||
parser.add_argument("-W", "--webclient", dest="webclient", default=True,
|
||||
action="store_false", help="Don't host a web client.")
|
||||
parser.add_argument("--manhole", dest="manhole", type=int, default=None,
|
||||
help="Turn on the twisted telnet manhole service.")
|
||||
args = parser.parse_args()
|
||||
|
||||
verbosity = int(args.verbose) if args.verbose else None
|
||||
|
@ -255,9 +259,16 @@ def setup():
|
|||
|
||||
logger.info("Server hostname: %s", args.host)
|
||||
|
||||
if re.search(":[0-9]+$", args.host):
|
||||
domain_with_port = args.host
|
||||
else:
|
||||
domain_with_port = "%s:%s" % (args.host, args.port)
|
||||
|
||||
hs = SynapseHomeServer(
|
||||
args.host,
|
||||
db_name=db_name
|
||||
domain_with_port=domain_with_port,
|
||||
upload_dir=os.path.abspath("uploads"),
|
||||
db_name=db_name,
|
||||
)
|
||||
|
||||
# This object doesn't need to be saved because it's set as the handler for
|
||||
|
@ -273,6 +284,13 @@ def setup():
|
|||
|
||||
hs.build_db_pool()
|
||||
|
||||
if args.manhole:
|
||||
f = twisted.manhole.telnet.ShellFactory()
|
||||
f.username = "matrix"
|
||||
f.password = "rabbithole"
|
||||
f.namespace['hs'] = hs
|
||||
reactor.listenTCP(args.manhole, f, interface='127.0.0.1')
|
||||
|
||||
if args.daemonize:
|
||||
daemon = Daemonize(
|
||||
app="synapse-homeserver",
|
||||
|
|
|
@ -509,10 +509,10 @@ class _TransactionQueue(object):
|
|||
# a transaction in progress. If we do, stick it in the pending_pdus
|
||||
# table and we'll get back to it later.
|
||||
|
||||
destinations = [
|
||||
destinations = set([
|
||||
d for d in pdu.destinations
|
||||
if d != self.server_name
|
||||
]
|
||||
])
|
||||
|
||||
logger.debug("Sending to: %s", str(destinations))
|
||||
|
||||
|
|
|
@ -24,4 +24,5 @@ class BaseHandler(object):
|
|||
self.notifier = hs.get_notifier()
|
||||
self.room_lock = hs.get_room_lock_manager()
|
||||
self.state_handler = hs.get_state_handler()
|
||||
self.distributor = hs.get_distributor()
|
||||
self.hs = hs
|
||||
|
|
|
@ -32,6 +32,15 @@ logger = logging.getLogger(__name__)
|
|||
class FederationHandler(BaseHandler):
|
||||
|
||||
"""Handles events that originated from federation."""
|
||||
def __init__(self, hs):
|
||||
super(FederationHandler, self).__init__(hs)
|
||||
|
||||
self.distributor.observe(
|
||||
"user_joined_room",
|
||||
self._on_user_joined
|
||||
)
|
||||
|
||||
self.waiting_for_join_list = {}
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
|
@ -56,7 +65,7 @@ class FederationHandler(BaseHandler):
|
|||
content.update({"membership": Membership.JOIN})
|
||||
new_event = self.event_factory.create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
target_user_id=event.user_id,
|
||||
state_key=event.user_id,
|
||||
room_id=event.room_id,
|
||||
user_id=event.user_id,
|
||||
membership=Membership.JOIN,
|
||||
|
@ -103,6 +112,13 @@ class FederationHandler(BaseHandler):
|
|||
if not backfilled:
|
||||
yield self.notifier.on_new_room_event(event, store_id)
|
||||
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
if event.membership == Membership.JOIN:
|
||||
user = self.hs.parse_userid(event.target_user_id)
|
||||
self.distributor.fire(
|
||||
"user_joined_room", user=user, room_id=event.room_id
|
||||
)
|
||||
|
||||
|
||||
@log_function
|
||||
@defer.inlineCallbacks
|
||||
|
@ -152,12 +168,14 @@ class FederationHandler(BaseHandler):
|
|||
|
||||
yield federation.handle_new_event(new_event)
|
||||
|
||||
store_id = yield self.store.persist_event(new_event)
|
||||
self.notifier.on_new_room_event(new_event, store_id)
|
||||
# TODO (erikj): Time out here.
|
||||
d = defer.Deferred()
|
||||
self.waiting_for_join_list.setdefault((joinee, room_id), []).append(d)
|
||||
yield d
|
||||
|
||||
try:
|
||||
yield self.store.store_room(
|
||||
event.room_id,
|
||||
room_id,
|
||||
"",
|
||||
is_public=False
|
||||
)
|
||||
|
@ -166,3 +184,10 @@ class FederationHandler(BaseHandler):
|
|||
|
||||
|
||||
defer.returnValue(True)
|
||||
|
||||
|
||||
@log_function
|
||||
def _on_user_joined(self, user, room_id):
|
||||
waiters = self.waiting_for_join_list.get((user.to_string(), room_id), [])
|
||||
while waiters:
|
||||
waiters.pop().callback(None)
|
||||
|
|
|
@ -142,6 +142,10 @@ class PresenceHandler(BaseHandler):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
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)
|
||||
|
||||
if observer_user == observed_user:
|
||||
|
@ -155,12 +159,11 @@ class PresenceHandler(BaseHandler):
|
|||
if allowed_by_subscription:
|
||||
defer.returnValue(True)
|
||||
|
||||
rm_handler = self.homeserver.get_handlers().room_member_handler
|
||||
for room_id in (yield rm_handler.get_rooms_for_user(observer_user)):
|
||||
if observed_user in (yield rm_handler.get_room_members(room_id)):
|
||||
defer.returnValue(True)
|
||||
share_room = yield self.store.do_users_share_a_room(
|
||||
[observer_user, observed_user]
|
||||
)
|
||||
|
||||
defer.returnValue(False)
|
||||
defer.returnValue(share_room)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_state(self, target_user, auth_user):
|
||||
|
@ -187,6 +190,10 @@ class PresenceHandler(BaseHandler):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def set_state(self, target_user, auth_user, state):
|
||||
return
|
||||
# TODO (erikj): Turn this back on. Why did we end up sending EDUs
|
||||
# everywhere?
|
||||
|
||||
if not target_user.is_mine:
|
||||
raise SynapseError(400, "User is not hosted on this Home Server")
|
||||
|
||||
|
@ -667,7 +674,7 @@ class PresenceHandler(BaseHandler):
|
|||
|
||||
def push_update_to_clients(self, observer_user, observed_user,
|
||||
statuscache):
|
||||
state = statuscache.make_event(user=observed_user, clock=self.clock)
|
||||
statuscache.make_event(user=observed_user, clock=self.clock)
|
||||
|
||||
self.notifier.on_new_user_event(
|
||||
observer_user.to_string(),
|
||||
|
|
|
@ -20,15 +20,14 @@ from synapse.types import UserID, RoomAlias, RoomID
|
|||
from synapse.api.constants import Membership
|
||||
from synapse.api.errors import RoomError, StoreError, SynapseError
|
||||
from synapse.api.events.room import (
|
||||
RoomTopicEvent, MessageEvent, InviteJoinEvent, RoomMemberEvent,
|
||||
RoomConfigEvent
|
||||
RoomTopicEvent, RoomMemberEvent, RoomConfigEvent
|
||||
)
|
||||
from synapse.api.streams.event import EventStream, EventsStreamData
|
||||
from synapse.handlers.presence import PresenceStreamData
|
||||
from synapse.util import stringutils
|
||||
from ._base import BaseHandler
|
||||
|
||||
import logging
|
||||
import json
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -260,21 +259,38 @@ class MessageHandler(BaseHandler):
|
|||
membership_list=[Membership.INVITE, Membership.JOIN]
|
||||
)
|
||||
|
||||
ret = []
|
||||
rooms_ret = []
|
||||
|
||||
now_rooms_token = yield self.store.get_room_events_max_id()
|
||||
|
||||
# FIXME (erikj): Fix this.
|
||||
presence_stream = PresenceStreamData(self.hs)
|
||||
now_presence_token = yield presence_stream.max_token()
|
||||
presence = yield presence_stream.get_rows(
|
||||
user_id, 0, now_presence_token, None, None
|
||||
)
|
||||
|
||||
# FIXME (erikj): We need to not generate this token,
|
||||
now_token = "%s_%s" % (now_rooms_token, now_presence_token)
|
||||
|
||||
for event in room_list:
|
||||
d = {
|
||||
"room_id": event.room_id,
|
||||
"membership": event.membership,
|
||||
}
|
||||
ret.append(d)
|
||||
|
||||
if event.membership == Membership.INVITE:
|
||||
d["inviter"] = event.user_id
|
||||
|
||||
rooms_ret.append(d)
|
||||
|
||||
if event.membership != Membership.JOIN:
|
||||
continue
|
||||
try:
|
||||
messages, token = yield self.store.get_recent_events_for_room(
|
||||
event.room_id,
|
||||
limit=50,
|
||||
limit=10,
|
||||
end_token=now_rooms_token,
|
||||
)
|
||||
|
||||
d["messages"] = {
|
||||
|
@ -282,10 +298,15 @@ class MessageHandler(BaseHandler):
|
|||
"start": token[0],
|
||||
"end": token[1],
|
||||
}
|
||||
|
||||
current_state = yield self.store.get_current_state(event.room_id)
|
||||
d["state"] = [c.get_dict() for c in current_state]
|
||||
except:
|
||||
logger.exception("Failed to get snapshot")
|
||||
|
||||
logger.debug("snapshot_all_rooms returning: %s", ret)
|
||||
ret = {"rooms": rooms_ret, "presence": presence[0], "end": now_token}
|
||||
|
||||
# logger.debug("snapshot_all_rooms returning: %s", ret)
|
||||
|
||||
defer.returnValue(ret)
|
||||
|
||||
|
@ -377,7 +398,7 @@ class RoomCreationHandler(BaseHandler):
|
|||
content = {"membership": Membership.JOIN}
|
||||
join_event = self.event_factory.create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
target_user_id=user_id,
|
||||
state_key=user_id,
|
||||
room_id=room_id,
|
||||
user_id=user_id,
|
||||
membership=Membership.JOIN,
|
||||
|
@ -505,6 +526,7 @@ class RoomMemberHandler(BaseHandler):
|
|||
Raises:
|
||||
SynapseError if there was a problem changing the membership.
|
||||
"""
|
||||
target_user_id = event.state_key
|
||||
|
||||
snapshot = yield self.store.snapshot_room(
|
||||
event.room_id, event.user_id,
|
||||
|
@ -512,7 +534,7 @@ class RoomMemberHandler(BaseHandler):
|
|||
)
|
||||
## TODO(markjh): get prev state from snapshot.
|
||||
prev_state = yield self.store.get_room_member(
|
||||
event.target_user_id, event.room_id
|
||||
target_user_id, event.room_id
|
||||
)
|
||||
|
||||
if prev_state:
|
||||
|
@ -569,7 +591,7 @@ class RoomMemberHandler(BaseHandler):
|
|||
content.update({"membership": Membership.JOIN})
|
||||
new_event = self.event_factory.create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
target_user_id=joinee.to_string(),
|
||||
state_key=joinee.to_string(),
|
||||
room_id=room_id,
|
||||
user_id=joinee.to_string(),
|
||||
membership=Membership.JOIN,
|
||||
|
@ -586,7 +608,7 @@ class RoomMemberHandler(BaseHandler):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def _do_join(self, event, snapshot, room_host=None, do_auth=True):
|
||||
joinee = self.hs.parse_userid(event.target_user_id)
|
||||
joinee = self.hs.parse_userid(event.state_key)
|
||||
# room_id = RoomID.from_string(event.room_id, self.hs)
|
||||
room_id = event.room_id
|
||||
|
||||
|
@ -697,16 +719,17 @@ class RoomMemberHandler(BaseHandler):
|
|||
|
||||
# If we're inviting someone, then we should also send it to that
|
||||
# HS.
|
||||
target_user_id = event.state_key
|
||||
if membership == Membership.INVITE:
|
||||
host = UserID.from_string(
|
||||
event.target_user_id, self.hs
|
||||
target_user_id, self.hs
|
||||
).domain
|
||||
destinations.append(host)
|
||||
|
||||
# If we are joining a remote HS, include that.
|
||||
if membership == Membership.JOIN:
|
||||
host = UserID.from_string(
|
||||
event.target_user_id, self.hs
|
||||
target_user_id, self.hs
|
||||
).domain
|
||||
destinations.append(host)
|
||||
|
||||
|
|
|
@ -212,8 +212,9 @@ class ContentRepoResource(resource.Resource):
|
|||
"""
|
||||
isLeaf = True
|
||||
|
||||
def __init__(self, directory, auth):
|
||||
def __init__(self, hs, directory, auth):
|
||||
resource.Resource.__init__(self)
|
||||
self.hs = hs
|
||||
self.directory = directory
|
||||
self.auth = auth
|
||||
|
||||
|
@ -250,7 +251,8 @@ class ContentRepoResource(resource.Resource):
|
|||
file_ext = re.sub("[^a-z]", "", file_ext)
|
||||
suffix += "." + file_ext
|
||||
|
||||
file_path = os.path.join(self.directory, prefix + main_part + suffix)
|
||||
file_name = prefix + main_part + suffix
|
||||
file_path = os.path.join(self.directory, file_name)
|
||||
logger.info("User %s is uploading a file to path %s",
|
||||
auth_user.to_string(),
|
||||
file_path)
|
||||
|
@ -259,8 +261,8 @@ class ContentRepoResource(resource.Resource):
|
|||
attempts = 0
|
||||
while os.path.exists(file_path):
|
||||
main_part = random_string(24)
|
||||
file_path = os.path.join(self.directory,
|
||||
prefix + main_part + suffix)
|
||||
file_name = prefix + main_part + suffix
|
||||
file_path = os.path.join(self.directory, file_name)
|
||||
attempts += 1
|
||||
if attempts > 25: # really? Really?
|
||||
raise SynapseError(500, "Unable to create file.")
|
||||
|
@ -272,11 +274,14 @@ class ContentRepoResource(resource.Resource):
|
|||
# servers.
|
||||
|
||||
# TODO: A little crude here, we could do this better.
|
||||
filename = request.path.split(self.directory + "/")[1]
|
||||
filename = request.path.split('/')[-1]
|
||||
# be paranoid
|
||||
filename = re.sub("[^0-9A-z.-_]", "", filename)
|
||||
|
||||
file_path = self.directory + "/" + filename
|
||||
|
||||
logger.debug("Searching for %s", file_path)
|
||||
|
||||
if os.path.isfile(file_path):
|
||||
# filename has the content type
|
||||
base64_contentype = filename.split(".")[1]
|
||||
|
@ -304,6 +309,10 @@ class ContentRepoResource(resource.Resource):
|
|||
self._async_render(request)
|
||||
return server.NOT_DONE_YET
|
||||
|
||||
def render_OPTIONS(self, request):
|
||||
respond_with_json_bytes(request, 200, {}, send_cors=True)
|
||||
return server.NOT_DONE_YET
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _async_render(self, request):
|
||||
try:
|
||||
|
@ -313,8 +322,15 @@ class ContentRepoResource(resource.Resource):
|
|||
with open(fname, "wb") as f:
|
||||
f.write(request.content.read())
|
||||
|
||||
|
||||
# FIXME (erikj): These should use constants.
|
||||
file_name = os.path.basename(fname)
|
||||
url = "http://%s/matrix/content/%s" % (
|
||||
self.hs.domain_with_port, file_name
|
||||
)
|
||||
|
||||
respond_with_json_bytes(request, 200,
|
||||
json.dumps({"content_token": fname}),
|
||||
json.dumps({"content_token": url}),
|
||||
send_cors=True)
|
||||
|
||||
except CodeMessageException as e:
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
|
||||
""" This module contains base REST classes for constructing REST servlets. """
|
||||
from synapse.api.urls import CLIENT_PREFIX
|
||||
from synapse.rest.transactions import HttpTransactionStore
|
||||
import re
|
||||
|
||||
|
||||
|
@ -59,6 +60,7 @@ class RestServlet(object):
|
|||
self.handlers = hs.get_handlers()
|
||||
self.event_factory = hs.get_event_factory()
|
||||
self.auth = hs.get_auth()
|
||||
self.txns = HttpTransactionStore()
|
||||
|
||||
def register(self, http_server):
|
||||
""" Register this servlet with the given HTTP server. """
|
||||
|
|
|
@ -33,10 +33,10 @@ class RegisterRestServlet(RestServlet):
|
|||
try:
|
||||
register_json = json.loads(request.content.read())
|
||||
if "password" in register_json:
|
||||
password = register_json["password"]
|
||||
password = register_json["password"].encode("utf-8")
|
||||
|
||||
if type(register_json["user_id"]) == unicode:
|
||||
desired_user_id = register_json["user_id"]
|
||||
desired_user_id = register_json["user_id"].encode("utf-8")
|
||||
if urllib.quote(desired_user_id) != desired_user_id:
|
||||
raise SynapseError(
|
||||
400,
|
||||
|
|
|
@ -18,9 +18,10 @@ from twisted.internet import defer
|
|||
|
||||
from base import RestServlet, client_path_pattern
|
||||
from synapse.api.errors import SynapseError, Codes
|
||||
from synapse.api.events.room import (RoomTopicEvent, MessageEvent,
|
||||
RoomMemberEvent, FeedbackEvent)
|
||||
from synapse.api.constants import Feedback, Membership
|
||||
from synapse.api.events.room import (
|
||||
MessageEvent, RoomMemberEvent, FeedbackEvent
|
||||
)
|
||||
from synapse.api.constants import Feedback
|
||||
from synapse.api.streams import PaginationConfig
|
||||
|
||||
import json
|
||||
|
@ -95,46 +96,76 @@ class RoomCreateRestServlet(RestServlet):
|
|||
return (200, {})
|
||||
|
||||
|
||||
class RoomTopicRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/topic$")
|
||||
class RoomStateEventRestServlet(RestServlet):
|
||||
def register(self, http_server):
|
||||
# /room/$roomid/state/$eventtype
|
||||
no_state_key = "/rooms/(?P<room_id>[^/]*)/state/(?P<event_type>[^/]*)$"
|
||||
|
||||
def get_event_type(self):
|
||||
return RoomTopicEvent.TYPE
|
||||
# /room/$roomid/state/$eventtype/$statekey
|
||||
state_key = ("/rooms/(?P<room_id>[^/]*)/state/" +
|
||||
"(?P<event_type>[^/]*)/(?P<state_key>[^/]*)$")
|
||||
|
||||
http_server.register_path("GET",
|
||||
client_path_pattern(state_key),
|
||||
self.on_GET)
|
||||
http_server.register_path("PUT",
|
||||
client_path_pattern(state_key),
|
||||
self.on_PUT)
|
||||
http_server.register_path("GET",
|
||||
client_path_pattern(no_state_key),
|
||||
self.on_GET_no_state_key)
|
||||
http_server.register_path("PUT",
|
||||
client_path_pattern(no_state_key),
|
||||
self.on_PUT_no_state_key)
|
||||
|
||||
def on_GET_no_state_key(self, request, room_id, event_type):
|
||||
return self.on_GET(request, room_id, event_type, "")
|
||||
|
||||
def on_PUT_no_state_key(self, request, room_id, event_type):
|
||||
return self.on_PUT(request, room_id, event_type, "")
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id):
|
||||
def on_GET(self, request, room_id, event_type, state_key):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
msg_handler = self.handlers.message_handler
|
||||
data = yield msg_handler.get_room_data(
|
||||
user_id=user.to_string(),
|
||||
room_id=urllib.unquote(room_id),
|
||||
event_type=RoomTopicEvent.TYPE,
|
||||
state_key="",
|
||||
event_type=urllib.unquote(event_type),
|
||||
state_key=urllib.unquote(state_key),
|
||||
)
|
||||
|
||||
if not data:
|
||||
raise SynapseError(404, "Topic not found.", errcode=Codes.NOT_FOUND)
|
||||
defer.returnValue((200, data.content))
|
||||
raise SynapseError(404, "Event not found.", errcode=Codes.NOT_FOUND)
|
||||
defer.returnValue((200, data[0].get_dict()["content"]))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id):
|
||||
def on_PUT(self, request, room_id, event_type, state_key):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
event_type = urllib.unquote(event_type)
|
||||
|
||||
content = _parse_json(request)
|
||||
|
||||
event = self.event_factory.create_event(
|
||||
etype=self.get_event_type(),
|
||||
etype=event_type,
|
||||
content=content,
|
||||
room_id=urllib.unquote(room_id),
|
||||
user_id=user.to_string(),
|
||||
state_key=urllib.unquote(state_key)
|
||||
)
|
||||
|
||||
msg_handler = self.handlers.message_handler
|
||||
yield msg_handler.store_room_data(
|
||||
event=event
|
||||
)
|
||||
defer.returnValue((200, ""))
|
||||
if event_type == RoomMemberEvent.TYPE:
|
||||
# membership events are special
|
||||
handler = self.handlers.room_member_handler
|
||||
yield handler.change_membership(event)
|
||||
defer.returnValue((200, ""))
|
||||
else:
|
||||
# store random bits of state
|
||||
msg_handler = self.handlers.message_handler
|
||||
yield msg_handler.store_room_data(
|
||||
event=event
|
||||
)
|
||||
defer.returnValue((200, ""))
|
||||
|
||||
|
||||
class JoinRoomAliasServlet(RestServlet):
|
||||
|
@ -157,73 +188,6 @@ class JoinRoomAliasServlet(RestServlet):
|
|||
defer.returnValue((200, ret_dict))
|
||||
|
||||
|
||||
class RoomMemberRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/members/"
|
||||
+ "(?P<target_user_id>[^/]*)/state$")
|
||||
|
||||
def get_event_type(self):
|
||||
return RoomMemberEvent.TYPE
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id, target_user_id):
|
||||
room_id = urllib.unquote(room_id)
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
handler = self.handlers.room_member_handler
|
||||
member = yield handler.get_room_member(
|
||||
room_id,
|
||||
urllib.unquote(target_user_id),
|
||||
user.to_string())
|
||||
if not member:
|
||||
raise SynapseError(404, "Member not found.",
|
||||
errcode=Codes.NOT_FOUND)
|
||||
defer.returnValue((200, member.content))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_DELETE(self, request, roomid, target_user_id):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
event = self.event_factory.create_event(
|
||||
etype=self.get_event_type(),
|
||||
target_user_id=urllib.unquote(target_user_id),
|
||||
room_id=urllib.unquote(roomid),
|
||||
user_id=user.to_string(),
|
||||
membership=Membership.LEAVE,
|
||||
content={"membership": Membership.LEAVE}
|
||||
)
|
||||
|
||||
handler = self.handlers.room_member_handler
|
||||
yield handler.change_membership(event)
|
||||
defer.returnValue((200, ""))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, roomid, target_user_id):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
content = _parse_json(request)
|
||||
if "membership" not in content:
|
||||
raise SynapseError(400, "No membership key.",
|
||||
errcode=Codes.BAD_JSON)
|
||||
|
||||
valid_membership_values = [Membership.JOIN, Membership.INVITE]
|
||||
if (content["membership"] not in valid_membership_values):
|
||||
raise SynapseError(400, "Membership value must be %s." % (
|
||||
valid_membership_values,), errcode=Codes.BAD_JSON)
|
||||
|
||||
event = self.event_factory.create_event(
|
||||
etype=self.get_event_type(),
|
||||
target_user_id=urllib.unquote(target_user_id),
|
||||
room_id=urllib.unquote(roomid),
|
||||
user_id=user.to_string(),
|
||||
membership=content["membership"],
|
||||
content=content
|
||||
)
|
||||
|
||||
handler = self.handlers.room_member_handler
|
||||
yield handler.change_membership(event)
|
||||
defer.returnValue((200, ""))
|
||||
|
||||
|
||||
class MessageRestServlet(RestServlet):
|
||||
PATTERN = client_path_pattern("/rooms/(?P<room_id>[^/]*)/messages/"
|
||||
+ "(?P<sender_id>[^/]*)/(?P<msg_id>[^/]*)$")
|
||||
|
@ -285,7 +249,7 @@ class FeedbackRestServlet(RestServlet):
|
|||
@defer.inlineCallbacks
|
||||
def on_GET(self, request, room_id, msg_sender_id, msg_id, fb_sender_id,
|
||||
feedback_type):
|
||||
user = yield (self.auth.get_user_by_req(request))
|
||||
yield (self.auth.get_user_by_req(request))
|
||||
|
||||
# TODO (erikj): Implement this?
|
||||
raise NotImplementedError("Getting feedback is not supported")
|
||||
|
@ -354,7 +318,8 @@ class RoomMemberListRestServlet(RestServlet):
|
|||
user_id=user.to_string())
|
||||
|
||||
for event in members["chunk"]:
|
||||
target_user = self.hs.parse_userid(event["target_user_id"])
|
||||
# FIXME: should probably be state_key here, not user_id
|
||||
target_user = self.hs.parse_userid(event["user_id"])
|
||||
# Presence is an optional cache; don't fail if we can't fetch it
|
||||
try:
|
||||
presence_state = yield self.handlers.presence_handler.get_state(
|
||||
|
@ -400,6 +365,52 @@ class RoomTriggerBackfill(RestServlet):
|
|||
res = [event.get_dict() for event in events]
|
||||
defer.returnValue((200, res))
|
||||
|
||||
|
||||
class RoomMembershipRestServlet(RestServlet):
|
||||
|
||||
def register(self, http_server):
|
||||
# /rooms/$roomid/[invite|join|leave]
|
||||
PATTERN = ("/rooms/(?P<room_id>[^/]*)/" +
|
||||
"(?P<membership_action>join|invite|leave)")
|
||||
register_txn_path(self, PATTERN, http_server)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_POST(self, request, room_id, membership_action):
|
||||
user = yield self.auth.get_user_by_req(request)
|
||||
|
||||
content = _parse_json(request)
|
||||
|
||||
# target user is you unless it is an invite
|
||||
state_key = user.to_string()
|
||||
if membership_action == "invite":
|
||||
if "user_id" not in content:
|
||||
raise SynapseError(400, "Missing user_id key.")
|
||||
state_key = content["user_id"]
|
||||
|
||||
event = self.event_factory.create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
content={"membership": unicode(membership_action)},
|
||||
room_id=urllib.unquote(room_id),
|
||||
user_id=user.to_string(),
|
||||
state_key=state_key
|
||||
)
|
||||
handler = self.handlers.room_member_handler
|
||||
yield handler.change_membership(event)
|
||||
defer.returnValue((200, ""))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def on_PUT(self, request, room_id, membership_action, txn_id):
|
||||
try:
|
||||
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||
except:
|
||||
pass
|
||||
|
||||
response = yield self.on_POST(request, room_id, membership_action)
|
||||
|
||||
self.txns.store_client_transaction(request, txn_id, response)
|
||||
defer.returnValue(response)
|
||||
|
||||
|
||||
def _parse_json(request):
|
||||
try:
|
||||
content = json.loads(request.content.read())
|
||||
|
@ -411,9 +422,32 @@ def _parse_json(request):
|
|||
raise SynapseError(400, "Content not JSON.", errcode=Codes.NOT_JSON)
|
||||
|
||||
|
||||
def register_txn_path(servlet, regex_string, http_server):
|
||||
"""Registers a transaction-based path.
|
||||
|
||||
This registers two paths:
|
||||
PUT regex_string/$txnid
|
||||
POST regex_string
|
||||
|
||||
Args:
|
||||
regex_string (str): The regex string to register. Must NOT have a
|
||||
trailing $ as this string will be appended to.
|
||||
http_server : The http_server to register paths with.
|
||||
"""
|
||||
http_server.register_path(
|
||||
"POST",
|
||||
client_path_pattern(regex_string + "$"),
|
||||
servlet.on_POST
|
||||
)
|
||||
http_server.register_path(
|
||||
"PUT",
|
||||
client_path_pattern(regex_string + "/(?P<txn_id>[^/]*)$"),
|
||||
servlet.on_PUT
|
||||
)
|
||||
|
||||
|
||||
def register_servlets(hs, http_server):
|
||||
RoomTopicRestServlet(hs).register(http_server)
|
||||
RoomMemberRestServlet(hs).register(http_server)
|
||||
RoomStateEventRestServlet(hs).register(http_server)
|
||||
MessageRestServlet(hs).register(http_server)
|
||||
FeedbackRestServlet(hs).register(http_server)
|
||||
RoomCreateRestServlet(hs).register(http_server)
|
||||
|
@ -421,3 +455,4 @@ def register_servlets(hs, http_server):
|
|||
RoomMessageListRestServlet(hs).register(http_server)
|
||||
JoinRoomAliasServlet(hs).register(http_server)
|
||||
RoomTriggerBackfill(hs).register(http_server)
|
||||
RoomMembershipRestServlet(hs).register(http_server)
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Copyright 2014 matrix.org
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
"""This module contains logic for storing HTTP PUT transactions. This is used
|
||||
to ensure idempotency when performing PUTs using the REST API."""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HttpTransactionStore(object):
|
||||
|
||||
def __init__(self):
|
||||
# { key : (txn_id, response) }
|
||||
self.transactions = {}
|
||||
|
||||
def get_response(self, key, txn_id):
|
||||
"""Retrieve a response for this request.
|
||||
|
||||
Args:
|
||||
key (str): A transaction-independent key for this request. Typically
|
||||
this is a combination of the path (without the transaction id) and
|
||||
the user's access token.
|
||||
txn_id (str): The transaction ID for this request
|
||||
Returns:
|
||||
A tuple of (HTTP response code, response content) or None.
|
||||
"""
|
||||
try:
|
||||
logger.debug("get_response Key: %s TxnId: %s", key, txn_id)
|
||||
(last_txn_id, response) = self.transactions[key]
|
||||
if txn_id == last_txn_id:
|
||||
logger.info("get_response: Returning a response for %s", key)
|
||||
return response
|
||||
except KeyError:
|
||||
pass
|
||||
return None
|
||||
|
||||
def store_response(self, key, txn_id, response):
|
||||
"""Stores an HTTP response tuple.
|
||||
|
||||
Args:
|
||||
key (str): A transaction-independent key for this request. Typically
|
||||
this is a combination of the path (without the transaction id) and
|
||||
the user's access token.
|
||||
txn_id (str): The transaction ID for this request.
|
||||
response (tuple): A tuple of (HTTP response code, response content)
|
||||
"""
|
||||
logger.debug("store_response Key: %s TxnId: %s", key, txn_id)
|
||||
self.transactions[key] = (txn_id, response)
|
||||
|
||||
def store_client_transaction(self, request, txn_id, response):
|
||||
"""Stores the request/response pair of an HTTP transaction.
|
||||
|
||||
Args:
|
||||
request (twisted.web.http.Request): The twisted HTTP request. This
|
||||
request must have the transaction ID as the last path segment.
|
||||
response (tuple): A tuple of (response code, response dict)
|
||||
txn_id (str): The transaction ID for this request.
|
||||
"""
|
||||
self.store_response(self._get_key(request), txn_id, response)
|
||||
|
||||
def get_client_transaction(self, request, txn_id):
|
||||
"""Retrieves a stored response if there was one.
|
||||
|
||||
Args:
|
||||
request (twisted.web.http.Request): The twisted HTTP request. This
|
||||
request must have the transaction ID as the last path segment.
|
||||
txn_id (str): The transaction ID for this request.
|
||||
Returns:
|
||||
The response tuple.
|
||||
Raises:
|
||||
KeyError if the transaction was not found.
|
||||
"""
|
||||
response = self.get_response(self._get_key(request), txn_id)
|
||||
if response is None:
|
||||
raise KeyError("Transaction not found.")
|
||||
return response
|
||||
|
||||
def _get_key(self, request):
|
||||
token = request.args["access_token"][0]
|
||||
path_without_txn_id = request.path.rsplit("/", 1)[0]
|
||||
return path_without_txn_id + "/" + token
|
||||
|
||||
|
|
@ -159,7 +159,7 @@ class HomeServer(BaseHomeServer):
|
|||
return DataStore(self)
|
||||
|
||||
def build_event_factory(self):
|
||||
return EventFactory()
|
||||
return EventFactory(self)
|
||||
|
||||
def build_handlers(self):
|
||||
return Handlers(self)
|
||||
|
|
|
@ -16,8 +16,9 @@
|
|||
from twisted.internet import defer
|
||||
|
||||
from synapse.api.events.room import (
|
||||
RoomMemberEvent, MessageEvent, RoomTopicEvent, FeedbackEvent,
|
||||
RoomConfigEvent, RoomNameEvent,
|
||||
RoomMemberEvent, RoomTopicEvent, FeedbackEvent,
|
||||
# RoomConfigEvent,
|
||||
RoomNameEvent,
|
||||
)
|
||||
|
||||
from synapse.util.logutils import log_function
|
||||
|
@ -116,6 +117,11 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||
if stream_ordering is not None:
|
||||
vals["stream_ordering"] = stream_ordering
|
||||
|
||||
if hasattr(event, "outlier"):
|
||||
vals["outlier"] = event.outlier
|
||||
else:
|
||||
vals["outlier"] = False
|
||||
|
||||
unrec = {
|
||||
k: v
|
||||
for k, v in event.get_full_dict().items()
|
||||
|
|
|
@ -296,6 +296,11 @@ class SQLBaseStore(object):
|
|||
|
||||
def _parse_event_from_row(self, row_dict):
|
||||
d = copy.deepcopy({k: v for k, v in row_dict.items() if v})
|
||||
|
||||
d.pop("stream_ordering", None)
|
||||
d.pop("topological_ordering", None)
|
||||
d.pop("processed", None)
|
||||
|
||||
d.update(json.loads(row_dict["unrecognized_keys"]))
|
||||
d["content"] = json.loads(d["content"])
|
||||
del d["unrecognized_keys"]
|
||||
|
|
|
@ -15,11 +15,7 @@
|
|||
|
||||
from twisted.internet import defer
|
||||
|
||||
from ._base import SQLBaseStore, Table
|
||||
from synapse.api.events.room import FeedbackEvent
|
||||
|
||||
import collections
|
||||
import json
|
||||
from ._base import SQLBaseStore
|
||||
|
||||
|
||||
class FeedbackStore(SQLBaseStore):
|
||||
|
|
|
@ -18,12 +18,10 @@ from twisted.internet import defer
|
|||
from sqlite3 import IntegrityError
|
||||
|
||||
from synapse.api.errors import StoreError
|
||||
from synapse.api.events.room import RoomTopicEvent
|
||||
|
||||
from ._base import SQLBaseStore, Table
|
||||
|
||||
import collections
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
|
@ -15,15 +15,10 @@
|
|||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.types import UserID
|
||||
from ._base import SQLBaseStore
|
||||
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.api.events.room import RoomMemberEvent
|
||||
|
||||
from ._base import SQLBaseStore, Table
|
||||
|
||||
|
||||
import collections
|
||||
import json
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -34,14 +29,15 @@ class RoomMemberStore(SQLBaseStore):
|
|||
def _store_room_member_txn(self, txn, event):
|
||||
"""Store a room member in the database.
|
||||
"""
|
||||
domain = self.hs.parse_userid(event.target_user_id).domain
|
||||
target_user_id = event.state_key
|
||||
domain = self.hs.parse_userid(target_user_id).domain
|
||||
|
||||
self._simple_insert_txn(
|
||||
txn,
|
||||
"room_memberships",
|
||||
{
|
||||
"event_id": event.event_id,
|
||||
"user_id": event.target_user_id,
|
||||
"user_id": target_user_id,
|
||||
"sender": event.user_id,
|
||||
"room_id": event.room_id,
|
||||
"membership": event.membership,
|
||||
|
@ -145,7 +141,28 @@ class RoomMemberStore(SQLBaseStore):
|
|||
|
||||
rows = yield self._execute_and_decode(sql, *where_values)
|
||||
|
||||
logger.debug("_get_members_query Got rows %s", rows)
|
||||
# logger.debug("_get_members_query Got rows %s", rows)
|
||||
|
||||
results = [self._parse_event_from_row(r) for r in rows]
|
||||
defer.returnValue(results)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def do_users_share_a_room(self, user_list):
|
||||
""" Checks whether a list of users share a room.
|
||||
"""
|
||||
user_list_clause = " OR ".join(["m.user_id = ?"] * len(user_list))
|
||||
sql = (
|
||||
"SELECT m.room_id FROM room_memberships as m "
|
||||
"INNER JOIN current_state_events as c "
|
||||
"ON m.event_id = c.event_id "
|
||||
"WHERE m.membership = 'join' "
|
||||
"AND (%(clause)s) "
|
||||
"GROUP BY m.room_id HAVING COUNT(m.room_id) = ?"
|
||||
) % {"clause": user_list_clause}
|
||||
|
||||
args = user_list
|
||||
args.append(len(user_list))
|
||||
|
||||
rows = yield self._execute(None, sql, *args)
|
||||
|
||||
defer.returnValue(len(rows) > 0)
|
||||
|
|
|
@ -22,9 +22,15 @@ CREATE TABLE IF NOT EXISTS events(
|
|||
content TEXT NOT NULL,
|
||||
unrecognized_keys TEXT,
|
||||
processed BOOL NOT NULL,
|
||||
outlier BOOL NOT NULL,
|
||||
CONSTRAINT ev_uniq UNIQUE (event_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS events_event_id ON events (event_id);
|
||||
CREATE INDEX IF NOT EXISTS events_stream_ordering ON events (stream_ordering);
|
||||
CREATE INDEX IF NOT EXISTS events_topological_ordering ON events (topological_ordering);
|
||||
CREATE INDEX IF NOT EXISTS events_room_id ON events (room_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS state_events(
|
||||
event_id TEXT NOT NULL,
|
||||
room_id TEXT NOT NULL,
|
||||
|
@ -33,6 +39,12 @@ CREATE TABLE IF NOT EXISTS state_events(
|
|||
prev_state TEXT
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS state_events_event_id ON state_events (event_id);
|
||||
CREATE INDEX IF NOT EXISTS state_events_room_id ON state_events (room_id);
|
||||
CREATE INDEX IF NOT EXISTS state_events_type ON state_events (type);
|
||||
CREATE INDEX IF NOT EXISTS state_events_state_key ON state_events (state_key);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS current_state_events(
|
||||
event_id TEXT NOT NULL,
|
||||
room_id TEXT NOT NULL,
|
||||
|
@ -41,6 +53,11 @@ CREATE TABLE IF NOT EXISTS current_state_events(
|
|||
CONSTRAINT curr_uniq UNIQUE (room_id, type, state_key) ON CONFLICT REPLACE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS curr_events_event_id ON current_state_events (event_id);
|
||||
CREATE INDEX IF NOT EXISTS current_state_events_room_id ON current_state_events (room_id);
|
||||
CREATE INDEX IF NOT EXISTS current_state_events_type ON current_state_events (type);
|
||||
CREATE INDEX IF NOT EXISTS current_state_events_state_key ON current_state_events (state_key);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS room_memberships(
|
||||
event_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
|
@ -49,6 +66,10 @@ CREATE TABLE IF NOT EXISTS room_memberships(
|
|||
membership TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS room_memberships_event_id ON room_memberships (event_id);
|
||||
CREATE INDEX IF NOT EXISTS room_memberships_room_id ON room_memberships (room_id);
|
||||
CREATE INDEX IF NOT EXISTS room_memberships_user_id ON room_memberships (user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS feedback(
|
||||
event_id TEXT NOT NULL,
|
||||
feedback_type TEXT,
|
||||
|
@ -77,5 +98,6 @@ CREATE TABLE IF NOT EXISTS rooms(
|
|||
|
||||
CREATE TABLE IF NOT EXISTS room_hosts(
|
||||
room_id TEXT NOT NULL,
|
||||
host TEXT NOT NULL
|
||||
host TEXT NOT NULL,
|
||||
CONSTRAINT room_hosts_uniq UNIQUE (room_id, host) ON CONFLICT IGNORE
|
||||
);
|
||||
|
|
|
@ -37,10 +37,8 @@ from twisted.internet import defer
|
|||
|
||||
from ._base import SQLBaseStore
|
||||
from synapse.api.errors import SynapseError
|
||||
from synapse.api.constants import Membership
|
||||
from synapse.util.logutils import log_function
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
||||
|
||||
|
@ -177,6 +175,7 @@ class StreamStore(SQLBaseStore):
|
|||
"((room_id IN (%(current)s)) OR "
|
||||
"(event_id IN (%(invites)s))) "
|
||||
"AND e.stream_ordering > ? AND e.stream_ordering < ? "
|
||||
"AND e.outlier = 0 "
|
||||
"ORDER BY stream_ordering ASC LIMIT %(limit)d "
|
||||
) % {
|
||||
"current": current_room_membership_sql,
|
||||
|
@ -224,7 +223,7 @@ class StreamStore(SQLBaseStore):
|
|||
|
||||
sql = (
|
||||
"SELECT * FROM events "
|
||||
"WHERE room_id = ? AND %(bounds)s "
|
||||
"WHERE outlier = 0 AND room_id = ? AND %(bounds)s "
|
||||
"ORDER BY topological_ordering %(order)s, stream_ordering %(order)s %(limit)s "
|
||||
) % {"bounds": bounds, "order": order, "limit": limit_str}
|
||||
|
||||
|
@ -249,15 +248,14 @@ class StreamStore(SQLBaseStore):
|
|||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def get_recent_events_for_room(self, room_id, limit, with_feedback=False):
|
||||
def get_recent_events_for_room(self, room_id, limit, end_token,
|
||||
with_feedback=False):
|
||||
# TODO (erikj): Handle compressed feedback
|
||||
|
||||
end_token = yield self.get_room_events_max_id()
|
||||
|
||||
sql = (
|
||||
"SELECT * FROM events "
|
||||
"WHERE room_id = ? AND stream_ordering <= ? "
|
||||
"ORDER BY topological_ordering, stream_ordering DESC LIMIT ? "
|
||||
"ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? "
|
||||
)
|
||||
|
||||
rows = yield self._execute_and_decode(
|
||||
|
|
|
@ -99,7 +99,6 @@ class FederationTestCase(unittest.TestCase):
|
|||
new_event = mem_handler.change_membership.call_args[0][0]
|
||||
self.assertEquals(RoomMemberEvent.TYPE, new_event.type)
|
||||
self.assertEquals(room_id, new_event.room_id)
|
||||
self.assertEquals(user_id, new_event.target_user_id)
|
||||
self.assertEquals(user_id, new_event.state_key)
|
||||
self.assertEquals(Membership.JOIN, new_event.membership)
|
||||
|
||||
|
|
|
@ -190,6 +190,7 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||
),
|
||||
SynapseError
|
||||
)
|
||||
test_get_disallowed_state.skip = "Presence polling is disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_set_my_state(self):
|
||||
|
@ -214,6 +215,7 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||
state={"state": OFFLINE})
|
||||
|
||||
self.mock_stop.assert_called_with(self.u_apple)
|
||||
test_set_my_state.skip = "Presence polling is disabled"
|
||||
|
||||
|
||||
class PresenceInvitesTestCase(unittest.TestCase):
|
||||
|
@ -653,6 +655,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||
observed_user=self.u_banana,
|
||||
statuscache=ANY), # self-reflection
|
||||
]) # and no others...
|
||||
test_push_local.skip = "Presence polling is disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_push_remote(self):
|
||||
|
@ -704,6 +707,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||
)
|
||||
|
||||
yield put_json.await_calls()
|
||||
test_push_remote.skip = "Presence polling is disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_recv_remote(self):
|
||||
|
@ -996,6 +1000,8 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||
|
||||
self.assertFalse("banana" in self.handler._local_pushmap)
|
||||
self.assertFalse("clementine" in self.handler._local_pushmap)
|
||||
test_push_local.skip = "Presence polling is disabled"
|
||||
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_remote_poll_send(self):
|
||||
|
@ -1044,6 +1050,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||
put_json.await_calls()
|
||||
|
||||
self.assertFalse(self.u_potato in self.handler._remote_recvmap)
|
||||
test_remote_poll_send.skip = "Presence polling is disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_remote_poll_receive(self):
|
||||
|
|
|
@ -135,6 +135,7 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||
|
||||
mocked_set.assert_called_with("apple",
|
||||
{"state": UNAVAILABLE, "status_msg": "Away"})
|
||||
test_set_my_state.skip = "Presence polling is disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_push_local(self):
|
||||
|
@ -209,6 +210,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||
"displayname": "I am an Apple",
|
||||
"avatar_url": "http://foo",
|
||||
}, statuscache.state)
|
||||
test_push_local.skip = "Presence polling is disabled"
|
||||
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_push_remote(self):
|
||||
|
@ -239,6 +242,7 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||
],
|
||||
},
|
||||
)
|
||||
test_push_remote.skip = "Presence polling is disabled"
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_recv_remote(self):
|
||||
|
|
|
@ -90,7 +90,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
|||
event = self.hs.get_event_factory().create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
user_id=user_id,
|
||||
target_user_id=target_user_id,
|
||||
state_key=target_user_id,
|
||||
room_id=room_id,
|
||||
membership=Membership.INVITE,
|
||||
content=content,
|
||||
|
@ -143,7 +143,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
|||
event = self.hs.get_event_factory().create_event(
|
||||
etype=RoomMemberEvent.TYPE,
|
||||
user_id=user_id,
|
||||
target_user_id=target_user_id,
|
||||
state_key=target_user_id,
|
||||
room_id=room_id,
|
||||
membership=Membership.JOIN,
|
||||
content=content,
|
||||
|
@ -374,7 +374,7 @@ class RoomCreationTest(unittest.TestCase):
|
|||
self.assertEquals(RoomMemberEvent.TYPE, join_event.type)
|
||||
self.assertEquals(room_id, join_event.room_id)
|
||||
self.assertEquals(user_id, join_event.user_id)
|
||||
self.assertEquals(user_id, join_event.target_user_id)
|
||||
self.assertEquals(user_id, join_event.state_key)
|
||||
|
||||
self.assertTrue(self.state_handler.handle_new_event.called)
|
||||
|
||||
|
|
|
@ -114,6 +114,7 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||
self.assertEquals(200, code)
|
||||
mocked_set.assert_called_with("apple",
|
||||
{"state": UNAVAILABLE, "status_msg": "Away"})
|
||||
test_set_my_status.skip = "Presence polling is disabled"
|
||||
|
||||
|
||||
class PresenceListTestCase(unittest.TestCase):
|
||||
|
@ -309,3 +310,4 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
|||
"mtime_age": 0,
|
||||
}},
|
||||
]}, response)
|
||||
test_shortpoll.skip = "Presence polling is disabled"
|
||||
|
|
|
@ -94,7 +94,7 @@ class RoomPermissionsTestCase(RestTestCase):
|
|||
# set topic for public room
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT",
|
||||
"/rooms/%s/topic" % self.created_public_rmid,
|
||||
"/rooms/%s/state/m.room.topic" % self.created_public_rmid,
|
||||
'{"topic":"Public Room Topic"}')
|
||||
self.assertEquals(200, code, msg=str(response))
|
||||
|
||||
|
@ -175,15 +175,15 @@ class RoomPermissionsTestCase(RestTestCase):
|
|||
@defer.inlineCallbacks
|
||||
def test_topic_perms(self):
|
||||
topic_content = '{"topic":"My Topic Name"}'
|
||||
topic_path = "/rooms/%s/topic" % self.created_rmid
|
||||
topic_path = "/rooms/%s/state/m.room.topic" % self.created_rmid
|
||||
|
||||
# set/get topic in uncreated room, expect 403
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT", "/rooms/%s/topic" % self.uncreated_rmid,
|
||||
"PUT", "/rooms/%s/state/m.room.topic" % self.uncreated_rmid,
|
||||
topic_content)
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
"/rooms/%s/topic" % self.uncreated_rmid)
|
||||
"/rooms/%s/state/m.room.topic" % self.uncreated_rmid)
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
|
||||
# set/get topic in created PRIVATE room not joined, expect 403
|
||||
|
@ -223,19 +223,19 @@ class RoomPermissionsTestCase(RestTestCase):
|
|||
|
||||
# get topic in PUBLIC room, not joined, expect 200 (or 404)
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
"/rooms/%s/topic" % self.created_public_rmid)
|
||||
"/rooms/%s/state/m.room.topic" % self.created_public_rmid)
|
||||
self.assertEquals(200, code, msg=str(response))
|
||||
|
||||
# set topic in PUBLIC room, not joined, expect 403
|
||||
(code, response) = yield self.mock_resource.trigger(
|
||||
"PUT",
|
||||
"/rooms/%s/topic" % self.created_public_rmid,
|
||||
"/rooms/%s/state/m.room.topic" % self.created_public_rmid,
|
||||
topic_content)
|
||||
self.assertEquals(403, code, msg=str(response))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def _test_get_membership(self, room=None, members=[], expect_code=None):
|
||||
path = "/rooms/%s/members/%s/state"
|
||||
path = "/rooms/%s/state/m.room.member/%s"
|
||||
for member in members:
|
||||
(code, response) = yield self.mock_resource.trigger_get(
|
||||
path %
|
||||
|
@ -291,12 +291,12 @@ class RoomPermissionsTestCase(RestTestCase):
|
|||
def test_membership_public_room_perms(self):
|
||||
room = self.created_public_rmid
|
||||
# get membership of self, get membership of other, public room + invite
|
||||
# expect all 403s
|
||||
# expect all 200s - public rooms, you can see who is in them.
|
||||
yield self.invite(room=room, src=self.rmcreator_id,
|
||||
targ=self.user_id)
|
||||
yield self._test_get_membership(
|
||||
members=[self.user_id, self.rmcreator_id],
|
||||
room=room, expect_code=403)
|
||||
room=room, expect_code=200)
|
||||
|
||||
# get membership of self, get membership of other, public room + joined
|
||||
# expect all 200s
|
||||
|
@ -306,11 +306,11 @@ class RoomPermissionsTestCase(RestTestCase):
|
|||
room=room, expect_code=200)
|
||||
|
||||
# get membership of self, get membership of other, public room + left
|
||||
# expect all 403s
|
||||
# expect all 200s - public rooms, you can always see who is in them.
|
||||
yield self.leave(room=room, user=self.user_id)
|
||||
yield self._test_get_membership(
|
||||
members=[self.user_id, self.rmcreator_id],
|
||||
room=room, expect_code=403)
|
||||
room=room, expect_code=200)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_invited_permissions(self):
|
||||
|
@ -614,7 +614,7 @@ class RoomTopicTestCase(RestTestCase):
|
|||
self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
|
||||
self.auth_user_id = self.user_id
|
||||
self.room_id = "!rid1:test"
|
||||
self.path = "/rooms/%s/topic" % self.room_id
|
||||
self.path = "/rooms/%s/state/m.room.topic" % self.room_id
|
||||
|
||||
state_handler = Mock(spec=["handle_new_event"])
|
||||
state_handler.handle_new_event.return_value = True
|
||||
|
@ -749,7 +749,7 @@ class RoomMemberStateTestCase(RestTestCase):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def test_invalid_puts(self):
|
||||
path = "/rooms/%s/members/%s/state" % (self.room_id, self.user_id)
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (self.room_id, self.user_id)
|
||||
# missing keys or invalid json
|
||||
(code, response) = yield self.mock_resource.trigger("PUT",
|
||||
path, '{}')
|
||||
|
@ -783,7 +783,7 @@ class RoomMemberStateTestCase(RestTestCase):
|
|||
|
||||
@defer.inlineCallbacks
|
||||
def test_rooms_members_self(self):
|
||||
path = "/rooms/%s/members/%s/state" % (
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (
|
||||
urllib.quote(self.room_id), self.user_id
|
||||
)
|
||||
|
||||
|
@ -804,7 +804,7 @@ class RoomMemberStateTestCase(RestTestCase):
|
|||
@defer.inlineCallbacks
|
||||
def test_rooms_members_other(self):
|
||||
self.other_id = "@zzsid1:red"
|
||||
path = "/rooms/%s/members/%s/state" % (
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (
|
||||
urllib.quote(self.room_id), self.other_id
|
||||
)
|
||||
|
||||
|
@ -820,7 +820,7 @@ class RoomMemberStateTestCase(RestTestCase):
|
|||
@defer.inlineCallbacks
|
||||
def test_rooms_members_other_custom_keys(self):
|
||||
self.other_id = "@zzsid1:red"
|
||||
path = "/rooms/%s/members/%s/state" % (
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (
|
||||
urllib.quote(self.room_id), self.other_id
|
||||
)
|
||||
|
||||
|
|
|
@ -21,6 +21,7 @@ from twisted.trial import unittest
|
|||
|
||||
from synapse.api.constants import Membership
|
||||
|
||||
import json
|
||||
import time
|
||||
|
||||
class RestTestCase(unittest.TestCase):
|
||||
|
@ -71,23 +72,22 @@ class RestTestCase(unittest.TestCase):
|
|||
expect_code=expect_code)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def change_membership(self, room=None, src=None, targ=None,
|
||||
membership=None, expect_code=200, tok=None):
|
||||
def change_membership(self, room, src, targ, membership, tok=None,
|
||||
expect_code=200):
|
||||
temp_id = self.auth_user_id
|
||||
self.auth_user_id = src
|
||||
|
||||
path = "/rooms/%s/members/%s/state" % (room, targ)
|
||||
path = "/rooms/%s/state/m.room.member/%s" % (room, targ)
|
||||
if tok:
|
||||
path = path + "?access_token=%s" % tok
|
||||
|
||||
if membership == Membership.LEAVE:
|
||||
(code, response) = yield self.mock_resource.trigger("DELETE", path,
|
||||
None)
|
||||
self.assertEquals(expect_code, code, msg=str(response))
|
||||
else:
|
||||
(code, response) = yield self.mock_resource.trigger("PUT", path,
|
||||
'{"membership":"%s"}' % membership)
|
||||
self.assertEquals(expect_code, code, msg=str(response))
|
||||
data = {
|
||||
"membership": membership
|
||||
}
|
||||
|
||||
(code, response) = yield self.mock_resource.trigger("PUT", path,
|
||||
json.dumps(data))
|
||||
self.assertEquals(expect_code, code, msg=str(response))
|
||||
|
||||
self.auth_user_id = temp_id
|
||||
|
||||
|
|
|
@ -190,7 +190,7 @@ class MemoryDataStore(object):
|
|||
def persist_event(self, event):
|
||||
if event.type == RoomMemberEvent.TYPE:
|
||||
room_id = event.room_id
|
||||
user = event.target_user_id
|
||||
user = event.state_key
|
||||
membership = event.membership
|
||||
self.members.setdefault(room_id, {})[user] = event
|
||||
|
||||
|
@ -203,7 +203,9 @@ class MemoryDataStore(object):
|
|||
def get_current_state(self, room_id, event_type=None, state_key=""):
|
||||
if event_type:
|
||||
key = (room_id, event_type, state_key)
|
||||
return self.current_state.get(key)
|
||||
if self.current_state.get(key):
|
||||
return [self.current_state.get(key)]
|
||||
return None
|
||||
else:
|
||||
return [
|
||||
e for e in self.current_state
|
||||
|
@ -221,7 +223,7 @@ class MemoryDataStore(object):
|
|||
|
||||
def _format_call(args, kwargs):
|
||||
return ", ".join(
|
||||
["%r" % (a) for a in args] +
|
||||
["%r" % (a) for a in args] +
|
||||
["%s=%r" % (k, v) for k, v in kwargs.items()]
|
||||
)
|
||||
|
||||
|
|
|
@ -20,9 +20,9 @@ limitations under the License.
|
|||
|
||||
'use strict';
|
||||
|
||||
angular.module('MatrixWebClientController', ['matrixService'])
|
||||
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'eventStreamService',
|
||||
function($scope, $location, $rootScope, matrixService, eventStreamService) {
|
||||
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
|
||||
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService',
|
||||
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService) {
|
||||
|
||||
// Check current URL to avoid to display the logout button on the login page
|
||||
$scope.location = $location.path();
|
||||
|
@ -31,42 +31,31 @@ angular.module('MatrixWebClientController', ['matrixService'])
|
|||
$rootScope.$on('$routeChangeSuccess', function (event, current, previous) {
|
||||
$scope.location = $location.path();
|
||||
});
|
||||
|
||||
|
||||
// Manage the display of the current config
|
||||
$scope.config;
|
||||
|
||||
// Toggles the config display
|
||||
$scope.showConfig = function() {
|
||||
if ($scope.config) {
|
||||
$scope.config = undefined;
|
||||
}
|
||||
else {
|
||||
$scope.config = matrixService.config();
|
||||
}
|
||||
};
|
||||
|
||||
$scope.closeConfig = function() {
|
||||
if ($scope.config) {
|
||||
$scope.config = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
if (matrixService.isUserLoggedIn()) {
|
||||
eventStreamService.resume();
|
||||
// eventStreamService.resume();
|
||||
mPresence.start();
|
||||
}
|
||||
|
||||
$scope.go = function(url) {
|
||||
$location.url(url);
|
||||
};
|
||||
|
||||
// Logs the user out
|
||||
$scope.logout = function() {
|
||||
|
||||
// kill the event stream
|
||||
eventStreamService.stop();
|
||||
|
||||
|
||||
// Do not update presence anymore
|
||||
mPresence.stop();
|
||||
|
||||
// Clean permanent data
|
||||
matrixService.setConfig({});
|
||||
matrixService.saveConfig();
|
||||
|
||||
// And go to the login page
|
||||
$location.path("login");
|
||||
$location.url("login");
|
||||
};
|
||||
|
||||
// Listen to the event indicating that the access token is no longer valid.
|
||||
|
@ -83,7 +72,6 @@ angular.module('MatrixWebClientController', ['matrixService'])
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
}]);
|
||||
|
||||
|
||||
|
|
|
@ -54,12 +54,15 @@ angular.module('matrixWebClient')
|
|||
});
|
||||
|
||||
// FIXME: we shouldn't disambiguate displayNames on every orderMembersList
|
||||
// invocation but keep track of duplicates incrementally somewhere
|
||||
// invocation but keep track of duplicates incrementally somewhere
|
||||
angular.forEach(displayNames, function(value, key) {
|
||||
if (value.length > 1) {
|
||||
// console.log(key + ": " + value);
|
||||
for (i=0; i < value.length; i++) {
|
||||
for (var i=0; i < value.length; i++) {
|
||||
var v = value[i];
|
||||
// FIXME: this permenantly rewrites the displayname for a given
|
||||
// room member. which means we can't reset their name if it is
|
||||
// no longer ambiguous!
|
||||
members[v].displayname += " (" + v + ")";
|
||||
// console.log(v + " " + members[v]);
|
||||
};
|
||||
|
|
|
@ -1,3 +1,71 @@
|
|||
/*** Mobile voodoo ***/
|
||||
@media all and (max-device-width: 640px) {
|
||||
|
||||
#messageTableWrapper {
|
||||
margin-right: 0px ! important;
|
||||
}
|
||||
|
||||
.leftBlock {
|
||||
width: 8em ! important;
|
||||
}
|
||||
|
||||
#header,
|
||||
#messageTable,
|
||||
#wrapper,
|
||||
#roomName,
|
||||
#controls {
|
||||
max-width: 640px ! important;
|
||||
}
|
||||
|
||||
#userIdCell,
|
||||
#usersTableWrapper,
|
||||
#extraControls {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#buttonsCell {
|
||||
width: 60px ! important;
|
||||
padding-left: 20px ! important;
|
||||
}
|
||||
|
||||
#roomLogo {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#roomName {
|
||||
text-align: left ! important;
|
||||
top: -35px ! important;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
font-size: 12px ! important;
|
||||
min-height: 20px ! important;
|
||||
}
|
||||
|
||||
#page {
|
||||
top: 35px ! important;
|
||||
bottom: 70px ! important;
|
||||
}
|
||||
|
||||
#header,
|
||||
#page {
|
||||
margin: 5px ! important;
|
||||
}
|
||||
|
||||
#header {
|
||||
padding: 5px ! important;
|
||||
}
|
||||
|
||||
/* stop zoom on select */
|
||||
select:focus,
|
||||
textarea,
|
||||
input
|
||||
{
|
||||
font-size: 16px ! important;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Myriad Pro", "Myriad", Helvetica, Arial, sans-serif;
|
||||
font-size: 12pt;
|
||||
|
@ -17,7 +85,6 @@ h1 {
|
|||
left: 0px;
|
||||
right: 0px;
|
||||
margin: 20px;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
#wrapper {
|
||||
|
@ -32,8 +99,7 @@ h1 {
|
|||
text-align: right;
|
||||
top: -40px;
|
||||
position: absolute;
|
||||
font-size: 16pt;
|
||||
margin-bottom: 10px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
#controlPanel {
|
||||
|
@ -50,6 +116,10 @@ h1 {
|
|||
margin: auto;
|
||||
}
|
||||
|
||||
#buttonsCell {
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#inputBarTable {
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -66,6 +136,10 @@ h1 {
|
|||
background-color: #faa;
|
||||
}
|
||||
|
||||
.mouse-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*** Participant list ***/
|
||||
|
||||
#usersTableWrapper {
|
||||
|
@ -89,7 +163,6 @@ h1 {
|
|||
height: 100px;
|
||||
position: relative;
|
||||
background-color: #000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.userAvatar .userAvatarImage {
|
||||
|
@ -108,13 +181,13 @@ h1 {
|
|||
color: #fff;
|
||||
margin: 2px;
|
||||
bottom: 0px;
|
||||
font-size: 8pt;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.userPresence {
|
||||
text-align: center;
|
||||
font-size: 8pt;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background-color: #aaa;
|
||||
border-bottom: 1px #ddd solid;
|
||||
|
@ -142,6 +215,7 @@ h1 {
|
|||
max-width: 1280px;
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
#messageTable td {
|
||||
|
@ -149,12 +223,13 @@ h1 {
|
|||
}
|
||||
|
||||
.leftBlock {
|
||||
width: 10em;
|
||||
width: 14em;
|
||||
word-wrap: break-word;
|
||||
vertical-align: top;
|
||||
background-color: #fff;
|
||||
color: #888;
|
||||
font-weight: medium;
|
||||
font-size: 8pt;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
border-top: 1px #ddd solid;
|
||||
}
|
||||
|
@ -187,24 +262,13 @@ h1 {
|
|||
object-fit: cover;
|
||||
}
|
||||
|
||||
.text {
|
||||
background-color: #eee;
|
||||
border: 1px solid #d8d8d8;
|
||||
height: 31px;
|
||||
display: inline-table;
|
||||
max-width: 90%;
|
||||
font-size: 16px;
|
||||
/* word-wrap: break-word; */
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.emote {
|
||||
background-color: #fff ! important;
|
||||
background-color: transparent ! important;
|
||||
border: 0px ! important;
|
||||
}
|
||||
|
||||
.membership {
|
||||
background-color: #fff ! important;
|
||||
background-color: transparent ! important;
|
||||
border: 0px ! important;
|
||||
}
|
||||
|
||||
|
@ -216,41 +280,74 @@ h1 {
|
|||
height: auto;
|
||||
}
|
||||
|
||||
.text {
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.bubble {
|
||||
background-color: #eee;
|
||||
border: 1px solid #d8d8d8;
|
||||
display: inline-block;
|
||||
margin-bottom: -1px;
|
||||
max-width: 90%;
|
||||
font-size: 16px;
|
||||
word-wrap: break-word;
|
||||
padding-top: 7px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 1em;
|
||||
padding-right: 1em;
|
||||
vertical-align: middle;
|
||||
-webkit-text-size-adjust:100%
|
||||
}
|
||||
|
||||
.differentUser td {
|
||||
padding-top: 5px ! important;
|
||||
margin-top: 5px ! important;
|
||||
padding-bottom: 5px ! important;
|
||||
}
|
||||
|
||||
.mine {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.mine .text {
|
||||
background-color: #f8f8ff ! important;
|
||||
}
|
||||
|
||||
.mine .emote {
|
||||
background-color: #fff ! important;
|
||||
.text.emote .bubble,
|
||||
.text.membership .bubble,
|
||||
.mine .text.emote .bubble,
|
||||
.mine .text.membership .bubble
|
||||
{
|
||||
background-color: transparent ! important;
|
||||
border: 0px ! important;
|
||||
}
|
||||
|
||||
.mine .text .bubble {
|
||||
background-color: #f8f8ff ! important;
|
||||
text-align: left ! important;
|
||||
}
|
||||
|
||||
#room-fullscreen-image {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
height: 0px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#room-fullscreen-image img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
margin: auto;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
/*** Profile ***/
|
||||
|
||||
.profile-avatar {
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
display:table-cell;
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
|
@ -266,13 +363,19 @@ h1 {
|
|||
}
|
||||
|
||||
#user-displayname {
|
||||
font-size: 16pt;
|
||||
font-size: 24px;
|
||||
}
|
||||
/******************************/
|
||||
|
||||
#header {
|
||||
padding-left: 20px;
|
||||
padding-right: 20px;
|
||||
#header
|
||||
{
|
||||
padding: 20px;
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
#logo,
|
||||
#roomLogo {
|
||||
max-width: 1280px;
|
||||
margin: auto;
|
||||
}
|
||||
|
@ -281,18 +384,6 @@ h1 {
|
|||
float: right;
|
||||
}
|
||||
|
||||
#config {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
top: 100px;
|
||||
left: 50%;
|
||||
width: 500px;
|
||||
margin-left: -250px;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background-color: #aaa;
|
||||
}
|
||||
|
||||
.text_entry_section {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
|
|
|
@ -19,7 +19,8 @@ var matrixWebClient = angular.module('matrixWebClient', [
|
|||
'MatrixWebClientController',
|
||||
'LoginController',
|
||||
'RoomController',
|
||||
'RoomsController',
|
||||
'HomeController',
|
||||
'SettingsController',
|
||||
'UserController',
|
||||
'matrixService',
|
||||
'eventStreamService',
|
||||
|
@ -44,16 +45,20 @@ matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
|
|||
templateUrl: 'room/room.html',
|
||||
controller: 'RoomController'
|
||||
}).
|
||||
when('/rooms', {
|
||||
templateUrl: 'rooms/rooms.html',
|
||||
controller: 'RoomsController'
|
||||
when('/', {
|
||||
templateUrl: 'home/home.html',
|
||||
controller: 'HomeController'
|
||||
}).
|
||||
when('/settings', {
|
||||
templateUrl: 'settings/settings.html',
|
||||
controller: 'SettingsController'
|
||||
}).
|
||||
when('/user/:user_matrix_id', {
|
||||
templateUrl: 'user/user.html',
|
||||
controller: 'UserController'
|
||||
}).
|
||||
otherwise({
|
||||
redirectTo: '/rooms'
|
||||
redirectTo: '/'
|
||||
});
|
||||
|
||||
$provide.factory('AccessTokenInterceptor', ['$q', '$rootScope',
|
||||
|
@ -80,6 +85,6 @@ matrixWebClient.run(['$location', 'matrixService', 'eventStreamService', functio
|
|||
$location.path("login");
|
||||
}
|
||||
else {
|
||||
eventStreamService.resume();
|
||||
// eventStreamService.resume();
|
||||
}
|
||||
}]);
|
||||
|
|
|
@ -20,19 +20,20 @@
|
|||
/*
|
||||
* Upload an HTML5 file to a server
|
||||
*/
|
||||
angular.module('mFileUpload', [])
|
||||
.service('mFileUpload', ['matrixService', '$q', function (matrixService, $q) {
|
||||
angular.module('mFileUpload', ['matrixService', 'mUtilities'])
|
||||
.service('mFileUpload', ['$q', 'matrixService', 'mUtilities', function ($q, matrixService, mUtilities) {
|
||||
|
||||
/*
|
||||
* Upload an HTML5 file to a server and returned a promise
|
||||
* Upload an HTML5 file or blob to a server and returned a promise
|
||||
* that will provide the URL of the uploaded file.
|
||||
* @param {File|Blob} file the file data to send
|
||||
*/
|
||||
this.uploadFile = function(file, body) {
|
||||
this.uploadFile = function(file) {
|
||||
var deferred = $q.defer();
|
||||
console.log("Uploading " + file.name + "... to /matrix/content");
|
||||
matrixService.uploadContent(file, body).then(
|
||||
matrixService.uploadContent(file).then(
|
||||
function(response) {
|
||||
var content_url = location.origin + "/matrix/content/" + response.data.content_token;
|
||||
var content_url = response.data.content_token;
|
||||
console.log(" -> Successfully uploaded! Available at " + content_url);
|
||||
deferred.resolve(content_url);
|
||||
},
|
||||
|
@ -44,4 +45,135 @@ angular.module('mFileUpload', [])
|
|||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/*
|
||||
* Upload an image file plus generate a thumbnail of it and upload it so that
|
||||
* we will have all information to fulfill an image message request data.
|
||||
* @param {File} imageFile the imageFile to send
|
||||
* @param {Integer} thumbnailSize the max side size of the thumbnail to create
|
||||
* @returns {promise} A promise that will be resolved by a image message object
|
||||
* ready to be send with the Matrix API
|
||||
*/
|
||||
this.uploadImageAndThumbnail = function(imageFile, thumbnailSize) {
|
||||
var self = this;
|
||||
var deferred = $q.defer();
|
||||
|
||||
console.log("uploadImageAndThumbnail " + imageFile.name + " - thumbnailSize: " + thumbnailSize);
|
||||
|
||||
// The message structure that will be returned in the promise
|
||||
var imageMessage = {
|
||||
msgtype: "m.image",
|
||||
url: undefined,
|
||||
body: {
|
||||
size: undefined,
|
||||
w: undefined,
|
||||
h: undefined,
|
||||
mimetype: undefined
|
||||
},
|
||||
thumbnail_url: undefined,
|
||||
thumbnail_info: {
|
||||
size: undefined,
|
||||
w: undefined,
|
||||
h: undefined,
|
||||
mimetype: undefined
|
||||
}
|
||||
};
|
||||
|
||||
// First, get the image size
|
||||
mUtilities.getImageSize(imageFile).then(
|
||||
function(size) {
|
||||
console.log("image size: " + JSON.stringify(size));
|
||||
|
||||
// The final operation: send imageFile
|
||||
var uploadImage = function() {
|
||||
self.uploadFile(imageFile).then(
|
||||
function(url) {
|
||||
// Update message metadata
|
||||
imageMessage.url = url;
|
||||
imageMessage.body = {
|
||||
size: imageFile.size,
|
||||
w: size.width,
|
||||
h: size.height,
|
||||
mimetype: imageFile.type
|
||||
};
|
||||
|
||||
// If there is no thumbnail (because the original image is smaller than thumbnailSize),
|
||||
// reuse the original image info for thumbnail data
|
||||
if (!imageMessage.thumbnail_url) {
|
||||
imageMessage.thumbnail_url = imageMessage.url;
|
||||
imageMessage.thumbnail_info = imageMessage.body;
|
||||
}
|
||||
|
||||
// We are done
|
||||
deferred.resolve(imageMessage);
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Can't upload image");
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
// Create a thumbnail if the image size exceeds thumbnailSize
|
||||
if (Math.max(size.width, size.height) > thumbnailSize) {
|
||||
console.log(" Creating thumbnail...");
|
||||
mUtilities.resizeImage(imageFile, thumbnailSize).then(
|
||||
function(thumbnailBlob) {
|
||||
|
||||
// Get its size
|
||||
mUtilities.getImageSize(thumbnailBlob).then(
|
||||
function(thumbnailSize) {
|
||||
console.log(" -> Thumbnail size: " + JSON.stringify(thumbnailSize));
|
||||
|
||||
// Upload it to the server
|
||||
self.uploadFile(thumbnailBlob).then(
|
||||
function(thumbnailUrl) {
|
||||
|
||||
// Update image message data
|
||||
imageMessage.thumbnail_url = thumbnailUrl;
|
||||
imageMessage.thumbnail_info = {
|
||||
size: thumbnailBlob.size,
|
||||
w: thumbnailSize.width,
|
||||
h: thumbnailSize.height,
|
||||
mimetype: thumbnailBlob.type
|
||||
};
|
||||
|
||||
// Then, upload the original image
|
||||
uploadImage();
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Can't upload thumbnail");
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Failed to get thumbnail size");
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Failed to create thumbnail: " + error);
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
}
|
||||
else {
|
||||
// No need of thumbnail
|
||||
console.log(" Thumbnail is not required");
|
||||
uploadImage();
|
||||
}
|
||||
|
||||
},
|
||||
function(error) {
|
||||
console.log(" -> Failed to get image size");
|
||||
deferred.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
}]);
|
||||
|
|
|
@ -35,6 +35,8 @@ angular.module('eventHandlerService', [])
|
|||
$rootScope.events = {
|
||||
rooms: {}, // will contain roomId: { messages:[], members:{userid1: event} }
|
||||
};
|
||||
|
||||
$rootScope.presence = {};
|
||||
|
||||
var initRoom = function(room_id) {
|
||||
if (!(room_id in $rootScope.events.rooms)) {
|
||||
|
@ -44,12 +46,14 @@ angular.module('eventHandlerService', [])
|
|||
$rootScope.events.rooms[room_id].members = {};
|
||||
}
|
||||
}
|
||||
|
||||
var reInitRoom = function(room_id) {
|
||||
$rootScope.events.rooms[room_id] = {};
|
||||
$rootScope.events.rooms[room_id].messages = [];
|
||||
$rootScope.events.rooms[room_id].members = {};
|
||||
}
|
||||
|
||||
var handleMessage = function(event, isLiveEvent) {
|
||||
if ("membership_target" in event.content) {
|
||||
event.user_id = event.content.membership_target;
|
||||
}
|
||||
|
||||
initRoom(event.room_id);
|
||||
|
||||
if (isLiveEvent) {
|
||||
|
@ -69,11 +73,23 @@ angular.module('eventHandlerService', [])
|
|||
|
||||
var handleRoomMember = function(event, isLiveEvent) {
|
||||
initRoom(event.room_id);
|
||||
|
||||
// add membership changes as if they were a room message if something interesting changed
|
||||
if (event.content.prev !== event.content.membership) {
|
||||
if (isLiveEvent) {
|
||||
$rootScope.events.rooms[event.room_id].messages.push(event);
|
||||
}
|
||||
else {
|
||||
$rootScope.events.rooms[event.room_id].messages.unshift(event);
|
||||
}
|
||||
}
|
||||
|
||||
$rootScope.events.rooms[event.room_id].members[event.user_id] = event;
|
||||
$rootScope.$broadcast(MEMBER_EVENT, event, isLiveEvent);
|
||||
};
|
||||
|
||||
var handlePresence = function(event, isLiveEvent) {
|
||||
$rootScope.presence[event.content.user_id] = event;
|
||||
$rootScope.$broadcast(PRESENCE_EVENT, event, isLiveEvent);
|
||||
};
|
||||
|
||||
|
@ -107,6 +123,10 @@ angular.module('eventHandlerService', [])
|
|||
for (var i=0; i<events.length; i++) {
|
||||
this.handleEvent(events[i], isLiveEvents);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reInitRoom: function(room_id) {
|
||||
reInitRoom(room_id);
|
||||
},
|
||||
};
|
||||
}]);
|
||||
|
|
|
@ -48,11 +48,12 @@ angular.module('eventStreamService', [])
|
|||
var saveStreamSettings = function() {
|
||||
localStorage.setItem("streamSettings", JSON.stringify(settings));
|
||||
};
|
||||
|
||||
var startEventStream = function() {
|
||||
|
||||
var doEventStream = function(deferred) {
|
||||
settings.shouldPoll = true;
|
||||
settings.isActive = true;
|
||||
var deferred = $q.defer();
|
||||
deferred = deferred || $q.defer();
|
||||
|
||||
// run the stream from the latest token
|
||||
matrixService.getEventStream(settings.from, TIMEOUT_MS).then(
|
||||
function(response) {
|
||||
|
@ -63,13 +64,16 @@ angular.module('eventStreamService', [])
|
|||
|
||||
settings.from = response.data.end;
|
||||
|
||||
console.log("[EventStream] Got response from "+settings.from+" to "+response.data.end);
|
||||
console.log(
|
||||
"[EventStream] Got response from "+settings.from+
|
||||
" to "+response.data.end
|
||||
);
|
||||
eventHandlerService.handleEvents(response.data.chunk, true);
|
||||
|
||||
deferred.resolve(response);
|
||||
|
||||
if (settings.shouldPoll) {
|
||||
$timeout(startEventStream, 0);
|
||||
$timeout(doEventStream, 0);
|
||||
}
|
||||
else {
|
||||
console.log("[EventStream] Stopping poll.");
|
||||
|
@ -83,13 +87,48 @@ angular.module('eventStreamService', [])
|
|||
deferred.reject(error);
|
||||
|
||||
if (settings.shouldPoll) {
|
||||
$timeout(startEventStream, ERR_TIMEOUT_MS);
|
||||
$timeout(doEventStream, ERR_TIMEOUT_MS);
|
||||
}
|
||||
else {
|
||||
console.log("[EventStream] Stopping polling.");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
var startEventStream = function() {
|
||||
settings.shouldPoll = true;
|
||||
settings.isActive = true;
|
||||
var deferred = $q.defer();
|
||||
|
||||
// FIXME: We are discarding all the messages.
|
||||
matrixService.rooms().then(
|
||||
function(response) {
|
||||
var rooms = response.data.rooms;
|
||||
for (var i = 0; i < rooms.length; ++i) {
|
||||
var room = rooms[i];
|
||||
if ("state" in room) {
|
||||
for (var j = 0; j < room.state.length; ++j) {
|
||||
eventHandlerService.handleEvents(room.state[j], false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var presence = response.data.presence;
|
||||
for (var i = 0; i < presence.length; ++i) {
|
||||
eventHandlerService.handleEvent(presence[i], false);
|
||||
}
|
||||
|
||||
settings.from = response.data.end
|
||||
doEventStream(deferred);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
}
|
||||
);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
|
|
|
@ -61,16 +61,23 @@ angular.module('matrixService', [])
|
|||
return doBaseRequest(config.homeserver, method, path, params, data, undefined);
|
||||
};
|
||||
|
||||
var doBaseRequest = function(baseUrl, method, path, params, data, headers) {
|
||||
return $http({
|
||||
var doBaseRequest = function(baseUrl, method, path, params, data, headers, $httpParams) {
|
||||
|
||||
var request = {
|
||||
method: method,
|
||||
url: baseUrl + path,
|
||||
params: params,
|
||||
data: data,
|
||||
headers: headers
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
// Add additional $http parameters
|
||||
if ($httpParams) {
|
||||
angular.extend(request, $httpParams);
|
||||
}
|
||||
|
||||
return $http(request);
|
||||
};
|
||||
|
||||
return {
|
||||
/****** Home server API ******/
|
||||
|
@ -108,19 +115,7 @@ angular.module('matrixService', [])
|
|||
|
||||
// Joins a room
|
||||
join: function(room_id) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/members/$user_id/state";
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$user_id", config.user_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, {
|
||||
membership: "join"
|
||||
});
|
||||
return this.membershipChange(room_id, undefined, "join");
|
||||
},
|
||||
|
||||
joinAlias: function(room_alias) {
|
||||
|
@ -134,34 +129,27 @@ angular.module('matrixService', [])
|
|||
|
||||
// Invite a user to a room
|
||||
invite: function(room_id, user_id) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/members/$user_id/state";
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$user_id", user_id);
|
||||
|
||||
return doRequest("PUT", path, undefined, {
|
||||
membership: "invite"
|
||||
});
|
||||
return this.membershipChange(room_id, user_id, "invite");
|
||||
},
|
||||
|
||||
// Leaves a room
|
||||
leave: function(room_id) {
|
||||
return this.membershipChange(room_id, undefined, "leave");
|
||||
},
|
||||
|
||||
membershipChange: function(room_id, user_id, membershipValue) {
|
||||
// The REST path spec
|
||||
var path = "/rooms/$room_id/members/$user_id/state";
|
||||
var path = "/rooms/$room_id/$membership";
|
||||
path = path.replace("$room_id", encodeURIComponent(room_id));
|
||||
path = path.replace("$membership", encodeURIComponent(membershipValue));
|
||||
|
||||
// Like the cmd client, escape room ids
|
||||
room_id = encodeURIComponent(room_id);
|
||||
var data = {};
|
||||
if (user_id !== undefined) {
|
||||
data = { user_id: user_id };
|
||||
}
|
||||
|
||||
// Customize it
|
||||
path = path.replace("$room_id", room_id);
|
||||
path = path.replace("$user_id", config.user_id);
|
||||
|
||||
return doRequest("DELETE", path, undefined, undefined);
|
||||
// TODO: Use PUT with transaction IDs
|
||||
return doRequest("POST", path, undefined, data);
|
||||
},
|
||||
|
||||
// Retrieves the room ID corresponding to a room alias
|
||||
|
@ -302,17 +290,25 @@ angular.module('matrixService', [])
|
|||
},
|
||||
|
||||
// hit the Identity Server for a 3PID request.
|
||||
linkEmail: function(email) {
|
||||
linkEmail: function(email, clientSecret, sendAttempt) {
|
||||
var path = "/matrix/identity/api/v1/validate/email/requestToken"
|
||||
var data = "clientSecret=abc123&email=" + encodeURIComponent(email);
|
||||
var data = "clientSecret="+clientSecret+"&email=" + encodeURIComponent(email)+"&sendAttempt="+sendAttempt;
|
||||
var headers = {};
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||
},
|
||||
|
||||
authEmail: function(userId, tokenId, code) {
|
||||
authEmail: function(clientSecret, tokenId, code) {
|
||||
var path = "/matrix/identity/api/v1/validate/email/submitToken";
|
||||
var data = "token="+code+"&mxId="+encodeURIComponent(userId)+"&tokenId="+tokenId;
|
||||
var data = "token="+code+"&sid="+tokenId+"&clientSecret="+clientSecret;
|
||||
var headers = {};
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||
},
|
||||
|
||||
bindEmail: function(userId, tokenId, clientSecret) {
|
||||
var path = "/matrix/identity/api/v1/3pid/bind";
|
||||
var data = "mxid="+encodeURIComponent(userId)+"&sid="+tokenId+"&clientSecret="+clientSecret;
|
||||
var headers = {};
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||
|
@ -326,7 +322,17 @@ angular.module('matrixService', [])
|
|||
var params = {
|
||||
access_token: config.access_token
|
||||
};
|
||||
return doBaseRequest(config.homeserver, "POST", path, params, file, headers);
|
||||
|
||||
// If the file is actually a Blob object, prevent $http from JSON-stringified it before sending
|
||||
// (Equivalent to jQuery ajax processData = false)
|
||||
var $httpParams;
|
||||
if (file instanceof Blob) {
|
||||
$httpParams = {
|
||||
transformRequest: angular.identity
|
||||
};
|
||||
}
|
||||
|
||||
return doBaseRequest(config.homeserver, "POST", path, params, file, headers, $httpParams);
|
||||
},
|
||||
|
||||
// start listening on /events
|
||||
|
@ -352,6 +358,23 @@ angular.module('matrixService', [])
|
|||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
// Enum of presence state
|
||||
presence: {
|
||||
offline: "offline",
|
||||
unavailable: "unavailable",
|
||||
online: "online",
|
||||
free_for_chat: "free_for_chat"
|
||||
},
|
||||
|
||||
// Set the logged in user presence state
|
||||
setUserPresence: function(presence) {
|
||||
var path = "/presence/$user_id/status";
|
||||
path = path.replace("$user_id", config.user_id);
|
||||
return doRequest("PUT", path, undefined, {
|
||||
state: presence
|
||||
});
|
||||
},
|
||||
|
||||
/****** Permanent storage of user information ******/
|
||||
|
||||
|
@ -375,6 +398,7 @@ angular.module('matrixService', [])
|
|||
// Set a new config (Use saveConfig to actually store it permanently)
|
||||
setConfig: function(newConfig) {
|
||||
config = newConfig;
|
||||
console.log("new IS: "+config.identityServer);
|
||||
},
|
||||
|
||||
// Commits config into permanent storage
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
* This service tracks user activity on the page to determine his presence state.
|
||||
* Any state change will be sent to the Home Server.
|
||||
*/
|
||||
angular.module('mPresence', [])
|
||||
.service('mPresence', ['$timeout', 'matrixService', function ($timeout, matrixService) {
|
||||
|
||||
// Time in ms after that a user is considered as offline/away
|
||||
var OFFLINE_TIME = 5 * 60000; // 5 mins
|
||||
|
||||
// The current presence state
|
||||
var state = undefined;
|
||||
|
||||
var self =this;
|
||||
var timer;
|
||||
|
||||
/**
|
||||
* Start listening the user activity to evaluate his presence state.
|
||||
* Any state change will be sent to the Home Server.
|
||||
*/
|
||||
this.start = function() {
|
||||
if (undefined === state) {
|
||||
// The user is online if he moves the mouser or press a key
|
||||
document.onmousemove = resetTimer;
|
||||
document.onkeypress = resetTimer;
|
||||
|
||||
resetTimer();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Stop tracking user activity
|
||||
*/
|
||||
this.stop = function() {
|
||||
if (timer) {
|
||||
$timeout.cancel(timer);
|
||||
timer = undefined;
|
||||
}
|
||||
state = undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the current presence state.
|
||||
* @returns {matrixService.presence} the presence state
|
||||
*/
|
||||
this.getState = function() {
|
||||
return state;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the presence state.
|
||||
* If the state has changed, the Home Server will be notified.
|
||||
* @param {matrixService.presence} newState the new presence state
|
||||
*/
|
||||
this.setState = function(newState) {
|
||||
if (newState !== state) {
|
||||
console.log("mPresence - New state: " + newState);
|
||||
|
||||
state = newState;
|
||||
|
||||
// Inform the HS on the new user state
|
||||
matrixService.setUserPresence(state).then(
|
||||
function() {
|
||||
|
||||
},
|
||||
function(error) {
|
||||
console.log("mPresence - Failed to send new presence state: " + JSON.stringify(error));
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Callback called when the user made no action on the page for OFFLINE_TIME ms.
|
||||
* @private
|
||||
*/
|
||||
function onOfflineTimerFire() {
|
||||
self.setState(matrixService.presence.offline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback called when the user made an action on the page
|
||||
* @private
|
||||
*/
|
||||
function resetTimer() {
|
||||
// User is still here
|
||||
self.setState(matrixService.presence.online);
|
||||
|
||||
// Re-arm the timer
|
||||
$timeout.cancel(timer);
|
||||
timer = $timeout(onOfflineTimerFire, OFFLINE_TIME);
|
||||
}
|
||||
|
||||
}]);
|
||||
|
||||
|
|
@ -22,8 +22,8 @@
|
|||
angular.module('mUtilities', [])
|
||||
.service('mUtilities', ['$q', function ($q) {
|
||||
/*
|
||||
* Gets the size of an image
|
||||
* @param {File} imageFile the file containing the image
|
||||
* Get the size of an image
|
||||
* @param {File|Blob} imageFile the file containing the image
|
||||
* @returns {promise} A promise that will be resolved by an object with 2 members:
|
||||
* width & height
|
||||
*/
|
||||
|
@ -38,10 +38,15 @@ angular.module('mUtilities', [])
|
|||
img.src = e.target.result;
|
||||
|
||||
// Once ready, returns its size
|
||||
deferred.resolve({
|
||||
width: img.width,
|
||||
height: img.height
|
||||
});
|
||||
img.onload = function() {
|
||||
deferred.resolve({
|
||||
width: img.width,
|
||||
height: img.height
|
||||
});
|
||||
};
|
||||
img.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
};
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
|
@ -50,4 +55,97 @@ angular.module('mUtilities', [])
|
|||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/*
|
||||
* Resize the image to fit in a square of the side maxSize.
|
||||
* The aspect ratio is kept. The returned image data uses JPEG compression.
|
||||
* Source: http://hacks.mozilla.org/2011/01/how-to-develop-a-html5-image-uploader/
|
||||
* @param {File} imageFile the file containing the image
|
||||
* @param {Integer} maxSize the max side size
|
||||
* @returns {promise} A promise that will be resolved by a Blob object containing
|
||||
* the resized image data
|
||||
*/
|
||||
this.resizeImage = function(imageFile, maxSize) {
|
||||
var self = this;
|
||||
var deferred = $q.defer();
|
||||
|
||||
var canvas = document.createElement("canvas");
|
||||
|
||||
var img = document.createElement("img");
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
|
||||
img.src = e.target.result;
|
||||
|
||||
// Once ready, returns its size
|
||||
img.onload = function() {
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
var MAX_WIDTH = maxSize;
|
||||
var MAX_HEIGHT = maxSize;
|
||||
var width = img.width;
|
||||
var height = img.height;
|
||||
|
||||
if (width > height) {
|
||||
if (width > MAX_WIDTH) {
|
||||
height *= MAX_WIDTH / width;
|
||||
width = MAX_WIDTH;
|
||||
}
|
||||
} else {
|
||||
if (height > MAX_HEIGHT) {
|
||||
width *= MAX_HEIGHT / height;
|
||||
height = MAX_HEIGHT;
|
||||
}
|
||||
}
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
// Extract image data in the same format as the original one.
|
||||
// The 0.7 compression value will work with formats that supports it like JPEG.
|
||||
var dataUrl = canvas.toDataURL(imageFile.type, 0.7);
|
||||
deferred.resolve(self.dataURItoBlob(dataUrl));
|
||||
};
|
||||
img.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
};
|
||||
};
|
||||
reader.onerror = function(e) {
|
||||
deferred.reject(e);
|
||||
};
|
||||
reader.readAsDataURL(imageFile);
|
||||
|
||||
return deferred.promise;
|
||||
};
|
||||
|
||||
/*
|
||||
* Convert a dataURI string to a blob
|
||||
* Source: http://stackoverflow.com/a/17682951
|
||||
* @param {String} dataURI the dataURI can be a base64 encoded string or an URL encoded string.
|
||||
* @returns {Blob} the blob
|
||||
*/
|
||||
this.dataURItoBlob = function(dataURI) {
|
||||
// convert base64 to raw binary data held in a string
|
||||
// doesn't handle URLEncoded DataURIs
|
||||
var byteString;
|
||||
if (dataURI.split(',')[0].indexOf('base64') >= 0)
|
||||
byteString = atob(dataURI.split(',')[1]);
|
||||
else
|
||||
byteString = unescape(dataURI.split(',')[1]);
|
||||
// separate out the mime component
|
||||
var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
|
||||
|
||||
// write the bytes of the string to an ArrayBuffer
|
||||
var ab = new ArrayBuffer(byteString.length);
|
||||
var ia = new Uint8Array(ab);
|
||||
for (var i = 0; i < byteString.length; i++) {
|
||||
ia[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
|
||||
// write the ArrayBuffer to a blob, and you're done
|
||||
return new Blob([ab],{type: mimeString});
|
||||
};
|
||||
|
||||
}]);
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('HomeController', ['matrixService', 'mFileInput', 'mFileUpload', 'eventHandlerService'])
|
||||
.controller('HomeController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService', 'eventStreamService',
|
||||
function($scope, $location, matrixService, mFileUpload, eventHandlerService, eventStreamService) {
|
||||
|
||||
$scope.config = matrixService.config();
|
||||
$scope.rooms = {};
|
||||
$scope.public_rooms = [];
|
||||
$scope.newRoomId = "";
|
||||
$scope.feedback = "";
|
||||
|
||||
$scope.newRoom = {
|
||||
room_id: "",
|
||||
private: false
|
||||
};
|
||||
|
||||
$scope.goToRoom = {
|
||||
room_id: "",
|
||||
};
|
||||
|
||||
$scope.joinAlias = {
|
||||
room_alias: "",
|
||||
};
|
||||
|
||||
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
||||
var config = matrixService.config();
|
||||
if (event.state_key === config.user_id && event.content.membership === "invite") {
|
||||
console.log("Invited to room " + event.room_id);
|
||||
// FIXME push membership to top level key to match /im/sync
|
||||
event.membership = event.content.membership;
|
||||
// FIXME bodge a nicer name than the room ID for this invite.
|
||||
event.room_display_name = event.user_id + "'s room";
|
||||
$scope.rooms[event.room_id] = event;
|
||||
}
|
||||
});
|
||||
|
||||
var assignRoomAliases = function(data) {
|
||||
for (var i=0; i<data.length; i++) {
|
||||
var alias = matrixService.getRoomIdToAliasMapping(data[i].room_id);
|
||||
if (alias) {
|
||||
// use the existing alias from storage
|
||||
data[i].room_alias = alias;
|
||||
data[i].room_display_name = alias;
|
||||
}
|
||||
else if (data[i].aliases && data[i].aliases[0]) {
|
||||
// save the mapping
|
||||
// TODO: select the smarter alias from the array
|
||||
matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
|
||||
data[i].room_display_name = data[i].aliases[0];
|
||||
}
|
||||
else if (data[i].membership == "invite" && "inviter" in data[i]) {
|
||||
data[i].room_display_name = data[i].inviter + "'s room"
|
||||
}
|
||||
else {
|
||||
// last resort use the room id
|
||||
data[i].room_display_name = data[i].room_id;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
var refresh = function() {
|
||||
// List all rooms joined or been invited to
|
||||
matrixService.rooms(1,true).then(
|
||||
function(response) {
|
||||
var data = assignRoomAliases(response.data.rooms);
|
||||
$scope.feedback = "Success";
|
||||
for (var i=0; i<data.length; i++) {
|
||||
$scope.rooms[data[i].room_id] = data[i];
|
||||
}
|
||||
|
||||
var presence = response.data.presence;
|
||||
for (var i = 0; i < presence.length; ++i) {
|
||||
eventHandlerService.handleEvent(presence[i], false);
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
});
|
||||
|
||||
matrixService.publicRooms().then(
|
||||
function(response) {
|
||||
$scope.public_rooms = assignRoomAliases(response.data.chunk);
|
||||
}
|
||||
);
|
||||
|
||||
eventStreamService.resume();
|
||||
};
|
||||
|
||||
$scope.createNewRoom = function(room_id, isPrivate) {
|
||||
|
||||
var visibility = "public";
|
||||
if (isPrivate) {
|
||||
visibility = "private";
|
||||
}
|
||||
|
||||
matrixService.create(room_id, visibility).then(
|
||||
function(response) {
|
||||
// This room has been created. Refresh the rooms list
|
||||
console.log("Created room " + response.data.room_alias + " with id: "+
|
||||
response.data.room_id);
|
||||
matrixService.createRoomIdToAliasMapping(
|
||||
response.data.room_id, response.data.room_alias);
|
||||
refresh();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
});
|
||||
};
|
||||
|
||||
// Go to a room
|
||||
$scope.goToRoom = function(room_id) {
|
||||
// Simply open the room page on this room id
|
||||
//$location.url("room/" + room_id);
|
||||
matrixService.join(room_id).then(
|
||||
function(response) {
|
||||
if (response.data.hasOwnProperty("room_id")) {
|
||||
if (response.data.room_id != room_id) {
|
||||
$location.url("room/" + response.data.room_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$location.url("room/" + room_id);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't join room: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.joinAlias = function(room_alias) {
|
||||
matrixService.joinAlias(room_alias).then(
|
||||
function(response) {
|
||||
// Go to this room
|
||||
$location.url("room/" + room_alias);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't join room: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.onInit = function() {
|
||||
refresh();
|
||||
};
|
||||
}]);
|
|
@ -0,0 +1,63 @@
|
|||
<div ng-controller="HomeController" data-ng-init="onInit()">
|
||||
|
||||
<div id="page">
|
||||
<div id="wrapper">
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="profile-avatar">
|
||||
<img ng-src="{{ config.avatarUrl || 'img/default-profile.jpg' }}"/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="user-ids">
|
||||
<div id="user-displayname">{{ config.displayName }}</div>
|
||||
<div>{{ config.user_id }}</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h3>My rooms</h3>
|
||||
|
||||
<div class="rooms" ng-repeat="(rm_id, room) in rooms">
|
||||
<div>
|
||||
<a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_display_name }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>Public rooms</h3>
|
||||
|
||||
<div class="public_rooms" ng-repeat="room in public_rooms">
|
||||
<div>
|
||||
<a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/>
|
||||
<input type="checkbox" ng-model="newRoom.private">private
|
||||
<button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo_channel:example.org)"/>
|
||||
<button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
{{ feedback }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -2,10 +2,12 @@
|
|||
<html xmlns:ng="http://angularjs.org" ng-app="matrixWebClient" ng-controller="MatrixWebClientController">
|
||||
<head>
|
||||
<title>[matrix]</title>
|
||||
|
||||
|
||||
<link rel="stylesheet" href="app.css">
|
||||
<link rel="icon" href="favicon.ico">
|
||||
|
||||
<meta name="viewport" content="width=device-width">
|
||||
|
||||
<script type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
|
||||
<script src="js/angular.min.js"></script>
|
||||
<script src="js/angular-route.min.js"></script>
|
||||
|
@ -15,14 +17,16 @@
|
|||
<script src="app-controller.js"></script>
|
||||
<script src="app-directive.js"></script>
|
||||
<script src="app-filter.js"></script>
|
||||
<script src="home/home-controller.js"></script>
|
||||
<script src="login/login-controller.js"></script>
|
||||
<script src="room/room-controller.js"></script>
|
||||
<script src="room/room-directive.js"></script>
|
||||
<script src="rooms/rooms-controller.js"></script>
|
||||
<script src="settings/settings-controller.js"></script>
|
||||
<script src="user/user-controller.js"></script>
|
||||
<script src="components/matrix/matrix-service.js"></script>
|
||||
<script src="components/matrix/event-stream-service.js"></script>
|
||||
<script src="components/matrix/event-handler-service.js"></script>
|
||||
<script src="components/matrix/presence-service.js"></script>
|
||||
<script src="components/fileInput/file-input-directive.js"></script>
|
||||
<script src="components/fileUpload/file-upload-service.js"></script>
|
||||
<script src="components/utilities/utilities-service.js"></script>
|
||||
|
@ -33,22 +37,11 @@
|
|||
<header id="header">
|
||||
<!-- Do not show buttons on the login page -->
|
||||
<div id="header-buttons" ng-hide="'/login' == location ">
|
||||
<button ng-click="showConfig()">Config</button>
|
||||
<button ng-click='go("settings")'>Settings</button>
|
||||
<button ng-click="logout()">Log out</button>
|
||||
</div>
|
||||
|
||||
<h1>[matrix]</h1>
|
||||
</header>
|
||||
|
||||
<div id="config" ng-hide="!config">
|
||||
<div>Home server: {{ config.homeserver }} </div>
|
||||
<div>User ID: {{ config.user_id }} </div>
|
||||
<div>Access token: {{ config.access_token }} </div>
|
||||
<div><button ng-click="requestNotifications()">Request notifications</button></div>
|
||||
<div><button ng-click="closeConfig()">Close</button></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div ng-view></div>
|
||||
|
||||
</body>
|
||||
|
|
|
@ -53,7 +53,7 @@ angular.module('LoginController', ['matrixService'])
|
|||
matrixService.saveConfig();
|
||||
eventStreamService.resume();
|
||||
// Go to the user's rooms list page
|
||||
$location.path("rooms");
|
||||
$location.url("home");
|
||||
},
|
||||
function(error) {
|
||||
if (error.data) {
|
||||
|
@ -70,6 +70,7 @@ angular.module('LoginController', ['matrixService'])
|
|||
$scope.login = function() {
|
||||
matrixService.setConfig({
|
||||
homeserver: $scope.account.homeserver,
|
||||
identityServer: $scope.account.identityServer,
|
||||
user_id: $scope.account.user_id
|
||||
});
|
||||
// try to login
|
||||
|
@ -79,12 +80,13 @@ angular.module('LoginController', ['matrixService'])
|
|||
$scope.feedback = "Login successful.";
|
||||
matrixService.setConfig({
|
||||
homeserver: $scope.account.homeserver,
|
||||
identityServer: $scope.account.identityServer,
|
||||
user_id: response.data.user_id,
|
||||
access_token: response.data.access_token
|
||||
});
|
||||
matrixService.saveConfig();
|
||||
eventStreamService.resume();
|
||||
$location.path("rooms");
|
||||
$location.url("home");
|
||||
}
|
||||
else {
|
||||
$scope.feedback = "Failed to login: " + JSON.stringify(response.data);
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
<div ng-controller="LoginController" class="login">
|
||||
<div ng-controller="LoginController" class="login">
|
||||
<h1 id="logo">[matrix]</h1>
|
||||
|
||||
<div id="page">
|
||||
<div id="wrapper">
|
||||
|
||||
|
|
|
@ -15,10 +15,11 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
||||
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'mFileUpload', 'mUtilities',
|
||||
function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, mFileUpload, mUtilities) {
|
||||
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'mFileUpload', 'mUtilities', '$rootScope',
|
||||
function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, mFileUpload, mUtilities, $rootScope) {
|
||||
'use strict';
|
||||
var MESSAGES_PER_PAGINATION = 30;
|
||||
var THUMBNAIL_SIZE = 320;
|
||||
|
||||
// Room ids. Computed and resolved in onInit
|
||||
$scope.room_id = undefined;
|
||||
|
@ -28,9 +29,11 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
user_id: matrixService.config().user_id,
|
||||
events_from: "END", // when to start the event stream from.
|
||||
earliest_token: "END", // stores how far back we've paginated.
|
||||
first_pagination: true, // this is toggled off when the first pagination is done
|
||||
can_paginate: true, // this is toggled off when we run out of items
|
||||
paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
|
||||
stream_failure: undefined, // the response when the stream fails
|
||||
// FIXME: sending has been disabled, as surely messages should be sent in the background rather than locking the UI synchronously --Matthew
|
||||
sending: false // true when a message is being sent. It helps to disable the UI when a process is running
|
||||
};
|
||||
$scope.members = {};
|
||||
|
@ -99,7 +102,6 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
var originalTopRow = $("#messageTable>tbody>tr:first")[0];
|
||||
matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then(
|
||||
function(response) {
|
||||
var firstPagination = !$scope.events.rooms[$scope.room_id];
|
||||
eventHandlerService.handleEvents(response.data.chunk, false);
|
||||
$scope.state.earliest_token = response.data.end;
|
||||
if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
|
||||
|
@ -125,8 +127,9 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
}, 0);
|
||||
}
|
||||
|
||||
if (firstPagination) {
|
||||
if ($scope.state.first_pagination) {
|
||||
scrollToBottom();
|
||||
$scope.state.first_pagination = false;
|
||||
}
|
||||
else {
|
||||
// lock the scroll position
|
||||
|
@ -149,7 +152,12 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
};
|
||||
|
||||
var updateMemberList = function(chunk) {
|
||||
var isNewMember = !(chunk.target_user_id in $scope.members);
|
||||
if (chunk.room_id != $scope.room_id) return;
|
||||
|
||||
// set target_user_id to keep things clear
|
||||
var target_user_id = chunk.state_key;
|
||||
|
||||
var isNewMember = !(target_user_id in $scope.members);
|
||||
if (isNewMember) {
|
||||
// FIXME: why are we copying these fields around inside chunk?
|
||||
if ("state" in chunk.content) {
|
||||
|
@ -158,8 +166,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
if ("mtime_age" in chunk.content) {
|
||||
chunk.mtime_age = chunk.content.mtime_age;
|
||||
}
|
||||
/*
|
||||
// FIXME: once the HS reliably returns the displaynames & avatar_urls for both
|
||||
// Once the HS reliably returns the displaynames & avatar_urls for both
|
||||
// local and remote users, we should use this rather than the evalAsync block
|
||||
// below
|
||||
if ("displayname" in chunk.content) {
|
||||
|
@ -168,9 +175,11 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
if ("avatar_url" in chunk.content) {
|
||||
chunk.avatar_url = chunk.content.avatar_url;
|
||||
}
|
||||
*/
|
||||
$scope.members[chunk.target_user_id] = chunk;
|
||||
$scope.members[target_user_id] = chunk;
|
||||
|
||||
/*
|
||||
// Stale code for explicitly hammering the homeserver for every displayname & avatar_url
|
||||
|
||||
// get their display name and profile picture and set it to their
|
||||
// member entry in $scope.members. We HAVE to use $timeout with 0 delay
|
||||
// to make this function run AFTER the current digest cycle, else the
|
||||
|
@ -194,10 +203,15 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
}
|
||||
);
|
||||
});
|
||||
*/
|
||||
|
||||
if (target_user_id in $rootScope.presence) {
|
||||
updatePresence($rootScope.presence[target_user_id]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// selectively update membership else it will nuke the picture and displayname too :/
|
||||
var member = $scope.members[chunk.target_user_id];
|
||||
var member = $scope.members[target_user_id];
|
||||
member.content.membership = chunk.content.membership;
|
||||
}
|
||||
}
|
||||
|
@ -235,7 +249,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
}
|
||||
|
||||
$scope.state.sending = true;
|
||||
|
||||
|
||||
// Send the text message
|
||||
var promise;
|
||||
// FIXME: handle other commands too
|
||||
|
@ -259,9 +273,8 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
};
|
||||
|
||||
$scope.onInit = function() {
|
||||
// $timeout(function() { document.getElementById('textInput').focus() }, 0);
|
||||
console.log("onInit");
|
||||
|
||||
|
||||
// Does the room ID provided in the URL?
|
||||
var room_id_or_alias;
|
||||
if ($routeParams.room_id_or_alias) {
|
||||
|
@ -289,7 +302,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
else {
|
||||
// In case of issue, go to the default page
|
||||
console.log("Error: cannot extract room alias");
|
||||
$location.path("/");
|
||||
$location.url("/");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -306,12 +319,14 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
function () {
|
||||
// In case of issue, go to the default page
|
||||
console.log("Error: cannot resolve room alias");
|
||||
$location.path("/");
|
||||
$location.url("/");
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var onInit2 = function() {
|
||||
eventHandlerService.reInitRoom($scope.room_id);
|
||||
|
||||
// Join the room
|
||||
matrixService.join($scope.room_id).then(
|
||||
function() {
|
||||
|
@ -324,6 +339,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
var chunk = response.data.chunk[i];
|
||||
updateMemberList(chunk);
|
||||
}
|
||||
eventStreamService.resume();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failed get member list: " + error.data.error;
|
||||
|
@ -359,7 +375,7 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
matrixService.leave($scope.room_id).then(
|
||||
function(response) {
|
||||
console.log("Left room ");
|
||||
$location.path("rooms");
|
||||
$location.url("home");
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failed to leave room: " + error.data.error;
|
||||
|
@ -386,33 +402,22 @@ angular.module('RoomController', ['ngSanitize', 'mUtilities'])
|
|||
|
||||
$scope.state.sending = true;
|
||||
|
||||
// First, get the image sise
|
||||
mUtilities.getImageSize($scope.imageFileToSend).then(
|
||||
function(size) {
|
||||
|
||||
// Upload the image to the Internet
|
||||
console.log("Uploading image...");
|
||||
mFileUpload.uploadFile($scope.imageFileToSend).then(
|
||||
function(url) {
|
||||
// Build the image info data
|
||||
var imageInfo = {
|
||||
size: $scope.imageFileToSend.size,
|
||||
mimetype: $scope.imageFileToSend.type,
|
||||
w: size.width,
|
||||
h: size.height
|
||||
};
|
||||
|
||||
// Then share the URL and the metadata
|
||||
$scope.sendImage(url, imageInfo);
|
||||
// Upload this image with its thumbnail to Internet
|
||||
mFileUpload.uploadImageAndThumbnail($scope.imageFileToSend, THUMBNAIL_SIZE).then(
|
||||
function(imageMessage) {
|
||||
// imageMessage is complete message structure, send it as is
|
||||
matrixService.sendMessage($scope.room_id, undefined, imageMessage).then(
|
||||
function() {
|
||||
console.log("Image message sent");
|
||||
$scope.state.sending = false;
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't upload image";
|
||||
$scope.feedback = "Failed to send image message: " + error.data.error;
|
||||
$scope.state.sending = false;
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't get selected image size";
|
||||
$scope.feedback = "Can't upload image";
|
||||
$scope.state.sending = false;
|
||||
}
|
||||
);
|
||||
|
|
|
@ -17,30 +17,30 @@
|
|||
'use strict';
|
||||
|
||||
angular.module('RoomController')
|
||||
.directive('autoComplete', ['$timeout', function ($timeout) {
|
||||
.directive('tabComplete', ['$timeout', function ($timeout) {
|
||||
return function (scope, element, attrs) {
|
||||
element.bind("keydown keypress", function (event) {
|
||||
// console.log("event: " + event.which);
|
||||
if (event.which === 9) {
|
||||
if (!scope.autoCompleting) { // cache our starting text
|
||||
if (!scope.tabCompleting) { // cache our starting text
|
||||
// console.log("caching " + element[0].value);
|
||||
scope.autoCompleteOriginal = element[0].value;
|
||||
scope.autoCompleting = true;
|
||||
scope.tabCompleteOriginal = element[0].value;
|
||||
scope.tabCompleting = true;
|
||||
}
|
||||
|
||||
if (event.shiftKey) {
|
||||
scope.autoCompleteIndex--;
|
||||
if (scope.autoCompleteIndex < 0) {
|
||||
scope.autoCompleteIndex = 0;
|
||||
scope.tabCompleteIndex--;
|
||||
if (scope.tabCompleteIndex < 0) {
|
||||
scope.tabCompleteIndex = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
scope.autoCompleteIndex++;
|
||||
scope.tabCompleteIndex++;
|
||||
}
|
||||
|
||||
var searchIndex = 0;
|
||||
var targetIndex = scope.autoCompleteIndex;
|
||||
var text = scope.autoCompleteOriginal;
|
||||
var targetIndex = scope.tabCompleteIndex;
|
||||
var text = scope.tabCompleteOriginal;
|
||||
|
||||
// console.log("targetIndex: " + targetIndex + ", text=" + text);
|
||||
|
||||
|
@ -90,17 +90,17 @@ angular.module('RoomController')
|
|||
element[0].className = "";
|
||||
}, 150);
|
||||
element[0].value = text;
|
||||
scope.autoCompleteIndex = 0;
|
||||
scope.tabCompleteIndex = 0;
|
||||
}
|
||||
}
|
||||
else {
|
||||
scope.autoCompleteIndex = 0;
|
||||
scope.tabCompleteIndex = 0;
|
||||
}
|
||||
event.preventDefault();
|
||||
}
|
||||
else if (event.which !== 16 && scope.autoCompleting) {
|
||||
scope.autoCompleting = false;
|
||||
scope.autoCompleteIndex = 0;
|
||||
else if (event.which !== 16 && scope.tabCompleting) {
|
||||
scope.tabCompleting = false;
|
||||
scope.tabCompleteIndex = 0;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
<div ng-controller="RoomController" data-ng-init="onInit()" class="room">
|
||||
<h1 id="roomLogo">[matrix]</h1>
|
||||
|
||||
<div id="page">
|
||||
<div id="wrapper">
|
||||
|
@ -10,7 +11,7 @@
|
|||
<div id="usersTableWrapper">
|
||||
<table id="usersTable">
|
||||
<tr ng-repeat="member in members | orderMembersList">
|
||||
<td class="userAvatar" ng-click="goToUserPage(member.id)">
|
||||
<td class="userAvatar mouse-pointer" ng-click="goToUserPage(member.id)">
|
||||
<img class="userAvatarImage"
|
||||
ng-src="{{member.avatar_url || 'img/default-profile.jpg'}}"
|
||||
alt="{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}"
|
||||
|
@ -26,24 +27,35 @@
|
|||
</div>
|
||||
|
||||
<div id="messageTableWrapper" keep-scroll>
|
||||
<!-- FIXME: need to have better timestamp semantics than the (msg.content.hsob_ts || msg.ts) hack below -->
|
||||
<table id="messageTable" infinite-scroll="paginateMore()">
|
||||
<tr ng-repeat="msg in events.rooms[room_id].messages"
|
||||
ng-class="(events.rooms[room_id].messages[$index - 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
|
||||
ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
|
||||
<td class="leftBlock">
|
||||
<div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
|
||||
<div class="timestamp">{{ msg.content.hsob_ts | date:'MMM d HH:mm:ss' }}</div>
|
||||
<div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}</div>
|
||||
</td>
|
||||
<td class="avatar">
|
||||
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.jpg' }}" width="32" height="32"
|
||||
ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id || msg.user_id === state.user_id"/>
|
||||
</td>
|
||||
<td ng-class="!msg.content.membership_target ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
|
||||
<td ng-class="!msg.content.membership ? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
|
||||
<div class="bubble">
|
||||
<span ng-hide='msg.type !== "m.room.member"'>
|
||||
{{ members[msg.user_id].displayname || msg.user_id }}
|
||||
{{ {"join": "joined", "leave": "left", "invite": "invited"}[msg.content.membership] }}
|
||||
{{ msg.content.membership === "invite" ? (msg.state_key || '') : '' }}
|
||||
</span>
|
||||
<span ng-hide='msg.content.msgtype !== "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
|
||||
<span ng-hide='msg.content.msgtype !== "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
|
||||
<div ng-hide='msg.content.msgtype !== "m.image"'
|
||||
ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
|
||||
<img class="image" ng-src="{{ msg.content.url }}"/>
|
||||
<div ng-show='msg.content.msgtype === "m.image"'>
|
||||
<div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
|
||||
<img class="image" ng-src="{{ msg.content.url }}"/>
|
||||
</div>
|
||||
<div ng-show='msg.content.thumbnail_url' ng-style="{ 'height' : msg.content.thumbnail_info.h }">
|
||||
<img class="image mouse-pointer" ng-src="{{ msg.content.thumbnail_url }}"
|
||||
ng-click="$parent.fullScreenImageURL = msg.content.url"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -62,29 +74,28 @@
|
|||
<div id="controls">
|
||||
<table id="inputBarTable">
|
||||
<tr>
|
||||
<td width="1">
|
||||
<td id="userIdCell" width="1px">
|
||||
{{ state.user_id }}
|
||||
</td>
|
||||
<td width="*" style="min-width: 100px">
|
||||
<input id="mainInput" ng-model="textInput" ng-enter="send()" ng-disabled="state.sending" ng-focus="true" auto-complete/>
|
||||
<td width="*">
|
||||
<input id="mainInput" ng-model="textInput" ng-enter="send()" ng-focus="true" autocomplete="off" tab-complete/>
|
||||
</td>
|
||||
<td width="150px">
|
||||
<button ng-click="send()" ng-disabled="state.sending">Send</button>
|
||||
<button m-file-input="imageFileToSend">Send Image</button>
|
||||
</td>
|
||||
<td width="1">
|
||||
|
||||
<td id="buttonsCell">
|
||||
<button ng-click="send()">Send</button>
|
||||
<button m-file-input="imageFileToSend">Image</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<span>
|
||||
Invite a user:
|
||||
<input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>
|
||||
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
|
||||
</span>
|
||||
<button ng-click="leaveRoom()">Leave</button>
|
||||
<button ng-click="loadMoreHistory()" ng-disabled="!state.can_paginate">Load more history</button>
|
||||
<div id="extraControls">
|
||||
<span>
|
||||
Invite a user:
|
||||
<input ng-model="userIDToInvite" size="32" type="text" placeholder="User ID (ex:@user:homeserver)"/>
|
||||
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
|
||||
</span>
|
||||
<button ng-click="leaveRoom()">Leave</button>
|
||||
</div>
|
||||
|
||||
{{ feedback }}
|
||||
<div ng-hide="!state.stream_failure">
|
||||
{{ state.stream_failure.data.error || "Connection failure" }}
|
||||
|
@ -92,4 +103,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="room-fullscreen-image" ng-show="fullScreenImageURL" ng-click="fullScreenImageURL = undefined;">
|
||||
<img ng-src="{{ fullScreenImageURL }}"/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
|
|
@ -1,264 +0,0 @@
|
|||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload', 'eventHandlerService'])
|
||||
.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload', 'eventHandlerService',
|
||||
function($scope, $location, matrixService, mFileUpload, eventHandlerService) {
|
||||
|
||||
$scope.rooms = {};
|
||||
$scope.public_rooms = [];
|
||||
$scope.newRoomId = "";
|
||||
$scope.feedback = "";
|
||||
|
||||
$scope.newRoom = {
|
||||
room_id: "",
|
||||
private: false
|
||||
};
|
||||
|
||||
$scope.goToRoom = {
|
||||
room_id: "",
|
||||
};
|
||||
|
||||
$scope.joinAlias = {
|
||||
room_alias: "",
|
||||
};
|
||||
|
||||
$scope.newProfileInfo = {
|
||||
name: matrixService.config().displayName,
|
||||
avatar: matrixService.config().avatarUrl,
|
||||
avatarFile: undefined
|
||||
};
|
||||
|
||||
$scope.linkedEmails = {
|
||||
linkNewEmail: "", // the email entry box
|
||||
emailBeingAuthed: undefined, // to populate verification text
|
||||
authTokenId: undefined, // the token id from the IS
|
||||
emailCode: "", // the code entry box
|
||||
linkedEmailList: matrixService.config().emailList // linked email list
|
||||
};
|
||||
|
||||
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
||||
var config = matrixService.config();
|
||||
if (event.target_user_id === config.user_id && event.content.membership === "invite") {
|
||||
console.log("Invited to room " + event.room_id);
|
||||
// FIXME push membership to top level key to match /im/sync
|
||||
event.membership = event.content.membership;
|
||||
// FIXME bodge a nicer name than the room ID for this invite.
|
||||
event.room_alias = event.user_id + "'s room";
|
||||
$scope.rooms[event.room_id] = event;
|
||||
}
|
||||
});
|
||||
|
||||
var assignRoomAliases = function(data) {
|
||||
for (var i=0; i<data.length; i++) {
|
||||
var alias = matrixService.getRoomIdToAliasMapping(data[i].room_id);
|
||||
if (alias) {
|
||||
// use the existing alias from storage
|
||||
data[i].room_alias = alias;
|
||||
}
|
||||
else if (data[i].aliases && data[i].aliases[0]) {
|
||||
// save the mapping
|
||||
// TODO: select the smarter alias from the array
|
||||
matrixService.createRoomIdToAliasMapping(data[i].room_id, data[i].aliases[0]);
|
||||
}
|
||||
else {
|
||||
// last resort use the room id
|
||||
data[i].room_alias = data[i].room_id;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
$scope.refresh = function() {
|
||||
// List all rooms joined or been invited to
|
||||
matrixService.rooms().then(
|
||||
function(response) {
|
||||
var data = assignRoomAliases(response.data);
|
||||
$scope.feedback = "Success";
|
||||
for (var i=0; i<data.length; i++) {
|
||||
$scope.rooms[data[i].room_id] = data[i];
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
});
|
||||
|
||||
matrixService.publicRooms().then(
|
||||
function(response) {
|
||||
$scope.public_rooms = assignRoomAliases(response.data.chunk);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.createNewRoom = function(room_id, isPrivate) {
|
||||
|
||||
var visibility = "public";
|
||||
if (isPrivate) {
|
||||
visibility = "private";
|
||||
}
|
||||
|
||||
matrixService.create(room_id, visibility).then(
|
||||
function(response) {
|
||||
// This room has been created. Refresh the rooms list
|
||||
console.log("Created room " + response.data.room_alias + " with id: "+
|
||||
response.data.room_id);
|
||||
matrixService.createRoomIdToAliasMapping(
|
||||
response.data.room_id, response.data.room_alias);
|
||||
$scope.refresh();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Failure: " + error.data;
|
||||
});
|
||||
};
|
||||
|
||||
// Go to a room
|
||||
$scope.goToRoom = function(room_id) {
|
||||
// Simply open the room page on this room id
|
||||
//$location.path("room/" + room_id);
|
||||
matrixService.join(room_id).then(
|
||||
function(response) {
|
||||
if (response.data.hasOwnProperty("room_id")) {
|
||||
if (response.data.room_id != room_id) {
|
||||
$location.path("room/" + response.data.room_id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$location.path("room/" + room_id);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't join room: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.joinAlias = function(room_alias) {
|
||||
matrixService.joinAlias(room_alias).then(
|
||||
function(response) {
|
||||
// Go to this room
|
||||
$location.path("room/" + room_alias);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't join room: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.setDisplayName = function(newName) {
|
||||
matrixService.setDisplayName(newName).then(
|
||||
function(response) {
|
||||
$scope.feedback = "Updated display name.";
|
||||
var config = matrixService.config();
|
||||
config.displayName = newName;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't update display name: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
$scope.$watch("newProfileInfo.avatarFile", function(newValue, oldValue) {
|
||||
if ($scope.newProfileInfo.avatarFile) {
|
||||
console.log("Uploading new avatar file...");
|
||||
mFileUpload.uploadFile($scope.newProfileInfo.avatarFile).then(
|
||||
function(url) {
|
||||
$scope.newProfileInfo.avatar = url;
|
||||
$scope.setAvatar($scope.newProfileInfo.avatar);
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't upload image";
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.setAvatar = function(newUrl) {
|
||||
console.log("Updating avatar to "+newUrl);
|
||||
matrixService.setProfilePictureUrl(newUrl).then(
|
||||
function(response) {
|
||||
console.log("Updated avatar");
|
||||
$scope.feedback = "Updated avatar.";
|
||||
var config = matrixService.config();
|
||||
config.avatarUrl = newUrl;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't update avatar: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.linkEmail = function(email) {
|
||||
matrixService.linkEmail(email).then(
|
||||
function(response) {
|
||||
if (response.data.success === true) {
|
||||
$scope.linkedEmails.authTokenId = response.data.tokenId;
|
||||
$scope.emailFeedback = "You have been sent an email.";
|
||||
$scope.linkedEmails.emailBeingAuthed = email;
|
||||
}
|
||||
else {
|
||||
$scope.emailFeedback = "Failed to send email.";
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.emailFeedback = "Can't send email: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.submitEmailCode = function(code) {
|
||||
var tokenId = $scope.linkedEmails.authTokenId;
|
||||
if (tokenId === undefined) {
|
||||
$scope.emailFeedback = "You have not requested a code with this email.";
|
||||
return;
|
||||
}
|
||||
matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
|
||||
function(response) {
|
||||
if ("success" in response.data && response.data.success === false) {
|
||||
$scope.emailFeedback = "Failed to authenticate email.";
|
||||
return;
|
||||
}
|
||||
var config = matrixService.config();
|
||||
var emailList = {};
|
||||
if ("emailList" in config) {
|
||||
emailList = config.emailList;
|
||||
}
|
||||
emailList[response.address] = response;
|
||||
// save the new email list
|
||||
config.emailList = emailList;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
// invalidate the email being authed and update UI.
|
||||
$scope.linkedEmails.emailBeingAuthed = undefined;
|
||||
$scope.emailFeedback = "";
|
||||
$scope.linkedEmails.linkedEmailList = emailList;
|
||||
$scope.linkedEmails.linkNewEmail = "";
|
||||
$scope.linkedEmails.emailCode = "";
|
||||
},
|
||||
function(reason) {
|
||||
$scope.emailFeedback = "Failed to auth email: " + reason;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.refresh();
|
||||
}]);
|
|
@ -1,101 +0,0 @@
|
|||
<div ng-controller="RoomsController" class="rooms">
|
||||
|
||||
<div id="page">
|
||||
<div id="wrapper">
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="profile-avatar">
|
||||
<img ng-src="{{ newProfileInfo.avatar || 'img/default-profile.jpg' }}" m-file-input="newProfileInfo.avatarFile"/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<!-- TODO: To enable once we have an upload server
|
||||
<button m-file-input="newProfileInfo.avatarFile">Upload new Avatar</button>
|
||||
or use an existing image URL:
|
||||
-->
|
||||
<div>
|
||||
<input size="40" ng-model="newProfileInfo.avatar" ng-enter="setAvatar(newProfileInfo.avatar)" placeholder="Image URL"/>
|
||||
<button ng-disabled="!newProfileInfo.avatar" ng-click="setAvatar(newProfileInfo.avatar)">Update Avatar</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newProfileInfo.name" ng-enter="setDisplayName(newProfileInfo.name)" />
|
||||
<button ng-disabled="!newProfileInfo.name" ng-click="setDisplayName(newProfileInfo.name)">Update Name</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
|
||||
<button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
|
||||
Link Email
|
||||
</button>
|
||||
{{ emailFeedback }}
|
||||
</form>
|
||||
<form ng-hide="!linkedEmails.emailBeingAuthed">
|
||||
Enter validation token for {{ linkedEmails.emailBeingAuthed }}:
|
||||
<br />
|
||||
<input size="20" ng-model="linkedEmails.emailCode" ng-enter="submitEmailCode(linkedEmails.emailCode)" />
|
||||
<button ng-disabled="!linkedEmails.emailCode || !linkedEmails.linkNewEmail" ng-click="submitEmailCode(linkedEmails.emailCode)">
|
||||
Submit Code
|
||||
</button>
|
||||
</form>
|
||||
Linked emails:
|
||||
<table>
|
||||
<tr ng-repeat="(address, info) in linkedEmails.linkedEmailList">
|
||||
<td>{{address}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>My rooms</h3>
|
||||
|
||||
<div class="rooms" ng-repeat="(rm_id, room) in rooms">
|
||||
<div>
|
||||
<a href="#/room/{{ room.room_alias ? room.room_alias : rm_id }}" >{{ room.room_alias }}</a> {{room.membership === 'invite' ? ' (invited)' : ''}}
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>Public rooms</h3>
|
||||
|
||||
<div class="public_rooms" ng-repeat="room in public_rooms">
|
||||
<div>
|
||||
<a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="newRoom.room_id" ng-enter="createNewRoom(newRoom.room_id, newRoom.private)" placeholder="(e.g. foo_channel)"/>
|
||||
<input type="checkbox" ng-model="newRoom.private">private
|
||||
<button ng-disabled="!newRoom.room_id" ng-click="createNewRoom(newRoom.room_id, newRoom.private)">Create room</button>
|
||||
</form>
|
||||
</div>
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo_channel:example.org)"/>
|
||||
<button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
{{ feedback }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
Copyright 2014 matrix.org
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
angular.module('SettingsController', ['matrixService', 'mFileUpload'])
|
||||
.controller('SettingsController', ['$scope', 'matrixService', 'mFileUpload',
|
||||
function($scope, matrixService, mFileUpload) {
|
||||
$scope.config = matrixService.config();
|
||||
|
||||
$scope.profile = {
|
||||
displayName: $scope.config.displayName,
|
||||
avatarUrl: $scope.config.avatarUrl
|
||||
};
|
||||
|
||||
$scope.$watch("profile.avatarFile", function(newValue, oldValue) {
|
||||
if ($scope.profile.avatarFile) {
|
||||
console.log("Uploading new avatar file...");
|
||||
mFileUpload.uploadFile($scope.profile.avatarFile).then(
|
||||
function(url) {
|
||||
$scope.profile.avatarUrl = url;
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't upload image";
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
$scope.saveProfile = function() {
|
||||
if ($scope.profile.displayName !== $scope.config.displayName) {
|
||||
setDisplayName($scope.profile.displayName);
|
||||
}
|
||||
if ($scope.profile.avatarUrl !== $scope.config.avatarUrl) {
|
||||
setAvatar($scope.profile.avatarUrl);
|
||||
}
|
||||
};
|
||||
|
||||
var setDisplayName = function(displayName) {
|
||||
matrixService.setDisplayName(displayName).then(
|
||||
function(response) {
|
||||
$scope.feedback = "Updated display name.";
|
||||
|
||||
var config = matrixService.config();
|
||||
config.displayName = displayName;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't update display name: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
var setAvatar = function(avatarURL) {
|
||||
console.log("Updating avatar to " + avatarURL);
|
||||
matrixService.setProfilePictureUrl(avatarURL).then(
|
||||
function(response) {
|
||||
console.log("Updated avatar");
|
||||
$scope.feedback = "Updated avatar.";
|
||||
|
||||
var config = matrixService.config();
|
||||
config.avatarUrl = avatarURL;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
},
|
||||
function(error) {
|
||||
$scope.feedback = "Can't update avatar: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.linkedEmails = {
|
||||
linkNewEmail: "", // the email entry box
|
||||
emailBeingAuthed: undefined, // to populate verification text
|
||||
authTokenId: undefined, // the token id from the IS
|
||||
emailCode: "", // the code entry box
|
||||
linkedEmailList: matrixService.config().emailList // linked email list
|
||||
};
|
||||
|
||||
$scope.linkEmail = function(email) {
|
||||
matrixService.linkEmail(email).then(
|
||||
function(response) {
|
||||
if (response.data.success === true) {
|
||||
$scope.linkedEmails.authTokenId = response.data.tokenId;
|
||||
$scope.emailFeedback = "You have been sent an email.";
|
||||
$scope.linkedEmails.emailBeingAuthed = email;
|
||||
}
|
||||
else {
|
||||
$scope.emailFeedback = "Failed to send email.";
|
||||
}
|
||||
},
|
||||
function(error) {
|
||||
$scope.emailFeedback = "Can't send email: " + error.data;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.submitEmailCode = function(code) {
|
||||
var tokenId = $scope.linkedEmails.authTokenId;
|
||||
if (tokenId === undefined) {
|
||||
$scope.emailFeedback = "You have not requested a code with this email.";
|
||||
return;
|
||||
}
|
||||
matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
|
||||
function(response) {
|
||||
if ("success" in response.data && response.data.success === false) {
|
||||
$scope.emailFeedback = "Failed to authenticate email.";
|
||||
return;
|
||||
}
|
||||
var config = matrixService.config();
|
||||
var emailList = {};
|
||||
if ("emailList" in config) {
|
||||
emailList = config.emailList;
|
||||
}
|
||||
emailList[response.address] = response;
|
||||
// save the new email list
|
||||
config.emailList = emailList;
|
||||
matrixService.setConfig(config);
|
||||
matrixService.saveConfig();
|
||||
// invalidate the email being authed and update UI.
|
||||
$scope.linkedEmails.emailBeingAuthed = undefined;
|
||||
$scope.emailFeedback = "";
|
||||
$scope.linkedEmails.linkedEmailList = emailList;
|
||||
$scope.linkedEmails.linkNewEmail = "";
|
||||
$scope.linkedEmails.emailCode = "";
|
||||
},
|
||||
function(reason) {
|
||||
$scope.emailFeedback = "Failed to auth email: " + reason;
|
||||
}
|
||||
);
|
||||
};
|
||||
}]);
|
|
@ -0,0 +1,73 @@
|
|||
<div ng-controller="SettingsController" class="user">
|
||||
|
||||
<div id="page">
|
||||
<div id="wrapper">
|
||||
|
||||
<h3>Me</h3>
|
||||
<div>
|
||||
<form>
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<div class="profile-avatar">
|
||||
<img ng-src="{{ profile.avatarUrl || 'img/default-profile.jpg' }}" m-file-input="profile.avatarFile"/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div id="user-ids">
|
||||
<input size="40" ng-model="profile.displayName" placeholder="Your name"/>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<button ng-disabled="(profile.displayName == config.displayName) && (profile.avatarUrl == config.avatarUrl)"
|
||||
ng-click="saveProfile()">Save</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>Linked emails</h3>
|
||||
<div>
|
||||
<form>
|
||||
<input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
|
||||
<button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
|
||||
Link Email
|
||||
</button>
|
||||
{{ emailFeedback }}
|
||||
</form>
|
||||
<form ng-hide="!linkedEmails.emailBeingAuthed">
|
||||
Enter validation token for {{ linkedEmails.emailBeingAuthed }}:
|
||||
<br />
|
||||
<input size="20" ng-model="linkedEmails.emailCode" ng-enter="submitEmailCode(linkedEmails.emailCode)" />
|
||||
<button ng-disabled="!linkedEmails.emailCode || !linkedEmails.linkNewEmail" ng-click="submitEmailCode(linkedEmails.emailCode)">
|
||||
Submit Code
|
||||
</button>
|
||||
</form>
|
||||
<table>
|
||||
<tr ng-repeat="(address, info) in linkedEmails.linkedEmailList">
|
||||
<td>{{address}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<h3>Configuration</h3>
|
||||
<div>
|
||||
<div>Home server: {{ config.homeserver }} </div>
|
||||
<div>User ID: {{ config.user_id }} </div>
|
||||
<div>Access token: {{ config.access_token }} </div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<div><button ng-click="requestNotifications()">Request notifications</button></div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
{{ feedback }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,4 +1,5 @@
|
|||
<div ng-controller="UserController" class="user">
|
||||
<h1 id="logo">[matrix]</h1>
|
||||
|
||||
<div id="page">
|
||||
<div id="wrapper">
|
||||
|
|
Loading…
Reference in New Issue