mirror of
https://git.anonymousland.org/anonymousland/synapse-product.git
synced 2024-10-01 08:25:44 -04:00
Merge branch 'release-v0.2.2' of github.com:matrix-org/synapse
This commit is contained in:
commit
d12feed623
3
.gitignore
vendored
3
.gitignore
vendored
@ -18,9 +18,12 @@ htmlcov
|
|||||||
demo/*.db
|
demo/*.db
|
||||||
demo/*.log
|
demo/*.log
|
||||||
demo/*.pid
|
demo/*.pid
|
||||||
|
demo/etc
|
||||||
|
|
||||||
graph/*.svg
|
graph/*.svg
|
||||||
graph/*.png
|
graph/*.png
|
||||||
graph/*.dot
|
graph/*.dot
|
||||||
|
|
||||||
|
webclient/config.js
|
||||||
|
|
||||||
uploads
|
uploads
|
||||||
|
20
CHANGES.rst
20
CHANGES.rst
@ -1,3 +1,23 @@
|
|||||||
|
Changes in synapse 0.2.2 (2014-09-06)
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* When the server returns state events it now also includes the previous
|
||||||
|
content.
|
||||||
|
* Add support for inviting people when creating a new room.
|
||||||
|
* Make the homeserver inform the room via `m.room.aliases` when a new alias
|
||||||
|
is added for a room.
|
||||||
|
* Validate `m.room.power_level` events.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Add support for captchas on registration.
|
||||||
|
* Handle `m.room.aliases` events.
|
||||||
|
* Asynchronously send messages and show a local echo.
|
||||||
|
* Inform the UI when a message failed to send.
|
||||||
|
* Only autoscroll on receiving a new message if the user was already at the
|
||||||
|
bottom of the screen.
|
||||||
|
* Add support for ban/kick reasons.
|
||||||
|
|
||||||
Changes in synapse 0.2.1 (2014-09-03)
|
Changes in synapse 0.2.1 (2014-09-03)
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
@ -347,11 +347,12 @@ Receiving live updates on a client
|
|||||||
Clients can receive new events by long-polling the home server. This will hold open the
|
Clients can receive new events by long-polling the home server. This will hold open the
|
||||||
HTTP connection for a short period of time waiting for new events, returning early if an
|
HTTP connection for a short period of time waiting for new events, returning early if an
|
||||||
event occurs. This is called the `Event Stream`_. All events which are visible to the
|
event occurs. This is called the `Event Stream`_. All events which are visible to the
|
||||||
client and match the client's query will appear in the event stream. When the request
|
client will appear in the event stream. When the request
|
||||||
returns, an ``end`` token is included in the response. This token can be used in the next
|
returns, an ``end`` token is included in the response. This token can be used in the next
|
||||||
request to continue where the client left off.
|
request to continue where the client left off.
|
||||||
|
|
||||||
.. TODO
|
.. TODO
|
||||||
|
How do we filter the event stream?
|
||||||
Do we ever return multiple events in a single request? Don't we get lots of request
|
Do we ever return multiple events in a single request? Don't we get lots of request
|
||||||
setup RTT latency if we only do one event per request? Do we ever support streaming
|
setup RTT latency if we only do one event per request? Do we ever support streaming
|
||||||
requests? Why not websockets?
|
requests? Why not websockets?
|
||||||
@ -417,6 +418,16 @@ which can be set when creating a room:
|
|||||||
If this is included, an ``m.room.topic`` event will be sent into the room to indicate the
|
If this is included, an ``m.room.topic`` event will be sent into the room to indicate the
|
||||||
topic for the room. See `Room Events`_ for more information on ``m.room.topic``.
|
topic for the room. See `Room Events`_ for more information on ``m.room.topic``.
|
||||||
|
|
||||||
|
``invite``
|
||||||
|
Type:
|
||||||
|
List
|
||||||
|
Optional:
|
||||||
|
Yes
|
||||||
|
Value:
|
||||||
|
A list of user ids to invite.
|
||||||
|
Description:
|
||||||
|
This will tell the server to invite everyone in the list to the newly created room.
|
||||||
|
|
||||||
Example::
|
Example::
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -473,7 +484,9 @@ action in a room a user must have a suitable power level.
|
|||||||
|
|
||||||
Power levels for users are defined in ``m.room.power_levels``, where both
|
Power levels for users are defined in ``m.room.power_levels``, where both
|
||||||
a default and specific users' power levels can be set. By default all users
|
a default and specific users' power levels can be set. By default all users
|
||||||
have a power level of 0.
|
have a power level of 0, other than the room creator whose power level defaults to 100.
|
||||||
|
Power levels for users are tracked per-room even if the user is not present in
|
||||||
|
the room.
|
||||||
|
|
||||||
State events may contain a ``required_power_level`` key, which indicates the
|
State events may contain a ``required_power_level`` key, which indicates the
|
||||||
minimum power a user must have before they can update that state key. The only
|
minimum power a user must have before they can update that state key. The only
|
||||||
@ -483,11 +496,11 @@ To perform certain actions there are additional power level requirements
|
|||||||
defined in the following state events:
|
defined in the following state events:
|
||||||
|
|
||||||
- ``m.room.send_event_level`` defines the minimum level for sending non-state
|
- ``m.room.send_event_level`` defines the minimum level for sending non-state
|
||||||
events. Defaults to 5.
|
events. Defaults to 50.
|
||||||
- ``m.room.add_state_level`` defines the minimum level for adding new state,
|
- ``m.room.add_state_level`` defines the minimum level for adding new state,
|
||||||
rather than updating existing state. Defaults to 5.
|
rather than updating existing state. Defaults to 50.
|
||||||
- ``m.room.ops_level`` defines the minimum levels to ban and kick other users.
|
- ``m.room.ops_level`` defines the minimum levels to ban and kick other users.
|
||||||
This defaults to a kick and ban levels of 5 each.
|
This defaults to a kick and ban levels of 50 each.
|
||||||
|
|
||||||
|
|
||||||
Joining rooms
|
Joining rooms
|
||||||
@ -908,6 +921,22 @@ prefixed with ``m.``
|
|||||||
``ban_level`` will be greater than or equal to ``kick_level`` since
|
``ban_level`` will be greater than or equal to ``kick_level`` since
|
||||||
banning is more severe than kicking.
|
banning is more severe than kicking.
|
||||||
|
|
||||||
|
``m.room.aliases``
|
||||||
|
Summary:
|
||||||
|
These state events are used to inform the room about what room aliases it has.
|
||||||
|
Type:
|
||||||
|
State event
|
||||||
|
JSON format:
|
||||||
|
``{ "aliases": ["string", ...] }``
|
||||||
|
Example:
|
||||||
|
``{ "aliases": ["#foo:example.com"] }``
|
||||||
|
Description:
|
||||||
|
A server `may` inform the room that it has added or removed an alias for
|
||||||
|
the room. This is purely for informational purposes and may become stale.
|
||||||
|
Clients `should` check that the room alias is still valid before using it.
|
||||||
|
The ``state_key`` of the event is the homeserver which owns the room
|
||||||
|
alias.
|
||||||
|
|
||||||
``m.room.message``
|
``m.room.message``
|
||||||
Summary:
|
Summary:
|
||||||
A message.
|
A message.
|
||||||
@ -1124,19 +1153,104 @@ Typing notifications
|
|||||||
|
|
||||||
Voice over IP
|
Voice over IP
|
||||||
=============
|
=============
|
||||||
.. NOTE::
|
Matrix can also be used to set up VoIP calls. This is part of the core specification,
|
||||||
This section is a work in progress.
|
although is still in a very early stage. Voice (and video) over Matrix is based on
|
||||||
|
the WebRTC standards.
|
||||||
|
|
||||||
.. TODO Dave
|
Call events are sent to a room, like any other event. This means that clients
|
||||||
- what are the event types.
|
must only send call events to rooms with exactly two participants as currently
|
||||||
- what are the valid keys/values. What do they represent. Any gotchas?
|
the WebRTC standard is based around two-party communication.
|
||||||
- In what sequence should the events be sent?
|
|
||||||
- How do you accept / decline inbound calls? How do you make outbound calls?
|
|
||||||
Give examples.
|
|
||||||
- How does negotiation work? Give examples.
|
|
||||||
- How do you hang up?
|
|
||||||
- What does call log information look like e.g. duration of call?
|
|
||||||
|
|
||||||
|
Events
|
||||||
|
------
|
||||||
|
``m.call.invite``
|
||||||
|
This event is sent by the caller when they wish to establish a call.
|
||||||
|
|
||||||
|
Required keys:
|
||||||
|
- ``call_id`` : "string" - A unique identifier for the call
|
||||||
|
- ``offer`` : "offer object" - The session description
|
||||||
|
- ``version`` : "integer" - The version of the VoIP specification this message
|
||||||
|
adheres to. This specification is version 0.
|
||||||
|
|
||||||
|
Optional keys:
|
||||||
|
None.
|
||||||
|
Example:
|
||||||
|
``{ "version" : 0, "call_id": "12345", "offer": { "type" : "offer", "sdp" : "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]" } }``
|
||||||
|
|
||||||
|
``Offer Object``
|
||||||
|
Required keys:
|
||||||
|
- ``type`` : "string" - The type of session description, in this case 'offer'
|
||||||
|
- ``sdp`` : "string" - The SDP text of the session description
|
||||||
|
|
||||||
|
``m.call.candidate``
|
||||||
|
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
|
||||||
|
communicate.
|
||||||
|
|
||||||
|
Required keys:
|
||||||
|
- ``call_id`` : "string" - The ID of the call this event relates to
|
||||||
|
- ``version`` : "integer" - The version of the VoIP specification this messages
|
||||||
|
adheres to. his specification is version 0.
|
||||||
|
- ``candidate`` : "candidate object" - Object describing the candidate.
|
||||||
|
|
||||||
|
``Candidate Object``
|
||||||
|
|
||||||
|
Required Keys:
|
||||||
|
- ``sdpMid`` : "string" - The SDP media type this candidate is intended for.
|
||||||
|
- ``sdpMLineIndex`` : "integer" - The index of the SDP 'm' line this
|
||||||
|
candidate is intended for
|
||||||
|
- ``candidate`` : "string" - The SDP 'a' line of the candidate
|
||||||
|
|
||||||
|
``m.call.answer``
|
||||||
|
|
||||||
|
Required keys:
|
||||||
|
- ``call_id`` : "string" - The ID of the call this event relates to
|
||||||
|
- ``version`` : "integer" - The version of the VoIP specification this messages
|
||||||
|
- ``answer`` : "answer object" - Object giving the SDK answer
|
||||||
|
|
||||||
|
``Answer Object``
|
||||||
|
|
||||||
|
Required keys:
|
||||||
|
- ``type`` : "string" - The type of session description. 'answer' in this case.
|
||||||
|
- ``sdp`` : "string" - The SDP text of the session description
|
||||||
|
|
||||||
|
``m.call.hangup``
|
||||||
|
Sent by either party to signal their termination of the call. This can be sent either once
|
||||||
|
the call has has been established or before to abort the call.
|
||||||
|
|
||||||
|
Required keys:
|
||||||
|
- ``call_id`` : "string" - The ID of the call this event relates to
|
||||||
|
- ``version`` : "integer" - The version of the VoIP specification this messages
|
||||||
|
|
||||||
|
Message Exchange
|
||||||
|
----------------
|
||||||
|
A call is set up with messages exchanged as follows:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Caller Callee
|
||||||
|
m.call.invite ----------->
|
||||||
|
m.call.candidate -------->
|
||||||
|
[more candidates events]
|
||||||
|
User answers call
|
||||||
|
<------ m.call.answer
|
||||||
|
[...]
|
||||||
|
<------ m.call.hangup
|
||||||
|
|
||||||
|
Or a rejected call:
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
Caller Callee
|
||||||
|
m.call.invite ----------->
|
||||||
|
m.call.candidate -------->
|
||||||
|
[more candidates events]
|
||||||
|
User rejects call
|
||||||
|
<------- m.call.hangup
|
||||||
|
|
||||||
|
Calls are negotiated according to the WebRTC specification.
|
||||||
|
|
||||||
|
|
||||||
Profiles
|
Profiles
|
||||||
========
|
========
|
||||||
.. NOTE::
|
.. NOTE::
|
||||||
@ -1151,8 +1265,8 @@ Profiles
|
|||||||
- Display name changes also generates m.room.member with displayname key f.e. room
|
- Display name changes also generates m.room.member with displayname key f.e. room
|
||||||
the user is in.
|
the user is in.
|
||||||
|
|
||||||
Internally within Matrix users are referred to by their user ID, which is not a
|
Internally within Matrix users are referred to by their user ID, which is typically
|
||||||
human-friendly string. Profiles grant users the ability to see human-readable
|
a compact unique identifier. Profiles grant users the ability to see human-readable
|
||||||
names for other users that are in some way meaningful to them. Additionally,
|
names for other users that are in some way meaningful to them. Additionally,
|
||||||
profiles can publish additional information, such as the user's age or location.
|
profiles can publish additional information, such as the user's age or location.
|
||||||
|
|
||||||
@ -1466,17 +1580,19 @@ Federation is the term used to describe how to communicate between Matrix home
|
|||||||
servers. Federation is a mechanism by which two home servers can exchange
|
servers. Federation is a mechanism by which two home servers can exchange
|
||||||
Matrix event messages, both as a real-time push of current events, and as a
|
Matrix event messages, both as a real-time push of current events, and as a
|
||||||
historic fetching mechanism to synchronise past history for clients to view. It
|
historic fetching mechanism to synchronise past history for clients to view. It
|
||||||
uses HTTP connections between each pair of servers involved as the underlying
|
uses HTTPS connections between each pair of servers involved as the underlying
|
||||||
transport. Messages are exchanged between servers in real-time by active pushing
|
transport. Messages are exchanged between servers in real-time by active pushing
|
||||||
from each server's HTTP client into the server of the other. Queries to fetch
|
from each server's HTTP client into the server of the other. Queries to fetch
|
||||||
historic data for the purpose of back-filling scrollback buffers and the like
|
historic data for the purpose of back-filling scrollback buffers and the like
|
||||||
can also be performed.
|
can also be performed. Currently routing of messages between homeservers is full
|
||||||
|
mesh (like email) - however, fan-out refinements to this design are currently
|
||||||
|
under consideration.
|
||||||
|
|
||||||
There are three main kinds of communication that occur between home servers:
|
There are three main kinds of communication that occur between home servers:
|
||||||
|
|
||||||
:Queries:
|
:Queries:
|
||||||
These are single request/response interactions between a given pair of
|
These are single request/response interactions between a given pair of
|
||||||
servers, initiated by one side sending an HTTP GET request to obtain some
|
servers, initiated by one side sending an HTTPS GET request to obtain some
|
||||||
information, and responded by the other. They are not persisted and contain
|
information, and responded by the other. They are not persisted and contain
|
||||||
no long-term significant history. They simply request a snapshot state at the
|
no long-term significant history. They simply request a snapshot state at the
|
||||||
instant the query is made.
|
instant the query is made.
|
||||||
@ -1692,7 +1808,7 @@ by the same origin as the current one, or other origins.
|
|||||||
Because of the distributed nature of participants in a Matrix conversation, it
|
Because of the distributed nature of participants in a Matrix conversation, it
|
||||||
is impossible to establish a globally-consistent total ordering on the events.
|
is impossible to establish a globally-consistent total ordering on the events.
|
||||||
However, by annotating each outbound PDU at its origin with IDs of other PDUs it
|
However, by annotating each outbound PDU at its origin with IDs of other PDUs it
|
||||||
has received, a partial ordering can be constructed allowing causallity
|
has received, a partial ordering can be constructed allowing causality
|
||||||
relationships to be preserved. A client can then display these messages to the
|
relationships to be preserved. A client can then display these messages to the
|
||||||
end-user in some order consistent with their content and ensure that no message
|
end-user in some order consistent with their content and ensure that no message
|
||||||
that is semantically in reply of an earlier one is ever displayed before it.
|
that is semantically in reply of an earlier one is ever displayed before it.
|
||||||
@ -1778,7 +1894,7 @@ Retrieves a sliding-window history of previous PDUs that occurred on the
|
|||||||
given context. Starting from the PDU ID(s) given in the "v" argument, the
|
given context. Starting from the PDU ID(s) given in the "v" argument, the
|
||||||
PDUs that preceeded it are retrieved, up to a total number given by the
|
PDUs that preceeded it are retrieved, up to a total number given by the
|
||||||
"limit" argument. These are then returned in a new Transaction containing all
|
"limit" argument. These are then returned in a new Transaction containing all
|
||||||
off the PDUs.
|
of the PDUs.
|
||||||
|
|
||||||
|
|
||||||
To stream events all the events::
|
To stream events all the events::
|
||||||
@ -1858,6 +1974,10 @@ victim would then include in their view of the chatroom history. Other servers
|
|||||||
in the chatroom would reject the invalid messages and potentially reject the
|
in the chatroom would reject the invalid messages and potentially reject the
|
||||||
victims messages as well since they depended on the invalid messages.
|
victims messages as well since they depended on the invalid messages.
|
||||||
|
|
||||||
|
.. TODO
|
||||||
|
Track trustworthiness of HS or users based on if they try to pretend they
|
||||||
|
haven't seen recent events, and fake a splitbrain... --M
|
||||||
|
|
||||||
Threat: Block Network Traffic
|
Threat: Block Network Traffic
|
||||||
+++++++++++++++++++++++++++++
|
+++++++++++++++++++++++++++++
|
||||||
|
|
||||||
@ -1963,6 +2083,9 @@ The ``retry_after_ms`` key SHOULD be included to tell the client how long they h
|
|||||||
in milliseconds before they can try again.
|
in milliseconds before they can try again.
|
||||||
|
|
||||||
.. TODO
|
.. TODO
|
||||||
|
- Surely we should recommend an algorithm for the rate limiting, rather than letting every
|
||||||
|
homeserver come up with their own idea, causing totally unpredictable performance over
|
||||||
|
federated rooms?
|
||||||
- crypto (s-s auth)
|
- crypto (s-s auth)
|
||||||
- E2E
|
- E2E
|
||||||
- Lawful intercept + Key Escrow
|
- Lawful intercept + Key Escrow
|
||||||
@ -1973,6 +2096,9 @@ Policy Servers
|
|||||||
.. NOTE::
|
.. NOTE::
|
||||||
This section is a work in progress.
|
This section is a work in progress.
|
||||||
|
|
||||||
|
.. TODO
|
||||||
|
We should mention them in the Architecture section at least...
|
||||||
|
|
||||||
Content repository
|
Content repository
|
||||||
==================
|
==================
|
||||||
.. NOTE::
|
.. NOTE::
|
||||||
@ -2071,6 +2197,9 @@ Transaction:
|
|||||||
A message which relates to the communication between a given pair of servers.
|
A message which relates to the communication between a given pair of servers.
|
||||||
A transaction contains possibly-empty lists of PDUs and EDUs.
|
A transaction contains possibly-empty lists of PDUs and EDUs.
|
||||||
|
|
||||||
|
.. TODO
|
||||||
|
This glossary contradicts the terms used above - especially on State Events v. "State"
|
||||||
|
and Non-State Events v. "Events". We need better consistent names.
|
||||||
|
|
||||||
.. Links through the external API docs are below
|
.. Links through the external API docs are below
|
||||||
.. =============================================
|
.. =============================================
|
||||||
@ -2118,3 +2247,4 @@ Transaction:
|
|||||||
.. _/join/<room_alias_or_id>: /docs/api/client-server/#!/-rooms/join
|
.. _/join/<room_alias_or_id>: /docs/api/client-server/#!/-rooms/join
|
||||||
|
|
||||||
.. _`Event Stream`: /docs/api/client-server/#!/-events/get_event_stream
|
.. _`Event Stream`: /docs/api/client-server/#!/-events/get_event_stream
|
||||||
|
|
||||||
|
@ -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.1"
|
__version__ = "0.2.2"
|
||||||
|
@ -18,8 +18,8 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.constants import Membership, JoinRules
|
from synapse.api.constants import Membership, JoinRules
|
||||||
from synapse.api.errors import AuthError, StoreError, Codes
|
from synapse.api.errors import AuthError, StoreError, Codes, SynapseError
|
||||||
from synapse.api.events.room import RoomMemberEvent
|
from synapse.api.events.room import RoomMemberEvent, RoomPowerLevelsEvent
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -67,6 +67,9 @@ class Auth(object):
|
|||||||
else:
|
else:
|
||||||
yield self._can_send_event(event)
|
yield self._can_send_event(event)
|
||||||
|
|
||||||
|
if event.type == RoomPowerLevelsEvent.TYPE:
|
||||||
|
yield self._check_power_levels(event)
|
||||||
|
|
||||||
defer.returnValue(True)
|
defer.returnValue(True)
|
||||||
else:
|
else:
|
||||||
raise AuthError(500, "Unknown event: %s" % event)
|
raise AuthError(500, "Unknown event: %s" % event)
|
||||||
@ -172,7 +175,7 @@ class Auth(object):
|
|||||||
if kick_level:
|
if kick_level:
|
||||||
kick_level = int(kick_level)
|
kick_level = int(kick_level)
|
||||||
else:
|
else:
|
||||||
kick_level = 5
|
kick_level = 50
|
||||||
|
|
||||||
if user_level < kick_level:
|
if user_level < kick_level:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
@ -189,7 +192,7 @@ class Auth(object):
|
|||||||
if ban_level:
|
if ban_level:
|
||||||
ban_level = int(ban_level)
|
ban_level = int(ban_level)
|
||||||
else:
|
else:
|
||||||
ban_level = 5 # FIXME (erikj): What should we do here?
|
ban_level = 50 # FIXME (erikj): What should we do here?
|
||||||
|
|
||||||
if user_level < ban_level:
|
if user_level < ban_level:
|
||||||
raise AuthError(403, "You don't have permission to ban")
|
raise AuthError(403, "You don't have permission to ban")
|
||||||
@ -305,7 +308,9 @@ class Auth(object):
|
|||||||
else:
|
else:
|
||||||
user_level = 0
|
user_level = 0
|
||||||
|
|
||||||
logger.debug("Checking power level for %s, %s", event.user_id, user_level)
|
logger.debug(
|
||||||
|
"Checking power level for %s, %s", event.user_id, user_level
|
||||||
|
)
|
||||||
if current_state and hasattr(current_state, "required_power_level"):
|
if current_state and hasattr(current_state, "required_power_level"):
|
||||||
req = current_state.required_power_level
|
req = current_state.required_power_level
|
||||||
|
|
||||||
@ -315,3 +320,101 @@ class Auth(object):
|
|||||||
403,
|
403,
|
||||||
"You don't have permission to change that state"
|
"You don't have permission to change that state"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _check_power_levels(self, event):
|
||||||
|
for k, v in event.content.items():
|
||||||
|
if k == "default":
|
||||||
|
continue
|
||||||
|
|
||||||
|
# FIXME (erikj): We don't want hsob_Ts in content.
|
||||||
|
if k == "hsob_ts":
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
self.hs.parse_userid(k)
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Not a valid user_id: %s" % (k,))
|
||||||
|
|
||||||
|
try:
|
||||||
|
int(v)
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Not a valid power level: %s" % (v,))
|
||||||
|
|
||||||
|
current_state = yield self.store.get_current_state(
|
||||||
|
event.room_id,
|
||||||
|
event.type,
|
||||||
|
event.state_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not current_state:
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
current_state = current_state[0]
|
||||||
|
|
||||||
|
user_level = yield self.store.get_power_level(
|
||||||
|
event.room_id,
|
||||||
|
event.user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_level:
|
||||||
|
user_level = int(user_level)
|
||||||
|
else:
|
||||||
|
user_level = 0
|
||||||
|
|
||||||
|
old_list = current_state.content
|
||||||
|
|
||||||
|
# FIXME (erikj)
|
||||||
|
old_people = {k: v for k, v in old_list.items() if k.startswith("@")}
|
||||||
|
new_people = {
|
||||||
|
k: v for k, v in event.content.items()
|
||||||
|
if k.startswith("@")
|
||||||
|
}
|
||||||
|
|
||||||
|
removed = set(old_people.keys()) - set(new_people.keys())
|
||||||
|
added = set(old_people.keys()) - set(new_people.keys())
|
||||||
|
same = set(old_people.keys()) & set(new_people.keys())
|
||||||
|
|
||||||
|
for r in removed:
|
||||||
|
if int(old_list.content[r]) > user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to remove user: %s" % (r, )
|
||||||
|
)
|
||||||
|
|
||||||
|
for n in added:
|
||||||
|
if int(event.content[n]) > user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to add ops level greater "
|
||||||
|
"than your own"
|
||||||
|
)
|
||||||
|
|
||||||
|
for s in same:
|
||||||
|
if int(event.content[s]) != int(old_list[s]):
|
||||||
|
if int(event.content[s]) > user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to add ops level greater "
|
||||||
|
"than your own"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "default" in old_list:
|
||||||
|
old_default = int(old_list["default"])
|
||||||
|
|
||||||
|
if old_default > user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to add ops level greater than "
|
||||||
|
"your own"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "default" in event.content:
|
||||||
|
new_default = int(event.content["default"])
|
||||||
|
|
||||||
|
if new_default > user_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to add ops level greater "
|
||||||
|
"than your own"
|
||||||
|
)
|
||||||
|
@ -29,6 +29,8 @@ class Codes(object):
|
|||||||
NOT_FOUND = "M_NOT_FOUND"
|
NOT_FOUND = "M_NOT_FOUND"
|
||||||
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
UNKNOWN_TOKEN = "M_UNKNOWN_TOKEN"
|
||||||
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
LIMIT_EXCEEDED = "M_LIMIT_EXCEEDED"
|
||||||
|
CAPTCHA_NEEDED = "M_CAPTCHA_NEEDED"
|
||||||
|
CAPTCHA_INVALID = "M_CAPTCHA_INVALID"
|
||||||
|
|
||||||
|
|
||||||
class CodeMessageException(Exception):
|
class CodeMessageException(Exception):
|
||||||
@ -101,6 +103,19 @@ class StoreError(SynapseError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidCaptchaError(SynapseError):
|
||||||
|
def __init__(self, code=400, msg="Invalid captcha.", error_url=None,
|
||||||
|
errcode=Codes.CAPTCHA_INVALID):
|
||||||
|
super(InvalidCaptchaError, self).__init__(code, msg, errcode)
|
||||||
|
self.error_url = error_url
|
||||||
|
|
||||||
|
def error_dict(self):
|
||||||
|
return cs_error(
|
||||||
|
self.msg,
|
||||||
|
self.errcode,
|
||||||
|
error_url=self.error_url,
|
||||||
|
)
|
||||||
|
|
||||||
class LimitExceededError(SynapseError):
|
class LimitExceededError(SynapseError):
|
||||||
"""A client has sent too many requests and is being throttled.
|
"""A client has sent too many requests and is being throttled.
|
||||||
"""
|
"""
|
||||||
|
@ -157,7 +157,12 @@ class SynapseEvent(JsonEncodedObject):
|
|||||||
|
|
||||||
|
|
||||||
class SynapseStateEvent(SynapseEvent):
|
class SynapseStateEvent(SynapseEvent):
|
||||||
def __init__(self, **kwargs):
|
|
||||||
|
valid_keys = SynapseEvent.valid_keys + [
|
||||||
|
"prev_content",
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, **kwargs):
|
||||||
if "state_key" not in kwargs:
|
if "state_key" not in kwargs:
|
||||||
kwargs["state_key"] = ""
|
kwargs["state_key"] = ""
|
||||||
super(SynapseStateEvent, self).__init__(**kwargs)
|
super(SynapseStateEvent, self).__init__(**kwargs)
|
||||||
|
@ -47,11 +47,14 @@ class EventFactory(object):
|
|||||||
self._event_list[event_class.TYPE] = event_class
|
self._event_list[event_class.TYPE] = event_class
|
||||||
|
|
||||||
self.clock = hs.get_clock()
|
self.clock = hs.get_clock()
|
||||||
|
self.hs = hs
|
||||||
|
|
||||||
def create_event(self, etype=None, **kwargs):
|
def create_event(self, etype=None, **kwargs):
|
||||||
kwargs["type"] = etype
|
kwargs["type"] = etype
|
||||||
if "event_id" not in kwargs:
|
if "event_id" not in kwargs:
|
||||||
kwargs["event_id"] = random_string(10)
|
kwargs["event_id"] = "%s@%s" % (
|
||||||
|
random_string(10), self.hs.hostname
|
||||||
|
)
|
||||||
|
|
||||||
if "ts" not in kwargs:
|
if "ts" not in kwargs:
|
||||||
kwargs["ts"] = int(self.clock.time_msec())
|
kwargs["ts"] = int(self.clock.time_msec())
|
||||||
|
@ -173,3 +173,10 @@ class RoomOpsPowerLevelsEvent(SynapseStateEvent):
|
|||||||
|
|
||||||
def get_content_template(self):
|
def get_content_template(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomAliasesEvent(SynapseStateEvent):
|
||||||
|
TYPE = "m.room.aliases"
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
@ -57,7 +57,7 @@ SCHEMAS = [
|
|||||||
|
|
||||||
# Remember to update this number every time an incompatible change is made to
|
# Remember to update this number every time an incompatible change is made to
|
||||||
# database schema files, so the users will be informed on server restarts.
|
# database schema files, so the users will be informed on server restarts.
|
||||||
SCHEMA_VERSION = 2
|
SCHEMA_VERSION = 3
|
||||||
|
|
||||||
|
|
||||||
class SynapseHomeServer(HomeServer):
|
class SynapseHomeServer(HomeServer):
|
||||||
|
42
synapse/config/captcha.py
Normal file
42
synapse/config/captcha.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# 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 CaptchaConfig(Config):
|
||||||
|
|
||||||
|
def __init__(self, args):
|
||||||
|
super(CaptchaConfig, self).__init__(args)
|
||||||
|
self.recaptcha_private_key = args.recaptcha_private_key
|
||||||
|
self.enable_registration_captcha = args.enable_registration_captcha
|
||||||
|
self.captcha_ip_origin_is_x_forwarded = args.captcha_ip_origin_is_x_forwarded
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(CaptchaConfig, cls).add_arguments(parser)
|
||||||
|
group = parser.add_argument_group("recaptcha")
|
||||||
|
group.add_argument(
|
||||||
|
"--recaptcha-private-key", type=str, default="YOUR_PRIVATE_KEY",
|
||||||
|
help="The matching private key for the web client's public key."
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--enable-registration-captcha", type=bool, default=False,
|
||||||
|
help="Enables ReCaptcha checks when registering, preventing signup "+
|
||||||
|
"unless a captcha is answered. Requires a valid ReCaptcha public/private key."
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--captcha_ip_origin_is_x_forwarded", type=bool, default=False,
|
||||||
|
help="When checking captchas, use the X-Forwarded-For (XFF) header as the client IP "+
|
||||||
|
"and not the actual client IP."
|
||||||
|
)
|
@ -19,9 +19,10 @@ from .logger import LoggingConfig
|
|||||||
from .database import DatabaseConfig
|
from .database import DatabaseConfig
|
||||||
from .ratelimiting import RatelimitConfig
|
from .ratelimiting import RatelimitConfig
|
||||||
from .repository import ContentRepositoryConfig
|
from .repository import ContentRepositoryConfig
|
||||||
|
from .captcha import CaptchaConfig
|
||||||
|
|
||||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
RatelimitConfig, ContentRepositoryConfig):
|
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
if __name__=='__main__':
|
if __name__=='__main__':
|
||||||
|
@ -42,9 +42,6 @@ class BaseHandler(object):
|
|||||||
retry_after_ms=int(1000*(time_allowed - time_now)),
|
retry_after_ms=int(1000*(time_allowed - time_now)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class BaseRoomHandler(BaseHandler):
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _on_new_room_event(self, event, snapshot, extra_destinations=[],
|
def _on_new_room_event(self, event, snapshot, extra_destinations=[],
|
||||||
extra_users=[]):
|
extra_users=[]):
|
||||||
|
@ -19,8 +19,10 @@ from ._base import BaseHandler
|
|||||||
|
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError
|
||||||
from synapse.http.client import HttpClient
|
from synapse.http.client import HttpClient
|
||||||
|
from synapse.api.events.room import RoomAliasesEvent
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
import sqlite3
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -37,7 +39,8 @@ class DirectoryHandler(BaseHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def create_association(self, room_alias, room_id, servers=None):
|
def create_association(self, user_id, room_alias, room_id, servers=None):
|
||||||
|
|
||||||
# TODO(erikj): Do auth.
|
# TODO(erikj): Do auth.
|
||||||
|
|
||||||
if not room_alias.is_mine:
|
if not room_alias.is_mine:
|
||||||
@ -54,12 +57,37 @@ class DirectoryHandler(BaseHandler):
|
|||||||
if not servers:
|
if not servers:
|
||||||
raise SynapseError(400, "Failed to get server list")
|
raise SynapseError(400, "Failed to get server list")
|
||||||
|
|
||||||
yield self.store.create_room_alias_association(
|
|
||||||
room_alias,
|
try:
|
||||||
room_id,
|
yield self.store.create_room_alias_association(
|
||||||
servers
|
room_alias,
|
||||||
|
room_id,
|
||||||
|
servers
|
||||||
|
)
|
||||||
|
except sqlite3.IntegrityError:
|
||||||
|
defer.returnValue("Already exists")
|
||||||
|
|
||||||
|
# TODO: Send the room event.
|
||||||
|
|
||||||
|
aliases = yield self.store.get_aliases_for_room(room_id)
|
||||||
|
|
||||||
|
event = self.event_factory.create_event(
|
||||||
|
etype=RoomAliasesEvent.TYPE,
|
||||||
|
state_key=self.hs.hostname,
|
||||||
|
room_id=room_id,
|
||||||
|
user_id=user_id,
|
||||||
|
content={"aliases": aliases},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
snapshot = yield self.store.snapshot_room(
|
||||||
|
room_id=room_id,
|
||||||
|
user_id=user_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.state_handler.handle_new_event(event, snapshot)
|
||||||
|
yield self._on_new_room_event(event, snapshot, extra_users=[user_id])
|
||||||
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_association(self, room_alias):
|
def get_association(self, room_alias):
|
||||||
room_id = None
|
room_id = None
|
||||||
|
@ -19,7 +19,7 @@ from synapse.api.constants import Membership
|
|||||||
from synapse.api.events.room import RoomTopicEvent
|
from synapse.api.events.room import RoomTopicEvent
|
||||||
from synapse.api.errors import RoomError
|
from synapse.api.errors import RoomError
|
||||||
from synapse.streams.config import PaginationConfig
|
from synapse.streams.config import PaginationConfig
|
||||||
from ._base import BaseRoomHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MessageHandler(BaseRoomHandler):
|
class MessageHandler(BaseHandler):
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
super(MessageHandler, self).__init__(hs)
|
super(MessageHandler, self).__init__(hs)
|
||||||
|
@ -796,11 +796,12 @@ class PresenceEventSource(object):
|
|||||||
updates = []
|
updates = []
|
||||||
# TODO(paul): use a DeferredList ? How to limit concurrency.
|
# TODO(paul): use a DeferredList ? How to limit concurrency.
|
||||||
for observed_user in cachemap.keys():
|
for observed_user in cachemap.keys():
|
||||||
if not (from_key < cachemap[observed_user].serial):
|
cached = cachemap[observed_user]
|
||||||
|
if not (from_key < cached.serial):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if (yield self.is_visible(observer_user, observed_user)):
|
if (yield self.is_visible(observer_user, observed_user)):
|
||||||
updates.append((observed_user, cachemap[observed_user]))
|
updates.append((observed_user, cached))
|
||||||
|
|
||||||
# TODO(paul): limit
|
# TODO(paul): limit
|
||||||
|
|
||||||
|
@ -17,7 +17,9 @@
|
|||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.types import UserID
|
from synapse.types import UserID
|
||||||
from synapse.api.errors import SynapseError, RegistrationError
|
from synapse.api.errors import (
|
||||||
|
SynapseError, RegistrationError, InvalidCaptchaError
|
||||||
|
)
|
||||||
from ._base import BaseHandler
|
from ._base import BaseHandler
|
||||||
import synapse.util.stringutils as stringutils
|
import synapse.util.stringutils as stringutils
|
||||||
from synapse.http.client import PlainHttpClient
|
from synapse.http.client import PlainHttpClient
|
||||||
@ -38,7 +40,8 @@ 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, threepidCreds=None,
|
||||||
|
captcha_info={}):
|
||||||
"""Registers a new client on the server.
|
"""Registers a new client on the server.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -51,10 +54,26 @@ 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:
|
if threepidCreds:
|
||||||
for c in threepidCreds:
|
for c in threepidCreds:
|
||||||
logger.info("validating theeepidcred sid %s on id server %s", c['sid'], c['idServer'])
|
logger.info("validating theeepidcred sid %s on id server %s",
|
||||||
|
c['sid'], c['idServer'])
|
||||||
try:
|
try:
|
||||||
threepid = yield self._threepid_from_creds(c)
|
threepid = yield self._threepid_from_creds(c)
|
||||||
except:
|
except:
|
||||||
@ -63,7 +82,8 @@ class RegistrationHandler(BaseHandler):
|
|||||||
|
|
||||||
if not threepid:
|
if not threepid:
|
||||||
raise RegistrationError(400, "Couldn't validate 3pid")
|
raise RegistrationError(400, "Couldn't validate 3pid")
|
||||||
logger.info("got threepid medium %s address %s", threepid['medium'], threepid['address'])
|
logger.info("got threepid medium %s address %s",
|
||||||
|
threepid['medium'], threepid['address'])
|
||||||
|
|
||||||
password_hash = None
|
password_hash = None
|
||||||
if password:
|
if password:
|
||||||
@ -131,7 +151,8 @@ class RegistrationHandler(BaseHandler):
|
|||||||
# 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 credentials', creds['idServer'])
|
logger.warn('%s is not a trusted ID server: rejecting 3pid '+
|
||||||
|
'credentials', creds['idServer'])
|
||||||
defer.returnValue(None)
|
defer.returnValue(None)
|
||||||
data = yield httpCli.get_json(
|
data = yield httpCli.get_json(
|
||||||
creds['idServer'],
|
creds['idServer'],
|
||||||
@ -149,9 +170,44 @@ 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'], 'mxid':mxid }
|
{ 'sid': creds['sid'], 'clientSecret': creds['clientSecret'],
|
||||||
|
'mxid':mxid }
|
||||||
)
|
)
|
||||||
defer.returnValue(data)
|
defer.returnValue(data)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _validate_captcha(self, ip_addr, private_key, challenge, response):
|
||||||
|
"""Validates the captcha provided.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Containing 'valid'(bool) and 'error_url'(str) if invalid.
|
||||||
|
|
||||||
|
"""
|
||||||
|
response = yield self._submit_captcha(ip_addr, private_key, challenge,
|
||||||
|
response)
|
||||||
|
# parse Google's response. Lovely format..
|
||||||
|
lines = response.split('\n')
|
||||||
|
json = {
|
||||||
|
"valid": lines[0] == 'true',
|
||||||
|
"error_url": "http://www.google.com/recaptcha/api/challenge?"+
|
||||||
|
"error=%s" % lines[1]
|
||||||
|
}
|
||||||
|
defer.returnValue(json)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _submit_captcha(self, ip_addr, private_key, challenge, response):
|
||||||
|
client = PlainHttpClient(self.hs)
|
||||||
|
data = yield client.post_urlencoded_get_raw(
|
||||||
|
"www.google.com:80",
|
||||||
|
"/recaptcha/api/verify",
|
||||||
|
accept_partial=True, # twisted dislikes google's response, no content length.
|
||||||
|
args={
|
||||||
|
'privatekey': private_key,
|
||||||
|
'remoteip': ip_addr,
|
||||||
|
'challenge': challenge,
|
||||||
|
'response': response
|
||||||
|
}
|
||||||
|
)
|
||||||
|
defer.returnValue(data)
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,14 +25,14 @@ from synapse.api.events.room import (
|
|||||||
RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent, RoomNameEvent,
|
RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent, RoomNameEvent,
|
||||||
)
|
)
|
||||||
from synapse.util import stringutils
|
from synapse.util import stringutils
|
||||||
from ._base import BaseRoomHandler
|
from ._base import BaseHandler
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class RoomCreationHandler(BaseRoomHandler):
|
class RoomCreationHandler(BaseHandler):
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def create_room(self, user_id, room_id, config):
|
def create_room(self, user_id, room_id, config):
|
||||||
@ -65,6 +65,13 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
else:
|
else:
|
||||||
room_alias = None
|
room_alias = None
|
||||||
|
|
||||||
|
invite_list = config.get("invite", [])
|
||||||
|
for i in invite_list:
|
||||||
|
try:
|
||||||
|
self.hs.parse_userid(i)
|
||||||
|
except:
|
||||||
|
raise SynapseError(400, "Invalid user_id: %s" % (i,))
|
||||||
|
|
||||||
is_public = config.get("visibility", None) == "public"
|
is_public = config.get("visibility", None) == "public"
|
||||||
|
|
||||||
if room_id:
|
if room_id:
|
||||||
@ -105,7 +112,9 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if room_alias:
|
if room_alias:
|
||||||
yield self.store.create_room_alias_association(
|
directory_handler = self.hs.get_handlers().directory_handler
|
||||||
|
yield directory_handler.create_association(
|
||||||
|
user_id=user_id,
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
room_alias=room_alias,
|
room_alias=room_alias,
|
||||||
servers=[self.hs.hostname],
|
servers=[self.hs.hostname],
|
||||||
@ -132,7 +141,7 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
etype=RoomNameEvent.TYPE,
|
etype=RoomNameEvent.TYPE,
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
required_power_level=5,
|
required_power_level=50,
|
||||||
content={"name": name},
|
content={"name": name},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -143,7 +152,7 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
etype=RoomNameEvent.TYPE,
|
etype=RoomNameEvent.TYPE,
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
required_power_level=5,
|
required_power_level=50,
|
||||||
content={"name": name},
|
content={"name": name},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -155,7 +164,7 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
etype=RoomTopicEvent.TYPE,
|
etype=RoomTopicEvent.TYPE,
|
||||||
room_id=room_id,
|
room_id=room_id,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
required_power_level=5,
|
required_power_level=50,
|
||||||
content={"topic": topic},
|
content={"topic": topic},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -176,6 +185,25 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
do_auth=False
|
do_auth=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
content = {"membership": Membership.INVITE}
|
||||||
|
for invitee in invite_list:
|
||||||
|
invite_event = self.event_factory.create_event(
|
||||||
|
etype=RoomMemberEvent.TYPE,
|
||||||
|
state_key=invitee,
|
||||||
|
room_id=room_id,
|
||||||
|
user_id=user_id,
|
||||||
|
content=content
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.hs.get_handlers().room_member_handler.change_membership(
|
||||||
|
invite_event,
|
||||||
|
do_auth=False
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.hs.get_handlers().room_member_handler.change_membership(
|
||||||
|
join_event,
|
||||||
|
do_auth=False
|
||||||
|
)
|
||||||
result = {"room_id": room_id}
|
result = {"room_id": room_id}
|
||||||
if room_alias:
|
if room_alias:
|
||||||
result["room_alias"] = room_alias.to_string()
|
result["room_alias"] = room_alias.to_string()
|
||||||
@ -186,7 +214,7 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
event_keys = {
|
event_keys = {
|
||||||
"room_id": room_id,
|
"room_id": room_id,
|
||||||
"user_id": creator.to_string(),
|
"user_id": creator.to_string(),
|
||||||
"required_power_level": 10,
|
"required_power_level": 100,
|
||||||
}
|
}
|
||||||
|
|
||||||
def create(etype, **content):
|
def create(etype, **content):
|
||||||
@ -203,7 +231,7 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
|
|
||||||
power_levels_event = self.event_factory.create_event(
|
power_levels_event = self.event_factory.create_event(
|
||||||
etype=RoomPowerLevelsEvent.TYPE,
|
etype=RoomPowerLevelsEvent.TYPE,
|
||||||
content={creator.to_string(): 10, "default": 0},
|
content={creator.to_string(): 100, "default": 0},
|
||||||
**event_keys
|
**event_keys
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -215,7 +243,7 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
|
|
||||||
add_state_event = create(
|
add_state_event = create(
|
||||||
etype=RoomAddStateLevelEvent.TYPE,
|
etype=RoomAddStateLevelEvent.TYPE,
|
||||||
level=10,
|
level=100,
|
||||||
)
|
)
|
||||||
|
|
||||||
send_event = create(
|
send_event = create(
|
||||||
@ -225,8 +253,8 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
|
|
||||||
ops = create(
|
ops = create(
|
||||||
etype=RoomOpsPowerLevelsEvent.TYPE,
|
etype=RoomOpsPowerLevelsEvent.TYPE,
|
||||||
ban_level=5,
|
ban_level=50,
|
||||||
kick_level=5,
|
kick_level=50,
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@ -239,7 +267,7 @@ class RoomCreationHandler(BaseRoomHandler):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class RoomMemberHandler(BaseRoomHandler):
|
class RoomMemberHandler(BaseHandler):
|
||||||
# TODO(paul): This handler currently contains a messy conflation of
|
# TODO(paul): This handler currently contains a messy conflation of
|
||||||
# low-level API that works on UserID objects and so on, and REST-level
|
# low-level API that works on UserID objects and so on, and REST-level
|
||||||
# API that takes ID strings and returns pagination chunks. These concerns
|
# API that takes ID strings and returns pagination chunks. These concerns
|
||||||
@ -560,7 +588,7 @@ class RoomMemberHandler(BaseRoomHandler):
|
|||||||
extra_users=[target_user]
|
extra_users=[target_user]
|
||||||
)
|
)
|
||||||
|
|
||||||
class RoomListHandler(BaseRoomHandler):
|
class RoomListHandler(BaseHandler):
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_public_room_list(self):
|
def get_public_room_list(self):
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
from twisted.internet import defer, reactor
|
from twisted.internet import defer, reactor
|
||||||
from twisted.internet.error import DNSLookupError
|
from twisted.internet.error import DNSLookupError
|
||||||
from twisted.web.client import _AgentBase, _URI, readBody, FileBodyProducer
|
from twisted.web.client import _AgentBase, _URI, readBody, FileBodyProducer, PartialDownloadError
|
||||||
from twisted.web.http_headers import Headers
|
from twisted.web.http_headers import Headers
|
||||||
|
|
||||||
from synapse.http.endpoint import matrix_endpoint
|
from synapse.http.endpoint import matrix_endpoint
|
||||||
@ -188,6 +188,32 @@ class TwistedHttpClient(HttpClient):
|
|||||||
body = yield readBody(response)
|
body = yield readBody(response)
|
||||||
|
|
||||||
defer.returnValue(json.loads(body))
|
defer.returnValue(json.loads(body))
|
||||||
|
|
||||||
|
# XXX FIXME : I'm so sorry.
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def post_urlencoded_get_raw(self, destination, path, accept_partial=False, args={}):
|
||||||
|
if destination in _destination_mappings:
|
||||||
|
destination = _destination_mappings[destination]
|
||||||
|
|
||||||
|
query_bytes = urllib.urlencode(args, True)
|
||||||
|
|
||||||
|
response = yield self._create_request(
|
||||||
|
destination.encode("ascii"),
|
||||||
|
"POST",
|
||||||
|
path.encode("ascii"),
|
||||||
|
producer=FileBodyProducer(StringIO(urllib.urlencode(args))),
|
||||||
|
headers_dict={"Content-Type": ["application/x-www-form-urlencoded"]}
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
body = yield readBody(response)
|
||||||
|
defer.returnValue(body)
|
||||||
|
except PartialDownloadError as e:
|
||||||
|
if accept_partial:
|
||||||
|
defer.returnValue(e.response)
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
|
||||||
@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"",
|
||||||
|
@ -45,6 +45,8 @@ class ClientDirectoryServer(RestServlet):
|
|||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_PUT(self, request, room_alias):
|
def on_PUT(self, request, room_alias):
|
||||||
|
user = yield self.auth.get_user_by_req(request)
|
||||||
|
|
||||||
content = _parse_json(request)
|
content = _parse_json(request)
|
||||||
if not "room_id" in content:
|
if not "room_id" in content:
|
||||||
raise SynapseError(400, "Missing room_id key",
|
raise SynapseError(400, "Missing room_id key",
|
||||||
@ -69,12 +71,13 @@ class ClientDirectoryServer(RestServlet):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
yield dir_handler.create_association(
|
yield dir_handler.create_association(
|
||||||
room_alias, room_id, servers
|
user.to_string(), room_alias, room_id, servers
|
||||||
)
|
)
|
||||||
except SynapseError as e:
|
except SynapseError as e:
|
||||||
raise e
|
raise e
|
||||||
except:
|
except:
|
||||||
logger.exception("Failed to create association")
|
logger.exception("Failed to create association")
|
||||||
|
raise
|
||||||
|
|
||||||
defer.returnValue((200, {}))
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
|
@ -70,7 +70,7 @@ class LoginFallbackRestServlet(RestServlet):
|
|||||||
def on_GET(self, request):
|
def on_GET(self, request):
|
||||||
# TODO(kegan): This should be returning some HTML which is capable of
|
# TODO(kegan): This should be returning some HTML which is capable of
|
||||||
# hitting LoginRestServlet
|
# hitting LoginRestServlet
|
||||||
return (200, "")
|
return (200, {})
|
||||||
|
|
||||||
|
|
||||||
def _parse_json(request):
|
def _parse_json(request):
|
||||||
|
@ -51,7 +51,7 @@ class ProfileDisplaynameRestServlet(RestServlet):
|
|||||||
yield self.handlers.profile_handler.set_displayname(
|
yield self.handlers.profile_handler.set_displayname(
|
||||||
user, auth_user, new_name)
|
user, auth_user, new_name)
|
||||||
|
|
||||||
defer.returnValue((200, ""))
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
def on_OPTIONS(self, request, user_id):
|
def on_OPTIONS(self, request, user_id):
|
||||||
return (200, {})
|
return (200, {})
|
||||||
@ -86,7 +86,7 @@ class ProfileAvatarURLRestServlet(RestServlet):
|
|||||||
yield self.handlers.profile_handler.set_avatar_url(
|
yield self.handlers.profile_handler.set_avatar_url(
|
||||||
user, auth_user, new_name)
|
user, auth_user, new_name)
|
||||||
|
|
||||||
defer.returnValue((200, ""))
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
def on_OPTIONS(self, request, user_id):
|
def on_OPTIONS(self, request, user_id):
|
||||||
return (200, {})
|
return (200, {})
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
"""This module contains REST servlets to do with registration: /register"""
|
"""This module contains REST servlets to do with registration: /register"""
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.errors import SynapseError
|
from synapse.api.errors import SynapseError, Codes
|
||||||
from base import RestServlet, client_path_pattern
|
from base import RestServlet, client_path_pattern
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@ -50,12 +50,44 @@ class RegisterRestServlet(RestServlet):
|
|||||||
threepidCreds = None
|
threepidCreds = None
|
||||||
if 'threepidCreds' in register_json:
|
if 'threepidCreds' in register_json:
|
||||||
threepidCreds = register_json['threepidCreds']
|
threepidCreds = register_json['threepidCreds']
|
||||||
|
|
||||||
|
captcha = {}
|
||||||
|
if self.hs.config.enable_registration_captcha:
|
||||||
|
challenge = None
|
||||||
|
user_response = None
|
||||||
|
try:
|
||||||
|
captcha_type = register_json["captcha"]["type"]
|
||||||
|
if captcha_type != "m.login.recaptcha":
|
||||||
|
raise SynapseError(400, "Sorry, only m.login.recaptcha " +
|
||||||
|
"requests are supported.")
|
||||||
|
challenge = register_json["captcha"]["challenge"]
|
||||||
|
user_response = register_json["captcha"]["response"]
|
||||||
|
except KeyError:
|
||||||
|
raise SynapseError(400, "Captcha response is required",
|
||||||
|
errcode=Codes.CAPTCHA_NEEDED)
|
||||||
|
|
||||||
|
# TODO determine the source IP : May be an X-Forwarding-For header depending on config
|
||||||
|
ip_addr = request.getClientIP()
|
||||||
|
if self.hs.config.captcha_ip_origin_is_x_forwarded:
|
||||||
|
# use the header
|
||||||
|
if request.requestHeaders.hasHeader("X-Forwarded-For"):
|
||||||
|
ip_addr = request.requestHeaders.getRawHeaders(
|
||||||
|
"X-Forwarded-For")[0]
|
||||||
|
|
||||||
|
captcha = {
|
||||||
|
"ip": ip_addr,
|
||||||
|
"private_key": self.hs.config.recaptcha_private_key,
|
||||||
|
"challenge": challenge,
|
||||||
|
"response": user_response
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
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)
|
threepidCreds=threepidCreds,
|
||||||
|
captcha_info=captcha)
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
"user_id": user_id,
|
"user_id": user_id,
|
||||||
|
@ -154,14 +154,14 @@ class RoomStateEventRestServlet(RestServlet):
|
|||||||
# membership events are special
|
# membership events are special
|
||||||
handler = self.handlers.room_member_handler
|
handler = self.handlers.room_member_handler
|
||||||
yield handler.change_membership(event)
|
yield handler.change_membership(event)
|
||||||
defer.returnValue((200, ""))
|
defer.returnValue((200, {}))
|
||||||
else:
|
else:
|
||||||
# store random bits of state
|
# store random bits of state
|
||||||
msg_handler = self.handlers.message_handler
|
msg_handler = self.handlers.message_handler
|
||||||
yield msg_handler.store_room_data(
|
yield msg_handler.store_room_data(
|
||||||
event=event
|
event=event
|
||||||
)
|
)
|
||||||
defer.returnValue((200, ""))
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
|
|
||||||
# TODO: Needs unit testing for generic events + feedback
|
# TODO: Needs unit testing for generic events + feedback
|
||||||
@ -249,7 +249,7 @@ class JoinRoomAliasServlet(RestServlet):
|
|||||||
)
|
)
|
||||||
handler = self.handlers.room_member_handler
|
handler = self.handlers.room_member_handler
|
||||||
yield handler.change_membership(event)
|
yield handler.change_membership(event)
|
||||||
defer.returnValue((200, ""))
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_PUT(self, request, room_identifier, txn_id):
|
def on_PUT(self, request, room_identifier, txn_id):
|
||||||
@ -416,7 +416,7 @@ class RoomMembershipRestServlet(RestServlet):
|
|||||||
)
|
)
|
||||||
handler = self.handlers.room_member_handler
|
handler = self.handlers.room_member_handler
|
||||||
yield handler.change_membership(event)
|
yield handler.change_membership(event)
|
||||||
defer.returnValue((200, ""))
|
defer.returnValue((200, {}))
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_PUT(self, request, room_id, membership_action, txn_id):
|
def on_PUT(self, request, room_id, membership_action, txn_id):
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.federation.pdu_codec import encode_event_id
|
from synapse.federation.pdu_codec import encode_event_id, decode_event_id
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
@ -87,9 +87,11 @@ class StateHandler(object):
|
|||||||
# than the power level of the user
|
# than the power level of the user
|
||||||
# power_level = self._get_power_level_for_event(event)
|
# power_level = self._get_power_level_for_event(event)
|
||||||
|
|
||||||
|
pdu_id, origin = decode_event_id(event.event_id, self.server_name)
|
||||||
|
|
||||||
yield self.store.update_current_state(
|
yield self.store.update_current_state(
|
||||||
pdu_id=event.event_id,
|
pdu_id=pdu_id,
|
||||||
origin=self.server_name,
|
origin=origin,
|
||||||
context=key.context,
|
context=key.context,
|
||||||
pdu_type=key.type,
|
pdu_type=key.type,
|
||||||
state_key=key.state_key
|
state_key=key.state_key
|
||||||
|
@ -81,7 +81,7 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||||||
defer.returnValue(latest)
|
defer.returnValue(latest)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_event(self, event_id):
|
def get_event(self, event_id, allow_none=False):
|
||||||
events_dict = yield self._simple_select_one(
|
events_dict = yield self._simple_select_one(
|
||||||
"events",
|
"events",
|
||||||
{"event_id": event_id},
|
{"event_id": event_id},
|
||||||
@ -92,8 +92,12 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||||||
"content",
|
"content",
|
||||||
"unrecognized_keys"
|
"unrecognized_keys"
|
||||||
],
|
],
|
||||||
|
allow_none=allow_none,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if not events_dict:
|
||||||
|
defer.returnValue(None)
|
||||||
|
|
||||||
event = self._parse_event_from_row(events_dict)
|
event = self._parse_event_from_row(events_dict)
|
||||||
defer.returnValue(event)
|
defer.returnValue(event)
|
||||||
|
|
||||||
@ -220,7 +224,8 @@ class DataStore(RoomMemberStore, RoomStore,
|
|||||||
|
|
||||||
results = yield self._execute_and_decode(sql, *args)
|
results = yield self._execute_and_decode(sql, *args)
|
||||||
|
|
||||||
defer.returnValue([self._parse_event_from_row(r) for r in results])
|
events = yield self._parse_events(results)
|
||||||
|
defer.returnValue(events)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _get_min_token(self):
|
def _get_min_token(self):
|
||||||
|
@ -312,6 +312,25 @@ class SQLBaseStore(object):
|
|||||||
**d
|
**d
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _parse_events(self, rows):
|
||||||
|
return self._db_pool.runInteraction(self._parse_events_txn, rows)
|
||||||
|
|
||||||
|
def _parse_events_txn(self, txn, rows):
|
||||||
|
events = [self._parse_event_from_row(r) for r in rows]
|
||||||
|
|
||||||
|
sql = "SELECT * FROM events WHERE event_id = ?"
|
||||||
|
|
||||||
|
for ev in events:
|
||||||
|
if hasattr(ev, "prev_state"):
|
||||||
|
# Load previous state_content.
|
||||||
|
# TODO: Should we be pulling this out above?
|
||||||
|
cursor = txn.execute(sql, (ev.prev_state,))
|
||||||
|
prevs = self.cursor_to_dict(cursor)
|
||||||
|
if prevs:
|
||||||
|
prev = self._parse_event_from_row(prevs[0])
|
||||||
|
ev.prev_content = prev.content
|
||||||
|
|
||||||
|
return events
|
||||||
|
|
||||||
class Table(object):
|
class Table(object):
|
||||||
""" A base class used to store information about a particular table.
|
""" A base class used to store information about a particular table.
|
||||||
|
@ -92,3 +92,10 @@ class DirectoryStore(SQLBaseStore):
|
|||||||
"server": server,
|
"server": server,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_aliases_for_room(self, room_id):
|
||||||
|
return self._simple_select_onecol(
|
||||||
|
"room_aliases",
|
||||||
|
{"room_id": room_id},
|
||||||
|
"room_alias",
|
||||||
|
)
|
||||||
|
@ -88,7 +88,7 @@ class RoomMemberStore(SQLBaseStore):
|
|||||||
txn.execute(sql, (user_id, room_id))
|
txn.execute(sql, (user_id, room_id))
|
||||||
rows = self.cursor_to_dict(txn)
|
rows = self.cursor_to_dict(txn)
|
||||||
if rows:
|
if rows:
|
||||||
return self._parse_event_from_row(rows[0])
|
return self._parse_events_txn(txn, rows)[0]
|
||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ class RoomMemberStore(SQLBaseStore):
|
|||||||
|
|
||||||
# logger.debug("_get_members_query Got rows %s", rows)
|
# logger.debug("_get_members_query Got rows %s", rows)
|
||||||
|
|
||||||
results = [self._parse_event_from_row(r) for r in rows]
|
results = yield self._parse_events(rows)
|
||||||
defer.returnValue(results)
|
defer.returnValue(results)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
|
27
synapse/storage/schema/delta/v3.sql
Normal file
27
synapse/storage/schema/delta/v3.sql
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
/* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS room_aliases_alias ON room_aliases(room_alias);
|
||||||
|
CREATE INDEX IF NOT EXISTS room_aliases_id ON room_aliases(room_id);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS room_alias_servers_alias ON room_alias_servers(room_alias);
|
||||||
|
|
||||||
|
DELETE FROM room_aliases WHERE rowid NOT IN (SELECT max(rowid) FROM room_aliases GROUP BY room_alias, room_id);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS room_aliases_uniq ON room_aliases(room_alias, room_id);
|
||||||
|
|
||||||
|
PRAGMA user_version = 3;
|
@ -188,7 +188,7 @@ class StreamStore(SQLBaseStore):
|
|||||||
user_id, user_id, from_id, to_id
|
user_id, user_id, from_id, to_id
|
||||||
)
|
)
|
||||||
|
|
||||||
ret = [self._parse_event_from_row(r) for r in rows]
|
ret = yield self._parse_events(rows)
|
||||||
|
|
||||||
if rows:
|
if rows:
|
||||||
key = "s%d" % max([r["stream_ordering"] for r in rows])
|
key = "s%d" % max([r["stream_ordering"] for r in rows])
|
||||||
@ -243,9 +243,11 @@ class StreamStore(SQLBaseStore):
|
|||||||
# TODO (erikj): We should work out what to do here instead.
|
# TODO (erikj): We should work out what to do here instead.
|
||||||
next_token = to_key if to_key else from_key
|
next_token = to_key if to_key else from_key
|
||||||
|
|
||||||
|
events = yield self._parse_events(rows)
|
||||||
|
|
||||||
defer.returnValue(
|
defer.returnValue(
|
||||||
(
|
(
|
||||||
[self._parse_event_from_row(r) for r in rows],
|
events,
|
||||||
next_token
|
next_token
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -277,12 +279,11 @@ class StreamStore(SQLBaseStore):
|
|||||||
else:
|
else:
|
||||||
token = (end_token, end_token)
|
token = (end_token, end_token)
|
||||||
|
|
||||||
defer.returnValue(
|
events = yield self._parse_events(rows)
|
||||||
(
|
|
||||||
[self._parse_event_from_row(r) for r in rows],
|
ret = (events, token)
|
||||||
token
|
|
||||||
)
|
defer.returnValue(ret)
|
||||||
)
|
|
||||||
|
|
||||||
def get_room_events_max_id(self):
|
def get_room_events_max_id(self):
|
||||||
return self._db_pool.runInteraction(self._get_room_events_max_id_txn)
|
return self._db_pool.runInteraction(self._get_room_events_max_id_txn)
|
||||||
|
@ -145,6 +145,7 @@ class EventStreamPermissionsTestCase(RestTestCase):
|
|||||||
)
|
)
|
||||||
self.ratelimiter = hs.get_ratelimiter()
|
self.ratelimiter = hs.get_ratelimiter()
|
||||||
self.ratelimiter.send_message.return_value = (True, 0)
|
self.ratelimiter.send_message.return_value = (True, 0)
|
||||||
|
hs.config.enable_registration_captcha = False
|
||||||
|
|
||||||
hs.get_handlers().federation_handler = Mock()
|
hs.get_handlers().federation_handler = Mock()
|
||||||
|
|
||||||
|
@ -240,6 +240,7 @@ class StateTestCase(unittest.TestCase):
|
|||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_new_event(self):
|
def test_new_event(self):
|
||||||
event = Mock()
|
event = Mock()
|
||||||
|
event.event_id = "12123123@test"
|
||||||
|
|
||||||
state_pdu = new_fake_pdu_entry("C", "test", "mem", "x", "A", 20)
|
state_pdu = new_fake_pdu_entry("C", "test", "mem", "x", "A", 20)
|
||||||
|
|
||||||
|
46
webclient/CAPTCHA_SETUP
Normal file
46
webclient/CAPTCHA_SETUP
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
Captcha can be enabled for this web client / home server. This file explains how to do that.
|
||||||
|
The captcha mechanism used is Google's ReCaptcha. This requires API keys from Google.
|
||||||
|
|
||||||
|
Getting keys
|
||||||
|
------------
|
||||||
|
Requires a public/private key pair from:
|
||||||
|
|
||||||
|
https://developers.google.com/recaptcha/
|
||||||
|
|
||||||
|
|
||||||
|
Setting Private ReCaptcha Key
|
||||||
|
-----------------------------
|
||||||
|
The private key is a config option on the home server config. If it is not
|
||||||
|
visible, you can generate it via --generate-config. Set the following value:
|
||||||
|
|
||||||
|
recaptcha_private_key: YOUR_PRIVATE_KEY
|
||||||
|
|
||||||
|
In addition, you MUST enable captchas via:
|
||||||
|
|
||||||
|
enable_registration_captcha: true
|
||||||
|
|
||||||
|
Setting Public ReCaptcha Key
|
||||||
|
----------------------------
|
||||||
|
The web client will look for the global variable webClientConfig for config
|
||||||
|
options. You should put your ReCaptcha public key there like so:
|
||||||
|
|
||||||
|
webClientConfig = {
|
||||||
|
useCaptcha: true,
|
||||||
|
recaptcha_public_key: "YOUR_PUBLIC_KEY"
|
||||||
|
}
|
||||||
|
|
||||||
|
This should be put in webclient/config.js which is already .gitignored, rather
|
||||||
|
than in the web client source files. You MUST set useCaptcha to true else a
|
||||||
|
ReCaptcha widget will not be generated.
|
||||||
|
|
||||||
|
Configuring IP used for auth
|
||||||
|
----------------------------
|
||||||
|
The ReCaptcha API requires that the IP address of the user who solved the
|
||||||
|
captcha is sent. If the client is connecting through a proxy or load balancer,
|
||||||
|
it may be required to use the X-Forwarded-For (XFF) header instead of the origin
|
||||||
|
IP address. This can be configured as an option on the home server like so:
|
||||||
|
|
||||||
|
captcha_ip_origin_is_x_forwarded: true
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
|||||||
Basic Usage
|
Basic Usage
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
The Synapse web client needs to be hosted by a basic HTTP server.
|
The web client should automatically run when running the home server. Alternatively, you can run
|
||||||
|
it stand-alone:
|
||||||
You can use the Python simple HTTP server::
|
|
||||||
|
|
||||||
$ python -m SimpleHTTPServer
|
$ python -m SimpleHTTPServer
|
||||||
|
|
||||||
Then, open this URL in a WEB browser::
|
Then, open this URL in a WEB browser::
|
||||||
|
|
||||||
http://127.0.0.1:8000/
|
http://127.0.0.1:8000/
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,8 +21,8 @@ limitations under the License.
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
|
angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'eventStreamService'])
|
||||||
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', 'matrixService', 'mPresence', 'eventStreamService', 'matrixPhoneService',
|
.controller('MatrixWebClientController', ['$scope', '$location', '$rootScope', '$timeout', '$animate', 'matrixService', 'mPresence', 'eventStreamService', 'matrixPhoneService',
|
||||||
function($scope, $location, $rootScope, matrixService, mPresence, eventStreamService, matrixPhoneService) {
|
function($scope, $location, $rootScope, $timeout, $animate, matrixService, mPresence, eventStreamService, matrixPhoneService) {
|
||||||
|
|
||||||
// Check current URL to avoid to display the logout button on the login page
|
// Check current URL to avoid to display the logout button on the login page
|
||||||
$scope.location = $location.path();
|
$scope.location = $location.path();
|
||||||
@ -89,6 +89,23 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
|
|||||||
$scope.user_id = matrixService.config().user_id;
|
$scope.user_id = matrixService.config().user_id;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$rootScope.$watch('currentCall', function(newVal, oldVal) {
|
||||||
|
if (!$rootScope.currentCall) return;
|
||||||
|
|
||||||
|
var roomMembers = angular.copy($rootScope.events.rooms[$rootScope.currentCall.room_id].members);
|
||||||
|
delete roomMembers[matrixService.config().user_id];
|
||||||
|
|
||||||
|
$rootScope.currentCall.user_id = Object.keys(roomMembers)[0];
|
||||||
|
matrixService.getProfile($rootScope.currentCall.user_id).then(
|
||||||
|
function(response) {
|
||||||
|
$rootScope.currentCall.userProfile = response.data;
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
$scope.feedback = "Can't load user profile";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
|
$rootScope.$on(matrixPhoneService.INCOMING_CALL_EVENT, function(ngEvent, call) {
|
||||||
console.trace("incoming call");
|
console.trace("incoming call");
|
||||||
call.onError = $scope.onCallError;
|
call.onError = $scope.onCallError;
|
||||||
@ -97,12 +114,19 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
|
|||||||
});
|
});
|
||||||
|
|
||||||
$scope.answerCall = function() {
|
$scope.answerCall = function() {
|
||||||
$scope.currentCall.answer();
|
$rootScope.currentCall.answer();
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.hangupCall = function() {
|
$scope.hangupCall = function() {
|
||||||
$scope.currentCall.hangup();
|
$rootScope.currentCall.hangup();
|
||||||
$scope.currentCall = undefined;
|
|
||||||
|
$timeout(function() {
|
||||||
|
var icon = angular.element('#callEndedIcon');
|
||||||
|
$animate.addClass(icon, 'callIconRotate');
|
||||||
|
$timeout(function(){
|
||||||
|
$rootScope.currentCall = undefined;
|
||||||
|
}, 2000);
|
||||||
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
$rootScope.onCallError = function(errStr) {
|
$rootScope.onCallError = function(errStr) {
|
||||||
@ -110,5 +134,12 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
|
|||||||
}
|
}
|
||||||
|
|
||||||
$rootScope.onCallHangup = function() {
|
$rootScope.onCallHangup = function() {
|
||||||
|
$timeout(function() {
|
||||||
|
var icon = angular.element('#callEndedIcon');
|
||||||
|
$animate.addClass(icon, 'callIconRotate');
|
||||||
|
$timeout(function(){
|
||||||
|
$rootScope.currentCall = undefined;
|
||||||
|
}, 2000);
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
}]);
|
}]);
|
||||||
|
@ -79,85 +79,4 @@ angular.module('matrixWebClient')
|
|||||||
return function(text) {
|
return function(text) {
|
||||||
return $sce.trustAsHtml(text);
|
return $sce.trustAsHtml(text);
|
||||||
};
|
};
|
||||||
}])
|
}]);
|
||||||
|
|
||||||
// Compute the room name according to information we have
|
|
||||||
.filter('roomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) {
|
|
||||||
return function(room_id) {
|
|
||||||
var roomName;
|
|
||||||
|
|
||||||
// If there is an alias, use it
|
|
||||||
// TODO: only one alias is managed for now
|
|
||||||
var alias = matrixService.getRoomIdToAliasMapping(room_id);
|
|
||||||
if (alias) {
|
|
||||||
roomName = alias;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (undefined === roomName) {
|
|
||||||
// Else, build the name from its users
|
|
||||||
var room = $rootScope.events.rooms[room_id];
|
|
||||||
if (room) {
|
|
||||||
var room_name_event = room["m.room.name"];
|
|
||||||
|
|
||||||
if (room_name_event) {
|
|
||||||
roomName = room_name_event.content.name;
|
|
||||||
}
|
|
||||||
else if (room.members) {
|
|
||||||
// Limit the room renaming to 1:1 room
|
|
||||||
if (2 === Object.keys(room.members).length) {
|
|
||||||
for (var i in room.members) {
|
|
||||||
var member = room.members[i];
|
|
||||||
if (member.state_key !== matrixService.config().user_id) {
|
|
||||||
|
|
||||||
if (member.state_key in $rootScope.presence) {
|
|
||||||
// If the user is available in presence, use the displayname there
|
|
||||||
// as it is the most uptodate
|
|
||||||
roomName = $rootScope.presence[member.state_key].content.displayname;
|
|
||||||
}
|
|
||||||
else if (member.content.displayname) {
|
|
||||||
roomName = member.content.displayname;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
roomName = member.state_key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (1 === Object.keys(room.members).length) {
|
|
||||||
// The other member may be in the invite list, get all invited users
|
|
||||||
var invitedUserIDs = [];
|
|
||||||
for (var i in room.messages) {
|
|
||||||
var message = room.messages[i];
|
|
||||||
if ("m.room.member" === message.type && "invite" === message.membership) {
|
|
||||||
// Make sure there is no duplicate user
|
|
||||||
if (-1 === invitedUserIDs.indexOf(message.state_key)) {
|
|
||||||
invitedUserIDs.push(message.state_key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// For now, only 1:1 room needs to be renamed. It means only 1 invited user
|
|
||||||
if (1 === invitedUserIDs.length) {
|
|
||||||
var userID = invitedUserIDs[0];
|
|
||||||
|
|
||||||
// Try to resolve his displayname in presence global data
|
|
||||||
if (userID in $rootScope.presence) {
|
|
||||||
roomName = $rootScope.presence[userID].content.displayname;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
roomName = userID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (undefined === roomName) {
|
|
||||||
// By default, use the room ID
|
|
||||||
roomName = room_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return roomName;
|
|
||||||
};
|
|
||||||
}]);
|
|
@ -44,7 +44,49 @@ a:active { color: #000; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
#callBar {
|
#callBar {
|
||||||
float: left;
|
float: left;
|
||||||
|
height: 32px;
|
||||||
|
margin: auto;
|
||||||
|
text-align: right;
|
||||||
|
line-height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callIcon {
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
-webkit-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||||
|
-moz-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||||
|
-o-transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||||
|
transition:all cubic-bezier(0.250, 0.460, 0.450, 0.940) 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callIconRotate {
|
||||||
|
-webkit-transform: rotateZ(45deg);
|
||||||
|
-moz-transform: rotateZ(45deg);
|
||||||
|
-ms-transform: rotateZ(45deg);
|
||||||
|
-o-transform: rotateZ(45deg);
|
||||||
|
transform: rotateZ(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
#callPeerImage {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
#callPeerNameAndState {
|
||||||
|
float: left;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#callState {
|
||||||
|
font-size: 60%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#callPeerName {
|
||||||
|
font-size: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#headerContent {
|
#headerContent {
|
||||||
@ -105,6 +147,10 @@ a:active { color: #000; }
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#recaptcha_area {
|
||||||
|
margin: auto
|
||||||
|
}
|
||||||
|
|
||||||
#loginForm {
|
#loginForm {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
@ -251,12 +297,14 @@ a:active { color: #000; }
|
|||||||
.userAvatar .userAvatarImage {
|
.userAvatar .userAvatarImage {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userAvatar .userAvatarGradient {
|
.userAvatar .userAvatarGradient {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.userAvatar .userName {
|
.userAvatar .userName {
|
||||||
@ -417,6 +465,13 @@ a:active { color: #000; }
|
|||||||
text-align: left ! important;
|
text-align: left ! important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bubble .messagePending {
|
||||||
|
opacity: 0.3
|
||||||
|
}
|
||||||
|
.messageUnSent {
|
||||||
|
color: #F00;
|
||||||
|
}
|
||||||
|
|
||||||
#room-fullscreen-image {
|
#room-fullscreen-image {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0px;
|
top: 0px;
|
||||||
|
@ -41,6 +41,11 @@ angular.module('eventHandlerService', [])
|
|||||||
$rootScope.events = {
|
$rootScope.events = {
|
||||||
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
|
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 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 = {};
|
||||||
|
|
||||||
@ -66,11 +71,22 @@ angular.module('eventHandlerService', [])
|
|||||||
$rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent);
|
$rootScope.$broadcast(ROOM_CREATE_EVENT, event, isLiveEvent);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var handleRoomAliases = function(event, isLiveEvent) {
|
||||||
|
matrixService.createRoomIdToAliasMapping(event.room_id, event.content.aliases[0]);
|
||||||
|
};
|
||||||
|
|
||||||
var handleMessage = function(event, isLiveEvent) {
|
var handleMessage = function(event, isLiveEvent) {
|
||||||
initRoom(event.room_id);
|
initRoom(event.room_id);
|
||||||
|
|
||||||
if (isLiveEvent) {
|
if (isLiveEvent) {
|
||||||
$rootScope.events.rooms[event.room_id].messages.push(event);
|
if (event.user_id === matrixService.config().user_id &&
|
||||||
|
(event.content.msgtype === "m.text" || event.content.msgtype === "m.emote") ) {
|
||||||
|
// assume we've already echoed it
|
||||||
|
// FIXME: track events by ID and ungrey the right message to show it's been delivered
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$rootScope.events.rooms[event.room_id].messages.push(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$rootScope.events.rooms[event.room_id].messages.unshift(event);
|
$rootScope.events.rooms[event.room_id].messages.unshift(event);
|
||||||
@ -87,6 +103,14 @@ angular.module('eventHandlerService', [])
|
|||||||
var handleRoomMember = function(event, isLiveEvent) {
|
var handleRoomMember = function(event, isLiveEvent) {
|
||||||
initRoom(event.room_id);
|
initRoom(event.room_id);
|
||||||
|
|
||||||
|
// if the server is stupidly re-relaying a no-op join, discard it.
|
||||||
|
if (event.prev_content &&
|
||||||
|
event.content.membership === "join" &&
|
||||||
|
event.content.membership === event.prev_content.membership)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 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
|
||||||
if (event.content.prev !== event.content.membership) {
|
if (event.content.prev !== event.content.membership) {
|
||||||
if (isLiveEvent) {
|
if (isLiveEvent) {
|
||||||
@ -137,40 +161,55 @@ angular.module('eventHandlerService', [])
|
|||||||
POWERLEVEL_EVENT: POWERLEVEL_EVENT,
|
POWERLEVEL_EVENT: POWERLEVEL_EVENT,
|
||||||
CALL_EVENT: CALL_EVENT,
|
CALL_EVENT: CALL_EVENT,
|
||||||
NAME_EVENT: NAME_EVENT,
|
NAME_EVENT: NAME_EVENT,
|
||||||
|
|
||||||
|
|
||||||
handleEvent: function(event, isLiveEvent) {
|
handleEvent: function(event, isLiveEvent) {
|
||||||
switch(event.type) {
|
// FIXME: event duplication suppression is all broken as the code currently expect to handles
|
||||||
case "m.room.create":
|
// events multiple times to get their side-effects...
|
||||||
handleRoomCreate(event, isLiveEvent);
|
/*
|
||||||
break;
|
if (eventMap[event.event_id]) {
|
||||||
case "m.room.message":
|
console.log("discarding duplicate event: " + JSON.stringify(event));
|
||||||
handleMessage(event, isLiveEvent);
|
return;
|
||||||
break;
|
|
||||||
case "m.room.member":
|
|
||||||
handleRoomMember(event, isLiveEvent);
|
|
||||||
break;
|
|
||||||
case "m.presence":
|
|
||||||
handlePresence(event, isLiveEvent);
|
|
||||||
break;
|
|
||||||
case 'm.room.ops_levels':
|
|
||||||
case 'm.room.send_event_level':
|
|
||||||
case 'm.room.add_state_level':
|
|
||||||
case 'm.room.join_rules':
|
|
||||||
case 'm.room.power_levels':
|
|
||||||
handlePowerLevels(event, isLiveEvent);
|
|
||||||
break;
|
|
||||||
case 'm.room.name':
|
|
||||||
handleRoomName(event, isLiveEvent);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log("Unable to handle event type " + event.type);
|
|
||||||
console.log(JSON.stringify(event, undefined, 4));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
eventMap[event.event_id] = 1;
|
||||||
|
}
|
||||||
|
*/
|
||||||
if (event.type.indexOf('m.call.') === 0) {
|
if (event.type.indexOf('m.call.') === 0) {
|
||||||
handleCallEvent(event, isLiveEvent);
|
handleCallEvent(event, isLiveEvent);
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
switch(event.type) {
|
||||||
|
case "m.room.create":
|
||||||
|
handleRoomCreate(event, isLiveEvent);
|
||||||
|
break;
|
||||||
|
case "m.room.aliases":
|
||||||
|
handleRoomAliases(event, isLiveEvent);
|
||||||
|
break;
|
||||||
|
case "m.room.message":
|
||||||
|
handleMessage(event, isLiveEvent);
|
||||||
|
break;
|
||||||
|
case "m.room.member":
|
||||||
|
handleRoomMember(event, isLiveEvent);
|
||||||
|
break;
|
||||||
|
case "m.presence":
|
||||||
|
handlePresence(event, isLiveEvent);
|
||||||
|
break;
|
||||||
|
case 'm.room.ops_levels':
|
||||||
|
case 'm.room.send_event_level':
|
||||||
|
case 'm.room.add_state_level':
|
||||||
|
case 'm.room.join_rules':
|
||||||
|
case 'm.room.power_levels':
|
||||||
|
handlePowerLevels(event, isLiveEvent);
|
||||||
|
break;
|
||||||
|
case 'm.room.name':
|
||||||
|
handleRoomName(event, isLiveEvent);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log("Unable to handle event type " + event.type);
|
||||||
|
console.log(JSON.stringify(event, undefined, 4));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// isLiveEvents determines whether notifications should be shown, whether
|
// isLiveEvents determines whether notifications should be shown, whether
|
||||||
|
@ -110,6 +110,7 @@ angular.module('eventStreamService', [])
|
|||||||
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) {
|
||||||
var room = rooms[i];
|
var room = rooms[i];
|
||||||
|
// console.log("got room: " + room.room_id);
|
||||||
if ("state" in room) {
|
if ("state" in room) {
|
||||||
eventHandlerService.handleEvents(room.state, false);
|
eventHandlerService.handleEvents(room.state, false);
|
||||||
}
|
}
|
||||||
|
@ -41,6 +41,7 @@ angular.module('MatrixCall', [])
|
|||||||
this.room_id = room_id;
|
this.room_id = room_id;
|
||||||
this.call_id = "c" + new Date().getTime();
|
this.call_id = "c" + new Date().getTime();
|
||||||
this.state = 'fledgling';
|
this.state = 'fledgling';
|
||||||
|
this.didConnect = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
|
||||||
@ -52,6 +53,7 @@ angular.module('MatrixCall', [])
|
|||||||
matrixPhoneService.callPlaced(this);
|
matrixPhoneService.callPlaced(this);
|
||||||
navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); });
|
navigator.getUserMedia({audio: true, video: false}, function(s) { self.gotUserMediaForInvite(s); }, function(e) { self.getUserMediaFailed(e); });
|
||||||
self.state = 'wait_local_media';
|
self.state = 'wait_local_media';
|
||||||
|
this.direction = 'outbound';
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixCall.prototype.initWithInvite = function(msg) {
|
MatrixCall.prototype.initWithInvite = function(msg) {
|
||||||
@ -64,6 +66,7 @@ angular.module('MatrixCall', [])
|
|||||||
this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
|
this.peerConn.onaddstream = function(s) { self.onAddStream(s); };
|
||||||
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
|
this.peerConn.setRemoteDescription(new RTCSessionDescription(this.msg.offer), self.onSetRemoteDescriptionSuccess, self.onSetRemoteDescriptionError);
|
||||||
this.state = 'ringing';
|
this.state = 'ringing';
|
||||||
|
this.direction = 'inbound';
|
||||||
};
|
};
|
||||||
|
|
||||||
MatrixCall.prototype.answer = function() {
|
MatrixCall.prototype.answer = function() {
|
||||||
@ -204,10 +207,12 @@ angular.module('MatrixCall', [])
|
|||||||
};
|
};
|
||||||
|
|
||||||
MatrixCall.prototype.onIceConnectionStateChanged = function() {
|
MatrixCall.prototype.onIceConnectionStateChanged = function() {
|
||||||
|
if (this.state == 'ended') return; // because ICE can still complete as we're ending the call
|
||||||
console.trace("Ice connection state changed to: "+this.peerConn.iceConnectionState);
|
console.trace("Ice connection state changed to: "+this.peerConn.iceConnectionState);
|
||||||
// ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet
|
// ideally we'd consider the call to be connected when we get media but chrome doesn't implement nay of the 'onstarted' events yet
|
||||||
if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
|
if (this.peerConn.iceConnectionState == 'completed' || this.peerConn.iceConnectionState == 'connected') {
|
||||||
this.state = 'connected';
|
this.state = 'connected';
|
||||||
|
this.didConnect = true;
|
||||||
$rootScope.$apply();
|
$rootScope.$apply();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
135
webclient/components/matrix/matrix-filter.js
Normal file
135
webclient/components/matrix/matrix-filter.js
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
/*
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
angular.module('matrixFilter', [])
|
||||||
|
|
||||||
|
// Compute the room name according to information we have
|
||||||
|
.filter('mRoomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) {
|
||||||
|
return function(room_id) {
|
||||||
|
var roomName;
|
||||||
|
|
||||||
|
// If there is an alias, use it
|
||||||
|
// TODO: only one alias is managed for now
|
||||||
|
var alias = matrixService.getRoomIdToAliasMapping(room_id);
|
||||||
|
if (alias) {
|
||||||
|
roomName = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefined === roomName) {
|
||||||
|
// Else, build the name from its users
|
||||||
|
var room = $rootScope.events.rooms[room_id];
|
||||||
|
if (room) {
|
||||||
|
var room_name_event = room["m.room.name"];
|
||||||
|
|
||||||
|
if (room_name_event) {
|
||||||
|
roomName = room_name_event.content.name;
|
||||||
|
}
|
||||||
|
else if (room.members) {
|
||||||
|
// Limit the room renaming to 1:1 room
|
||||||
|
if (2 === Object.keys(room.members).length) {
|
||||||
|
for (var i in room.members) {
|
||||||
|
var member = room.members[i];
|
||||||
|
if (member.state_key !== matrixService.config().user_id) {
|
||||||
|
|
||||||
|
if (member.state_key in $rootScope.presence) {
|
||||||
|
// If the user is available in presence, use the displayname there
|
||||||
|
// as it is the most uptodate
|
||||||
|
roomName = $rootScope.presence[member.state_key].content.displayname;
|
||||||
|
}
|
||||||
|
else if (member.content.displayname) {
|
||||||
|
roomName = member.content.displayname;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
roomName = member.state_key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (1 === Object.keys(room.members).length) {
|
||||||
|
// The other member may be in the invite list, get all invited users
|
||||||
|
var invitedUserIDs = [];
|
||||||
|
for (var i in room.messages) {
|
||||||
|
var message = room.messages[i];
|
||||||
|
if ("m.room.member" === message.type && "invite" === message.membership) {
|
||||||
|
// Make sure there is no duplicate user
|
||||||
|
if (-1 === invitedUserIDs.indexOf(message.state_key)) {
|
||||||
|
invitedUserIDs.push(message.state_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For now, only 1:1 room needs to be renamed. It means only 1 invited user
|
||||||
|
if (1 === invitedUserIDs.length) {
|
||||||
|
var userID = invitedUserIDs[0];
|
||||||
|
|
||||||
|
// Try to resolve his displayname in presence global data
|
||||||
|
if (userID in $rootScope.presence) {
|
||||||
|
roomName = $rootScope.presence[userID].content.displayname;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
roomName = userID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefined === roomName) {
|
||||||
|
// By default, use the room ID
|
||||||
|
roomName = room_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return roomName;
|
||||||
|
};
|
||||||
|
}])
|
||||||
|
|
||||||
|
// Compute the user display name in a room according to the data already downloaded
|
||||||
|
.filter('mUserDisplayName', ['$rootScope', function($rootScope) {
|
||||||
|
return function(user_id, room_id) {
|
||||||
|
var displayName;
|
||||||
|
|
||||||
|
// Try to find the user name among presence data
|
||||||
|
// Warning: that means we have received before a presence event for this
|
||||||
|
// user which cannot be guaranted.
|
||||||
|
// However, if we get the info by this way, we are sure this is the latest user display name
|
||||||
|
// See FIXME comment below
|
||||||
|
if (user_id in $rootScope.presence) {
|
||||||
|
displayName = $rootScope.presence[user_id].content.displayname;
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: Would like to use the display name as defined in room members of the room.
|
||||||
|
// But this information is the display name of the user when he has joined the room.
|
||||||
|
// It does not take into account user display name update
|
||||||
|
if (room_id) {
|
||||||
|
var room = $rootScope.events.rooms[room_id];
|
||||||
|
if (room && (user_id in room.members)) {
|
||||||
|
var member = room.members[user_id];
|
||||||
|
if (member.content.displayname) {
|
||||||
|
displayName = member.content.displayname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefined === displayName) {
|
||||||
|
// By default, use the user ID
|
||||||
|
displayName = user_id;
|
||||||
|
}
|
||||||
|
return displayName;
|
||||||
|
};
|
||||||
|
}]);
|
@ -36,6 +36,9 @@ angular.module('matrixService', [])
|
|||||||
*/
|
*/
|
||||||
var config;
|
var config;
|
||||||
|
|
||||||
|
var roomIdToAlias = {};
|
||||||
|
var aliasToRoomId = {};
|
||||||
|
|
||||||
// Current version of permanent storage
|
// Current version of permanent storage
|
||||||
var configVersion = 0;
|
var configVersion = 0;
|
||||||
var prefixPath = "/_matrix/client/api/v1";
|
var prefixPath = "/_matrix/client/api/v1";
|
||||||
@ -84,15 +87,32 @@ angular.module('matrixService', [])
|
|||||||
prefix: prefixPath,
|
prefix: prefixPath,
|
||||||
|
|
||||||
// Register an user
|
// Register an user
|
||||||
register: function(user_name, password, threepidCreds) {
|
register: function(user_name, password, threepidCreds, useCaptcha) {
|
||||||
// The REST path spec
|
// The REST path spec
|
||||||
var path = "/register";
|
var path = "/register";
|
||||||
|
|
||||||
return doRequest("POST", path, undefined, {
|
var data = {
|
||||||
user_id: user_name,
|
user_id: user_name,
|
||||||
password: password,
|
password: password,
|
||||||
threepidCreds: threepidCreds
|
threepidCreds: threepidCreds
|
||||||
});
|
};
|
||||||
|
|
||||||
|
if (useCaptcha) {
|
||||||
|
// Not all home servers will require captcha on signup, but if this flag is checked,
|
||||||
|
// send captcha information.
|
||||||
|
// TODO: Might be nice to make this a bit more flexible..
|
||||||
|
var challengeToken = Recaptcha.get_challenge();
|
||||||
|
var captchaEntry = Recaptcha.get_response();
|
||||||
|
var captchaType = "m.login.recaptcha";
|
||||||
|
|
||||||
|
data.captcha = {
|
||||||
|
type: captchaType,
|
||||||
|
challenge: challengeToken,
|
||||||
|
response: captchaEntry
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return doRequest("POST", path, undefined, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Create a room
|
// Create a room
|
||||||
@ -168,18 +188,20 @@ angular.module('matrixService', [])
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Change the membership of an another user
|
// Change the membership of an another user
|
||||||
setMembership: function(room_id, user_id, membershipValue) {
|
setMembership: function(room_id, user_id, membershipValue, reason) {
|
||||||
|
|
||||||
// The REST path spec
|
// The REST path spec
|
||||||
var path = "/rooms/$room_id/state/m.room.member/$user_id";
|
var path = "/rooms/$room_id/state/m.room.member/$user_id";
|
||||||
path = path.replace("$room_id", encodeURIComponent(room_id));
|
path = path.replace("$room_id", encodeURIComponent(room_id));
|
||||||
path = path.replace("$user_id", user_id);
|
path = path.replace("$user_id", user_id);
|
||||||
|
|
||||||
return doRequest("PUT", path, undefined, {
|
return doRequest("PUT", path, undefined, {
|
||||||
membership: membershipValue
|
membership : membershipValue,
|
||||||
|
reason: reason
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
// Bans a user from from a room
|
// Bans a user from a room
|
||||||
ban: function(room_id, user_id, reason) {
|
ban: function(room_id, user_id, reason) {
|
||||||
var path = "/rooms/$room_id/ban";
|
var path = "/rooms/$room_id/ban";
|
||||||
path = path.replace("$room_id", encodeURIComponent(room_id));
|
path = path.replace("$room_id", encodeURIComponent(room_id));
|
||||||
@ -189,7 +211,20 @@ angular.module('matrixService', [])
|
|||||||
reason: reason
|
reason: reason
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Unbans a user in a room
|
||||||
|
unban: function(room_id, user_id) {
|
||||||
|
// FIXME: To update when there will be homeserver API for unban
|
||||||
|
// For now, do an unban by resetting the user membership to "leave"
|
||||||
|
return this.setMembership(room_id, user_id, "leave");
|
||||||
|
},
|
||||||
|
|
||||||
|
// Kicks a user from a room
|
||||||
|
kick: function(room_id, user_id, reason) {
|
||||||
|
// Set the user membership to "leave" to kick him
|
||||||
|
return this.setMembership(room_id, user_id, "leave", reason);
|
||||||
|
},
|
||||||
|
|
||||||
// Retrieves the room ID corresponding to a room alias
|
// Retrieves the room ID corresponding to a room alias
|
||||||
resolveRoomAlias:function(room_alias) {
|
resolveRoomAlias:function(room_alias) {
|
||||||
var path = "/_matrix/client/api/v1/directory/room/$room_alias";
|
var path = "/_matrix/client/api/v1/directory/room/$room_alias";
|
||||||
@ -280,6 +315,11 @@ angular.module('matrixService', [])
|
|||||||
return doRequest("GET", path);
|
return doRequest("GET", path);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// get a user's profile
|
||||||
|
getProfile: function(userId) {
|
||||||
|
return this.getProfileInfo(userId);
|
||||||
|
},
|
||||||
|
|
||||||
// get a display name for this user ID
|
// get a display name for this user ID
|
||||||
getDisplayName: function(userId) {
|
getDisplayName: function(userId) {
|
||||||
return this.getProfileInfo(userId, "displayname");
|
return this.getProfileInfo(userId, "displayname");
|
||||||
@ -313,8 +353,8 @@ angular.module('matrixService', [])
|
|||||||
},
|
},
|
||||||
|
|
||||||
getProfileInfo: function(userId, info_segment) {
|
getProfileInfo: function(userId, info_segment) {
|
||||||
var path = "/profile/$user_id/" + info_segment;
|
var path = "/profile/"+userId
|
||||||
path = path.replace("$user_id", userId);
|
if (info_segment) path += '/' + info_segment;
|
||||||
return doRequest("GET", path);
|
return doRequest("GET", path);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -485,18 +525,20 @@ angular.module('matrixService', [])
|
|||||||
room_alias: undefined,
|
room_alias: undefined,
|
||||||
room_display_name: undefined
|
room_display_name: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
var alias = this.getRoomIdToAliasMapping(room.room_id);
|
var alias = this.getRoomIdToAliasMapping(room.room_id);
|
||||||
if (alias) {
|
if (alias) {
|
||||||
// use the existing alias from storage
|
// use the existing alias from storage
|
||||||
result.room_alias = alias;
|
result.room_alias = alias;
|
||||||
result.room_display_name = alias;
|
result.room_display_name = alias;
|
||||||
}
|
}
|
||||||
|
// XXX: this only lets us learn aliases from our local HS - we should
|
||||||
|
// make the client stop returning this if we can trust m.room.aliases state events
|
||||||
else if (room.aliases && room.aliases[0]) {
|
else if (room.aliases && room.aliases[0]) {
|
||||||
// save the mapping
|
// save the mapping
|
||||||
// TODO: select the smarter alias from the array
|
// TODO: select the smarter alias from the array
|
||||||
this.createRoomIdToAliasMapping(room.room_id, room.aliases[0]);
|
this.createRoomIdToAliasMapping(room.room_id, room.aliases[0]);
|
||||||
result.room_display_name = room.aliases[0];
|
result.room_display_name = room.aliases[0];
|
||||||
|
result.room_alias = room.aliases[0];
|
||||||
}
|
}
|
||||||
else if (room.membership === "invite" && "inviter" in room) {
|
else if (room.membership === "invite" && "inviter" in room) {
|
||||||
result.room_display_name = room.inviter + "'s room";
|
result.room_display_name = room.inviter + "'s room";
|
||||||
@ -509,13 +551,22 @@ angular.module('matrixService', [])
|
|||||||
},
|
},
|
||||||
|
|
||||||
createRoomIdToAliasMapping: function(roomId, alias) {
|
createRoomIdToAliasMapping: function(roomId, alias) {
|
||||||
localStorage.setItem(MAPPING_PREFIX+roomId, alias);
|
roomIdToAlias[roomId] = alias;
|
||||||
|
aliasToRoomId[alias] = roomId;
|
||||||
|
// localStorage.setItem(MAPPING_PREFIX+roomId, alias);
|
||||||
},
|
},
|
||||||
|
|
||||||
getRoomIdToAliasMapping: function(roomId) {
|
getRoomIdToAliasMapping: function(roomId) {
|
||||||
return localStorage.getItem(MAPPING_PREFIX+roomId);
|
var alias = roomIdToAlias[roomId]; // was localStorage.getItem(MAPPING_PREFIX+roomId)
|
||||||
|
//console.log("looking for alias for " + roomId + "; found: " + alias);
|
||||||
|
return alias;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getAliasToRoomIdMapping: function(alias) {
|
||||||
|
var roomId = aliasToRoomId[alias];
|
||||||
|
//console.log("looking for roomId for " + alias + "; found: " + roomId);
|
||||||
|
return roomId;
|
||||||
|
},
|
||||||
|
|
||||||
/****** Power levels management ******/
|
/****** Power levels management ******/
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
<div class="public_rooms" ng-repeat="room in public_rooms">
|
<div class="public_rooms" ng-repeat="room in public_rooms">
|
||||||
<div>
|
<div>
|
||||||
<a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_alias }}</a>
|
<a href="#/room/{{ room.room_alias ? room.room_alias : room.room_id }}" >{{ room.room_display_name }}</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
|
BIN
webclient/img/green_phone.png
Normal file
BIN
webclient/img/green_phone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 434 B |
BIN
webclient/img/red_phone.png
Normal file
BIN
webclient/img/red_phone.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 378 B |
@ -10,12 +10,14 @@
|
|||||||
|
|
||||||
<meta name="viewport" content="width=device-width">
|
<meta name="viewport" content="width=device-width">
|
||||||
|
|
||||||
<script type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
|
<script type='text/javascript' src='js/jquery-1.8.3.min.js'></script>
|
||||||
|
<script type="text/javascript" src="https://www.google.com/recaptcha/api/js/recaptcha_ajax.js"></script>
|
||||||
<script src="js/angular.min.js"></script>
|
<script src="js/angular.min.js"></script>
|
||||||
<script src="js/angular-route.min.js"></script>
|
<script src="js/angular-route.min.js"></script>
|
||||||
<script src="js/angular-sanitize.min.js"></script>
|
<script src="js/angular-sanitize.min.js"></script>
|
||||||
<script type='text/javascript' src='js/ng-infinite-scroll-matrix.js'></script>
|
<script type='text/javascript' src='js/ng-infinite-scroll-matrix.js'></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
|
<script src="config.js"></script>
|
||||||
<script src="app-controller.js"></script>
|
<script src="app-controller.js"></script>
|
||||||
<script src="app-directive.js"></script>
|
<script src="app-directive.js"></script>
|
||||||
<script src="app-filter.js"></script>
|
<script src="app-filter.js"></script>
|
||||||
@ -29,6 +31,7 @@
|
|||||||
<script src="settings/settings-controller.js"></script>
|
<script src="settings/settings-controller.js"></script>
|
||||||
<script src="user/user-controller.js"></script>
|
<script src="user/user-controller.js"></script>
|
||||||
<script src="components/matrix/matrix-service.js"></script>
|
<script src="components/matrix/matrix-service.js"></script>
|
||||||
|
<script src="components/matrix/matrix-filter.js"></script>
|
||||||
<script src="components/matrix/matrix-call.js"></script>
|
<script src="components/matrix/matrix-call.js"></script>
|
||||||
<script src="components/matrix/matrix-phone-service.js"></script>
|
<script src="components/matrix/matrix-phone-service.js"></script>
|
||||||
<script src="components/matrix/event-stream-service.js"></script>
|
<script src="components/matrix/event-stream-service.js"></script>
|
||||||
@ -44,18 +47,29 @@
|
|||||||
<div id="header">
|
<div id="header">
|
||||||
<!-- Do not show buttons on the login page -->
|
<!-- Do not show buttons on the login page -->
|
||||||
<div id="headerContent" ng-hide="'/login' == location || '/register' == location">
|
<div id="headerContent" ng-hide="'/login' == location || '/register' == location">
|
||||||
<div id="callBar">
|
<div id="callBar" ng-show="currentCall">
|
||||||
<div ng-show="currentCall.state == 'ringing'">
|
<img id="callPeerImage" ng-show="currentCall.userProfile.avatar_url" ngSrc="{{ currentCall.userProfile.avatar_url }}" />
|
||||||
Incoming call from {{ currentCall.user_id }}
|
<img class="callIcon" src="img/green_phone.png" ng-show="currentCall.state != 'ended'" />
|
||||||
<button ng-click="answerCall()">Answer</button>
|
<img class="callIcon" id="callEndedIcon" src="img/red_phone.png" ng-show="currentCall.state == 'ended'" />
|
||||||
<button ng-click="hangupCall()">Reject</button>
|
<div id="callPeerNameAndState">
|
||||||
|
<span id="callPeerName">{{ currentCall.userProfile.displayname }}</span>
|
||||||
|
<br />
|
||||||
|
<span id="callState">
|
||||||
|
<span ng-show="currentCall.state == 'invite_sent'">Calling...</span>
|
||||||
|
<span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
|
||||||
|
<span ng-show="currentCall.state == 'connected'">Call Connected</span>
|
||||||
|
<span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'outbound'">Call Rejected</span>
|
||||||
|
<span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'outbound'">Call Ended</span>
|
||||||
|
<span ng-show="currentCall.state == 'ended' && !currentCall.didConnect && currentCall.direction == 'inbound'">Call Canceled</span>
|
||||||
|
<span ng-show="currentCall.state == 'ended' && currentCall.didConnect && currentCall.direction == 'inbound'">Call Ended</span>
|
||||||
|
<span ng-show="currentCall.state == 'wait_local_media'">Waiting for media permission...</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<span ng-show="currentCall.state == 'ringing'">
|
||||||
|
<button ng-click="answerCall()">Answer</button>
|
||||||
|
<button ng-click="hangupCall()">Reject</button>
|
||||||
|
</span>
|
||||||
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button>
|
<button ng-click="hangupCall()" ng-show="currentCall && currentCall.state != 'ringing' && currentCall.state != 'ended' && currentCall.state != 'fledgling'">Hang up</button>
|
||||||
<span ng-show="currentCall.state == 'invite_sent'">Calling...</span>
|
|
||||||
<span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
|
|
||||||
<span ng-show="currentCall.state == 'connected'">Call Connected</span>
|
|
||||||
<span ng-show="currentCall.state == 'ended'">Call Ended</span>
|
|
||||||
<span style="display: none; ">{{ currentCall.state }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<a href id="headerUserId" ng-click='goToUserPage(user_id)'>{{ user_id }}</a>
|
<a href id="headerUserId" ng-click='goToUserPage(user_id)'>{{ user_id }}</a>
|
||||||
|
|
||||||
|
@ -39,8 +39,8 @@
|
|||||||
Only http://matrix.org:8090 currently exists.</div>
|
Only http://matrix.org:8090 currently exists.</div>
|
||||||
<br/>
|
<br/>
|
||||||
<br/>
|
<br/>
|
||||||
<a href="#/register" style="padding-right: 3em">Create account</a>
|
<a href="#/register" style="padding-right: 0em">Create account</a>
|
||||||
<a href="#/reset_password">Forgotten password?</a>
|
<a href="#/reset_password" style="display: none; ">Forgotten password?</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -19,6 +19,12 @@ angular.module('RegisterController', ['matrixService'])
|
|||||||
function($scope, $rootScope, $location, matrixService, eventStreamService) {
|
function($scope, $rootScope, $location, matrixService, eventStreamService) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
|
var config = window.webClientConfig;
|
||||||
|
var useCaptcha = true;
|
||||||
|
if (config !== undefined) {
|
||||||
|
useCaptcha = config.useCaptcha;
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: factor out duplication with login-controller.js
|
// FIXME: factor out duplication with login-controller.js
|
||||||
|
|
||||||
// Assume that this is hosted on the home server, in which case the URL
|
// Assume that this is hosted on the home server, in which case the URL
|
||||||
@ -87,9 +93,12 @@ angular.module('RegisterController', ['matrixService'])
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.registerWithMxidAndPassword = function(mxid, password, threepidCreds) {
|
$scope.registerWithMxidAndPassword = function(mxid, password, threepidCreds) {
|
||||||
matrixService.register(mxid, password, threepidCreds).then(
|
matrixService.register(mxid, password, threepidCreds, useCaptcha).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
$scope.feedback = "Success";
|
$scope.feedback = "Success";
|
||||||
|
if (useCaptcha) {
|
||||||
|
Recaptcha.destroy();
|
||||||
|
}
|
||||||
// Update the current config
|
// Update the current config
|
||||||
var config = matrixService.config();
|
var config = matrixService.config();
|
||||||
angular.extend(config, {
|
angular.extend(config, {
|
||||||
@ -116,11 +125,21 @@ angular.module('RegisterController', ['matrixService'])
|
|||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
console.trace("Registration error: "+error);
|
console.trace("Registration error: "+error);
|
||||||
|
if (useCaptcha) {
|
||||||
|
Recaptcha.reload();
|
||||||
|
}
|
||||||
if (error.data) {
|
if (error.data) {
|
||||||
if (error.data.errcode === "M_USER_IN_USE") {
|
if (error.data.errcode === "M_USER_IN_USE") {
|
||||||
$scope.feedback = "Username already taken.";
|
$scope.feedback = "Username already taken.";
|
||||||
$scope.reenter_username = true;
|
$scope.reenter_username = true;
|
||||||
}
|
}
|
||||||
|
else if (error.data.errcode == "M_CAPTCHA_INVALID") {
|
||||||
|
$scope.feedback = "Failed captcha.";
|
||||||
|
}
|
||||||
|
else if (error.data.errcode == "M_CAPTCHA_NEEDED") {
|
||||||
|
$scope.feedback = "Captcha is required on this home " +
|
||||||
|
"server.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (error.status === 0) {
|
else if (error.status === 0) {
|
||||||
$scope.feedback = "Unable to talk to the server.";
|
$scope.feedback = "Unable to talk to the server.";
|
||||||
@ -142,6 +161,33 @@ angular.module('RegisterController', ['matrixService'])
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var setupCaptcha = function() {
|
||||||
|
console.log("Setting up ReCaptcha")
|
||||||
|
var config = window.webClientConfig;
|
||||||
|
var public_key = undefined;
|
||||||
|
if (config === undefined) {
|
||||||
|
console.error("Couldn't find webClientConfig. Cannot get public key for captcha.");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
public_key = webClientConfig.recaptcha_public_key;
|
||||||
|
if (public_key === undefined) {
|
||||||
|
console.error("No public key defined for captcha!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Recaptcha.create(public_key,
|
||||||
|
"regcaptcha",
|
||||||
|
{
|
||||||
|
theme: "red",
|
||||||
|
callback: Recaptcha.focus_response_field
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.init = function() {
|
||||||
|
if (useCaptcha) {
|
||||||
|
setupCaptcha();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
|
|
||||||
<div style="text-align: center">
|
<div style="text-align: center">
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<input ng-show="!wait_3pid_code" id="email" size="32" type="text" ng-focus="true" ng-model="account.email" placeholder="Email address (optional)"/>
|
<input ng-show="!wait_3pid_code" id="email" size="32" type="text" ng-focus="true" ng-model="account.email" placeholder="Email address (optional)"/>
|
||||||
<div ng-show="!wait_3pid_code" class="smallPrint">Specifying an email address lets other users find you on Matrix more easily,<br/>
|
<div ng-show="!wait_3pid_code" class="smallPrint">Specifying an email address lets other users find you on Matrix more easily,<br/>
|
||||||
and will give you a way to reset your password in the future</div>
|
and will give you a way to reset your password in the future</div>
|
||||||
@ -26,7 +25,10 @@
|
|||||||
<input ng-show="!wait_3pid_code" id="displayName" size="32" type="text" ng-model="account.displayName" placeholder="Display name (e.g. Bob Obson)"/>
|
<input ng-show="!wait_3pid_code" id="displayName" size="32" type="text" ng-model="account.displayName" placeholder="Display name (e.g. Bob Obson)"/>
|
||||||
<br ng-show="!wait_3pid_code" />
|
<br ng-show="!wait_3pid_code" />
|
||||||
<br ng-show="!wait_3pid_code" />
|
<br ng-show="!wait_3pid_code" />
|
||||||
|
|
||||||
|
|
||||||
|
<div id="regcaptcha" ng-init="init()" />
|
||||||
|
|
||||||
<button ng-show="!wait_3pid_code" ng-click="register()" ng-disabled="!account.desired_user_id || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Sign up</button>
|
<button ng-show="!wait_3pid_code" ng-click="register()" ng-disabled="!account.desired_user_id || !account.homeserver || !account.pwd1 || !account.pwd2 || account.pwd1 !== account.pwd2">Sign up</button>
|
||||||
|
|
||||||
<div ng-show="wait_3pid_code">
|
<div ng-show="wait_3pid_code">
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
|
angular.module('RecentsController', ['matrixService', 'matrixFilter', 'eventHandlerService'])
|
||||||
.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService',
|
.controller('RecentsController', ['$scope', 'matrixService', 'eventHandlerService',
|
||||||
function($scope, matrixService, eventHandlerService) {
|
function($scope, matrixService, eventHandlerService) {
|
||||||
$scope.rooms = {};
|
$scope.rooms = {};
|
||||||
@ -28,13 +28,8 @@ angular.module('RecentsController', ['matrixService', 'eventHandlerService'])
|
|||||||
var listenToEventStream = function() {
|
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) {
|
||||||
var config = matrixService.config();
|
if (isLive) {
|
||||||
if (isLive && event.state_key === config.user_id && event.content.membership === "invite") {
|
$scope.rooms[event.room_id].lastMsg = event;
|
||||||
console.log("Invited to room " + event.room_id);
|
|
||||||
// FIXME push membership to top level key to match /im/sync
|
|
||||||
event.membership = event.content.membership;
|
|
||||||
|
|
||||||
$scope.rooms[event.room_id] = event;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
|
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
|
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
|
||||||
<tr>
|
<tr>
|
||||||
<td class="recentsRoomName">
|
<td class="recentsRoomName">
|
||||||
{{ room.room_id | roomName }}
|
{{ room.room_id | mRoomName }}
|
||||||
</td>
|
</td>
|
||||||
<td class="recentsRoomSummaryTS">
|
<td class="recentsRoomSummaryTS">
|
||||||
{{ (room.lastMsg.ts) | date:'MMM d HH:mm' }}
|
{{ (room.lastMsg.ts) | date:'MMM d HH:mm' }}
|
||||||
@ -16,27 +16,48 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td colspan="2" class="recentsRoomSummary">
|
<td colspan="2" class="recentsRoomSummary">
|
||||||
|
|
||||||
<div ng-show="room.membership === 'invite'" >
|
<div ng-show="room.membership === 'invite'">
|
||||||
{{ room.inviter }} invited you
|
{{ room.lastMsg.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="room.lastMsg.type">
|
||||||
<div ng-switch-when="m.room.member">
|
<div ng-switch-when="m.room.member">
|
||||||
{{ room.lastMsg.user_id }}
|
<span ng-if="'join' === room.lastMsg.content.membership">
|
||||||
{{ {"join": "joined", "leave": "left", "invite": "invited", "ban": "banned"}[msg.content.membership] }}
|
{{ room.lastMsg.state_key | mUserDisplayName: room.room_id}} joined
|
||||||
{{ (msg.content.membership === "invite" || msg.content.membership === "ban") ? (msg.state_key || '') : '' }}
|
</span>
|
||||||
|
<span ng-if="'leave' === room.lastMsg.content.membership">
|
||||||
|
<span ng-if="room.lastMsg.user_id === room.lastMsg.state_key">
|
||||||
|
{{room.lastMsg.state_key | mUserDisplayName: room.room_id }} left
|
||||||
|
</span>
|
||||||
|
<span ng-if="room.lastMsg.user_id !== room.lastMsg.state_key">
|
||||||
|
{{ room.lastMsg.user_id | mUserDisplayName: room.room_id }}
|
||||||
|
{{ {"join": "kicked", "ban": "unbanned"}[room.lastMsg.content.prev] }}
|
||||||
|
{{ room.lastMsg.state_key | mUserDisplayName: room.room_id }}
|
||||||
|
</span>
|
||||||
|
<span ng-if="'join' === room.lastMsg.content.prev && room.lastMsg.content.reason">
|
||||||
|
: {{ room.lastMsg.content.reason }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span ng-if="'invite' === room.lastMsg.content.membership || 'ban' === room.lastMsg.content.membership">
|
||||||
|
{{ room.lastMsg.user_id | mUserDisplayName: room.room_id }}
|
||||||
|
{{ {"invite": "invited", "ban": "banned"}[room.lastMsg.content.membership] }}
|
||||||
|
{{ room.lastMsg.state_key | mUserDisplayName: room.room_id }}
|
||||||
|
<span ng-if="'ban' === room.lastMsg.content.prev && room.lastMsg.content.reason">
|
||||||
|
: {{ room.lastMsg.content.reason }}
|
||||||
|
</span>
|
||||||
|
</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="room.lastMsg.content.msgtype">
|
||||||
<div ng-switch-when="m.text">
|
<div ng-switch-when="m.text">
|
||||||
{{ room.lastMsg.user_id }} :
|
{{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} :
|
||||||
<span ng-bind-html="(room.lastMsg.content.body) | linky:'_blank'">
|
<span ng-bind-html="(room.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 }} sent an image
|
{{ room.lastMsg.user_id | mUserDisplayName: room.room_id }} sent an image
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-switch-when="m.emote">
|
<div ng-switch-when="m.emote">
|
||||||
@ -51,7 +72,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-switch-default>
|
<div ng-switch-default>
|
||||||
<div ng-if="room.lastMsg.type.indexOf('m.call.') == 0">
|
<div ng-if="room.lastMsg.type.indexOf('m.call.') === 0">
|
||||||
Call
|
Call
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||||
.controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mPresence', 'matrixPhoneService', 'MatrixCall',
|
.controller('RoomController', ['$scope', '$timeout', '$routeParams', '$location', '$rootScope', 'matrixService', 'eventHandlerService', 'mFileUpload', 'mPresence', 'matrixPhoneService', 'MatrixCall',
|
||||||
function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, mPresence, matrixPhoneService, MatrixCall) {
|
function($scope, $timeout, $routeParams, $location, $rootScope, matrixService, eventHandlerService, mFileUpload, mPresence, matrixPhoneService, MatrixCall) {
|
||||||
'use strict';
|
'use strict';
|
||||||
@ -32,9 +32,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
|||||||
first_pagination: true, // this is toggled off when the first pagination is done
|
first_pagination: true, // this is toggled off when the first pagination is done
|
||||||
can_paginate: true, // this is toggled off when we run out of items
|
can_paginate: true, // this is toggled off when we run out of items
|
||||||
paginating: false, // used to avoid concurrent pagination requests pulling in dup contents
|
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
|
||||||
// FIXME: sending has been disabled, as surely messages should be sent in the background rather than locking the UI synchronously --Matthew
|
|
||||||
sending: false // true when a message is being sent. It helps to disable the UI when a process is running
|
|
||||||
};
|
};
|
||||||
$scope.members = {};
|
$scope.members = {};
|
||||||
$scope.autoCompleting = false;
|
$scope.autoCompleting = false;
|
||||||
@ -44,18 +42,25 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
|||||||
$scope.imageURLToSend = "";
|
$scope.imageURLToSend = "";
|
||||||
$scope.userIDToInvite = "";
|
$scope.userIDToInvite = "";
|
||||||
|
|
||||||
var scrollToBottom = function() {
|
var scrollToBottom = function(force) {
|
||||||
console.log("Scrolling to bottom");
|
console.log("Scrolling to bottom");
|
||||||
$timeout(function() {
|
|
||||||
var objDiv = document.getElementById("messageTableWrapper");
|
// Do not autoscroll to the bottom to display the new event if the user is not at the bottom.
|
||||||
objDiv.scrollTop = objDiv.scrollHeight;
|
// Exception: in case where the event is from the user, we want to force scroll to the bottom
|
||||||
}, 0);
|
var objDiv = document.getElementById("messageTableWrapper");
|
||||||
|
if ((objDiv.offsetHeight + objDiv.scrollTop >= objDiv.scrollHeight) || force) {
|
||||||
|
|
||||||
|
$timeout(function() {
|
||||||
|
objDiv.scrollTop = objDiv.scrollHeight;
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
|
$scope.$on(eventHandlerService.MSG_EVENT, function(ngEvent, event, isLive) {
|
||||||
if (isLive && event.room_id === $scope.room_id) {
|
if (isLive && event.room_id === $scope.room_id) {
|
||||||
scrollToBottom();
|
|
||||||
|
|
||||||
|
scrollToBottom();
|
||||||
|
|
||||||
if (window.Notification) {
|
if (window.Notification) {
|
||||||
// Show notification when the user is idle
|
// Show notification when the user is idle
|
||||||
if (matrixService.presence.offline === mPresence.getState()) {
|
if (matrixService.presence.offline === mPresence.getState()) {
|
||||||
@ -76,6 +81,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
|||||||
|
|
||||||
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
$scope.$on(eventHandlerService.MEMBER_EVENT, function(ngEvent, event, isLive) {
|
||||||
if (isLive) {
|
if (isLive) {
|
||||||
|
scrollToBottom();
|
||||||
updateMemberList(event);
|
updateMemberList(event);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -169,16 +175,18 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
|||||||
var updateMemberList = function(chunk) {
|
var updateMemberList = function(chunk) {
|
||||||
if (chunk.room_id != $scope.room_id) return;
|
if (chunk.room_id != $scope.room_id) return;
|
||||||
|
|
||||||
// Ignore banned and kicked (leave) people
|
|
||||||
if ("ban" === chunk.membership || "leave" === chunk.membership) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set target_user_id to keep things clear
|
// set target_user_id to keep things clear
|
||||||
var target_user_id = chunk.state_key;
|
var target_user_id = chunk.state_key;
|
||||||
|
|
||||||
var isNewMember = !(target_user_id in $scope.members);
|
var isNewMember = !(target_user_id in $scope.members);
|
||||||
if (isNewMember) {
|
if (isNewMember) {
|
||||||
|
|
||||||
|
// Ignore banned and kicked (leave) people
|
||||||
|
if ("ban" === chunk.membership || "leave" === chunk.membership) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: why are we copying these fields around inside chunk?
|
// FIXME: why are we copying these fields around inside chunk?
|
||||||
if ("presence" in chunk.content) {
|
if ("presence" in chunk.content) {
|
||||||
chunk.presence = chunk.content.presence;
|
chunk.presence = chunk.content.presence;
|
||||||
@ -202,6 +210,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// selectively update membership and presence else it will nuke the picture and displayname too :/
|
// selectively update membership and presence else it will nuke the picture and displayname too :/
|
||||||
|
|
||||||
|
// Remove banned and kicked (leave) people
|
||||||
|
if ("ban" === chunk.membership || "leave" === chunk.membership) {
|
||||||
|
delete $scope.members[target_user_id];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var member = $scope.members[target_user_id];
|
var member = $scope.members[target_user_id];
|
||||||
member.membership = chunk.content.membership;
|
member.membership = chunk.content.membership;
|
||||||
if ("presence" in chunk.content) {
|
if ("presence" in chunk.content) {
|
||||||
@ -256,7 +271,7 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
|||||||
|
|
||||||
normaliseMembersPowerLevels();
|
normaliseMembersPowerLevels();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
// Normalise users power levels so that the user with the higher power level
|
// Normalise users power levels so that the user with the higher power level
|
||||||
// will have a bar covering 100% of the width of his avatar
|
// will have a bar covering 100% of the width of his avatar
|
||||||
@ -277,104 +292,225 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
|||||||
member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel;
|
member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
$scope.send = function() {
|
$scope.send = function() {
|
||||||
if ($scope.textInput === "") {
|
if ($scope.textInput === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$scope.state.sending = true;
|
scrollToBottom(true);
|
||||||
|
|
||||||
var promise;
|
var promise;
|
||||||
|
var cmd;
|
||||||
|
var args;
|
||||||
|
var echo = false;
|
||||||
|
|
||||||
// Check for IRC style commands first
|
// Check for IRC style commands first
|
||||||
if ($scope.textInput.indexOf("/") === 0) {
|
var line = $scope.textInput;
|
||||||
var args = $scope.textInput.split(' ');
|
|
||||||
var cmd = args[0];
|
// trim any trailing whitespace, as it can confuse the parser for IRC-style commands
|
||||||
|
line = line.replace(/\s+$/, "");
|
||||||
|
|
||||||
|
if (line[0] === "/" && line[1] !== "/") {
|
||||||
|
var bits = line.match(/^(\S+?)( +(.*))?$/);
|
||||||
|
cmd = bits[1];
|
||||||
|
args = bits[3];
|
||||||
|
|
||||||
|
console.log("cmd: " + cmd + ", args: " + args);
|
||||||
|
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
case "/me":
|
case "/me":
|
||||||
var emoteMsg = args.slice(1).join(' ');
|
promise = matrixService.sendEmoteMessage($scope.room_id, args);
|
||||||
promise = matrixService.sendEmoteMessage($scope.room_id, emoteMsg);
|
echo = true;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "/nick":
|
case "/nick":
|
||||||
// Change user display name
|
// Change user display name
|
||||||
if (2 === args.length) {
|
if (args) {
|
||||||
promise = matrixService.setDisplayName(args[1]);
|
promise = matrixService.setDisplayName(args);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.feedback = "Usage: /nick <display_name>";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "/join":
|
||||||
|
// Join a room
|
||||||
|
if (args) {
|
||||||
|
var matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
|
var room_alias = matches[1];
|
||||||
|
if (room_alias.indexOf(':') == -1) {
|
||||||
|
// FIXME: actually track the :domain style name of our homeserver
|
||||||
|
// with or without port as is appropriate and append it at this point
|
||||||
|
}
|
||||||
|
|
||||||
|
var room_id = matrixService.getAliasToRoomIdMapping(room_alias);
|
||||||
|
console.log("joining " + room_alias + " id=" + room_id);
|
||||||
|
if ($rootScope.events.rooms[room_id]) {
|
||||||
|
// don't send a join event for a room you're already in.
|
||||||
|
$location.url("room/" + room_alias);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
promise = matrixService.joinAlias(room_alias).then(
|
||||||
|
function(response) {
|
||||||
|
$location.url("room/" + room_alias);
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
$scope.feedback = "Can't join room: " + JSON.stringify(error.data);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.feedback = "Usage: /join <room_alias>";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "/kick":
|
case "/kick":
|
||||||
// Kick a user from the room
|
// Kick a user from the room with an optional reason
|
||||||
if (2 === args.length) {
|
if (args) {
|
||||||
var user_id = args[1];
|
var matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
|
if (matches) {
|
||||||
// Set his state in the room as leave
|
promise = matrixService.kick($scope.room_id, matches[1], matches[3]);
|
||||||
promise = matrixService.setMembership($scope.room_id, user_id, "leave");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "/ban":
|
|
||||||
// Ban a user from the room
|
|
||||||
if (2 <= args.length) {
|
|
||||||
// TODO: The user may have entered the display name
|
|
||||||
// Need display name -> user_id resolution. Pb: how to manage user with same display names?
|
|
||||||
var user_id = args[1];
|
|
||||||
|
|
||||||
// Does the user provide a reason?
|
|
||||||
if (3 <= args.length) {
|
|
||||||
var reason = args.slice(2).join(' ');
|
|
||||||
}
|
}
|
||||||
promise = matrixService.ban($scope.room_id, user_id, reason);
|
}
|
||||||
|
|
||||||
|
if (!promise) {
|
||||||
|
$scope.feedback = "Usage: /kick <userId> [<reason>]";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "/ban":
|
||||||
|
// Ban a user from the room with an optional reason
|
||||||
|
if (args) {
|
||||||
|
var matches = args.match(/^(\S+?)( +(.*))?$/);
|
||||||
|
if (matches) {
|
||||||
|
promise = matrixService.ban($scope.room_id, matches[1], matches[3]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!promise) {
|
||||||
|
$scope.feedback = "Usage: /ban <userId> [<reason>]";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
case "/unban":
|
case "/unban":
|
||||||
// Unban a user from the room
|
// Unban a user from the room
|
||||||
if (2 === args.length) {
|
if (args) {
|
||||||
var user_id = args[1];
|
var matches = args.match(/^(\S+)$/);
|
||||||
|
if (matches) {
|
||||||
// Reset the user membership to leave to unban him
|
// Reset the user membership to "leave" to unban him
|
||||||
promise = matrixService.setMembership($scope.room_id, user_id, "leave");
|
promise = matrixService.unban($scope.room_id, matches[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!promise) {
|
||||||
|
$scope.feedback = "Usage: /unban <userId>";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "/op":
|
case "/op":
|
||||||
// Define the power level of a user
|
// Define the power level of a user
|
||||||
if (3 === args.length) {
|
if (args) {
|
||||||
var user_id = args[1];
|
var matches = args.match(/^(\S+?)( +(\d+))?$/);
|
||||||
var powerLevel = parseInt(args[2]);
|
var powerLevel = 50; // default power level for op
|
||||||
promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel);
|
if (matches) {
|
||||||
|
var user_id = matches[1];
|
||||||
|
if (matches.length === 4) {
|
||||||
|
powerLevel = parseInt(matches[3]);
|
||||||
|
}
|
||||||
|
if (powerLevel !== NaN) {
|
||||||
|
promise = matrixService.setUserPowerLevel($scope.room_id, user_id, powerLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!promise) {
|
||||||
|
$scope.feedback = "Usage: /op <userId> [<power level>]";
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "/deop":
|
case "/deop":
|
||||||
// Reset the power level of a user
|
// Reset the power level of a user
|
||||||
if (2 === args.length) {
|
if (args) {
|
||||||
var user_id = args[1];
|
var matches = args.match(/^(\S+)$/);
|
||||||
promise = matrixService.setUserPowerLevel($scope.room_id, user_id, undefined);
|
if (matches) {
|
||||||
|
promise = matrixService.setUserPowerLevel($scope.room_id, args, undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!promise) {
|
||||||
|
$scope.feedback = "Usage: /deop <userId>";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
$scope.feedback = ("Unrecognised IRC-style command: " + cmd);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!promise) {
|
// By default send this as a message unless it's an IRC-style command
|
||||||
// Send the text message
|
if (!promise && !cmd) {
|
||||||
promise = matrixService.sendTextMessage($scope.room_id, $scope.textInput);
|
// Make the request
|
||||||
|
promise = matrixService.sendTextMessage($scope.room_id, line);
|
||||||
|
echo = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
promise.then(
|
if (echo) {
|
||||||
function() {
|
// Echo the message to the room
|
||||||
console.log("Request successfully sent");
|
// To do so, create a minimalist fake text message event and add it to the in-memory list of room messages
|
||||||
$scope.textInput = "";
|
var echoMessage = {
|
||||||
$scope.state.sending = false;
|
content: {
|
||||||
},
|
body: (cmd === "/me" ? args : line),
|
||||||
function(error) {
|
hsob_ts: new Date().getTime(), // fake a timestamp
|
||||||
$scope.feedback = "Request failed: " + error.data.error;
|
msgtype: (cmd === "/me" ? "m.emote" : "m.text"),
|
||||||
$scope.state.sending = false;
|
},
|
||||||
});
|
room_id: $scope.room_id,
|
||||||
|
type: "m.room.message",
|
||||||
|
user_id: $scope.state.user_id,
|
||||||
|
// FIXME: re-enable echo_msg_state when we have a nice way to turn the field off again
|
||||||
|
// echo_msg_state: "messagePending" // Add custom field to indicate the state of this fake message to HTML
|
||||||
|
};
|
||||||
|
|
||||||
|
$scope.textInput = "";
|
||||||
|
$rootScope.events.rooms[$scope.room_id].messages.push(echoMessage);
|
||||||
|
scrollToBottom();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promise) {
|
||||||
|
promise.then(
|
||||||
|
function() {
|
||||||
|
console.log("Request successfully sent");
|
||||||
|
$scope.textInput = "";
|
||||||
|
/*
|
||||||
|
if (echoMessage) {
|
||||||
|
// Remove the fake echo message from the room messages
|
||||||
|
// It will be replaced by the one acknowledged by the server
|
||||||
|
// ...except this causes a nasty flicker. So don't swap messages for now. --matthew
|
||||||
|
// var index = $rootScope.events.rooms[$scope.room_id].messages.indexOf(echoMessage);
|
||||||
|
// if (index > -1) {
|
||||||
|
// $rootScope.events.rooms[$scope.room_id].messages.splice(index, 1);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$scope.textInput = "";
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
$scope.feedback = "Request failed: " + error.data.error;
|
||||||
|
|
||||||
|
if (echoMessage) {
|
||||||
|
// Mark the message as unsent for the rest of the page life
|
||||||
|
echoMessage.content.hsob_ts = "Unsent";
|
||||||
|
echoMessage.echo_msg_state = "messageUnSent";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.onInit = function() {
|
$scope.onInit = function() {
|
||||||
@ -531,25 +667,20 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
|||||||
};
|
};
|
||||||
|
|
||||||
$scope.sendImage = function(url, body) {
|
$scope.sendImage = function(url, body) {
|
||||||
$scope.state.sending = true;
|
scrollToBottom(true);
|
||||||
|
|
||||||
matrixService.sendImageMessage($scope.room_id, url, body).then(
|
matrixService.sendImageMessage($scope.room_id, url, body).then(
|
||||||
function() {
|
function() {
|
||||||
console.log("Image sent");
|
console.log("Image sent");
|
||||||
$scope.state.sending = false;
|
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Failed to send image: " + error.data.error;
|
$scope.feedback = "Failed to send image: " + error.data.error;
|
||||||
$scope.state.sending = false;
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.imageFileToSend;
|
$scope.imageFileToSend;
|
||||||
$scope.$watch("imageFileToSend", function(newValue, oldValue) {
|
$scope.$watch("imageFileToSend", function(newValue, oldValue) {
|
||||||
if ($scope.imageFileToSend) {
|
if ($scope.imageFileToSend) {
|
||||||
|
|
||||||
$scope.state.sending = true;
|
|
||||||
|
|
||||||
// Upload this image with its thumbnail to Internet
|
// Upload this image with its thumbnail to Internet
|
||||||
mFileUpload.uploadImageAndThumbnail($scope.imageFileToSend, THUMBNAIL_SIZE).then(
|
mFileUpload.uploadImageAndThumbnail($scope.imageFileToSend, THUMBNAIL_SIZE).then(
|
||||||
function(imageMessage) {
|
function(imageMessage) {
|
||||||
@ -557,16 +688,13 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
|||||||
matrixService.sendMessage($scope.room_id, undefined, imageMessage).then(
|
matrixService.sendMessage($scope.room_id, undefined, imageMessage).then(
|
||||||
function() {
|
function() {
|
||||||
console.log("Image message sent");
|
console.log("Image message sent");
|
||||||
$scope.state.sending = false;
|
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Failed to send image message: " + error.data.error;
|
$scope.feedback = "Failed to send image message: " + error.data.error;
|
||||||
$scope.state.sending = false;
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Can't upload image";
|
$scope.feedback = "Can't upload image";
|
||||||
$scope.state.sending = false;
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -582,6 +710,6 @@ angular.module('RoomController', ['ngSanitize', 'mFileInput'])
|
|||||||
call.onHangup = $rootScope.onCallHangup;
|
call.onHangup = $rootScope.onCallHangup;
|
||||||
call.placeCall();
|
call.placeCall();
|
||||||
$rootScope.currentCall = call;
|
$rootScope.currentCall = call;
|
||||||
}
|
};
|
||||||
|
|
||||||
}]);
|
}]);
|
||||||
|
@ -48,6 +48,9 @@ angular.module('RoomController')
|
|||||||
var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text);
|
var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text);
|
||||||
if (targetIndex === 0) {
|
if (targetIndex === 0) {
|
||||||
element[0].value = text;
|
element[0].value = text;
|
||||||
|
|
||||||
|
// Force angular to wake up and update the input ng-model by firing up input event
|
||||||
|
angular.element(element[0]).triggerHandler('input');
|
||||||
}
|
}
|
||||||
else if (search && search[1]) {
|
else if (search && search[1]) {
|
||||||
// console.log("search found: " + search);
|
// console.log("search found: " + search);
|
||||||
@ -81,7 +84,10 @@ angular.module('RoomController')
|
|||||||
expansion += " ";
|
expansion += " ";
|
||||||
element[0].value = text.replace(/@?([a-zA-Z0-9_\-:\.]+)$/, expansion);
|
element[0].value = text.replace(/@?([a-zA-Z0-9_\-:\.]+)$/, expansion);
|
||||||
// cancel blink
|
// cancel blink
|
||||||
element[0].className = "";
|
element[0].className = "";
|
||||||
|
|
||||||
|
// Force angular to wake up and update the input ng-model by firing up input event
|
||||||
|
angular.element(element[0]).triggerHandler('input');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// console.log("wrapped!");
|
// console.log("wrapped!");
|
||||||
@ -91,6 +97,9 @@ angular.module('RoomController')
|
|||||||
}, 150);
|
}, 150);
|
||||||
element[0].value = text;
|
element[0].value = text;
|
||||||
scope.tabCompleteIndex = 0;
|
scope.tabCompleteIndex = 0;
|
||||||
|
|
||||||
|
// Force angular to wake up and update the input ng-model by firing up input event
|
||||||
|
angular.element(element[0]).triggerHandler('input');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<div id="roomHeader">
|
<div id="roomHeader">
|
||||||
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
|
<a href ng-click="goToPage('/')"><img src="img/logo-small.png" width="100" height="43" alt="[matrix]"/></a>
|
||||||
<div id="roomName">
|
<div id="roomName">
|
||||||
{{ room_id | roomName }}
|
{{ room_id | mRoomName }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -40,7 +40,10 @@
|
|||||||
ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
|
ng-class="(events.rooms[room_id].messages[$index + 1].user_id !== msg.user_id ? 'differentUser' : '') + (msg.user_id === state.user_id ? ' mine' : '')" scroll-item>
|
||||||
<td class="leftBlock">
|
<td class="leftBlock">
|
||||||
<div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
|
<div class="sender" ng-hide="events.rooms[room_id].messages[$index - 1].user_id === msg.user_id">{{ members[msg.user_id].displayname || msg.user_id }}</div>
|
||||||
<div class="timestamp">{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}</div>
|
<div class="timestamp"
|
||||||
|
ng-class="msg.echo_msg_state">
|
||||||
|
{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="avatar">
|
<td class="avatar">
|
||||||
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32"
|
<img class="avatarImage" ng-src="{{ members[msg.user_id].avatar_url || 'img/default-profile.png' }}" width="32" height="32"
|
||||||
@ -59,15 +62,24 @@
|
|||||||
{{ members[msg.user_id].displayname || msg.user_id }}
|
{{ members[msg.user_id].displayname || msg.user_id }}
|
||||||
{{ {"join": "kicked", "ban": "unbanned"}[msg.content.prev] }}
|
{{ {"join": "kicked", "ban": "unbanned"}[msg.content.prev] }}
|
||||||
{{ members[msg.state_key].displayname || msg.state_key }}
|
{{ members[msg.state_key].displayname || msg.state_key }}
|
||||||
|
<span ng-if="'join' === msg.content.prev && msg.content.reason">
|
||||||
|
: {{ msg.content.reason }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="'invite' === msg.content.membership || 'ban' === msg.content.membership">
|
<span ng-if="'invite' === msg.content.membership || 'ban' === msg.content.membership">
|
||||||
{{ members[msg.user_id].displayname || msg.user_id }}
|
{{ members[msg.user_id].displayname || msg.user_id }}
|
||||||
{{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }}
|
{{ {"invite": "invited", "ban": "banned"}[msg.content.membership] }}
|
||||||
{{ members[msg.state_key].displayname || msg.state_key }}
|
{{ members[msg.state_key].displayname || msg.state_key }}
|
||||||
</span>
|
<span ng-if="'ban' === msg.content.prev && msg.content.reason">
|
||||||
|
: {{ msg.content.reason }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
<span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
|
<span ng-show='msg.content.msgtype === "m.emote"' ng-bind-html="'* ' + (members[msg.user_id].displayname || msg.user_id) + ' ' + msg.content.body | linky:'_blank'"/>
|
||||||
<span ng-show='msg.content.msgtype === "m.text"' ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
|
<span ng-show='msg.content.msgtype === "m.text"'
|
||||||
|
ng-class="msg.echo_msg_state"
|
||||||
|
ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
|
||||||
<div ng-show='msg.content.msgtype === "m.image"'>
|
<div ng-show='msg.content.msgtype === "m.image"'>
|
||||||
<div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
|
<div ng-hide='msg.content.thumbnail_url' ng-style="msg.content.body.h && { 'height' : (msg.content.body.h < 320) ? msg.content.body.h : 320}">
|
||||||
<img class="image" ng-src="{{ msg.content.url }}"/>
|
<img class="image" ng-src="{{ msg.content.url }}"/>
|
||||||
|
@ -19,6 +19,17 @@ limitations under the License.
|
|||||||
angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInput'])
|
angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInput'])
|
||||||
.controller('SettingsController', ['$scope', 'matrixService', 'mFileUpload',
|
.controller('SettingsController', ['$scope', 'matrixService', 'mFileUpload',
|
||||||
function($scope, matrixService, mFileUpload) {
|
function($scope, matrixService, mFileUpload) {
|
||||||
|
// XXX: duplicated from register
|
||||||
|
var generateClientSecret = function() {
|
||||||
|
var ret = "";
|
||||||
|
var chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
|
|
||||||
|
for (var i = 0; i < 32; i++) {
|
||||||
|
ret += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
};
|
||||||
$scope.config = matrixService.config();
|
$scope.config = matrixService.config();
|
||||||
|
|
||||||
$scope.profile = {
|
$scope.profile = {
|
||||||
@ -106,16 +117,22 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
|
|||||||
$scope.linkedEmails = {
|
$scope.linkedEmails = {
|
||||||
linkNewEmail: "", // the email entry box
|
linkNewEmail: "", // the email entry box
|
||||||
emailBeingAuthed: undefined, // to populate verification text
|
emailBeingAuthed: undefined, // to populate verification text
|
||||||
authTokenId: undefined, // the token id from the IS
|
authSid: undefined, // the token id from the IS
|
||||||
emailCode: "", // the code entry box
|
emailCode: "", // the code entry box
|
||||||
linkedEmailList: matrixService.config().emailList // linked email list
|
linkedEmailList: matrixService.config().emailList // linked email list
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.linkEmail = function(email) {
|
$scope.linkEmail = function(email) {
|
||||||
matrixService.linkEmail(email).then(
|
if (email != $scope.linkedEmails.emailBeingAuthed) {
|
||||||
|
$scope.linkedEmails.emailBeingAuthed = email;
|
||||||
|
$scope.clientSecret = generateClientSecret();
|
||||||
|
$scope.sendAttempt = 0;
|
||||||
|
}
|
||||||
|
$scope.sendAttempt++;
|
||||||
|
matrixService.linkEmail(email, $scope.clientSecret, $scope.sendAttempt).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
if (response.data.success === true) {
|
if (response.data.success === true) {
|
||||||
$scope.linkedEmails.authTokenId = response.data.tokenId;
|
$scope.linkedEmails.authSid = response.data.sid;
|
||||||
$scope.emailFeedback = "You have been sent an email.";
|
$scope.emailFeedback = "You have been sent an email.";
|
||||||
$scope.linkedEmails.emailBeingAuthed = email;
|
$scope.linkedEmails.emailBeingAuthed = email;
|
||||||
}
|
}
|
||||||
@ -129,34 +146,44 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
$scope.submitEmailCode = function(code) {
|
$scope.submitEmailCode = function() {
|
||||||
var tokenId = $scope.linkedEmails.authTokenId;
|
var tokenId = $scope.linkedEmails.authSid;
|
||||||
if (tokenId === undefined) {
|
if (tokenId === undefined) {
|
||||||
$scope.emailFeedback = "You have not requested a code with this email.";
|
$scope.emailFeedback = "You have not requested a code with this email.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
matrixService.authEmail(matrixService.config().user_id, tokenId, code).then(
|
matrixService.authEmail($scope.clientSecret, $scope.linkedEmails.authSid, $scope.linkedEmails.emailCode).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
if ("success" in response.data && response.data.success === false) {
|
if ("errcode" in response.data) {
|
||||||
$scope.emailFeedback = "Failed to authenticate email.";
|
$scope.emailFeedback = "Failed to authenticate email.";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var config = matrixService.config();
|
matrixService.bindEmail(matrixService.config().user_id, tokenId, $scope.clientSecret).then(
|
||||||
var emailList = {};
|
function(response) {
|
||||||
if ("emailList" in config) {
|
if ('errcode' in response.data) {
|
||||||
emailList = config.emailList;
|
$scope.emailFeedback = "Failed to link email.";
|
||||||
}
|
return;
|
||||||
emailList[response.address] = response;
|
}
|
||||||
// save the new email list
|
var config = matrixService.config();
|
||||||
config.emailList = emailList;
|
var emailList = {};
|
||||||
matrixService.setConfig(config);
|
if ("emailList" in config) {
|
||||||
matrixService.saveConfig();
|
emailList = config.emailList;
|
||||||
// invalidate the email being authed and update UI.
|
}
|
||||||
$scope.linkedEmails.emailBeingAuthed = undefined;
|
emailList[$scope.linkedEmails.emailBeingAuthed] = response;
|
||||||
$scope.emailFeedback = "";
|
// save the new email list
|
||||||
$scope.linkedEmails.linkedEmailList = emailList;
|
config.emailList = emailList;
|
||||||
$scope.linkedEmails.linkNewEmail = "";
|
matrixService.setConfig(config);
|
||||||
$scope.linkedEmails.emailCode = "";
|
matrixService.saveConfig();
|
||||||
|
// invalidate the email being authed and update UI.
|
||||||
|
$scope.linkedEmails.emailBeingAuthed = undefined;
|
||||||
|
$scope.emailFeedback = "";
|
||||||
|
$scope.linkedEmails.linkedEmailList = emailList;
|
||||||
|
$scope.linkedEmails.linkNewEmail = "";
|
||||||
|
$scope.linkedEmails.emailCode = "";
|
||||||
|
}, function(reason) {
|
||||||
|
$scope.emailFeedback = "Failed to link email: " + reason;
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(reason) {
|
||||||
$scope.emailFeedback = "Failed to auth email: " + reason;
|
$scope.emailFeedback = "Failed to auth email: " + reason;
|
||||||
@ -182,4 +209,4 @@ angular.module('SettingsController', ['matrixService', 'mFileUpload', 'mFileInpu
|
|||||||
$scope.settings.notifications = permission;
|
$scope.settings.notifications = permission;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
|
@ -23,14 +23,14 @@
|
|||||||
</div>
|
</div>
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
<h3 style="display: none; ">Linked emails</h3>
|
<h3>Linked emails</h3>
|
||||||
<div class="section" style="display: none; ">
|
<div class="section">
|
||||||
<form>
|
<form>
|
||||||
<input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
|
<input size="40" ng-model="linkedEmails.linkNewEmail" ng-enter="linkEmail(linkedEmails.linkNewEmail)" />
|
||||||
<button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
|
<button ng-disabled="!linkedEmails.linkNewEmail" ng-click="linkEmail(linkedEmails.linkNewEmail)">
|
||||||
Link Email
|
Link Email
|
||||||
</button>
|
</button>
|
||||||
{{ emailFeedback }}
|
{{ emailFeedback }}
|
||||||
</form>
|
</form>
|
||||||
<form ng-hide="!linkedEmails.emailBeingAuthed">
|
<form ng-hide="!linkedEmails.emailBeingAuthed">
|
||||||
Enter validation token for {{ linkedEmails.emailBeingAuthed }}:
|
Enter validation token for {{ linkedEmails.emailBeingAuthed }}:
|
||||||
@ -81,7 +81,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li>/nick <display_name>: change your display name</li>
|
<li>/nick <display_name>: change your display name</li>
|
||||||
<li>/me <action>: send the action you are doing. /me will be replaced by your display name</li>
|
<li>/me <action>: send the action you are doing. /me will be replaced by your display name</li>
|
||||||
<li>/kick <user_id>: kick the user</li>
|
<li>/kick <user_id> [<reason>]: kick the user</li>
|
||||||
<li>/ban <user_id> [<reason>]: ban the user</li>
|
<li>/ban <user_id> [<reason>]: ban the user</li>
|
||||||
<li>/unban <user_id>: unban the user</li>
|
<li>/unban <user_id>: unban the user</li>
|
||||||
<li>/op <user_id> <power_level>: set user power level</li>
|
<li>/op <user_id> <power_level>: set user power level</li>
|
||||||
|
Loading…
Reference in New Issue
Block a user