Merge branch 'develop' into test-sqlite-memory
						commit
						4571cf7baa
					
				
							
								
								
									
										11
									
								
								CHANGES.rst
								
								
								
								
							
							
						
						
									
										11
									
								
								CHANGES.rst
								
								
								
								
							| 
						 | 
				
			
			@ -1,3 +1,14 @@
 | 
			
		|||
Latest
 | 
			
		||||
======
 | 
			
		||||
 | 
			
		||||
Registration API:
 | 
			
		||||
 * The registration API has been overhauled to function like the login API. In
 | 
			
		||||
   practice, this means registration requests must now include the following:
 | 
			
		||||
   'type':'m.login.password'. See UPGRADE for more information on this.
 | 
			
		||||
 * The 'user_id' key has been renamed to 'user' to better match the login API.
 | 
			
		||||
 * There is an additional login type: 'm.login.email.identity'.
 | 
			
		||||
 * The command client and web client have been updated to reflect these changes.
 | 
			
		||||
 | 
			
		||||
Changes in synapse 0.2.3 (2014-09-12)
 | 
			
		||||
=====================================
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										23
									
								
								UPGRADE.rst
								
								
								
								
							
							
						
						
									
										23
									
								
								UPGRADE.rst
								
								
								
								
							| 
						 | 
				
			
			@ -1,3 +1,26 @@
 | 
			
		|||
Upgrading to Latest
 | 
			
		||||
===================
 | 
			
		||||
 | 
			
		||||
This registration API now closely matches the login API. This introduces a bit
 | 
			
		||||
more backwards and forwards between the HS and the client, but this improves
 | 
			
		||||
the overall flexibility of the API. You can now GET on /register to retrieve a list
 | 
			
		||||
of valid registration flows. Upon choosing one, they are submitted in the same
 | 
			
		||||
way as login, e.g::
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    type: m.login.password,
 | 
			
		||||
    user: foo,
 | 
			
		||||
    password: bar
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
The default HS supports 2 flows, with and without Identity Server email
 | 
			
		||||
authentication. Enabling captcha on the HS will add in an extra step to all
 | 
			
		||||
flows: ``m.login.recaptcha`` which must be completed before you can transition
 | 
			
		||||
to the next stage. There is a new login type: ``m.login.email.identity`` which
 | 
			
		||||
contains the ``threepidCreds`` key which were previously sent in the original
 | 
			
		||||
register request. For more information on this, see the specification.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Upgrading to v0.2.0
 | 
			
		||||
===================
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -145,35 +145,50 @@ class SynapseCmd(cmd.Cmd):
 | 
			
		|||
        <noupdate> : Do not automatically clobber config values.
 | 
			
		||||
        """
 | 
			
		||||
        args = self._parse(line, ["userid", "noupdate"])
 | 
			
		||||
        path = "/register"
 | 
			
		||||
 | 
			
		||||
        password = None
 | 
			
		||||
        pwd = None
 | 
			
		||||
        pwd2 = "_"
 | 
			
		||||
        while pwd != pwd2:
 | 
			
		||||
            pwd = getpass.getpass("(Optional) Type a password for this user: ")
 | 
			
		||||
            if len(pwd) == 0:
 | 
			
		||||
                print "Not using a password for this user."
 | 
			
		||||
                break
 | 
			
		||||
            pwd = getpass.getpass("Type a password for this user: ")
 | 
			
		||||
            pwd2 = getpass.getpass("Retype the password: ")
 | 
			
		||||
            if pwd != pwd2:
 | 
			
		||||
            if pwd != pwd2 or len(pwd) == 0:
 | 
			
		||||
                print "Password mismatch."
 | 
			
		||||
                pwd = None
 | 
			
		||||
            else:
 | 
			
		||||
                password = pwd
 | 
			
		||||
 | 
			
		||||
        body = {}
 | 
			
		||||
        body = {
 | 
			
		||||
            "type": "m.login.password"
 | 
			
		||||
        }
 | 
			
		||||
        if "userid" in args:
 | 
			
		||||
            body["user_id"] = args["userid"]
 | 
			
		||||
            body["user"] = args["userid"]
 | 
			
		||||
        if password:
 | 
			
		||||
            body["password"] = password
 | 
			
		||||
 | 
			
		||||
        reactor.callFromThread(self._do_register, "POST", path, body,
 | 
			
		||||
        reactor.callFromThread(self._do_register, body,
 | 
			
		||||
                               "noupdate" not in args)
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def _do_register(self, method, path, data, update_config):
 | 
			
		||||
        url = self._url() + path
 | 
			
		||||
        json_res = yield self.http_client.do_request(method, url, data=data)
 | 
			
		||||
    def _do_register(self, data, update_config):
 | 
			
		||||
        # check the registration flows
 | 
			
		||||
        url = self._url() + "/register"
 | 
			
		||||
        json_res = yield self.http_client.do_request("GET", url)
 | 
			
		||||
        print json.dumps(json_res, indent=4)
 | 
			
		||||
 | 
			
		||||
        passwordFlow = None
 | 
			
		||||
        for flow in json_res["flows"]:
 | 
			
		||||
            if flow["type"] == "m.login.recaptcha" or ("stages" in flow and "m.login.recaptcha" in flow["stages"]):
 | 
			
		||||
                print "Unable to register: Home server requires captcha."
 | 
			
		||||
                return
 | 
			
		||||
            if flow["type"] == "m.login.password" and "stages" not in flow:
 | 
			
		||||
                passwordFlow = flow
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        if not passwordFlow:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        json_res = yield self.http_client.do_request("POST", url, data=data)
 | 
			
		||||
        print json.dumps(json_res, indent=4)
 | 
			
		||||
        if update_config and "user_id" in json_res:
 | 
			
		||||
            self.config["user"] = json_res["user_id"]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,35 +3,38 @@
 | 
			
		|||
  "apis": [
 | 
			
		||||
    {
 | 
			
		||||
      "operations": [
 | 
			
		||||
        {
 | 
			
		||||
          "method": "GET", 
 | 
			
		||||
          "nickname": "get_registration_info", 
 | 
			
		||||
          "notes": "All login stages MUST be mentioned if there is >1 login type.", 
 | 
			
		||||
          "summary": "Get the login mechanism to use when registering.", 
 | 
			
		||||
          "type": "RegistrationFlows"
 | 
			
		||||
        }, 
 | 
			
		||||
        {
 | 
			
		||||
          "method": "POST", 
 | 
			
		||||
          "nickname": "register", 
 | 
			
		||||
          "notes": "Volatile: This API is likely to change.", 
 | 
			
		||||
          "nickname": "submit_registration", 
 | 
			
		||||
          "notes": "If this is part of a multi-stage registration, there MUST be a 'session' key.", 
 | 
			
		||||
          "parameters": [
 | 
			
		||||
            {
 | 
			
		||||
              "description": "A registration request", 
 | 
			
		||||
              "description": "A registration submission", 
 | 
			
		||||
              "name": "body", 
 | 
			
		||||
              "paramType": "body", 
 | 
			
		||||
              "required": true, 
 | 
			
		||||
              "type": "RegistrationRequest"
 | 
			
		||||
              "type": "RegistrationSubmission"
 | 
			
		||||
            }
 | 
			
		||||
          ], 
 | 
			
		||||
          "responseMessages": [
 | 
			
		||||
            {
 | 
			
		||||
              "code": 400, 
 | 
			
		||||
              "message": "No JSON object."
 | 
			
		||||
              "message": "Bad login type"
 | 
			
		||||
            }, 
 | 
			
		||||
            {
 | 
			
		||||
              "code": 400, 
 | 
			
		||||
              "message": "User ID must only contain characters which do not require url encoding."
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              "code": 400, 
 | 
			
		||||
              "message": "User ID already taken."
 | 
			
		||||
              "message": "Missing JSON keys"
 | 
			
		||||
            }
 | 
			
		||||
          ], 
 | 
			
		||||
          "summary": "Register with the home server.", 
 | 
			
		||||
          "type": "RegistrationResponse"
 | 
			
		||||
          "summary": "Submit a registration action.", 
 | 
			
		||||
          "type": "RegistrationResult"
 | 
			
		||||
        }
 | 
			
		||||
      ], 
 | 
			
		||||
      "path": "/register"
 | 
			
		||||
| 
						 | 
				
			
			@ -42,30 +45,68 @@
 | 
			
		|||
    "application/json"
 | 
			
		||||
  ], 
 | 
			
		||||
  "models": {
 | 
			
		||||
    "RegistrationResponse": {
 | 
			
		||||
      "id": "RegistrationResponse", 
 | 
			
		||||
    "RegistrationFlows": {
 | 
			
		||||
      "id": "RegistrationFlows",
 | 
			
		||||
      "properties": {
 | 
			
		||||
        "access_token": {
 | 
			
		||||
          "description": "The access token for this user.", 
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        "flows": {
 | 
			
		||||
          "description": "A list of valid registration flows.",
 | 
			
		||||
          "type": "array",
 | 
			
		||||
          "items": {
 | 
			
		||||
            "$ref": "RegistrationInfo"
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "RegistrationInfo": {
 | 
			
		||||
      "id": "RegistrationInfo", 
 | 
			
		||||
      "properties": {
 | 
			
		||||
        "stages": {
 | 
			
		||||
          "description": "Multi-stage registration only: An array of all the login types required to registration.", 
 | 
			
		||||
          "items": {
 | 
			
		||||
            "$ref": "string"
 | 
			
		||||
          }, 
 | 
			
		||||
          "type": "array"
 | 
			
		||||
        }, 
 | 
			
		||||
        "user_id": {
 | 
			
		||||
          "description": "The fully-qualified user ID.", 
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        },
 | 
			
		||||
        "home_server": {
 | 
			
		||||
          "description": "The name of the home server.",
 | 
			
		||||
        "type": {
 | 
			
		||||
          "description": "The first login type that must be used when logging in.", 
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }, 
 | 
			
		||||
    "RegistrationRequest": {
 | 
			
		||||
      "id": "RegistrationRequest", 
 | 
			
		||||
    "RegistrationResult": {
 | 
			
		||||
      "id": "RegistrationResult", 
 | 
			
		||||
      "properties": {
 | 
			
		||||
        "access_token": {
 | 
			
		||||
          "description": "The access token for this user's registration if this is the final stage of the registration process.", 
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        },
 | 
			
		||||
        "user_id": {
 | 
			
		||||
          "description": "The desired user ID. If not specified, a random user ID will be allocated.", 
 | 
			
		||||
          "type": "string",
 | 
			
		||||
          "required": false
 | 
			
		||||
          "description": "The user's fully-qualified user ID.",
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        }, 
 | 
			
		||||
        "next": {
 | 
			
		||||
          "description": "Multi-stage registration only: The next registration type to submit.", 
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        },
 | 
			
		||||
        "session": {
 | 
			
		||||
          "description": "Multi-stage registration only: The session token to send when submitting the next registration type.",
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }, 
 | 
			
		||||
    "RegistrationSubmission": {
 | 
			
		||||
      "id": "RegistrationSubmission", 
 | 
			
		||||
      "properties": {
 | 
			
		||||
        "type": {
 | 
			
		||||
          "description": "The type of registration being submitted.", 
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        },
 | 
			
		||||
        "session": {
 | 
			
		||||
          "description": "Multi-stage registration only: The session token from an earlier registration stage.",
 | 
			
		||||
          "type": "string"
 | 
			
		||||
        },
 | 
			
		||||
        "_registration_type_defined_keys_": {
 | 
			
		||||
          "description": "Keys as defined by the specified registration type, e.g. \"user\", \"password\""
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1169,8 +1169,14 @@ This event is sent by the caller when they wish to establish a call.
 | 
			
		|||
  Required keys:
 | 
			
		||||
    - ``call_id`` : "string" - A unique identifier for the call
 | 
			
		||||
    - ``offer`` : "offer object" - The session description
 | 
			
		||||
    - ``version`` : "integer" - The version of the VoIP specification this message
 | 
			
		||||
                                adheres to. This specification is version 0.
 | 
			
		||||
    - ``version`` : "integer" - The version of the VoIP specification this
 | 
			
		||||
                                message adheres to. This specification is
 | 
			
		||||
                                version 0.
 | 
			
		||||
    - ``lifetime`` : "integer" - The time in milliseconds that the invite is
 | 
			
		||||
                                 valid for. Once the invite age exceeds this
 | 
			
		||||
                                 value, clients should discard it. They
 | 
			
		||||
                                 should also no longer show the call as
 | 
			
		||||
                                 awaiting an answer in the UI.
 | 
			
		||||
      
 | 
			
		||||
  Optional keys:
 | 
			
		||||
    None.
 | 
			
		||||
| 
						 | 
				
			
			@ -1305,12 +1311,6 @@ display name other than it being a valid unicode string.
 | 
			
		|||
 | 
			
		||||
Registration and login
 | 
			
		||||
======================
 | 
			
		||||
.. WARNING::
 | 
			
		||||
  The registration API is likely to change.
 | 
			
		||||
 | 
			
		||||
.. TODO
 | 
			
		||||
  - TODO Kegan : Make registration like login (just omit the "user" key on the 
 | 
			
		||||
    initial request?)
 | 
			
		||||
 | 
			
		||||
Clients must register with a home server in order to use Matrix. After 
 | 
			
		||||
registering, the client will be given an access token which must be used in ALL
 | 
			
		||||
| 
						 | 
				
			
			@ -1323,9 +1323,11 @@ a token sent to their email address, etc. This specification does not define how
 | 
			
		|||
home servers should authorise their users who want to login to their existing 
 | 
			
		||||
accounts, but instead defines the standard interface which implementations 
 | 
			
		||||
should follow so that ANY client can login to ANY home server. Clients login
 | 
			
		||||
using the |login|_ API.
 | 
			
		||||
using the |login|_ API. Clients register using the |register|_ API. Registration
 | 
			
		||||
follows the same procedure as login, but the path requests are sent to are
 | 
			
		||||
different.
 | 
			
		||||
 | 
			
		||||
The login process breaks down into the following:
 | 
			
		||||
The registration/login process breaks down into the following:
 | 
			
		||||
  1. Determine the requirements for logging in.
 | 
			
		||||
  2. Submit the login stage credentials.
 | 
			
		||||
  3. Get credentials or be told the next stage in the login process and repeat 
 | 
			
		||||
| 
						 | 
				
			
			@ -1383,7 +1385,7 @@ This specification defines the following login types:
 | 
			
		|||
 - ``m.login.oauth2``
 | 
			
		||||
 - ``m.login.email.code``
 | 
			
		||||
 - ``m.login.email.url``
 | 
			
		||||
 | 
			
		||||
 - ``m.login.email.identity``
 | 
			
		||||
 | 
			
		||||
Password-based
 | 
			
		||||
--------------
 | 
			
		||||
| 
						 | 
				
			
			@ -1531,6 +1533,31 @@ If the link has not been visited yet, a standard error response with an errcode
 | 
			
		|||
``M_LOGIN_EMAIL_URL_NOT_YET`` should be returned.
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Email-based (identity server)
 | 
			
		||||
-----------------------------
 | 
			
		||||
:Type:
 | 
			
		||||
  ``m.login.email.identity``
 | 
			
		||||
:Description:
 | 
			
		||||
  Login is supported by authorising an email address with an identity server.
 | 
			
		||||
 | 
			
		||||
Prior to submitting this, the client should authenticate with an identity server.
 | 
			
		||||
After authenticating, the session information should be submitted to the home server.
 | 
			
		||||
 | 
			
		||||
To respond to this type, reply with::
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    "type": "m.login.email.identity",
 | 
			
		||||
    "threepidCreds": [
 | 
			
		||||
      {
 | 
			
		||||
        "sid": "<identity server session id>",
 | 
			
		||||
        "clientSecret": "<identity server client secret>",
 | 
			
		||||
        "idServer": "<url of identity server authed with, e.g. 'matrix.org:8090'>"
 | 
			
		||||
      }
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
N-Factor Authentication
 | 
			
		||||
-----------------------
 | 
			
		||||
Multiple login stages can be combined to create N-factor authentication during login.
 | 
			
		||||
| 
						 | 
				
			
			@ -2242,6 +2269,9 @@ Transaction:
 | 
			
		|||
.. |login| replace:: ``/login``
 | 
			
		||||
.. _login: /docs/api/client-server/#!/-login
 | 
			
		||||
 | 
			
		||||
.. |register| replace:: ``/register``
 | 
			
		||||
.. _register: /docs/api/client-server/#!/-registration
 | 
			
		||||
 | 
			
		||||
.. |/rooms/<room_id>/messages| replace:: ``/rooms/<room_id>/messages``
 | 
			
		||||
.. _/rooms/<room_id>/messages: /docs/api/client-server/#!/-rooms/get_messages
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,3 +50,12 @@ class JoinRules(object):
 | 
			
		|||
    KNOCK = u"knock"
 | 
			
		||||
    INVITE = u"invite"
 | 
			
		||||
    PRIVATE = u"private"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class LoginType(object):
 | 
			
		||||
    PASSWORD = u"m.login.password"
 | 
			
		||||
    OAUTH = u"m.login.oauth2"
 | 
			
		||||
    EMAIL_CODE = u"m.login.email.code"
 | 
			
		||||
    EMAIL_URL = u"m.login.email.url"
 | 
			
		||||
    EMAIL_IDENTITY = u"m.login.email.identity"
 | 
			
		||||
    RECAPTCHA = u"m.login.recaptcha"
 | 
			
		||||
| 
						 | 
				
			
			@ -25,6 +25,7 @@ def serialize_event(hs, e):
 | 
			
		|||
    d = e.get_dict()
 | 
			
		||||
    if "age_ts" in d:
 | 
			
		||||
        d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"]
 | 
			
		||||
        del d["age_ts"]
 | 
			
		||||
 | 
			
		||||
    return d
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -56,6 +57,7 @@ class SynapseEvent(JsonEncodedObject):
 | 
			
		|||
        "state_key",
 | 
			
		||||
        "required_power_level",
 | 
			
		||||
        "age_ts",
 | 
			
		||||
        "prev_content",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    internal_keys = [
 | 
			
		||||
| 
						 | 
				
			
			@ -172,10 +174,6 @@ class SynapseEvent(JsonEncodedObject):
 | 
			
		|||
 | 
			
		||||
class SynapseStateEvent(SynapseEvent):
 | 
			
		||||
 | 
			
		||||
    valid_keys = SynapseEvent.valid_keys + [
 | 
			
		||||
        "prev_content",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    def __init__(self, **kwargs):
 | 
			
		||||
        if "state_key" not in kwargs:
 | 
			
		||||
            kwargs["state_key"] = ""
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,39 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
# Copyright 2014 OpenMarket Ltd
 | 
			
		||||
#
 | 
			
		||||
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
# you may not use this file except in compliance with the License.
 | 
			
		||||
# You may obtain a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
# Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
# See the License for the specific language governing permissions and
 | 
			
		||||
# limitations under the License.
 | 
			
		||||
 | 
			
		||||
from ._base import Config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EmailConfig(Config):
 | 
			
		||||
 | 
			
		||||
    def __init__(self, args):
 | 
			
		||||
        super(EmailConfig, self).__init__(args)
 | 
			
		||||
        self.email_from_address = args.email_from_address
 | 
			
		||||
        self.email_smtp_server = args.email_smtp_server
 | 
			
		||||
 | 
			
		||||
    @classmethod
 | 
			
		||||
    def add_arguments(cls, parser):
 | 
			
		||||
        super(EmailConfig, cls).add_arguments(parser)
 | 
			
		||||
        email_group = parser.add_argument_group("email")
 | 
			
		||||
        email_group.add_argument(
 | 
			
		||||
            "--email-from-address",
 | 
			
		||||
            default="FROM@EXAMPLE.COM",
 | 
			
		||||
            help="The address to send emails from (e.g. for password resets)."
 | 
			
		||||
        )
 | 
			
		||||
        email_group.add_argument(
 | 
			
		||||
            "--email-smtp-server",
 | 
			
		||||
            default="",
 | 
			
		||||
            help="The SMTP server to send emails from (e.g. for password resets)."
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -20,11 +20,15 @@ from .database import DatabaseConfig
 | 
			
		|||
from .ratelimiting import RatelimitConfig
 | 
			
		||||
from .repository import ContentRepositoryConfig
 | 
			
		||||
from .captcha import CaptchaConfig
 | 
			
		||||
from .email import EmailConfig
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
 | 
			
		||||
                       RatelimitConfig, ContentRepositoryConfig, CaptchaConfig):
 | 
			
		||||
                       RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
 | 
			
		||||
                       EmailConfig):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
if __name__=='__main__':
 | 
			
		||||
 | 
			
		||||
if __name__ == '__main__':
 | 
			
		||||
    import sys
 | 
			
		||||
    HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -293,7 +293,8 @@ class ReplicationLayer(object):
 | 
			
		|||
 | 
			
		||||
        for p in transaction.pdus:
 | 
			
		||||
            if "age" in p:
 | 
			
		||||
                p["age_ts"] = int(self.clock.time_msec()) - int(p["age"])
 | 
			
		||||
                p["age_ts"] = int(self._clock.time_msec()) - int(p["age"])
 | 
			
		||||
                del p["age"]
 | 
			
		||||
 | 
			
		||||
        pdu_list = [Pdu(**p) for p in transaction.pdus]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -602,8 +603,21 @@ class _TransactionQueue(object):
 | 
			
		|||
            logger.debug("TX [%s] Sending transaction...", destination)
 | 
			
		||||
 | 
			
		||||
            # Actually send the transaction
 | 
			
		||||
 | 
			
		||||
            # FIXME (erikj): This is a bit of a hack to make the Pdu age
 | 
			
		||||
            # keys work
 | 
			
		||||
            def cb(transaction):
 | 
			
		||||
                now = int(self._clock.time_msec())
 | 
			
		||||
                if "pdus" in transaction:
 | 
			
		||||
                    for p in transaction["pdus"]:
 | 
			
		||||
                        if "age_ts" in p:
 | 
			
		||||
                            p["age"] = now - int(p["age_ts"])
 | 
			
		||||
 | 
			
		||||
                return transaction
 | 
			
		||||
 | 
			
		||||
            code, response = yield self.transport_layer.send_transaction(
 | 
			
		||||
                transaction
 | 
			
		||||
                transaction,
 | 
			
		||||
                on_send_callback=cb,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            logger.debug("TX [%s] Sent transaction", destination)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -144,7 +144,7 @@ class TransportLayer(object):
 | 
			
		|||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    @log_function
 | 
			
		||||
    def send_transaction(self, transaction):
 | 
			
		||||
    def send_transaction(self, transaction, on_send_callback=None):
 | 
			
		||||
        """ Sends the given Transaction to it's destination
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
| 
						 | 
				
			
			@ -165,10 +165,23 @@ class TransportLayer(object):
 | 
			
		|||
 | 
			
		||||
        data = transaction.get_dict()
 | 
			
		||||
 | 
			
		||||
        # FIXME (erikj): This is a bit of a hack to make the Pdu age
 | 
			
		||||
        # keys work
 | 
			
		||||
        def cb(destination, method, path_bytes, producer):
 | 
			
		||||
            if not on_send_callback:
 | 
			
		||||
                return
 | 
			
		||||
 | 
			
		||||
            transaction = json.loads(producer.body)
 | 
			
		||||
 | 
			
		||||
            new_transaction = on_send_callback(transaction)
 | 
			
		||||
 | 
			
		||||
            producer.reset(new_transaction)
 | 
			
		||||
 | 
			
		||||
        code, response = yield self.client.put_json(
 | 
			
		||||
            transaction.destination,
 | 
			
		||||
            path=PREFIX + "/send/%s/" % transaction.transaction_id,
 | 
			
		||||
            data=data
 | 
			
		||||
            data=data,
 | 
			
		||||
            on_send_callback=cb,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        logger.debug(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -93,22 +93,18 @@ class FederationHandler(BaseHandler):
 | 
			
		|||
        """
 | 
			
		||||
        event = self.pdu_codec.event_from_pdu(pdu)
 | 
			
		||||
 | 
			
		||||
        logger.debug("Got event: %s", event.event_id)
 | 
			
		||||
 | 
			
		||||
        with (yield self.lock_manager.lock(pdu.context)):
 | 
			
		||||
            if event.is_state and not backfilled:
 | 
			
		||||
                is_new_state = yield self.state_handler.handle_new_state(
 | 
			
		||||
                    pdu
 | 
			
		||||
                )
 | 
			
		||||
                if not is_new_state:
 | 
			
		||||
                    return
 | 
			
		||||
            else:
 | 
			
		||||
                is_new_state = False
 | 
			
		||||
        # TODO: Implement something in federation that allows us to
 | 
			
		||||
        # respond to PDU.
 | 
			
		||||
 | 
			
		||||
        if hasattr(event, "state_key") and not is_new_state:
 | 
			
		||||
            logger.debug("Ignoring old state.")
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        target_is_mine = False
 | 
			
		||||
        if hasattr(event, "target_host"):
 | 
			
		||||
            target_is_mine = event.target_host == self.hs.hostname
 | 
			
		||||
| 
						 | 
				
			
			@ -139,7 +135,11 @@ class FederationHandler(BaseHandler):
 | 
			
		|||
 | 
			
		||||
        else:
 | 
			
		||||
            with (yield self.room_lock.lock(event.room_id)):
 | 
			
		||||
                yield self.store.persist_event(event, backfilled)
 | 
			
		||||
                yield self.store.persist_event(
 | 
			
		||||
                    event,
 | 
			
		||||
                    backfilled,
 | 
			
		||||
                    is_new_state=is_new_state
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            room = yield self.store.get_room(event.room_id)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,9 +17,13 @@ from twisted.internet import defer
 | 
			
		|||
 | 
			
		||||
from ._base import BaseHandler
 | 
			
		||||
from synapse.api.errors import LoginError, Codes
 | 
			
		||||
from synapse.http.client import PlainHttpClient
 | 
			
		||||
from synapse.util.emailutils import EmailException
 | 
			
		||||
import synapse.util.emailutils as emailutils
 | 
			
		||||
 | 
			
		||||
import bcrypt
 | 
			
		||||
import logging
 | 
			
		||||
import urllib
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -62,4 +66,41 @@ class LoginHandler(BaseHandler):
 | 
			
		|||
            defer.returnValue(token)
 | 
			
		||||
        else:
 | 
			
		||||
            logger.warn("Failed password login for user %s", user)
 | 
			
		||||
            raise LoginError(403, "", errcode=Codes.FORBIDDEN)
 | 
			
		||||
            raise LoginError(403, "", errcode=Codes.FORBIDDEN)
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def reset_password(self, user_id, email):
 | 
			
		||||
        is_valid = yield self._check_valid_association(user_id, email)
 | 
			
		||||
        logger.info("reset_password user=%s email=%s valid=%s", user_id, email,
 | 
			
		||||
                    is_valid)
 | 
			
		||||
        if is_valid:
 | 
			
		||||
            try:
 | 
			
		||||
                # send an email out
 | 
			
		||||
                emailutils.send_email(
 | 
			
		||||
                    smtp_server=self.hs.config.email_smtp_server,
 | 
			
		||||
                    from_addr=self.hs.config.email_from_address,
 | 
			
		||||
                    to_addr=email,
 | 
			
		||||
                    subject="Password Reset",
 | 
			
		||||
                    body="TODO."
 | 
			
		||||
                )
 | 
			
		||||
            except EmailException as e:
 | 
			
		||||
                logger.exception(e)
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def _check_valid_association(self, user_id, email):
 | 
			
		||||
        identity = yield self._query_email(email)
 | 
			
		||||
        if identity and "mxid" in identity:
 | 
			
		||||
            if identity["mxid"] == user_id:
 | 
			
		||||
                defer.returnValue(True)
 | 
			
		||||
                return
 | 
			
		||||
        defer.returnValue(False)
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def _query_email(self, email):
 | 
			
		||||
        httpCli = PlainHttpClient(self.hs)
 | 
			
		||||
        data = yield httpCli.get_json(
 | 
			
		||||
            'matrix.org:8090',  # TODO FIXME This should be configurable.
 | 
			
		||||
            "/_matrix/identity/api/v1/lookup?medium=email&address=" +
 | 
			
		||||
            "%s" % urllib.quote(email)
 | 
			
		||||
        )
 | 
			
		||||
        defer.returnValue(data)
 | 
			
		||||
| 
						 | 
				
			
			@ -40,8 +40,7 @@ class RegistrationHandler(BaseHandler):
 | 
			
		|||
        self.distributor.declare("registered_user")
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def register(self, localpart=None, password=None, threepidCreds=None, 
 | 
			
		||||
                 captcha_info={}):
 | 
			
		||||
    def register(self, localpart=None, password=None):
 | 
			
		||||
        """Registers a new client on the server.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
| 
						 | 
				
			
			@ -54,37 +53,6 @@ class RegistrationHandler(BaseHandler):
 | 
			
		|||
        Raises:
 | 
			
		||||
            RegistrationError if there was a problem registering.
 | 
			
		||||
        """
 | 
			
		||||
        if captcha_info:
 | 
			
		||||
            captcha_response = yield self._validate_captcha(
 | 
			
		||||
                captcha_info["ip"], 
 | 
			
		||||
                captcha_info["private_key"],
 | 
			
		||||
                captcha_info["challenge"],
 | 
			
		||||
                captcha_info["response"]
 | 
			
		||||
            )
 | 
			
		||||
            if not captcha_response["valid"]:
 | 
			
		||||
                logger.info("Invalid captcha entered from %s. Error: %s", 
 | 
			
		||||
                            captcha_info["ip"], captcha_response["error_url"])
 | 
			
		||||
                raise InvalidCaptchaError(
 | 
			
		||||
                    error_url=captcha_response["error_url"]
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                logger.info("Valid captcha entered from %s", captcha_info["ip"])
 | 
			
		||||
 | 
			
		||||
        if threepidCreds:
 | 
			
		||||
            for c in threepidCreds:
 | 
			
		||||
                logger.info("validating theeepidcred sid %s on id server %s",
 | 
			
		||||
                            c['sid'], c['idServer'])
 | 
			
		||||
                try:
 | 
			
		||||
                    threepid = yield self._threepid_from_creds(c)
 | 
			
		||||
                except:
 | 
			
		||||
                    logger.err()
 | 
			
		||||
                    raise RegistrationError(400, "Couldn't validate 3pid")
 | 
			
		||||
                    
 | 
			
		||||
                if not threepid:
 | 
			
		||||
                    raise RegistrationError(400, "Couldn't validate 3pid")
 | 
			
		||||
                logger.info("got threepid medium %s address %s", 
 | 
			
		||||
                            threepid['medium'], threepid['address'])
 | 
			
		||||
 | 
			
		||||
        password_hash = None
 | 
			
		||||
        if password:
 | 
			
		||||
            password_hash = bcrypt.hashpw(password, bcrypt.gensalt())
 | 
			
		||||
| 
						 | 
				
			
			@ -126,15 +94,54 @@ class RegistrationHandler(BaseHandler):
 | 
			
		|||
                        raise RegistrationError(
 | 
			
		||||
                            500, "Cannot generate user ID.")
 | 
			
		||||
 | 
			
		||||
        # Now we have a matrix ID, bind it to the threepids we were given
 | 
			
		||||
        if threepidCreds:
 | 
			
		||||
            for c in threepidCreds:
 | 
			
		||||
                # XXX: This should be a deferred list, shouldn't it?
 | 
			
		||||
                yield self._bind_threepid(c, user_id)
 | 
			
		||||
                
 | 
			
		||||
 | 
			
		||||
        defer.returnValue((user_id, token))
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def check_recaptcha(self, ip, private_key, challenge, response):
 | 
			
		||||
        """Checks a recaptcha is correct."""
 | 
			
		||||
 | 
			
		||||
        captcha_response = yield self._validate_captcha(
 | 
			
		||||
            ip,
 | 
			
		||||
            private_key,
 | 
			
		||||
            challenge,
 | 
			
		||||
            response
 | 
			
		||||
        )
 | 
			
		||||
        if not captcha_response["valid"]:
 | 
			
		||||
            logger.info("Invalid captcha entered from %s. Error: %s",
 | 
			
		||||
                        ip, captcha_response["error_url"])
 | 
			
		||||
            raise InvalidCaptchaError(
 | 
			
		||||
                error_url=captcha_response["error_url"]
 | 
			
		||||
            )
 | 
			
		||||
        else:
 | 
			
		||||
            logger.info("Valid captcha entered from %s", ip)
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def register_email(self, threepidCreds):
 | 
			
		||||
        """Registers emails with an identity server."""
 | 
			
		||||
 | 
			
		||||
        for c in threepidCreds:
 | 
			
		||||
            logger.info("validating theeepidcred sid %s on id server %s",
 | 
			
		||||
                        c['sid'], c['idServer'])
 | 
			
		||||
            try:
 | 
			
		||||
                threepid = yield self._threepid_from_creds(c)
 | 
			
		||||
            except:
 | 
			
		||||
                logger.err()
 | 
			
		||||
                raise RegistrationError(400, "Couldn't validate 3pid")
 | 
			
		||||
 | 
			
		||||
            if not threepid:
 | 
			
		||||
                raise RegistrationError(400, "Couldn't validate 3pid")
 | 
			
		||||
            logger.info("got threepid medium %s address %s",
 | 
			
		||||
                        threepid['medium'], threepid['address'])
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def bind_emails(self, user_id, threepidCreds):
 | 
			
		||||
        """Links emails with a user ID and informs an identity server."""
 | 
			
		||||
 | 
			
		||||
        # Now we have a matrix ID, bind it to the threepids we were given
 | 
			
		||||
        for c in threepidCreds:
 | 
			
		||||
            # XXX: This should be a deferred list, shouldn't it?
 | 
			
		||||
            yield self._bind_threepid(c, user_id)
 | 
			
		||||
 | 
			
		||||
    def _generate_token(self, user_id):
 | 
			
		||||
        # urlsafe variant uses _ and - so use . as the separator and replace
 | 
			
		||||
        # all =s with .s so http clients don't quote =s when it is used as
 | 
			
		||||
| 
						 | 
				
			
			@ -149,17 +156,17 @@ class RegistrationHandler(BaseHandler):
 | 
			
		|||
    def _threepid_from_creds(self, creds):
 | 
			
		||||
        httpCli = PlainHttpClient(self.hs)
 | 
			
		||||
        # XXX: make this configurable!
 | 
			
		||||
        trustedIdServers = [ 'matrix.org:8090' ]
 | 
			
		||||
        trustedIdServers = ['matrix.org:8090']
 | 
			
		||||
        if not creds['idServer'] in trustedIdServers:
 | 
			
		||||
            logger.warn('%s is not a trusted ID server: rejecting 3pid '+
 | 
			
		||||
            logger.warn('%s is not a trusted ID server: rejecting 3pid ' +
 | 
			
		||||
                        'credentials', creds['idServer'])
 | 
			
		||||
            defer.returnValue(None)
 | 
			
		||||
        data = yield httpCli.get_json(
 | 
			
		||||
            creds['idServer'],
 | 
			
		||||
            "/_matrix/identity/api/v1/3pid/getValidated3pid",
 | 
			
		||||
            { 'sid': creds['sid'], 'clientSecret': creds['clientSecret'] }
 | 
			
		||||
            {'sid': creds['sid'], 'clientSecret': creds['clientSecret']}
 | 
			
		||||
        )
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        if 'medium' in data:
 | 
			
		||||
            defer.returnValue(data)
 | 
			
		||||
        defer.returnValue(None)
 | 
			
		||||
| 
						 | 
				
			
			@ -170,44 +177,45 @@ class RegistrationHandler(BaseHandler):
 | 
			
		|||
        data = yield httpCli.post_urlencoded_get_json(
 | 
			
		||||
            creds['idServer'],
 | 
			
		||||
            "/_matrix/identity/api/v1/3pid/bind",
 | 
			
		||||
            { 'sid': creds['sid'], 'clientSecret': creds['clientSecret'], 
 | 
			
		||||
            'mxid':mxid }
 | 
			
		||||
            {'sid': creds['sid'], 'clientSecret': creds['clientSecret'],
 | 
			
		||||
            'mxid': mxid}
 | 
			
		||||
        )
 | 
			
		||||
        defer.returnValue(data)
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def _validate_captcha(self, ip_addr, private_key, challenge, response):
 | 
			
		||||
        """Validates the captcha provided.
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            dict: Containing 'valid'(bool) and 'error_url'(str) if invalid.
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        response = yield self._submit_captcha(ip_addr, private_key, challenge, 
 | 
			
		||||
        response = yield self._submit_captcha(ip_addr, private_key, challenge,
 | 
			
		||||
                                              response)
 | 
			
		||||
        # parse Google's response. Lovely format..
 | 
			
		||||
        lines = response.split('\n')
 | 
			
		||||
        json = {
 | 
			
		||||
            "valid": lines[0] == 'true',
 | 
			
		||||
            "error_url": "http://www.google.com/recaptcha/api/challenge?"+
 | 
			
		||||
            "error_url": "http://www.google.com/recaptcha/api/challenge?" +
 | 
			
		||||
                         "error=%s" % lines[1]
 | 
			
		||||
        }
 | 
			
		||||
        defer.returnValue(json)
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def _submit_captcha(self, ip_addr, private_key, challenge, response):
 | 
			
		||||
        client = PlainHttpClient(self.hs)
 | 
			
		||||
        data = yield client.post_urlencoded_get_raw(
 | 
			
		||||
            "www.google.com:80",
 | 
			
		||||
            "/recaptcha/api/verify",
 | 
			
		||||
            accept_partial=True,  # twisted dislikes google's response, no content length.
 | 
			
		||||
            args={ 
 | 
			
		||||
                'privatekey': private_key, 
 | 
			
		||||
            # twisted dislikes google's response, no content length.
 | 
			
		||||
            accept_partial=True,
 | 
			
		||||
            args={
 | 
			
		||||
                'privatekey': private_key,
 | 
			
		||||
                'remoteip': ip_addr,
 | 
			
		||||
                'challenge': challenge,
 | 
			
		||||
                'response': response
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        defer.returnValue(data)
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -122,7 +122,7 @@ class TwistedHttpClient(HttpClient):
 | 
			
		|||
        self.hs = hs
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def put_json(self, destination, path, data):
 | 
			
		||||
    def put_json(self, destination, path, data, on_send_callback=None):
 | 
			
		||||
        if destination in _destination_mappings:
 | 
			
		||||
            destination = _destination_mappings[destination]
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -131,7 +131,8 @@ class TwistedHttpClient(HttpClient):
 | 
			
		|||
            "PUT",
 | 
			
		||||
            path.encode("ascii"),
 | 
			
		||||
            producer=_JsonProducer(data),
 | 
			
		||||
            headers_dict={"Content-Type": ["application/json"]}
 | 
			
		||||
            headers_dict={"Content-Type": ["application/json"]},
 | 
			
		||||
            on_send_callback=on_send_callback,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        logger.debug("Getting resp body")
 | 
			
		||||
| 
						 | 
				
			
			@ -218,7 +219,7 @@ class TwistedHttpClient(HttpClient):
 | 
			
		|||
    @defer.inlineCallbacks
 | 
			
		||||
    def _create_request(self, destination, method, path_bytes, param_bytes=b"",
 | 
			
		||||
                        query_bytes=b"", producer=None, headers_dict={},
 | 
			
		||||
                        retry_on_dns_fail=True):
 | 
			
		||||
                        retry_on_dns_fail=True, on_send_callback=None):
 | 
			
		||||
        """ Creates and sends a request to the given url
 | 
			
		||||
        """
 | 
			
		||||
        headers_dict[b"User-Agent"] = [b"Synapse"]
 | 
			
		||||
| 
						 | 
				
			
			@ -242,6 +243,9 @@ class TwistedHttpClient(HttpClient):
 | 
			
		|||
        endpoint = self._getEndpoint(reactor, destination);
 | 
			
		||||
 | 
			
		||||
        while True:
 | 
			
		||||
            if on_send_callback:
 | 
			
		||||
                on_send_callback(destination, method, path_bytes, producer)
 | 
			
		||||
 | 
			
		||||
            try:
 | 
			
		||||
                response = yield self.agent.request(
 | 
			
		||||
                    destination,
 | 
			
		||||
| 
						 | 
				
			
			@ -310,6 +314,9 @@ class _JsonProducer(object):
 | 
			
		|||
    """ Used by the twisted http client to create the HTTP body from json
 | 
			
		||||
    """
 | 
			
		||||
    def __init__(self, jsn):
 | 
			
		||||
        self.reset(jsn)
 | 
			
		||||
 | 
			
		||||
    def reset(self, jsn):
 | 
			
		||||
        self.body = encode_canonical_json(jsn)
 | 
			
		||||
        self.length = len(self.body)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -73,6 +73,27 @@ class LoginFallbackRestServlet(RestServlet):
 | 
			
		|||
        return (200, {})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PasswordResetRestServlet(RestServlet):
 | 
			
		||||
    PATTERN = client_path_pattern("/login/reset")
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def on_POST(self, request):
 | 
			
		||||
        reset_info = _parse_json(request)
 | 
			
		||||
        try:
 | 
			
		||||
            email = reset_info["email"]
 | 
			
		||||
            user_id = reset_info["user_id"]
 | 
			
		||||
            handler = self.handlers.login_handler
 | 
			
		||||
            yield handler.reset_password(user_id, email)
 | 
			
		||||
            # purposefully give no feedback to avoid people hammering different
 | 
			
		||||
            # combinations.
 | 
			
		||||
            defer.returnValue((200, {}))
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            raise SynapseError(
 | 
			
		||||
                400,
 | 
			
		||||
                "Missing keys. Requires 'email' and 'user_id'."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _parse_json(request):
 | 
			
		||||
    try:
 | 
			
		||||
        content = json.loads(request.content.read())
 | 
			
		||||
| 
						 | 
				
			
			@ -85,3 +106,4 @@ def _parse_json(request):
 | 
			
		|||
 | 
			
		||||
def register_servlets(hs, http_server):
 | 
			
		||||
    LoginRestServlet(hs).register(http_server)
 | 
			
		||||
    # TODO PasswordResetRestServlet(hs).register(http_server)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,89 +17,218 @@
 | 
			
		|||
from twisted.internet import defer
 | 
			
		||||
 | 
			
		||||
from synapse.api.errors import SynapseError, Codes
 | 
			
		||||
from synapse.api.constants import LoginType
 | 
			
		||||
from base import RestServlet, client_path_pattern
 | 
			
		||||
import synapse.util.stringutils as stringutils
 | 
			
		||||
 | 
			
		||||
import json
 | 
			
		||||
import logging
 | 
			
		||||
import urllib
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class RegisterRestServlet(RestServlet):
 | 
			
		||||
    """Handles registration with the home server.
 | 
			
		||||
 | 
			
		||||
    This servlet is in control of the registration flow; the registration
 | 
			
		||||
    handler doesn't have a concept of multi-stages or sessions.
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    PATTERN = client_path_pattern("/register$")
 | 
			
		||||
 | 
			
		||||
    def __init__(self, hs):
 | 
			
		||||
        super(RegisterRestServlet, self).__init__(hs)
 | 
			
		||||
        # sessions are stored as:
 | 
			
		||||
        # self.sessions = {
 | 
			
		||||
        #   "session_id" : { __session_dict__ }
 | 
			
		||||
        # }
 | 
			
		||||
        # TODO: persistent storage
 | 
			
		||||
        self.sessions = {}
 | 
			
		||||
 | 
			
		||||
    def on_GET(self, request):
 | 
			
		||||
        if self.hs.config.enable_registration_captcha:
 | 
			
		||||
            return (200, {
 | 
			
		||||
                "flows": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": LoginType.RECAPTCHA,
 | 
			
		||||
                        "stages": ([LoginType.RECAPTCHA,
 | 
			
		||||
                                    LoginType.EMAIL_IDENTITY,
 | 
			
		||||
                                    LoginType.PASSWORD])
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": LoginType.RECAPTCHA,
 | 
			
		||||
                        "stages": [LoginType.RECAPTCHA, LoginType.PASSWORD]
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            })
 | 
			
		||||
        else:
 | 
			
		||||
            return (200, {
 | 
			
		||||
                "flows": [
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": LoginType.EMAIL_IDENTITY,
 | 
			
		||||
                        "stages": ([LoginType.EMAIL_IDENTITY,
 | 
			
		||||
                                    LoginType.PASSWORD])
 | 
			
		||||
                    },
 | 
			
		||||
                    {
 | 
			
		||||
                        "type": LoginType.PASSWORD
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            })
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def on_POST(self, request):
 | 
			
		||||
        desired_user_id = None
 | 
			
		||||
        password = None
 | 
			
		||||
        register_json = _parse_json(request)
 | 
			
		||||
 | 
			
		||||
        session = (register_json["session"] if "session" in register_json
 | 
			
		||||
                  else None)
 | 
			
		||||
        login_type = None
 | 
			
		||||
        if "type" not in register_json:
 | 
			
		||||
            raise SynapseError(400, "Missing 'type' key.")
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            register_json = json.loads(request.content.read())
 | 
			
		||||
            if "password" in register_json:
 | 
			
		||||
                password = register_json["password"].encode("utf-8")
 | 
			
		||||
 | 
			
		||||
            if type(register_json["user_id"]) == unicode:
 | 
			
		||||
                desired_user_id = register_json["user_id"].encode("utf-8")
 | 
			
		||||
                if urllib.quote(desired_user_id) != desired_user_id:
 | 
			
		||||
                    raise SynapseError(
 | 
			
		||||
                        400,
 | 
			
		||||
                        "User ID must only contain characters which do not " +
 | 
			
		||||
                        "require URL encoding.")
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            defer.returnValue((400, "No JSON object."))
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            pass  # user_id is optional
 | 
			
		||||
 | 
			
		||||
        threepidCreds = None
 | 
			
		||||
        if 'threepidCreds' in register_json:
 | 
			
		||||
            threepidCreds = register_json['threepidCreds']
 | 
			
		||||
            
 | 
			
		||||
        captcha = {}
 | 
			
		||||
        if self.hs.config.enable_registration_captcha:
 | 
			
		||||
            challenge = None
 | 
			
		||||
            user_response = None
 | 
			
		||||
            try:
 | 
			
		||||
                captcha_type = register_json["captcha"]["type"]
 | 
			
		||||
                if captcha_type != "m.login.recaptcha":
 | 
			
		||||
                    raise SynapseError(400, "Sorry, only m.login.recaptcha " +
 | 
			
		||||
                                       "requests are supported.")
 | 
			
		||||
                challenge = register_json["captcha"]["challenge"]
 | 
			
		||||
                user_response = register_json["captcha"]["response"]
 | 
			
		||||
            except KeyError:
 | 
			
		||||
                raise SynapseError(400, "Captcha response is required",
 | 
			
		||||
                                   errcode=Codes.CAPTCHA_NEEDED)
 | 
			
		||||
            
 | 
			
		||||
            # TODO determine the source IP : May be an X-Forwarding-For header depending on config
 | 
			
		||||
            ip_addr = request.getClientIP()
 | 
			
		||||
            if self.hs.config.captcha_ip_origin_is_x_forwarded:
 | 
			
		||||
                # use the header
 | 
			
		||||
                if request.requestHeaders.hasHeader("X-Forwarded-For"):
 | 
			
		||||
                    ip_addr = request.requestHeaders.getRawHeaders(
 | 
			
		||||
                        "X-Forwarded-For")[0]
 | 
			
		||||
            
 | 
			
		||||
            captcha = {
 | 
			
		||||
                "ip": ip_addr,
 | 
			
		||||
                "private_key": self.hs.config.recaptcha_private_key,
 | 
			
		||||
                "challenge": challenge,
 | 
			
		||||
                "response": user_response
 | 
			
		||||
            login_type = register_json["type"]
 | 
			
		||||
            stages = {
 | 
			
		||||
                LoginType.RECAPTCHA: self._do_recaptcha,
 | 
			
		||||
                LoginType.PASSWORD: self._do_password,
 | 
			
		||||
                LoginType.EMAIL_IDENTITY: self._do_email_identity
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
 | 
			
		||||
            session_info = self._get_session_info(request, session)
 | 
			
		||||
            logger.debug("%s : session info %s   request info %s",
 | 
			
		||||
                         login_type, session_info, register_json)
 | 
			
		||||
            response = yield stages[login_type](
 | 
			
		||||
                request,
 | 
			
		||||
                register_json,
 | 
			
		||||
                session_info
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if "access_token" not in response:
 | 
			
		||||
                # isn't a final response
 | 
			
		||||
                response["session"] = session_info["id"]
 | 
			
		||||
 | 
			
		||||
            defer.returnValue((200, response))
 | 
			
		||||
        except KeyError as e:
 | 
			
		||||
            logger.exception(e)
 | 
			
		||||
            raise SynapseError(400, "Missing JSON keys for login type %s." % login_type)
 | 
			
		||||
 | 
			
		||||
    def on_OPTIONS(self, request):
 | 
			
		||||
        return (200, {})
 | 
			
		||||
 | 
			
		||||
    def _get_session_info(self, request, session_id):
 | 
			
		||||
        if not session_id:
 | 
			
		||||
            # create a new session
 | 
			
		||||
            while session_id is None or session_id in self.sessions:
 | 
			
		||||
                session_id = stringutils.random_string(24)
 | 
			
		||||
            self.sessions[session_id] = {
 | 
			
		||||
                "id": session_id,
 | 
			
		||||
                LoginType.EMAIL_IDENTITY: False,
 | 
			
		||||
                LoginType.RECAPTCHA: False
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
        return self.sessions[session_id]
 | 
			
		||||
 | 
			
		||||
    def _save_session(self, session):
 | 
			
		||||
        # TODO: Persistent storage
 | 
			
		||||
        logger.debug("Saving session %s", session)
 | 
			
		||||
        self.sessions[session["id"]] = session
 | 
			
		||||
 | 
			
		||||
    def _remove_session(self, session):
 | 
			
		||||
        logger.debug("Removing session %s", session)
 | 
			
		||||
        self.sessions.pop(session["id"])
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def _do_recaptcha(self, request, register_json, session):
 | 
			
		||||
        if not self.hs.config.enable_registration_captcha:
 | 
			
		||||
            raise SynapseError(400, "Captcha not required.")
 | 
			
		||||
 | 
			
		||||
        challenge = None
 | 
			
		||||
        user_response = None
 | 
			
		||||
        try:
 | 
			
		||||
            challenge = register_json["challenge"]
 | 
			
		||||
            user_response = register_json["response"]
 | 
			
		||||
        except KeyError:
 | 
			
		||||
            raise SynapseError(400, "Captcha response is required",
 | 
			
		||||
                               errcode=Codes.CAPTCHA_NEEDED)
 | 
			
		||||
 | 
			
		||||
        # May be an X-Forwarding-For header depending on config
 | 
			
		||||
        ip_addr = request.getClientIP()
 | 
			
		||||
        if self.hs.config.captcha_ip_origin_is_x_forwarded:
 | 
			
		||||
            # use the header
 | 
			
		||||
            if request.requestHeaders.hasHeader("X-Forwarded-For"):
 | 
			
		||||
                ip_addr = request.requestHeaders.getRawHeaders(
 | 
			
		||||
                    "X-Forwarded-For")[0]
 | 
			
		||||
 | 
			
		||||
        handler = self.handlers.registration_handler
 | 
			
		||||
        yield handler.check_recaptcha(
 | 
			
		||||
            ip_addr,
 | 
			
		||||
            self.hs.config.recaptcha_private_key,
 | 
			
		||||
            challenge,
 | 
			
		||||
            user_response
 | 
			
		||||
        )
 | 
			
		||||
        session[LoginType.RECAPTCHA] = True  # mark captcha as done
 | 
			
		||||
        self._save_session(session)
 | 
			
		||||
        defer.returnValue({
 | 
			
		||||
            "next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY]
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def _do_email_identity(self, request, register_json, session):
 | 
			
		||||
        if (self.hs.config.enable_registration_captcha and
 | 
			
		||||
                not session[LoginType.RECAPTCHA]):
 | 
			
		||||
            raise SynapseError(400, "Captcha is required.")
 | 
			
		||||
 | 
			
		||||
        threepidCreds = register_json['threepidCreds']
 | 
			
		||||
        handler = self.handlers.registration_handler
 | 
			
		||||
        yield handler.register_email(threepidCreds)
 | 
			
		||||
        session["threepidCreds"] = threepidCreds  # store creds for next stage
 | 
			
		||||
        session[LoginType.EMAIL_IDENTITY] = True  # mark email as done
 | 
			
		||||
        self._save_session(session)
 | 
			
		||||
        defer.returnValue({
 | 
			
		||||
            "next": LoginType.PASSWORD
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def _do_password(self, request, register_json, session):
 | 
			
		||||
        if (self.hs.config.enable_registration_captcha and
 | 
			
		||||
                not session[LoginType.RECAPTCHA]):
 | 
			
		||||
            # captcha should've been done by this stage!
 | 
			
		||||
            raise SynapseError(400, "Captcha is required.")
 | 
			
		||||
 | 
			
		||||
        password = register_json["password"].encode("utf-8")
 | 
			
		||||
        desired_user_id = (register_json["user"].encode("utf-8") if "user"
 | 
			
		||||
                          in register_json else None)
 | 
			
		||||
        if desired_user_id and urllib.quote(desired_user_id) != desired_user_id:
 | 
			
		||||
            raise SynapseError(
 | 
			
		||||
                400,
 | 
			
		||||
                "User ID must only contain characters which do not " +
 | 
			
		||||
                "require URL encoding.")
 | 
			
		||||
        handler = self.handlers.registration_handler
 | 
			
		||||
        (user_id, token) = yield handler.register(
 | 
			
		||||
            localpart=desired_user_id,
 | 
			
		||||
            password=password,
 | 
			
		||||
            threepidCreds=threepidCreds,
 | 
			
		||||
            captcha_info=captcha)
 | 
			
		||||
            password=password
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if session[LoginType.EMAIL_IDENTITY]:
 | 
			
		||||
            yield handler.bind_emails(user_id, session["threepidCreds"])
 | 
			
		||||
 | 
			
		||||
        result = {
 | 
			
		||||
            "user_id": user_id,
 | 
			
		||||
            "access_token": token,
 | 
			
		||||
            "home_server": self.hs.hostname,
 | 
			
		||||
        }
 | 
			
		||||
        defer.returnValue(
 | 
			
		||||
            (200, result)
 | 
			
		||||
        )
 | 
			
		||||
        self._remove_session(session)
 | 
			
		||||
        defer.returnValue(result)
 | 
			
		||||
 | 
			
		||||
    def on_OPTIONS(self, request):
 | 
			
		||||
        return (200, {})
 | 
			
		||||
 | 
			
		||||
def _parse_json(request):
 | 
			
		||||
    try:
 | 
			
		||||
        content = json.loads(request.content.read())
 | 
			
		||||
        if type(content) != dict:
 | 
			
		||||
            raise SynapseError(400, "Content must be a JSON object.")
 | 
			
		||||
        return content
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        raise SynapseError(400, "Content not JSON.")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_servlets(hs, http_server):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -85,7 +85,8 @@ class DataStore(RoomMemberStore, RoomStore,
 | 
			
		|||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    @log_function
 | 
			
		||||
    def persist_event(self, event=None, backfilled=False, pdu=None):
 | 
			
		||||
    def persist_event(self, event=None, backfilled=False, pdu=None,
 | 
			
		||||
                      is_new_state=True):
 | 
			
		||||
        stream_ordering = None
 | 
			
		||||
        if backfilled:
 | 
			
		||||
            if not self.min_token_deferred.called:
 | 
			
		||||
| 
						 | 
				
			
			@ -100,6 +101,7 @@ class DataStore(RoomMemberStore, RoomStore,
 | 
			
		|||
                event=event,
 | 
			
		||||
                backfilled=backfilled,
 | 
			
		||||
                stream_ordering=stream_ordering,
 | 
			
		||||
                is_new_state=is_new_state,
 | 
			
		||||
            )
 | 
			
		||||
        except _RollbackButIsFineException as e:
 | 
			
		||||
            pass
 | 
			
		||||
| 
						 | 
				
			
			@ -126,12 +128,14 @@ class DataStore(RoomMemberStore, RoomStore,
 | 
			
		|||
        defer.returnValue(event)
 | 
			
		||||
 | 
			
		||||
    def _persist_pdu_event_txn(self, txn, pdu=None, event=None,
 | 
			
		||||
                               backfilled=False, stream_ordering=None):
 | 
			
		||||
                               backfilled=False, stream_ordering=None,
 | 
			
		||||
                               is_new_state=True):
 | 
			
		||||
        if pdu is not None:
 | 
			
		||||
            self._persist_event_pdu_txn(txn, pdu)
 | 
			
		||||
        if event is not None:
 | 
			
		||||
            return self._persist_event_txn(
 | 
			
		||||
                txn, event, backfilled, stream_ordering
 | 
			
		||||
                txn, event, backfilled, stream_ordering,
 | 
			
		||||
                is_new_state=is_new_state,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    def _persist_event_pdu_txn(self, txn, pdu):
 | 
			
		||||
| 
						 | 
				
			
			@ -158,7 +162,8 @@ class DataStore(RoomMemberStore, RoomStore,
 | 
			
		|||
        self._update_min_depth_for_context_txn(txn, pdu.context, pdu.depth)
 | 
			
		||||
 | 
			
		||||
    @log_function
 | 
			
		||||
    def _persist_event_txn(self, txn, event, backfilled, stream_ordering=None):
 | 
			
		||||
    def _persist_event_txn(self, txn, event, backfilled, stream_ordering=None,
 | 
			
		||||
                           is_new_state=True):
 | 
			
		||||
        if event.type == RoomMemberEvent.TYPE:
 | 
			
		||||
            self._store_room_member_txn(txn, event)
 | 
			
		||||
        elif event.type == FeedbackEvent.TYPE:
 | 
			
		||||
| 
						 | 
				
			
			@ -212,7 +217,7 @@ class DataStore(RoomMemberStore, RoomStore,
 | 
			
		|||
            )
 | 
			
		||||
            raise _RollbackButIsFineException("_persist_event")
 | 
			
		||||
 | 
			
		||||
        if not backfilled and hasattr(event, "state_key"):
 | 
			
		||||
        if is_new_state and hasattr(event, "state_key"):
 | 
			
		||||
            vals = {
 | 
			
		||||
                "event_id": event.event_id,
 | 
			
		||||
                "room_id": event.room_id,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,71 @@
 | 
			
		|||
# -*- coding: utf-8 -*-
 | 
			
		||||
# Copyright 2014 OpenMarket Ltd
 | 
			
		||||
#
 | 
			
		||||
# Licensed under the Apache License, Version 2.0 (the "License");
 | 
			
		||||
# you may not use this file except in compliance with the License.
 | 
			
		||||
# You may obtain a copy of the License at
 | 
			
		||||
#
 | 
			
		||||
#     http://www.apache.org/licenses/LICENSE-2.0
 | 
			
		||||
#
 | 
			
		||||
# Unless required by applicable law or agreed to in writing, software
 | 
			
		||||
# distributed under the License is distributed on an "AS IS" BASIS,
 | 
			
		||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
			
		||||
# See the License for the specific language governing permissions and
 | 
			
		||||
# limitations under the License.
 | 
			
		||||
""" This module allows you to send out emails.
 | 
			
		||||
"""
 | 
			
		||||
import email.utils
 | 
			
		||||
import smtplib
 | 
			
		||||
import twisted.python.log
 | 
			
		||||
from email.mime.text import MIMEText
 | 
			
		||||
from email.mime.multipart import MIMEMultipart
 | 
			
		||||
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
logger = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class EmailException(Exception):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def send_email(smtp_server, from_addr, to_addr, subject, body):
 | 
			
		||||
    """Sends an email.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        smtp_server(str): The SMTP server to use.
 | 
			
		||||
        from_addr(str): The address to send from.
 | 
			
		||||
        to_addr(str): The address to send to.
 | 
			
		||||
        subject(str): The subject of the email.
 | 
			
		||||
        body(str): The plain text body of the email.
 | 
			
		||||
    Raises:
 | 
			
		||||
        EmailException if there was a problem sending the mail.
 | 
			
		||||
    """
 | 
			
		||||
    if not smtp_server or not from_addr or not to_addr:
 | 
			
		||||
        raise EmailException("Need SMTP server, from and to addresses. Check " +
 | 
			
		||||
                             "the config to set these.")
 | 
			
		||||
 | 
			
		||||
    msg = MIMEMultipart('alternative')
 | 
			
		||||
    msg['Subject'] = subject
 | 
			
		||||
    msg['From'] = from_addr
 | 
			
		||||
    msg['To'] = to_addr
 | 
			
		||||
    plain_part = MIMEText(body)
 | 
			
		||||
    msg.attach(plain_part)
 | 
			
		||||
 | 
			
		||||
    raw_from = email.utils.parseaddr(from_addr)[1]
 | 
			
		||||
    raw_to = email.utils.parseaddr(to_addr)[1]
 | 
			
		||||
    if not raw_from or not raw_to:
 | 
			
		||||
        raise EmailException("Couldn't parse from/to address.")
 | 
			
		||||
 | 
			
		||||
    logger.info("Sending email to %s on server %s with subject %s",
 | 
			
		||||
                to_addr, smtp_server, subject)
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        smtp = smtplib.SMTP(smtp_server)
 | 
			
		||||
        smtp.sendmail(raw_from, raw_to, msg.as_string())
 | 
			
		||||
        smtp.quit()
 | 
			
		||||
    except Exception as origException:
 | 
			
		||||
        twisted.python.log.err()
 | 
			
		||||
        ese = EmailException()
 | 
			
		||||
        ese.cause = origException
 | 
			
		||||
        raise ese
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ from twisted.internet import defer
 | 
			
		|||
from tests import unittest
 | 
			
		||||
 | 
			
		||||
# python imports
 | 
			
		||||
from mock import Mock
 | 
			
		||||
from mock import Mock, ANY
 | 
			
		||||
 | 
			
		||||
from ..utils import MockHttpResource, MockClock
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -181,7 +181,8 @@ class FederationTestCase(unittest.TestCase):
 | 
			
		|||
                            "depth": 1,
 | 
			
		||||
                        },
 | 
			
		||||
                    ]
 | 
			
		||||
                }
 | 
			
		||||
                },
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
| 
						 | 
				
			
			@ -212,7 +213,9 @@ class FederationTestCase(unittest.TestCase):
 | 
			
		|||
                            "content": {"testing": "content here"},
 | 
			
		||||
                        }
 | 
			
		||||
                    ],
 | 
			
		||||
                })
 | 
			
		||||
                },
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def test_recv_edu(self):
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -74,7 +74,9 @@ class FederationTestCase(unittest.TestCase):
 | 
			
		|||
 | 
			
		||||
        yield self.handlers.federation_handler.on_receive_pdu(pdu, False)
 | 
			
		||||
 | 
			
		||||
        self.datastore.persist_event.assert_called_once_with(ANY, False)
 | 
			
		||||
        self.datastore.persist_event.assert_called_once_with(
 | 
			
		||||
            ANY, False, is_new_state=False
 | 
			
		||||
        )
 | 
			
		||||
        self.notifier.on_new_room_event.assert_called_once_with(ANY)
 | 
			
		||||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -289,7 +289,8 @@ class PresenceInvitesTestCase(unittest.TestCase):
 | 
			
		|||
                        "observer_user": "@apple:test",
 | 
			
		||||
                        "observed_user": "@cabbage:elsewhere",
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -317,7 +318,8 @@ class PresenceInvitesTestCase(unittest.TestCase):
 | 
			
		|||
                        "observer_user": "@cabbage:elsewhere",
 | 
			
		||||
                        "observed_user": "@apple:test",
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -352,7 +354,8 @@ class PresenceInvitesTestCase(unittest.TestCase):
 | 
			
		|||
                        "observer_user": "@cabbage:elsewhere",
 | 
			
		||||
                        "observed_user": "@durian:test",
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -767,7 +770,8 @@ class PresencePushTestCase(unittest.TestCase):
 | 
			
		|||
                             "last_active_ago": 0},
 | 
			
		||||
                        ],
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -782,7 +786,8 @@ class PresencePushTestCase(unittest.TestCase):
 | 
			
		|||
                             "last_active_ago": 0},
 | 
			
		||||
                        ],
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -908,6 +913,7 @@ class PresencePushTestCase(unittest.TestCase):
 | 
			
		|||
                        ],
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -922,6 +928,7 @@ class PresencePushTestCase(unittest.TestCase):
 | 
			
		|||
                        ],
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -951,6 +958,7 @@ class PresencePushTestCase(unittest.TestCase):
 | 
			
		|||
                        ],
 | 
			
		||||
                    }
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -1147,6 +1155,7 @@ class PresencePollingTestCase(unittest.TestCase):
 | 
			
		|||
                        "poll": [ "@potato:remote" ],
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -1159,6 +1168,7 @@ class PresencePollingTestCase(unittest.TestCase):
 | 
			
		|||
                        "push": [ {"user_id": "@clementine:test" }],
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -1187,6 +1197,7 @@ class PresencePollingTestCase(unittest.TestCase):
 | 
			
		|||
                        "push": [ {"user_id": "@fig:test" }],
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -1219,6 +1230,7 @@ class PresencePollingTestCase(unittest.TestCase):
 | 
			
		|||
                        "unpoll": [ "@potato:remote" ],
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -1250,6 +1262,7 @@ class PresencePollingTestCase(unittest.TestCase):
 | 
			
		|||
                        ],
 | 
			
		||||
                    },
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -169,7 +169,8 @@ class TypingNotificationsTestCase(unittest.TestCase):
 | 
			
		|||
                        "user_id": self.u_apple.to_string(),
 | 
			
		||||
                        "typing": True,
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			@ -219,7 +220,8 @@ class TypingNotificationsTestCase(unittest.TestCase):
 | 
			
		|||
                        "user_id": self.u_apple.to_string(),
 | 
			
		||||
                        "typing": False,
 | 
			
		||||
                    }
 | 
			
		||||
                )
 | 
			
		||||
                ),
 | 
			
		||||
                on_send_callback=ANY,
 | 
			
		||||
            ),
 | 
			
		||||
            defer.succeed((200, "OK"))
 | 
			
		||||
        )
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -95,8 +95,14 @@ class RestTestCase(unittest.TestCase):
 | 
			
		|||
 | 
			
		||||
    @defer.inlineCallbacks
 | 
			
		||||
    def register(self, user_id):
 | 
			
		||||
        (code, response) = yield self.mock_resource.trigger("POST", "/register",
 | 
			
		||||
                                '{"user_id":"%s"}' % user_id)
 | 
			
		||||
        (code, response) = yield self.mock_resource.trigger(
 | 
			
		||||
            "POST",
 | 
			
		||||
            "/register",
 | 
			
		||||
            json.dumps({
 | 
			
		||||
                "user": user_id,
 | 
			
		||||
                "password": "test",
 | 
			
		||||
                "type": "m.login.password"
 | 
			
		||||
            }))
 | 
			
		||||
        self.assertEquals(200, code)
 | 
			
		||||
        defer.returnValue(response)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -130,6 +130,10 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
 | 
			
		|||
            angular.element('#ringAudio')[0].pause();
 | 
			
		||||
            angular.element('#ringbackAudio')[0].pause();
 | 
			
		||||
            angular.element('#busyAudio')[0].play();
 | 
			
		||||
        } else if (newVal == 'ended' && oldVal == 'invite_sent' && $rootScope.currentCall.hangupParty == 'local' && $rootScope.currentCall.hangupReason == 'invite_timeout') {
 | 
			
		||||
            angular.element('#ringAudio')[0].pause();
 | 
			
		||||
            angular.element('#ringbackAudio')[0].pause();
 | 
			
		||||
            angular.element('#busyAudio')[0].play();
 | 
			
		||||
        } else if (oldVal == 'invite_sent') {
 | 
			
		||||
            angular.element('#ringbackAudio')[0].pause();
 | 
			
		||||
        } else if (oldVal == 'ringing') {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -538,6 +538,10 @@ a:active  { color: #000; }
 | 
			
		|||
    color: #F00;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.messageBing {
 | 
			
		||||
    color: #00F;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#room-fullscreen-image {
 | 
			
		||||
    position: absolute;
 | 
			
		||||
    top: 0px;
 | 
			
		||||
| 
						 | 
				
			
			@ -599,7 +603,7 @@ a:active  { color: #000; }
 | 
			
		|||
    width: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.recentsRoomSummaryTS {
 | 
			
		||||
.recentsRoomSummaryUsersCount, .recentsRoomSummaryTS {
 | 
			
		||||
    color: #888;
 | 
			
		||||
    font-size: 12px;
 | 
			
		||||
    width: 7em;
 | 
			
		||||
| 
						 | 
				
			
			@ -612,6 +616,11 @@ a:active  { color: #000; }
 | 
			
		|||
    padding-bottom: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Do not show users count in the recents fragment displayed on the room page */
 | 
			
		||||
#roomPage .recentsRoomSummaryUsersCount {
 | 
			
		||||
    width: 0em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*** Recents in the room page ***/
 | 
			
		||||
 | 
			
		||||
#roomRecentsTableWrapper {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -27,7 +27,8 @@ Typically, this service will store events or broadcast them to any listeners
 | 
			
		|||
if typically all the $on method would do is update its own $scope.
 | 
			
		||||
*/
 | 
			
		||||
angular.module('eventHandlerService', [])
 | 
			
		||||
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
 | 
			
		||||
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence', 
 | 
			
		||||
function(matrixService, $rootScope, $q, $timeout, mPresence) {
 | 
			
		||||
    var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
 | 
			
		||||
    var MSG_EVENT = "MSG_EVENT";
 | 
			
		||||
    var MEMBER_EVENT = "MEMBER_EVENT";
 | 
			
		||||
| 
						 | 
				
			
			@ -44,6 +45,44 @@ angular.module('eventHandlerService', [])
 | 
			
		|||
    var eventMap = {};
 | 
			
		||||
 | 
			
		||||
    $rootScope.presence = {};
 | 
			
		||||
    
 | 
			
		||||
    // TODO: This is attached to the rootScope so .html can just go containsBingWord
 | 
			
		||||
    // for determining classes so it is easy to highlight bing messages. It seems a
 | 
			
		||||
    // bit strange to put the impl in this service though, but I can't think of a better
 | 
			
		||||
    // file to put it in.
 | 
			
		||||
    $rootScope.containsBingWord = function(content) {
 | 
			
		||||
        if (!content || $.type(content) != "string") {
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        var bingWords = matrixService.config().bingWords;
 | 
			
		||||
        var shouldBing = false;
 | 
			
		||||
        
 | 
			
		||||
        // case-insensitive name check for user_id OR display_name if they exist
 | 
			
		||||
        var myUserId = matrixService.config().user_id;
 | 
			
		||||
        if (myUserId) {
 | 
			
		||||
            myUserId = myUserId.toLocaleLowerCase();
 | 
			
		||||
        }
 | 
			
		||||
        var myDisplayName = matrixService.config().display_name;
 | 
			
		||||
        if (myDisplayName) {
 | 
			
		||||
            myDisplayName = myDisplayName.toLocaleLowerCase();
 | 
			
		||||
        }
 | 
			
		||||
        if ( (myDisplayName && content.toLocaleLowerCase().indexOf(myDisplayName) != -1) ||
 | 
			
		||||
             (myUserId && content.toLocaleLowerCase().indexOf(myUserId) != -1) ) {
 | 
			
		||||
            shouldBing = true;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        // bing word list check
 | 
			
		||||
        if (bingWords && !shouldBing) {
 | 
			
		||||
            for (var i=0; i<bingWords.length; i++) {
 | 
			
		||||
                var re = RegExp(bingWords[i]);
 | 
			
		||||
                if (content.search(re) != -1) {
 | 
			
		||||
                    shouldBing = true;
 | 
			
		||||
                    break;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        return shouldBing;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    var initialSyncDeferred;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -63,13 +102,14 @@ angular.module('eventHandlerService', [])
 | 
			
		|||
    var initRoom = function(room_id) {
 | 
			
		||||
        if (!(room_id in $rootScope.events.rooms)) {
 | 
			
		||||
            console.log("Creating new handler entry for " + room_id);
 | 
			
		||||
            $rootScope.events.rooms[room_id] = {};
 | 
			
		||||
            $rootScope.events.rooms[room_id].messages = [];
 | 
			
		||||
            $rootScope.events.rooms[room_id].members = {};
 | 
			
		||||
 | 
			
		||||
            // Pagination information
 | 
			
		||||
            $rootScope.events.rooms[room_id].pagination = {
 | 
			
		||||
                earliest_token: "END"   // how far back we've paginated
 | 
			
		||||
            $rootScope.events.rooms[room_id] = {
 | 
			
		||||
                room_id: room_id,
 | 
			
		||||
                messages: [],
 | 
			
		||||
                members: {},
 | 
			
		||||
                // Pagination information
 | 
			
		||||
                pagination: {
 | 
			
		||||
                    earliest_token: "END"   // how far back we've paginated
 | 
			
		||||
                }
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			@ -136,6 +176,42 @@ angular.module('eventHandlerService', [])
 | 
			
		|||
            else {
 | 
			
		||||
                $rootScope.events.rooms[event.room_id].messages.push(event);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
            if (window.Notification && event.user_id != matrixService.config().user_id) {
 | 
			
		||||
                var shouldBing = $rootScope.containsBingWord(event.content.body);
 | 
			
		||||
            
 | 
			
		||||
                // TODO: Binging every message when idle doesn't make much sense. Can we use this more sensibly?
 | 
			
		||||
                // Unfortunately document.hidden = false on ubuntu chrome if chrome is minimised / does not have focus;
 | 
			
		||||
                // true when you swap tabs though. However, for the case where the chat screen is OPEN and there is
 | 
			
		||||
                // another window on top, we want to be notifying for those events. This DOES mean that there will be
 | 
			
		||||
                // notifications when currently viewing the chat screen though, but that is preferable to the alternative imo.
 | 
			
		||||
                var isIdle = (document.hidden || matrixService.presence.unavailable === mPresence.getState());
 | 
			
		||||
                
 | 
			
		||||
                // always bing if there are 0 bing words... apparently.
 | 
			
		||||
                var bingWords = matrixService.config().bingWords;
 | 
			
		||||
                if (bingWords && bingWords.length === 0) {
 | 
			
		||||
                    shouldBing = true;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                if (shouldBing) {
 | 
			
		||||
                    console.log("Displaying notification for "+JSON.stringify(event));
 | 
			
		||||
                    var member = $rootScope.events.rooms[event.room_id].members[event.user_id];
 | 
			
		||||
                    var displayname = undefined;
 | 
			
		||||
                    if (member) {
 | 
			
		||||
                        displayname = member.displayname;
 | 
			
		||||
                    }
 | 
			
		||||
                    var notification = new window.Notification(
 | 
			
		||||
                        (displayname || event.user_id) +
 | 
			
		||||
                        " (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
 | 
			
		||||
                    {
 | 
			
		||||
                        "body": event.content.body,
 | 
			
		||||
                        "icon": member ? member.avatar_url : undefined
 | 
			
		||||
                    });
 | 
			
		||||
                    $timeout(function() {
 | 
			
		||||
                        notification.close();
 | 
			
		||||
                    }, 5 * 1000);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        else {
 | 
			
		||||
            $rootScope.events.rooms[event.room_id].messages.unshift(event);
 | 
			
		||||
| 
						 | 
				
			
			@ -257,7 +333,9 @@ angular.module('eventHandlerService', [])
 | 
			
		|||
 | 
			
		||||
            // FIXME: /initialSync on a particular room is not yet available
 | 
			
		||||
            // So initRoom on a new room is not called. Make sure the room data is initialised here
 | 
			
		||||
            initRoom(event.room_id);
 | 
			
		||||
            if (event.room_id) {
 | 
			
		||||
                initRoom(event.room_id);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Avoid duplicated events
 | 
			
		||||
            // Needed for rooms where initialSync has not been done. 
 | 
			
		||||
| 
						 | 
				
			
			@ -326,13 +404,29 @@ angular.module('eventHandlerService', [])
 | 
			
		|||
        },
 | 
			
		||||
 | 
			
		||||
        // Handle messages from /initialSync or /messages
 | 
			
		||||
        handleRoomMessages: function(room_id, messages, isLiveEvents) {
 | 
			
		||||
        handleRoomMessages: function(room_id, messages, isLiveEvents, dir) {
 | 
			
		||||
            initRoom(room_id);
 | 
			
		||||
            this.handleEvents(messages.chunk, isLiveEvents);
 | 
			
		||||
 | 
			
		||||
            // Store how far back we've paginated
 | 
			
		||||
            // This assumes the paginations requests are contiguous and in reverse chronological order
 | 
			
		||||
            $rootScope.events.rooms[room_id].pagination.earliest_token = messages.end;
 | 
			
		||||
            var events = messages.chunk;
 | 
			
		||||
 | 
			
		||||
            // Handles messages according to their time order
 | 
			
		||||
            if (dir && 'b' === dir) {
 | 
			
		||||
                // paginateBackMessages requests messages to be in reverse chronological order
 | 
			
		||||
                for (var i=0; i<events.length; i++) {
 | 
			
		||||
                    this.handleEvent(events[i], isLiveEvents, isLiveEvents);
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Store how far back we've paginated
 | 
			
		||||
                $rootScope.events.rooms[room_id].pagination.earliest_token = messages.end;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                // InitialSync returns messages in chronological order
 | 
			
		||||
                for (var i=events.length - 1; i>=0; i--) {
 | 
			
		||||
                    this.handleEvent(events[i], isLiveEvents, isLiveEvents);
 | 
			
		||||
                }
 | 
			
		||||
                // Store where to start pagination
 | 
			
		||||
                $rootScope.events.rooms[room_id].pagination.earliest_token = messages.start;
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        handleInitialSyncDone: function(initialSyncData) {
 | 
			
		||||
| 
						 | 
				
			
			@ -347,6 +441,70 @@ angular.module('eventHandlerService', [])
 | 
			
		|||
 | 
			
		||||
        resetRoomMessages: function(room_id) {
 | 
			
		||||
            resetRoomMessages(room_id);
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        /**
 | 
			
		||||
         * Return the last message event of a room
 | 
			
		||||
         * @param {String} room_id the room id
 | 
			
		||||
         * @param {Boolean} filterFake true to not take into account fake messages
 | 
			
		||||
         * @returns {undefined | Event} the last message event if available
 | 
			
		||||
         */
 | 
			
		||||
        getLastMessage: function(room_id, filterEcho) {
 | 
			
		||||
            var lastMessage;
 | 
			
		||||
 | 
			
		||||
            var room = $rootScope.events.rooms[room_id];
 | 
			
		||||
            if (room) {
 | 
			
		||||
                for (var i = room.messages.length - 1; i >= 0; i--) {
 | 
			
		||||
                    var message = room.messages[i];
 | 
			
		||||
 | 
			
		||||
                    if (!filterEcho || undefined === message.echo_msg_state) {
 | 
			
		||||
                        lastMessage = message;
 | 
			
		||||
                        break;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return lastMessage;
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        /**
 | 
			
		||||
         * Compute the room users number, ie the number of members who has joined the room.
 | 
			
		||||
         * @param {String} room_id the room id
 | 
			
		||||
         * @returns {undefined | Number} the room users number if available
 | 
			
		||||
         */
 | 
			
		||||
        getUsersCountInRoom: function(room_id) {
 | 
			
		||||
            var memberCount;
 | 
			
		||||
 | 
			
		||||
            var room = $rootScope.events.rooms[room_id];
 | 
			
		||||
            if (room) {
 | 
			
		||||
                memberCount = 0;
 | 
			
		||||
 | 
			
		||||
                for (var i in room.members) {
 | 
			
		||||
                    var member = room.members[i];
 | 
			
		||||
 | 
			
		||||
                    if ("join" === member.membership) {
 | 
			
		||||
                        memberCount = memberCount + 1;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return memberCount;
 | 
			
		||||
        },
 | 
			
		||||
        
 | 
			
		||||
        /**
 | 
			
		||||
         * Get the member object of a room member
 | 
			
		||||
         * @param {String} room_id the room id
 | 
			
		||||
         * @param {String} user_id the id of the user
 | 
			
		||||
         * @returns {undefined | Object} the member object of this user in this room if he is part of the room
 | 
			
		||||
         */
 | 
			
		||||
        getMember: function(room_id, user_id) {
 | 
			
		||||
            var member;
 | 
			
		||||
            
 | 
			
		||||
            var room = $rootScope.events.rooms[room_id];
 | 
			
		||||
            if (room) {
 | 
			
		||||
                member = room.members[user_id];
 | 
			
		||||
            }
 | 
			
		||||
            return member;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
}]);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -104,8 +104,10 @@ angular.module('eventStreamService', [])
 | 
			
		|||
        settings.isActive = true;
 | 
			
		||||
        var deferred = $q.defer();
 | 
			
		||||
 | 
			
		||||
        // Initial sync: get all information and the last message of all rooms of the user
 | 
			
		||||
        matrixService.initialSync(1, false).then(
 | 
			
		||||
        // Initial sync: get all information and the last 30 messages of all rooms of the user
 | 
			
		||||
        // 30 messages should be enough to display a full page of messages in a room
 | 
			
		||||
        // without requiring to make an additional request
 | 
			
		||||
        matrixService.initialSync(30, false).then(
 | 
			
		||||
            function(response) {
 | 
			
		||||
                var rooms = response.data.rooms;
 | 
			
		||||
                for (var i = 0; i < rooms.length; ++i) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -53,6 +53,8 @@ angular.module('MatrixCall', [])
 | 
			
		|||
        this.candidateSendTries = 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    MatrixCall.CALL_TIMEOUT = 60000;
 | 
			
		||||
 | 
			
		||||
    MatrixCall.prototype.createPeerConnection = function() {
 | 
			
		||||
        var stunServer = 'stun:stun.l.google.com:19302';
 | 
			
		||||
        var pc;
 | 
			
		||||
| 
						 | 
				
			
			@ -78,12 +80,30 @@ angular.module('MatrixCall', [])
 | 
			
		|||
        this.config = config;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    MatrixCall.prototype.initWithInvite = function(msg) {
 | 
			
		||||
        this.msg = msg;
 | 
			
		||||
    MatrixCall.prototype.initWithInvite = function(event) {
 | 
			
		||||
        this.msg = event.content;
 | 
			
		||||
        this.peerConn = this.createPeerConnection();
 | 
			
		||||
        this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
 | 
			
		||||
        this.state = 'ringing';
 | 
			
		||||
        this.direction = 'inbound';
 | 
			
		||||
        var self = this;
 | 
			
		||||
        $timeout(function() {
 | 
			
		||||
            if (self.state == 'ringing') {
 | 
			
		||||
                self.state = 'ended';
 | 
			
		||||
                self.hangupParty = 'remote'; // effectively
 | 
			
		||||
                self.stopAllMedia();
 | 
			
		||||
                if (self.peerConn.signalingState != 'closed') self.peerConn.close();
 | 
			
		||||
                if (self.onHangup) self.onHangup(self);
 | 
			
		||||
            }
 | 
			
		||||
        }, this.msg.lifetime - event.age);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // perverse as it may seem, sometimes we want to instantiate a call with a hangup message
 | 
			
		||||
    // (because when getting the state of the room on load, events come in reverse order and
 | 
			
		||||
    // we want to remember that a call has been hung up)
 | 
			
		||||
    MatrixCall.prototype.initWithHangup = function(event) {
 | 
			
		||||
        this.msg = event.content;
 | 
			
		||||
        this.state = 'ended';
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    MatrixCall.prototype.answer = function() {
 | 
			
		||||
| 
						 | 
				
			
			@ -188,14 +208,12 @@ angular.module('MatrixCall', [])
 | 
			
		|||
            console.log("Ignoring remote ICE candidate because call has ended");
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        var candidateObject = new RTCIceCandidate({
 | 
			
		||||
            sdpMLineIndex: cand.label,
 | 
			
		||||
            candidate: cand.candidate
 | 
			
		||||
        });
 | 
			
		||||
        this.peerConn.addIceCandidate(candidateObject, function() {}, function(e) {});
 | 
			
		||||
        this.peerConn.addIceCandidate(new RTCIceCandidate(cand), function() {}, function(e) {});
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    MatrixCall.prototype.receivedAnswer = function(msg) {
 | 
			
		||||
        if (this.state == 'ended') return;
 | 
			
		||||
 | 
			
		||||
        this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
 | 
			
		||||
        this.state = 'connecting';
 | 
			
		||||
    };
 | 
			
		||||
| 
						 | 
				
			
			@ -213,11 +231,19 @@ angular.module('MatrixCall', [])
 | 
			
		|||
        var content = {
 | 
			
		||||
            version: 0,
 | 
			
		||||
            call_id: this.call_id,
 | 
			
		||||
            offer: description
 | 
			
		||||
            offer: description,
 | 
			
		||||
            lifetime: MatrixCall.CALL_TIMEOUT
 | 
			
		||||
        };
 | 
			
		||||
        this.sendEventWithRetry('m.call.invite', content);
 | 
			
		||||
 | 
			
		||||
        var self = this;
 | 
			
		||||
        $timeout(function() {
 | 
			
		||||
            if (self.state == 'invite_sent') {
 | 
			
		||||
                self.hangupReason = 'invite_timeout';
 | 
			
		||||
                self.hangup();
 | 
			
		||||
            }
 | 
			
		||||
        }, MatrixCall.CALL_TIMEOUT);
 | 
			
		||||
 | 
			
		||||
        $rootScope.$apply(function() {
 | 
			
		||||
            self.state = 'invite_sent';
 | 
			
		||||
        });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -24,22 +24,52 @@ angular.module('matrixPhoneService', [])
 | 
			
		|||
    matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT";
 | 
			
		||||
    matrixPhoneService.REPLACED_CALL_EVENT = "REPLACED_CALL_EVENT";
 | 
			
		||||
    matrixPhoneService.allCalls = {};
 | 
			
		||||
    // a place to save candidates that come in for calls we haven't got invites for yet (when paginating backwards)
 | 
			
		||||
    matrixPhoneService.candidatesByCall = {};
 | 
			
		||||
 | 
			
		||||
    matrixPhoneService.callPlaced = function(call) {
 | 
			
		||||
        matrixPhoneService.allCalls[call.call_id] = call;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
 | 
			
		||||
        if (!isLive) return; // until matrix supports expiring messages
 | 
			
		||||
        if (event.user_id == matrixService.config().user_id) return;
 | 
			
		||||
 | 
			
		||||
        var msg = event.content;
 | 
			
		||||
 | 
			
		||||
        if (event.type == 'm.call.invite') {
 | 
			
		||||
            if (event.age == undefined || msg.lifetime == undefined) {
 | 
			
		||||
                // if the event doesn't have either an age (the HS is too old) or a lifetime
 | 
			
		||||
                // (the sending client was too old when it sent it) then fall back to old behaviour
 | 
			
		||||
                if (!isLive) return; // until matrix supports expiring messages
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (event.age > msg.lifetime) {
 | 
			
		||||
                console.log("Ignoring expired call event of type "+event.type);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var call = undefined;
 | 
			
		||||
            if (!isLive) {
 | 
			
		||||
                // if this event wasn't live then this call may already be over
 | 
			
		||||
                call = matrixPhoneService.allCalls[msg.call_id];
 | 
			
		||||
                if (call && call.state == 'ended') {
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var MatrixCall = $injector.get('MatrixCall');
 | 
			
		||||
            var call = new MatrixCall(event.room_id);
 | 
			
		||||
            call.call_id = msg.call_id;
 | 
			
		||||
            call.initWithInvite(msg);
 | 
			
		||||
            call.initWithInvite(event);
 | 
			
		||||
            matrixPhoneService.allCalls[call.call_id] = call;
 | 
			
		||||
 | 
			
		||||
            // if we stashed candidate events for that call ID, play them back now
 | 
			
		||||
            if (!isLive && matrixPhoneService.candidatesByCall[call.call_id] != undefined) {
 | 
			
		||||
                for (var i = 0; i < matrixPhoneService.candidatesByCall[call.call_id].length; ++i) {
 | 
			
		||||
                    call.gotRemoteIceCandidate(matrixPhoneService.candidatesByCall[call.call_id][i]);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Were we trying to call that user (room)?
 | 
			
		||||
            var existingCall;
 | 
			
		||||
            var callIds = Object.keys(matrixPhoneService.allCalls);
 | 
			
		||||
| 
						 | 
				
			
			@ -79,21 +109,35 @@ angular.module('matrixPhoneService', [])
 | 
			
		|||
            call.receivedAnswer(msg);
 | 
			
		||||
        } else if (event.type == 'm.call.candidates') {
 | 
			
		||||
            var call = matrixPhoneService.allCalls[msg.call_id];
 | 
			
		||||
            if (!call) {
 | 
			
		||||
            if (!call && isLive) {
 | 
			
		||||
                console.log("Got candidates for unknown call ID "+msg.call_id);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            for (var i = 0; i < msg.candidates.length; ++i) {
 | 
			
		||||
                call.gotRemoteIceCandidate(msg.candidates[i]);
 | 
			
		||||
            } else if (!call) {
 | 
			
		||||
                if (matrixPhoneService.candidatesByCall[msg.call_id] == undefined) {
 | 
			
		||||
                    matrixPhoneService.candidatesByCall[msg.call_id] = [];
 | 
			
		||||
                }
 | 
			
		||||
                matrixPhoneService.candidatesByCall[msg.call_id] = matrixPhoneService.candidatesByCall[msg.call_id].concat(msg.candidates);
 | 
			
		||||
            } else {
 | 
			
		||||
                for (var i = 0; i < msg.candidates.length; ++i) {
 | 
			
		||||
                    call.gotRemoteIceCandidate(msg.candidates[i]);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        } else if (event.type == 'm.call.hangup') {
 | 
			
		||||
            var call = matrixPhoneService.allCalls[msg.call_id];
 | 
			
		||||
            if (!call) {
 | 
			
		||||
            if (!call && isLive) {
 | 
			
		||||
                console.log("Got hangup for unknown call ID "+msg.call_id);
 | 
			
		||||
                return;
 | 
			
		||||
            } else if (!call) {
 | 
			
		||||
                // if not live, store the fact that the call has ended because we're probably getting events backwards so
 | 
			
		||||
                // the hangup will come before the invite
 | 
			
		||||
                var MatrixCall = $injector.get('MatrixCall');
 | 
			
		||||
                var call = new MatrixCall(event.room_id);
 | 
			
		||||
                call.call_id = msg.call_id;
 | 
			
		||||
                call.initWithHangup(event);
 | 
			
		||||
                matrixPhoneService.allCalls[msg.call_id] = call;
 | 
			
		||||
            } else {
 | 
			
		||||
                call.onHangupReceived();
 | 
			
		||||
                delete(matrixPhoneService.allCalls[msg.call_id]);
 | 
			
		||||
            }
 | 
			
		||||
            call.onHangupReceived();
 | 
			
		||||
            delete(matrixPhoneService.allCalls[msg.call_id]);
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,38 +81,155 @@ angular.module('matrixService', [])
 | 
			
		|||
 | 
			
		||||
        return $http(request);
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    var doRegisterLogin = function(path, loginType, sessionId, userName, password, threepidCreds) {
 | 
			
		||||
        var data = {};
 | 
			
		||||
        if (loginType === "m.login.recaptcha") {
 | 
			
		||||
            var challengeToken = Recaptcha.get_challenge();
 | 
			
		||||
            var captchaEntry = Recaptcha.get_response();
 | 
			
		||||
            data = {
 | 
			
		||||
                type: "m.login.recaptcha",
 | 
			
		||||
                challenge: challengeToken,
 | 
			
		||||
                response: captchaEntry
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        else if (loginType === "m.login.email.identity") {
 | 
			
		||||
            data = {
 | 
			
		||||
                threepidCreds: threepidCreds
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        else if (loginType === "m.login.password") {
 | 
			
		||||
            data = {
 | 
			
		||||
                user: userName,
 | 
			
		||||
                password: password
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        if (sessionId) {
 | 
			
		||||
            data.session = sessionId;
 | 
			
		||||
        }
 | 
			
		||||
        data.type = loginType;
 | 
			
		||||
        console.log("doRegisterLogin >>> " + loginType);
 | 
			
		||||
        return doRequest("POST", path, undefined, data);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        /****** Home server API ******/
 | 
			
		||||
        prefix: prefixPath,
 | 
			
		||||
 | 
			
		||||
        // Register an user
 | 
			
		||||
        register: function(user_name, password, threepidCreds, useCaptcha) {         
 | 
			
		||||
            // The REST path spec
 | 
			
		||||
        register: function(user_name, password, threepidCreds, useCaptcha) {
 | 
			
		||||
            // registration is composed of multiple requests, to check you can
 | 
			
		||||
            // register, then to actually register. This deferred will fire when
 | 
			
		||||
            // all the requests are done, along with the final response.
 | 
			
		||||
            var deferred = $q.defer();
 | 
			
		||||
            var path = "/register";
 | 
			
		||||
            
 | 
			
		||||
            var data = {
 | 
			
		||||
                 user_id: user_name,
 | 
			
		||||
                 password: password,
 | 
			
		||||
                 threepidCreds: threepidCreds
 | 
			
		||||
            };
 | 
			
		||||
            // check we can actually register with this HS.
 | 
			
		||||
            doRequest("GET", path, undefined, undefined).then(
 | 
			
		||||
                function(response) {
 | 
			
		||||
                    console.log("/register [1] : "+JSON.stringify(response));
 | 
			
		||||
                    var flows = response.data.flows;
 | 
			
		||||
                    var knownTypes = [
 | 
			
		||||
                        "m.login.password",
 | 
			
		||||
                        "m.login.recaptcha",
 | 
			
		||||
                        "m.login.email.identity"
 | 
			
		||||
                    ];
 | 
			
		||||
                    // if they entered 3pid creds, we want to use a flow which uses it.
 | 
			
		||||
                    var useThreePidFlow = threepidCreds != undefined;
 | 
			
		||||
                    var flowIndex = 0;
 | 
			
		||||
                    var firstRegType = undefined;
 | 
			
		||||
                    
 | 
			
		||||
                    for (var i=0; i<flows.length; i++) {
 | 
			
		||||
                        var isThreePidFlow = false;
 | 
			
		||||
                        if (flows[i].stages) {
 | 
			
		||||
                            for (var j=0; j<flows[i].stages.length; j++) {
 | 
			
		||||
                                var regType = flows[i].stages[j];
 | 
			
		||||
                                if (knownTypes.indexOf(regType) === -1) {
 | 
			
		||||
                                    deferred.reject("Unknown type: "+regType);
 | 
			
		||||
                                    return;
 | 
			
		||||
                                }
 | 
			
		||||
                                if (regType == "m.login.email.identity") {
 | 
			
		||||
                                    isThreePidFlow = true;
 | 
			
		||||
                                }
 | 
			
		||||
                                if (!useCaptcha && regType == "m.login.recaptcha") {
 | 
			
		||||
                                    console.error("Web client setup to not use captcha, but HS demands a captcha.");
 | 
			
		||||
                                    deferred.reject({
 | 
			
		||||
                                        data: {
 | 
			
		||||
                                            errcode: "M_CAPTCHA_NEEDED",
 | 
			
		||||
                                            error: "Home server requires a captcha."
 | 
			
		||||
                                        }
 | 
			
		||||
                                    });
 | 
			
		||||
                                    return;
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                        if ( (isThreePidFlow && useThreePidFlow) || (!isThreePidFlow && !useThreePidFlow) ) {
 | 
			
		||||
                            flowIndex = i;
 | 
			
		||||
                        }
 | 
			
		||||
                        
 | 
			
		||||
                        if (knownTypes.indexOf(flows[i].type) == -1) {
 | 
			
		||||
                            deferred.reject("Unknown type: "+flows[i].type);
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    // looks like we can register fine, go ahead and do it.
 | 
			
		||||
                    console.log("Using flow " + JSON.stringify(flows[flowIndex]));
 | 
			
		||||
                    firstRegType = flows[flowIndex].type;
 | 
			
		||||
                    var sessionId = undefined;
 | 
			
		||||
                    
 | 
			
		||||
                    // generic response processor so it can loop as many times as required
 | 
			
		||||
                    var loginResponseFunc = function(response) {
 | 
			
		||||
                        if (response.data.session) {
 | 
			
		||||
                            sessionId = response.data.session;
 | 
			
		||||
                        }
 | 
			
		||||
                        console.log("login response: " + JSON.stringify(response.data));
 | 
			
		||||
                        if (response.data.access_token) {
 | 
			
		||||
                            deferred.resolve(response);
 | 
			
		||||
                        }
 | 
			
		||||
                        else if (response.data.next) {
 | 
			
		||||
                            var nextType = response.data.next;
 | 
			
		||||
                            if (response.data.next instanceof Array) {
 | 
			
		||||
                                for (var i=0; i<response.data.next.length; i++) {
 | 
			
		||||
                                    if (useThreePidFlow && response.data.next[i] == "m.login.email.identity") {
 | 
			
		||||
                                        nextType = response.data.next[i];
 | 
			
		||||
                                        break;
 | 
			
		||||
                                    }
 | 
			
		||||
                                    else if (!useThreePidFlow && response.data.next[i] != "m.login.email.identity") {
 | 
			
		||||
                                        nextType = response.data.next[i];
 | 
			
		||||
                                        break;
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            }
 | 
			
		||||
                            return doRegisterLogin(path, nextType, sessionId, user_name, password, threepidCreds).then(
 | 
			
		||||
                                loginResponseFunc,
 | 
			
		||||
                                function(err) {
 | 
			
		||||
                                    deferred.reject(err);
 | 
			
		||||
                                }
 | 
			
		||||
                            );
 | 
			
		||||
                        }
 | 
			
		||||
                        else {
 | 
			
		||||
                            deferred.reject("Unknown continuation: "+JSON.stringify(response));
 | 
			
		||||
                        }
 | 
			
		||||
                    };
 | 
			
		||||
                    
 | 
			
		||||
                    // set the ball rolling
 | 
			
		||||
                    doRegisterLogin(path, firstRegType, undefined, user_name, password, threepidCreds).then(
 | 
			
		||||
                        loginResponseFunc,
 | 
			
		||||
                        function(err) {
 | 
			
		||||
                            deferred.reject(err);
 | 
			
		||||
                        }
 | 
			
		||||
                    );
 | 
			
		||||
                    
 | 
			
		||||
                },
 | 
			
		||||
                function(err) {
 | 
			
		||||
                    deferred.reject(err);
 | 
			
		||||
                }
 | 
			
		||||
            );
 | 
			
		||||
            
 | 
			
		||||
            if (useCaptcha) {
 | 
			
		||||
                // Not all home servers will require captcha on signup, but if this flag is checked,
 | 
			
		||||
                // send captcha information.
 | 
			
		||||
                // TODO: Might be nice to make this a bit more flexible..
 | 
			
		||||
                var challengeToken = Recaptcha.get_challenge();
 | 
			
		||||
                var captchaEntry = Recaptcha.get_response();
 | 
			
		||||
                var captchaType = "m.login.recaptcha";
 | 
			
		||||
                
 | 
			
		||||
                data.captcha = {
 | 
			
		||||
                    type: captchaType,
 | 
			
		||||
                    challenge: challengeToken,
 | 
			
		||||
                    response: captchaEntry
 | 
			
		||||
                };
 | 
			
		||||
            }   
 | 
			
		||||
 | 
			
		||||
            return doRequest("POST", path, undefined, data);
 | 
			
		||||
            return deferred.promise;
 | 
			
		||||
        },
 | 
			
		||||
 | 
			
		||||
        // Create a room
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -117,6 +117,10 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
 | 
			
		|||
        matrixService.getDisplayName($scope.config.user_id).then(
 | 
			
		||||
            function(response) {
 | 
			
		||||
                $scope.profile.displayName = response.data.displayname;
 | 
			
		||||
                var config = matrixService.config();
 | 
			
		||||
                config.display_name = response.data.displayname;
 | 
			
		||||
                matrixService.setConfig(config);
 | 
			
		||||
                matrixService.saveConfig();
 | 
			
		||||
            },
 | 
			
		||||
            function(error) {
 | 
			
		||||
                $scope.feedback = "Can't load display name";
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -62,7 +62,8 @@
 | 
			
		|||
                        <span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
 | 
			
		||||
                        <span ng-show="currentCall.state == 'connected'">Call Connected</span>
 | 
			
		||||
                        <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'remote'">Call Rejected</span>
 | 
			
		||||
                        <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">Call Canceled</span>
 | 
			
		||||
                        <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local' && currentCall.hangupReason == undefined">Call Canceled</span>
 | 
			
		||||
                        <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local' && currentCall.hangupReason == 'invite_timeout'">User Not Responding</span>
 | 
			
		||||
                        <span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'outbound'">Call Ended</span>
 | 
			
		||||
                        <span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'inbound'">Call Canceled</span>
 | 
			
		||||
                        <span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'inbound'">Call Ended</span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,134 +16,16 @@
 | 
			
		|||
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
angular.module('RecentsController', ['matrixService', 'matrixFilter', 'eventHandlerService'])
 | 
			
		||||
.controller('RecentsController', ['$rootScope', '$scope', 'matrixService', 'eventHandlerService', 
 | 
			
		||||
                               function($rootScope, $scope, matrixService, eventHandlerService) {
 | 
			
		||||
                                   
 | 
			
		||||
    // FIXME: Angularjs reloads the controller (and resets its $scope) each time
 | 
			
		||||
    // the page URL changes, use $rootScope to avoid to have to reload data
 | 
			
		||||
    $rootScope.rooms;
 | 
			
		||||
angular.module('RecentsController', ['matrixService', 'matrixFilter'])
 | 
			
		||||
.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService', 
 | 
			
		||||
                               function($rootScope, $scope, eventHandlerService) {
 | 
			
		||||
 | 
			
		||||
    // Expose the service to the view
 | 
			
		||||
    $scope.eventHandlerService = eventHandlerService;
 | 
			
		||||
 | 
			
		||||
    // $rootScope of the parent where the recents component is included can override this value
 | 
			
		||||
    // in order to highlight a specific room in the list
 | 
			
		||||
    $rootScope.recentsSelectedRoomID;
 | 
			
		||||
    
 | 
			
		||||
    var listenToEventStream = function() {
 | 
			
		||||
        // Refresh the list on matrix invitation and message event
 | 
			
		||||
        $rootScope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
 | 
			
		||||
            if (isLive) {
 | 
			
		||||
                if (!$rootScope.rooms[event.room_id]) {
 | 
			
		||||
                    // The user has joined a new room, which we do not have data yet. The reason is that
 | 
			
		||||
                    // the room has appeared in the scope of the user rooms after the global initialSync
 | 
			
		||||
                    // FIXME: an initialSync on this specific room should be done
 | 
			
		||||
                    $rootScope.rooms[event.room_id] = {
 | 
			
		||||
                        room_id:event.room_id
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
                else if (event.state_key === matrixService.config().user_id && "invite" !== event.membership && "join" !== event.membership) {
 | 
			
		||||
                    // The user has been kicked or banned from the room, remove this room from the recents
 | 
			
		||||
                    delete $rootScope.rooms[event.room_id];
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                if ($rootScope.rooms[event.room_id]) {
 | 
			
		||||
                    $rootScope.rooms[event.room_id].lastMsg = event;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                // Update room users count
 | 
			
		||||
                $rootScope.rooms[event.room_id].numUsersInRoom = getUsersCountInRoom(event.room_id);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        $rootScope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
 | 
			
		||||
            if (isLive) {
 | 
			
		||||
                $rootScope.rooms[event.room_id].lastMsg = event;              
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        $rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
 | 
			
		||||
            if (isLive) {
 | 
			
		||||
                $rootScope.rooms[event.room_id].lastMsg = event;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        $rootScope.$on(eventHandlerService.ROOM_CREATE_EVENT, function(ngEvent, event, isLive) {
 | 
			
		||||
            if (isLive) {
 | 
			
		||||
                $rootScope.rooms[event.room_id] = event;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        $rootScope.$on(eventHandlerService.NAME_EVENT, function(ngEvent, event, isLive) {
 | 
			
		||||
            if (isLive) {
 | 
			
		||||
                $rootScope.rooms[event.room_id].lastMsg = event;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        $rootScope.$on(eventHandlerService.TOPIC_EVENT, function(ngEvent, event, isLive) {
 | 
			
		||||
            if (isLive) {
 | 
			
		||||
                $rootScope.rooms[event.room_id].lastMsg = event;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    /**
 | 
			
		||||
     * Compute the room users number, ie the number of members who has joined the room.
 | 
			
		||||
     * @param {String} room_id the room id
 | 
			
		||||
     * @returns {undefined | Number} the room users number if available
 | 
			
		||||
     */
 | 
			
		||||
    var getUsersCountInRoom = function(room_id) {
 | 
			
		||||
        var memberCount;
 | 
			
		||||
        
 | 
			
		||||
        var room = $rootScope.events.rooms[room_id];
 | 
			
		||||
        if (room) {
 | 
			
		||||
            memberCount = 0;
 | 
			
		||||
            
 | 
			
		||||
            for (var i in room.members) {
 | 
			
		||||
                var member = room.members[i];
 | 
			
		||||
                
 | 
			
		||||
                if ("join" === member.membership) {
 | 
			
		||||
                    memberCount = memberCount + 1;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return memberCount;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    $scope.onInit = function() {
 | 
			
		||||
        // Init recents list only once
 | 
			
		||||
        if ($rootScope.rooms) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        $rootScope.rooms = {};
 | 
			
		||||
        
 | 
			
		||||
        // Use initialSync data to init the recents list
 | 
			
		||||
        eventHandlerService.waitForInitialSyncCompletion().then(
 | 
			
		||||
            function(initialSyncData) {
 | 
			
		||||
            
 | 
			
		||||
                var rooms = initialSyncData.data.rooms;
 | 
			
		||||
                for (var i=0; i<rooms.length; i++) {
 | 
			
		||||
                    var room = rooms[i];
 | 
			
		||||
                    
 | 
			
		||||
                    // Add room_alias & room_display_name members
 | 
			
		||||
                    $rootScope.rooms[room.room_id] = angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
 | 
			
		||||
 | 
			
		||||
                    // Create a shortcut for the last message of this room
 | 
			
		||||
                    if (room.messages && room.messages.chunk && room.messages.chunk[0]) {
 | 
			
		||||
                        $rootScope.rooms[room.room_id].lastMsg = room.messages.chunk[0];
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
                    $rootScope.rooms[room.room_id].numUsersInRoom = getUsersCountInRoom(room.room_id);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                // From now, update recents from the stream
 | 
			
		||||
                listenToEventStream();
 | 
			
		||||
            },
 | 
			
		||||
            function(error) {
 | 
			
		||||
                $rootScope.feedback = "Failure: " + error.data;
 | 
			
		||||
            }
 | 
			
		||||
        );
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // Clean data when user logs out
 | 
			
		||||
    $scope.$on(eventHandlerService.RESET_EVENT, function() {
 | 
			
		||||
 | 
			
		||||
        delete $rootScope.rooms;
 | 
			
		||||
    });
 | 
			
		||||
}]);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -17,31 +17,48 @@
 | 
			
		|||
'use strict';
 | 
			
		||||
 | 
			
		||||
angular.module('RecentsController')
 | 
			
		||||
.filter('orderRecents', function() {
 | 
			
		||||
.filter('orderRecents', ["matrixService", "eventHandlerService", function(matrixService, eventHandlerService) {
 | 
			
		||||
    return function(rooms) {
 | 
			
		||||
 | 
			
		||||
        var user_id = matrixService.config().user_id;
 | 
			
		||||
 | 
			
		||||
        // Transform the dict into an array
 | 
			
		||||
        // The key, room_id, is already in value objects
 | 
			
		||||
        var filtered = [];
 | 
			
		||||
        angular.forEach(rooms, function(value, key) {
 | 
			
		||||
            filtered.push( value );
 | 
			
		||||
        angular.forEach(rooms, function(room, room_id) {
 | 
			
		||||
 | 
			
		||||
            // Show the room only if the user has joined it or has been invited
 | 
			
		||||
            // (ie, do not show it if he has been banned)
 | 
			
		||||
            var member = eventHandlerService.getMember(room_id, user_id);
 | 
			
		||||
            if (member && ("invite" === member.membership || "join" === member.membership)) {
 | 
			
		||||
            
 | 
			
		||||
                // Count users here
 | 
			
		||||
                // TODO: Compute it directly in eventHandlerService
 | 
			
		||||
                room.numUsersInRoom = eventHandlerService.getUsersCountInRoom(room_id);
 | 
			
		||||
 | 
			
		||||
                filtered.push(room);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // And time sort them
 | 
			
		||||
        // The room with the lastest message at first
 | 
			
		||||
        filtered.sort(function (a, b) {
 | 
			
		||||
        filtered.sort(function (roomA, roomB) {
 | 
			
		||||
 | 
			
		||||
            var lastMsgRoomA = eventHandlerService.getLastMessage(roomA.room_id, true);
 | 
			
		||||
            var lastMsgRoomB = eventHandlerService.getLastMessage(roomB.room_id, true);
 | 
			
		||||
 | 
			
		||||
            // Invite message does not have a body message nor ts
 | 
			
		||||
            // Puth them at the top of the list
 | 
			
		||||
            if (undefined === a.lastMsg) {
 | 
			
		||||
            if (undefined === lastMsgRoomA) {
 | 
			
		||||
                return -1;
 | 
			
		||||
            }
 | 
			
		||||
            else if (undefined === b.lastMsg) {
 | 
			
		||||
            else if (undefined === lastMsgRoomB) {
 | 
			
		||||
                return 1;
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                return b.lastMsg.ts - a.lastMsg.ts;
 | 
			
		||||
                return lastMsgRoomB.ts - lastMsgRoomA.ts;
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
        return filtered;
 | 
			
		||||
    };
 | 
			
		||||
});
 | 
			
		||||
}]);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,20 +1,24 @@
 | 
			
		|||
<div ng-controller="RecentsController" data-ng-init="onInit()">
 | 
			
		||||
<div ng-controller="RecentsController">
 | 
			
		||||
    <table class="recentsTable">
 | 
			
		||||
        <tbody ng-repeat="(rm_id, room) in rooms | orderRecents" 
 | 
			
		||||
        <tbody ng-repeat="(index, room) in events.rooms | orderRecents" 
 | 
			
		||||
               ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )" 
 | 
			
		||||
               class ="recentsRoom" 
 | 
			
		||||
               ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
 | 
			
		||||
               ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">                                           
 | 
			
		||||
            <tr>
 | 
			
		||||
                <td class="recentsRoomName">
 | 
			
		||||
                    {{ room.room_id | mRoomName }}
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="recentsRoomSummaryTS">
 | 
			
		||||
                <td class="recentsRoomSummaryUsersCount">
 | 
			
		||||
                    <span ng-show="undefined !== room.numUsersInRoom">
 | 
			
		||||
                        {{ room.numUsersInRoom || '1' }} {{ room.numUsersInRoom == 1 ? 'user' : 'users' }}                     
 | 
			
		||||
                    </span>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td class="recentsRoomSummaryTS">
 | 
			
		||||
                    {{ (room.lastMsg.ts) | date:'MMM d HH:mm' }}
 | 
			
		||||
                    <!-- Use a temp var as alias to the last room message.
 | 
			
		||||
                         Declaring it in this way ensures the data-binding -->
 | 
			
		||||
                    {{ lastMsg = eventHandlerService.getLastMessage(room.room_id, true);"" }}
 | 
			
		||||
 | 
			
		||||
                    {{ (lastMsg.ts) | date:'MMM d HH:mm' }}
 | 
			
		||||
                </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -25,67 +29,67 @@
 | 
			
		|||
                        {{ room.inviter | mUserDisplayName: room.room_id }} invited you
 | 
			
		||||
                    </div>
 | 
			
		||||
                    
 | 
			
		||||
                    <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type">
 | 
			
		||||
                    <div ng-hide="room.membership === 'invite'" ng-switch="lastMsg.type">
 | 
			
		||||
                        <div ng-switch-when="m.room.member">
 | 
			
		||||
                            <span ng-if="'join' === room.lastMsg.content.membership">
 | 
			
		||||
                                {{ room.lastMsg.state_key | mUserDisplayName: room.room_id}} joined
 | 
			
		||||
                            <span ng-if="'join' === lastMsg.content.membership">
 | 
			
		||||
                                {{ lastMsg.state_key | mUserDisplayName: room.room_id}} joined
 | 
			
		||||
                            </span>
 | 
			
		||||
                            <span ng-if="'leave' === room.lastMsg.content.membership">
 | 
			
		||||
                                <span ng-if="room.lastMsg.user_id === room.lastMsg.state_key">
 | 
			
		||||
                                    {{room.lastMsg.state_key | mUserDisplayName: room.room_id }} left
 | 
			
		||||
                            <span ng-if="'leave' === lastMsg.content.membership">
 | 
			
		||||
                                <span ng-if="lastMsg.user_id === lastMsg.state_key">
 | 
			
		||||
                                    {{lastMsg.state_key | mUserDisplayName: room.room_id }} left
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <span ng-if="room.lastMsg.user_id !== room.lastMsg.state_key">
 | 
			
		||||
                                    {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }}
 | 
			
		||||
                                    {{ {"join": "kicked", "ban": "unbanned"}[room.lastMsg.content.prev] }}
 | 
			
		||||
                                    {{ room.lastMsg.state_key | mUserDisplayName: room.room_id }}
 | 
			
		||||
                                <span ng-if="lastMsg.user_id !== lastMsg.state_key">
 | 
			
		||||
                                    {{ lastMsg.user_id | mUserDisplayName: room.room_id }}
 | 
			
		||||
                                    {{ {"join": "kicked", "ban": "unbanned"}[lastMsg.content.prev] }}
 | 
			
		||||
                                    {{ lastMsg.state_key | mUserDisplayName: room.room_id }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                                <span ng-if="'join' === room.lastMsg.content.prev && room.lastMsg.content.reason">
 | 
			
		||||
                                    : {{ room.lastMsg.content.reason }}
 | 
			
		||||
                                <span ng-if="'join' === lastMsg.content.prev && lastMsg.content.reason">
 | 
			
		||||
                                    : {{ lastMsg.content.reason }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </span>
 | 
			
		||||
                            <span ng-if="'invite' === room.lastMsg.content.membership || 'ban' === room.lastMsg.content.membership">
 | 
			
		||||
                                {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }}
 | 
			
		||||
                                {{ {"invite": "invited", "ban": "banned"}[room.lastMsg.content.membership] }}
 | 
			
		||||
                                {{ room.lastMsg.state_key | mUserDisplayName: room.room_id }}
 | 
			
		||||
                                <span ng-if="'ban' === room.lastMsg.content.prev && room.lastMsg.content.reason">
 | 
			
		||||
                                    : {{ room.lastMsg.content.reason }}
 | 
			
		||||
                            <span ng-if="'invite' === lastMsg.content.membership || 'ban' === lastMsg.content.membership">
 | 
			
		||||
                                {{ lastMsg.user_id | mUserDisplayName: room.room_id }}
 | 
			
		||||
                                {{ {"invite": "invited", "ban": "banned"}[lastMsg.content.membership] }}
 | 
			
		||||
                                {{ lastMsg.state_key | mUserDisplayName: room.room_id }}
 | 
			
		||||
                                <span ng-if="'ban' === lastMsg.content.prev && lastMsg.content.reason">
 | 
			
		||||
                                    : {{ lastMsg.content.reason }}
 | 
			
		||||
                                </span>
 | 
			
		||||
                            </span>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div ng-switch-when="m.room.message">
 | 
			
		||||
                            <div ng-switch="room.lastMsg.content.msgtype">
 | 
			
		||||
                            <div ng-switch="lastMsg.content.msgtype">
 | 
			
		||||
                                <div ng-switch-when="m.text">
 | 
			
		||||
                                    {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} :
 | 
			
		||||
                                    <span ng-bind-html="(room.lastMsg.content.body) | linky:'_blank'">
 | 
			
		||||
                                    {{ lastMsg.user_id | mUserDisplayName: room.room_id }} :
 | 
			
		||||
                                    <span ng-bind-html="(lastMsg.content.body) | linky:'_blank'">
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <div ng-switch-when="m.image">
 | 
			
		||||
                                    {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} sent an image
 | 
			
		||||
                                    {{ lastMsg.user_id | mUserDisplayName: room.room_id }} sent an image
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <div ng-switch-when="m.emote">
 | 
			
		||||
                                    <span ng-bind-html="'* ' + (room.lastMsg.user_id | mUserDisplayName: room.room_id) + ' ' + room.lastMsg.content.body | linky:'_blank'">
 | 
			
		||||
                                    <span ng-bind-html="'* ' + (lastMsg.user_id | mUserDisplayName: room.room_id) + ' ' + lastMsg.content.body | linky:'_blank'">
 | 
			
		||||
                                    </span>
 | 
			
		||||
                                </div>
 | 
			
		||||
 | 
			
		||||
                                <div ng-switch-default>
 | 
			
		||||
                                    {{ room.lastMsg.content }}
 | 
			
		||||
                                    {{ lastMsg.content }}
 | 
			
		||||
                                </div>
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div ng-switch-when="m.room.topic">
 | 
			
		||||
                            {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} changed the topic to: {{ room.lastMsg.content.topic }}
 | 
			
		||||
                            {{ lastMsg.user_id | mUserDisplayName: room.room_id }} changed the topic to: {{ lastMsg.content.topic }}
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div ng-switch-when="m.room.name">
 | 
			
		||||
                            {{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} changed the room name to: {{ room.lastMsg.content.name }}
 | 
			
		||||
                            {{ lastMsg.user_id | mUserDisplayName: room.room_id }} changed the room name to: {{ lastMsg.content.name }}
 | 
			
		||||
                        </div>
 | 
			
		||||
 | 
			
		||||
                        <div ng-switch-default>
 | 
			
		||||
                            <div ng-if="room.lastMsg.type.indexOf('m.call.') === 0">
 | 
			
		||||
                            <div ng-if="lastMsg.type.indexOf('m.call.') === 0">
 | 
			
		||||
                                Call
 | 
			
		||||
                            </div>
 | 
			
		||||
                        </div>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,8 +15,8 @@ limitations under the License.
 | 
			
		|||
*/
 | 
			
		||||
 | 
			
		||||
angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
 | 
			
		||||
.controller('RoomController', ['$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mPresence', 'matrixPhoneService', 'MatrixCall',
 | 
			
		||||
                               function($filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, mPresence, matrixPhoneService, MatrixCall) {
 | 
			
		||||
.controller('RoomController', ['$filter', '$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'matrixPhoneService', 'MatrixCall',
 | 
			
		||||
                               function($filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall) {
 | 
			
		||||
   'use strict';
 | 
			
		||||
    var MESSAGES_PER_PAGINATION = 30;
 | 
			
		||||
    var THUMBNAIL_SIZE = 320;
 | 
			
		||||
| 
						 | 
				
			
			@ -139,27 +139,11 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
 | 
			
		|||
        if (isLive && event.room_id === $scope.room_id) {
 | 
			
		||||
            
 | 
			
		||||
            scrollToBottom();
 | 
			
		||||
 | 
			
		||||
            if (window.Notification) {
 | 
			
		||||
                // Show notification when the window is hidden, or the user is idle
 | 
			
		||||
                if (document.hidden || matrixService.presence.unavailable === mPresence.getState()) {
 | 
			
		||||
                    var notification = new window.Notification(
 | 
			
		||||
                        ($scope.members[event.user_id].displayname || event.user_id) +
 | 
			
		||||
                        " (" + ($scope.room_alias || $scope.room_id) + ")", // FIXME: don't leak room_ids here
 | 
			
		||||
                    {
 | 
			
		||||
                        "body": event.content.body,
 | 
			
		||||
                        "icon": $scope.members[event.user_id].avatar_url
 | 
			
		||||
                    });
 | 
			
		||||
                    $timeout(function() {
 | 
			
		||||
                        notification.close();
 | 
			
		||||
                    }, 5 * 1000);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
    
 | 
			
		||||
    $scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
 | 
			
		||||
        if (isLive) {
 | 
			
		||||
        if (isLive && event.room_id === $scope.room_id) {
 | 
			
		||||
            if ($scope.state.waiting_for_joined_event) {
 | 
			
		||||
                // The user has successfully joined the room, we can getting data for this room
 | 
			
		||||
                $scope.state.waiting_for_joined_event = false;
 | 
			
		||||
| 
						 | 
				
			
			@ -177,19 +161,33 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
 | 
			
		|||
                else {
 | 
			
		||||
                    user = event.user_id;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                 
 | 
			
		||||
                if ("ban" === event.membership) {
 | 
			
		||||
                    $scope.state.permission_denied = "You have been banned by " + user;
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    $scope.state.permission_denied = "You have been kicked by " + user;
 | 
			
		||||
                }
 | 
			
		||||
                
 | 
			
		||||
                }  
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                scrollToBottom();
 | 
			
		||||
                updateMemberList(event); 
 | 
			
		||||
 | 
			
		||||
                // Notify when a user joins
 | 
			
		||||
                if ((document.hidden  || matrixService.presence.unavailable === mPresence.getState())
 | 
			
		||||
                        && event.state_key !== $scope.state.user_id  && "join" === event.membership) {
 | 
			
		||||
                    debugger;
 | 
			
		||||
                    var notification = new window.Notification(
 | 
			
		||||
                        event.content.displayname +
 | 
			
		||||
                        " (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
 | 
			
		||||
                    {
 | 
			
		||||
                        "body": event.content.displayname + " joined",
 | 
			
		||||
                        "icon": event.content.avatar_url ? event.content.avatar_url : undefined
 | 
			
		||||
                    });
 | 
			
		||||
                    $timeout(function() {
 | 
			
		||||
                        notification.close();
 | 
			
		||||
                    }, 5 * 1000);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -235,7 +233,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
 | 
			
		|||
        matrixService.paginateBackMessages($scope.room_id, $rootScope.events.rooms[$scope.room_id].pagination.earliest_token, numItems).then(
 | 
			
		||||
            function(response) {
 | 
			
		||||
 | 
			
		||||
                eventHandlerService.handleRoomMessages($scope.room_id, response.data, false);
 | 
			
		||||
                eventHandlerService.handleRoomMessages($scope.room_id, response.data, false, 'b');
 | 
			
		||||
                if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
 | 
			
		||||
                    // no more messages to paginate. this currently never gets turned true again, as we never
 | 
			
		||||
                    // expire paginated contents in the current implementation.
 | 
			
		||||
| 
						 | 
				
			
			@ -676,6 +674,10 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
 | 
			
		|||
    var onInit2 = function() {
 | 
			
		||||
        console.log("onInit2");
 | 
			
		||||
        
 | 
			
		||||
        // Scroll down as soon as possible so that we point to the last message
 | 
			
		||||
        // if it already exists in memory
 | 
			
		||||
        scrollToBottom(true);
 | 
			
		||||
 | 
			
		||||
        // Make sure the initialSync has been before going further
 | 
			
		||||
        eventHandlerService.waitForInitialSyncCompletion().then(
 | 
			
		||||
            function() {
 | 
			
		||||
| 
						 | 
				
			
			@ -684,6 +686,10 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
 | 
			
		|||
                
 | 
			
		||||
                // The room members is available in the data fetched by initialSync
 | 
			
		||||
                if ($rootScope.events.rooms[$scope.room_id]) {
 | 
			
		||||
 | 
			
		||||
                    // There is no need to do a 1st pagination (initialSync provided enough to fill a page)
 | 
			
		||||
                    $scope.state.first_pagination = false;
 | 
			
		||||
 | 
			
		||||
                    var members = $rootScope.events.rooms[$scope.room_id].members;
 | 
			
		||||
 | 
			
		||||
                    // Update the member list
 | 
			
		||||
| 
						 | 
				
			
			@ -743,9 +749,18 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
 | 
			
		|||
                // Arm list timing update timer
 | 
			
		||||
                updateMemberListPresenceAge();
 | 
			
		||||
 | 
			
		||||
                // Start pagination
 | 
			
		||||
                // Allow pagination
 | 
			
		||||
                $scope.state.can_paginate = true;
 | 
			
		||||
                paginate(MESSAGES_PER_PAGINATION);
 | 
			
		||||
 | 
			
		||||
                // Do a first pagination only if it is required
 | 
			
		||||
                // FIXME: Should be no more require when initialSync/{room_id} will be available
 | 
			
		||||
                if ($scope.state.first_pagination) {
 | 
			
		||||
                    paginate(MESSAGES_PER_PAGINATION);
 | 
			
		||||
                }
 | 
			
		||||
                else {
 | 
			
		||||
                    // There are already messages, go to the last message
 | 
			
		||||
                    scrollToBottom(true);
 | 
			
		||||
                }
 | 
			
		||||
            },
 | 
			
		||||
            function(error) {
 | 
			
		||||
                $scope.feedback = "Failed get member list: " + error.data.error;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -105,7 +105,7 @@
 | 
			
		|||
                        
 | 
			
		||||
                        <span ng-show='msg.content.msgtype === "m.text"' 
 | 
			
		||||
                              class="message"
 | 
			
		||||
                              ng-class="msg.echo_msg_state"
 | 
			
		||||
                              ng-class="containsBingWord(msg.content.body) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
 | 
			
		||||
                              ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
 | 
			
		||||
 | 
			
		||||
                        <span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id'>Outgoing Call</span>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -194,7 +194,16 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
 | 
			
		|||
    
 | 
			
		||||
    /*** Desktop notifications section ***/
 | 
			
		||||
    $scope.settings = {
 | 
			
		||||
        notifications: undefined
 | 
			
		||||
        notifications: undefined,
 | 
			
		||||
        bingWords: matrixService.config().bingWords
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    $scope.saveBingWords = function() {
 | 
			
		||||
        console.log("Saving words: "+JSON.stringify($scope.settings.bingWords));
 | 
			
		||||
        var config = matrixService.config();
 | 
			
		||||
        config.bingWords = $scope.settings.bingWords;
 | 
			
		||||
        matrixService.setConfig(config);
 | 
			
		||||
        matrixService.saveConfig();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    // If the browser supports it, check the desktop notification state
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -51,7 +51,16 @@
 | 
			
		|||
        <h3>Desktop notifications</h3>
 | 
			
		||||
        <div class="section" ng-switch="settings.notifications">
 | 
			
		||||
            <div ng-switch-when="granted">
 | 
			
		||||
                Notifications are enabled.
 | 
			
		||||
                Notifications are enabled. You will be alerted when a message contains your user ID or display name.
 | 
			
		||||
                <div class="section">
 | 
			
		||||
                    <h4>Additional words to alert on:</h4>
 | 
			
		||||
                    <p>Leave blank to alert on all messages.</p>
 | 
			
		||||
                    <input size=40 name="bingWords" ng-model="settings.bingWords" ng-list placeholder="Enter words separated with , (supports regex)"
 | 
			
		||||
                    ng-blur="saveBingWords()"/>
 | 
			
		||||
                    <ul>
 | 
			
		||||
                        <li ng-repeat="word in settings.bingWords">{{word}}</li>
 | 
			
		||||
                    </ul>
 | 
			
		||||
                </div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div ng-switch-when="denied">
 | 
			
		||||
                You have denied permission for notifications.<br/>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue