mirror of
https://github.com/element-hq/synapse.git
synced 2024-11-28 07:00:51 +03:00
Merge branch 'master' of github.com:matrix-org/synapse into sql_refactor
Conflicts: synapse/storage/stream.py
This commit is contained in:
commit
d72f897f07
47 changed files with 716 additions and 272 deletions
23
README.rst
23
README.rst
|
@ -26,8 +26,8 @@ To get up and running:
|
||||||
with ``python setup.py develop --user`` and then run one with
|
with ``python setup.py develop --user`` and then run one with
|
||||||
``python synapse/app/homeserver.py``
|
``python synapse/app/homeserver.py``
|
||||||
|
|
||||||
- To run your own webclient:
|
- To run your own webclient, add ``-w``:
|
||||||
``cd webclient; python -m SimpleHTTPServer`` and hit http://localhost:8000
|
``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,
|
in your web browser (a recent Chrome, Safari or Firefox for now,
|
||||||
please...)
|
please...)
|
||||||
|
|
||||||
|
@ -120,6 +120,10 @@ may need to also run:
|
||||||
$ sudo apt-get install python-pip
|
$ sudo apt-get install python-pip
|
||||||
$ sudo pip install --upgrade setuptools
|
$ sudo pip install --upgrade setuptools
|
||||||
|
|
||||||
|
If you don't have access to github, then you may need to install ``syutil``
|
||||||
|
manually by checking it out and running ``python setup.py develop --user`` on it
|
||||||
|
too.
|
||||||
|
|
||||||
If you get errors about ``sodium.h`` being missing, you may also need to
|
If you get errors about ``sodium.h`` being missing, you may also need to
|
||||||
manually install a newer PyNaCl via pip as setuptools installs an old one. Or
|
manually install a newer PyNaCl via pip as setuptools installs an old one. Or
|
||||||
you can check PyNaCl out of git directly (https://github.com/pyca/pynacl) and
|
you can check PyNaCl out of git directly (https://github.com/pyca/pynacl) and
|
||||||
|
@ -189,22 +193,17 @@ Running a Demo Federation of Homeservers
|
||||||
|
|
||||||
If you want to get up and running quickly with a trio of homeservers in a
|
If you want to get up and running quickly with a trio of homeservers in a
|
||||||
private federation (``localhost:8080``, ``localhost:8081`` and
|
private federation (``localhost:8080``, ``localhost:8081`` and
|
||||||
``localhost:8082``) which you can then access through the webclient running at http://localhost:8080. Simply run::
|
``localhost:8082``) which you can then access through the webclient running at
|
||||||
|
http://localhost:8080. Simply run::
|
||||||
|
|
||||||
$ demo/start.sh
|
$ demo/start.sh
|
||||||
|
|
||||||
Running The Demo Web Client
|
Running The Demo Web Client
|
||||||
===========================
|
===========================
|
||||||
|
|
||||||
At the present time, the web client is not directly served by the homeserver's
|
You can run the web client when you run the homeserver by adding ``-w`` to the
|
||||||
HTTP server. To serve this in a form the web browser can reach, arrange for the
|
command to run ``homeserver.py``. The web client can be accessed via
|
||||||
'webclient' sub-directory to be made available by any sort of HTTP server that
|
http://localhost:8080/matrix/client
|
||||||
can serve static files. For example, python's SimpleHTTPServer will suffice::
|
|
||||||
|
|
||||||
$ cd webclient
|
|
||||||
$ python -m SimpleHTTPServer
|
|
||||||
|
|
||||||
You can now point your browser at http://localhost:8000/ to find the client.
|
|
||||||
|
|
||||||
If this is the first time you have used the client from that browser (it uses
|
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
|
HTML5 local storage to remember its config), you will need to log in to your
|
||||||
|
|
|
@ -2,9 +2,32 @@ import argparse
|
||||||
import BaseHTTPServer
|
import BaseHTTPServer
|
||||||
import os
|
import os
|
||||||
import SimpleHTTPServer
|
import SimpleHTTPServer
|
||||||
|
import cgi, logging
|
||||||
|
|
||||||
from daemonize import Daemonize
|
from daemonize import Daemonize
|
||||||
|
|
||||||
|
class SimpleHTTPRequestHandlerWithPOST(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||||
|
UPLOAD_PATH = "upload"
|
||||||
|
|
||||||
|
"""
|
||||||
|
Accept all post request as file upload
|
||||||
|
"""
|
||||||
|
def do_POST(self):
|
||||||
|
|
||||||
|
path = os.path.join(self.UPLOAD_PATH, os.path.basename(self.path))
|
||||||
|
length = self.headers['content-length']
|
||||||
|
data = self.rfile.read(int(length))
|
||||||
|
|
||||||
|
with open(path, 'wb') as fh:
|
||||||
|
fh.write(data)
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header('Content-Type', 'application/json')
|
||||||
|
self.end_headers()
|
||||||
|
|
||||||
|
# Return the absolute path of the uploaded file
|
||||||
|
self.wfile.write('{"url":"/%s"}' % path)
|
||||||
|
|
||||||
|
|
||||||
def setup():
|
def setup():
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
|
@ -19,7 +42,7 @@ def setup():
|
||||||
|
|
||||||
httpd = BaseHTTPServer.HTTPServer(
|
httpd = BaseHTTPServer.HTTPServer(
|
||||||
('', args.port),
|
('', args.port),
|
||||||
SimpleHTTPServer.SimpleHTTPRequestHandler
|
SimpleHTTPRequestHandlerWithPOST
|
||||||
)
|
)
|
||||||
|
|
||||||
def run():
|
def run():
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
=========================
|
========================
|
||||||
Synapse Client-Server API
|
Matrix Client-Server API
|
||||||
=========================
|
========================
|
||||||
|
|
||||||
The following specification outlines how a client can send and receive data from
|
The following specification outlines how a client can send and receive data from
|
||||||
a home server.
|
a home server.
|
||||||
|
@ -262,7 +262,10 @@ the error, but the keys 'error' and 'errcode' will always be present.
|
||||||
Some standard error codes are below:
|
Some standard error codes are below:
|
||||||
|
|
||||||
M_FORBIDDEN:
|
M_FORBIDDEN:
|
||||||
Forbidden access, e.g. bad access token, failed login.
|
Forbidden access, e.g. joining a room without permission, failed login.
|
||||||
|
|
||||||
|
M_UNKNOWN_TOKEN:
|
||||||
|
The access token specified was not recognised.
|
||||||
|
|
||||||
M_BAD_JSON:
|
M_BAD_JSON:
|
||||||
Request contained valid JSON, but it was malformed in some way, e.g. missing
|
Request contained valid JSON, but it was malformed in some way, e.g. missing
|
||||||
|
@ -411,6 +414,9 @@ The server checks this, finds it is valid, and returns:
|
||||||
{
|
{
|
||||||
"access_token": "abcdef0123456789"
|
"access_token": "abcdef0123456789"
|
||||||
}
|
}
|
||||||
|
The server may optionally return "user_id" to confirm or change the user's ID.
|
||||||
|
This is particularly useful if the home server wishes to support localpart entry
|
||||||
|
of usernames (e.g. "bob" rather than "@bob:matrix.org").
|
||||||
|
|
||||||
OAuth2-based
|
OAuth2-based
|
||||||
------------
|
------------
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
============================
|
===========================
|
||||||
Synapse Server-to-Server API
|
Matrix Server-to-Server API
|
||||||
============================
|
===========================
|
||||||
|
|
||||||
A description of the protocol used to communicate between Synapse home servers;
|
A description of the protocol used to communicate between Matrix home servers;
|
||||||
also known as Federation.
|
also known as Federation.
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,7 +10,7 @@ Overview
|
||||||
========
|
========
|
||||||
|
|
||||||
The server-server API is a mechanism by which two home servers can exchange
|
The server-server API is a mechanism by which two home servers can exchange
|
||||||
Synapse event messages, both as a real-time push of current events, and as a
|
Matrix event messages, both as a real-time push of current events, and as a
|
||||||
historic fetching mechanism to synchronise past history for clients to view. It
|
historic fetching mechanism to synchronise past history for clients to view. It
|
||||||
uses HTTP connections between each pair of servers involved as the underlying
|
uses HTTP connections between each pair of servers involved as the underlying
|
||||||
transport. Messages are exchanged between servers in real-time by active pushing
|
transport. Messages are exchanged between servers in real-time by active pushing
|
||||||
|
@ -19,7 +19,7 @@ historic data for the purpose of back-filling scrollback buffers and the like
|
||||||
can also be performed.
|
can also be performed.
|
||||||
|
|
||||||
|
|
||||||
{ Synapse entities } { Synapse entities }
|
{ Matrix clients } { Matrix clients }
|
||||||
^ | ^ |
|
^ | ^ |
|
||||||
| events | | events |
|
| events | | events |
|
||||||
| V | V
|
| V | V
|
||||||
|
@ -29,27 +29,53 @@ can also be performed.
|
||||||
| |<--------( HTTP )-----------| |
|
| |<--------( HTTP )-----------| |
|
||||||
+------------------+ +------------------+
|
+------------------+ +------------------+
|
||||||
|
|
||||||
|
There are three main kinds of communication that occur between home servers:
|
||||||
|
|
||||||
Transactions and PDUs
|
* Queries
|
||||||
=====================
|
These are single request/response interactions between a given pair of
|
||||||
|
servers, initiated by one side sending an HTTP request to obtain some
|
||||||
|
information, and responded by the other. They are not persisted and contain
|
||||||
|
no long-term significant history. They simply request a snapshot state at the
|
||||||
|
instant the query is made.
|
||||||
|
|
||||||
The communication between home servers is performed by a bidirectional exchange
|
* EDUs - Ephemeral Data Units
|
||||||
of messages. These messages are called Transactions, and are encoded as JSON
|
These are notifications of events that are pushed from one home server to
|
||||||
objects with a dict as the top-level element, passed over HTTP. A Transaction is
|
another. They are not persisted and contain no long-term significant history,
|
||||||
meaningful only to the pair of home servers that exchanged it; they are not
|
nor does the receiving home server have to reply to them.
|
||||||
globally-meaningful.
|
|
||||||
|
|
||||||
Each transaction has an opaque ID and timestamp (UNIX epoch time in miliseconds)
|
* PDUs - Persisted Data Units
|
||||||
generated by its origin server, an origin and destination server name, a list of
|
These are notifications of events that are broadcast from one home server to
|
||||||
"previous IDs", and a list of PDUs - the actual message payload that the
|
any others that are interested in the same "context" (namely, a Room ID).
|
||||||
Transaction carries.
|
They are persisted to long-term storage and form the record of history for
|
||||||
|
that context.
|
||||||
|
|
||||||
|
Where Queries are presented directly across the HTTP connection as GET requests
|
||||||
|
to specific URLs, EDUs and PDUs are further wrapped in an envelope called a
|
||||||
|
Transaction, which is transferred from the origin to the destination home server
|
||||||
|
using a PUT request.
|
||||||
|
|
||||||
|
|
||||||
|
Transactions and EDUs/PDUs
|
||||||
|
==========================
|
||||||
|
|
||||||
|
The transfer of EDUs and PDUs between home servers is performed by an exchange
|
||||||
|
of Transaction messages, which are encoded as JSON objects with a dict as the
|
||||||
|
top-level element, passed over an HTTP PUT request. A Transaction is meaningful
|
||||||
|
only to the pair of home servers that exchanged it; they are not globally-
|
||||||
|
meaningful.
|
||||||
|
|
||||||
|
Each transaction has an opaque ID and timestamp (UNIX epoch time in
|
||||||
|
milliseconds) generated by its origin server, an origin and destination server
|
||||||
|
name, a list of "previous IDs", and a list of PDUs - the actual message payload
|
||||||
|
that the Transaction carries.
|
||||||
|
|
||||||
{"transaction_id":"916d630ea616342b42e98a3be0b74113",
|
{"transaction_id":"916d630ea616342b42e98a3be0b74113",
|
||||||
"ts":1404835423000,
|
"ts":1404835423000,
|
||||||
"origin":"red",
|
"origin":"red",
|
||||||
"destination":"blue",
|
"destination":"blue",
|
||||||
"prev_ids":["e1da392e61898be4d2009b9fecce5325"],
|
"prev_ids":["e1da392e61898be4d2009b9fecce5325"],
|
||||||
"pdus":[...]}
|
"pdus":[...],
|
||||||
|
"edus":[...]}
|
||||||
|
|
||||||
The "previous IDs" field will contain a list of previous transaction IDs that
|
The "previous IDs" field will contain a list of previous transaction IDs that
|
||||||
the origin server has sent to this destination. Its purpose is to act as a
|
the origin server has sent to this destination. Its purpose is to act as a
|
||||||
|
@ -58,7 +84,9 @@ successfully received that Transaction, or ask for a retransmission if not.
|
||||||
|
|
||||||
The "pdus" field of a transaction is a list, containing zero or more PDUs.[*]
|
The "pdus" field of a transaction is a list, containing zero or more PDUs.[*]
|
||||||
Each PDU is itself a dict containing a number of keys, the exact details of
|
Each PDU is itself a dict containing a number of keys, the exact details of
|
||||||
which will vary depending on the type of PDU.
|
which will vary depending on the type of PDU. Similarly, the "edus" field is
|
||||||
|
another list containing the EDUs. This key may be entirely absent if there are
|
||||||
|
no EDUs to transfer.
|
||||||
|
|
||||||
(* Normally the PDU list will be non-empty, but the server should cope with
|
(* Normally the PDU list will be non-empty, but the server should cope with
|
||||||
receiving an "empty" transaction, as this is useful for informing peers of other
|
receiving an "empty" transaction, as this is useful for informing peers of other
|
||||||
|
@ -86,7 +114,7 @@ field of a PDU refers to PDUs that any origin server has sent, rather than
|
||||||
previous IDs that this origin has sent. This list may refer to other PDUs sent
|
previous IDs that this origin has sent. This list may refer to other PDUs sent
|
||||||
by the same origin as the current one, or other origins.
|
by the same origin as the current one, or other origins.
|
||||||
|
|
||||||
Because of the distributed nature of participants in a Synapse conversation, it
|
Because of the distributed nature of participants in a Matrix conversation, it
|
||||||
is impossible to establish a globally-consistent total ordering on the events.
|
is impossible to establish a globally-consistent total ordering on the events.
|
||||||
However, by annotating each outbound PDU at its origin with IDs of other PDUs it
|
However, by annotating each outbound PDU at its origin with IDs of other PDUs it
|
||||||
has received, a partial ordering can be constructed allowing causallity
|
has received, a partial ordering can be constructed allowing causallity
|
||||||
|
@ -112,6 +140,15 @@ so on. This part needs refining. And writing in its own document as the details
|
||||||
relate to the server/system as a whole, not specifically to server-server
|
relate to the server/system as a whole, not specifically to server-server
|
||||||
federation.]]
|
federation.]]
|
||||||
|
|
||||||
|
EDUs, by comparison to PDUs, do not have an ID, a context, or a list of
|
||||||
|
"previous" IDs. The only mandatory fields for these are the type, origin and
|
||||||
|
destination home server names, and the actual nested content.
|
||||||
|
|
||||||
|
{"edu_type":"m.presence",
|
||||||
|
"origin":"blue",
|
||||||
|
"destination":"orange",
|
||||||
|
"content":...}
|
||||||
|
|
||||||
|
|
||||||
Protocol URLs
|
Protocol URLs
|
||||||
=============
|
=============
|
||||||
|
@ -179,3 +216,16 @@ To stream events all the events:
|
||||||
Retrieves all of the transactions later than any version given by the "v"
|
Retrieves all of the transactions later than any version given by the "v"
|
||||||
arguments. [[TODO(paul): I'm not sure what the "origin" argument does because
|
arguments. [[TODO(paul): I'm not sure what the "origin" argument does because
|
||||||
I think at some point in the code it's got swapped around.]]
|
I think at some point in the code it's got swapped around.]]
|
||||||
|
|
||||||
|
|
||||||
|
To make a query:
|
||||||
|
|
||||||
|
GET .../query/:query_type
|
||||||
|
Query args: as specified by the individual query types
|
||||||
|
|
||||||
|
Response: JSON encoding of a response object
|
||||||
|
|
||||||
|
Performs a single query request on the receiving home server. The Query Type
|
||||||
|
part of the path specifies the kind of query being made, and its query
|
||||||
|
arguments have a meaning specific to that kind of query. The response is a
|
||||||
|
JSON-encoded object whose meaning also depends on the kind of query.
|
||||||
|
|
|
@ -18,7 +18,7 @@
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import Membership
|
from synapse.api.constants import Membership
|
||||||
from synapse.api.errors import AuthError, StoreError
|
from synapse.api.errors import AuthError, StoreError, Codes
|
||||||
from synapse.api.events.room import (RoomTopicEvent, RoomMemberEvent,
|
from synapse.api.events.room import (RoomTopicEvent, RoomMemberEvent,
|
||||||
MessageEvent, FeedbackEvent)
|
MessageEvent, FeedbackEvent)
|
||||||
|
|
||||||
|
@ -163,4 +163,5 @@ class Auth(object):
|
||||||
user_id = yield self.store.get_user_by_token(token=token)
|
user_id = yield self.store.get_user_by_token(token=token)
|
||||||
defer.returnValue(self.hs.parse_userid(user_id))
|
defer.returnValue(self.hs.parse_userid(user_id))
|
||||||
except StoreError:
|
except StoreError:
|
||||||
raise AuthError(403, "Unrecognised access token.")
|
raise AuthError(403, "Unrecognised access token.",
|
||||||
|
errcode=Codes.UNKNOWN_TOKEN)
|
||||||
|
|
|
@ -27,6 +27,7 @@ class Codes(object):
|
||||||
BAD_PAGINATION = "M_BAD_PAGINATION"
|
BAD_PAGINATION = "M_BAD_PAGINATION"
|
||||||
UNKNOWN = "M_UNKNOWN"
|
UNKNOWN = "M_UNKNOWN"
|
||||||
NOT_FOUND = "M_NOT_FOUND"
|
NOT_FOUND = "M_NOT_FOUND"
|
||||||
|
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
||||||
|
|
||||||
|
|
||||||
class CodeMessageException(Exception):
|
class CodeMessageException(Exception):
|
||||||
|
@ -74,7 +75,10 @@ class AuthError(SynapseError):
|
||||||
|
|
||||||
class EventStreamError(SynapseError):
|
class EventStreamError(SynapseError):
|
||||||
"""An error raised when there a problem with the event stream."""
|
"""An error raised when there a problem with the event stream."""
|
||||||
pass
|
def __init__(self, *args, **kwargs):
|
||||||
|
if "errcode" not in kwargs:
|
||||||
|
kwargs["errcode"] = Codes.BAD_PAGINATION
|
||||||
|
super(EventStreamError, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class LoginError(SynapseError):
|
class LoginError(SynapseError):
|
||||||
|
|
|
@ -56,6 +56,10 @@ class Notifier(object):
|
||||||
if (event.type == RoomMemberEvent.TYPE and
|
if (event.type == RoomMemberEvent.TYPE and
|
||||||
event.content["membership"] == Membership.INVITE):
|
event.content["membership"] == Membership.INVITE):
|
||||||
member_list.append(event.target_user_id)
|
member_list.append(event.target_user_id)
|
||||||
|
# similarly, LEAVEs must be sent to the person leaving
|
||||||
|
if (event.type == RoomMemberEvent.TYPE and
|
||||||
|
event.content["membership"] == Membership.LEAVE):
|
||||||
|
member_list.append(event.target_user_id)
|
||||||
|
|
||||||
for user_id in member_list:
|
for user_id in member_list:
|
||||||
if user_id in self.stored_event_listeners:
|
if user_id in self.stored_event_listeners:
|
||||||
|
|
20
synapse/api/urls.py
Normal file
20
synapse/api/urls.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
# -*- 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.
|
||||||
|
|
||||||
|
"""Contains the URL paths to prefix various aspects of the server with. """
|
||||||
|
|
||||||
|
CLIENT_PREFIX = "/matrix/client/api/v1"
|
||||||
|
FEDERATION_PREFIX = "/matrix/federation/v1"
|
||||||
|
WEB_CLIENT_PREFIX = "/matrix/client"
|
114
synapse/app/homeserver.py
Normal file → Executable file
114
synapse/app/homeserver.py
Normal file → Executable file
|
@ -21,8 +21,12 @@ from synapse.server import HomeServer
|
||||||
from twisted.internet import reactor
|
from twisted.internet import reactor
|
||||||
from twisted.enterprise import adbapi
|
from twisted.enterprise import adbapi
|
||||||
from twisted.python.log import PythonLoggingObserver
|
from twisted.python.log import PythonLoggingObserver
|
||||||
from synapse.http.server import TwistedHttpServer
|
from twisted.web.resource import Resource
|
||||||
|
from twisted.web.static import File
|
||||||
|
from twisted.web.server import Site
|
||||||
|
from synapse.http.server import JsonResource, RootRedirect
|
||||||
from synapse.http.client import TwistedHttpClient
|
from synapse.http.client import TwistedHttpClient
|
||||||
|
from synapse.api.urls import CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX
|
||||||
|
|
||||||
from daemonize import Daemonize
|
from daemonize import Daemonize
|
||||||
|
|
||||||
|
@ -36,12 +40,19 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class SynapseHomeServer(HomeServer):
|
class SynapseHomeServer(HomeServer):
|
||||||
def build_http_server(self):
|
|
||||||
return TwistedHttpServer()
|
|
||||||
|
|
||||||
def build_http_client(self):
|
def build_http_client(self):
|
||||||
return TwistedHttpClient()
|
return TwistedHttpClient()
|
||||||
|
|
||||||
|
def build_resource_for_client(self):
|
||||||
|
return JsonResource()
|
||||||
|
|
||||||
|
def build_resource_for_federation(self):
|
||||||
|
return JsonResource()
|
||||||
|
|
||||||
|
def build_resource_for_web_client(self):
|
||||||
|
return File("webclient") # TODO configurable?
|
||||||
|
|
||||||
def build_db_pool(self):
|
def build_db_pool(self):
|
||||||
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
|
""" Set up all the dbs. Since all the *.sql have IF NOT EXISTS, so we
|
||||||
don't have to worry about overwriting existing content.
|
don't have to worry about overwriting existing content.
|
||||||
|
@ -74,6 +85,98 @@ class SynapseHomeServer(HomeServer):
|
||||||
|
|
||||||
return pool
|
return pool
|
||||||
|
|
||||||
|
def create_resource_tree(self, web_client, redirect_root_to_web_client):
|
||||||
|
"""Create the resource tree for this Home Server.
|
||||||
|
|
||||||
|
This in unduly complicated because Twisted does not support putting
|
||||||
|
child resources more than 1 level deep at a time.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
web_client (bool): True to enable the web client.
|
||||||
|
redirect_root_to_web_client (bool): True to redirect '/' to the
|
||||||
|
location of the web client. This does nothing if web_client is not
|
||||||
|
True.
|
||||||
|
"""
|
||||||
|
# list containing (path_str, Resource) e.g:
|
||||||
|
# [ ("/aaa/bbb/cc", Resource1), ("/aaa/dummy", Resource2) ]
|
||||||
|
desired_tree = [
|
||||||
|
(CLIENT_PREFIX, self.get_resource_for_client()),
|
||||||
|
(FEDERATION_PREFIX, self.get_resource_for_federation())
|
||||||
|
]
|
||||||
|
if web_client:
|
||||||
|
logger.info("Adding the web client.")
|
||||||
|
desired_tree.append((WEB_CLIENT_PREFIX,
|
||||||
|
self.get_resource_for_web_client()))
|
||||||
|
|
||||||
|
if web_client and redirect_root_to_web_client:
|
||||||
|
self.root_resource = RootRedirect(WEB_CLIENT_PREFIX)
|
||||||
|
else:
|
||||||
|
self.root_resource = Resource()
|
||||||
|
|
||||||
|
# ideally we'd just use getChild and putChild but getChild doesn't work
|
||||||
|
# unless you give it a Request object IN ADDITION to the name :/ So
|
||||||
|
# instead, we'll store a copy of this mapping so we can actually add
|
||||||
|
# extra resources to existing nodes. See self._resource_id for the key.
|
||||||
|
resource_mappings = {}
|
||||||
|
for (full_path, resource) in desired_tree:
|
||||||
|
logging.info("Attaching %s to path %s", resource, full_path)
|
||||||
|
last_resource = self.root_resource
|
||||||
|
for path_seg in full_path.split('/')[1:-1]:
|
||||||
|
if not path_seg in last_resource.listNames():
|
||||||
|
# resource doesn't exist, so make a "dummy resource"
|
||||||
|
child_resource = Resource()
|
||||||
|
last_resource.putChild(path_seg, child_resource)
|
||||||
|
res_id = self._resource_id(last_resource, path_seg)
|
||||||
|
resource_mappings[res_id] = child_resource
|
||||||
|
last_resource = child_resource
|
||||||
|
else:
|
||||||
|
# we have an existing Resource, use that instead.
|
||||||
|
res_id = self._resource_id(last_resource, path_seg)
|
||||||
|
last_resource = resource_mappings[res_id]
|
||||||
|
|
||||||
|
# ===========================
|
||||||
|
# now attach the actual desired resource
|
||||||
|
last_path_seg = full_path.split('/')[-1]
|
||||||
|
|
||||||
|
# if there is already a resource here, thieve its children and
|
||||||
|
# replace it
|
||||||
|
res_id = self._resource_id(last_resource, last_path_seg)
|
||||||
|
if res_id in resource_mappings:
|
||||||
|
# there is a dummy resource at this path already, which needs
|
||||||
|
# to be replaced with the desired resource.
|
||||||
|
existing_dummy_resource = resource_mappings[res_id]
|
||||||
|
for child_name in existing_dummy_resource.listNames():
|
||||||
|
child_res_id = self._resource_id(existing_dummy_resource,
|
||||||
|
child_name)
|
||||||
|
child_resource = resource_mappings[child_res_id]
|
||||||
|
# steal the children
|
||||||
|
resource.putChild(child_name, child_resource)
|
||||||
|
|
||||||
|
# finally, insert the desired resource in the right place
|
||||||
|
last_resource.putChild(last_path_seg, resource)
|
||||||
|
res_id = self._resource_id(last_resource, last_path_seg)
|
||||||
|
resource_mappings[res_id] = resource
|
||||||
|
|
||||||
|
return self.root_resource
|
||||||
|
|
||||||
|
def _resource_id(self, resource, path_seg):
|
||||||
|
"""Construct an arbitrary resource ID so you can retrieve the mapping
|
||||||
|
later.
|
||||||
|
|
||||||
|
If you want to represent resource A putChild resource B with path C,
|
||||||
|
the mapping should looks like _resource_id(A,C) = B.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
resource (Resource): The *parent* Resource
|
||||||
|
path_seg (str): The name of the child Resource to be attached.
|
||||||
|
Returns:
|
||||||
|
str: A unique string which can be a key to the child Resource.
|
||||||
|
"""
|
||||||
|
return "%s-%s" % (resource, path_seg)
|
||||||
|
|
||||||
|
def start_listening(self, port):
|
||||||
|
reactor.listenTCP(port, Site(self.root_resource))
|
||||||
|
|
||||||
|
|
||||||
def setup_logging(verbosity=0, filename=None, config_path=None):
|
def setup_logging(verbosity=0, filename=None, config_path=None):
|
||||||
""" Sets up logging with verbosity levels.
|
""" Sets up logging with verbosity levels.
|
||||||
|
@ -157,7 +260,10 @@ def setup():
|
||||||
|
|
||||||
hs.register_servlets()
|
hs.register_servlets()
|
||||||
|
|
||||||
hs.get_http_server().start_listening(args.port)
|
hs.create_resource_tree(
|
||||||
|
web_client=args.webclient,
|
||||||
|
redirect_root_to_web_client=True)
|
||||||
|
hs.start_listening(args.port)
|
||||||
|
|
||||||
hs.build_db_pool()
|
hs.build_db_pool()
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,7 @@ from .transport import TransportLayer
|
||||||
def initialize_http_replication(homeserver):
|
def initialize_http_replication(homeserver):
|
||||||
transport = TransportLayer(
|
transport = TransportLayer(
|
||||||
homeserver.hostname,
|
homeserver.hostname,
|
||||||
server=homeserver.get_http_server(),
|
server=homeserver.get_resource_for_federation(),
|
||||||
client=homeserver.get_http_client()
|
client=homeserver.get_http_client()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ over a different (albeit still reliable) protocol.
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from synapse.api.urls import FEDERATION_PREFIX as PREFIX
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -33,9 +34,6 @@ import re
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
PREFIX = "/matrix/federation/v1"
|
|
||||||
|
|
||||||
|
|
||||||
class TransportLayer(object):
|
class TransportLayer(object):
|
||||||
"""This is a basic implementation of the transport layer that translates
|
"""This is a basic implementation of the transport layer that translates
|
||||||
transactions and other requests to/from HTTP.
|
transactions and other requests to/from HTTP.
|
||||||
|
|
|
@ -20,17 +20,11 @@ from ._base import BaseHandler
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import json
|
|
||||||
import urllib
|
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# TODO(erikj): This needs to be factored out somewere
|
|
||||||
PREFIX = "/matrix/client/api/v1"
|
|
||||||
|
|
||||||
|
|
||||||
class DirectoryHandler(BaseHandler):
|
class DirectoryHandler(BaseHandler):
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
|
|
|
@ -16,7 +16,7 @@
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
from synapse.api.errors import LoginError
|
from synapse.api.errors import LoginError, Codes
|
||||||
|
|
||||||
import bcrypt
|
import bcrypt
|
||||||
import logging
|
import logging
|
||||||
|
@ -51,7 +51,7 @@ class LoginHandler(BaseHandler):
|
||||||
user_info = yield self.store.get_user_by_id(user_id=user)
|
user_info = yield self.store.get_user_by_id(user_id=user)
|
||||||
if not user_info:
|
if not user_info:
|
||||||
logger.warn("Attempted to login as %s but they do not exist.", user)
|
logger.warn("Attempted to login as %s but they do not exist.", user)
|
||||||
raise LoginError(403, "")
|
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
||||||
|
|
||||||
stored_hash = user_info[0]["password_hash"]
|
stored_hash = user_info[0]["password_hash"]
|
||||||
if bcrypt.checkpw(password, stored_hash):
|
if bcrypt.checkpw(password, stored_hash):
|
||||||
|
@ -62,4 +62,4 @@ class LoginHandler(BaseHandler):
|
||||||
defer.returnValue(token)
|
defer.returnValue(token)
|
||||||
else:
|
else:
|
||||||
logger.warn("Failed password login for user %s", user)
|
logger.warn("Failed password login for user %s", user)
|
||||||
raise LoginError(403, "")
|
raise LoginError(403, "", errcode=Codes.FORBIDDEN)
|
|
@ -177,7 +177,9 @@ class PresenceHandler(BaseHandler):
|
||||||
state = self._get_or_offline_usercache(target_user).get_state()
|
state = self._get_or_offline_usercache(target_user).get_state()
|
||||||
|
|
||||||
if "mtime" in state:
|
if "mtime" in state:
|
||||||
state["mtime_age"] = self.clock.time_msec() - state.pop("mtime")
|
state["mtime_age"] = int(
|
||||||
|
self.clock.time_msec() - state.pop("mtime")
|
||||||
|
)
|
||||||
defer.returnValue(state)
|
defer.returnValue(state)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
@ -367,7 +369,9 @@ class PresenceHandler(BaseHandler):
|
||||||
p["observed_user"] = observed_user
|
p["observed_user"] = observed_user
|
||||||
p.update(self._get_or_offline_usercache(observed_user).get_state())
|
p.update(self._get_or_offline_usercache(observed_user).get_state())
|
||||||
if "mtime" in p:
|
if "mtime" in p:
|
||||||
p["mtime_age"] = self.clock.time_msec() - p.pop("mtime")
|
p["mtime_age"] = int(
|
||||||
|
self.clock.time_msec() - p.pop("mtime")
|
||||||
|
)
|
||||||
|
|
||||||
defer.returnValue(presence)
|
defer.returnValue(presence)
|
||||||
|
|
||||||
|
@ -560,7 +564,9 @@ class PresenceHandler(BaseHandler):
|
||||||
|
|
||||||
if "mtime" in state:
|
if "mtime" in state:
|
||||||
state = dict(state)
|
state = dict(state)
|
||||||
state["mtime_age"] = self.clock.time_msec() - state.pop("mtime")
|
state["mtime_age"] = int(
|
||||||
|
self.clock.time_msec() - state.pop("mtime")
|
||||||
|
)
|
||||||
|
|
||||||
yield self.federation.send_edu(
|
yield self.federation.send_edu(
|
||||||
destination=destination,
|
destination=destination,
|
||||||
|
@ -598,7 +604,9 @@ class PresenceHandler(BaseHandler):
|
||||||
del state["user_id"]
|
del state["user_id"]
|
||||||
|
|
||||||
if "mtime_age" in state:
|
if "mtime_age" in state:
|
||||||
state["mtime"] = self.clock.time_msec() - state.pop("mtime_age")
|
state["mtime"] = int(
|
||||||
|
self.clock.time_msec() - state.pop("mtime_age")
|
||||||
|
)
|
||||||
|
|
||||||
statuscache = self._get_or_make_usercache(user)
|
statuscache = self._get_or_make_usercache(user)
|
||||||
|
|
||||||
|
@ -720,6 +728,8 @@ class UserPresenceCache(object):
|
||||||
content["user_id"] = user.to_string()
|
content["user_id"] = user.to_string()
|
||||||
|
|
||||||
if "mtime" in content:
|
if "mtime" in content:
|
||||||
content["mtime_age"] = clock.time_msec() - content.pop("mtime")
|
content["mtime_age"] = int(
|
||||||
|
clock.time_msec() - content.pop("mtime")
|
||||||
|
)
|
||||||
|
|
||||||
return {"type": "m.presence", "content": content}
|
return {"type": "m.presence", "content": content}
|
||||||
|
|
|
@ -94,10 +94,10 @@ class MessageHandler(BaseHandler):
|
||||||
event.room_id
|
event.room_id
|
||||||
)
|
)
|
||||||
|
|
||||||
yield self.hs.get_federation().handle_new_event(event)
|
|
||||||
|
|
||||||
self.notifier.on_new_room_event(event, store_id)
|
self.notifier.on_new_room_event(event, store_id)
|
||||||
|
|
||||||
|
yield self.hs.get_federation().handle_new_event(event)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
|
def get_messages(self, user_id=None, room_id=None, pagin_config=None,
|
||||||
feedback=False):
|
feedback=False):
|
||||||
|
|
|
@ -22,6 +22,7 @@ from synapse.api.errors import cs_exception, CodeMessageException
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
from twisted.web import server, resource
|
from twisted.web import server, resource
|
||||||
from twisted.web.server import NOT_DONE_YET
|
from twisted.web.server import NOT_DONE_YET
|
||||||
|
from twisted.web.util import redirectTo
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import logging
|
import logging
|
||||||
|
@ -52,10 +53,9 @@ class HttpServer(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
# The actual HTTP server impl, using twisted http server
|
class JsonResource(HttpServer, resource.Resource):
|
||||||
class TwistedHttpServer(HttpServer, resource.Resource):
|
""" This implements the HttpServer interface and provides JSON support for
|
||||||
""" This wraps the twisted HTTP server, and triggers the correct callbacks
|
Resources.
|
||||||
on the transport_layer.
|
|
||||||
|
|
||||||
Register callbacks via register_path()
|
Register callbacks via register_path()
|
||||||
"""
|
"""
|
||||||
|
@ -160,6 +160,22 @@ class TwistedHttpServer(HttpServer, resource.Resource):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class RootRedirect(resource.Resource):
|
||||||
|
"""Redirects the root '/' path to another path."""
|
||||||
|
|
||||||
|
def __init__(self, path):
|
||||||
|
resource.Resource.__init__(self)
|
||||||
|
self.url = path
|
||||||
|
|
||||||
|
def render_GET(self, request):
|
||||||
|
return redirectTo(self.url, request)
|
||||||
|
|
||||||
|
def getChild(self, name, request):
|
||||||
|
if len(name) == 0:
|
||||||
|
return self # select ourselves as the child to render
|
||||||
|
return resource.Resource.getChild(self, name, request)
|
||||||
|
|
||||||
|
|
||||||
def respond_with_json_bytes(request, code, json_bytes, send_cors=False):
|
def respond_with_json_bytes(request, code, json_bytes, send_cors=False):
|
||||||
"""Sends encoded JSON in response to the given request.
|
"""Sends encoded JSON in response to the given request.
|
||||||
|
|
||||||
|
|
|
@ -15,8 +15,7 @@
|
||||||
|
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
room, events, register, login, profile, public, presence, im, directory,
|
room, events, register, login, profile, public, presence, im, directory
|
||||||
webclient
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -32,19 +31,15 @@ class RestServletFactory(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
http_server = hs.get_http_server()
|
client_resource = hs.get_resource_for_client()
|
||||||
|
|
||||||
# TODO(erikj): There *must* be a better way of doing this.
|
# TODO(erikj): There *must* be a better way of doing this.
|
||||||
room.register_servlets(hs, http_server)
|
room.register_servlets(hs, client_resource)
|
||||||
events.register_servlets(hs, http_server)
|
events.register_servlets(hs, client_resource)
|
||||||
register.register_servlets(hs, http_server)
|
register.register_servlets(hs, client_resource)
|
||||||
login.register_servlets(hs, http_server)
|
login.register_servlets(hs, client_resource)
|
||||||
profile.register_servlets(hs, http_server)
|
profile.register_servlets(hs, client_resource)
|
||||||
public.register_servlets(hs, http_server)
|
public.register_servlets(hs, client_resource)
|
||||||
presence.register_servlets(hs, http_server)
|
presence.register_servlets(hs, client_resource)
|
||||||
im.register_servlets(hs, http_server)
|
im.register_servlets(hs, client_resource)
|
||||||
directory.register_servlets(hs, http_server)
|
directory.register_servlets(hs, client_resource)
|
||||||
|
|
||||||
def register_web_client(self, hs):
|
|
||||||
http_server = hs.get_http_server()
|
|
||||||
webclient.register_servlets(hs, http_server)
|
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
""" This module contains base REST classes for constructing REST servlets. """
|
""" This module contains base REST classes for constructing REST servlets. """
|
||||||
|
from synapse.api.urls import CLIENT_PREFIX
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ def client_path_pattern(path_regex):
|
||||||
Returns:
|
Returns:
|
||||||
SRE_Pattern
|
SRE_Pattern
|
||||||
"""
|
"""
|
||||||
return re.compile("^/matrix/client/api/v1" + path_regex)
|
return re.compile("^" + CLIENT_PREFIX + path_regex)
|
||||||
|
|
||||||
|
|
||||||
class RestServlet(object):
|
class RestServlet(object):
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
|
from synapse.types import UserID
|
||||||
from base import RestServlet, client_path_pattern
|
from base import RestServlet, client_path_pattern
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
@ -45,12 +46,17 @@ class LoginRestServlet(RestServlet):
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def do_password_login(self, login_submission):
|
def do_password_login(self, login_submission):
|
||||||
|
if not login_submission["user"].startswith('@'):
|
||||||
|
login_submission["user"] = UserID.create_local(
|
||||||
|
login_submission["user"], self.hs).to_string()
|
||||||
|
|
||||||
handler = self.handlers.login_handler
|
handler = self.handlers.login_handler
|
||||||
token = yield handler.login(
|
token = yield handler.login(
|
||||||
user=login_submission["user"],
|
user=login_submission["user"],
|
||||||
password=login_submission["password"])
|
password=login_submission["password"])
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
|
"user_id": login_submission["user"], # may have changed
|
||||||
"access_token": token,
|
"access_token": token,
|
||||||
"home_server": self.hs.hostname,
|
"home_server": self.hs.hostname,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,45 +0,0 @@
|
||||||
# -*- coding: utf-8 -*-
|
|
||||||
# Copyright 2014 matrix.org
|
|
||||||
#
|
|
||||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
# you may not use this file except in compliance with the License.
|
|
||||||
# You may obtain a copy of the License at
|
|
||||||
#
|
|
||||||
# http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
#
|
|
||||||
# Unless required by applicable law or agreed to in writing, software
|
|
||||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
# See the License for the specific language governing permissions and
|
|
||||||
# limitations under the License.
|
|
||||||
|
|
||||||
from synapse.rest.base import RestServlet
|
|
||||||
|
|
||||||
import logging
|
|
||||||
import re
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class WebClientRestServlet(RestServlet):
|
|
||||||
# No PATTERN; we have custom dispatch rules here
|
|
||||||
|
|
||||||
def register(self, http_server):
|
|
||||||
http_server.register_path("GET",
|
|
||||||
re.compile("^/$"),
|
|
||||||
self.on_GET_redirect)
|
|
||||||
http_server.register_path("GET",
|
|
||||||
re.compile("^/matrix/client$"),
|
|
||||||
self.on_GET)
|
|
||||||
|
|
||||||
def on_GET(self, request):
|
|
||||||
return (200, "not implemented")
|
|
||||||
|
|
||||||
def on_GET_redirect(self, request):
|
|
||||||
request.setHeader("Location", request.uri + "matrix/client")
|
|
||||||
return (302, None)
|
|
||||||
|
|
||||||
|
|
||||||
def register_servlets(hs, http_server):
|
|
||||||
logger.info("Registering web client.")
|
|
||||||
WebClientRestServlet(hs).register(http_server)
|
|
|
@ -55,7 +55,6 @@ class BaseHomeServer(object):
|
||||||
|
|
||||||
DEPENDENCIES = [
|
DEPENDENCIES = [
|
||||||
'clock',
|
'clock',
|
||||||
'http_server',
|
|
||||||
'http_client',
|
'http_client',
|
||||||
'db_pool',
|
'db_pool',
|
||||||
'persistence_service',
|
'persistence_service',
|
||||||
|
@ -70,6 +69,9 @@ class BaseHomeServer(object):
|
||||||
'room_lock_manager',
|
'room_lock_manager',
|
||||||
'notifier',
|
'notifier',
|
||||||
'distributor',
|
'distributor',
|
||||||
|
'resource_for_client',
|
||||||
|
'resource_for_federation',
|
||||||
|
'resource_for_web_client',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, hostname, **kwargs):
|
def __init__(self, hostname, **kwargs):
|
||||||
|
@ -135,7 +137,9 @@ class HomeServer(BaseHomeServer):
|
||||||
required.
|
required.
|
||||||
|
|
||||||
It still requires the following to be specified by the caller:
|
It still requires the following to be specified by the caller:
|
||||||
http_server
|
resource_for_client
|
||||||
|
resource_for_web_client
|
||||||
|
resource_for_federation
|
||||||
http_client
|
http_client
|
||||||
db_pool
|
db_pool
|
||||||
"""
|
"""
|
||||||
|
@ -178,9 +182,6 @@ class HomeServer(BaseHomeServer):
|
||||||
|
|
||||||
def register_servlets(self):
|
def register_servlets(self):
|
||||||
""" Register all servlets associated with this HomeServer.
|
""" Register all servlets associated with this HomeServer.
|
||||||
|
|
||||||
Args:
|
|
||||||
host_web_client (bool): True to host the web client as well.
|
|
||||||
"""
|
"""
|
||||||
# Simply building the ServletFactory is sufficient to have it register
|
# Simply building the ServletFactory is sufficient to have it register
|
||||||
factory = self.get_rest_servlet_factory()
|
self.get_rest_servlet_factory()
|
||||||
|
|
|
@ -157,7 +157,10 @@ class StateHandler(object):
|
||||||
defer.returnValue(True)
|
defer.returnValue(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
if new_branch[-1] == current_branch[-1]:
|
n = new_branch[-1]
|
||||||
|
c = current_branch[-1]
|
||||||
|
|
||||||
|
if n.pdu_id == c.pdu_id and n.origin == c.origin:
|
||||||
# We have all the PDUs we need, so we can just do the conflict
|
# We have all the PDUs we need, so we can just do the conflict
|
||||||
# resolution.
|
# resolution.
|
||||||
|
|
||||||
|
@ -188,10 +191,18 @@ class StateHandler(object):
|
||||||
key=lambda x: x.depth
|
key=lambda x: x.depth
|
||||||
)
|
)
|
||||||
|
|
||||||
|
pdu_id = missing_prev.prev_state_id
|
||||||
|
origin = missing_prev.prev_state_origin
|
||||||
|
|
||||||
|
is_missing = yield self.store.get_pdu(pdu_id, origin) is None
|
||||||
|
|
||||||
|
if not is_missing:
|
||||||
|
raise Exception("Conflict resolution failed.")
|
||||||
|
|
||||||
yield self._replication.get_pdu(
|
yield self._replication.get_pdu(
|
||||||
destination=missing_prev.origin,
|
destination=missing_prev.origin,
|
||||||
pdu_origin=missing_prev.prev_state_origin,
|
pdu_origin=origin,
|
||||||
pdu_id=missing_prev.prev_state_id,
|
pdu_id=pdu_id,
|
||||||
outlier=True
|
outlier=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ from twisted.internet import defer
|
||||||
|
|
||||||
from sqlite3 import IntegrityError
|
from sqlite3 import IntegrityError
|
||||||
|
|
||||||
from synapse.api.errors import StoreError
|
from synapse.api.errors import StoreError, Codes
|
||||||
|
|
||||||
from ._base import SQLBaseStore
|
from ._base import SQLBaseStore
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ class RegistrationStore(SQLBaseStore):
|
||||||
"VALUES (?,?,?)",
|
"VALUES (?,?,?)",
|
||||||
[user_id, password_hash, now])
|
[user_id, password_hash, now])
|
||||||
except IntegrityError:
|
except IntegrityError:
|
||||||
raise StoreError(400, "User ID already taken.")
|
raise StoreError(400, "User ID already taken.", errcode=Codes.USER_IN_USE)
|
||||||
|
|
||||||
# it's possible for this to get a conflict, but only for a single user
|
# it's possible for this to get a conflict, but only for a single user
|
||||||
# since tokens are namespaced based on their user ID
|
# since tokens are namespaced based on their user ID
|
||||||
|
|
|
@ -24,9 +24,10 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class Lock(object):
|
class Lock(object):
|
||||||
|
|
||||||
def __init__(self, deferred):
|
def __init__(self, deferred, key):
|
||||||
self._deferred = deferred
|
self._deferred = deferred
|
||||||
self.released = False
|
self.released = False
|
||||||
|
self.key = key
|
||||||
|
|
||||||
def release(self):
|
def release(self):
|
||||||
self.released = True
|
self.released = True
|
||||||
|
@ -41,6 +42,7 @@ class Lock(object):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
def __exit__(self, type, value, traceback):
|
||||||
|
logger.debug("Releasing lock for key=%r", self.key)
|
||||||
self.release()
|
self.release()
|
||||||
|
|
||||||
|
|
||||||
|
@ -63,6 +65,10 @@ class LockManager(object):
|
||||||
self._lock_deferreds[key] = new_deferred
|
self._lock_deferreds[key] = new_deferred
|
||||||
|
|
||||||
if old_deferred:
|
if old_deferred:
|
||||||
|
logger.debug("Queueing on lock for key=%r", key)
|
||||||
yield old_deferred
|
yield old_deferred
|
||||||
|
logger.debug("Obtained lock for key=%r", key)
|
||||||
|
else:
|
||||||
|
logger.debug("Entering uncontended lock for key=%r", key)
|
||||||
|
|
||||||
defer.returnValue(Lock(new_deferred))
|
defer.returnValue(Lock(new_deferred, key))
|
||||||
|
|
|
@ -70,7 +70,7 @@ class FederationTestCase(unittest.TestCase):
|
||||||
)
|
)
|
||||||
self.clock = MockClock()
|
self.clock = MockClock()
|
||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
http_server=self.mock_http_server,
|
resource_for_federation=self.mock_http_server,
|
||||||
http_client=self.mock_http_client,
|
http_client=self.mock_http_client,
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
datastore=self.mock_persistence,
|
datastore=self.mock_persistence,
|
||||||
|
|
|
@ -51,7 +51,7 @@ class DirectoryTestCase(unittest.TestCase):
|
||||||
"get_association_from_room_alias",
|
"get_association_from_room_alias",
|
||||||
]),
|
]),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
http_server=Mock(),
|
resource_for_federation=Mock(),
|
||||||
replication_layer=self.mock_federation,
|
replication_layer=self.mock_federation,
|
||||||
)
|
)
|
||||||
hs.handlers = DirectoryHandlers(hs)
|
hs.handlers = DirectoryHandlers(hs)
|
||||||
|
|
|
@ -42,7 +42,7 @@ class FederationTestCase(unittest.TestCase):
|
||||||
"persist_event",
|
"persist_event",
|
||||||
"store_room",
|
"store_room",
|
||||||
]),
|
]),
|
||||||
http_server=NonCallableMock(),
|
resource_for_federation=NonCallableMock(),
|
||||||
http_client=NonCallableMock(spec_set=[]),
|
http_client=NonCallableMock(spec_set=[]),
|
||||||
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
||||||
handlers=NonCallableMock(spec_set=[
|
handlers=NonCallableMock(spec_set=[
|
||||||
|
|
|
@ -66,7 +66,7 @@ class PresenceStateTestCase(unittest.TestCase):
|
||||||
"set_presence_list_accepted",
|
"set_presence_list_accepted",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_federation=Mock(),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
)
|
)
|
||||||
hs.handlers = JustPresenceHandlers(hs)
|
hs.handlers = JustPresenceHandlers(hs)
|
||||||
|
@ -188,7 +188,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
||||||
"del_presence_list",
|
"del_presence_list",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_client=Mock(),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
replication_layer=self.replication
|
replication_layer=self.replication
|
||||||
)
|
)
|
||||||
|
@ -402,7 +402,7 @@ class PresencePushTestCase(unittest.TestCase):
|
||||||
"set_presence_state",
|
"set_presence_state",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_client=Mock(),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
replication_layer=self.replication,
|
replication_layer=self.replication,
|
||||||
)
|
)
|
||||||
|
@ -727,7 +727,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
datastore=Mock(spec=[]),
|
datastore=Mock(spec=[]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_client=Mock(),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
replication_layer=self.replication,
|
replication_layer=self.replication,
|
||||||
)
|
)
|
||||||
|
|
|
@ -71,7 +71,7 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||||
"set_profile_displayname",
|
"set_profile_displayname",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_federation=Mock(),
|
||||||
http_client=None,
|
http_client=None,
|
||||||
replication_layer=MockReplication(),
|
replication_layer=MockReplication(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -56,7 +56,7 @@ class ProfileTestCase(unittest.TestCase):
|
||||||
"set_profile_avatar_url",
|
"set_profile_avatar_url",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
http_server=Mock(),
|
resource_for_federation=Mock(),
|
||||||
replication_layer=self.mock_federation,
|
replication_layer=self.mock_federation,
|
||||||
)
|
)
|
||||||
hs.handlers = ProfileHandlers(hs)
|
hs.handlers = ProfileHandlers(hs)
|
||||||
|
|
|
@ -46,7 +46,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||||
"get_room",
|
"get_room",
|
||||||
"store_room",
|
"store_room",
|
||||||
]),
|
]),
|
||||||
http_server=NonCallableMock(),
|
resource_for_federation=NonCallableMock(),
|
||||||
http_client=NonCallableMock(spec_set=[]),
|
http_client=NonCallableMock(spec_set=[]),
|
||||||
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
||||||
handlers=NonCallableMock(spec_set=[
|
handlers=NonCallableMock(spec_set=[
|
||||||
|
@ -317,7 +317,6 @@ class RoomCreationTest(unittest.TestCase):
|
||||||
datastore=NonCallableMock(spec_set=[
|
datastore=NonCallableMock(spec_set=[
|
||||||
"store_room",
|
"store_room",
|
||||||
]),
|
]),
|
||||||
http_server=NonCallableMock(),
|
|
||||||
http_client=NonCallableMock(spec_set=[]),
|
http_client=NonCallableMock(spec_set=[]),
|
||||||
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
notifier=NonCallableMock(spec_set=["on_new_room_event"]),
|
||||||
handlers=NonCallableMock(spec_set=[
|
handlers=NonCallableMock(spec_set=[
|
||||||
|
|
|
@ -51,7 +51,8 @@ class PresenceStateTestCase(unittest.TestCase):
|
||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
http_server=self.mock_server,
|
resource_for_client=self.mock_server,
|
||||||
|
resource_for_federation=self.mock_server,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_user_by_token(token=None):
|
def _get_user_by_token(token=None):
|
||||||
|
@ -108,7 +109,8 @@ class PresenceListTestCase(unittest.TestCase):
|
||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
http_server=self.mock_server,
|
resource_for_client=self.mock_server,
|
||||||
|
resource_for_federation=self.mock_server
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_user_by_token(token=None):
|
def _get_user_by_token(token=None):
|
||||||
|
@ -183,7 +185,8 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
http_server=self.mock_server,
|
resource_for_client=self.mock_server,
|
||||||
|
resource_for_federation=self.mock_server,
|
||||||
datastore=Mock(spec=[
|
datastore=Mock(spec=[
|
||||||
"set_presence_state",
|
"set_presence_state",
|
||||||
"get_presence_list",
|
"get_presence_list",
|
||||||
|
|
|
@ -43,7 +43,7 @@ class ProfileTestCase(unittest.TestCase):
|
||||||
hs = HomeServer("test",
|
hs = HomeServer("test",
|
||||||
db_pool=None,
|
db_pool=None,
|
||||||
http_client=None,
|
http_client=None,
|
||||||
http_server=self.mock_server,
|
resource_for_client=self.mock_server,
|
||||||
federation=Mock(),
|
federation=Mock(),
|
||||||
replication_layer=Mock(),
|
replication_layer=Mock(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -37,6 +37,7 @@ class StateTestCase(unittest.TestCase):
|
||||||
"update_current_state",
|
"update_current_state",
|
||||||
"get_latest_pdus_in_context",
|
"get_latest_pdus_in_context",
|
||||||
"get_current_state",
|
"get_current_state",
|
||||||
|
"get_pdu",
|
||||||
])
|
])
|
||||||
self.replication = Mock(spec=["get_pdu"])
|
self.replication = Mock(spec=["get_pdu"])
|
||||||
|
|
||||||
|
@ -220,6 +221,8 @@ class StateTestCase(unittest.TestCase):
|
||||||
|
|
||||||
self.replication.get_pdu.side_effect = set_return_tree
|
self.replication.get_pdu.side_effect = set_return_tree
|
||||||
|
|
||||||
|
self.persistence.get_pdu.return_value = None
|
||||||
|
|
||||||
is_new = yield self.state.handle_new_state(new_pdu)
|
is_new = yield self.state.handle_new_state(new_pdu)
|
||||||
|
|
||||||
self.assertTrue(is_new)
|
self.assertTrue(is_new)
|
||||||
|
|
|
@ -57,6 +57,12 @@ angular.module('MatrixWebClientController', ['matrixService'])
|
||||||
$location.path("login");
|
$location.path("login");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Listen to the event indicating that the access token is no more valid.
|
||||||
|
// In this case, the user needs to log in again.
|
||||||
|
$scope.$on("M_UNKNOWN_TOKEN", function() {
|
||||||
|
console.log("Invalid access token -> log user out");
|
||||||
|
$scope.logout();
|
||||||
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
|
|
@ -219,6 +219,20 @@ h1 {
|
||||||
background-color: #fff ! important;
|
background-color: #fff ! important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*** Profile ***/
|
||||||
|
|
||||||
|
.profile-avatar {
|
||||||
|
width: 160px;
|
||||||
|
height: 160px;
|
||||||
|
display:table-cell;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-avatar img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
/******************************/
|
/******************************/
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
|
|
|
@ -23,8 +23,8 @@ var matrixWebClient = angular.module('matrixWebClient', [
|
||||||
'matrixService'
|
'matrixService'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
matrixWebClient.config(['$routeProvider',
|
matrixWebClient.config(['$routeProvider', '$provide', '$httpProvider',
|
||||||
function($routeProvider) {
|
function($routeProvider, $provide, $httpProvider) {
|
||||||
$routeProvider.
|
$routeProvider.
|
||||||
when('/login', {
|
when('/login', {
|
||||||
templateUrl: 'login/login.html',
|
templateUrl: 'login/login.html',
|
||||||
|
@ -41,6 +41,22 @@ matrixWebClient.config(['$routeProvider',
|
||||||
otherwise({
|
otherwise({
|
||||||
redirectTo: '/rooms'
|
redirectTo: '/rooms'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$provide.factory('AccessTokenInterceptor', ['$q', '$rootScope',
|
||||||
|
function ($q, $rootScope) {
|
||||||
|
return {
|
||||||
|
responseError: function(rejection) {
|
||||||
|
if (rejection.status === 403 && "data" in rejection &&
|
||||||
|
"errcode" in rejection.data &&
|
||||||
|
rejection.data.errcode === "M_UNKNOWN_TOKEN") {
|
||||||
|
console.log("Got a 403 with an unknown token. Logging out.")
|
||||||
|
$rootScope.$broadcast("M_UNKNOWN_TOKEN");
|
||||||
|
}
|
||||||
|
return $q.reject(rejection);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}]);
|
||||||
|
$httpProvider.interceptors.push('AccessTokenInterceptor');
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
matrixWebClient.run(['$location', 'matrixService' , function($location, matrixService) {
|
matrixWebClient.run(['$location', 'matrixService' , function($location, matrixService) {
|
||||||
|
|
43
webclient/components/fileInput/file-input-directive.js
Normal file
43
webclient/components/fileInput/file-input-directive.js
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
/*
|
||||||
|
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';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Transform an element into an image file input button.
|
||||||
|
* Watch to the passed variable change. It will contain the selected HTML5 file object.
|
||||||
|
*/
|
||||||
|
angular.module('mFileInput', [])
|
||||||
|
.directive('mFileInput', function() {
|
||||||
|
return {
|
||||||
|
restrict: 'A',
|
||||||
|
transclude: 'true',
|
||||||
|
template: '<div ng-transclude></div><input ng-hide="true" type="file" accept="image/*"/>',
|
||||||
|
scope: {
|
||||||
|
selectedFile: '=mFileInput'
|
||||||
|
},
|
||||||
|
|
||||||
|
link: function(scope, element, attrs, ctrl) {
|
||||||
|
element.bind("click", function() {
|
||||||
|
element.find("input")[0].click();
|
||||||
|
element.find("input").bind("change", function(e) {
|
||||||
|
scope.selectedFile = this.files[0];
|
||||||
|
scope.$apply();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
47
webclient/components/fileUpload/file-upload-service.js
Normal file
47
webclient/components/fileUpload/file-upload-service.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
/*
|
||||||
|
Copyright 2014 matrix.org
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Upload an HTML5 file to a server
|
||||||
|
*/
|
||||||
|
angular.module('mFileUpload', [])
|
||||||
|
.service('mFileUpload', ['$http', '$q', function ($http, $q) {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Upload an HTML5 file to a server and returned a promise
|
||||||
|
* that will provide the URL of the uploaded file.
|
||||||
|
*/
|
||||||
|
this.uploadFile = function(file) {
|
||||||
|
var deferred = $q.defer();
|
||||||
|
|
||||||
|
// @TODO: This service runs with the do_POST hacky implementation of /synapse/demos/webserver.py.
|
||||||
|
// This is temporary until we have a true file upload service
|
||||||
|
console.log("Uploading " + file.name + "...");
|
||||||
|
$http.post(file.name, file)
|
||||||
|
.success(function(data, status, headers, config) {
|
||||||
|
deferred.resolve(location.origin + data.url);
|
||||||
|
console.log(" -> Successfully uploaded! Available at " + location.origin + data.url);
|
||||||
|
}).
|
||||||
|
error(function(data, status, headers, config) {
|
||||||
|
console.log(" -> Failed to upload" + file.name);
|
||||||
|
deferred.reject();
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
}]);
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('matrixService', [])
|
angular.module('matrixService', [])
|
||||||
.factory('matrixService', ['$http', '$q', function($http, $q) {
|
.factory('matrixService', ['$http', '$q', '$rootScope', function($http, $q, $rootScope) {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Permanent storage of user information
|
* Permanent storage of user information
|
||||||
|
@ -49,28 +49,13 @@ angular.module('matrixService', [])
|
||||||
if (path.indexOf(prefixPath) !== 0) {
|
if (path.indexOf(prefixPath) !== 0) {
|
||||||
path = prefixPath + path;
|
path = prefixPath + path;
|
||||||
}
|
}
|
||||||
// Do not directly return the $http instance but return a promise
|
return $http({
|
||||||
// with enriched or cleaned information
|
|
||||||
var deferred = $q.defer();
|
|
||||||
$http({
|
|
||||||
method: method,
|
method: method,
|
||||||
url: baseUrl + path,
|
url: baseUrl + path,
|
||||||
params: params,
|
params: params,
|
||||||
data: data,
|
data: data,
|
||||||
headers: headers
|
headers: headers
|
||||||
})
|
})
|
||||||
.success(function(data, status, headers, config) {
|
|
||||||
// @TODO: We could detect a bad access token here and make an automatic logout
|
|
||||||
deferred.resolve(data, status, headers, config);
|
|
||||||
})
|
|
||||||
.error(function(data, status, headers, config) {
|
|
||||||
// Enrich the error callback with an human readable error reason
|
|
||||||
var reason = data.error;
|
|
||||||
if (!data.error) {
|
|
||||||
reason = JSON.stringify(data);
|
|
||||||
}
|
|
||||||
deferred.reject(reason, data, status, headers, config);
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
@ -228,6 +213,17 @@ angular.module('matrixService', [])
|
||||||
return doRequest("GET", path);
|
return doRequest("GET", path);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
paginateBackMessages: function(room_id, from_token, limit) {
|
||||||
|
var path = "/rooms/$room_id/messages/list";
|
||||||
|
path = path.replace("$room_id", room_id);
|
||||||
|
var params = {
|
||||||
|
from: from_token,
|
||||||
|
to: "START",
|
||||||
|
limit: limit
|
||||||
|
};
|
||||||
|
return doRequest("GET", path, params);
|
||||||
|
},
|
||||||
|
|
||||||
// get a list of public rooms on your home server
|
// get a list of public rooms on your home server
|
||||||
publicRooms: function() {
|
publicRooms: function() {
|
||||||
var path = "/public/rooms"
|
var path = "/public/rooms"
|
||||||
|
@ -301,6 +297,12 @@ angular.module('matrixService', [])
|
||||||
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
return doBaseRequest(config.identityServer, "POST", path, {}, data, headers);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
testLogin: function() {
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
/****** Permanent storage of user information ******/
|
/****** Permanent storage of user information ******/
|
||||||
|
|
||||||
// Returns the current config
|
// Returns the current config
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
<script src="room/room-controller.js"></script>
|
<script src="room/room-controller.js"></script>
|
||||||
<script src="rooms/rooms-controller.js"></script>
|
<script src="rooms/rooms-controller.js"></script>
|
||||||
<script src="components/matrix/matrix-service.js"></script>
|
<script src="components/matrix/matrix-service.js"></script>
|
||||||
|
<script src="components/fileInput/file-input-directive.js"></script>
|
||||||
|
<script src="components/fileUpload/file-upload-service.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -3,8 +3,16 @@ angular.module('LoginController', ['matrixService'])
|
||||||
function($scope, $location, matrixService) {
|
function($scope, $location, matrixService) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
|
||||||
|
// Assume that this is hosted on the home server, in which case the URL
|
||||||
|
// contains the home server.
|
||||||
|
var hs_url = $location.protocol() + "://" + $location.host();
|
||||||
|
if ($location.port()) {
|
||||||
|
hs_url += ":" + $location.port();
|
||||||
|
}
|
||||||
|
|
||||||
$scope.account = {
|
$scope.account = {
|
||||||
homeserver: "http://localhost:8080",
|
homeserver: hs_url,
|
||||||
desired_user_name: "",
|
desired_user_name: "",
|
||||||
user_id: "",
|
user_id: "",
|
||||||
password: "",
|
password: "",
|
||||||
|
@ -31,14 +39,13 @@ angular.module('LoginController', ['matrixService'])
|
||||||
}
|
}
|
||||||
|
|
||||||
matrixService.register($scope.account.desired_user_name, $scope.account.pwd1).then(
|
matrixService.register($scope.account.desired_user_name, $scope.account.pwd1).then(
|
||||||
function(data) {
|
function(response) {
|
||||||
$scope.feedback = "Success";
|
$scope.feedback = "Success";
|
||||||
|
|
||||||
// Update the current config
|
// Update the current config
|
||||||
var config = matrixService.config();
|
var config = matrixService.config();
|
||||||
angular.extend(config, {
|
angular.extend(config, {
|
||||||
access_token: data.access_token,
|
access_token: response.data.access_token,
|
||||||
user_id: data.user_id
|
user_id: response.data.user_id
|
||||||
});
|
});
|
||||||
matrixService.setConfig(config);
|
matrixService.setConfig(config);
|
||||||
|
|
||||||
|
@ -48,8 +55,15 @@ angular.module('LoginController', ['matrixService'])
|
||||||
// Go to the user's rooms list page
|
// Go to the user's rooms list page
|
||||||
$location.path("rooms");
|
$location.path("rooms");
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Failure: " + reason;
|
if (error.data) {
|
||||||
|
if (error.data.errcode === "M_USER_IN_USE") {
|
||||||
|
$scope.feedback = "Username already taken.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (error.status === 0) {
|
||||||
|
$scope.feedback = "Unable to talk to the server.";
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -61,18 +75,28 @@ angular.module('LoginController', ['matrixService'])
|
||||||
// try to login
|
// try to login
|
||||||
matrixService.login($scope.account.user_id, $scope.account.password).then(
|
matrixService.login($scope.account.user_id, $scope.account.password).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
if ("access_token" in response) {
|
if ("access_token" in response.data) {
|
||||||
$scope.feedback = "Login successful.";
|
$scope.feedback = "Login successful.";
|
||||||
matrixService.setConfig({
|
matrixService.setConfig({
|
||||||
homeserver: $scope.account.homeserver,
|
homeserver: $scope.account.homeserver,
|
||||||
user_id: $scope.account.user_id,
|
user_id: response.data.user_id,
|
||||||
access_token: response.access_token
|
access_token: response.data.access_token
|
||||||
});
|
});
|
||||||
matrixService.saveConfig();
|
matrixService.saveConfig();
|
||||||
$location.path("rooms");
|
$location.path("rooms");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$scope.feedback = "Failed to login: " + JSON.stringify(response);
|
$scope.feedback = "Failed to login: " + JSON.stringify(response.data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
if (error.data) {
|
||||||
|
if (error.data.errcode === "M_FORBIDDEN") {
|
||||||
|
$scope.login_error_msg = "Incorrect username or password.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (error.status === 0) {
|
||||||
|
$scope.login_error_msg = "Unable to talk to the server.";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -15,15 +15,16 @@
|
||||||
<!-- New user registration -->
|
<!-- New user registration -->
|
||||||
<div>
|
<div>
|
||||||
<br/>
|
<br/>
|
||||||
<button ng-click="register()" ng-disabled="!account.desired_user_name || !account.homeserver || !account.identityServer || !account.pwd1 || !account.pwd2">Register</button>
|
<button ng-click="register()" ng-disabled="!account.desired_user_name || !account.homeserver || !account.identityServer || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Register</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<h3>Got an account?</h3>
|
<h3>Got an account?</h3>
|
||||||
<form novalidate>
|
<form novalidate>
|
||||||
<!-- Login with an registered user -->
|
<!-- Login with an registered user -->
|
||||||
|
<div>{{ login_error_msg }} </div>
|
||||||
<div>
|
<div>
|
||||||
<input id="user_id" size="70" type="text" auto-focus ng-model="account.user_id" placeholder="User ID (ex:@bob:localhost)"/>
|
<input id="user_id" size="70" type="text" auto-focus ng-model="account.user_id" placeholder="User ID (ex:@bob:localhost or bob)"/>
|
||||||
<br />
|
<br />
|
||||||
<input id="password" size="70" type="password" ng-model="account.password" placeholder="Password"/><br />
|
<input id="password" size="70" type="password" ng-model="account.password" placeholder="Password"/><br />
|
||||||
<br/>
|
<br/>
|
||||||
|
|
|
@ -18,11 +18,15 @@ angular.module('RoomController', [])
|
||||||
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService',
|
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService',
|
||||||
function($scope, $http, $timeout, $routeParams, $location, matrixService) {
|
function($scope, $http, $timeout, $routeParams, $location, matrixService) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
var MESSAGES_PER_PAGINATION = 10;
|
||||||
$scope.room_id = $routeParams.room_id;
|
$scope.room_id = $routeParams.room_id;
|
||||||
$scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
|
$scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
|
||||||
$scope.state = {
|
$scope.state = {
|
||||||
user_id: matrixService.config().user_id,
|
user_id: matrixService.config().user_id,
|
||||||
events_from: "START"
|
events_from: "END", // when to start the event stream from.
|
||||||
|
earliest_token: "END", // stores how far back we've paginated.
|
||||||
|
can_paginate: true, // this is toggled off when we run out of items
|
||||||
|
stream_failure: undefined // the response when the stream fails
|
||||||
};
|
};
|
||||||
$scope.messages = [];
|
$scope.messages = [];
|
||||||
$scope.members = {};
|
$scope.members = {};
|
||||||
|
@ -31,6 +35,53 @@ angular.module('RoomController', [])
|
||||||
$scope.imageURLToSend = "";
|
$scope.imageURLToSend = "";
|
||||||
$scope.userIDToInvite = "";
|
$scope.userIDToInvite = "";
|
||||||
|
|
||||||
|
var scrollToBottom = function() {
|
||||||
|
$timeout(function() {
|
||||||
|
var objDiv = document.getElementsByClassName("messageTableWrapper")[0];
|
||||||
|
objDiv.scrollTop = objDiv.scrollHeight;
|
||||||
|
},0);
|
||||||
|
};
|
||||||
|
|
||||||
|
var parseChunk = function(chunks, appendToStart) {
|
||||||
|
for (var i = 0; i < chunks.length; i++) {
|
||||||
|
var chunk = chunks[i];
|
||||||
|
if (chunk.room_id == $scope.room_id && chunk.type == "m.room.message") {
|
||||||
|
if ("membership_target" in chunk.content) {
|
||||||
|
chunk.user_id = chunk.content.membership_target;
|
||||||
|
}
|
||||||
|
if (appendToStart) {
|
||||||
|
$scope.messages.unshift(chunk);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.messages.push(chunk);
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (chunk.room_id == $scope.room_id && chunk.type == "m.room.member") {
|
||||||
|
updateMemberList(chunk);
|
||||||
|
}
|
||||||
|
else if (chunk.type === "m.presence") {
|
||||||
|
updatePresence(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var paginate = function(numItems) {
|
||||||
|
matrixService.paginateBackMessages($scope.room_id, $scope.state.earliest_token, numItems).then(
|
||||||
|
function(response) {
|
||||||
|
parseChunk(response.data.chunk, true);
|
||||||
|
$scope.state.earliest_token = response.data.end;
|
||||||
|
if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
|
||||||
|
// no more messages to paginate :(
|
||||||
|
$scope.state.can_paginate = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
console.log("Failed to paginateBackMessages: " + JSON.stringify(error));
|
||||||
|
}
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
var shortPoll = function() {
|
var shortPoll = function() {
|
||||||
$http.get(matrixService.config().homeserver + matrixService.prefix + "/events", {
|
$http.get(matrixService.config().homeserver + matrixService.prefix + "/events", {
|
||||||
"params": {
|
"params": {
|
||||||
|
@ -39,30 +90,13 @@ angular.module('RoomController', [])
|
||||||
"timeout": 5000
|
"timeout": 5000
|
||||||
}})
|
}})
|
||||||
.then(function(response) {
|
.then(function(response) {
|
||||||
|
$scope.state.stream_failure = undefined;
|
||||||
console.log("Got response from "+$scope.state.events_from+" to "+response.data.end);
|
console.log("Got response from "+$scope.state.events_from+" to "+response.data.end);
|
||||||
$scope.state.events_from = response.data.end;
|
$scope.state.events_from = response.data.end;
|
||||||
|
|
||||||
$scope.feedback = "";
|
$scope.feedback = "";
|
||||||
|
|
||||||
for (var i = 0; i < response.data.chunk.length; i++) {
|
parseChunk(response.data.chunk, false);
|
||||||
var chunk = response.data.chunk[i];
|
|
||||||
if (chunk.room_id == $scope.room_id && chunk.type == "m.room.message") {
|
|
||||||
if ("membership_target" in chunk.content) {
|
|
||||||
chunk.user_id = chunk.content.membership_target;
|
|
||||||
}
|
|
||||||
$scope.messages.push(chunk);
|
|
||||||
$timeout(function() {
|
|
||||||
var objDiv = document.getElementsByClassName("messageTableWrapper")[0];
|
|
||||||
objDiv.scrollTop = objDiv.scrollHeight;
|
|
||||||
},0);
|
|
||||||
}
|
|
||||||
else if (chunk.room_id == $scope.room_id && chunk.type == "m.room.member") {
|
|
||||||
updateMemberList(chunk);
|
|
||||||
}
|
|
||||||
else if (chunk.type === "m.presence") {
|
|
||||||
updatePresence(chunk);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($scope.stopPoll) {
|
if ($scope.stopPoll) {
|
||||||
console.log("Stopping polling.");
|
console.log("Stopping polling.");
|
||||||
}
|
}
|
||||||
|
@ -70,7 +104,7 @@ angular.module('RoomController', [])
|
||||||
$timeout(shortPoll, 0);
|
$timeout(shortPoll, 0);
|
||||||
}
|
}
|
||||||
}, function(response) {
|
}, function(response) {
|
||||||
$scope.feedback = "Can't stream: " + response.data;
|
$scope.state.stream_failure = response;
|
||||||
|
|
||||||
if (response.status == 403) {
|
if (response.status == 403) {
|
||||||
$scope.stopPoll = true;
|
$scope.stopPoll = true;
|
||||||
|
@ -99,8 +133,8 @@ angular.module('RoomController', [])
|
||||||
function(response) {
|
function(response) {
|
||||||
var member = $scope.members[chunk.target_user_id];
|
var member = $scope.members[chunk.target_user_id];
|
||||||
if (member !== undefined) {
|
if (member !== undefined) {
|
||||||
console.log("Updated displayname "+chunk.target_user_id+" to " + response.displayname);
|
console.log("Updated displayname "+chunk.target_user_id+" to " + response.data.displayname);
|
||||||
member.displayname = response.displayname;
|
member.displayname = response.data.displayname;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -108,8 +142,8 @@ angular.module('RoomController', [])
|
||||||
function(response) {
|
function(response) {
|
||||||
var member = $scope.members[chunk.target_user_id];
|
var member = $scope.members[chunk.target_user_id];
|
||||||
if (member !== undefined) {
|
if (member !== undefined) {
|
||||||
console.log("Updated image for "+chunk.target_user_id+" to " + response.avatar_url);
|
console.log("Updated image for "+chunk.target_user_id+" to " + response.data.avatar_url);
|
||||||
member.avatar_url = response.avatar_url;
|
member.avatar_url = response.data.avatar_url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -171,8 +205,8 @@ angular.module('RoomController', [])
|
||||||
console.log("Sent message");
|
console.log("Sent message");
|
||||||
$scope.textInput = "";
|
$scope.textInput = "";
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Failed to send: " + reason;
|
$scope.feedback = "Failed to send: " + error.data.error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -183,22 +217,24 @@ angular.module('RoomController', [])
|
||||||
// Join the room
|
// Join the room
|
||||||
matrixService.join($scope.room_id).then(
|
matrixService.join($scope.room_id).then(
|
||||||
function() {
|
function() {
|
||||||
console.log("Joined room");
|
console.log("Joined room "+$scope.room_id);
|
||||||
// Now start reading from the stream
|
// Now start reading from the stream
|
||||||
$timeout(shortPoll, 0);
|
$timeout(shortPoll, 0);
|
||||||
|
|
||||||
// Get the current member list
|
// Get the current member list
|
||||||
matrixService.getMemberList($scope.room_id).then(
|
matrixService.getMemberList($scope.room_id).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
for (var i = 0; i < response.chunk.length; i++) {
|
for (var i = 0; i < response.data.chunk.length; i++) {
|
||||||
var chunk = response.chunk[i];
|
var chunk = response.data.chunk[i];
|
||||||
updateMemberList(chunk);
|
updateMemberList(chunk);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Failed get member list: " + reason;
|
$scope.feedback = "Failed get member list: " + error.data.error;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
paginate(MESSAGES_PER_PAGINATION);
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(reason) {
|
||||||
$scope.feedback = "Can't join room: " + reason;
|
$scope.feedback = "Can't join room: " + reason;
|
||||||
|
@ -224,8 +260,8 @@ angular.module('RoomController', [])
|
||||||
console.log("Left room ");
|
console.log("Left room ");
|
||||||
$location.path("rooms");
|
$location.path("rooms");
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Failed to leave room: " + reason;
|
$scope.feedback = "Failed to leave room: " + error.data.error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -234,11 +270,15 @@ angular.module('RoomController', [])
|
||||||
function() {
|
function() {
|
||||||
console.log("Image sent");
|
console.log("Image sent");
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Failed to send image: " + reason;
|
$scope.feedback = "Failed to send image: " + error.data.error;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.loadMoreHistory = function() {
|
||||||
|
paginate(MESSAGES_PER_PAGINATION);
|
||||||
|
};
|
||||||
|
|
||||||
$scope.$on('$destroy', function(e) {
|
$scope.$on('$destroy', function(e) {
|
||||||
console.log("onDestroyed: Stopping poll.");
|
console.log("onDestroyed: Stopping poll.");
|
||||||
$scope.stopPoll = true;
|
$scope.stopPoll = true;
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
{{ msg.content.msgtype === "m.emote" ? ("* " + (members[msg.user_id].displayname || msg.user_id) + " " + msg.content.body) : "" }}
|
{{ msg.content.msgtype === "m.emote" ? ("* " + (members[msg.user_id].displayname || msg.user_id) + " " + msg.content.body) : "" }}
|
||||||
{{ msg.content.msgtype === "m.text" ? msg.content.body : "" }}
|
{{ msg.content.msgtype === "m.text" ? msg.content.body : "" }}
|
||||||
<img class="image" ng-hide='msg.content.msgtype !== "m.image"' src="{{ msg.content.url }}" alt="{{ msg.content.body }}"/>
|
<img class="image" ng-hide='msg.content.msgtype !== "m.image"' ng-src="{{ msg.content.url }}" alt="{{ msg.content.body }}"/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="rightBlock">
|
<td class="rightBlock">
|
||||||
|
@ -86,6 +86,10 @@
|
||||||
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
|
<button ng-click="inviteUser(userIDToInvite)">Invite</button>
|
||||||
</span>
|
</span>
|
||||||
<button ng-click="leaveRoom()">Leave</button>
|
<button ng-click="leaveRoom()">Leave</button>
|
||||||
|
<button ng-click="loadMoreHistory()" ng-disabled="!state.can_paginate">Load more history</button>
|
||||||
|
<div ng-hide="!state.stream_failure">
|
||||||
|
{{ state.stream_failure.data.error || "Connection failure" }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -16,9 +16,9 @@ limitations under the License.
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('RoomsController', ['matrixService'])
|
angular.module('RoomsController', ['matrixService', 'mFileInput', 'mFileUpload'])
|
||||||
.controller('RoomsController', ['$scope', '$location', 'matrixService',
|
.controller('RoomsController', ['$scope', '$location', 'matrixService', 'mFileUpload',
|
||||||
function($scope, $location, matrixService) {
|
function($scope, $location, matrixService, mFileUpload) {
|
||||||
|
|
||||||
$scope.rooms = [];
|
$scope.rooms = [];
|
||||||
$scope.public_rooms = [];
|
$scope.public_rooms = [];
|
||||||
|
@ -40,7 +40,8 @@ angular.module('RoomsController', ['matrixService'])
|
||||||
|
|
||||||
$scope.newProfileInfo = {
|
$scope.newProfileInfo = {
|
||||||
name: matrixService.config().displayName,
|
name: matrixService.config().displayName,
|
||||||
avatar: matrixService.config().avatarUrl
|
avatar: matrixService.config().avatarUrl,
|
||||||
|
avatarFile: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.linkedEmails = {
|
$scope.linkedEmails = {
|
||||||
|
@ -74,18 +75,18 @@ angular.module('RoomsController', ['matrixService'])
|
||||||
// List all rooms joined or been invited to
|
// List all rooms joined or been invited to
|
||||||
$scope.rooms = matrixService.rooms();
|
$scope.rooms = matrixService.rooms();
|
||||||
matrixService.rooms().then(
|
matrixService.rooms().then(
|
||||||
function(data) {
|
function(response) {
|
||||||
data = assignRoomAliases(data);
|
var data = assignRoomAliases(response.data);
|
||||||
$scope.feedback = "Success";
|
$scope.feedback = "Success";
|
||||||
$scope.rooms = data;
|
$scope.rooms = data;
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Failure: " + reason;
|
$scope.feedback = "Failure: " + error.data;
|
||||||
});
|
});
|
||||||
|
|
||||||
matrixService.publicRooms().then(
|
matrixService.publicRooms().then(
|
||||||
function(data) {
|
function(response) {
|
||||||
$scope.public_rooms = assignRoomAliases(data.chunk);
|
$scope.public_rooms = assignRoomAliases(response.data.chunk);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -100,14 +101,14 @@ angular.module('RoomsController', ['matrixService'])
|
||||||
matrixService.create(room_id, visibility).then(
|
matrixService.create(room_id, visibility).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
// This room has been created. Refresh the rooms list
|
// This room has been created. Refresh the rooms list
|
||||||
console.log("Created room " + response.room_alias + " with id: "+
|
console.log("Created room " + response.data.room_alias + " with id: "+
|
||||||
response.room_id);
|
response.data.room_id);
|
||||||
matrixService.createRoomIdToAliasMapping(
|
matrixService.createRoomIdToAliasMapping(
|
||||||
response.room_id, response.room_alias);
|
response.data.room_id, response.data.room_alias);
|
||||||
$scope.refresh();
|
$scope.refresh();
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Failure: " + reason;
|
$scope.feedback = "Failure: " + error.data;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -117,17 +118,17 @@ angular.module('RoomsController', ['matrixService'])
|
||||||
//$location.path("room/" + room_id);
|
//$location.path("room/" + room_id);
|
||||||
matrixService.join(room_id).then(
|
matrixService.join(room_id).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
if (response.hasOwnProperty("room_id")) {
|
if (response.data.hasOwnProperty("room_id")) {
|
||||||
if (response.room_id != room_id) {
|
if (response.data.room_id != room_id) {
|
||||||
$location.path("room/" + response.room_id);
|
$location.path("room/" + response.data.room_id);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$location.path("room/" + room_id);
|
$location.path("room/" + room_id);
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Can't join room: " + reason;
|
$scope.feedback = "Can't join room: " + error.data;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -135,15 +136,15 @@ angular.module('RoomsController', ['matrixService'])
|
||||||
$scope.joinAlias = function(room_alias) {
|
$scope.joinAlias = function(room_alias) {
|
||||||
matrixService.joinAlias(room_alias).then(
|
matrixService.joinAlias(room_alias).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
if (response.hasOwnProperty("room_id")) {
|
if (response.data.hasOwnProperty("room_id")) {
|
||||||
$location.path("room/" + response.room_id);
|
$location.path("room/" + response.data.room_id);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
// TODO (erikj): Do something here?
|
// TODO (erikj): Do something here?
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Can't join room: " + reason;
|
$scope.feedback = "Can't join room: " + error.data;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -157,12 +158,28 @@ angular.module('RoomsController', ['matrixService'])
|
||||||
matrixService.setConfig(config);
|
matrixService.setConfig(config);
|
||||||
matrixService.saveConfig();
|
matrixService.saveConfig();
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Can't update display name: " + reason;
|
$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) {
|
$scope.setAvatar = function(newUrl) {
|
||||||
console.log("Updating avatar to "+newUrl);
|
console.log("Updating avatar to "+newUrl);
|
||||||
matrixService.setProfilePictureUrl(newUrl).then(
|
matrixService.setProfilePictureUrl(newUrl).then(
|
||||||
|
@ -174,8 +191,8 @@ angular.module('RoomsController', ['matrixService'])
|
||||||
matrixService.setConfig(config);
|
matrixService.setConfig(config);
|
||||||
matrixService.saveConfig();
|
matrixService.saveConfig();
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.feedback = "Can't update avatar: " + reason;
|
$scope.feedback = "Can't update avatar: " + error.data;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -183,8 +200,8 @@ angular.module('RoomsController', ['matrixService'])
|
||||||
$scope.linkEmail = function(email) {
|
$scope.linkEmail = function(email) {
|
||||||
matrixService.linkEmail(email).then(
|
matrixService.linkEmail(email).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
if (response.success === true) {
|
if (response.data.success === true) {
|
||||||
$scope.linkedEmails.authTokenId = response.tokenId;
|
$scope.linkedEmails.authTokenId = response.data.tokenId;
|
||||||
$scope.emailFeedback = "You have been sent an email.";
|
$scope.emailFeedback = "You have been sent an email.";
|
||||||
$scope.linkedEmails.emailBeingAuthed = email;
|
$scope.linkedEmails.emailBeingAuthed = email;
|
||||||
}
|
}
|
||||||
|
@ -192,8 +209,8 @@ angular.module('RoomsController', ['matrixService'])
|
||||||
$scope.emailFeedback = "Failed to send email.";
|
$scope.emailFeedback = "Failed to send email.";
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(error) {
|
||||||
$scope.emailFeedback = "Can't send email: " + reason;
|
$scope.emailFeedback = "Can't send email: " + error.data;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -206,7 +223,7 @@ angular.module('RoomsController', ['matrixService'])
|
||||||
}
|
}
|
||||||
matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
|
matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
if ("success" in response && response.success === false) {
|
if ("success" in response.data && response.data.success === false) {
|
||||||
$scope.emailFeedback = "Failed to authenticate email.";
|
$scope.emailFeedback = "Failed to authenticate email.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,35 @@
|
||||||
<div class="page">
|
<div class="page">
|
||||||
<div class="wrapper">
|
<div class="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>
|
||||||
|
<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)" />
|
||||||
|
<button ng-disabled="!newProfileInfo.avatar" ng-click="setAvatar(newProfileInfo.avatar)">Update Avatar</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<form>
|
<form>
|
||||||
<input size="40" ng-model="newProfileInfo.name" ng-enter="setDisplayName(newProfileInfo.name)" />
|
<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>
|
<button ng-disabled="!newProfileInfo.name" ng-click="setDisplayName(newProfileInfo.name)">Update Name</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<form>
|
|
||||||
<input size="40" ng-model="newProfileInfo.avatar" ng-enter="setAvatar(newProfileInfo.avatar)" />
|
|
||||||
<button ng-disabled="!newProfileInfo.avatar" ng-click="setAvatar(newProfileInfo.avatar)">Update Avatar</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
Loading…
Reference in a new issue