mirror of
https://git.anonymousland.org/anonymousland/synapse-product.git
synced 2025-01-16 13:07:06 -05:00
Merge branch 'release-v0.3.0' of github.com:matrix-org/synapse
This commit is contained in:
commit
704e7e9f44
41
CHANGES.rst
41
CHANGES.rst
@ -1,3 +1,41 @@
|
|||||||
|
Changes in synapse 0.3.0 (2014-09-18)
|
||||||
|
=====================================
|
||||||
|
See UPGRADE for information about changes to the client server API, including
|
||||||
|
breaking backwards compatibility with VoIP calls and registration API.
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* When a user changes their displayname or avatar the server will now update
|
||||||
|
all their join states to reflect this.
|
||||||
|
* The server now adds "age" key to events to indicate how old they are. This
|
||||||
|
is clock independent, so at no point does any server or webclient have to
|
||||||
|
assume their clock is in sync with everyone else.
|
||||||
|
* Fix bug where we didn't correctly pull in missing PDUs.
|
||||||
|
* Fix bug where prev_content key wasn't always returned.
|
||||||
|
* Add support for password resets.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Improve page content loading.
|
||||||
|
* Join/parts now trigger desktop notifications.
|
||||||
|
* Always show room aliases in the UI if one is present.
|
||||||
|
* No longer show user-count in the recents side panel.
|
||||||
|
* Add up & down arrow support to the text box for message sending to step
|
||||||
|
through your sent history.
|
||||||
|
* Don't display notifications for our own messages.
|
||||||
|
* Emotes are now formatted correctly in desktop notifications.
|
||||||
|
* The recents list now differentiates between public & private rooms.
|
||||||
|
* Fix bug where when switching between rooms the pagination flickered before
|
||||||
|
the view jumped to the bottom of the screen.
|
||||||
|
* Add support for password resets.
|
||||||
|
* Add bing word support.
|
||||||
|
|
||||||
|
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)
|
Changes in synapse 0.2.3 (2014-09-12)
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
@ -17,6 +55,9 @@ Webclient:
|
|||||||
* Add glare support for VoIP.
|
* Add glare support for VoIP.
|
||||||
* Improvements to initial startup speed.
|
* Improvements to initial startup speed.
|
||||||
* Don't display duplicate join events.
|
* Don't display duplicate join events.
|
||||||
|
* Local echo of messages.
|
||||||
|
* Differentiate sending and sent of local echo.
|
||||||
|
* Various minor bug fixes.
|
||||||
|
|
||||||
Changes in synapse 0.2.2 (2014-09-06)
|
Changes in synapse 0.2.2 (2014-09-06)
|
||||||
=====================================
|
=====================================
|
||||||
|
31
UPGRADE.rst
31
UPGRADE.rst
@ -1,3 +1,34 @@
|
|||||||
|
Upgrading to v0.3.0
|
||||||
|
===================
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Web Client
|
||||||
|
----------
|
||||||
|
|
||||||
|
The VoIP specification has changed between v0.2.0 and v0.3.0. Users should
|
||||||
|
refresh any browser tabs to get the latest web client code. Users on
|
||||||
|
v0.2.0 of the web client will not be able to call those on v0.3.0 and
|
||||||
|
vice versa.
|
||||||
|
|
||||||
|
|
||||||
Upgrading to v0.2.0
|
Upgrading to v0.2.0
|
||||||
===================
|
===================
|
||||||
|
|
||||||
|
@ -145,35 +145,50 @@ class SynapseCmd(cmd.Cmd):
|
|||||||
<noupdate> : Do not automatically clobber config values.
|
<noupdate> : Do not automatically clobber config values.
|
||||||
"""
|
"""
|
||||||
args = self._parse(line, ["userid", "noupdate"])
|
args = self._parse(line, ["userid", "noupdate"])
|
||||||
path = "/register"
|
|
||||||
|
|
||||||
password = None
|
password = None
|
||||||
pwd = None
|
pwd = None
|
||||||
pwd2 = "_"
|
pwd2 = "_"
|
||||||
while pwd != pwd2:
|
while pwd != pwd2:
|
||||||
pwd = getpass.getpass("(Optional) Type a password for this user: ")
|
pwd = getpass.getpass("Type a password for this user: ")
|
||||||
if len(pwd) == 0:
|
|
||||||
print "Not using a password for this user."
|
|
||||||
break
|
|
||||||
pwd2 = getpass.getpass("Retype the password: ")
|
pwd2 = getpass.getpass("Retype the password: ")
|
||||||
if pwd != pwd2:
|
if pwd != pwd2 or len(pwd) == 0:
|
||||||
print "Password mismatch."
|
print "Password mismatch."
|
||||||
|
pwd = None
|
||||||
else:
|
else:
|
||||||
password = pwd
|
password = pwd
|
||||||
|
|
||||||
body = {}
|
body = {
|
||||||
|
"type": "m.login.password"
|
||||||
|
}
|
||||||
if "userid" in args:
|
if "userid" in args:
|
||||||
body["user_id"] = args["userid"]
|
body["user"] = args["userid"]
|
||||||
if password:
|
if password:
|
||||||
body["password"] = password
|
body["password"] = password
|
||||||
|
|
||||||
reactor.callFromThread(self._do_register, "POST", path, body,
|
reactor.callFromThread(self._do_register, body,
|
||||||
"noupdate" not in args)
|
"noupdate" not in args)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_register(self, method, path, data, update_config):
|
def _do_register(self, data, update_config):
|
||||||
url = self._url() + path
|
# check the registration flows
|
||||||
json_res = yield self.http_client.do_request(method, url, data=data)
|
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)
|
print json.dumps(json_res, indent=4)
|
||||||
if update_config and "user_id" in json_res:
|
if update_config and "user_id" in json_res:
|
||||||
self.config["user"] = json_res["user_id"]
|
self.config["user"] = json_res["user_id"]
|
||||||
|
@ -3,35 +3,38 @@
|
|||||||
"apis": [
|
"apis": [
|
||||||
{
|
{
|
||||||
"operations": [
|
"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",
|
"method": "POST",
|
||||||
"nickname": "register",
|
"nickname": "submit_registration",
|
||||||
"notes": "Volatile: This API is likely to change.",
|
"notes": "If this is part of a multi-stage registration, there MUST be a 'session' key.",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
{
|
||||||
"description": "A registration request",
|
"description": "A registration submission",
|
||||||
"name": "body",
|
"name": "body",
|
||||||
"paramType": "body",
|
"paramType": "body",
|
||||||
"required": true,
|
"required": true,
|
||||||
"type": "RegistrationRequest"
|
"type": "RegistrationSubmission"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responseMessages": [
|
"responseMessages": [
|
||||||
{
|
{
|
||||||
"code": 400,
|
"code": 400,
|
||||||
"message": "No JSON object."
|
"message": "Bad login type"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"code": 400,
|
"code": 400,
|
||||||
"message": "User ID must only contain characters which do not require url encoding."
|
"message": "Missing JSON keys"
|
||||||
},
|
|
||||||
{
|
|
||||||
"code": 400,
|
|
||||||
"message": "User ID already taken."
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"summary": "Register with the home server.",
|
"summary": "Submit a registration action.",
|
||||||
"type": "RegistrationResponse"
|
"type": "RegistrationResult"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"path": "/register"
|
"path": "/register"
|
||||||
@ -42,30 +45,68 @@
|
|||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
"models": {
|
"models": {
|
||||||
"RegistrationResponse": {
|
"RegistrationFlows": {
|
||||||
"id": "RegistrationResponse",
|
"id": "RegistrationFlows",
|
||||||
|
"properties": {
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"description": "The first login type that must be used when logging in.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RegistrationResult": {
|
||||||
|
"id": "RegistrationResult",
|
||||||
"properties": {
|
"properties": {
|
||||||
"access_token": {
|
"access_token": {
|
||||||
"description": "The access token for this user.",
|
"description": "The access token for this user's registration if this is the final stage of the registration process.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"user_id": {
|
"user_id": {
|
||||||
"description": "The fully-qualified user ID.",
|
"description": "The user's fully-qualified user ID.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"home_server": {
|
"next": {
|
||||||
"description": "The name of the home server.",
|
"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"
|
"type": "string"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"RegistrationRequest": {
|
"RegistrationSubmission": {
|
||||||
"id": "RegistrationRequest",
|
"id": "RegistrationSubmission",
|
||||||
"properties": {
|
"properties": {
|
||||||
"user_id": {
|
"type": {
|
||||||
"description": "The desired user ID. If not specified, a random user ID will be allocated.",
|
"description": "The type of registration being submitted.",
|
||||||
"type": "string",
|
"type": "string"
|
||||||
"required": false
|
},
|
||||||
|
"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:
|
Required keys:
|
||||||
- ``call_id`` : "string" - A unique identifier for the call
|
- ``call_id`` : "string" - A unique identifier for the call
|
||||||
- ``offer`` : "offer object" - The session description
|
- ``offer`` : "offer object" - The session description
|
||||||
- ``version`` : "integer" - The version of the VoIP specification this message
|
- ``version`` : "integer" - The version of the VoIP specification this
|
||||||
adheres to. This specification is version 0.
|
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:
|
Optional keys:
|
||||||
None.
|
None.
|
||||||
@ -1182,16 +1188,16 @@ This event is sent by the caller when they wish to establish a call.
|
|||||||
- ``type`` : "string" - The type of session description, in this case 'offer'
|
- ``type`` : "string" - The type of session description, in this case 'offer'
|
||||||
- ``sdp`` : "string" - The SDP text of the session description
|
- ``sdp`` : "string" - The SDP text of the session description
|
||||||
|
|
||||||
``m.call.candidate``
|
``m.call.candidates``
|
||||||
This event is sent by callers after sending an invite and by the callee after answering.
|
This event is sent by callers after sending an invite and by the callee after answering.
|
||||||
Its purpose is to give the other party an additional ICE candidate to try using to
|
Its purpose is to give the other party additional ICE candidates to try using to
|
||||||
communicate.
|
communicate.
|
||||||
|
|
||||||
Required keys:
|
Required keys:
|
||||||
- ``call_id`` : "string" - The ID of the call this event relates to
|
- ``call_id`` : "string" - The ID of the call this event relates to
|
||||||
- ``version`` : "integer" - The version of the VoIP specification this messages
|
- ``version`` : "integer" - The version of the VoIP specification this messages
|
||||||
adheres to. his specification is version 0.
|
adheres to. his specification is version 0.
|
||||||
- ``candidate`` : "candidate object" - Object describing the candidate.
|
- ``candidates`` : "array of candidate objects" - Array of object describing the candidates.
|
||||||
|
|
||||||
``Candidate Object``
|
``Candidate Object``
|
||||||
|
|
||||||
@ -1305,12 +1311,6 @@ display name other than it being a valid unicode string.
|
|||||||
|
|
||||||
Registration and login
|
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
|
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
|
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
|
home servers should authorise their users who want to login to their existing
|
||||||
accounts, but instead defines the standard interface which implementations
|
accounts, but instead defines the standard interface which implementations
|
||||||
should follow so that ANY client can login to ANY home server. Clients login
|
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.
|
1. Determine the requirements for logging in.
|
||||||
2. Submit the login stage credentials.
|
2. Submit the login stage credentials.
|
||||||
3. Get credentials or be told the next stage in the login process and repeat
|
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.oauth2``
|
||||||
- ``m.login.email.code``
|
- ``m.login.email.code``
|
||||||
- ``m.login.email.url``
|
- ``m.login.email.url``
|
||||||
|
- ``m.login.email.identity``
|
||||||
|
|
||||||
Password-based
|
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.
|
``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
|
N-Factor Authentication
|
||||||
-----------------------
|
-----------------------
|
||||||
Multiple login stages can be combined to create N-factor authentication during login.
|
Multiple login stages can be combined to create N-factor authentication during login.
|
||||||
@ -2242,6 +2269,9 @@ Transaction:
|
|||||||
.. |login| replace:: ``/login``
|
.. |login| replace:: ``/login``
|
||||||
.. _login: /docs/api/client-server/#!/-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| replace:: ``/rooms/<room_id>/messages``
|
||||||
.. _/rooms/<room_id>/messages: /docs/api/client-server/#!/-rooms/get_messages
|
.. _/rooms/<room_id>/messages: /docs/api/client-server/#!/-rooms/get_messages
|
||||||
|
|
||||||
|
@ -16,4 +16,4 @@
|
|||||||
""" This is a reference implementation of a synapse home server.
|
""" This is a reference implementation of a synapse home server.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
__version__ = "0.2.3"
|
__version__ = "0.3.0"
|
||||||
|
@ -50,3 +50,12 @@ class JoinRules(object):
|
|||||||
KNOCK = u"knock"
|
KNOCK = u"knock"
|
||||||
INVITE = u"invite"
|
INVITE = u"invite"
|
||||||
PRIVATE = u"private"
|
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"
|
@ -17,6 +17,19 @@ from synapse.api.errors import SynapseError, Codes
|
|||||||
from synapse.util.jsonobject import JsonEncodedObject
|
from synapse.util.jsonobject import JsonEncodedObject
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_event(hs, e):
|
||||||
|
# FIXME(erikj): To handle the case of presence events and the like
|
||||||
|
if not isinstance(e, SynapseEvent):
|
||||||
|
return 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
|
||||||
|
|
||||||
|
|
||||||
class SynapseEvent(JsonEncodedObject):
|
class SynapseEvent(JsonEncodedObject):
|
||||||
|
|
||||||
"""Base class for Synapse events. These are JSON objects which must abide
|
"""Base class for Synapse events. These are JSON objects which must abide
|
||||||
@ -43,6 +56,8 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
"content", # HTTP body, JSON
|
"content", # HTTP body, JSON
|
||||||
"state_key",
|
"state_key",
|
||||||
"required_power_level",
|
"required_power_level",
|
||||||
|
"age_ts",
|
||||||
|
"prev_content",
|
||||||
]
|
]
|
||||||
|
|
||||||
internal_keys = [
|
internal_keys = [
|
||||||
@ -158,10 +173,6 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
|
|
||||||
class SynapseStateEvent(SynapseEvent):
|
class SynapseStateEvent(SynapseEvent):
|
||||||
|
|
||||||
valid_keys = SynapseEvent.valid_keys + [
|
|
||||||
"prev_content",
|
|
||||||
]
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
if "state_key" not in kwargs:
|
if "state_key" not in kwargs:
|
||||||
kwargs["state_key"] = ""
|
kwargs["state_key"] = ""
|
||||||
|
@ -59,6 +59,14 @@ class EventFactory(object):
|
|||||||
if "ts" not in kwargs:
|
if "ts" not in kwargs:
|
||||||
kwargs["ts"] = int(self.clock.time_msec())
|
kwargs["ts"] = int(self.clock.time_msec())
|
||||||
|
|
||||||
|
# The "age" key is a delta timestamp that should be converted into an
|
||||||
|
# absolute timestamp the minute we see it.
|
||||||
|
if "age" in kwargs:
|
||||||
|
kwargs["age_ts"] = int(self.clock.time_msec()) - int(kwargs["age"])
|
||||||
|
del kwargs["age"]
|
||||||
|
elif "age_ts" not in kwargs:
|
||||||
|
kwargs["age_ts"] = int(self.clock.time_msec())
|
||||||
|
|
||||||
if etype in self._event_list:
|
if etype in self._event_list:
|
||||||
handler = self._event_list[etype]
|
handler = self._event_list[etype]
|
||||||
else:
|
else:
|
||||||
|
39
synapse/config/email.py
Normal file
39
synapse/config/email.py
Normal file
@ -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 .ratelimiting import RatelimitConfig
|
||||||
from .repository import ContentRepositoryConfig
|
from .repository import ContentRepositoryConfig
|
||||||
from .captcha import CaptchaConfig
|
from .captcha import CaptchaConfig
|
||||||
|
from .email import EmailConfig
|
||||||
|
|
||||||
|
|
||||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig):
|
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
||||||
|
EmailConfig):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if __name__=='__main__':
|
|
||||||
|
if __name__ == '__main__':
|
||||||
import sys
|
import sys
|
||||||
HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
|
HomeServerConfig.load_config("Generate config", sys.argv[1:], "HomeServer")
|
||||||
|
@ -291,6 +291,13 @@ class ReplicationLayer(object):
|
|||||||
def on_incoming_transaction(self, transaction_data):
|
def on_incoming_transaction(self, transaction_data):
|
||||||
transaction = Transaction(**transaction_data)
|
transaction = Transaction(**transaction_data)
|
||||||
|
|
||||||
|
for p in transaction.pdus:
|
||||||
|
if "age" in p:
|
||||||
|
p["age_ts"] = int(self._clock.time_msec()) - int(p["age"])
|
||||||
|
del p["age"]
|
||||||
|
|
||||||
|
pdu_list = [Pdu(**p) for p in transaction.pdus]
|
||||||
|
|
||||||
logger.debug("[%s] Got transaction", transaction.transaction_id)
|
logger.debug("[%s] Got transaction", transaction.transaction_id)
|
||||||
|
|
||||||
response = yield self.transaction_actions.have_responded(transaction)
|
response = yield self.transaction_actions.have_responded(transaction)
|
||||||
@ -303,8 +310,6 @@ class ReplicationLayer(object):
|
|||||||
|
|
||||||
logger.debug("[%s] Transacition is new", transaction.transaction_id)
|
logger.debug("[%s] Transacition is new", transaction.transaction_id)
|
||||||
|
|
||||||
pdu_list = [Pdu(**p) for p in transaction.pdus]
|
|
||||||
|
|
||||||
dl = []
|
dl = []
|
||||||
for pdu in pdu_list:
|
for pdu in pdu_list:
|
||||||
dl.append(self._handle_new_pdu(pdu))
|
dl.append(self._handle_new_pdu(pdu))
|
||||||
@ -405,9 +410,14 @@ class ReplicationLayer(object):
|
|||||||
"""Returns a new Transaction containing the given PDUs suitable for
|
"""Returns a new Transaction containing the given PDUs suitable for
|
||||||
transmission.
|
transmission.
|
||||||
"""
|
"""
|
||||||
|
pdus = [p.get_dict() for p in pdu_list]
|
||||||
|
for p in pdus:
|
||||||
|
if "age_ts" in pdus:
|
||||||
|
p["age"] = int(self.clock.time_msec()) - p["age_ts"]
|
||||||
|
|
||||||
return Transaction(
|
return Transaction(
|
||||||
pdus=[p.get_dict() for p in pdu_list],
|
|
||||||
origin=self.server_name,
|
origin=self.server_name,
|
||||||
|
pdus=pdus,
|
||||||
ts=int(self._clock.time_msec()),
|
ts=int(self._clock.time_msec()),
|
||||||
destination=None,
|
destination=None,
|
||||||
)
|
)
|
||||||
@ -593,8 +603,21 @@ class _TransactionQueue(object):
|
|||||||
logger.debug("TX [%s] Sending transaction...", destination)
|
logger.debug("TX [%s] Sending transaction...", destination)
|
||||||
|
|
||||||
# Actually send the transaction
|
# 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(
|
code, response = yield self.transport_layer.send_transaction(
|
||||||
transaction
|
transaction,
|
||||||
|
on_send_callback=cb,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug("TX [%s] Sent transaction", destination)
|
logger.debug("TX [%s] Sent transaction", destination)
|
||||||
|
@ -144,7 +144,7 @@ class TransportLayer(object):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@log_function
|
||||||
def send_transaction(self, transaction):
|
def send_transaction(self, transaction, on_send_callback=None):
|
||||||
""" Sends the given Transaction to it's destination
|
""" Sends the given Transaction to it's destination
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -165,10 +165,23 @@ class TransportLayer(object):
|
|||||||
|
|
||||||
data = transaction.get_dict()
|
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(
|
code, response = yield self.client.put_json(
|
||||||
transaction.destination,
|
transaction.destination,
|
||||||
path=PREFIX + "/send/%s/" % transaction.transaction_id,
|
path=PREFIX + "/send/%s/" % transaction.transaction_id,
|
||||||
data=data
|
data=data,
|
||||||
|
on_send_callback=cb,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
|
@ -15,7 +15,6 @@
|
|||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.events import SynapseEvent
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
@ -71,10 +70,7 @@ class EventStreamHandler(BaseHandler):
|
|||||||
auth_user, room_ids, pagin_config, timeout
|
auth_user, room_ids, pagin_config, timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
chunks = [
|
chunks = [self.hs.serialize_event(e) for e in events]
|
||||||
e.get_dict() if isinstance(e, SynapseEvent) else e
|
|
||||||
for e in events
|
|
||||||
]
|
|
||||||
|
|
||||||
chunk = {
|
chunk = {
|
||||||
"chunk": chunks,
|
"chunk": chunks,
|
||||||
@ -92,7 +88,9 @@ class EventStreamHandler(BaseHandler):
|
|||||||
# 10 seconds of grace to allow the client to reconnect again
|
# 10 seconds of grace to allow the client to reconnect again
|
||||||
# before we think they're gone
|
# before we think they're gone
|
||||||
def _later():
|
def _later():
|
||||||
logger.debug("_later stopped_user_eventstream %s", auth_user)
|
logger.debug(
|
||||||
|
"_later stopped_user_eventstream %s", auth_user
|
||||||
|
)
|
||||||
self.distributor.fire(
|
self.distributor.fire(
|
||||||
"stopped_user_eventstream", auth_user
|
"stopped_user_eventstream", auth_user
|
||||||
)
|
)
|
||||||
|
@ -93,22 +93,18 @@ class FederationHandler(BaseHandler):
|
|||||||
"""
|
"""
|
||||||
event = self.pdu_codec.event_from_pdu(pdu)
|
event = self.pdu_codec.event_from_pdu(pdu)
|
||||||
|
|
||||||
|
logger.debug("Got event: %s", event.event_id)
|
||||||
|
|
||||||
with (yield self.lock_manager.lock(pdu.context)):
|
with (yield self.lock_manager.lock(pdu.context)):
|
||||||
if event.is_state and not backfilled:
|
if event.is_state and not backfilled:
|
||||||
is_new_state = yield self.state_handler.handle_new_state(
|
is_new_state = yield self.state_handler.handle_new_state(
|
||||||
pdu
|
pdu
|
||||||
)
|
)
|
||||||
if not is_new_state:
|
|
||||||
return
|
|
||||||
else:
|
else:
|
||||||
is_new_state = False
|
is_new_state = False
|
||||||
# TODO: Implement something in federation that allows us to
|
# TODO: Implement something in federation that allows us to
|
||||||
# respond to PDU.
|
# respond to PDU.
|
||||||
|
|
||||||
if hasattr(event, "state_key") and not is_new_state:
|
|
||||||
logger.debug("Ignoring old state.")
|
|
||||||
return
|
|
||||||
|
|
||||||
target_is_mine = False
|
target_is_mine = False
|
||||||
if hasattr(event, "target_host"):
|
if hasattr(event, "target_host"):
|
||||||
target_is_mine = event.target_host == self.hs.hostname
|
target_is_mine = event.target_host == self.hs.hostname
|
||||||
@ -139,7 +135,11 @@ class FederationHandler(BaseHandler):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
with (yield self.room_lock.lock(event.room_id)):
|
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)
|
room = yield self.store.get_room(event.room_id)
|
||||||
|
|
||||||
|
@ -17,9 +17,13 @@ from twisted.internet import defer
|
|||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
from synapse.api.errors import LoginError, Codes
|
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 bcrypt
|
||||||
import logging
|
import logging
|
||||||
|
import urllib
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -63,3 +67,40 @@ class LoginHandler(BaseHandler):
|
|||||||
else:
|
else:
|
||||||
logger.warn("Failed password login for user %s", user)
|
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)
|
@ -124,7 +124,7 @@ class MessageHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
chunk = {
|
chunk = {
|
||||||
"chunk": [e.get_dict() for e in events],
|
"chunk": [self.hs.serialize_event(e) for e in events],
|
||||||
"start": pagin_config.from_token.to_string(),
|
"start": pagin_config.from_token.to_string(),
|
||||||
"end": next_token.to_string(),
|
"end": next_token.to_string(),
|
||||||
}
|
}
|
||||||
@ -268,6 +268,9 @@ class MessageHandler(BaseHandler):
|
|||||||
user, pagination_config, None
|
user, pagination_config, None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
public_rooms = yield self.store.get_rooms(is_public=True)
|
||||||
|
public_room_ids = [r["room_id"] for r in public_rooms]
|
||||||
|
|
||||||
limit = pagin_config.limit
|
limit = pagin_config.limit
|
||||||
if not limit:
|
if not limit:
|
||||||
limit = 10
|
limit = 10
|
||||||
@ -276,6 +279,8 @@ class MessageHandler(BaseHandler):
|
|||||||
d = {
|
d = {
|
||||||
"room_id": event.room_id,
|
"room_id": event.room_id,
|
||||||
"membership": event.membership,
|
"membership": event.membership,
|
||||||
|
"visibility": ("public" if event.room_id in
|
||||||
|
public_room_ids else "private"),
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.membership == Membership.INVITE:
|
if event.membership == Membership.INVITE:
|
||||||
@ -296,7 +301,7 @@ class MessageHandler(BaseHandler):
|
|||||||
end_token = now_token.copy_and_replace("room_key", token[1])
|
end_token = now_token.copy_and_replace("room_key", token[1])
|
||||||
|
|
||||||
d["messages"] = {
|
d["messages"] = {
|
||||||
"chunk": [m.get_dict() for m in messages],
|
"chunk": [self.hs.serialize_event(m) for m in messages],
|
||||||
"start": start_token.to_string(),
|
"start": start_token.to_string(),
|
||||||
"end": end_token.to_string(),
|
"end": end_token.to_string(),
|
||||||
}
|
}
|
||||||
@ -304,7 +309,7 @@ class MessageHandler(BaseHandler):
|
|||||||
current_state = yield self.store.get_current_state(
|
current_state = yield self.store.get_current_state(
|
||||||
event.room_id
|
event.room_id
|
||||||
)
|
)
|
||||||
d["state"] = [c.get_dict() for c in current_state]
|
d["state"] = [self.hs.serialize_event(c) for c in current_state]
|
||||||
except:
|
except:
|
||||||
logger.exception("Failed to get snapshot")
|
logger.exception("Failed to get snapshot")
|
||||||
|
|
||||||
|
@ -15,9 +15,9 @@
|
|||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError, AuthError
|
from synapse.api.errors import SynapseError, AuthError, CodeMessageException
|
||||||
|
from synapse.api.constants import Membership
|
||||||
from synapse.api.errors import CodeMessageException
|
from synapse.api.events.room import RoomMemberEvent
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
@ -97,6 +97,8 @@ class ProfileHandler(BaseHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
yield self._update_join_states(target_user)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_avatar_url(self, target_user):
|
def get_avatar_url(self, target_user):
|
||||||
if target_user.is_mine:
|
if target_user.is_mine:
|
||||||
@ -144,6 +146,8 @@ class ProfileHandler(BaseHandler):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
yield self._update_join_states(target_user)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def collect_presencelike_data(self, user, state):
|
def collect_presencelike_data(self, user, state):
|
||||||
if not user.is_mine:
|
if not user.is_mine:
|
||||||
@ -180,3 +184,39 @@ class ProfileHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
defer.returnValue(response)
|
defer.returnValue(response)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _update_join_states(self, user):
|
||||||
|
if not user.is_mine:
|
||||||
|
return
|
||||||
|
|
||||||
|
joins = yield self.store.get_rooms_for_user_where_membership_is(
|
||||||
|
user.to_string(),
|
||||||
|
[Membership.JOIN],
|
||||||
|
)
|
||||||
|
|
||||||
|
for j in joins:
|
||||||
|
snapshot = yield self.store.snapshot_room(
|
||||||
|
j.room_id, j.state_key, RoomMemberEvent.TYPE,
|
||||||
|
j.state_key
|
||||||
|
)
|
||||||
|
|
||||||
|
content = {
|
||||||
|
"membership": j.content["membership"],
|
||||||
|
"prev": j.content["membership"],
|
||||||
|
}
|
||||||
|
|
||||||
|
yield self.distributor.fire(
|
||||||
|
"collect_presencelike_data", user, content
|
||||||
|
)
|
||||||
|
|
||||||
|
new_event = self.event_factory.create_event(
|
||||||
|
etype=j.type,
|
||||||
|
room_id=j.room_id,
|
||||||
|
state_key=j.state_key,
|
||||||
|
content=content,
|
||||||
|
user_id=j.state_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.state_handler.handle_new_event(new_event, snapshot)
|
||||||
|
yield self._on_new_room_event(new_event, snapshot)
|
||||||
|
@ -40,8 +40,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
self.distributor.declare("registered_user")
|
self.distributor.declare("registered_user")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def register(self, localpart=None, password=None, threepidCreds=None,
|
def register(self, localpart=None, password=None):
|
||||||
captcha_info={}):
|
|
||||||
"""Registers a new client on the server.
|
"""Registers a new client on the server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -54,37 +53,6 @@ class RegistrationHandler(BaseHandler):
|
|||||||
Raises:
|
Raises:
|
||||||
RegistrationError if there was a problem registering.
|
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
|
password_hash = None
|
||||||
if password:
|
if password:
|
||||||
password_hash = bcrypt.hashpw(password, bcrypt.gensalt())
|
password_hash = bcrypt.hashpw(password, bcrypt.gensalt())
|
||||||
@ -126,15 +94,54 @@ class RegistrationHandler(BaseHandler):
|
|||||||
raise RegistrationError(
|
raise RegistrationError(
|
||||||
500, "Cannot generate user ID.")
|
500, "Cannot generate 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
|
# Now we have a matrix ID, bind it to the threepids we were given
|
||||||
if threepidCreds:
|
|
||||||
for c in threepidCreds:
|
for c in threepidCreds:
|
||||||
# XXX: This should be a deferred list, shouldn't it?
|
# XXX: This should be a deferred list, shouldn't it?
|
||||||
yield self._bind_threepid(c, user_id)
|
yield self._bind_threepid(c, user_id)
|
||||||
|
|
||||||
|
|
||||||
defer.returnValue((user_id, token))
|
|
||||||
|
|
||||||
def _generate_token(self, user_id):
|
def _generate_token(self, user_id):
|
||||||
# urlsafe variant uses _ and - so use . as the separator and replace
|
# 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
|
# all =s with .s so http clients don't quote =s when it is used as
|
||||||
@ -149,15 +156,15 @@ class RegistrationHandler(BaseHandler):
|
|||||||
def _threepid_from_creds(self, creds):
|
def _threepid_from_creds(self, creds):
|
||||||
httpCli = PlainHttpClient(self.hs)
|
httpCli = PlainHttpClient(self.hs)
|
||||||
# XXX: make this configurable!
|
# XXX: make this configurable!
|
||||||
trustedIdServers = [ 'matrix.org:8090' ]
|
trustedIdServers = ['matrix.org:8090']
|
||||||
if not creds['idServer'] in trustedIdServers:
|
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'])
|
'credentials', creds['idServer'])
|
||||||
defer.returnValue(None)
|
defer.returnValue(None)
|
||||||
data = yield httpCli.get_json(
|
data = yield httpCli.get_json(
|
||||||
creds['idServer'],
|
creds['idServer'],
|
||||||
"/_matrix/identity/api/v1/3pid/getValidated3pid",
|
"/_matrix/identity/api/v1/3pid/getValidated3pid",
|
||||||
{ 'sid': creds['sid'], 'clientSecret': creds['clientSecret'] }
|
{'sid': creds['sid'], 'clientSecret': creds['clientSecret']}
|
||||||
)
|
)
|
||||||
|
|
||||||
if 'medium' in data:
|
if 'medium' in data:
|
||||||
@ -170,8 +177,8 @@ class RegistrationHandler(BaseHandler):
|
|||||||
data = yield httpCli.post_urlencoded_get_json(
|
data = yield httpCli.post_urlencoded_get_json(
|
||||||
creds['idServer'],
|
creds['idServer'],
|
||||||
"/_matrix/identity/api/v1/3pid/bind",
|
"/_matrix/identity/api/v1/3pid/bind",
|
||||||
{ 'sid': creds['sid'], 'clientSecret': creds['clientSecret'],
|
{'sid': creds['sid'], 'clientSecret': creds['clientSecret'],
|
||||||
'mxid':mxid }
|
'mxid': mxid}
|
||||||
)
|
)
|
||||||
defer.returnValue(data)
|
defer.returnValue(data)
|
||||||
|
|
||||||
@ -189,7 +196,7 @@ class RegistrationHandler(BaseHandler):
|
|||||||
lines = response.split('\n')
|
lines = response.split('\n')
|
||||||
json = {
|
json = {
|
||||||
"valid": lines[0] == 'true',
|
"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]
|
"error=%s" % lines[1]
|
||||||
}
|
}
|
||||||
defer.returnValue(json)
|
defer.returnValue(json)
|
||||||
@ -200,7 +207,8 @@ class RegistrationHandler(BaseHandler):
|
|||||||
data = yield client.post_urlencoded_get_raw(
|
data = yield client.post_urlencoded_get_raw(
|
||||||
"www.google.com:80",
|
"www.google.com:80",
|
||||||
"/recaptcha/api/verify",
|
"/recaptcha/api/verify",
|
||||||
accept_partial=True, # twisted dislikes google's response, no content length.
|
# twisted dislikes google's response, no content length.
|
||||||
|
accept_partial=True,
|
||||||
args={
|
args={
|
||||||
'privatekey': private_key,
|
'privatekey': private_key,
|
||||||
'remoteip': ip_addr,
|
'remoteip': ip_addr,
|
||||||
|
@ -335,7 +335,7 @@ class RoomMemberHandler(BaseHandler):
|
|||||||
|
|
||||||
member_list = yield self.store.get_room_members(room_id=room_id)
|
member_list = yield self.store.get_room_members(room_id=room_id)
|
||||||
event_list = [
|
event_list = [
|
||||||
entry.get_dict()
|
self.hs.serialize_event(entry)
|
||||||
for entry in member_list
|
for entry in member_list
|
||||||
]
|
]
|
||||||
chunk_data = {
|
chunk_data = {
|
||||||
|
@ -122,7 +122,7 @@ class TwistedHttpClient(HttpClient):
|
|||||||
self.hs = hs
|
self.hs = hs
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@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:
|
if destination in _destination_mappings:
|
||||||
destination = _destination_mappings[destination]
|
destination = _destination_mappings[destination]
|
||||||
|
|
||||||
@ -131,7 +131,8 @@ class TwistedHttpClient(HttpClient):
|
|||||||
"PUT",
|
"PUT",
|
||||||
path.encode("ascii"),
|
path.encode("ascii"),
|
||||||
producer=_JsonProducer(data),
|
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")
|
logger.debug("Getting resp body")
|
||||||
@ -218,7 +219,7 @@ class TwistedHttpClient(HttpClient):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _create_request(self, destination, method, path_bytes, param_bytes=b"",
|
def _create_request(self, destination, method, path_bytes, param_bytes=b"",
|
||||||
query_bytes=b"", producer=None, headers_dict={},
|
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
|
""" Creates and sends a request to the given url
|
||||||
"""
|
"""
|
||||||
headers_dict[b"User-Agent"] = [b"Synapse"]
|
headers_dict[b"User-Agent"] = [b"Synapse"]
|
||||||
@ -242,6 +243,9 @@ class TwistedHttpClient(HttpClient):
|
|||||||
endpoint = self._getEndpoint(reactor, destination);
|
endpoint = self._getEndpoint(reactor, destination);
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
if on_send_callback:
|
||||||
|
on_send_callback(destination, method, path_bytes, producer)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
response = yield self.agent.request(
|
response = yield self.agent.request(
|
||||||
destination,
|
destination,
|
||||||
@ -310,6 +314,9 @@ class _JsonProducer(object):
|
|||||||
""" Used by the twisted http client to create the HTTP body from json
|
""" Used by the twisted http client to create the HTTP body from json
|
||||||
"""
|
"""
|
||||||
def __init__(self, jsn):
|
def __init__(self, jsn):
|
||||||
|
self.reset(jsn)
|
||||||
|
|
||||||
|
def reset(self, jsn):
|
||||||
self.body = encode_canonical_json(jsn)
|
self.body = encode_canonical_json(jsn)
|
||||||
self.length = len(self.body)
|
self.length = len(self.body)
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class EventRestServlet(RestServlet):
|
|||||||
event = yield handler.get_event(auth_user, event_id)
|
event = yield handler.get_event(auth_user, event_id)
|
||||||
|
|
||||||
if event:
|
if event:
|
||||||
defer.returnValue((200, event.get_dict()))
|
defer.returnValue((200, self.hs.serialize_event(event)))
|
||||||
else:
|
else:
|
||||||
defer.returnValue((404, "Event not found."))
|
defer.returnValue((404, "Event not found."))
|
||||||
|
|
||||||
|
@ -73,6 +73,27 @@ class LoginFallbackRestServlet(RestServlet):
|
|||||||
return (200, {})
|
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):
|
def _parse_json(request):
|
||||||
try:
|
try:
|
||||||
content = json.loads(request.content.read())
|
content = json.loads(request.content.read())
|
||||||
@ -85,3 +106,4 @@ def _parse_json(request):
|
|||||||
|
|
||||||
def register_servlets(hs, http_server):
|
def register_servlets(hs, http_server):
|
||||||
LoginRestServlet(hs).register(http_server)
|
LoginRestServlet(hs).register(http_server)
|
||||||
|
# TODO PasswordResetRestServlet(hs).register(http_server)
|
||||||
|
@ -17,56 +17,141 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError, Codes
|
from synapse.api.errors import SynapseError, Codes
|
||||||
|
from synapse.api.constants import LoginType
|
||||||
from base import RestServlet, client_path_pattern
|
from base import RestServlet, client_path_pattern
|
||||||
|
import synapse.util.stringutils as stringutils
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RegisterRestServlet(RestServlet):
|
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$")
|
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
|
@defer.inlineCallbacks
|
||||||
def on_POST(self, request):
|
def on_POST(self, request):
|
||||||
desired_user_id = None
|
register_json = _parse_json(request)
|
||||||
password = None
|
|
||||||
|
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:
|
try:
|
||||||
register_json = json.loads(request.content.read())
|
login_type = register_json["type"]
|
||||||
if "password" in register_json:
|
stages = {
|
||||||
password = register_json["password"].encode("utf-8")
|
LoginType.RECAPTCHA: self._do_recaptcha,
|
||||||
|
LoginType.PASSWORD: self._do_password,
|
||||||
|
LoginType.EMAIL_IDENTITY: self._do_email_identity
|
||||||
|
}
|
||||||
|
|
||||||
if type(register_json["user_id"]) == unicode:
|
session_info = self._get_session_info(request, session)
|
||||||
desired_user_id = register_json["user_id"].encode("utf-8")
|
logger.debug("%s : session info %s request info %s",
|
||||||
if urllib.quote(desired_user_id) != desired_user_id:
|
login_type, session_info, register_json)
|
||||||
raise SynapseError(
|
response = yield stages[login_type](
|
||||||
400,
|
request,
|
||||||
"User ID must only contain characters which do not " +
|
register_json,
|
||||||
"require URL encoding.")
|
session_info
|
||||||
except ValueError:
|
)
|
||||||
defer.returnValue((400, "No JSON object."))
|
|
||||||
except KeyError:
|
|
||||||
pass # user_id is optional
|
|
||||||
|
|
||||||
threepidCreds = None
|
if "access_token" not in response:
|
||||||
if 'threepidCreds' in register_json:
|
# isn't a final response
|
||||||
threepidCreds = register_json['threepidCreds']
|
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.")
|
||||||
|
|
||||||
captcha = {}
|
|
||||||
if self.hs.config.enable_registration_captcha:
|
|
||||||
challenge = None
|
challenge = None
|
||||||
user_response = None
|
user_response = None
|
||||||
try:
|
try:
|
||||||
captcha_type = register_json["captcha"]["type"]
|
challenge = register_json["challenge"]
|
||||||
if captcha_type != "m.login.recaptcha":
|
user_response = register_json["response"]
|
||||||
raise SynapseError(400, "Sorry, only m.login.recaptcha " +
|
|
||||||
"requests are supported.")
|
|
||||||
challenge = register_json["captcha"]["challenge"]
|
|
||||||
user_response = register_json["captcha"]["response"]
|
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise SynapseError(400, "Captcha response is required",
|
raise SynapseError(400, "Captcha response is required",
|
||||||
errcode=Codes.CAPTCHA_NEEDED)
|
errcode=Codes.CAPTCHA_NEEDED)
|
||||||
|
|
||||||
# TODO determine the source IP : May be an X-Forwarding-For header depending on config
|
# May be an X-Forwarding-For header depending on config
|
||||||
ip_addr = request.getClientIP()
|
ip_addr = request.getClientIP()
|
||||||
if self.hs.config.captcha_ip_origin_is_x_forwarded:
|
if self.hs.config.captcha_ip_origin_is_x_forwarded:
|
||||||
# use the header
|
# use the header
|
||||||
@ -74,32 +159,76 @@ class RegisterRestServlet(RestServlet):
|
|||||||
ip_addr = request.requestHeaders.getRawHeaders(
|
ip_addr = request.requestHeaders.getRawHeaders(
|
||||||
"X-Forwarded-For")[0]
|
"X-Forwarded-For")[0]
|
||||||
|
|
||||||
captcha = {
|
handler = self.handlers.registration_handler
|
||||||
"ip": ip_addr,
|
yield handler.check_recaptcha(
|
||||||
"private_key": self.hs.config.recaptcha_private_key,
|
ip_addr,
|
||||||
"challenge": challenge,
|
self.hs.config.recaptcha_private_key,
|
||||||
"response": user_response
|
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
|
handler = self.handlers.registration_handler
|
||||||
(user_id, token) = yield handler.register(
|
(user_id, token) = yield handler.register(
|
||||||
localpart=desired_user_id,
|
localpart=desired_user_id,
|
||||||
password=password,
|
password=password
|
||||||
threepidCreds=threepidCreds,
|
)
|
||||||
captcha_info=captcha)
|
|
||||||
|
if session[LoginType.EMAIL_IDENTITY]:
|
||||||
|
yield handler.bind_emails(user_id, session["threepidCreds"])
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
"access_token": token,
|
"access_token": token,
|
||||||
"home_server": self.hs.hostname,
|
"home_server": self.hs.hostname,
|
||||||
}
|
}
|
||||||
defer.returnValue(
|
self._remove_session(session)
|
||||||
(200, result)
|
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):
|
def register_servlets(hs, http_server):
|
||||||
|
@ -378,7 +378,7 @@ class RoomTriggerBackfill(RestServlet):
|
|||||||
handler = self.handlers.federation_handler
|
handler = self.handlers.federation_handler
|
||||||
events = yield handler.backfill(remote_server, room_id, limit)
|
events = yield handler.backfill(remote_server, room_id, limit)
|
||||||
|
|
||||||
res = [event.get_dict() for event in events]
|
res = [self.hs.serialize_event(event) for event in events]
|
||||||
defer.returnValue((200, res))
|
defer.returnValue((200, res))
|
||||||
|
|
||||||
|
|
||||||
|
@ -20,6 +20,7 @@
|
|||||||
|
|
||||||
# Imports required for the default HomeServer() implementation
|
# Imports required for the default HomeServer() implementation
|
||||||
from synapse.federation import initialize_http_replication
|
from synapse.federation import initialize_http_replication
|
||||||
|
from synapse.api.events import serialize_event
|
||||||
from synapse.api.events.factory import EventFactory
|
from synapse.api.events.factory import EventFactory
|
||||||
from synapse.notifier import Notifier
|
from synapse.notifier import Notifier
|
||||||
from synapse.api.auth import Auth
|
from synapse.api.auth import Auth
|
||||||
@ -138,6 +139,9 @@ class BaseHomeServer(object):
|
|||||||
object."""
|
object."""
|
||||||
return RoomID.from_string(s, hs=self)
|
return RoomID.from_string(s, hs=self)
|
||||||
|
|
||||||
|
def serialize_event(self, e):
|
||||||
|
return serialize_event(self, e)
|
||||||
|
|
||||||
# Build magic accessors for every dependency
|
# Build magic accessors for every dependency
|
||||||
for depname in BaseHomeServer.DEPENDENCIES:
|
for depname in BaseHomeServer.DEPENDENCIES:
|
||||||
BaseHomeServer._make_dependency_method(depname)
|
BaseHomeServer._make_dependency_method(depname)
|
||||||
|
@ -68,7 +68,8 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@log_function
|
@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
|
stream_ordering = None
|
||||||
if backfilled:
|
if backfilled:
|
||||||
if not self.min_token_deferred.called:
|
if not self.min_token_deferred.called:
|
||||||
@ -83,6 +84,7 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||||||
event=event,
|
event=event,
|
||||||
backfilled=backfilled,
|
backfilled=backfilled,
|
||||||
stream_ordering=stream_ordering,
|
stream_ordering=stream_ordering,
|
||||||
|
is_new_state=is_new_state,
|
||||||
)
|
)
|
||||||
except _RollbackButIsFineException as e:
|
except _RollbackButIsFineException as e:
|
||||||
pass
|
pass
|
||||||
@ -109,12 +111,14 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||||||
defer.returnValue(event)
|
defer.returnValue(event)
|
||||||
|
|
||||||
def _persist_pdu_event_txn(self, txn, pdu=None, event=None,
|
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:
|
if pdu is not None:
|
||||||
self._persist_event_pdu_txn(txn, pdu)
|
self._persist_event_pdu_txn(txn, pdu)
|
||||||
if event is not None:
|
if event is not None:
|
||||||
return self._persist_event_txn(
|
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):
|
def _persist_event_pdu_txn(self, txn, pdu):
|
||||||
@ -141,7 +145,8 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||||||
self._update_min_depth_for_context_txn(txn, pdu.context, pdu.depth)
|
self._update_min_depth_for_context_txn(txn, pdu.context, pdu.depth)
|
||||||
|
|
||||||
@log_function
|
@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:
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
self._store_room_member_txn(txn, event)
|
self._store_room_member_txn(txn, event)
|
||||||
elif event.type == FeedbackEvent.TYPE:
|
elif event.type == FeedbackEvent.TYPE:
|
||||||
@ -195,7 +200,7 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||||||
)
|
)
|
||||||
raise _RollbackButIsFineException("_persist_event")
|
raise _RollbackButIsFineException("_persist_event")
|
||||||
|
|
||||||
if not backfilled and hasattr(event, "state_key"):
|
if is_new_state and hasattr(event, "state_key"):
|
||||||
vals = {
|
vals = {
|
||||||
"event_id": event.event_id,
|
"event_id": event.event_id,
|
||||||
"room_id": event.room_id,
|
"room_id": event.room_id,
|
||||||
|
@ -315,6 +315,10 @@ class SQLBaseStore(object):
|
|||||||
d["content"] = json.loads(d["content"])
|
d["content"] = json.loads(d["content"])
|
||||||
del d["unrecognized_keys"]
|
del d["unrecognized_keys"]
|
||||||
|
|
||||||
|
if "age_ts" not in d:
|
||||||
|
# For compatibility
|
||||||
|
d["age_ts"] = d["ts"] if "ts" in d else 0
|
||||||
|
|
||||||
return self.event_factory.create_event(
|
return self.event_factory.create_event(
|
||||||
etype=d["type"],
|
etype=d["type"],
|
||||||
**d
|
**d
|
||||||
|
71
synapse/util/emailutils.py
Normal file
71
synapse/util/emailutils.py
Normal file
@ -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
|
@ -1,6 +1,6 @@
|
|||||||
from synapse.api.ratelimiting import Ratelimiter
|
from synapse.api.ratelimiting import Ratelimiter
|
||||||
|
|
||||||
import unittest
|
from tests import unittest
|
||||||
|
|
||||||
class TestRatelimiter(unittest.TestCase):
|
class TestRatelimiter(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
from synapse.api.events import SynapseEvent
|
from synapse.api.events import SynapseEvent
|
||||||
|
|
||||||
import unittest
|
from tests import unittest
|
||||||
|
|
||||||
|
|
||||||
class SynapseTemplateCheckTestCase(unittest.TestCase):
|
class SynapseTemplateCheckTestCase(unittest.TestCase):
|
||||||
|
@ -14,11 +14,10 @@
|
|||||||
|
|
||||||
# trial imports
|
# trial imports
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
|
|
||||||
# python imports
|
# python imports
|
||||||
from mock import Mock
|
from mock import Mock, ANY
|
||||||
import logging
|
|
||||||
|
|
||||||
from ..utils import MockHttpResource, MockClock
|
from ..utils import MockHttpResource, MockClock
|
||||||
|
|
||||||
@ -28,9 +27,6 @@ from synapse.federation.units import Pdu
|
|||||||
from synapse.storage.pdu import PduTuple, PduEntry
|
from synapse.storage.pdu import PduTuple, PduEntry
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
|
|
||||||
def make_pdu(prev_pdus=[], **kwargs):
|
def make_pdu(prev_pdus=[], **kwargs):
|
||||||
"""Provide some default fields for making a PduTuple."""
|
"""Provide some default fields for making a PduTuple."""
|
||||||
pdu_fields = {
|
pdu_fields = {
|
||||||
@ -185,7 +181,8 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
"depth": 1,
|
"depth": 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
on_send_callback=ANY,
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@ -216,7 +213,9 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
"content": {"testing": "content here"},
|
"content": {"testing": "content here"},
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
})
|
},
|
||||||
|
on_send_callback=ANY,
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_recv_edu(self):
|
def test_recv_edu(self):
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
|
|
||||||
from synapse.federation.pdu_codec import (
|
from synapse.federation.pdu_codec import (
|
||||||
PduCodec, encode_event_id, decode_event_id
|
PduCodec, encode_event_id, decode_event_id
|
||||||
|
@ -14,11 +14,10 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
import logging
|
|
||||||
|
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
from synapse.http.client import HttpClient
|
from synapse.http.client import HttpClient
|
||||||
@ -26,9 +25,6 @@ from synapse.handlers.directory import DirectoryHandler
|
|||||||
from synapse.storage.directory import RoomAliasMapping
|
from synapse.storage.directory import RoomAliasMapping
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
|
|
||||||
class DirectoryHandlers(object):
|
class DirectoryHandlers(object):
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
self.directory_handler = DirectoryHandler(hs)
|
self.directory_handler = DirectoryHandler(hs)
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
|
|
||||||
from synapse.api.events.room import (
|
from synapse.api.events.room import (
|
||||||
InviteJoinEvent, MessageEvent, RoomMemberEvent
|
InviteJoinEvent, MessageEvent, RoomMemberEvent
|
||||||
@ -26,12 +26,8 @@ from synapse.federation.units import Pdu
|
|||||||
|
|
||||||
from mock import NonCallableMock, ANY
|
from mock import NonCallableMock, ANY
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from ..utils import get_mock_call_args
|
from ..utils import get_mock_call_args
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
|
|
||||||
class FederationTestCase(unittest.TestCase):
|
class FederationTestCase(unittest.TestCase):
|
||||||
|
|
||||||
@ -78,7 +74,9 @@ class FederationTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
yield self.handlers.federation_handler.on_receive_pdu(pdu, False)
|
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)
|
self.notifier.on_new_room_event.assert_called_once_with(ANY)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
@ -14,11 +14,10 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
|
|
||||||
from mock import Mock, call, ANY
|
from mock import Mock, call, ANY
|
||||||
import logging
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from ..utils import MockHttpResource, MockClock, DeferredMockCallable
|
from ..utils import MockHttpResource, MockClock, DeferredMockCallable
|
||||||
@ -34,9 +33,6 @@ UNAVAILABLE = PresenceState.UNAVAILABLE
|
|||||||
ONLINE = PresenceState.ONLINE
|
ONLINE = PresenceState.ONLINE
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
|
|
||||||
def _expect_edu(destination, edu_type, content, origin="test"):
|
def _expect_edu(destination, edu_type, content, origin="test"):
|
||||||
return {
|
return {
|
||||||
"origin": origin,
|
"origin": origin,
|
||||||
@ -92,7 +88,6 @@ class PresenceStateTestCase(unittest.TestCase):
|
|||||||
# Mock the RoomMemberHandler
|
# Mock the RoomMemberHandler
|
||||||
room_member_handler = Mock(spec=[])
|
room_member_handler = Mock(spec=[])
|
||||||
hs.handlers.room_member_handler = room_member_handler
|
hs.handlers.room_member_handler = room_member_handler
|
||||||
logging.getLogger().debug("Mocking room_member_handler=%r", room_member_handler)
|
|
||||||
|
|
||||||
# Some local users to test with
|
# Some local users to test with
|
||||||
self.u_apple = hs.parse_userid("@apple:test")
|
self.u_apple = hs.parse_userid("@apple:test")
|
||||||
@ -324,7 +319,8 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
"observer_user": "@apple:test",
|
"observer_user": "@apple:test",
|
||||||
"observed_user": "@cabbage:elsewhere",
|
"observed_user": "@cabbage:elsewhere",
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -350,7 +346,8 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
"observer_user": "@cabbage:elsewhere",
|
"observer_user": "@cabbage:elsewhere",
|
||||||
"observed_user": "@apple:test",
|
"observed_user": "@apple:test",
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -381,7 +378,8 @@ class PresenceInvitesTestCase(unittest.TestCase):
|
|||||||
"observer_user": "@cabbage:elsewhere",
|
"observer_user": "@cabbage:elsewhere",
|
||||||
"observed_user": "@durian:test",
|
"observed_user": "@durian:test",
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -770,7 +768,8 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
"last_active_ago": 0},
|
"last_active_ago": 0},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -785,7 +784,8 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
"last_active_ago": 0},
|
"last_active_ago": 0},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -911,6 +911,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -925,6 +926,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -954,6 +956,7 @@ class PresencePushTestCase(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -1150,6 +1153,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
"poll": [ "@potato:remote" ],
|
"poll": [ "@potato:remote" ],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -1162,6 +1166,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
"push": [ {"user_id": "@clementine:test" }],
|
"push": [ {"user_id": "@clementine:test" }],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -1190,6 +1195,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
"push": [ {"user_id": "@fig:test" }],
|
"push": [ {"user_id": "@fig:test" }],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -1222,6 +1228,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
"unpoll": [ "@potato:remote" ],
|
"unpoll": [ "@potato:remote" ],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -1253,6 +1260,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
|
@ -16,11 +16,10 @@
|
|||||||
"""This file contains tests of the "presence-like" data that is shared between
|
"""This file contains tests of the "presence-like" data that is shared between
|
||||||
presence and profiles; namely, the displayname and avatar_url."""
|
presence and profiles; namely, the displayname and avatar_url."""
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from mock import Mock, call, ANY
|
from mock import Mock, call, ANY
|
||||||
import logging
|
|
||||||
|
|
||||||
from ..utils import MockClock
|
from ..utils import MockClock
|
||||||
|
|
||||||
@ -35,9 +34,6 @@ UNAVAILABLE = PresenceState.UNAVAILABLE
|
|||||||
ONLINE = PresenceState.ONLINE
|
ONLINE = PresenceState.ONLINE
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
|
|
||||||
class MockReplication(object):
|
class MockReplication(object):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.edu_handlers = {}
|
self.edu_handlers = {}
|
||||||
@ -69,6 +65,8 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
"is_presence_visible",
|
"is_presence_visible",
|
||||||
|
|
||||||
"set_profile_displayname",
|
"set_profile_displayname",
|
||||||
|
|
||||||
|
"get_rooms_for_user_where_membership_is",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
resource_for_federation=Mock(),
|
resource_for_federation=Mock(),
|
||||||
@ -136,6 +134,10 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
# Remote user
|
# Remote user
|
||||||
self.u_potato = hs.parse_userid("@potato:remote")
|
self.u_potato = hs.parse_userid("@potato:remote")
|
||||||
|
|
||||||
|
self.mock_get_joined = (
|
||||||
|
self.datastore.get_rooms_for_user_where_membership_is
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_set_my_state(self):
|
def test_set_my_state(self):
|
||||||
self.presence_list = [
|
self.presence_list = [
|
||||||
@ -156,6 +158,11 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_push_local(self):
|
def test_push_local(self):
|
||||||
|
def get_joined(*args):
|
||||||
|
return defer.succeed([])
|
||||||
|
|
||||||
|
self.mock_get_joined.side_effect = get_joined
|
||||||
|
|
||||||
self.presence_list = [
|
self.presence_list = [
|
||||||
{"observed_user_id": "@banana:test"},
|
{"observed_user_id": "@banana:test"},
|
||||||
{"observed_user_id": "@clementine:test"},
|
{"observed_user_id": "@clementine:test"},
|
||||||
|
@ -14,18 +14,15 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
import logging
|
|
||||||
|
|
||||||
from synapse.api.errors import AuthError
|
from synapse.api.errors import AuthError
|
||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
from synapse.handlers.profile import ProfileHandler
|
from synapse.handlers.profile import ProfileHandler
|
||||||
|
from synapse.api.constants import Membership
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
|
|
||||||
class ProfileHandlers(object):
|
class ProfileHandlers(object):
|
||||||
@ -54,6 +51,7 @@ class ProfileTestCase(unittest.TestCase):
|
|||||||
"set_profile_displayname",
|
"set_profile_displayname",
|
||||||
"get_profile_avatar_url",
|
"get_profile_avatar_url",
|
||||||
"set_profile_avatar_url",
|
"set_profile_avatar_url",
|
||||||
|
"get_rooms_for_user_where_membership_is",
|
||||||
]),
|
]),
|
||||||
handlers=None,
|
handlers=None,
|
||||||
resource_for_federation=Mock(),
|
resource_for_federation=Mock(),
|
||||||
@ -69,6 +67,10 @@ class ProfileTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
self.handler = hs.get_handlers().profile_handler
|
self.handler = hs.get_handlers().profile_handler
|
||||||
|
|
||||||
|
self.mock_get_joined = (
|
||||||
|
self.datastore.get_rooms_for_user_where_membership_is
|
||||||
|
)
|
||||||
|
|
||||||
# TODO(paul): Icky signal declarings.. booo
|
# TODO(paul): Icky signal declarings.. booo
|
||||||
hs.get_distributor().declare("changed_presencelike_data")
|
hs.get_distributor().declare("changed_presencelike_data")
|
||||||
|
|
||||||
@ -87,8 +89,15 @@ class ProfileTestCase(unittest.TestCase):
|
|||||||
mocked_set = self.datastore.set_profile_displayname
|
mocked_set = self.datastore.set_profile_displayname
|
||||||
mocked_set.return_value = defer.succeed(())
|
mocked_set.return_value = defer.succeed(())
|
||||||
|
|
||||||
|
self.mock_get_joined.return_value = defer.succeed([])
|
||||||
|
|
||||||
yield self.handler.set_displayname(self.frank, self.frank, "Frank Jr.")
|
yield self.handler.set_displayname(self.frank, self.frank, "Frank Jr.")
|
||||||
|
|
||||||
|
self.mock_get_joined.assert_called_once_with(
|
||||||
|
self.frank.to_string(),
|
||||||
|
[Membership.JOIN]
|
||||||
|
)
|
||||||
|
|
||||||
mocked_set.assert_called_with("1234ABCD", "Frank Jr.")
|
mocked_set.assert_called_with("1234ABCD", "Frank Jr.")
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
@ -139,7 +148,15 @@ class ProfileTestCase(unittest.TestCase):
|
|||||||
mocked_set = self.datastore.set_profile_avatar_url
|
mocked_set = self.datastore.set_profile_avatar_url
|
||||||
mocked_set.return_value = defer.succeed(())
|
mocked_set.return_value = defer.succeed(())
|
||||||
|
|
||||||
|
self.mock_get_joined.return_value = defer.succeed([])
|
||||||
|
|
||||||
yield self.handler.set_avatar_url(self.frank, self.frank,
|
yield self.handler.set_avatar_url(self.frank, self.frank,
|
||||||
"http://my.server/pic.gif")
|
"http://my.server/pic.gif")
|
||||||
|
|
||||||
|
self.mock_get_joined.assert_called_once_with(
|
||||||
|
self.frank.to_string(),
|
||||||
|
[Membership.JOIN]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
mocked_set.assert_called_with("1234ABCD", "http://my.server/pic.gif")
|
mocked_set.assert_called_with("1234ABCD", "http://my.server/pic.gif")
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
|
|
||||||
from synapse.api.events.room import (
|
from synapse.api.events.room import (
|
||||||
InviteJoinEvent, RoomMemberEvent, RoomConfigEvent
|
InviteJoinEvent, RoomMemberEvent, RoomConfigEvent
|
||||||
@ -27,10 +27,6 @@ from synapse.server import HomeServer
|
|||||||
|
|
||||||
from mock import Mock, NonCallableMock
|
from mock import Mock, NonCallableMock
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
|
|
||||||
class RoomMemberHandlerTestCase(unittest.TestCase):
|
class RoomMemberHandlerTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -14,12 +14,11 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from mock import Mock, call, ANY
|
from mock import Mock, call, ANY
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
|
|
||||||
from ..utils import MockHttpResource, MockClock, DeferredMockCallable
|
from ..utils import MockHttpResource, MockClock, DeferredMockCallable
|
||||||
|
|
||||||
@ -27,9 +26,6 @@ from synapse.server import HomeServer
|
|||||||
from synapse.handlers.typing import TypingNotificationHandler
|
from synapse.handlers.typing import TypingNotificationHandler
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
|
|
||||||
def _expect_edu(destination, edu_type, content, origin="test"):
|
def _expect_edu(destination, edu_type, content, origin="test"):
|
||||||
return {
|
return {
|
||||||
"origin": origin,
|
"origin": origin,
|
||||||
@ -173,7 +169,8 @@ class TypingNotificationsTestCase(unittest.TestCase):
|
|||||||
"user_id": self.u_apple.to_string(),
|
"user_id": self.u_apple.to_string(),
|
||||||
"typing": True,
|
"typing": True,
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
@ -223,7 +220,8 @@ class TypingNotificationsTestCase(unittest.TestCase):
|
|||||||
"user_id": self.u_apple.to_string(),
|
"user_id": self.u_apple.to_string(),
|
||||||
"typing": False,
|
"typing": False,
|
||||||
}
|
}
|
||||||
)
|
),
|
||||||
|
on_send_callback=ANY,
|
||||||
),
|
),
|
||||||
defer.succeed((200, "OK"))
|
defer.succeed((200, "OK"))
|
||||||
)
|
)
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
""" Tests REST events for /events paths."""
|
""" Tests REST events for /events paths."""
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
|
|
||||||
# twisted imports
|
# twisted imports
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
@ -27,14 +27,12 @@ from synapse.server import HomeServer
|
|||||||
|
|
||||||
# python imports
|
# python imports
|
||||||
import json
|
import json
|
||||||
import logging
|
|
||||||
|
|
||||||
from ..utils import MockHttpResource, MemoryDataStore
|
from ..utils import MockHttpResource, MemoryDataStore
|
||||||
from .utils import RestTestCase
|
from .utils import RestTestCase
|
||||||
|
|
||||||
from mock import Mock, NonCallableMock
|
from mock import Mock, NonCallableMock
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
PATH_PREFIX = "/_matrix/client/api/v1"
|
PATH_PREFIX = "/_matrix/client/api/v1"
|
||||||
|
|
||||||
|
@ -15,11 +15,10 @@
|
|||||||
|
|
||||||
"""Tests REST events for /presence paths."""
|
"""Tests REST events for /presence paths."""
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
import logging
|
|
||||||
|
|
||||||
from ..utils import MockHttpResource
|
from ..utils import MockHttpResource
|
||||||
|
|
||||||
@ -28,9 +27,6 @@ from synapse.handlers.presence import PresenceHandler
|
|||||||
from synapse.server import HomeServer
|
from synapse.server import HomeServer
|
||||||
|
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
|
||||||
|
|
||||||
|
|
||||||
OFFLINE = PresenceState.OFFLINE
|
OFFLINE = PresenceState.OFFLINE
|
||||||
UNAVAILABLE = PresenceState.UNAVAILABLE
|
UNAVAILABLE = PresenceState.UNAVAILABLE
|
||||||
ONLINE = PresenceState.ONLINE
|
ONLINE = PresenceState.ONLINE
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
"""Tests REST events for /profile paths."""
|
"""Tests REST events for /profile paths."""
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
@ -28,6 +28,7 @@ from synapse.server import HomeServer
|
|||||||
myid = "@1234ABCD:test"
|
myid = "@1234ABCD:test"
|
||||||
PATH_PREFIX = "/_matrix/client/api/v1"
|
PATH_PREFIX = "/_matrix/client/api/v1"
|
||||||
|
|
||||||
|
|
||||||
class ProfileTestCase(unittest.TestCase):
|
class ProfileTestCase(unittest.TestCase):
|
||||||
""" Tests profile management. """
|
""" Tests profile management. """
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
# trial imports
|
# trial imports
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
|
|
||||||
from synapse.api.constants import Membership
|
from synapse.api.constants import Membership
|
||||||
|
|
||||||
@ -95,8 +95,14 @@ class RestTestCase(unittest.TestCase):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def register(self, user_id):
|
def register(self, user_id):
|
||||||
(code, response) = yield self.mock_resource.trigger("POST", "/register",
|
(code, response) = yield self.mock_resource.trigger(
|
||||||
'{"user_id":"%s"}' % user_id)
|
"POST",
|
||||||
|
"/register",
|
||||||
|
json.dumps({
|
||||||
|
"user": user_id,
|
||||||
|
"password": "test",
|
||||||
|
"type": "m.login.password"
|
||||||
|
}))
|
||||||
self.assertEquals(200, code)
|
self.assertEquals(200, code)
|
||||||
defer.returnValue(response)
|
defer.returnValue(response)
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from mock import Mock, call
|
from mock import Mock, call
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from tests import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.trial import unittest
|
|
||||||
|
|
||||||
from mock import Mock, patch
|
from mock import Mock, patch
|
||||||
|
|
||||||
|
@ -13,8 +13,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
|
from tests import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.trial import unittest
|
|
||||||
from twisted.python.log import PythonLoggingObserver
|
from twisted.python.log import PythonLoggingObserver
|
||||||
|
|
||||||
from synapse.state import StateHandler
|
from synapse.state import StateHandler
|
||||||
@ -26,7 +26,6 @@ from collections import namedtuple
|
|||||||
|
|
||||||
from mock import Mock
|
from mock import Mock
|
||||||
|
|
||||||
import logging
|
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import unittest
|
from tests import unittest
|
||||||
|
|
||||||
from synapse.server import BaseHomeServer
|
from synapse.server import BaseHomeServer
|
||||||
from synapse.types import UserID, RoomAlias
|
from synapse.types import UserID, RoomAlias
|
||||||
|
79
tests/unittest.py
Normal file
79
tests/unittest.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# -*- 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 twisted.trial import unittest
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
|
# logging doesn't have a "don't log anything at all EVARRRR setting,
|
||||||
|
# but since the highest value is 50, 1000000 should do ;)
|
||||||
|
NEVER = 1000000
|
||||||
|
|
||||||
|
logging.getLogger().addHandler(logging.StreamHandler())
|
||||||
|
logging.getLogger().setLevel(NEVER)
|
||||||
|
|
||||||
|
|
||||||
|
def around(target):
|
||||||
|
"""A CLOS-style 'around' modifier, which wraps the original method of the
|
||||||
|
given instance with another piece of code.
|
||||||
|
|
||||||
|
@around(self)
|
||||||
|
def method_name(orig, *args, **kwargs):
|
||||||
|
return orig(*args, **kwargs)
|
||||||
|
"""
|
||||||
|
def _around(code):
|
||||||
|
name = code.__name__
|
||||||
|
orig = getattr(target, name)
|
||||||
|
def new(*args, **kwargs):
|
||||||
|
return code(orig, *args, **kwargs)
|
||||||
|
setattr(target, name, new)
|
||||||
|
return _around
|
||||||
|
|
||||||
|
|
||||||
|
class TestCase(unittest.TestCase):
|
||||||
|
"""A subclass of twisted.trial's TestCase which looks for 'loglevel'
|
||||||
|
attributes on both itself and its individual test methods, to override the
|
||||||
|
root logger's logging level while that test (case|method) runs."""
|
||||||
|
|
||||||
|
def __init__(self, methodName, *args, **kwargs):
|
||||||
|
super(TestCase, self).__init__(methodName, *args, **kwargs)
|
||||||
|
|
||||||
|
method = getattr(self, methodName)
|
||||||
|
|
||||||
|
level = getattr(method, "loglevel",
|
||||||
|
getattr(self, "loglevel",
|
||||||
|
NEVER))
|
||||||
|
|
||||||
|
@around(self)
|
||||||
|
def setUp(orig):
|
||||||
|
old_level = logging.getLogger().level
|
||||||
|
|
||||||
|
if old_level != level:
|
||||||
|
@around(self)
|
||||||
|
def tearDown(orig):
|
||||||
|
ret = orig()
|
||||||
|
logging.getLogger().setLevel(old_level)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
logging.getLogger().setLevel(level)
|
||||||
|
return orig()
|
||||||
|
|
||||||
|
|
||||||
|
def DEBUG(target):
|
||||||
|
"""A decorator to set the .loglevel attribute to logging.DEBUG.
|
||||||
|
Can apply to either a TestCase or an individual test method."""
|
||||||
|
target.loglevel = logging.DEBUG
|
||||||
|
return target
|
@ -15,7 +15,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
from twisted.trial import unittest
|
from tests import unittest
|
||||||
|
|
||||||
from synapse.util.lockutils import LockManager
|
from synapse.util.lockutils import LockManager
|
||||||
|
|
||||||
|
@ -130,6 +130,10 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
|
|||||||
angular.element('#ringAudio')[0].pause();
|
angular.element('#ringAudio')[0].pause();
|
||||||
angular.element('#ringbackAudio')[0].pause();
|
angular.element('#ringbackAudio')[0].pause();
|
||||||
angular.element('#busyAudio')[0].play();
|
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') {
|
} else if (oldVal == 'invite_sent') {
|
||||||
angular.element('#ringbackAudio')[0].pause();
|
angular.element('#ringbackAudio')[0].pause();
|
||||||
} else if (oldVal == 'ringing') {
|
} else if (oldVal == 'ringing') {
|
||||||
|
@ -528,9 +528,8 @@ a:active { color: #000; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.bubble .message {
|
.bubble .message {
|
||||||
/* Break lines when encountering CR+LF */
|
/* Wrap words and break lines on CR+LF */
|
||||||
/* FIXME: this breaks wordwrapping. We need to s#CRLF#<br/>#g instead */
|
white-space: pre-wrap;
|
||||||
/* white-space: pre; */
|
|
||||||
}
|
}
|
||||||
.bubble .messagePending {
|
.bubble .messagePending {
|
||||||
opacity: 0.3
|
opacity: 0.3
|
||||||
@ -539,6 +538,10 @@ a:active { color: #000; }
|
|||||||
color: #F00;
|
color: #F00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.messageBing {
|
||||||
|
color: #00F;
|
||||||
|
}
|
||||||
|
|
||||||
#room-fullscreen-image {
|
#room-fullscreen-image {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
@ -600,7 +603,11 @@ a:active { color: #000; }
|
|||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.recentsRoomSummaryTS {
|
.recentsPublicRoom {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recentsRoomSummaryUsersCount, .recentsRoomSummaryTS {
|
||||||
color: #888;
|
color: #888;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
width: 7em;
|
width: 7em;
|
||||||
@ -613,6 +620,11 @@ a:active { color: #000; }
|
|||||||
padding-bottom: 5px;
|
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 ***/
|
/*** Recents in the room page ***/
|
||||||
|
|
||||||
#roomRecentsTableWrapper {
|
#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.
|
if typically all the $on method would do is update its own $scope.
|
||||||
*/
|
*/
|
||||||
angular.module('eventHandlerService', [])
|
angular.module('eventHandlerService', [])
|
||||||
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
|
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', '$timeout', 'mPresence',
|
||||||
|
function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||||
var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
|
var ROOM_CREATE_EVENT = "ROOM_CREATE_EVENT";
|
||||||
var MSG_EVENT = "MSG_EVENT";
|
var MSG_EVENT = "MSG_EVENT";
|
||||||
var MEMBER_EVENT = "MEMBER_EVENT";
|
var MEMBER_EVENT = "MEMBER_EVENT";
|
||||||
@ -38,6 +39,51 @@ angular.module('eventHandlerService', [])
|
|||||||
var TOPIC_EVENT = "TOPIC_EVENT";
|
var TOPIC_EVENT = "TOPIC_EVENT";
|
||||||
var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted
|
var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted
|
||||||
|
|
||||||
|
// used for dedupping events - could be expanded in future...
|
||||||
|
// FIXME: means that we leak memory over time (along with lots of the rest
|
||||||
|
// of the app, given we never try to reap memory yet)
|
||||||
|
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;
|
var initialSyncDeferred;
|
||||||
|
|
||||||
var reset = function() {
|
var reset = function() {
|
||||||
@ -46,26 +92,24 @@ angular.module('eventHandlerService', [])
|
|||||||
$rootScope.events = {
|
$rootScope.events = {
|
||||||
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
|
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
|
||||||
};
|
};
|
||||||
}
|
|
||||||
reset();
|
|
||||||
|
|
||||||
// used for dedupping events - could be expanded in future...
|
|
||||||
// FIXME: means that we leak memory over time (along with lots of the rest
|
|
||||||
// of the app, given we never try to reap memory yet)
|
|
||||||
var eventMap = {};
|
|
||||||
|
|
||||||
$rootScope.presence = {};
|
$rootScope.presence = {};
|
||||||
|
|
||||||
|
eventMap = {};
|
||||||
|
};
|
||||||
|
reset();
|
||||||
|
|
||||||
var initRoom = function(room_id) {
|
var initRoom = function(room_id) {
|
||||||
if (!(room_id in $rootScope.events.rooms)) {
|
if (!(room_id in $rootScope.events.rooms)) {
|
||||||
console.log("Creating new handler entry for " + room_id);
|
console.log("Creating new handler entry for " + room_id);
|
||||||
$rootScope.events.rooms[room_id] = {};
|
$rootScope.events.rooms[room_id] = {
|
||||||
$rootScope.events.rooms[room_id].messages = [];
|
room_id: room_id,
|
||||||
$rootScope.events.rooms[room_id].members = {};
|
messages: [],
|
||||||
|
members: {},
|
||||||
// Pagination information
|
// Pagination information
|
||||||
$rootScope.events.rooms[room_id].pagination = {
|
pagination: {
|
||||||
earliest_token: "END" // how far back we've paginated
|
earliest_token: "END" // how far back we've paginated
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -132,6 +176,48 @@ angular.module('eventHandlerService', [])
|
|||||||
else {
|
else {
|
||||||
$rootScope.events.rooms[event.room_id].messages.push(event);
|
$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 message = event.content.body;
|
||||||
|
if (event.content.msgtype === "m.emote") {
|
||||||
|
message = "* " + displayname + " " + message;
|
||||||
|
}
|
||||||
|
|
||||||
|
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": message,
|
||||||
|
"icon": member ? member.avatar_url : undefined
|
||||||
|
});
|
||||||
|
$timeout(function() {
|
||||||
|
notification.close();
|
||||||
|
}, 5 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$rootScope.events.rooms[event.room_id].messages.unshift(event);
|
$rootScope.events.rooms[event.room_id].messages.unshift(event);
|
||||||
@ -157,8 +243,9 @@ angular.module('eventHandlerService', [])
|
|||||||
// add membership changes as if they were a room message if something interesting changed
|
// add membership changes as if they were a room message if something interesting changed
|
||||||
// Exception: Do not do this if the event is a room state event because such events already come
|
// Exception: Do not do this if the event is a room state event because such events already come
|
||||||
// as room messages events. Moreover, when they come as room messages events, they are relatively ordered
|
// as room messages events. Moreover, when they come as room messages events, they are relatively ordered
|
||||||
// with other other room messages
|
// with other other room messages XXX This is no longer true, you only get a single event, not a room message event.
|
||||||
if (event.content.prev !== event.content.membership && !isStateEvent) {
|
// FIXME: This possibly reintroduces multiple join messages.
|
||||||
|
if (event.content.prev !== event.content.membership) { // && !isStateEvent
|
||||||
if (isLiveEvent) {
|
if (isLiveEvent) {
|
||||||
$rootScope.events.rooms[event.room_id].messages.push(event);
|
$rootScope.events.rooms[event.room_id].messages.push(event);
|
||||||
}
|
}
|
||||||
@ -204,7 +291,7 @@ angular.module('eventHandlerService', [])
|
|||||||
|
|
||||||
var handleCallEvent = function(event, isLiveEvent) {
|
var handleCallEvent = function(event, isLiveEvent) {
|
||||||
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
|
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
|
||||||
if (event.type == 'm.call.invite') {
|
if (event.type === 'm.call.invite') {
|
||||||
$rootScope.events.rooms[event.room_id].messages.push(event);
|
$rootScope.events.rooms[event.room_id].messages.push(event);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -231,7 +318,7 @@ angular.module('eventHandlerService', [])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return index;
|
return index;
|
||||||
}
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
|
ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
|
||||||
@ -253,7 +340,9 @@ angular.module('eventHandlerService', [])
|
|||||||
|
|
||||||
// FIXME: /initialSync on a particular room is not yet available
|
// 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
|
// So initRoom on a new room is not called. Make sure the room data is initialised here
|
||||||
|
if (event.room_id) {
|
||||||
initRoom(event.room_id);
|
initRoom(event.room_id);
|
||||||
|
}
|
||||||
|
|
||||||
// Avoid duplicated events
|
// Avoid duplicated events
|
||||||
// Needed for rooms where initialSync has not been done.
|
// Needed for rooms where initialSync has not been done.
|
||||||
@ -287,6 +376,7 @@ angular.module('eventHandlerService', [])
|
|||||||
handleMessage(event, isLiveEvent);
|
handleMessage(event, isLiveEvent);
|
||||||
break;
|
break;
|
||||||
case "m.room.member":
|
case "m.room.member":
|
||||||
|
isStateEvent = true;
|
||||||
handleRoomMember(event, isLiveEvent, isStateEvent);
|
handleRoomMember(event, isLiveEvent, isStateEvent);
|
||||||
break;
|
break;
|
||||||
case "m.presence":
|
case "m.presence":
|
||||||
@ -316,19 +406,39 @@ angular.module('eventHandlerService', [])
|
|||||||
// isLiveEvents determines whether notifications should be shown, whether
|
// isLiveEvents determines whether notifications should be shown, whether
|
||||||
// messages get appended to the start/end of lists, etc.
|
// messages get appended to the start/end of lists, etc.
|
||||||
handleEvents: function(events, isLiveEvents, isStateEvents) {
|
handleEvents: function(events, isLiveEvents, isStateEvents) {
|
||||||
|
// XXX FIXME TODO: isStateEvents is being left as undefined sometimes. It makes no sense
|
||||||
|
// to have isStateEvents as an arg, since things like m.room.member are ALWAYS state events.
|
||||||
for (var i=0; i<events.length; i++) {
|
for (var i=0; i<events.length; i++) {
|
||||||
this.handleEvent(events[i], isLiveEvents, isStateEvents);
|
this.handleEvent(events[i], isLiveEvents, isStateEvents);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Handle messages from /initialSync or /messages
|
// Handle messages from /initialSync or /messages
|
||||||
handleRoomMessages: function(room_id, messages, isLiveEvents) {
|
handleRoomMessages: function(room_id, messages, isLiveEvents, dir) {
|
||||||
initRoom(room_id);
|
initRoom(room_id);
|
||||||
this.handleEvents(messages.chunk, isLiveEvents);
|
|
||||||
|
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++) {
|
||||||
|
// FIXME: Being live != being state
|
||||||
|
this.handleEvent(events[i], isLiveEvents, isLiveEvents);
|
||||||
|
}
|
||||||
|
|
||||||
// Store how far back we've paginated
|
// 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;
|
$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--) {
|
||||||
|
// FIXME: Being live != being state
|
||||||
|
this.handleEvent(events[i], isLiveEvents, isLiveEvents);
|
||||||
|
}
|
||||||
|
// Store where to start pagination
|
||||||
|
$rootScope.events.rooms[room_id].pagination.earliest_token = messages.start;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleInitialSyncDone: function(initialSyncData) {
|
handleInitialSyncDone: function(initialSyncData) {
|
||||||
@ -343,6 +453,82 @@ angular.module('eventHandlerService', [])
|
|||||||
|
|
||||||
resetRoomMessages: function(room_id) {
|
resetRoomMessages: function(room_id) {
|
||||||
resetRoomMessages(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;
|
||||||
|
},
|
||||||
|
|
||||||
|
setRoomVisibility: function(room_id, visible) {
|
||||||
|
if (!visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
initRoom(room_id);
|
||||||
|
|
||||||
|
var room = $rootScope.events.rooms[room_id];
|
||||||
|
if (room) {
|
||||||
|
room.visibility = visible;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
|
@ -104,8 +104,10 @@ angular.module('eventStreamService', [])
|
|||||||
settings.isActive = true;
|
settings.isActive = true;
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
|
|
||||||
// Initial sync: get all information and the last message of all rooms of the user
|
// Initial sync: get all information and the last 30 messages of all rooms of the user
|
||||||
matrixService.initialSync(1, false).then(
|
// 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) {
|
function(response) {
|
||||||
var rooms = response.data.rooms;
|
var rooms = response.data.rooms;
|
||||||
for (var i = 0; i < rooms.length; ++i) {
|
for (var i = 0; i < rooms.length; ++i) {
|
||||||
@ -118,6 +120,8 @@ angular.module('eventStreamService', [])
|
|||||||
if ("state" in room) {
|
if ("state" in room) {
|
||||||
eventHandlerService.handleEvents(room.state, false, true);
|
eventHandlerService.handleEvents(room.state, false, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
eventHandlerService.setRoomVisibility(room.room_id, room.visibility);
|
||||||
}
|
}
|
||||||
|
|
||||||
var presence = response.data.presence;
|
var presence = response.data.presence;
|
||||||
|
@ -47,13 +47,19 @@ angular.module('MatrixCall', [])
|
|||||||
this.call_id = "c" + new Date().getTime();
|
this.call_id = "c" + new Date().getTime();
|
||||||
this.state = 'fledgling';
|
this.state = 'fledgling';
|
||||||
this.didConnect = false;
|
this.didConnect = false;
|
||||||
|
|
||||||
|
// a queue for candidates waiting to go out. We try to amalgamate candidates into a single candidate message where possible
|
||||||
|
this.candidateSendQueue = [];
|
||||||
|
this.candidateSendTries = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MatrixCall.CALL_TIMEOUT = 60000;
|
||||||
|
|
||||||
MatrixCall.prototype.createPeerConnection = function() {
|
MatrixCall.prototype.createPeerConnection = function() {
|
||||||
var stunServer = 'stun:stun.l.google.com:19302';
|
var stunServer = 'stun:stun.l.google.com:19302';
|
||||||
var pc;
|
var pc;
|
||||||
if (window.mozRTCPeerConnection) {
|
if (window.mozRTCPeerConnection) {
|
||||||
pc = window.mozRTCPeerConnection({'url': stunServer});
|
pc = new window.mozRTCPeerConnection({'url': stunServer});
|
||||||
} else {
|
} else {
|
||||||
pc = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]});
|
pc = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]});
|
||||||
}
|
}
|
||||||
@ -74,12 +80,30 @@ angular.module('MatrixCall', [])
|
|||||||
this.config = config;
|
this.config = config;
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixCall.prototype.initWithInvite = function(msg) {
|
MatrixCall.prototype.initWithInvite = function(event) {
|
||||||
this.msg = msg;
|
this.msg = event.content;
|
||||||
this.peerConn = this.createPeerConnection();
|
this.peerConn = this.createPeerConnection();
|
||||||
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
|
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
|
||||||
this.state = 'ringing';
|
this.state = 'ringing';
|
||||||
this.direction = 'inbound';
|
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() {
|
MatrixCall.prototype.answer = function() {
|
||||||
@ -174,12 +198,7 @@ angular.module('MatrixCall', [])
|
|||||||
MatrixCall.prototype.gotLocalIceCandidate = function(event) {
|
MatrixCall.prototype.gotLocalIceCandidate = function(event) {
|
||||||
console.log(event);
|
console.log(event);
|
||||||
if (event.candidate) {
|
if (event.candidate) {
|
||||||
var content = {
|
this.sendCandidate(event.candidate);
|
||||||
version: 0,
|
|
||||||
call_id: this.call_id,
|
|
||||||
candidate: event.candidate
|
|
||||||
};
|
|
||||||
this.sendEventWithRetry('m.call.candidate', content);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,14 +208,12 @@ angular.module('MatrixCall', [])
|
|||||||
console.log("Ignoring remote ICE candidate because call has ended");
|
console.log("Ignoring remote ICE candidate because call has ended");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var candidateObject = new RTCIceCandidate({
|
this.peerConn.addIceCandidate(new RTCIceCandidate(cand), function() {}, function(e) {});
|
||||||
sdpMLineIndex: cand.label,
|
|
||||||
candidate: cand.candidate
|
|
||||||
});
|
|
||||||
this.peerConn.addIceCandidate(candidateObject, function() {}, function(e) {});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixCall.prototype.receivedAnswer = function(msg) {
|
MatrixCall.prototype.receivedAnswer = function(msg) {
|
||||||
|
if (this.state == 'ended') return;
|
||||||
|
|
||||||
this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
|
this.peerConn.setRemoteDescription(new RTCSessionDescription(msg.answer), this.onSetRemoteDescriptionSuccess, this.onSetRemoteDescriptionError);
|
||||||
this.state = 'connecting';
|
this.state = 'connecting';
|
||||||
};
|
};
|
||||||
@ -214,11 +231,19 @@ angular.module('MatrixCall', [])
|
|||||||
var content = {
|
var content = {
|
||||||
version: 0,
|
version: 0,
|
||||||
call_id: this.call_id,
|
call_id: this.call_id,
|
||||||
offer: description
|
offer: description,
|
||||||
|
lifetime: MatrixCall.CALL_TIMEOUT
|
||||||
};
|
};
|
||||||
this.sendEventWithRetry('m.call.invite', content);
|
this.sendEventWithRetry('m.call.invite', content);
|
||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
|
$timeout(function() {
|
||||||
|
if (self.state == 'invite_sent') {
|
||||||
|
self.hangupReason = 'invite_timeout';
|
||||||
|
self.hangup();
|
||||||
|
}
|
||||||
|
}, MatrixCall.CALL_TIMEOUT);
|
||||||
|
|
||||||
$rootScope.$apply(function() {
|
$rootScope.$apply(function() {
|
||||||
self.state = 'invite_sent';
|
self.state = 'invite_sent';
|
||||||
});
|
});
|
||||||
@ -370,5 +395,53 @@ angular.module('MatrixCall', [])
|
|||||||
}, delayMs);
|
}, delayMs);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sends candidates with are sent in a special way because we try to amalgamate them into one message
|
||||||
|
MatrixCall.prototype.sendCandidate = function(content) {
|
||||||
|
this.candidateSendQueue.push(content);
|
||||||
|
var self = this;
|
||||||
|
if (this.candidateSendTries == 0) $timeout(function() { self.sendCandidateQueue(); }, 100);
|
||||||
|
};
|
||||||
|
|
||||||
|
MatrixCall.prototype.sendCandidateQueue = function(content) {
|
||||||
|
if (this.candidateSendQueue.length == 0) return;
|
||||||
|
|
||||||
|
var cands = this.candidateSendQueue;
|
||||||
|
this.candidateSendQueue = [];
|
||||||
|
++this.candidateSendTries;
|
||||||
|
var content = {
|
||||||
|
version: 0,
|
||||||
|
call_id: this.call_id,
|
||||||
|
candidates: cands
|
||||||
|
};
|
||||||
|
var self = this;
|
||||||
|
console.log("Attempting to send "+cands.length+" candidates");
|
||||||
|
matrixService.sendEvent(self.room_id, 'm.call.candidates', undefined, content).then(function() { self.candsSent(); }, function(error) { self.candsSendFailed(cands, error); } );
|
||||||
|
};
|
||||||
|
|
||||||
|
MatrixCall.prototype.candsSent = function() {
|
||||||
|
this.candidateSendTries = 0;
|
||||||
|
this.sendCandidateQueue();
|
||||||
|
};
|
||||||
|
|
||||||
|
MatrixCall.prototype.candsSendFailed = function(cands, error) {
|
||||||
|
for (var i = 0; i < cands.length; ++i) {
|
||||||
|
this.candidateSendQueue.push(cands[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.candidateSendTries > 5) {
|
||||||
|
console.log("Failed to send candidates on attempt "+ev.tries+". Giving up for now.");
|
||||||
|
this.candidateSendTries = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var delayMs = 500 * Math.pow(2, this.candidateSendTries);
|
||||||
|
++this.candidateSendTries;
|
||||||
|
console.log("Failed to send candidates. Retrying in "+delayMs+"ms");
|
||||||
|
var self = this;
|
||||||
|
$timeout(function() {
|
||||||
|
self.sendCandidateQueue();
|
||||||
|
}, delayMs);
|
||||||
|
};
|
||||||
|
|
||||||
return MatrixCall;
|
return MatrixCall;
|
||||||
}]);
|
}]);
|
||||||
|
@ -26,11 +26,6 @@ angular.module('matrixFilter', [])
|
|||||||
// If there is an alias, use it
|
// If there is an alias, use it
|
||||||
// TODO: only one alias is managed for now
|
// TODO: only one alias is managed for now
|
||||||
var alias = matrixService.getRoomIdToAliasMapping(room_id);
|
var alias = matrixService.getRoomIdToAliasMapping(room_id);
|
||||||
if (alias) {
|
|
||||||
roomName = alias;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (undefined === roomName) {
|
|
||||||
|
|
||||||
var room = $rootScope.events.rooms[room_id];
|
var room = $rootScope.events.rooms[room_id];
|
||||||
if (room) {
|
if (room) {
|
||||||
@ -39,6 +34,9 @@ angular.module('matrixFilter', [])
|
|||||||
if (room_name_event) {
|
if (room_name_event) {
|
||||||
roomName = room_name_event.content.name;
|
roomName = room_name_event.content.name;
|
||||||
}
|
}
|
||||||
|
else if (alias) {
|
||||||
|
roomName = alias;
|
||||||
|
}
|
||||||
else if (room.members) {
|
else if (room.members) {
|
||||||
// Else, build the name from its users
|
// Else, build the name from its users
|
||||||
// FIXME: Is it still required?
|
// FIXME: Is it still required?
|
||||||
@ -90,6 +88,10 @@ angular.module('matrixFilter', [])
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always show the alias in the room displayed name
|
||||||
|
if (roomName && alias && alias !== roomName) {
|
||||||
|
roomName += " (" + alias + ")";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (undefined === roomName) {
|
if (undefined === roomName) {
|
||||||
|
@ -24,22 +24,52 @@ angular.module('matrixPhoneService', [])
|
|||||||
matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT";
|
matrixPhoneService.INCOMING_CALL_EVENT = "INCOMING_CALL_EVENT";
|
||||||
matrixPhoneService.REPLACED_CALL_EVENT = "REPLACED_CALL_EVENT";
|
matrixPhoneService.REPLACED_CALL_EVENT = "REPLACED_CALL_EVENT";
|
||||||
matrixPhoneService.allCalls = {};
|
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.callPlaced = function(call) {
|
||||||
matrixPhoneService.allCalls[call.call_id] = call;
|
matrixPhoneService.allCalls[call.call_id] = call;
|
||||||
};
|
};
|
||||||
|
|
||||||
$rootScope.$on(eventHandlerService.CALL_EVENT, function(ngEvent, event, isLive) {
|
$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;
|
if (event.user_id == matrixService.config().user_id) return;
|
||||||
|
|
||||||
var msg = event.content;
|
var msg = event.content;
|
||||||
|
|
||||||
if (event.type == 'm.call.invite') {
|
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 MatrixCall = $injector.get('MatrixCall');
|
||||||
var call = new MatrixCall(event.room_id);
|
var call = new MatrixCall(event.room_id);
|
||||||
call.call_id = msg.call_id;
|
call.call_id = msg.call_id;
|
||||||
call.initWithInvite(msg);
|
call.initWithInvite(event);
|
||||||
matrixPhoneService.allCalls[call.call_id] = call;
|
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)?
|
// Were we trying to call that user (room)?
|
||||||
var existingCall;
|
var existingCall;
|
||||||
var callIds = Object.keys(matrixPhoneService.allCalls);
|
var callIds = Object.keys(matrixPhoneService.allCalls);
|
||||||
@ -77,22 +107,38 @@ angular.module('matrixPhoneService', [])
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
call.receivedAnswer(msg);
|
call.receivedAnswer(msg);
|
||||||
} else if (event.type == 'm.call.candidate') {
|
} else if (event.type == 'm.call.candidates') {
|
||||||
var call = matrixPhoneService.allCalls[msg.call_id];
|
var call = matrixPhoneService.allCalls[msg.call_id];
|
||||||
if (!call) {
|
if (!call && isLive) {
|
||||||
console.log("Got candidate for unknown call ID "+msg.call_id);
|
console.log("Got candidates for unknown call ID "+msg.call_id);
|
||||||
return;
|
return;
|
||||||
|
} 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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
call.gotRemoteIceCandidate(msg.candidate);
|
|
||||||
} else if (event.type == 'm.call.hangup') {
|
} else if (event.type == 'm.call.hangup') {
|
||||||
var call = matrixPhoneService.allCalls[msg.call_id];
|
var call = matrixPhoneService.allCalls[msg.call_id];
|
||||||
if (!call) {
|
if (!call && isLive) {
|
||||||
console.log("Got hangup for unknown call ID "+msg.call_id);
|
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();
|
call.onHangupReceived();
|
||||||
delete(matrixPhoneService.allCalls[msg.call_id]);
|
delete(matrixPhoneService.allCalls[msg.call_id]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return matrixPhoneService;
|
return matrixPhoneService;
|
||||||
|
@ -82,37 +82,154 @@ angular.module('matrixService', [])
|
|||||||
return $http(request);
|
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 {
|
return {
|
||||||
/****** Home server API ******/
|
/****** Home server API ******/
|
||||||
prefix: prefixPath,
|
prefix: prefixPath,
|
||||||
|
|
||||||
// Register an user
|
// Register an user
|
||||||
register: function(user_name, password, threepidCreds, useCaptcha) {
|
register: function(user_name, password, threepidCreds, useCaptcha) {
|
||||||
// The REST path spec
|
// 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 path = "/register";
|
||||||
|
|
||||||
var data = {
|
// check we can actually register with this HS.
|
||||||
user_id: user_name,
|
doRequest("GET", path, undefined, undefined).then(
|
||||||
password: password,
|
function(response) {
|
||||||
threepidCreds: threepidCreds
|
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;
|
||||||
|
|
||||||
if (useCaptcha) {
|
for (var i=0; i<flows.length; i++) {
|
||||||
// Not all home servers will require captcha on signup, but if this flag is checked,
|
var isThreePidFlow = false;
|
||||||
// send captcha information.
|
if (flows[i].stages) {
|
||||||
// TODO: Might be nice to make this a bit more flexible..
|
for (var j=0; j<flows[i].stages.length; j++) {
|
||||||
var challengeToken = Recaptcha.get_challenge();
|
var regType = flows[i].stages[j];
|
||||||
var captchaEntry = Recaptcha.get_response();
|
if (knownTypes.indexOf(regType) === -1) {
|
||||||
var captchaType = "m.login.recaptcha";
|
deferred.reject("Unknown type: "+regType);
|
||||||
|
return;
|
||||||
data.captcha = {
|
}
|
||||||
type: captchaType,
|
if (regType == "m.login.email.identity") {
|
||||||
challenge: challengeToken,
|
isThreePidFlow = true;
|
||||||
response: captchaEntry
|
}
|
||||||
};
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return doRequest("POST", path, undefined, data);
|
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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
},
|
},
|
||||||
|
|
||||||
// Create a room
|
// Create a room
|
||||||
|
@ -53,6 +53,8 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
|
|||||||
|
|
||||||
// Add room_alias & room_display_name members
|
// Add room_alias & room_display_name members
|
||||||
angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
|
angular.extend(room, matrixService.getRoomAliasAndDisplayName(room));
|
||||||
|
|
||||||
|
eventHandlerService.setRoomVisibility(room.room_id, "public");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@ -117,6 +119,10 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
|
|||||||
matrixService.getDisplayName($scope.config.user_id).then(
|
matrixService.getDisplayName($scope.config.user_id).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
$scope.profile.displayName = response.data.displayname;
|
$scope.profile.displayName = response.data.displayname;
|
||||||
|
var config = matrixService.config();
|
||||||
|
config.display_name = response.data.displayname;
|
||||||
|
matrixService.setConfig(config);
|
||||||
|
matrixService.saveConfig();
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Can't load display name";
|
$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 == 'connecting'">Call Connecting...</span>
|
||||||
<span ng-show="currentCall.state == 'connected'">Call Connected</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 == '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 == '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 Canceled</span>
|
||||||
<span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'inbound'">Call Ended</span>
|
<span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'inbound'">Call Ended</span>
|
||||||
|
@ -16,134 +16,16 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('RecentsController', ['matrixService', 'matrixFilter', 'eventHandlerService'])
|
angular.module('RecentsController', ['matrixService', 'matrixFilter'])
|
||||||
.controller('RecentsController', ['$rootScope', '$scope', 'matrixService', 'eventHandlerService',
|
.controller('RecentsController', ['$rootScope', '$scope', 'eventHandlerService',
|
||||||
function($rootScope, $scope, matrixService, eventHandlerService) {
|
function($rootScope, $scope, eventHandlerService) {
|
||||||
|
|
||||||
// FIXME: Angularjs reloads the controller (and resets its $scope) each time
|
// Expose the service to the view
|
||||||
// the page URL changes, use $rootScope to avoid to have to reload data
|
$scope.eventHandlerService = eventHandlerService;
|
||||||
$rootScope.rooms;
|
|
||||||
|
|
||||||
// $rootScope of the parent where the recents component is included can override this value
|
// $rootScope of the parent where the recents component is included can override this value
|
||||||
// in order to highlight a specific room in the list
|
// in order to highlight a specific room in the list
|
||||||
$rootScope.recentsSelectedRoomID;
|
$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,47 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('RecentsController')
|
angular.module('RecentsController')
|
||||||
.filter('orderRecents', function() {
|
.filter('orderRecents', ["matrixService", "eventHandlerService", function(matrixService, eventHandlerService) {
|
||||||
return function(rooms) {
|
return function(rooms) {
|
||||||
|
|
||||||
|
var user_id = matrixService.config().user_id;
|
||||||
|
|
||||||
// Transform the dict into an array
|
// Transform the dict into an array
|
||||||
// The key, room_id, is already in value objects
|
// The key, room_id, is already in value objects
|
||||||
var filtered = [];
|
var filtered = [];
|
||||||
angular.forEach(rooms, function(value, key) {
|
angular.forEach(rooms, function(room, room_id) {
|
||||||
filtered.push( value );
|
|
||||||
|
// 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
|
// And time sort them
|
||||||
// The room with the lastest message at first
|
// 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
|
// Invite message does not have a body message nor ts
|
||||||
// Puth them at the top of the list
|
// Puth them at the top of the list
|
||||||
if (undefined === a.lastMsg) {
|
if (undefined === lastMsgRoomA) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
else if (undefined === b.lastMsg) {
|
else if (undefined === lastMsgRoomB) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return b.lastMsg.ts - a.lastMsg.ts;
|
return lastMsgRoomB.ts - lastMsgRoomA.ts;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return filtered;
|
return filtered;
|
||||||
};
|
};
|
||||||
});
|
}]);
|
||||||
|
@ -1,20 +1,24 @@
|
|||||||
<div ng-controller="RecentsController" data-ng-init="onInit()">
|
<div ng-controller="RecentsController">
|
||||||
<table class="recentsTable">
|
<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) )"
|
ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )"
|
||||||
class ="recentsRoom"
|
class ="recentsRoom"
|
||||||
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
|
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="recentsRoomName">
|
<td ng-class="room['m.room.join_rules'].content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
|
||||||
{{ room.room_id | mRoomName }}
|
{{ room.room_id | mRoomName }}
|
||||||
</td>
|
</td>
|
||||||
<td class="recentsRoomSummaryTS">
|
<td class="recentsRoomSummaryUsersCount">
|
||||||
<span ng-show="undefined !== room.numUsersInRoom">
|
<span ng-show="undefined !== room.numUsersInRoom">
|
||||||
{{ room.numUsersInRoom || '1' }} {{ room.numUsersInRoom == 1 ? 'user' : 'users' }}
|
{{ room.numUsersInRoom || '1' }} {{ room.numUsersInRoom == 1 ? 'user' : 'users' }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="recentsRoomSummaryTS">
|
<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>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
@ -22,70 +26,70 @@
|
|||||||
<td colspan="3" class="recentsRoomSummary">
|
<td colspan="3" class="recentsRoomSummary">
|
||||||
|
|
||||||
<div ng-show="room.membership === 'invite'">
|
<div ng-show="room.membership === 'invite'">
|
||||||
{{ room.lastMsg.inviter | mUserDisplayName: room.room_id }} invited you
|
{{ room.inviter | mUserDisplayName: room.room_id }} invited you
|
||||||
</div>
|
</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">
|
<div ng-switch-when="m.room.member">
|
||||||
<span ng-if="'join' === room.lastMsg.content.membership">
|
<span ng-if="'join' === lastMsg.content.membership">
|
||||||
{{ room.lastMsg.state_key | mUserDisplayName: room.room_id}} joined
|
{{ lastMsg.state_key | mUserDisplayName: room.room_id}} joined
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="'leave' === room.lastMsg.content.membership">
|
<span ng-if="'leave' === lastMsg.content.membership">
|
||||||
<span ng-if="room.lastMsg.user_id === room.lastMsg.state_key">
|
<span ng-if="lastMsg.user_id === lastMsg.state_key">
|
||||||
{{room.lastMsg.state_key | mUserDisplayName: room.room_id }} left
|
{{lastMsg.state_key | mUserDisplayName: room.room_id }} left
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="room.lastMsg.user_id !== room.lastMsg.state_key">
|
<span ng-if="lastMsg.user_id !== lastMsg.state_key">
|
||||||
{{ room.lastMsg.user_id | mUserDisplayName: room.room_id }}
|
{{ lastMsg.user_id | mUserDisplayName: room.room_id }}
|
||||||
{{ {"join": "kicked", "ban": "unbanned"}[room.lastMsg.content.prev] }}
|
{{ {"join": "kicked", "ban": "unbanned"}[lastMsg.content.prev] }}
|
||||||
{{ room.lastMsg.state_key | mUserDisplayName: room.room_id }}
|
{{ lastMsg.state_key | mUserDisplayName: room.room_id }}
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="'join' === room.lastMsg.content.prev && room.lastMsg.content.reason">
|
<span ng-if="'join' === lastMsg.content.prev && lastMsg.content.reason">
|
||||||
: {{ room.lastMsg.content.reason }}
|
: {{ lastMsg.content.reason }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="'invite' === room.lastMsg.content.membership || 'ban' === room.lastMsg.content.membership">
|
<span ng-if="'invite' === lastMsg.content.membership || 'ban' === lastMsg.content.membership">
|
||||||
{{ room.lastMsg.user_id | mUserDisplayName: room.room_id }}
|
{{ lastMsg.user_id | mUserDisplayName: room.room_id }}
|
||||||
{{ {"invite": "invited", "ban": "banned"}[room.lastMsg.content.membership] }}
|
{{ {"invite": "invited", "ban": "banned"}[lastMsg.content.membership] }}
|
||||||
{{ room.lastMsg.state_key | mUserDisplayName: room.room_id }}
|
{{ lastMsg.state_key | mUserDisplayName: room.room_id }}
|
||||||
<span ng-if="'ban' === room.lastMsg.content.prev && room.lastMsg.content.reason">
|
<span ng-if="'ban' === lastMsg.content.prev && lastMsg.content.reason">
|
||||||
: {{ room.lastMsg.content.reason }}
|
: {{ lastMsg.content.reason }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-switch-when="m.room.message">
|
<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">
|
<div ng-switch-when="m.text">
|
||||||
{{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} :
|
{{ lastMsg.user_id | mUserDisplayName: room.room_id }} :
|
||||||
<span ng-bind-html="(room.lastMsg.content.body) | linky:'_blank'">
|
<span ng-bind-html="(lastMsg.content.body) | linky:'_blank'">
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-switch-when="m.image">
|
<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>
|
||||||
|
|
||||||
<div ng-switch-when="m.emote">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-switch-default>
|
<div ng-switch-default>
|
||||||
{{ room.lastMsg.content }}
|
{{ lastMsg.content }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-switch-when="m.room.topic">
|
<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>
|
||||||
|
|
||||||
<div ng-switch-when="m.room.name">
|
<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>
|
||||||
|
|
||||||
<div ng-switch-default>
|
<div ng-switch-default>
|
||||||
<div ng-if="room.lastMsg.type.indexOf('m.call.') === 0">
|
<div ng-if="lastMsg.type.indexOf('m.call.') === 0">
|
||||||
Call
|
Call
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -15,8 +15,8 @@ limitations under the License.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||||
.controller('RoomController', ['$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, mPresence, matrixPhoneService, MatrixCall) {
|
function($filter, $scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, matrixPhoneService, MatrixCall) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var MESSAGES_PER_PAGINATION = 30;
|
var MESSAGES_PER_PAGINATION = 30;
|
||||||
var THUMBNAIL_SIZE = 320;
|
var THUMBNAIL_SIZE = 320;
|
||||||
@ -32,7 +32,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
can_paginate: false, // this is toggled off when we are not ready yet to paginate or when we run out of items
|
can_paginate: false, // this is toggled off when we are not ready yet to paginate or when we run out of items
|
||||||
paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
|
paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
|
||||||
stream_failure: undefined, // the response when the stream fails
|
stream_failure: undefined, // the response when the stream fails
|
||||||
waiting_for_joined_event: false // true when the join request is pending. Back to false once the corresponding m.room.member event is received
|
waiting_for_joined_event: false, // true when the join request is pending. Back to false once the corresponding m.room.member event is received
|
||||||
|
messages_visibility: "hidden" // In order to avoid flickering when scrolling down the message table at the page opening, delay the message table display
|
||||||
};
|
};
|
||||||
$scope.members = {};
|
$scope.members = {};
|
||||||
$scope.autoCompleting = false;
|
$scope.autoCompleting = false;
|
||||||
@ -53,8 +54,13 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the filter applied in html to set the input value
|
var nameEvent = $rootScope.events.rooms[$scope.room_id]['m.room.name'];
|
||||||
$scope.name.newNameText = $filter('mRoomName')($scope.room_id);
|
if (nameEvent) {
|
||||||
|
$scope.name.newNameText = nameEvent.content.name;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.name.newNameText = "";
|
||||||
|
}
|
||||||
|
|
||||||
// Force focus to the input
|
// Force focus to the input
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
@ -131,6 +137,13 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
|
|
||||||
$timeout(function() {
|
$timeout(function() {
|
||||||
objDiv.scrollTop = objDiv.scrollHeight;
|
objDiv.scrollTop = objDiv.scrollHeight;
|
||||||
|
|
||||||
|
// Show the message table once the first scrolldown is done
|
||||||
|
if ("visible" !== $scope.state.messages_visibility) {
|
||||||
|
$timeout(function() {
|
||||||
|
$scope.state.messages_visibility = "visible";
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -139,27 +152,11 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
if (isLive && event.room_id === $scope.room_id) {
|
if (isLive && event.room_id === $scope.room_id) {
|
||||||
|
|
||||||
scrollToBottom();
|
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) {
|
$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) {
|
if ($scope.state.waiting_for_joined_event) {
|
||||||
// The user has successfully joined the room, we can getting data for this room
|
// The user has successfully joined the room, we can getting data for this room
|
||||||
$scope.state.waiting_for_joined_event = false;
|
$scope.state.waiting_for_joined_event = false;
|
||||||
@ -178,18 +175,32 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
user = event.user_id;
|
user = event.user_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if ("ban" === event.membership) {
|
if ("ban" === event.membership) {
|
||||||
$scope.state.permission_denied = "You have been banned by " + user;
|
$scope.state.permission_denied = "You have been banned by " + user;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$scope.state.permission_denied = "You have been kicked by " + user;
|
$scope.state.permission_denied = "You have been kicked by " + user;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
updateMemberList(event);
|
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 +246,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
matrixService.paginateBackMessages($scope.room_id, $rootScope.events.rooms[$scope.room_id].pagination.earliest_token, numItems).then(
|
matrixService.paginateBackMessages($scope.room_id, $rootScope.events.rooms[$scope.room_id].pagination.earliest_token, numItems).then(
|
||||||
function(response) {
|
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) {
|
if (response.data.chunk.length < MESSAGES_PER_PAGINATION) {
|
||||||
// no more messages to paginate. this currently never gets turned true again, as we never
|
// no more messages to paginate. this currently never gets turned true again, as we never
|
||||||
// expire paginated contents in the current implementation.
|
// expire paginated contents in the current implementation.
|
||||||
@ -406,12 +417,15 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.send = function() {
|
$scope.send = function() {
|
||||||
if ($scope.textInput === "") {
|
if (undefined === $scope.textInput || $scope.textInput === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollToBottom(true);
|
scrollToBottom(true);
|
||||||
|
|
||||||
|
// Store the command in the history
|
||||||
|
history.push($scope.textInput);
|
||||||
|
|
||||||
var promise;
|
var promise;
|
||||||
var cmd;
|
var cmd;
|
||||||
var args;
|
var args;
|
||||||
@ -676,6 +690,10 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
var onInit2 = function() {
|
var onInit2 = function() {
|
||||||
console.log("onInit2");
|
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
|
// Make sure the initialSync has been before going further
|
||||||
eventHandlerService.waitForInitialSyncCompletion().then(
|
eventHandlerService.waitForInitialSyncCompletion().then(
|
||||||
function() {
|
function() {
|
||||||
@ -684,6 +702,10 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
|
|
||||||
// The room members is available in the data fetched by initialSync
|
// The room members is available in the data fetched by initialSync
|
||||||
if ($rootScope.events.rooms[$scope.room_id]) {
|
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;
|
var members = $rootScope.events.rooms[$scope.room_id].members;
|
||||||
|
|
||||||
// Update the member list
|
// Update the member list
|
||||||
@ -729,6 +751,9 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
// Make recents highlight the current room
|
// Make recents highlight the current room
|
||||||
$scope.recentsSelectedRoomID = $scope.room_id;
|
$scope.recentsSelectedRoomID = $scope.room_id;
|
||||||
|
|
||||||
|
// Init the history for this room
|
||||||
|
history.init();
|
||||||
|
|
||||||
// Get the up-to-date the current member list
|
// Get the up-to-date the current member list
|
||||||
matrixService.getMemberList($scope.room_id).then(
|
matrixService.getMemberList($scope.room_id).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
@ -743,9 +768,18 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
// Arm list timing update timer
|
// Arm list timing update timer
|
||||||
updateMemberListPresenceAge();
|
updateMemberListPresenceAge();
|
||||||
|
|
||||||
// Start pagination
|
// Allow pagination
|
||||||
$scope.state.can_paginate = true;
|
$scope.state.can_paginate = true;
|
||||||
|
|
||||||
|
// 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);
|
paginate(MESSAGES_PER_PAGINATION);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// There are already messages, go to the last message
|
||||||
|
scrollToBottom(true);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Failed get member list: " + error.data.error;
|
$scope.feedback = "Failed get member list: " + error.data.error;
|
||||||
@ -832,4 +866,82 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
|||||||
$rootScope.currentCall = call;
|
$rootScope.currentCall = call;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Manage history of typed messages
|
||||||
|
// History is saved in sessionStoratge so that it survives when the user
|
||||||
|
// navigates through the rooms and when it refreshes the page
|
||||||
|
var history = {
|
||||||
|
// The list of typed messages. Index 0 is the more recents
|
||||||
|
data: [],
|
||||||
|
|
||||||
|
// The position in the history currently displayed
|
||||||
|
position: -1,
|
||||||
|
|
||||||
|
// The message the user has started to type before going into the history
|
||||||
|
typingMessage: undefined,
|
||||||
|
|
||||||
|
// Init/load data for the current room
|
||||||
|
init: function() {
|
||||||
|
var data = sessionStorage.getItem("history_" + $scope.room_id);
|
||||||
|
if (data) {
|
||||||
|
this.data = JSON.parse(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Store a message in the history
|
||||||
|
push: function(message) {
|
||||||
|
this.data.unshift(message);
|
||||||
|
|
||||||
|
// Update the session storage
|
||||||
|
sessionStorage.setItem("history_" + $scope.room_id, JSON.stringify(this.data));
|
||||||
|
|
||||||
|
// Reset history position
|
||||||
|
this.position = -1;
|
||||||
|
this.typingMessage = undefined;
|
||||||
|
},
|
||||||
|
|
||||||
|
// Move in the history
|
||||||
|
go: function(offset) {
|
||||||
|
|
||||||
|
if (-1 === this.position) {
|
||||||
|
// User starts to go to into the history, save the current line
|
||||||
|
this.typingMessage = $scope.textInput;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If the user modified this line in history, keep the change
|
||||||
|
this.data[this.position] = $scope.textInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bounds the new position to valid data
|
||||||
|
var newPosition = this.position + offset;
|
||||||
|
newPosition = Math.max(-1, newPosition);
|
||||||
|
newPosition = Math.min(newPosition, this.data.length - 1);
|
||||||
|
this.position = newPosition;
|
||||||
|
|
||||||
|
if (-1 !== this.position) {
|
||||||
|
// Show the message from the history
|
||||||
|
$scope.textInput = this.data[this.position];
|
||||||
|
}
|
||||||
|
else if (undefined !== this.typingMessage) {
|
||||||
|
// Go back to the message the user started to type
|
||||||
|
$scope.textInput = this.typingMessage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Make history singleton methods available from HTML
|
||||||
|
$scope.history = {
|
||||||
|
goUp: function($event) {
|
||||||
|
if ($scope.room_id) {
|
||||||
|
history.go(1);
|
||||||
|
}
|
||||||
|
$event.preventDefault();
|
||||||
|
},
|
||||||
|
goDown: function($event) {
|
||||||
|
if ($scope.room_id) {
|
||||||
|
history.go(-1);
|
||||||
|
}
|
||||||
|
$event.preventDefault();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
{{ room_id | mRoomName }}
|
{{ room_id | mRoomName }}
|
||||||
</div>
|
</div>
|
||||||
<form ng-submit="name.updateName()" ng-show="name.isEditing" class="roomNameForm">
|
<form ng-submit="name.updateName()" ng-show="name.isEditing" class="roomNameForm">
|
||||||
<input ng-model="name.newNameText" ng-blur="name.cancelEdit()" class="roomNameInput" />
|
<input ng-model="name.newNameText" ng-blur="name.cancelEdit()" class="roomNameInput" placeholder="Room name"/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -23,7 +23,7 @@
|
|||||||
{{ events.rooms[room_id]['m.room.topic'].content.topic | limitTo: 200}}
|
{{ events.rooms[room_id]['m.room.topic'].content.topic | limitTo: 200}}
|
||||||
</div>
|
</div>
|
||||||
<form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm">
|
<form ng-submit="topic.updateTopic()" ng-show="topic.isEditing" class="roomTopicForm">
|
||||||
<input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" />
|
<input ng-model="topic.newTopicText" ng-blur="topic.cancelEdit()" class="roomTopicInput" placeholder="Topic"/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -56,7 +56,10 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="messageTableWrapper" ng-hide="state.permission_denied" keep-scroll>
|
<div id="messageTableWrapper"
|
||||||
|
ng-hide="state.permission_denied"
|
||||||
|
ng-style="{ 'visibility': state.messages_visibility }"
|
||||||
|
keep-scroll>
|
||||||
<!-- FIXME: need to have better timestamp semantics than the (msg.content.hsob_ts || msg.ts) hack below -->
|
<!-- FIXME: need to have better timestamp semantics than the (msg.content.hsob_ts || msg.ts) hack below -->
|
||||||
<table id="messageTable" infinite-scroll="paginateMore()">
|
<table id="messageTable" infinite-scroll="paginateMore()">
|
||||||
<tr ng-repeat="msg in events.rooms[room_id].messages"
|
<tr ng-repeat="msg in events.rooms[room_id].messages"
|
||||||
@ -105,7 +108,7 @@
|
|||||||
|
|
||||||
<span ng-show='msg.content.msgtype === "m.text"'
|
<span ng-show='msg.content.msgtype === "m.text"'
|
||||||
class="message"
|
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'"/>
|
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>
|
<span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id'>Outgoing Call</span>
|
||||||
@ -156,6 +159,7 @@
|
|||||||
<td width="*">
|
<td width="*">
|
||||||
<textarea id="mainInput" rows="1" ng-model="textInput" ng-enter="send()"
|
<textarea id="mainInput" rows="1" ng-model="textInput" ng-enter="send()"
|
||||||
ng-disabled="state.permission_denied"
|
ng-disabled="state.permission_denied"
|
||||||
|
ng-keydown="(38 === $event.which) ? history.goUp($event) : ((40 === $event.which) ? history.goDown($event) : 0)"
|
||||||
ng-focus="true" autocomplete="off" tab-complete/>
|
ng-focus="true" autocomplete="off" tab-complete/>
|
||||||
</td>
|
</td>
|
||||||
<td id="buttonsCell">
|
<td id="buttonsCell">
|
||||||
|
@ -194,7 +194,16 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
|
|||||||
|
|
||||||
/*** Desktop notifications section ***/
|
/*** Desktop notifications section ***/
|
||||||
$scope.settings = {
|
$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
|
// If the browser supports it, check the desktop notification state
|
||||||
|
@ -51,7 +51,16 @@
|
|||||||
<h3>Desktop notifications</h3>
|
<h3>Desktop notifications</h3>
|
||||||
<div class="section" ng-switch="settings.notifications">
|
<div class="section" ng-switch="settings.notifications">
|
||||||
<div ng-switch-when="granted">
|
<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>
|
||||||
<div ng-switch-when="denied">
|
<div ng-switch-when="denied">
|
||||||
You have denied permission for notifications.<br/>
|
You have denied permission for notifications.<br/>
|
||||||
|
Loading…
Reference in New Issue
Block a user