mirror of
https://github.com/element-hq/synapse
synced 2024-08-20 03:50:20 +00:00
Merge branch 'develop' of github.com:matrix-org/synapse into presence_logging
Conflicts: synapse/handlers/presence.py
This commit is contained in:
commit
54d0a75573
20 changed files with 495 additions and 301 deletions
|
@ -225,8 +225,13 @@ class SynapseCmd(cmd.Cmd):
|
||||||
json_res = yield self.http_client.do_request("GET", url)
|
json_res = yield self.http_client.do_request("GET", url)
|
||||||
print json_res
|
print json_res
|
||||||
|
|
||||||
if ("type" not in json_res or "m.login.password" != json_res["type"] or
|
if "flows" not in json_res:
|
||||||
"stages" in json_res):
|
print "Failed to find any login flows."
|
||||||
|
defer.returnValue(False)
|
||||||
|
|
||||||
|
flow = json_res["flows"][0] # assume first is the one we want.
|
||||||
|
if ("type" not in flow or "m.login.password" != flow["type"] or
|
||||||
|
"stages" in flow):
|
||||||
fallback_url = self._url() + "/login/fallback"
|
fallback_url = self._url() + "/login/fallback"
|
||||||
print ("Unable to login via the command line client. Please visit "
|
print ("Unable to login via the command line client. Please visit "
|
||||||
"%s to login." % fallback_url)
|
"%s to login." % fallback_url)
|
||||||
|
|
|
@ -68,15 +68,108 @@ Non-state messages
|
||||||
|
|
||||||
What are they, when are they used, what do they contain, how should they be used
|
What are they, when are they used, what do they contain, how should they be used
|
||||||
|
|
||||||
m.room.message types
|
m.room.message msgtypes
|
||||||
--------------------
|
-----------------------
|
||||||
- m.text
|
Each ``m.room.message`` MUST have a ``msgtype`` key which identifies the type of
|
||||||
- m.emote
|
message being sent. Each type has their own required and optional keys, as outlined
|
||||||
- m.audio
|
below:
|
||||||
- m.image
|
|
||||||
- m.video
|
|
||||||
- m.location
|
|
||||||
|
|
||||||
|
``m.text``
|
||||||
|
Required keys:
|
||||||
|
- ``body`` : "string" - The body of the message.
|
||||||
|
Optional keys:
|
||||||
|
None.
|
||||||
|
Example:
|
||||||
|
``{ "msgtype": "m.text", "body": "I am a fish" }``
|
||||||
|
|
||||||
|
``m.emote``
|
||||||
|
Required keys:
|
||||||
|
- ``body`` : "string" - The emote action to perform.
|
||||||
|
Optional keys:
|
||||||
|
None.
|
||||||
|
Example:
|
||||||
|
``{ "msgtype": "m.emote", "body": "tries to come up with a witty explanation" }``
|
||||||
|
|
||||||
|
``m.image``
|
||||||
|
Required keys:
|
||||||
|
- ``url`` : "string" - The URL to the image.
|
||||||
|
Optional keys:
|
||||||
|
- ``info`` : "string" - info : JSON object (ImageInfo) - The image info for image
|
||||||
|
referred to in ``url``.
|
||||||
|
- ``thumbnail_url`` : "string" - The URL to the thumbnail.
|
||||||
|
- ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
|
||||||
|
referred to in ``thumbnail_url``.
|
||||||
|
- ``body`` : "string" - The alt text of the image, or some kind of content
|
||||||
|
description for accessibility e.g. "image attachment".
|
||||||
|
|
||||||
|
ImageInfo:
|
||||||
|
Information about an image::
|
||||||
|
|
||||||
|
{
|
||||||
|
"size" : integer (size of image in bytes),
|
||||||
|
"w" : integer (width of image in pixels),
|
||||||
|
"h" : integer (height of image in pixels),
|
||||||
|
"mimetype" : "string (e.g. image/jpeg)",
|
||||||
|
}
|
||||||
|
|
||||||
|
``m.audio``
|
||||||
|
Required keys:
|
||||||
|
- ``url`` : "string" - The URL to the audio.
|
||||||
|
Optional keys:
|
||||||
|
- ``info`` : JSON object (AudioInfo) - The audio info for the audio referred to in
|
||||||
|
``url``.
|
||||||
|
- ``body`` : "string" - A description of the audio e.g. "Bee Gees -
|
||||||
|
Stayin' Alive", or some kind of content description for accessibility e.g.
|
||||||
|
"audio attachment".
|
||||||
|
|
||||||
|
AudioInfo:
|
||||||
|
Information about a piece of audio::
|
||||||
|
|
||||||
|
{
|
||||||
|
"mimetype" : "string (e.g. audio/aac)",
|
||||||
|
"size" : integer (size of audio in bytes),
|
||||||
|
"duration" : integer (duration of audio in milliseconds),
|
||||||
|
}
|
||||||
|
|
||||||
|
``m.video``
|
||||||
|
Required keys:
|
||||||
|
- ``url`` : "string" - The URL to the video.
|
||||||
|
Optional keys:
|
||||||
|
- ``info`` : JSON object (VideoInfo) - The video info for the video referred to in
|
||||||
|
``url``.
|
||||||
|
- ``body`` : "string" - A description of the video e.g. "Gangnam style",
|
||||||
|
or some kind of content description for accessibility e.g. "video attachment".
|
||||||
|
|
||||||
|
VideoInfo:
|
||||||
|
Information about a video::
|
||||||
|
|
||||||
|
{
|
||||||
|
"mimetype" : "string (e.g. video/mp4)",
|
||||||
|
"size" : integer (size of video in bytes),
|
||||||
|
"duration" : integer (duration of video in milliseconds),
|
||||||
|
"w" : integer (width of video in pixels),
|
||||||
|
"h" : integer (height of video in pixels),
|
||||||
|
"thumbnail_url" : "string (URL to image)",
|
||||||
|
"thumbanil_info" : JSON object (ImageInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
``m.location``
|
||||||
|
Required keys:
|
||||||
|
- ``geo_uri`` : "string" - The geo URI representing the location.
|
||||||
|
Optional keys:
|
||||||
|
- ``thumbnail_url`` : "string" - The URL to a thumnail of the location being
|
||||||
|
represented.
|
||||||
|
- ``thumbnail_info`` : JSON object (ImageInfo) - The image info for the image
|
||||||
|
referred to in ``thumbnail_url``.
|
||||||
|
- ``body`` : "string" - A description of the location e.g. "Big Ben,
|
||||||
|
London, UK", or some kind of content description for accessibility e.g.
|
||||||
|
"location attachment".
|
||||||
|
|
||||||
|
The following keys can be attached to any ``m.room.message``:
|
||||||
|
|
||||||
|
Optional keys:
|
||||||
|
- ``sender_ts`` : integer - A timestamp (ms resolution) representing the
|
||||||
|
wall-clock time when the message was sent from the client.
|
||||||
|
|
||||||
Presence
|
Presence
|
||||||
========
|
========
|
||||||
|
@ -201,124 +294,134 @@ 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
|
||||||
requests to that home server as a query parameter 'access_token'.
|
requests to that home server as a query parameter 'access_token'.
|
||||||
|
|
||||||
- TODO Kegan : Make registration like login
|
- TODO Kegan : Make registration like login (just omit the "user" key on the
|
||||||
- TODO Kegan : Allow alternative forms of login (>1 route)
|
initial request?)
|
||||||
|
|
||||||
If the client has already registered, they need to be able to login to their
|
If the client has already registered, they need to be able to login to their
|
||||||
account. The home server may provide many different ways of logging in, such
|
account. The home server may provide many different ways of logging in, such
|
||||||
as user/password auth, login via a social network (OAuth), login by confirming
|
as user/password auth, login via a social network (OAuth2), login by confirming
|
||||||
a token sent to their email address, etc. This specification does not define how
|
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.
|
should follow so that ANY client can login to ANY home server.
|
||||||
|
|
||||||
The login process breaks down into the following:
|
The login process breaks down into the following:
|
||||||
1. Get login process info.
|
1. Determine the requirements for logging in.
|
||||||
2. Submit the login stage credentials.
|
2. Submit the login stage credentials.
|
||||||
3. Get access token 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
|
||||||
step 2.
|
step 2.
|
||||||
|
|
||||||
- What are types?
|
As each home server may have different ways of logging in, the client needs to know how
|
||||||
|
they should login. All distinct login stages MUST have a corresponding ``'type'``.
|
||||||
|
A ``'type'`` is a namespaced string which details the mechanism for logging in.
|
||||||
|
|
||||||
Matrix-defined login types
|
A client may be able to login via multiple valid login flows, and should choose a single
|
||||||
--------------------------
|
flow when logging in. A flow is a series of login stages. The home server MUST respond
|
||||||
|
with all the valid login flows when requested::
|
||||||
|
|
||||||
|
The client can login via 3 paths: 1a and 1b, 2a and 2b, or 3. The client should
|
||||||
|
select one of these paths.
|
||||||
|
|
||||||
|
{
|
||||||
|
"flows": [
|
||||||
|
{
|
||||||
|
"type": "<login type1a>",
|
||||||
|
"stages": [ "<login type 1a>", "<login type 1b>" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "<login type2a>",
|
||||||
|
"stages": [ "<login type 2a>", "<login type 2b>" ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "<login type3>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
After the login is completed, the client's fully-qualified user ID and a new access
|
||||||
|
token MUST be returned::
|
||||||
|
|
||||||
|
{
|
||||||
|
"user_id": "@user:matrix.org",
|
||||||
|
"access_token": "abcdef0123456789"
|
||||||
|
}
|
||||||
|
|
||||||
|
The ``user_id`` key is particularly useful if the home server wishes to support
|
||||||
|
localpart entry of usernames (e.g. "user" rather than "@user:matrix.org"), as the
|
||||||
|
client may not be able to determine its ``user_id`` in this case.
|
||||||
|
|
||||||
|
If a login has multiple requests, the home server may wish to create a session. If
|
||||||
|
a home server responds with a 'session' key to a request, clients MUST submit it in
|
||||||
|
subsequent requests until the login is completed::
|
||||||
|
|
||||||
|
{
|
||||||
|
"session": "<session id>"
|
||||||
|
}
|
||||||
|
|
||||||
|
This specification defines the following login types:
|
||||||
- m.login.password
|
- m.login.password
|
||||||
- m.login.oauth2
|
- m.login.oauth2
|
||||||
- m.login.email.code
|
- m.login.email.code
|
||||||
- m.login.email.url
|
- m.login.email.url
|
||||||
|
|
||||||
|
|
||||||
Password-based
|
Password-based
|
||||||
--------------
|
--------------
|
||||||
Type: "m.login.password"
|
Type:
|
||||||
LoginSubmission::
|
"m.login.password"
|
||||||
|
Description:
|
||||||
|
Login is supported via a username and password.
|
||||||
|
|
||||||
|
To respond to this type, reply with::
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "m.login.password",
|
"type": "m.login.password",
|
||||||
"user": <user_id>,
|
"user": "<user_id or user localpart>",
|
||||||
"password": <password>
|
"password": "<password>"
|
||||||
}
|
}
|
||||||
|
|
||||||
Example:
|
The home server MUST respond with either new credentials, the next stage of the login
|
||||||
Assume you are @bob:matrix.org and you wish to login on another mobile device.
|
process, or a standard error response.
|
||||||
First, you GET /login which returns::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.password"
|
|
||||||
}
|
|
||||||
|
|
||||||
Your client knows how to handle this, so your client prompts the user to enter
|
|
||||||
their username and password. This is then submitted::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.password",
|
|
||||||
"user": "@bob:matrix.org",
|
|
||||||
"password": "monkey"
|
|
||||||
}
|
|
||||||
|
|
||||||
The server checks this, finds it is valid, and returns::
|
|
||||||
|
|
||||||
{
|
|
||||||
"access_token": "abcdef0123456789"
|
|
||||||
}
|
|
||||||
|
|
||||||
The server may optionally return "user_id" to confirm or change the user's ID.
|
|
||||||
This is particularly useful if the home server wishes to support localpart entry
|
|
||||||
of usernames (e.g. "bob" rather than "@bob:matrix.org").
|
|
||||||
|
|
||||||
OAuth2-based
|
OAuth2-based
|
||||||
------------
|
------------
|
||||||
Type: "m.login.oauth2"
|
Type:
|
||||||
This is a multi-stage login.
|
"m.login.oauth2"
|
||||||
|
Description:
|
||||||
|
Login is supported via OAuth2 URLs. This login consists of multiple requests.
|
||||||
|
|
||||||
LoginSubmission::
|
To respond to this type, reply with::
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "m.login.oauth2",
|
"type": "m.login.oauth2",
|
||||||
"user": <user_id>
|
"user": "<user_id or user localpart>"
|
||||||
}
|
}
|
||||||
|
|
||||||
Returns::
|
The server MUST respond with::
|
||||||
|
|
||||||
{
|
{
|
||||||
"uri": <Authorization Request uri OR service selection uri>
|
"uri": <Authorization Request URI OR service selection URI>
|
||||||
}
|
}
|
||||||
|
|
||||||
The home server acts as a 'confidential' Client for the purposes of OAuth2.
|
The home server acts as a 'confidential' client for the purposes of OAuth2.
|
||||||
|
If the uri is a ``sevice selection URI``, it MUST point to a webpage which prompts the
|
||||||
If the uri is a "sevice selection uri", it is a simple page which prompts the
|
user to choose which service to authorize with. On selection of a service, this
|
||||||
user to choose which service to authorize with. On selection of a service, they
|
MUST link through to an ``Authorization Request URI``. If there is only 1 service which the
|
||||||
link through to Authorization Request URIs. If there is only 1 service which the
|
|
||||||
home server accepts when logging in, this indirection can be skipped and the
|
home server accepts when logging in, this indirection can be skipped and the
|
||||||
"uri" key can be the Authorization Request URI.
|
"uri" key can be the ``Authorization Request URI``.
|
||||||
|
|
||||||
The client visits the Authorization Request URI, which then shows the OAuth2
|
The client then visits the ``Authorization Request URI``, which then shows the OAuth2
|
||||||
Allow/Deny prompt. Hitting 'Allow' returns the redirect URI with the auth code.
|
Allow/Deny prompt. Hitting 'Allow' returns the ``redirect URI`` with the auth code.
|
||||||
Home servers can choose any path for the redirect URI. The client should visit
|
Home servers can choose any path for the ``redirect URI``. The client should visit
|
||||||
the redirect URI, which will then finish the OAuth2 login process, granting the
|
the ``redirect URI``, which will then finish the OAuth2 login process, granting the
|
||||||
home server an access token for the chosen service. When the home server gets
|
home server an access token for the chosen service. When the home server gets
|
||||||
this access token, it knows that the cilent has authed with the 3rd party, and
|
this access token, it verifies that the cilent has authorised with the 3rd party, and
|
||||||
so can return a LoginResult.
|
can now complete the login. The OAuth2 ``redirect URI`` (with auth code) MUST respond
|
||||||
|
with either new credentials, the next stage of the login process, or a standard error
|
||||||
|
response.
|
||||||
|
|
||||||
The OAuth redirect URI (with auth code) MUST return a LoginResult.
|
For example, if a home server accepts OAuth2 from Google, it would return the
|
||||||
|
Authorization Request URI for Google::
|
||||||
Example:
|
|
||||||
Assume you are @bob:matrix.org and you wish to login on another mobile device.
|
|
||||||
First, you GET /login which returns::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.oauth2"
|
|
||||||
}
|
|
||||||
|
|
||||||
Your client knows how to handle this, so your client prompts the user to enter
|
|
||||||
their username. This is then submitted::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.oauth2",
|
|
||||||
"user": "@bob:matrix.org"
|
|
||||||
}
|
|
||||||
|
|
||||||
The server only accepts auth from Google, so returns the Authorization Request
|
|
||||||
URI for Google::
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"uri": "https://accounts.google.com/o/oauth2/auth?response_type=code&
|
"uri": "https://accounts.google.com/o/oauth2/auth?response_type=code&
|
||||||
|
@ -329,145 +432,142 @@ The client then visits this URI and authorizes the home server. The client then
|
||||||
visits the REDIRECT_URI with the auth code= query parameter which returns::
|
visits the REDIRECT_URI with the auth code= query parameter which returns::
|
||||||
|
|
||||||
{
|
{
|
||||||
|
"user_id": "@user:matrix.org",
|
||||||
"access_token": "0123456789abcdef"
|
"access_token": "0123456789abcdef"
|
||||||
}
|
}
|
||||||
|
|
||||||
Email-based (code)
|
Email-based (code)
|
||||||
------------------
|
------------------
|
||||||
Type: "m.login.email.code"
|
Type:
|
||||||
This is a multi-stage login.
|
"m.login.email.code"
|
||||||
|
Description:
|
||||||
|
Login is supported by typing in a code which is sent in an email. This login
|
||||||
|
consists of multiple requests.
|
||||||
|
|
||||||
First LoginSubmission::
|
To respond to this type, reply with::
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "m.login.email.code",
|
"type": "m.login.email.code",
|
||||||
"user": <user_id>
|
"user": "<user_id or user localpart>",
|
||||||
"email": <email address>
|
"email": "<email address>"
|
||||||
}
|
}
|
||||||
|
|
||||||
Returns::
|
After validating the email address, the home server MUST send an email containing
|
||||||
|
an authentication code and return::
|
||||||
{
|
|
||||||
"type": m.login.email.code
|
|
||||||
"session": <session id>
|
|
||||||
}
|
|
||||||
|
|
||||||
The email contains a code which must be sent in the next LoginSubmission::
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "m.login.email.code",
|
"type": "m.login.email.code",
|
||||||
"session": <session id>,
|
"session": "<session id>"
|
||||||
"code": <code in email sent>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Returns::
|
The second request in this login stage involves sending this authentication code::
|
||||||
|
|
||||||
{
|
{
|
||||||
"access_token": <access token>
|
"type": "m.login.email.code",
|
||||||
|
"session": "<session id>",
|
||||||
|
"code": "<code in email sent>"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
The home server MUST respond to this with either new credentials, the next stage of
|
||||||
|
the login process, or a standard error response.
|
||||||
|
|
||||||
Email-based (url)
|
Email-based (url)
|
||||||
-----------------
|
-----------------
|
||||||
Type: "m.login.email.url"
|
Type:
|
||||||
This is a multi-stage login.
|
"m.login.email.url"
|
||||||
|
Description:
|
||||||
|
Login is supported by clicking on a URL in an email. This login consists of
|
||||||
|
multiple requests.
|
||||||
|
|
||||||
First LoginSubmission::
|
To respond to this type, reply with::
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "m.login.email.url",
|
"type": "m.login.email.url",
|
||||||
"user": <user_id>
|
"user": "<user_id or user localpart>",
|
||||||
"email": <email address>
|
"email": "<email address>"
|
||||||
}
|
}
|
||||||
|
|
||||||
Returns::
|
After validating the email address, the home server MUST send an email containing
|
||||||
|
an authentication URL and return::
|
||||||
|
|
||||||
{
|
{
|
||||||
"session": <session id>
|
"type": "m.login.email.url",
|
||||||
|
"session": "<session id>"
|
||||||
}
|
}
|
||||||
|
|
||||||
The email contains a URL which must be clicked. After it has been clicked, the
|
The email contains a URL which must be clicked. After it has been clicked, the
|
||||||
client should perform a request::
|
client should perform another request::
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.email.code",
|
|
||||||
"session": <session id>
|
|
||||||
}
|
|
||||||
|
|
||||||
Returns::
|
|
||||||
|
|
||||||
{
|
|
||||||
"access_token": <access token>
|
|
||||||
}
|
|
||||||
|
|
||||||
Example:
|
|
||||||
Assume you are @bob:matrix.org and you wish to login on another mobile device.
|
|
||||||
First, you GET /login which returns::
|
|
||||||
|
|
||||||
{
|
|
||||||
"type": "m.login.email.url"
|
|
||||||
}
|
|
||||||
|
|
||||||
Your client knows how to handle this, so your client prompts the user to enter
|
|
||||||
their email address. This is then submitted::
|
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "m.login.email.url",
|
"type": "m.login.email.url",
|
||||||
"user": "@bob:matrix.org",
|
"session": "<session id>"
|
||||||
"email": "bob@mydomain.com"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
The server confirms that bob@mydomain.com is linked to @bob:matrix.org, then
|
The home server MUST respond to this with either new credentials, the next stage of
|
||||||
sends an email to this address and returns::
|
the login process, or a standard error response.
|
||||||
|
|
||||||
|
A common client implementation will be to periodically poll until the link is clicked.
|
||||||
|
If the link has not been visited yet, a standard error response with an errcode of
|
||||||
|
``M_LOGIN_EMAIL_URL_NOT_YET`` should be returned.
|
||||||
|
|
||||||
|
|
||||||
|
N-Factor Authentication
|
||||||
|
-----------------------
|
||||||
|
Multiple login stages can be combined to create N-factor authentication during login.
|
||||||
|
|
||||||
|
This can be achieved by responding with the ``'next'`` login type on completion of a
|
||||||
|
previous login stage::
|
||||||
|
|
||||||
{
|
{
|
||||||
"session": "ewuigf7462"
|
"next": "<next login type>"
|
||||||
}
|
}
|
||||||
|
|
||||||
The client then starts polling the server with the following::
|
If a home server implements N-factor authentication, it MUST respond with all
|
||||||
|
``'stages'`` when initially queried for their login requirements::
|
||||||
|
|
||||||
{
|
{
|
||||||
"type": "m.login.email.url",
|
"type": "<1st login type>",
|
||||||
"session": "ewuigf7462"
|
"stages": [ <1st login type>, <2nd login type>, ... , <Nth login type> ]
|
||||||
}
|
}
|
||||||
|
|
||||||
(Alternatively, the server could send the device a push notification when the
|
This can be represented conceptually as::
|
||||||
email has been validated). The email arrives and it contains a URL to click on.
|
|
||||||
The user clicks on the which completes the login process with the server. The
|
|
||||||
next time the client polls, it returns::
|
|
||||||
|
|
||||||
{
|
_______________________
|
||||||
"access_token": "abcdef0123456789"
|
| Login Stage 1 |
|
||||||
}
|
| type: "<login type1>" |
|
||||||
|
| ___________________ |
|
||||||
N-Factor auth
|
| |_Request_1_________| | <-- Returns "session" key which is used throughout.
|
||||||
-------------
|
| ___________________ |
|
||||||
Multiple login stages can be combined with the "next" key in the LoginResult.
|
| |_Request_2_________| | <-- Returns a "next" value of "login type2"
|
||||||
|
|_______________________|
|
||||||
Example:
|
|
|
||||||
A server demands an email.code then password auth before logging in. First, the
|
|
|
||||||
client performs a GET /login which returns::
|
_________V_____________
|
||||||
|
| Login Stage 2 |
|
||||||
{
|
| type: "<login type2>" |
|
||||||
"type": "m.login.email.code",
|
| ___________________ |
|
||||||
"stages": ["m.login.email.code", "m.login.password"]
|
| |_Request_1_________| |
|
||||||
}
|
| ___________________ |
|
||||||
|
| |_Request_2_________| |
|
||||||
The client performs the email login (See "Email-based (code)"), but instead of
|
| ___________________ |
|
||||||
returning an access_token, it returns::
|
| |_Request_3_________| | <-- Returns a "next" value of "login type3"
|
||||||
|
|_______________________|
|
||||||
{
|
|
|
||||||
"next": "m.login.password"
|
|
|
||||||
}
|
_________V_____________
|
||||||
|
| Login Stage 3 |
|
||||||
The client then presents a user/password screen and the login continues until
|
| type: "<login type3>" |
|
||||||
this is complete (See "Password-based"), which then returns the "access_token".
|
| ___________________ |
|
||||||
|
| |_Request_1_________| | <-- Returns user credentials
|
||||||
|
|_______________________|
|
||||||
|
|
||||||
Fallback
|
Fallback
|
||||||
--------
|
--------
|
||||||
|
Clients cannot be expected to be able to know how to process every single
|
||||||
|
login type. If a client determines it does not know how to handle a given
|
||||||
|
login type, it should request a login fallback page::
|
||||||
|
|
||||||
If the client does NOT know how to handle the given type, they should::
|
GET matrix/client/api/v1/login/fallback
|
||||||
|
|
||||||
GET /login/fallback
|
|
||||||
|
|
||||||
This MUST return an HTML page which can perform the entire login process.
|
This MUST return an HTML page which can perform the entire login process.
|
||||||
|
|
||||||
|
|
|
@ -541,6 +541,7 @@ class _TransactionQueue(object):
|
||||||
)
|
)
|
||||||
|
|
||||||
def eb(failure):
|
def eb(failure):
|
||||||
|
if not deferred.called:
|
||||||
deferred.errback(failure)
|
deferred.errback(failure)
|
||||||
self._attempt_new_transaction(destination).addErrback(eb)
|
self._attempt_new_transaction(destination).addErrback(eb)
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ class BaseRoomHandler(BaseHandler):
|
||||||
extra_users=[]):
|
extra_users=[]):
|
||||||
snapshot.fill_out_prev_events(event)
|
snapshot.fill_out_prev_events(event)
|
||||||
|
|
||||||
store_id = yield self.store.persist_event(event)
|
yield self.store.persist_event(event)
|
||||||
|
|
||||||
destinations = set(extra_destinations)
|
destinations = set(extra_destinations)
|
||||||
# Send a PDU to all hosts who have joined the room.
|
# Send a PDU to all hosts who have joined the room.
|
||||||
|
|
|
@ -22,8 +22,6 @@ from synapse.api.constants import Membership
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
from synapse.federation.pdu_codec import PduCodec
|
from synapse.federation.pdu_codec import PduCodec
|
||||||
|
|
||||||
from synapse.api.errors import AuthError
|
|
||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -86,12 +84,6 @@ class FederationHandler(BaseHandler):
|
||||||
|
|
||||||
yield self.replication_layer.send_pdu(pdu)
|
yield self.replication_layer.send_pdu(pdu)
|
||||||
|
|
||||||
@log_function
|
|
||||||
def get_state_for_room(self, destination, room_id):
|
|
||||||
return self.replication_layer.get_state_for_context(
|
|
||||||
destination, room_id
|
|
||||||
)
|
|
||||||
|
|
||||||
@log_function
|
@log_function
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_receive_pdu(self, pdu, backfilled):
|
def on_receive_pdu(self, pdu, backfilled):
|
||||||
|
@ -141,20 +133,20 @@ class FederationHandler(BaseHandler):
|
||||||
|
|
||||||
yield self.hs.get_handlers().room_member_handler.change_membership(
|
yield self.hs.get_handlers().room_member_handler.change_membership(
|
||||||
new_event,
|
new_event,
|
||||||
True
|
do_auth=True
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
with (yield self.room_lock.lock(event.room_id)):
|
with (yield self.room_lock.lock(event.room_id)):
|
||||||
store_id = yield self.store.persist_event(event, backfilled)
|
yield self.store.persist_event(event, backfilled)
|
||||||
|
|
||||||
room = yield self.store.get_room(event.room_id)
|
room = yield self.store.get_room(event.room_id)
|
||||||
|
|
||||||
if not room:
|
if not room:
|
||||||
# Huh, let's try and get the current state
|
# Huh, let's try and get the current state
|
||||||
try:
|
try:
|
||||||
yield self.get_state_for_room(
|
yield self.replication_layer.get_state_for_context(
|
||||||
event.origin, event.room_id
|
origin, event.room_id
|
||||||
)
|
)
|
||||||
|
|
||||||
hosts = yield self.store.get_joined_hosts_for_room(
|
hosts = yield self.store.get_joined_hosts_for_room(
|
||||||
|
@ -163,9 +155,9 @@ class FederationHandler(BaseHandler):
|
||||||
if self.hs.hostname in hosts:
|
if self.hs.hostname in hosts:
|
||||||
try:
|
try:
|
||||||
yield self.store.store_room(
|
yield self.store.store_room(
|
||||||
event.room_id,
|
room_id=event.room_id,
|
||||||
"",
|
room_creator_user_id="",
|
||||||
is_public=False
|
is_public=False,
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
@ -188,27 +180,14 @@ class FederationHandler(BaseHandler):
|
||||||
@log_function
|
@log_function
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def backfill(self, dest, room_id, limit):
|
def backfill(self, dest, room_id, limit):
|
||||||
events = yield self._backfill(dest, room_id, limit)
|
|
||||||
|
|
||||||
for event in events:
|
|
||||||
try:
|
|
||||||
yield self.store.persist_event(event, backfilled=True)
|
|
||||||
except:
|
|
||||||
logger.exception("Failed to persist event: %s", event)
|
|
||||||
|
|
||||||
defer.returnValue(events)
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
|
||||||
def _backfill(self, dest, room_id, limit):
|
|
||||||
pdus = yield self.replication_layer.backfill(dest, room_id, limit)
|
pdus = yield self.replication_layer.backfill(dest, room_id, limit)
|
||||||
|
|
||||||
if not pdus:
|
events = []
|
||||||
defer.returnValue([])
|
|
||||||
|
|
||||||
events = [
|
for pdu in pdus:
|
||||||
self.pdu_codec.event_from_pdu(pdu)
|
event = self.pdu_codec.event_from_pdu(pdu)
|
||||||
for pdu in pdus
|
events.append(event)
|
||||||
]
|
yield self.store.persist_event(event, backfilled=True)
|
||||||
|
|
||||||
defer.returnValue(events)
|
defer.returnValue(events)
|
||||||
|
|
||||||
|
@ -224,7 +203,9 @@ class FederationHandler(BaseHandler):
|
||||||
|
|
||||||
# First get current state to see if we are already joined.
|
# First get current state to see if we are already joined.
|
||||||
try:
|
try:
|
||||||
yield self.get_state_for_room(target_host, room_id)
|
yield self.replication_layer.get_state_for_context(
|
||||||
|
target_host, room_id
|
||||||
|
)
|
||||||
|
|
||||||
hosts = yield self.store.get_joined_hosts_for_room(room_id)
|
hosts = yield self.store.get_joined_hosts_for_room(room_id)
|
||||||
if self.hs.hostname in hosts:
|
if self.hs.hostname in hosts:
|
||||||
|
@ -254,8 +235,8 @@ class FederationHandler(BaseHandler):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
yield self.store.store_room(
|
yield self.store.store_room(
|
||||||
room_id,
|
room_id=room_id,
|
||||||
"",
|
room_creator_user_id="",
|
||||||
is_public=False
|
is_public=False
|
||||||
)
|
)
|
||||||
except:
|
except:
|
||||||
|
|
|
@ -451,16 +451,22 @@ class PresenceHandler(BaseHandler):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _start_polling_remote(self, user, domain, remoteusers):
|
def _start_polling_remote(self, user, domain, remoteusers):
|
||||||
|
to_poll = set()
|
||||||
|
|
||||||
for u in remoteusers:
|
for u in remoteusers:
|
||||||
if u not in self._remote_recvmap:
|
if u not in self._remote_recvmap:
|
||||||
self._remote_recvmap[u] = set()
|
self._remote_recvmap[u] = set()
|
||||||
|
to_poll.add(u)
|
||||||
|
|
||||||
self._remote_recvmap[u].add(user)
|
self._remote_recvmap[u].add(user)
|
||||||
|
|
||||||
|
if not to_poll:
|
||||||
|
return defer.succeed(None)
|
||||||
|
|
||||||
return self.federation.send_edu(
|
return self.federation.send_edu(
|
||||||
destination=domain,
|
destination=domain,
|
||||||
edu_type="m.presence",
|
edu_type="m.presence",
|
||||||
content={"poll": [u.to_string() for u in remoteusers]}
|
content={"poll": [u.to_string() for u in to_poll]}
|
||||||
)
|
)
|
||||||
|
|
||||||
@trace_function
|
@trace_function
|
||||||
|
@ -505,16 +511,22 @@ class PresenceHandler(BaseHandler):
|
||||||
|
|
||||||
@trace_function
|
@trace_function
|
||||||
def _stop_polling_remote(self, user, domain, remoteusers):
|
def _stop_polling_remote(self, user, domain, remoteusers):
|
||||||
|
to_unpoll = set()
|
||||||
|
|
||||||
for u in remoteusers:
|
for u in remoteusers:
|
||||||
self._remote_recvmap[u].remove(user)
|
self._remote_recvmap[u].remove(user)
|
||||||
|
|
||||||
if not self._remote_recvmap[u]:
|
if not self._remote_recvmap[u]:
|
||||||
del self._remote_recvmap[u]
|
del self._remote_recvmap[u]
|
||||||
|
to_unpoll.add(u)
|
||||||
|
|
||||||
|
if not to_unpoll:
|
||||||
|
return defer.succeed(None)
|
||||||
|
|
||||||
return self.federation.send_edu(
|
return self.federation.send_edu(
|
||||||
destination=domain,
|
destination=domain,
|
||||||
edu_type="m.presence",
|
edu_type="m.presence",
|
||||||
content={"unpoll": [u.to_string() for u in remoteusers]}
|
content={"unpoll": [u.to_string() for u in to_unpoll]}
|
||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
|
|
@ -17,11 +17,12 @@ from twisted.internet import defer
|
||||||
|
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
|
from synapse.api.errors import SynapseError, AuthError
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -27,7 +27,7 @@ class LoginRestServlet(RestServlet):
|
||||||
PASS_TYPE = "m.login.password"
|
PASS_TYPE = "m.login.password"
|
||||||
|
|
||||||
def on_GET(self, request):
|
def on_GET(self, request):
|
||||||
return (200, {"type": LoginRestServlet.PASS_TYPE})
|
return (200, {"flows": [{"type": LoginRestServlet.PASS_TYPE}]})
|
||||||
|
|
||||||
def on_OPTIONS(self, request):
|
def on_OPTIONS(self, request):
|
||||||
return (200, {})
|
return (200, {})
|
||||||
|
|
|
@ -126,11 +126,6 @@ class BaseHomeServer(object):
|
||||||
object."""
|
object."""
|
||||||
return UserID.from_string(s, hs=self)
|
return UserID.from_string(s, hs=self)
|
||||||
|
|
||||||
def parse_roomid(self, s):
|
|
||||||
"""Parse the string given by 's' as a Room ID and return a RoomID
|
|
||||||
object."""
|
|
||||||
return RoomID.from_string(s, hs=self)
|
|
||||||
|
|
||||||
def parse_roomalias(self, s):
|
def parse_roomalias(self, s):
|
||||||
"""Parse the string given by 's' as a Room Alias and return a RoomAlias
|
"""Parse the string given by 's' as a Room Alias and return a RoomAlias
|
||||||
object."""
|
object."""
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
|
|
||||||
from inspect import getcallargs
|
from inspect import getcallargs
|
||||||
|
from functools import wraps
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import inspect
|
import inspect
|
||||||
|
@ -28,6 +29,7 @@ def log_function(f):
|
||||||
lineno = f.func_code.co_firstlineno
|
lineno = f.func_code.co_firstlineno
|
||||||
pathname = f.func_code.co_filename
|
pathname = f.func_code.co_filename
|
||||||
|
|
||||||
|
@wraps(f)
|
||||||
def wrapped(*args, **kwargs):
|
def wrapped(*args, **kwargs):
|
||||||
name = f.__module__
|
name = f.__module__
|
||||||
logger = logging.getLogger(name)
|
logger = logging.getLogger(name)
|
||||||
|
|
|
@ -28,6 +28,8 @@ from mock import NonCallableMock, ANY
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
from ..utils import get_mock_call_args
|
||||||
|
|
||||||
logging.getLogger().addHandler(logging.NullHandler())
|
logging.getLogger().addHandler(logging.NullHandler())
|
||||||
|
|
||||||
|
|
||||||
|
@ -99,9 +101,13 @@ class FederationTestCase(unittest.TestCase):
|
||||||
|
|
||||||
mem_handler = self.handlers.room_member_handler
|
mem_handler = self.handlers.room_member_handler
|
||||||
self.assertEquals(1, mem_handler.change_membership.call_count)
|
self.assertEquals(1, mem_handler.change_membership.call_count)
|
||||||
self.assertEquals(True, mem_handler.change_membership.call_args[0][1])
|
call_args = get_mock_call_args(
|
||||||
|
lambda event, do_auth: None,
|
||||||
|
mem_handler.change_membership
|
||||||
|
)
|
||||||
|
self.assertEquals(True, call_args["do_auth"])
|
||||||
|
|
||||||
new_event = mem_handler.change_membership.call_args[0][0]
|
new_event = call_args["event"]
|
||||||
self.assertEquals(RoomMemberEvent.TYPE, new_event.type)
|
self.assertEquals(RoomMemberEvent.TYPE, new_event.type)
|
||||||
self.assertEquals(room_id, new_event.room_id)
|
self.assertEquals(room_id, new_event.room_id)
|
||||||
self.assertEquals(user_id, new_event.state_key)
|
self.assertEquals(user_id, new_event.state_key)
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
|
|
||||||
from twisted.trial import unittest
|
from twisted.trial import unittest
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer, reactor
|
||||||
|
|
||||||
from mock import Mock, call, ANY
|
from mock import Mock, call, ANY
|
||||||
import logging
|
import logging
|
||||||
|
@ -192,7 +192,6 @@ class PresenceStateTestCase(unittest.TestCase):
|
||||||
),
|
),
|
||||||
SynapseError
|
SynapseError
|
||||||
)
|
)
|
||||||
test_get_disallowed_state.skip = "Presence polling is disabled"
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_set_my_state(self):
|
def test_set_my_state(self):
|
||||||
|
@ -217,7 +216,6 @@ class PresenceStateTestCase(unittest.TestCase):
|
||||||
state={"state": OFFLINE})
|
state={"state": OFFLINE})
|
||||||
|
|
||||||
self.mock_stop.assert_called_with(self.u_apple)
|
self.mock_stop.assert_called_with(self.u_apple)
|
||||||
test_set_my_state.skip = "Presence polling is disabled"
|
|
||||||
|
|
||||||
|
|
||||||
class PresenceInvitesTestCase(unittest.TestCase):
|
class PresenceInvitesTestCase(unittest.TestCase):
|
||||||
|
@ -657,7 +655,6 @@ class PresencePushTestCase(unittest.TestCase):
|
||||||
observed_user=self.u_banana,
|
observed_user=self.u_banana,
|
||||||
statuscache=ANY), # self-reflection
|
statuscache=ANY), # self-reflection
|
||||||
]) # and no others...
|
]) # and no others...
|
||||||
test_push_local.skip = "Presence polling is disabled"
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_push_remote(self):
|
def test_push_remote(self):
|
||||||
|
@ -709,7 +706,6 @@ class PresencePushTestCase(unittest.TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
yield put_json.await_calls()
|
yield put_json.await_calls()
|
||||||
test_push_remote.skip = "Presence polling is disabled"
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_recv_remote(self):
|
def test_recv_remote(self):
|
||||||
|
@ -857,6 +853,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||||
'apple': [ "@banana:test", "@clementine:test" ],
|
'apple': [ "@banana:test", "@clementine:test" ],
|
||||||
'banana': [ "@apple:test" ],
|
'banana': [ "@apple:test" ],
|
||||||
'clementine': [ "@apple:test", "@potato:remote" ],
|
'clementine': [ "@apple:test", "@potato:remote" ],
|
||||||
|
'fig': [ "@potato:remote" ],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -909,6 +906,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||||
"apple": OFFLINE,
|
"apple": OFFLINE,
|
||||||
"banana": OFFLINE,
|
"banana": OFFLINE,
|
||||||
"clementine": OFFLINE,
|
"clementine": OFFLINE,
|
||||||
|
"fig": OFFLINE,
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_presence_state(user_localpart):
|
def get_presence_state(user_localpart):
|
||||||
|
@ -938,6 +936,7 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||||
self.u_apple = hs.parse_userid("@apple:test")
|
self.u_apple = hs.parse_userid("@apple:test")
|
||||||
self.u_banana = hs.parse_userid("@banana:test")
|
self.u_banana = hs.parse_userid("@banana:test")
|
||||||
self.u_clementine = hs.parse_userid("@clementine:test")
|
self.u_clementine = hs.parse_userid("@clementine:test")
|
||||||
|
self.u_fig = hs.parse_userid("@fig:test")
|
||||||
|
|
||||||
# Remote users
|
# Remote users
|
||||||
self.u_potato = hs.parse_userid("@potato:remote")
|
self.u_potato = hs.parse_userid("@potato:remote")
|
||||||
|
@ -1002,7 +1001,6 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||||
|
|
||||||
self.assertFalse("banana" in self.handler._local_pushmap)
|
self.assertFalse("banana" in self.handler._local_pushmap)
|
||||||
self.assertFalse("clementine" in self.handler._local_pushmap)
|
self.assertFalse("clementine" in self.handler._local_pushmap)
|
||||||
test_push_local.skip = "Presence polling is disabled"
|
|
||||||
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
@ -1028,10 +1026,32 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||||
yield put_json.await_calls()
|
yield put_json.await_calls()
|
||||||
|
|
||||||
# Gut-wrenching tests
|
# Gut-wrenching tests
|
||||||
self.assertTrue(self.u_potato in self.handler._remote_recvmap)
|
self.assertTrue(self.u_potato in self.handler._remote_recvmap,
|
||||||
|
msg="expected potato to be in _remote_recvmap"
|
||||||
|
)
|
||||||
self.assertTrue(self.u_clementine in
|
self.assertTrue(self.u_clementine in
|
||||||
self.handler._remote_recvmap[self.u_potato])
|
self.handler._remote_recvmap[self.u_potato])
|
||||||
|
|
||||||
|
# fig goes online; shouldn't send a second poll
|
||||||
|
yield self.handler.set_state(
|
||||||
|
target_user=self.u_fig, auth_user=self.u_fig,
|
||||||
|
state={"state": ONLINE}
|
||||||
|
)
|
||||||
|
|
||||||
|
reactor.iterate(delay=0)
|
||||||
|
|
||||||
|
put_json.assert_had_no_calls()
|
||||||
|
|
||||||
|
# fig goes offline
|
||||||
|
yield self.handler.set_state(
|
||||||
|
target_user=self.u_fig, auth_user=self.u_fig,
|
||||||
|
state={"state": OFFLINE}
|
||||||
|
)
|
||||||
|
|
||||||
|
reactor.iterate(delay=0)
|
||||||
|
|
||||||
|
put_json.assert_had_no_calls()
|
||||||
|
|
||||||
put_json.expect_call_and_return(
|
put_json.expect_call_and_return(
|
||||||
call("remote",
|
call("remote",
|
||||||
path="/matrix/federation/v1/send/1000001/",
|
path="/matrix/federation/v1/send/1000001/",
|
||||||
|
@ -1051,8 +1071,9 @@ class PresencePollingTestCase(unittest.TestCase):
|
||||||
|
|
||||||
put_json.await_calls()
|
put_json.await_calls()
|
||||||
|
|
||||||
self.assertFalse(self.u_potato in self.handler._remote_recvmap)
|
self.assertFalse(self.u_potato in self.handler._remote_recvmap,
|
||||||
test_remote_poll_send.skip = "Presence polling is disabled"
|
msg="expected potato not to be in _remote_recvmap"
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_remote_poll_receive(self):
|
def test_remote_poll_receive(self):
|
||||||
|
|
|
@ -139,7 +139,6 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||||
|
|
||||||
mocked_set.assert_called_with("apple",
|
mocked_set.assert_called_with("apple",
|
||||||
{"state": UNAVAILABLE, "status_msg": "Away"})
|
{"state": UNAVAILABLE, "status_msg": "Away"})
|
||||||
test_set_my_state.skip = "Presence polling is disabled"
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_push_local(self):
|
def test_push_local(self):
|
||||||
|
@ -214,7 +213,6 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||||
"displayname": "I am an Apple",
|
"displayname": "I am an Apple",
|
||||||
"avatar_url": "http://foo",
|
"avatar_url": "http://foo",
|
||||||
}, statuscache.state)
|
}, statuscache.state)
|
||||||
test_push_local.skip = "Presence polling is disabled"
|
|
||||||
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
@ -246,7 +244,6 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
test_push_remote.skip = "Presence polling is disabled"
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_recv_remote(self):
|
def test_recv_remote(self):
|
||||||
|
|
|
@ -114,7 +114,6 @@ class PresenceStateTestCase(unittest.TestCase):
|
||||||
self.assertEquals(200, code)
|
self.assertEquals(200, code)
|
||||||
mocked_set.assert_called_with("apple",
|
mocked_set.assert_called_with("apple",
|
||||||
{"state": UNAVAILABLE, "status_msg": "Away"})
|
{"state": UNAVAILABLE, "status_msg": "Away"})
|
||||||
test_set_my_status.skip = "Presence polling is disabled"
|
|
||||||
|
|
||||||
|
|
||||||
class PresenceListTestCase(unittest.TestCase):
|
class PresenceListTestCase(unittest.TestCase):
|
||||||
|
@ -318,4 +317,3 @@ class PresenceEventStreamTestCase(unittest.TestCase):
|
||||||
"mtime_age": 0,
|
"mtime_age": 0,
|
||||||
}},
|
}},
|
||||||
]}, response)
|
]}, response)
|
||||||
test_shortpoll.skip = "Presence polling is disabled"
|
|
||||||
|
|
|
@ -28,6 +28,16 @@ from mock import patch, Mock
|
||||||
import json
|
import json
|
||||||
import urlparse
|
import urlparse
|
||||||
|
|
||||||
|
from inspect import getcallargs
|
||||||
|
|
||||||
|
|
||||||
|
def get_mock_call_args(pattern_func, mock_func):
|
||||||
|
""" Return the arguments the mock function was called with interpreted
|
||||||
|
by the pattern functions argument list.
|
||||||
|
"""
|
||||||
|
invoked_args, invoked_kargs = mock_func.call_args
|
||||||
|
return getcallargs(pattern_func, *invoked_args, **invoked_kargs)
|
||||||
|
|
||||||
|
|
||||||
# This is a mock /resource/ not an entire server
|
# This is a mock /resource/ not an entire server
|
||||||
class MockHttpResource(HttpServer):
|
class MockHttpResource(HttpServer):
|
||||||
|
@ -238,8 +248,11 @@ class DeferredMockCallable(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.expectations = []
|
self.expectations = []
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
def __call__(self, *args, **kwargs):
|
||||||
|
self.calls.append((args, kwargs))
|
||||||
|
|
||||||
if not self.expectations:
|
if not self.expectations:
|
||||||
raise ValueError("%r has no pending calls to handle call(%s)" % (
|
raise ValueError("%r has no pending calls to handle call(%s)" % (
|
||||||
self, _format_call(args, kwargs))
|
self, _format_call(args, kwargs))
|
||||||
|
@ -262,3 +275,15 @@ class DeferredMockCallable(object):
|
||||||
while self.expectations:
|
while self.expectations:
|
||||||
(_, _, d) = self.expectations.pop(0)
|
(_, _, d) = self.expectations.pop(0)
|
||||||
yield d
|
yield d
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
def assert_had_no_calls(self):
|
||||||
|
if self.calls:
|
||||||
|
calls = self.calls
|
||||||
|
self.calls = []
|
||||||
|
|
||||||
|
raise AssertionError("Expected not to received any calls, got:\n" +
|
||||||
|
"\n".join([
|
||||||
|
"call(%s)" % _format_call(c[0], c[1]) for c in calls
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
|
@ -27,13 +27,15 @@ 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', function(matrixService, $rootScope) {
|
.factory('eventHandlerService', ['matrixService', '$rootScope', '$q', function(matrixService, $rootScope, $q) {
|
||||||
var MSG_EVENT = "MSG_EVENT";
|
var MSG_EVENT = "MSG_EVENT";
|
||||||
var MEMBER_EVENT = "MEMBER_EVENT";
|
var MEMBER_EVENT = "MEMBER_EVENT";
|
||||||
var PRESENCE_EVENT = "PRESENCE_EVENT";
|
var PRESENCE_EVENT = "PRESENCE_EVENT";
|
||||||
|
|
||||||
|
var InitialSyncDeferred = $q.defer();
|
||||||
|
|
||||||
$rootScope.events = {
|
$rootScope.events = {
|
||||||
rooms: {}, // will contain roomId: { messages:[], members:{userid1: event} }
|
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
|
||||||
};
|
};
|
||||||
|
|
||||||
$rootScope.presence = {};
|
$rootScope.presence = {};
|
||||||
|
@ -47,11 +49,11 @@ angular.module('eventHandlerService', [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var reInitRoom = function(room_id) {
|
var resetRoomMessages = function(room_id) {
|
||||||
$rootScope.events.rooms[room_id] = {};
|
if ($rootScope.events.rooms[room_id]) {
|
||||||
$rootScope.events.rooms[room_id].messages = [];
|
$rootScope.events.rooms[room_id].messages = [];
|
||||||
$rootScope.events.rooms[room_id].members = {};
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var handleMessage = function(event, isLiveEvent) {
|
var handleMessage = function(event, isLiveEvent) {
|
||||||
initRoom(event.room_id);
|
initRoom(event.room_id);
|
||||||
|
@ -125,8 +127,18 @@ angular.module('eventHandlerService', [])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
reInitRoom: function(room_id) {
|
handleInitialSyncDone: function() {
|
||||||
reInitRoom(room_id);
|
console.log("# handleInitialSyncDone");
|
||||||
|
InitialSyncDeferred.resolve($rootScope.events, $rootScope.presence);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Returns a promise that resolves when the initialSync request has been processed
|
||||||
|
waitForInitialSyncCompletion: function() {
|
||||||
|
return InitialSyncDeferred.promise;
|
||||||
|
},
|
||||||
|
|
||||||
|
resetRoomMessages: function(room_id) {
|
||||||
|
resetRoomMessages(room_id);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -96,7 +96,7 @@ angular.module('eventStreamService', [])
|
||||||
);
|
);
|
||||||
|
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
}
|
};
|
||||||
|
|
||||||
var startEventStream = function() {
|
var startEventStream = function() {
|
||||||
settings.shouldPoll = true;
|
settings.shouldPoll = true;
|
||||||
|
@ -110,18 +110,17 @@ angular.module('eventStreamService', [])
|
||||||
for (var i = 0; i < rooms.length; ++i) {
|
for (var i = 0; i < rooms.length; ++i) {
|
||||||
var room = rooms[i];
|
var room = rooms[i];
|
||||||
if ("state" in room) {
|
if ("state" in room) {
|
||||||
for (var j = 0; j < room.state.length; ++j) {
|
eventHandlerService.handleEvents(room.state, false);
|
||||||
eventHandlerService.handleEvents(room.state[j], false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var presence = response.data.presence;
|
var presence = response.data.presence;
|
||||||
for (var i = 0; i < presence.length; ++i) {
|
eventHandlerService.handleEvents(presence, false);
|
||||||
eventHandlerService.handleEvent(presence[i], false);
|
|
||||||
}
|
|
||||||
|
|
||||||
settings.from = response.data.end
|
// Initial sync is done
|
||||||
|
eventHandlerService.handleInitialSyncDone();
|
||||||
|
|
||||||
|
settings.from = response.data.end;
|
||||||
doEventStream(deferred);
|
doEventStream(deferred);
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
|
|
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController'])
|
angular.module('HomeController', ['matrixService', 'eventHandlerService', 'RecentsController'])
|
||||||
.controller('HomeController', ['$scope', '$location', 'matrixService', 'eventHandlerService', 'eventStreamService',
|
.controller('HomeController', ['$scope', '$location', 'matrixService',
|
||||||
function($scope, $location, matrixService, eventHandlerService, eventStreamService) {
|
function($scope, $location, matrixService) {
|
||||||
|
|
||||||
$scope.config = matrixService.config();
|
$scope.config = matrixService.config();
|
||||||
$scope.public_rooms = [];
|
$scope.public_rooms = [];
|
||||||
|
|
|
@ -17,21 +17,32 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
|
angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
|
||||||
.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService', 'eventStreamService',
|
.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService',
|
||||||
function($scope, matrixService, eventHandlerService, eventStreamService) {
|
function($scope, matrixService, eventHandlerService) {
|
||||||
$scope.rooms = {};
|
$scope.rooms = {};
|
||||||
|
|
||||||
// $scope of the parent where the recents component is included can override this value
|
// $scope 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
|
||||||
$scope.recentsSelectedRoomID;
|
$scope.recentsSelectedRoomID;
|
||||||
|
|
||||||
|
var listenToEventStream = function() {
|
||||||
// Refresh the list on matrix invitation and message event
|
// Refresh the list on matrix invitation and message event
|
||||||
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
||||||
refresh();
|
var config = matrixService.config();
|
||||||
|
if (event.state_key === config.user_id && event.content.membership === "invite") {
|
||||||
|
console.log("Invited to room " + event.room_id);
|
||||||
|
// FIXME push membership to top level key to match /im/sync
|
||||||
|
event.membership = event.content.membership;
|
||||||
|
// FIXME bodge a nicer name than the room ID for this invite.
|
||||||
|
event.room_display_name = event.user_id + "'s room";
|
||||||
|
$scope.rooms[event.room_id] = event;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
|
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
|
||||||
refresh();
|
$scope.rooms[event.room_id].lastMsg = event;
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
var refresh = function() {
|
var refresh = function() {
|
||||||
// List all rooms joined or been invited to
|
// List all rooms joined or been invited to
|
||||||
|
@ -56,6 +67,9 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
|
||||||
for (var i = 0; i < presence.length; ++i) {
|
for (var i = 0; i < presence.length; ++i) {
|
||||||
eventHandlerService.handleEvent(presence[i], false);
|
eventHandlerService.handleEvent(presence[i], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From now, update recents from the stream
|
||||||
|
listenToEventStream();
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Failure: " + error.data;
|
$scope.feedback = "Failure: " + error.data;
|
||||||
|
|
|
@ -15,8 +15,8 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
|
angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
|
||||||
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventStreamService', 'eventHandlerService', 'mFileUpload', 'mUtilities', '$rootScope',
|
.controller('RoomController', ['$scope', '$http', '$timeout', '$routeParams', '$location', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mUtilities', '$rootScope',
|
||||||
function($scope, $http, $timeout, $routeParams, $location, matrixService, eventStreamService, eventHandlerService, mFileUpload, mUtilities, $rootScope) {
|
function($scope, $http, $timeout, $routeParams, $location, matrixService, eventHandlerService, mFileUpload, mUtilities, $rootScope) {
|
||||||
'use strict';
|
'use strict';
|
||||||
var MESSAGES_PER_PAGINATION = 30;
|
var MESSAGES_PER_PAGINATION = 30;
|
||||||
var THUMBNAIL_SIZE = 320;
|
var THUMBNAIL_SIZE = 320;
|
||||||
|
@ -282,7 +282,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room_id_or_alias && '!' === room_id_or_alias[0]) {
|
if (room_id_or_alias && '!' === room_id_or_alias[0]) {
|
||||||
// Yes. We can start right now
|
// Yes. We can go on right now
|
||||||
$scope.room_id = room_id_or_alias;
|
$scope.room_id = room_id_or_alias;
|
||||||
$scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
|
$scope.room_alias = matrixService.getRoomIdToAliasMapping($scope.room_id);
|
||||||
onInit2();
|
onInit2();
|
||||||
|
@ -313,7 +313,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
|
||||||
$scope.room_id = response.data.room_id;
|
$scope.room_id = response.data.room_id;
|
||||||
console.log(" -> Room ID: " + $scope.room_id);
|
console.log(" -> Room ID: " + $scope.room_id);
|
||||||
|
|
||||||
// Now, we can start
|
// Now, we can go on
|
||||||
onInit2();
|
onInit2();
|
||||||
},
|
},
|
||||||
function () {
|
function () {
|
||||||
|
@ -325,34 +325,59 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput', 'mUtilities'])
|
||||||
};
|
};
|
||||||
|
|
||||||
var onInit2 = function() {
|
var onInit2 = function() {
|
||||||
eventHandlerService.reInitRoom($scope.room_id);
|
console.log("onInit2");
|
||||||
|
|
||||||
// Make recents highlight the current room
|
// Make sure the initialSync has been before going further
|
||||||
$scope.recentsSelectedRoomID = $scope.room_id;
|
eventHandlerService.waitForInitialSyncCompletion().then(
|
||||||
|
function() {
|
||||||
|
var needsToJoin = true;
|
||||||
|
|
||||||
// Join the room
|
// The room members is available in the data fetched by initialSync
|
||||||
|
if ($rootScope.events.rooms[$scope.room_id]) {
|
||||||
|
var members = $rootScope.events.rooms[$scope.room_id].members;
|
||||||
|
|
||||||
|
// Update the member list
|
||||||
|
for (var i in members) {
|
||||||
|
var member = members[i];
|
||||||
|
updateMemberList(member);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the user has already join the room
|
||||||
|
if ($scope.state.user_id in members) {
|
||||||
|
if ("join" === members[$scope.state.user_id].membership) {
|
||||||
|
needsToJoin = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do we to join the room before starting?
|
||||||
|
if (needsToJoin) {
|
||||||
matrixService.join($scope.room_id).then(
|
matrixService.join($scope.room_id).then(
|
||||||
function() {
|
function() {
|
||||||
console.log("Joined room "+$scope.room_id);
|
console.log("Joined room "+$scope.room_id);
|
||||||
|
onInit3();
|
||||||
// Get the current member list
|
|
||||||
matrixService.getMemberList($scope.room_id).then(
|
|
||||||
function(response) {
|
|
||||||
for (var i = 0; i < response.data.chunk.length; i++) {
|
|
||||||
var chunk = response.data.chunk[i];
|
|
||||||
updateMemberList(chunk);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
function(error) {
|
|
||||||
$scope.feedback = "Failed get member list: " + error.data.error;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
paginate(MESSAGES_PER_PAGINATION);
|
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(reason) {
|
||||||
$scope.feedback = "Can't join room: " + reason;
|
$scope.feedback = "Can't join room: " + reason;
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
onInit3();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
var onInit3 = function() {
|
||||||
|
console.log("onInit3");
|
||||||
|
|
||||||
|
// TODO: We should be able to keep them
|
||||||
|
eventHandlerService.resetRoomMessages($scope.room_id);
|
||||||
|
|
||||||
|
// Make recents highlight the current room
|
||||||
|
$scope.recentsSelectedRoomID = $scope.room_id;
|
||||||
|
|
||||||
|
paginate(MESSAGES_PER_PAGINATION);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.inviteUser = function(user_id) {
|
$scope.inviteUser = function(user_id) {
|
||||||
|
|
Loading…
Reference in a new issue