forked from MirrorHub/synapse
Merge branch 'release-v0.3.4' of github.com:matrix-org/synapse
This commit is contained in:
commit
697f6714a4
25
CHANGES.rst
25
CHANGES.rst
|
@ -1,3 +1,28 @@
|
||||||
|
Changes in synapse 0.3.4 (2014-09-25)
|
||||||
|
=====================================
|
||||||
|
This version adds support for using a TURN server. See docs/turn-howto.rst on
|
||||||
|
how to set one up.
|
||||||
|
|
||||||
|
Homeserver:
|
||||||
|
* Add support for redaction of messages.
|
||||||
|
* Fix bug where inviting a user on a remote home server could take up to
|
||||||
|
20-30s.
|
||||||
|
* Implement a get current room state API.
|
||||||
|
* Add support specifying and retrieving turn server configuration.
|
||||||
|
|
||||||
|
Webclient:
|
||||||
|
* Add button to send messages to users from the home page.
|
||||||
|
* Add support for using TURN for VoIP calls.
|
||||||
|
* Show display name change messages.
|
||||||
|
* Fix bug where the client didn't get the state of a newly joined room
|
||||||
|
until after it has been refreshed.
|
||||||
|
* Fix bugs with tab complete.
|
||||||
|
* Fix bug where holding down the down arrow caused chrome to chew 100% CPU.
|
||||||
|
* Fix bug where desktop notifications occasionally used "Undefined" as the
|
||||||
|
display name.
|
||||||
|
* Fix more places where we sometimes saw room IDs incorrectly.
|
||||||
|
* Fix bug which caused lag when entering text in the text box.
|
||||||
|
|
||||||
Changes in synapse 0.3.3 (2014-09-22)
|
Changes in synapse 0.3.3 (2014-09-22)
|
||||||
=====================================
|
=====================================
|
||||||
|
|
||||||
|
|
|
@ -639,7 +639,7 @@
|
||||||
{
|
{
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"summary": "Get a list of all the current state events for this room.",
|
"summary": "Get a list of all the current state events for this room.",
|
||||||
"notes": "NOT YET IMPLEMENTED.",
|
"notes": "This is equivalent to the events returned under the 'state' key for this room in /initialSync.",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "Event"
|
"$ref": "Event"
|
||||||
|
|
93
docs/turn-howto.rst
Normal file
93
docs/turn-howto.rst
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
How to enable VoIP relaying on your Home Server with TURN
|
||||||
|
|
||||||
|
Overview
|
||||||
|
--------
|
||||||
|
The synapse Matrix Home Server supports integration with TURN server via the
|
||||||
|
TURN server REST API
|
||||||
|
(http://tools.ietf.org/html/draft-uberti-behave-turn-rest-00). This allows
|
||||||
|
the Home Server to generate credentials that are valid for use on the TURN
|
||||||
|
server through the use of a secret shared between the Home Server and the
|
||||||
|
TURN server.
|
||||||
|
|
||||||
|
This document described how to install coturn
|
||||||
|
(https://code.google.com/p/coturn/) which also supports the TURN REST API,
|
||||||
|
and integrate it with synapse.
|
||||||
|
|
||||||
|
coturn Setup
|
||||||
|
============
|
||||||
|
|
||||||
|
1. Check out coturn::
|
||||||
|
svn checkout http://coturn.googlecode.com/svn/trunk/ coturn
|
||||||
|
cd coturn
|
||||||
|
|
||||||
|
2. Configure it::
|
||||||
|
./configure
|
||||||
|
|
||||||
|
You may need to install libevent2: if so, you should do so
|
||||||
|
in the way recommended by your operating system.
|
||||||
|
You can ignore warnings about lack of database support: a
|
||||||
|
database is unnecessary for this purpose.
|
||||||
|
|
||||||
|
3. Build and install it::
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
|
||||||
|
4. Make a config file in /etc/turnserver.conf. You can customise
|
||||||
|
a config file from turnserver.conf.default. The relevant
|
||||||
|
lines, with example values, are::
|
||||||
|
|
||||||
|
lt-cred-mech
|
||||||
|
use-auth-secret
|
||||||
|
static-auth-secret=[your secret key here]
|
||||||
|
realm=turn.myserver.org
|
||||||
|
|
||||||
|
See turnserver.conf.default for explanations of the options.
|
||||||
|
One way to generate the static-auth-secret is with pwgen::
|
||||||
|
|
||||||
|
pwgen -s 64 1
|
||||||
|
|
||||||
|
5. Ensure youe firewall allows traffic into the TURN server on
|
||||||
|
the ports you've configured it to listen on (remember to allow
|
||||||
|
both TCP and UDP if you've enabled both).
|
||||||
|
|
||||||
|
6. If you've configured coturn to support TLS/DTLS, generate or
|
||||||
|
import your private key and certificate.
|
||||||
|
|
||||||
|
7. Start the turn server::
|
||||||
|
bin/turnserver -o
|
||||||
|
|
||||||
|
|
||||||
|
synapse Setup
|
||||||
|
=============
|
||||||
|
|
||||||
|
Your home server configuration file needs the following extra keys:
|
||||||
|
|
||||||
|
1. "turn_uris": This needs to be a yaml list
|
||||||
|
of public-facing URIs for your TURN server to be given out
|
||||||
|
to your clients. Add separate entries for each transport your
|
||||||
|
TURN server supports.
|
||||||
|
|
||||||
|
2. "turn_shared_secret": This is the secret shared between your Home
|
||||||
|
server and your TURN server, so you should set it to the same
|
||||||
|
string you used in turnserver.conf.
|
||||||
|
|
||||||
|
3. "turn_user_lifetime": This is the amount of time credentials
|
||||||
|
generated by your Home Server are valid for (in milliseconds).
|
||||||
|
Shorter times offer less potential for abuse at the expense
|
||||||
|
of increased traffic between web clients and your home server
|
||||||
|
to refresh credentials. The TURN REST API specification recommends
|
||||||
|
one day (86400000).
|
||||||
|
|
||||||
|
As an example, here is the relevant section of the config file for
|
||||||
|
matrix.org::
|
||||||
|
|
||||||
|
turn_uris: turn:turn.matrix.org:3478?transport=udp,turn:turn.matrix.org:3478?transport=tcp
|
||||||
|
turn_shared_secret: n0t4ctuAllymatr1Xd0TorgSshar3d5ecret4obvIousreAsons
|
||||||
|
turn_user_lifetime: 86400000
|
||||||
|
|
||||||
|
Now, restart synapse::
|
||||||
|
|
||||||
|
cd /where/you/run/synapse
|
||||||
|
./synctl restart
|
||||||
|
|
||||||
|
...and your Home Server now supports VoIP relaying!
|
|
@ -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.3.3"
|
__version__ = "0.3.4"
|
||||||
|
|
|
@ -19,7 +19,9 @@ 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, SynapseError
|
from synapse.api.errors import AuthError, StoreError, Codes, SynapseError
|
||||||
from synapse.api.events.room import RoomMemberEvent, RoomPowerLevelsEvent
|
from synapse.api.events.room import (
|
||||||
|
RoomMemberEvent, RoomPowerLevelsEvent, RoomRedactionEvent,
|
||||||
|
)
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
@ -70,6 +72,9 @@ class Auth(object):
|
||||||
if event.type == RoomPowerLevelsEvent.TYPE:
|
if event.type == RoomPowerLevelsEvent.TYPE:
|
||||||
yield self._check_power_levels(event)
|
yield self._check_power_levels(event)
|
||||||
|
|
||||||
|
if event.type == RoomRedactionEvent.TYPE:
|
||||||
|
yield self._check_redaction(event)
|
||||||
|
|
||||||
defer.returnValue(True)
|
defer.returnValue(True)
|
||||||
else:
|
else:
|
||||||
raise AuthError(500, "Unknown event: %s" % event)
|
raise AuthError(500, "Unknown event: %s" % event)
|
||||||
|
@ -170,7 +175,7 @@ class Auth(object):
|
||||||
event.room_id,
|
event.room_id,
|
||||||
event.user_id,
|
event.user_id,
|
||||||
)
|
)
|
||||||
_, kick_level = yield self.store.get_ops_levels(event.room_id)
|
_, kick_level, _ = yield self.store.get_ops_levels(event.room_id)
|
||||||
|
|
||||||
if kick_level:
|
if kick_level:
|
||||||
kick_level = int(kick_level)
|
kick_level = int(kick_level)
|
||||||
|
@ -187,7 +192,7 @@ class Auth(object):
|
||||||
event.user_id,
|
event.user_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
ban_level, _ = yield self.store.get_ops_levels(event.room_id)
|
ban_level, _, _ = yield self.store.get_ops_levels(event.room_id)
|
||||||
|
|
||||||
if ban_level:
|
if ban_level:
|
||||||
ban_level = int(ban_level)
|
ban_level = int(ban_level)
|
||||||
|
@ -321,6 +326,29 @@ class Auth(object):
|
||||||
"You don't have permission to change that state"
|
"You don't have permission to change that state"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _check_redaction(self, event):
|
||||||
|
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
|
||||||
|
|
||||||
|
_, _, redact_level = yield self.store.get_ops_levels(event.room_id)
|
||||||
|
|
||||||
|
if not redact_level:
|
||||||
|
redact_level = 50
|
||||||
|
|
||||||
|
if user_level < redact_level:
|
||||||
|
raise AuthError(
|
||||||
|
403,
|
||||||
|
"You don't have permission to redact events"
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _check_power_levels(self, event):
|
def _check_power_levels(self, event):
|
||||||
for k, v in event.content.items():
|
for k, v in event.content.items():
|
||||||
|
@ -372,11 +400,11 @@ class Auth(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
removed = set(old_people.keys()) - set(new_people.keys())
|
removed = set(old_people.keys()) - set(new_people.keys())
|
||||||
added = set(old_people.keys()) - set(new_people.keys())
|
added = set(new_people.keys()) - set(old_people.keys())
|
||||||
same = set(old_people.keys()) & set(new_people.keys())
|
same = set(old_people.keys()) & set(new_people.keys())
|
||||||
|
|
||||||
for r in removed:
|
for r in removed:
|
||||||
if int(old_list.content[r]) > user_level:
|
if int(old_list[r]) > user_level:
|
||||||
raise AuthError(
|
raise AuthError(
|
||||||
403,
|
403,
|
||||||
"You don't have permission to remove user: %s" % (r, )
|
"You don't have permission to remove user: %s" % (r, )
|
||||||
|
|
|
@ -22,7 +22,8 @@ def serialize_event(hs, e):
|
||||||
if not isinstance(e, SynapseEvent):
|
if not isinstance(e, SynapseEvent):
|
||||||
return e
|
return e
|
||||||
|
|
||||||
d = e.get_dict()
|
# Should this strip out None's?
|
||||||
|
d = {k: v for k, v in e.get_dict().items()}
|
||||||
if "age_ts" in d:
|
if "age_ts" in d:
|
||||||
d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"]
|
d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"]
|
||||||
del d["age_ts"]
|
del d["age_ts"]
|
||||||
|
@ -58,17 +59,19 @@ class SynapseEvent(JsonEncodedObject):
|
||||||
"required_power_level",
|
"required_power_level",
|
||||||
"age_ts",
|
"age_ts",
|
||||||
"prev_content",
|
"prev_content",
|
||||||
|
"prev_state",
|
||||||
|
"redacted_because",
|
||||||
]
|
]
|
||||||
|
|
||||||
internal_keys = [
|
internal_keys = [
|
||||||
"is_state",
|
"is_state",
|
||||||
"prev_events",
|
"prev_events",
|
||||||
"prev_state",
|
|
||||||
"depth",
|
"depth",
|
||||||
"destinations",
|
"destinations",
|
||||||
"origin",
|
"origin",
|
||||||
"outlier",
|
"outlier",
|
||||||
"power_level",
|
"power_level",
|
||||||
|
"redacted",
|
||||||
]
|
]
|
||||||
|
|
||||||
required_keys = [
|
required_keys = [
|
||||||
|
|
|
@ -17,7 +17,8 @@ from synapse.api.events.room import (
|
||||||
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
RoomTopicEvent, MessageEvent, RoomMemberEvent, FeedbackEvent,
|
||||||
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
InviteJoinEvent, RoomConfigEvent, RoomNameEvent, GenericEvent,
|
||||||
RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent,
|
RoomPowerLevelsEvent, RoomJoinRulesEvent, RoomOpsPowerLevelsEvent,
|
||||||
RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent
|
RoomCreateEvent, RoomAddStateLevelEvent, RoomSendEventLevelEvent,
|
||||||
|
RoomRedactionEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from synapse.util.stringutils import random_string
|
from synapse.util.stringutils import random_string
|
||||||
|
@ -39,6 +40,7 @@ class EventFactory(object):
|
||||||
RoomAddStateLevelEvent,
|
RoomAddStateLevelEvent,
|
||||||
RoomSendEventLevelEvent,
|
RoomSendEventLevelEvent,
|
||||||
RoomOpsPowerLevelsEvent,
|
RoomOpsPowerLevelsEvent,
|
||||||
|
RoomRedactionEvent,
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, hs):
|
def __init__(self, hs):
|
||||||
|
|
|
@ -180,3 +180,12 @@ class RoomAliasesEvent(SynapseStateEvent):
|
||||||
|
|
||||||
def get_content_template(self):
|
def get_content_template(self):
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
class RoomRedactionEvent(SynapseEvent):
|
||||||
|
TYPE = "m.room.redaction"
|
||||||
|
|
||||||
|
valid_keys = SynapseEvent.valid_keys + ["redacts"]
|
||||||
|
|
||||||
|
def get_content_template(self):
|
||||||
|
return {}
|
||||||
|
|
64
synapse/api/events/utils.py
Normal file
64
synapse/api/events/utils.py
Normal file
|
@ -0,0 +1,64 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from .room import (
|
||||||
|
RoomMemberEvent, RoomJoinRulesEvent, RoomPowerLevelsEvent,
|
||||||
|
RoomAddStateLevelEvent, RoomSendEventLevelEvent, RoomOpsPowerLevelsEvent,
|
||||||
|
RoomAliasesEvent, RoomCreateEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
def prune_event(event):
|
||||||
|
""" Prunes the given event of all keys we don't know about or think could
|
||||||
|
potentially be dodgy.
|
||||||
|
|
||||||
|
This is used when we "redact" an event. We want to remove all fields that
|
||||||
|
the user has specified, but we do want to keep necessary information like
|
||||||
|
type, state_key etc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Remove all extraneous fields.
|
||||||
|
event.unrecognized_keys = {}
|
||||||
|
|
||||||
|
new_content = {}
|
||||||
|
|
||||||
|
def add_fields(*fields):
|
||||||
|
for field in fields:
|
||||||
|
if field in event.content:
|
||||||
|
new_content[field] = event.content[field]
|
||||||
|
|
||||||
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
|
add_fields("membership")
|
||||||
|
elif event.type == RoomCreateEvent.TYPE:
|
||||||
|
add_fields("creator")
|
||||||
|
elif event.type == RoomJoinRulesEvent.TYPE:
|
||||||
|
add_fields("join_rule")
|
||||||
|
elif event.type == RoomPowerLevelsEvent.TYPE:
|
||||||
|
# TODO: Actually check these are valid user_ids etc.
|
||||||
|
add_fields("default")
|
||||||
|
for k, v in event.content.items():
|
||||||
|
if k.startswith("@") and isinstance(v, (int, long)):
|
||||||
|
new_content[k] = v
|
||||||
|
elif event.type == RoomAddStateLevelEvent.TYPE:
|
||||||
|
add_fields("level")
|
||||||
|
elif event.type == RoomSendEventLevelEvent.TYPE:
|
||||||
|
add_fields("level")
|
||||||
|
elif event.type == RoomOpsPowerLevelsEvent.TYPE:
|
||||||
|
add_fields("kick_level", "ban_level", "redact_level")
|
||||||
|
elif event.type == RoomAliasesEvent.TYPE:
|
||||||
|
add_fields("aliases")
|
||||||
|
|
||||||
|
event.content = new_content
|
||||||
|
|
||||||
|
return event
|
|
@ -24,6 +24,7 @@ class CaptchaConfig(Config):
|
||||||
self.captcha_ip_origin_is_x_forwarded = (
|
self.captcha_ip_origin_is_x_forwarded = (
|
||||||
args.captcha_ip_origin_is_x_forwarded
|
args.captcha_ip_origin_is_x_forwarded
|
||||||
)
|
)
|
||||||
|
self.captcha_bypass_secret = args.captcha_bypass_secret
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_arguments(cls, parser):
|
def add_arguments(cls, parser):
|
||||||
|
@ -44,3 +45,7 @@ class CaptchaConfig(Config):
|
||||||
help="When checking captchas, use the X-Forwarded-For (XFF) header"
|
help="When checking captchas, use the X-Forwarded-For (XFF) header"
|
||||||
+ " as the client IP and not the actual client IP."
|
+ " as the client IP and not the actual client IP."
|
||||||
)
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--captcha_bypass_secret", type=str,
|
||||||
|
help="A secret key used to bypass the captcha test entirely."
|
||||||
|
)
|
||||||
|
|
|
@ -21,11 +21,12 @@ from .ratelimiting import RatelimitConfig
|
||||||
from .repository import ContentRepositoryConfig
|
from .repository import ContentRepositoryConfig
|
||||||
from .captcha import CaptchaConfig
|
from .captcha import CaptchaConfig
|
||||||
from .email import EmailConfig
|
from .email import EmailConfig
|
||||||
|
from .voip import VoipConfig
|
||||||
|
|
||||||
|
|
||||||
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
class HomeServerConfig(TlsConfig, ServerConfig, DatabaseConfig, LoggingConfig,
|
||||||
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
RatelimitConfig, ContentRepositoryConfig, CaptchaConfig,
|
||||||
EmailConfig):
|
EmailConfig, VoipConfig):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
41
synapse/config/voip.py
Normal file
41
synapse/config/voip.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# 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 VoipConfig(Config):
|
||||||
|
|
||||||
|
def __init__(self, args):
|
||||||
|
super(VoipConfig, self).__init__(args)
|
||||||
|
self.turn_uris = args.turn_uris
|
||||||
|
self.turn_shared_secret = args.turn_shared_secret
|
||||||
|
self.turn_user_lifetime = args.turn_user_lifetime
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def add_arguments(cls, parser):
|
||||||
|
super(VoipConfig, cls).add_arguments(parser)
|
||||||
|
group = parser.add_argument_group("voip")
|
||||||
|
group.add_argument(
|
||||||
|
"--turn-uris", type=str, default=None,
|
||||||
|
help="The public URIs of the TURN server to give to clients"
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--turn-shared-secret", type=str, default=None,
|
||||||
|
help="The shared secret used to compute passwords for the TURN server"
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"--turn-user-lifetime", type=int, default=(1000 * 60 * 60),
|
||||||
|
help="How long generated TURN credentials last, in ms"
|
||||||
|
)
|
|
@ -169,7 +169,15 @@ class FederationHandler(BaseHandler):
|
||||||
)
|
)
|
||||||
|
|
||||||
if not backfilled:
|
if not backfilled:
|
||||||
yield self.notifier.on_new_room_event(event)
|
extra_users = []
|
||||||
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
|
target_user_id = event.state_key
|
||||||
|
target_user = self.hs.parse_userid(target_user_id)
|
||||||
|
extra_users.append(target_user)
|
||||||
|
|
||||||
|
yield self.notifier.on_new_room_event(
|
||||||
|
event, extra_users=extra_users
|
||||||
|
)
|
||||||
|
|
||||||
if event.type == RoomMemberEvent.TYPE:
|
if event.type == RoomMemberEvent.TYPE:
|
||||||
if event.membership == Membership.JOIN:
|
if event.membership == Membership.JOIN:
|
||||||
|
|
|
@ -232,6 +232,22 @@ class MessageHandler(BaseHandler):
|
||||||
# store message in db
|
# store message in db
|
||||||
yield self._on_new_room_event(event, snapshot)
|
yield self._on_new_room_event(event, snapshot)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def get_state_events(self, user_id, room_id):
|
||||||
|
"""Retrieve all state events for a given room.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_id(str): The user requesting state events.
|
||||||
|
room_id(str): The room ID to get all state events from.
|
||||||
|
Returns:
|
||||||
|
A list of dicts representing state events. [{}, {}, {}]
|
||||||
|
"""
|
||||||
|
yield self.auth.check_joined_room(room_id, user_id)
|
||||||
|
|
||||||
|
# TODO: This is duplicating logic from snapshot_all_rooms
|
||||||
|
current_state = yield self.store.get_current_state(room_id)
|
||||||
|
defer.returnValue([self.hs.serialize_event(c) for c in current_state])
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def snapshot_all_rooms(self, user_id=None, pagin_config=None,
|
def snapshot_all_rooms(self, user_id=None, pagin_config=None,
|
||||||
feedback=False):
|
feedback=False):
|
||||||
|
|
|
@ -145,17 +145,6 @@ class RoomCreationHandler(BaseHandler):
|
||||||
content={"name": name},
|
content={"name": name},
|
||||||
)
|
)
|
||||||
|
|
||||||
yield handle_event(name_event)
|
|
||||||
elif room_alias:
|
|
||||||
name = room_alias.to_string()
|
|
||||||
name_event = self.event_factory.create_event(
|
|
||||||
etype=RoomNameEvent.TYPE,
|
|
||||||
room_id=room_id,
|
|
||||||
user_id=user_id,
|
|
||||||
required_power_level=50,
|
|
||||||
content={"name": name},
|
|
||||||
)
|
|
||||||
|
|
||||||
yield handle_event(name_event)
|
yield handle_event(name_event)
|
||||||
|
|
||||||
if "topic" in config:
|
if "topic" in config:
|
||||||
|
@ -255,6 +244,7 @@ class RoomCreationHandler(BaseHandler):
|
||||||
etype=RoomOpsPowerLevelsEvent.TYPE,
|
etype=RoomOpsPowerLevelsEvent.TYPE,
|
||||||
ban_level=50,
|
ban_level=50,
|
||||||
kick_level=50,
|
kick_level=50,
|
||||||
|
redact_level=50,
|
||||||
)
|
)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
|
|
||||||
|
|
||||||
from . import (
|
from . import (
|
||||||
room, events, register, login, profile, presence, initial_sync, directory
|
room, events, register, login, profile, presence, initial_sync, directory, voip
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -42,3 +42,4 @@ class RestServletFactory(object):
|
||||||
presence.register_servlets(hs, client_resource)
|
presence.register_servlets(hs, client_resource)
|
||||||
initial_sync.register_servlets(hs, client_resource)
|
initial_sync.register_servlets(hs, client_resource)
|
||||||
directory.register_servlets(hs, client_resource)
|
directory.register_servlets(hs, client_resource)
|
||||||
|
voip.register_servlets(hs, client_resource)
|
||||||
|
|
|
@ -21,6 +21,8 @@ from synapse.api.constants import LoginType
|
||||||
from base import RestServlet, client_path_pattern
|
from base import RestServlet, client_path_pattern
|
||||||
import synapse.util.stringutils as stringutils
|
import synapse.util.stringutils as stringutils
|
||||||
|
|
||||||
|
from hashlib import sha1
|
||||||
|
import hmac
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import urllib
|
import urllib
|
||||||
|
@ -28,6 +30,16 @@ import urllib
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# We ought to be using hmac.compare_digest() but on older pythons it doesn't
|
||||||
|
# exist. It's a _really minor_ security flaw to use plain string comparison
|
||||||
|
# because the timing attack is so obscured by all the other code here it's
|
||||||
|
# unlikely to make much difference
|
||||||
|
if hasattr(hmac, "compare_digest"):
|
||||||
|
compare_digest = hmac.compare_digest
|
||||||
|
else:
|
||||||
|
compare_digest = lambda a, b: a == b
|
||||||
|
|
||||||
|
|
||||||
class RegisterRestServlet(RestServlet):
|
class RegisterRestServlet(RestServlet):
|
||||||
"""Handles registration with the home server.
|
"""Handles registration with the home server.
|
||||||
|
|
||||||
|
@ -142,6 +154,38 @@ class RegisterRestServlet(RestServlet):
|
||||||
if not self.hs.config.enable_registration_captcha:
|
if not self.hs.config.enable_registration_captcha:
|
||||||
raise SynapseError(400, "Captcha not required.")
|
raise SynapseError(400, "Captcha not required.")
|
||||||
|
|
||||||
|
yield self._check_recaptcha(request, register_json, session)
|
||||||
|
|
||||||
|
session[LoginType.RECAPTCHA] = True # mark captcha as done
|
||||||
|
self._save_session(session)
|
||||||
|
defer.returnValue({
|
||||||
|
"next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY]
|
||||||
|
})
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def _check_recaptcha(self, request, register_json, session):
|
||||||
|
if ("captcha_bypass_hmac" in register_json and
|
||||||
|
self.hs.config.captcha_bypass_secret):
|
||||||
|
if "user" not in register_json:
|
||||||
|
raise SynapseError(400, "Captcha bypass needs 'user'")
|
||||||
|
|
||||||
|
want = hmac.new(
|
||||||
|
key=self.hs.config.captcha_bypass_secret,
|
||||||
|
msg=register_json["user"],
|
||||||
|
digestmod=sha1,
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
# str() because otherwise hmac complains that 'unicode' does not
|
||||||
|
# have the buffer interface
|
||||||
|
got = str(register_json["captcha_bypass_hmac"])
|
||||||
|
|
||||||
|
if compare_digest(want, got):
|
||||||
|
session["user"] = register_json["user"]
|
||||||
|
defer.returnValue(None)
|
||||||
|
else:
|
||||||
|
raise SynapseError(400, "Captcha bypass HMAC incorrect",
|
||||||
|
errcode=Codes.CAPTCHA_NEEDED)
|
||||||
|
|
||||||
challenge = None
|
challenge = None
|
||||||
user_response = None
|
user_response = None
|
||||||
try:
|
try:
|
||||||
|
@ -166,11 +210,6 @@ class RegisterRestServlet(RestServlet):
|
||||||
challenge,
|
challenge,
|
||||||
user_response
|
user_response
|
||||||
)
|
)
|
||||||
session[LoginType.RECAPTCHA] = True # mark captcha as done
|
|
||||||
self._save_session(session)
|
|
||||||
defer.returnValue({
|
|
||||||
"next": [LoginType.PASSWORD, LoginType.EMAIL_IDENTITY]
|
|
||||||
})
|
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def _do_email_identity(self, request, register_json, session):
|
def _do_email_identity(self, request, register_json, session):
|
||||||
|
@ -195,6 +234,10 @@ class RegisterRestServlet(RestServlet):
|
||||||
# captcha should've been done by this stage!
|
# captcha should've been done by this stage!
|
||||||
raise SynapseError(400, "Captcha is required.")
|
raise SynapseError(400, "Captcha is required.")
|
||||||
|
|
||||||
|
if ("user" in session and "user" in register_json and
|
||||||
|
session["user"] != register_json["user"]):
|
||||||
|
raise SynapseError(400, "Cannot change user ID during registration")
|
||||||
|
|
||||||
password = register_json["password"].encode("utf-8")
|
password = register_json["password"].encode("utf-8")
|
||||||
desired_user_id = (register_json["user"].encode("utf-8") if "user"
|
desired_user_id = (register_json["user"].encode("utf-8") if "user"
|
||||||
in register_json else None)
|
in register_json else None)
|
||||||
|
|
|
@ -19,7 +19,7 @@ from twisted.internet import defer
|
||||||
from base import RestServlet, client_path_pattern
|
from base import RestServlet, client_path_pattern
|
||||||
from synapse.api.errors import SynapseError, Codes
|
from synapse.api.errors import SynapseError, Codes
|
||||||
from synapse.streams.config import PaginationConfig
|
from synapse.streams.config import PaginationConfig
|
||||||
from synapse.api.events.room import RoomMemberEvent
|
from synapse.api.events.room import RoomMemberEvent, RoomRedactionEvent
|
||||||
from synapse.api.constants import Membership
|
from synapse.api.constants import Membership
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
@ -329,12 +329,13 @@ class RoomStateRestServlet(RestServlet):
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def on_GET(self, request, room_id):
|
def on_GET(self, request, room_id):
|
||||||
user = yield self.auth.get_user_by_req(request)
|
user = yield self.auth.get_user_by_req(request)
|
||||||
# TODO: Get all the current state for this room and return in the same
|
handler = self.handlers.message_handler
|
||||||
# format as initial sync, that is:
|
# Get all the current state for this room
|
||||||
# [
|
events = yield handler.get_state_events(
|
||||||
# { state event }, { state event }
|
room_id=urllib.unquote(room_id),
|
||||||
# ]
|
user_id=user.to_string(),
|
||||||
defer.returnValue((200, []))
|
)
|
||||||
|
defer.returnValue((200, events))
|
||||||
|
|
||||||
|
|
||||||
# TODO: Needs unit testing
|
# TODO: Needs unit testing
|
||||||
|
@ -430,6 +431,41 @@ class RoomMembershipRestServlet(RestServlet):
|
||||||
self.txns.store_client_transaction(request, txn_id, response)
|
self.txns.store_client_transaction(request, txn_id, response)
|
||||||
defer.returnValue(response)
|
defer.returnValue(response)
|
||||||
|
|
||||||
|
class RoomRedactEventRestServlet(RestServlet):
|
||||||
|
def register(self, http_server):
|
||||||
|
PATTERN = ("/rooms/(?P<room_id>[^/]*)/redact/(?P<event_id>[^/]*)")
|
||||||
|
register_txn_path(self, PATTERN, http_server)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_POST(self, request, room_id, event_id):
|
||||||
|
user = yield self.auth.get_user_by_req(request)
|
||||||
|
content = _parse_json(request)
|
||||||
|
|
||||||
|
event = self.event_factory.create_event(
|
||||||
|
etype=RoomRedactionEvent.TYPE,
|
||||||
|
room_id=urllib.unquote(room_id),
|
||||||
|
user_id=user.to_string(),
|
||||||
|
content=content,
|
||||||
|
redacts=event_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
msg_handler = self.handlers.message_handler
|
||||||
|
yield msg_handler.send_message(event)
|
||||||
|
|
||||||
|
defer.returnValue((200, {"event_id": event.event_id}))
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_PUT(self, request, room_id, event_id, txn_id):
|
||||||
|
try:
|
||||||
|
defer.returnValue(self.txns.get_client_transaction(request, txn_id))
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
response = yield self.on_POST(request, room_id, event_id)
|
||||||
|
|
||||||
|
self.txns.store_client_transaction(request, txn_id, response)
|
||||||
|
defer.returnValue(response)
|
||||||
|
|
||||||
|
|
||||||
def _parse_json(request):
|
def _parse_json(request):
|
||||||
try:
|
try:
|
||||||
|
@ -485,3 +521,4 @@ def register_servlets(hs, http_server):
|
||||||
PublicRoomListRestServlet(hs).register(http_server)
|
PublicRoomListRestServlet(hs).register(http_server)
|
||||||
RoomStateRestServlet(hs).register(http_server)
|
RoomStateRestServlet(hs).register(http_server)
|
||||||
RoomInitialSyncRestServlet(hs).register(http_server)
|
RoomInitialSyncRestServlet(hs).register(http_server)
|
||||||
|
RoomRedactEventRestServlet(hs).register(http_server)
|
||||||
|
|
60
synapse/rest/voip.py
Normal file
60
synapse/rest/voip.py
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from base import RestServlet, client_path_pattern
|
||||||
|
|
||||||
|
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
import base64
|
||||||
|
|
||||||
|
|
||||||
|
class VoipRestServlet(RestServlet):
|
||||||
|
PATTERN = client_path_pattern("/voip/turnServer$")
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def on_GET(self, request):
|
||||||
|
auth_user = yield self.auth.get_user_by_req(request)
|
||||||
|
|
||||||
|
turnUris = self.hs.config.turn_uris
|
||||||
|
turnSecret = self.hs.config.turn_shared_secret
|
||||||
|
userLifetime = self.hs.config.turn_user_lifetime
|
||||||
|
if not turnUris or not turnSecret or not userLifetime:
|
||||||
|
defer.returnValue( (200, {}) )
|
||||||
|
|
||||||
|
expiry = self.hs.get_clock().time_msec() + userLifetime
|
||||||
|
username = "%d:%s" % (expiry, auth_user.to_string())
|
||||||
|
|
||||||
|
mac = hmac.new(turnSecret, msg=username, digestmod=hashlib.sha1)
|
||||||
|
# We need to use standard base64 encoding here, *not* syutil's encode_base64
|
||||||
|
# because we need to add the standard padding to get the same result as the
|
||||||
|
# TURN server.
|
||||||
|
password = base64.b64encode(mac.digest())
|
||||||
|
|
||||||
|
defer.returnValue( (200, {
|
||||||
|
'username': username,
|
||||||
|
'password': password,
|
||||||
|
'ttl': userLifetime / 1000,
|
||||||
|
'uris': turnUris,
|
||||||
|
}) )
|
||||||
|
|
||||||
|
def on_OPTIONS(self, request):
|
||||||
|
return (200, {})
|
||||||
|
|
||||||
|
|
||||||
|
def register_servlets(hs, http_server):
|
||||||
|
VoipRestServlet(hs).register(http_server)
|
|
@ -24,6 +24,7 @@ from synapse.api.events.room import (
|
||||||
RoomAddStateLevelEvent,
|
RoomAddStateLevelEvent,
|
||||||
RoomSendEventLevelEvent,
|
RoomSendEventLevelEvent,
|
||||||
RoomOpsPowerLevelsEvent,
|
RoomOpsPowerLevelsEvent,
|
||||||
|
RoomRedactionEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
@ -56,12 +57,13 @@ SCHEMAS = [
|
||||||
"presence",
|
"presence",
|
||||||
"im",
|
"im",
|
||||||
"room_aliases",
|
"room_aliases",
|
||||||
|
"redactions",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# 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 = 3
|
SCHEMA_VERSION = 4
|
||||||
|
|
||||||
|
|
||||||
class _RollbackButIsFineException(Exception):
|
class _RollbackButIsFineException(Exception):
|
||||||
|
@ -182,6 +184,8 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||||
self._store_send_event_level(txn, event)
|
self._store_send_event_level(txn, event)
|
||||||
elif event.type == RoomOpsPowerLevelsEvent.TYPE:
|
elif event.type == RoomOpsPowerLevelsEvent.TYPE:
|
||||||
self._store_ops_level(txn, event)
|
self._store_ops_level(txn, event)
|
||||||
|
elif event.type == RoomRedactionEvent.TYPE:
|
||||||
|
self._store_redaction(txn, event)
|
||||||
|
|
||||||
vals = {
|
vals = {
|
||||||
"topological_ordering": event.depth,
|
"topological_ordering": event.depth,
|
||||||
|
@ -203,7 +207,7 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||||
unrec = {
|
unrec = {
|
||||||
k: v
|
k: v
|
||||||
for k, v in event.get_full_dict().items()
|
for k, v in event.get_full_dict().items()
|
||||||
if k not in vals.keys()
|
if k not in vals.keys() and k not in ["redacted", "redacted_because"]
|
||||||
}
|
}
|
||||||
vals["unrecognized_keys"] = json.dumps(unrec)
|
vals["unrecognized_keys"] = json.dumps(unrec)
|
||||||
|
|
||||||
|
@ -217,7 +221,8 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||||
)
|
)
|
||||||
raise _RollbackButIsFineException("_persist_event")
|
raise _RollbackButIsFineException("_persist_event")
|
||||||
|
|
||||||
if is_new_state and hasattr(event, "state_key"):
|
is_state = hasattr(event, "state_key") and event.state_key is not None
|
||||||
|
if is_new_state and is_state:
|
||||||
vals = {
|
vals = {
|
||||||
"event_id": event.event_id,
|
"event_id": event.event_id,
|
||||||
"room_id": event.room_id,
|
"room_id": event.room_id,
|
||||||
|
@ -241,14 +246,28 @@ class DataStore(RoomMemberStore, RoomStore,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _store_redaction(self, txn, event):
|
||||||
|
txn.execute(
|
||||||
|
"INSERT OR IGNORE INTO redactions "
|
||||||
|
"(event_id, redacts) VALUES (?,?)",
|
||||||
|
(event.event_id, event.redacts)
|
||||||
|
)
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def get_current_state(self, room_id, event_type=None, state_key=""):
|
def get_current_state(self, room_id, event_type=None, state_key=""):
|
||||||
|
del_sql = (
|
||||||
|
"SELECT event_id FROM redactions WHERE redacts = e.event_id "
|
||||||
|
"LIMIT 1"
|
||||||
|
)
|
||||||
|
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT e.* FROM events as e "
|
"SELECT e.*, (%(redacted)s) AS redacted FROM events as e "
|
||||||
"INNER JOIN current_state_events as c ON e.event_id = c.event_id "
|
"INNER JOIN current_state_events as c ON e.event_id = c.event_id "
|
||||||
"INNER JOIN state_events as s ON e.event_id = s.event_id "
|
"INNER JOIN state_events as s ON e.event_id = s.event_id "
|
||||||
"WHERE c.room_id = ? "
|
"WHERE c.room_id = ? "
|
||||||
)
|
) % {
|
||||||
|
"redacted": del_sql,
|
||||||
|
}
|
||||||
|
|
||||||
if event_type:
|
if event_type:
|
||||||
sql += " AND s.type = ? AND s.state_key = ? "
|
sql += " AND s.type = ? AND s.state_key = ? "
|
||||||
|
|
|
@ -17,6 +17,7 @@ import logging
|
||||||
from twisted.internet import defer
|
from twisted.internet import defer
|
||||||
|
|
||||||
from synapse.api.errors import StoreError
|
from synapse.api.errors import StoreError
|
||||||
|
from synapse.api.events.utils import prune_event
|
||||||
from synapse.util.logutils import log_function
|
from synapse.util.logutils import log_function
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
|
@ -345,7 +346,7 @@ class SQLBaseStore(object):
|
||||||
return self.runInteraction(func)
|
return self.runInteraction(func)
|
||||||
|
|
||||||
def _parse_event_from_row(self, row_dict):
|
def _parse_event_from_row(self, row_dict):
|
||||||
d = copy.deepcopy({k: v for k, v in row_dict.items() if v})
|
d = copy.deepcopy({k: v for k, v in row_dict.items()})
|
||||||
|
|
||||||
d.pop("stream_ordering", None)
|
d.pop("stream_ordering", None)
|
||||||
d.pop("topological_ordering", None)
|
d.pop("topological_ordering", None)
|
||||||
|
@ -382,8 +383,32 @@ class SQLBaseStore(object):
|
||||||
prev = self._parse_event_from_row(prevs[0])
|
prev = self._parse_event_from_row(prevs[0])
|
||||||
ev.prev_content = prev.content
|
ev.prev_content = prev.content
|
||||||
|
|
||||||
|
if not hasattr(ev, "redacted"):
|
||||||
|
logger.debug("Doesn't have redacted key: %s", ev)
|
||||||
|
ev.redacted = self._has_been_redacted_txn(txn, ev)
|
||||||
|
|
||||||
|
if ev.redacted:
|
||||||
|
# Get the redaction event.
|
||||||
|
sql = "SELECT * FROM events WHERE event_id = ?"
|
||||||
|
txn.execute(sql, (ev.redacted,))
|
||||||
|
|
||||||
|
del_evs = self._parse_events_txn(
|
||||||
|
txn, self.cursor_to_dict(txn)
|
||||||
|
)
|
||||||
|
|
||||||
|
if del_evs:
|
||||||
|
prune_event(ev)
|
||||||
|
ev.redacted_because = del_evs[0]
|
||||||
|
|
||||||
return events
|
return events
|
||||||
|
|
||||||
|
def _has_been_redacted_txn(self, txn, event):
|
||||||
|
sql = "SELECT event_id FROM redactions WHERE redacts = ?"
|
||||||
|
txn.execute(sql, (event.event_id,))
|
||||||
|
result = txn.fetchone()
|
||||||
|
return result[0] if result else None
|
||||||
|
|
||||||
|
|
||||||
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.
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -27,7 +27,7 @@ import logging
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level"))
|
OpsLevel = collections.namedtuple("OpsLevel", ("ban_level", "kick_level", "redact_level"))
|
||||||
|
|
||||||
|
|
||||||
class RoomStore(SQLBaseStore):
|
class RoomStore(SQLBaseStore):
|
||||||
|
@ -189,7 +189,8 @@ class RoomStore(SQLBaseStore):
|
||||||
|
|
||||||
def _get_ops_levels(self, txn, room_id):
|
def _get_ops_levels(self, txn, room_id):
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT ban_level, kick_level FROM room_ops_levels as r "
|
"SELECT ban_level, kick_level, redact_level "
|
||||||
|
"FROM room_ops_levels as r "
|
||||||
"INNER JOIN current_state_events as c "
|
"INNER JOIN current_state_events as c "
|
||||||
"ON r.event_id = c.event_id "
|
"ON r.event_id = c.event_id "
|
||||||
"WHERE c.room_id = ? "
|
"WHERE c.room_id = ? "
|
||||||
|
@ -198,7 +199,7 @@ class RoomStore(SQLBaseStore):
|
||||||
rows = txn.execute(sql, (room_id,)).fetchall()
|
rows = txn.execute(sql, (room_id,)).fetchall()
|
||||||
|
|
||||||
if len(rows) == 1:
|
if len(rows) == 1:
|
||||||
return OpsLevel(rows[0][0], rows[0][1])
|
return OpsLevel(rows[0][0], rows[0][1], rows[0][2])
|
||||||
else:
|
else:
|
||||||
return OpsLevel(None, None)
|
return OpsLevel(None, None)
|
||||||
|
|
||||||
|
@ -326,6 +327,9 @@ class RoomStore(SQLBaseStore):
|
||||||
if "ban_level" in event.content:
|
if "ban_level" in event.content:
|
||||||
content["ban_level"] = event.content["ban_level"]
|
content["ban_level"] = event.content["ban_level"]
|
||||||
|
|
||||||
|
if "redact_level" in event.content:
|
||||||
|
content["redact_level"] = event.content["redact_level"]
|
||||||
|
|
||||||
self._simple_insert_txn(
|
self._simple_insert_txn(
|
||||||
txn,
|
txn,
|
||||||
"room_ops_levels",
|
"room_ops_levels",
|
||||||
|
|
|
@ -182,14 +182,22 @@ class RoomMemberStore(SQLBaseStore):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _get_members_query_txn(self, txn, where_clause, where_values):
|
def _get_members_query_txn(self, txn, where_clause, where_values):
|
||||||
|
del_sql = (
|
||||||
|
"SELECT event_id FROM redactions WHERE redacts = e.event_id "
|
||||||
|
"LIMIT 1"
|
||||||
|
)
|
||||||
|
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT e.* FROM events as e "
|
"SELECT e.*, (%(redacted)s) AS redacted FROM events as e "
|
||||||
"INNER JOIN room_memberships as m "
|
"INNER JOIN room_memberships as m "
|
||||||
"ON e.event_id = m.event_id "
|
"ON e.event_id = m.event_id "
|
||||||
"INNER JOIN current_state_events as c "
|
"INNER JOIN current_state_events as c "
|
||||||
"ON m.event_id = c.event_id "
|
"ON m.event_id = c.event_id "
|
||||||
"WHERE %s "
|
"WHERE %(where)s "
|
||||||
) % (where_clause,)
|
) % {
|
||||||
|
"redacted": del_sql,
|
||||||
|
"where": where_clause,
|
||||||
|
}
|
||||||
|
|
||||||
txn.execute(sql, where_values)
|
txn.execute(sql, where_values)
|
||||||
rows = self.cursor_to_dict(txn)
|
rows = self.cursor_to_dict(txn)
|
||||||
|
|
12
synapse/storage/schema/delta/v4.sql
Normal file
12
synapse/storage/schema/delta/v4.sql
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS redactions (
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
redacts TEXT NOT NULL,
|
||||||
|
CONSTRAINT ev_uniq UNIQUE (event_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS redactions_event_id ON redactions (event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS redactions_redacts ON redactions (redacts);
|
||||||
|
|
||||||
|
ALTER TABLE room_ops_levels ADD COLUMN redact_level INTEGER;
|
||||||
|
|
||||||
|
PRAGMA user_version = 4;
|
|
@ -150,7 +150,8 @@ CREATE TABLE IF NOT EXISTS room_ops_levels(
|
||||||
event_id TEXT NOT NULL,
|
event_id TEXT NOT NULL,
|
||||||
room_id TEXT NOT NULL,
|
room_id TEXT NOT NULL,
|
||||||
ban_level INTEGER,
|
ban_level INTEGER,
|
||||||
kick_level INTEGER
|
kick_level INTEGER,
|
||||||
|
redact_level INTEGER
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id);
|
CREATE INDEX IF NOT EXISTS room_ops_levels_event_id ON room_ops_levels(event_id);
|
||||||
|
|
8
synapse/storage/schema/redactions.sql
Normal file
8
synapse/storage/schema/redactions.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS redactions (
|
||||||
|
event_id TEXT NOT NULL,
|
||||||
|
redacts TEXT NOT NULL,
|
||||||
|
CONSTRAINT ev_uniq UNIQUE (event_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS redactions_event_id ON redactions (event_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS redactions_redacts ON redactions (redacts);
|
|
@ -157,6 +157,11 @@ class StreamStore(SQLBaseStore):
|
||||||
"WHERE m.user_id = ? "
|
"WHERE m.user_id = ? "
|
||||||
)
|
)
|
||||||
|
|
||||||
|
del_sql = (
|
||||||
|
"SELECT event_id FROM redactions WHERE redacts = e.event_id "
|
||||||
|
"LIMIT 1"
|
||||||
|
)
|
||||||
|
|
||||||
if limit:
|
if limit:
|
||||||
limit = max(limit, MAX_STREAM_SIZE)
|
limit = max(limit, MAX_STREAM_SIZE)
|
||||||
else:
|
else:
|
||||||
|
@ -171,13 +176,14 @@ class StreamStore(SQLBaseStore):
|
||||||
return
|
return
|
||||||
|
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT * FROM events as e WHERE "
|
"SELECT *, (%(redacted)s) AS redacted FROM events AS e WHERE "
|
||||||
"((room_id IN (%(current)s)) OR "
|
"((room_id IN (%(current)s)) OR "
|
||||||
"(event_id IN (%(invites)s))) "
|
"(event_id IN (%(invites)s))) "
|
||||||
"AND e.stream_ordering > ? AND e.stream_ordering <= ? "
|
"AND e.stream_ordering > ? AND e.stream_ordering <= ? "
|
||||||
"AND e.outlier = 0 "
|
"AND e.outlier = 0 "
|
||||||
"ORDER BY stream_ordering ASC LIMIT %(limit)d "
|
"ORDER BY stream_ordering ASC LIMIT %(limit)d "
|
||||||
) % {
|
) % {
|
||||||
|
"redacted": del_sql,
|
||||||
"current": current_room_membership_sql,
|
"current": current_room_membership_sql,
|
||||||
"invites": membership_sql,
|
"invites": membership_sql,
|
||||||
"limit": limit
|
"limit": limit
|
||||||
|
@ -224,11 +230,21 @@ class StreamStore(SQLBaseStore):
|
||||||
else:
|
else:
|
||||||
limit_str = ""
|
limit_str = ""
|
||||||
|
|
||||||
|
del_sql = (
|
||||||
|
"SELECT event_id FROM redactions WHERE redacts = events.event_id "
|
||||||
|
"LIMIT 1"
|
||||||
|
)
|
||||||
|
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT * FROM events "
|
"SELECT *, (%(redacted)s) AS redacted FROM events "
|
||||||
"WHERE outlier = 0 AND room_id = ? AND %(bounds)s "
|
"WHERE outlier = 0 AND room_id = ? AND %(bounds)s "
|
||||||
"ORDER BY topological_ordering %(order)s, stream_ordering %(order)s %(limit)s "
|
"ORDER BY topological_ordering %(order)s, stream_ordering %(order)s %(limit)s "
|
||||||
) % {"bounds": bounds, "order": order, "limit": limit_str}
|
) % {
|
||||||
|
"redacted": del_sql,
|
||||||
|
"bounds": bounds,
|
||||||
|
"order": order,
|
||||||
|
"limit": limit_str
|
||||||
|
}
|
||||||
|
|
||||||
rows = yield self._execute_and_decode(
|
rows = yield self._execute_and_decode(
|
||||||
sql,
|
sql,
|
||||||
|
@ -257,11 +273,18 @@ class StreamStore(SQLBaseStore):
|
||||||
with_feedback=False):
|
with_feedback=False):
|
||||||
# TODO (erikj): Handle compressed feedback
|
# TODO (erikj): Handle compressed feedback
|
||||||
|
|
||||||
|
del_sql = (
|
||||||
|
"SELECT event_id FROM redactions WHERE redacts = events.event_id "
|
||||||
|
"LIMIT 1"
|
||||||
|
)
|
||||||
|
|
||||||
sql = (
|
sql = (
|
||||||
"SELECT * FROM events "
|
"SELECT *, (%(redacted)s) AS redacted FROM events "
|
||||||
"WHERE room_id = ? AND stream_ordering <= ? "
|
"WHERE room_id = ? AND stream_ordering <= ? "
|
||||||
"ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? "
|
"ORDER BY topological_ordering DESC, stream_ordering DESC LIMIT ? "
|
||||||
)
|
) % {
|
||||||
|
"redacted": del_sql,
|
||||||
|
}
|
||||||
|
|
||||||
rows = yield self._execute_and_decode(
|
rows = yield self._execute_and_decode(
|
||||||
sql,
|
sql,
|
||||||
|
|
|
@ -77,7 +77,7 @@ class FederationTestCase(unittest.TestCase):
|
||||||
self.datastore.persist_event.assert_called_once_with(
|
self.datastore.persist_event.assert_called_once_with(
|
||||||
ANY, False, is_new_state=False
|
ANY, False, is_new_state=False
|
||||||
)
|
)
|
||||||
self.notifier.on_new_room_event.assert_called_once_with(ANY)
|
self.notifier.on_new_room_event.assert_called_once_with(ANY, extra_users=[])
|
||||||
|
|
||||||
@defer.inlineCallbacks
|
@defer.inlineCallbacks
|
||||||
def test_invite_join_target_this(self):
|
def test_invite_join_target_this(self):
|
||||||
|
|
262
tests/storage/test_redaction.py
Normal file
262
tests/storage/test_redaction.py
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright 2014 OpenMarket Ltd
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
|
||||||
|
|
||||||
|
from tests import unittest
|
||||||
|
from twisted.internet import defer
|
||||||
|
|
||||||
|
from synapse.server import HomeServer
|
||||||
|
from synapse.api.constants import Membership
|
||||||
|
from synapse.api.events.room import (
|
||||||
|
RoomMemberEvent, MessageEvent, RoomRedactionEvent,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.utils import SQLiteMemoryDbPool
|
||||||
|
|
||||||
|
|
||||||
|
class RedactionTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def setUp(self):
|
||||||
|
db_pool = SQLiteMemoryDbPool()
|
||||||
|
yield db_pool.prepare()
|
||||||
|
|
||||||
|
hs = HomeServer(
|
||||||
|
"test",
|
||||||
|
db_pool=db_pool,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.store = hs.get_datastore()
|
||||||
|
self.event_factory = hs.get_event_factory()
|
||||||
|
|
||||||
|
self.u_alice = hs.parse_userid("@alice:test")
|
||||||
|
self.u_bob = hs.parse_userid("@bob:test")
|
||||||
|
|
||||||
|
self.room1 = hs.parse_roomid("!abc123:test")
|
||||||
|
|
||||||
|
self.depth = 1
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def inject_room_member(self, room, user, membership, prev_state=None,
|
||||||
|
extra_content={}):
|
||||||
|
self.depth += 1
|
||||||
|
|
||||||
|
event = self.event_factory.create_event(
|
||||||
|
etype=RoomMemberEvent.TYPE,
|
||||||
|
user_id=user.to_string(),
|
||||||
|
state_key=user.to_string(),
|
||||||
|
room_id=room.to_string(),
|
||||||
|
membership=membership,
|
||||||
|
content={"membership": membership},
|
||||||
|
depth=self.depth,
|
||||||
|
)
|
||||||
|
|
||||||
|
event.content.update(extra_content)
|
||||||
|
|
||||||
|
if prev_state:
|
||||||
|
event.prev_state = prev_state
|
||||||
|
|
||||||
|
# Have to create a join event using the eventfactory
|
||||||
|
yield self.store.persist_event(
|
||||||
|
event
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(event)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def inject_message(self, room, user, body):
|
||||||
|
self.depth += 1
|
||||||
|
|
||||||
|
event = self.event_factory.create_event(
|
||||||
|
etype=MessageEvent.TYPE,
|
||||||
|
user_id=user.to_string(),
|
||||||
|
room_id=room.to_string(),
|
||||||
|
content={"body": body, "msgtype": u"message"},
|
||||||
|
depth=self.depth,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.store.persist_event(
|
||||||
|
event
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(event)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def inject_redaction(self, room, event_id, user, reason):
|
||||||
|
event = self.event_factory.create_event(
|
||||||
|
etype=RoomRedactionEvent.TYPE,
|
||||||
|
user_id=user.to_string(),
|
||||||
|
room_id=room.to_string(),
|
||||||
|
content={"reason": reason},
|
||||||
|
depth=self.depth,
|
||||||
|
redacts=event_id,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield self.store.persist_event(
|
||||||
|
event
|
||||||
|
)
|
||||||
|
|
||||||
|
defer.returnValue(event)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_redact(self):
|
||||||
|
yield self.inject_room_member(
|
||||||
|
self.room1, self.u_alice, Membership.JOIN
|
||||||
|
)
|
||||||
|
|
||||||
|
start = yield self.store.get_room_events_max_id()
|
||||||
|
|
||||||
|
msg_event = yield self.inject_message(self.room1, self.u_alice, u"t")
|
||||||
|
|
||||||
|
end = yield self.store.get_room_events_max_id()
|
||||||
|
|
||||||
|
results, _ = yield self.store.get_room_events_stream(
|
||||||
|
self.u_alice.to_string(),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
None, # Is currently ignored
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(results))
|
||||||
|
|
||||||
|
# Check event has not been redacted:
|
||||||
|
event = results[0]
|
||||||
|
|
||||||
|
self.assertObjectHasAttributes(
|
||||||
|
{
|
||||||
|
"type": MessageEvent.TYPE,
|
||||||
|
"user_id": self.u_alice.to_string(),
|
||||||
|
"content": {"body": "t", "msgtype": "message"},
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(hasattr(event, "redacted_because"))
|
||||||
|
|
||||||
|
# Redact event
|
||||||
|
reason = "Because I said so"
|
||||||
|
yield self.inject_redaction(
|
||||||
|
self.room1, msg_event.event_id, self.u_alice, reason
|
||||||
|
)
|
||||||
|
|
||||||
|
results, _ = yield self.store.get_room_events_stream(
|
||||||
|
self.u_alice.to_string(),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
None, # Is currently ignored
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(results))
|
||||||
|
|
||||||
|
# Check redaction
|
||||||
|
|
||||||
|
event = results[0]
|
||||||
|
|
||||||
|
self.assertObjectHasAttributes(
|
||||||
|
{
|
||||||
|
"type": MessageEvent.TYPE,
|
||||||
|
"user_id": self.u_alice.to_string(),
|
||||||
|
"content": {},
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(hasattr(event, "redacted_because"))
|
||||||
|
|
||||||
|
self.assertObjectHasAttributes(
|
||||||
|
{
|
||||||
|
"type": RoomRedactionEvent.TYPE,
|
||||||
|
"user_id": self.u_alice.to_string(),
|
||||||
|
"content": {"reason": reason},
|
||||||
|
},
|
||||||
|
event.redacted_because,
|
||||||
|
)
|
||||||
|
|
||||||
|
@defer.inlineCallbacks
|
||||||
|
def test_redact_join(self):
|
||||||
|
yield self.inject_room_member(
|
||||||
|
self.room1, self.u_alice, Membership.JOIN
|
||||||
|
)
|
||||||
|
|
||||||
|
start = yield self.store.get_room_events_max_id()
|
||||||
|
|
||||||
|
msg_event = yield self.inject_room_member(
|
||||||
|
self.room1, self.u_bob, Membership.JOIN,
|
||||||
|
extra_content={"blue": "red"},
|
||||||
|
)
|
||||||
|
|
||||||
|
end = yield self.store.get_room_events_max_id()
|
||||||
|
|
||||||
|
results, _ = yield self.store.get_room_events_stream(
|
||||||
|
self.u_alice.to_string(),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
None, # Is currently ignored
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(results))
|
||||||
|
|
||||||
|
# Check event has not been redacted:
|
||||||
|
event = results[0]
|
||||||
|
|
||||||
|
self.assertObjectHasAttributes(
|
||||||
|
{
|
||||||
|
"type": RoomMemberEvent.TYPE,
|
||||||
|
"user_id": self.u_bob.to_string(),
|
||||||
|
"content": {"membership": Membership.JOIN, "blue": "red"},
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(hasattr(event, "redacted_because"))
|
||||||
|
|
||||||
|
# Redact event
|
||||||
|
reason = "Because I said so"
|
||||||
|
yield self.inject_redaction(
|
||||||
|
self.room1, msg_event.event_id, self.u_alice, reason
|
||||||
|
)
|
||||||
|
|
||||||
|
results, _ = yield self.store.get_room_events_stream(
|
||||||
|
self.u_alice.to_string(),
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
None, # Is currently ignored
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1, len(results))
|
||||||
|
|
||||||
|
# Check redaction
|
||||||
|
|
||||||
|
event = results[0]
|
||||||
|
|
||||||
|
self.assertObjectHasAttributes(
|
||||||
|
{
|
||||||
|
"type": RoomMemberEvent.TYPE,
|
||||||
|
"user_id": self.u_bob.to_string(),
|
||||||
|
"content": {"membership": Membership.JOIN},
|
||||||
|
},
|
||||||
|
event,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertTrue(hasattr(event, "redacted_because"))
|
||||||
|
|
||||||
|
self.assertObjectHasAttributes(
|
||||||
|
{
|
||||||
|
"type": RoomRedactionEvent.TYPE,
|
||||||
|
"user_id": self.u_alice.to_string(),
|
||||||
|
"content": {"reason": reason},
|
||||||
|
},
|
||||||
|
event.redacted_because,
|
||||||
|
)
|
|
@ -262,7 +262,7 @@ class MemoryDataStore(object):
|
||||||
return defer.succeed("invite")
|
return defer.succeed("invite")
|
||||||
|
|
||||||
def get_ops_levels(self, room_id):
|
def get_ops_levels(self, room_id):
|
||||||
return defer.succeed((5, 5))
|
return defer.succeed((5, 5, 5))
|
||||||
|
|
||||||
|
|
||||||
def _format_call(args, kwargs):
|
def _format_call(args, kwargs):
|
||||||
|
|
|
@ -67,6 +67,16 @@ angular.module('MatrixWebClientController', ['matrixService', 'mPresence', 'even
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.leave = function(room_id) {
|
||||||
|
matrixService.leave(room_id).then(
|
||||||
|
function(response) {
|
||||||
|
console.log("Left room " + room_id);
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
console.log("Failed to leave room " + room_id + ": " + error.data.error);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Logs the user out
|
// Logs the user out
|
||||||
$scope.logout = function() {
|
$scope.logout = function() {
|
||||||
|
|
||||||
|
|
|
@ -45,32 +45,33 @@ angular.module('matrixWebClient')
|
||||||
angular.forEach(members, function(value, key) {
|
angular.forEach(members, function(value, key) {
|
||||||
value["id"] = key;
|
value["id"] = key;
|
||||||
filtered.push( value );
|
filtered.push( value );
|
||||||
if (value["displayname"]) {
|
|
||||||
if (!displayNames[value["displayname"]]) {
|
|
||||||
displayNames[value["displayname"]] = [];
|
|
||||||
}
|
|
||||||
displayNames[value["displayname"]].push(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// FIXME: we shouldn't disambiguate displayNames on every orderMembersList
|
|
||||||
// invocation but keep track of duplicates incrementally somewhere
|
|
||||||
angular.forEach(displayNames, function(value, key) {
|
|
||||||
if (value.length > 1) {
|
|
||||||
// console.log(key + ": " + value);
|
|
||||||
for (var i=0; i < value.length; i++) {
|
|
||||||
var v = value[i];
|
|
||||||
// FIXME: this permenantly rewrites the displayname for a given
|
|
||||||
// room member. which means we can't reset their name if it is
|
|
||||||
// no longer ambiguous!
|
|
||||||
members[v].displayname += " (" + v + ")";
|
|
||||||
// console.log(v + " " + members[v]);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
filtered.sort(function (a, b) {
|
filtered.sort(function (a, b) {
|
||||||
return ((a["last_active_ago"] || 10e10) > (b["last_active_ago"] || 10e10) ? 1 : -1);
|
// Sort members on their last_active absolute time
|
||||||
|
var aLastActiveTS = 0, bLastActiveTS = 0;
|
||||||
|
if (undefined !== a.last_active_ago) {
|
||||||
|
aLastActiveTS = a.last_updated - a.last_active_ago;
|
||||||
|
}
|
||||||
|
if (undefined !== b.last_active_ago) {
|
||||||
|
bLastActiveTS = b.last_updated - b.last_active_ago;
|
||||||
|
}
|
||||||
|
if (aLastActiveTS || bLastActiveTS) {
|
||||||
|
return bLastActiveTS - aLastActiveTS;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// If they do not have last_active_ago, sort them according to their presence state
|
||||||
|
// Online users go first amongs members who do not have last_active_ago
|
||||||
|
var presenceLevels = {
|
||||||
|
offline: 1,
|
||||||
|
unavailable: 2,
|
||||||
|
online: 4,
|
||||||
|
free_for_chat: 3
|
||||||
|
};
|
||||||
|
var aPresence = (a.presence in presenceLevels) ? presenceLevels[a.presence] : 0;
|
||||||
|
var bPresence = (b.presence in presenceLevels) ? presenceLevels[b.presence] : 0;
|
||||||
|
return bPresence - aPresence;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return filtered;
|
return filtered;
|
||||||
};
|
};
|
||||||
|
|
|
@ -101,7 +101,7 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||||
|
|
||||||
var initRoom = function(room_id, room) {
|
var initRoom = function(room_id, room) {
|
||||||
if (!(room_id in $rootScope.events.rooms)) {
|
if (!(room_id in $rootScope.events.rooms)) {
|
||||||
console.log("Creating new handler entry for " + room_id);
|
console.log("Creating new rooms entry for " + room_id);
|
||||||
$rootScope.events.rooms[room_id] = {
|
$rootScope.events.rooms[room_id] = {
|
||||||
room_id: room_id,
|
room_id: room_id,
|
||||||
messages: [],
|
messages: [],
|
||||||
|
@ -113,10 +113,12 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room) {
|
if (room) { // we got an existing room object from initialsync, seemingly.
|
||||||
// Report all other metadata of the room object (membership, inviter, visibility, ...)
|
// Report all other metadata of the room object (membership, inviter, visibility, ...)
|
||||||
for (var field in room) {
|
for (var field in room) {
|
||||||
if (-1 === ["room_id", "messages", "state"].indexOf(field)) {
|
if (!room.hasOwnProperty(field)) continue;
|
||||||
|
|
||||||
|
if (-1 === ["room_id", "messages", "state"].indexOf(field)) { // why indexOf - why not ===? --Matthew
|
||||||
$rootScope.events.rooms[room_id][field] = room[field];
|
$rootScope.events.rooms[room_id][field] = room[field];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -211,11 +213,8 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||||
|
|
||||||
if (shouldBing && isIdle) {
|
if (shouldBing && isIdle) {
|
||||||
console.log("Displaying notification for "+JSON.stringify(event));
|
console.log("Displaying notification for "+JSON.stringify(event));
|
||||||
var member = $rootScope.events.rooms[event.room_id].members[event.user_id];
|
var member = getMember(event.room_id, event.user_id);
|
||||||
var displayname = undefined;
|
var displayname = getUserDisplayName(event.room_id, event.user_id);
|
||||||
if (member) {
|
|
||||||
displayname = member.displayname;
|
|
||||||
}
|
|
||||||
|
|
||||||
var message = event.content.body;
|
var message = event.content.body;
|
||||||
if (event.content.msgtype === "m.emote") {
|
if (event.content.msgtype === "m.emote") {
|
||||||
|
@ -223,7 +222,7 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var notification = new window.Notification(
|
var notification = new window.Notification(
|
||||||
(displayname || event.user_id) +
|
displayname +
|
||||||
" (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
|
" (" + (matrixService.getRoomIdToAliasMapping(event.room_id) || event.room_id) + ")", // FIXME: don't leak room_ids here
|
||||||
{
|
{
|
||||||
"body": message,
|
"body": message,
|
||||||
|
@ -253,7 +252,23 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||||
// Exception: Do not do this if the event is a room state event because such events already come
|
// Exception: Do not do this if the event is a room state event because such events already come
|
||||||
// as room messages events. Moreover, when they come as room messages events, they are relatively ordered
|
// as room messages events. Moreover, when they come as room messages events, they are relatively ordered
|
||||||
// with other other room messages
|
// with other other room messages
|
||||||
if (event.content.prev !== event.content.membership && !isStateEvent) {
|
if (!isStateEvent) {
|
||||||
|
// could be a membership change, display name change, etc.
|
||||||
|
// Find out which one.
|
||||||
|
var memberChanges = undefined;
|
||||||
|
if (event.content.prev !== event.content.membership) {
|
||||||
|
memberChanges = "membership";
|
||||||
|
}
|
||||||
|
else if (event.prev_content && (event.prev_content.displayname !== event.content.displayname)) {
|
||||||
|
memberChanges = "displayname";
|
||||||
|
}
|
||||||
|
|
||||||
|
// mark the key which changed
|
||||||
|
event.changedKey = memberChanges;
|
||||||
|
|
||||||
|
// If there was a change we want to display, dump it in the message
|
||||||
|
// list.
|
||||||
|
if (memberChanges) {
|
||||||
if (isLiveEvent) {
|
if (isLiveEvent) {
|
||||||
$rootScope.events.rooms[event.room_id].messages.push(event);
|
$rootScope.events.rooms[event.room_id].messages.push(event);
|
||||||
}
|
}
|
||||||
|
@ -261,6 +276,7 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||||
$rootScope.events.rooms[event.room_id].messages.unshift(event);
|
$rootScope.events.rooms[event.room_id].messages.unshift(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use data from state event or the latest data from the stream.
|
// Use data from state event or the latest data from the stream.
|
||||||
// Do not care of events that come when paginating back
|
// Do not care of events that come when paginating back
|
||||||
|
@ -328,6 +344,65 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||||
return index;
|
return index;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the member object of a room member
|
||||||
|
* @param {String} room_id the room id
|
||||||
|
* @param {String} user_id the id of the user
|
||||||
|
* @returns {undefined | Object} the member object of this user in this room if he is part of the room
|
||||||
|
*/
|
||||||
|
var getMember = function(room_id, user_id) {
|
||||||
|
var member;
|
||||||
|
|
||||||
|
var room = $rootScope.events.rooms[room_id];
|
||||||
|
if (room) {
|
||||||
|
member = room.members[user_id];
|
||||||
|
}
|
||||||
|
return member;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the display name of an user acccording to data already downloaded
|
||||||
|
* @param {String} room_id the room id
|
||||||
|
* @param {String} user_id the id of the user
|
||||||
|
* @returns {String} the user displayname or user_id if not available
|
||||||
|
*/
|
||||||
|
var getUserDisplayName = function(room_id, user_id) {
|
||||||
|
var displayName;
|
||||||
|
|
||||||
|
// Get the user display name from the member list of the room
|
||||||
|
var member = getMember(room_id, user_id);
|
||||||
|
if (member && member.content.displayname) { // Do not consider null displayname
|
||||||
|
displayName = member.content.displayname;
|
||||||
|
|
||||||
|
// Disambiguate users who have the same displayname in the room
|
||||||
|
if (user_id !== matrixService.config().user_id) {
|
||||||
|
var room = $rootScope.events.rooms[room_id];
|
||||||
|
|
||||||
|
for (var member_id in room.members) {
|
||||||
|
if (room.members.hasOwnProperty(member_id) && member_id !== user_id) {
|
||||||
|
var member2 = room.members[member_id];
|
||||||
|
if (member2.content.displayname && member2.content.displayname === displayName) {
|
||||||
|
displayName = displayName + " (" + user_id + ")";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The user may not have joined the room yet. So try to resolve display name from presence data
|
||||||
|
// Note: This data may not be available
|
||||||
|
if (undefined === displayName && user_id in $rootScope.presence) {
|
||||||
|
displayName = $rootScope.presence[user_id].content.displayname;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (undefined === displayName) {
|
||||||
|
// By default, use the user ID
|
||||||
|
displayName = user_id;
|
||||||
|
}
|
||||||
|
return displayName;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
|
ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,
|
||||||
MSG_EVENT: MSG_EVENT,
|
MSG_EVENT: MSG_EVENT,
|
||||||
|
@ -499,6 +574,8 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||||
memberCount = 0;
|
memberCount = 0;
|
||||||
|
|
||||||
for (var i in room.members) {
|
for (var i in room.members) {
|
||||||
|
if (!room.members.hasOwnProperty(i)) continue;
|
||||||
|
|
||||||
var member = room.members[i];
|
var member = room.members[i];
|
||||||
|
|
||||||
if ("join" === member.membership) {
|
if ("join" === member.membership) {
|
||||||
|
@ -517,13 +594,17 @@ function(matrixService, $rootScope, $q, $timeout, mPresence) {
|
||||||
* @returns {undefined | Object} the member object of this user in this room if he is part of the room
|
* @returns {undefined | Object} the member object of this user in this room if he is part of the room
|
||||||
*/
|
*/
|
||||||
getMember: function(room_id, user_id) {
|
getMember: function(room_id, user_id) {
|
||||||
var member;
|
return getMember(room_id, user_id);
|
||||||
|
},
|
||||||
|
|
||||||
var room = $rootScope.events.rooms[room_id];
|
/**
|
||||||
if (room) {
|
* Return the display name of an user acccording to data already downloaded
|
||||||
member = room.members[user_id];
|
* @param {String} room_id the room id
|
||||||
}
|
* @param {String} user_id the id of the user
|
||||||
return member;
|
* @returns {String} the user displayname or user_id if not available
|
||||||
|
*/
|
||||||
|
getUserDisplayName: function(room_id, user_id) {
|
||||||
|
return getUserDisplayName(room_id, user_id);
|
||||||
},
|
},
|
||||||
|
|
||||||
setRoomVisibility: function(room_id, visible) {
|
setRoomVisibility: function(room_id, visible) {
|
||||||
|
|
|
@ -66,15 +66,67 @@ angular.module('MatrixCall', [])
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MatrixCall.getTurnServer = function() {
|
||||||
|
matrixService.getTurnServer().then(function(response) {
|
||||||
|
if (response.data.uris) {
|
||||||
|
console.log("Got TURN URIs: "+response.data.uris);
|
||||||
|
MatrixCall.turnServer = response.data;
|
||||||
|
$rootScope.haveTurn = true;
|
||||||
|
// re-fetch when we're about to reach the TTL
|
||||||
|
$timeout(MatrixCall.getTurnServer, MatrixCall.turnServer.ttl * 1000 * 0.9);
|
||||||
|
} else {
|
||||||
|
console.log("Got no TURN URIs from HS");
|
||||||
|
$rootScope.haveTurn = false;
|
||||||
|
}
|
||||||
|
}, function(error) {
|
||||||
|
console.log("Failed to get TURN URIs");
|
||||||
|
MatrixCall.turnServer = {};
|
||||||
|
$timeout(MatrixCall.getTurnServer, 60000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: we should prevent any class from being placed or accepted before this has finished
|
||||||
|
MatrixCall.getTurnServer();
|
||||||
|
|
||||||
MatrixCall.CALL_TIMEOUT = 60000;
|
MatrixCall.CALL_TIMEOUT = 60000;
|
||||||
|
MatrixCall.FALLBACK_STUN_SERVER = 'stun:stun.l.google.com:19302';
|
||||||
|
|
||||||
MatrixCall.prototype.createPeerConnection = function() {
|
MatrixCall.prototype.createPeerConnection = function() {
|
||||||
var stunServer = 'stun:stun.l.google.com:19302';
|
|
||||||
var pc;
|
var pc;
|
||||||
if (window.mozRTCPeerConnection) {
|
if (window.mozRTCPeerConnection) {
|
||||||
pc = new window.mozRTCPeerConnection({'url': stunServer});
|
var iceServers = [];
|
||||||
|
if (MatrixCall.turnServer) {
|
||||||
|
if (MatrixCall.turnServer.uris) {
|
||||||
|
for (var i = 0; i < MatrixCall.turnServer.uris.length; i++) {
|
||||||
|
iceServers.push({
|
||||||
|
'url': MatrixCall.turnServer.uris[i],
|
||||||
|
'username': MatrixCall.turnServer.username,
|
||||||
|
'credential': MatrixCall.turnServer.password,
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
pc = new window.RTCPeerConnection({"iceServers":[{"urls":"stun:stun.l.google.com:19302"}]});
|
console.log("No TURN server: using fallback STUN server");
|
||||||
|
iceServers.push({ 'url' : MatrixCall.FALLBACK_STUN_SERVER });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pc = new window.mozRTCPeerConnection({"iceServers":iceServers});
|
||||||
|
} else {
|
||||||
|
var iceServers = [];
|
||||||
|
if (MatrixCall.turnServer) {
|
||||||
|
if (MatrixCall.turnServer.uris) {
|
||||||
|
iceServers.push({
|
||||||
|
'urls': MatrixCall.turnServer.uris,
|
||||||
|
'username': MatrixCall.turnServer.username,
|
||||||
|
'credential': MatrixCall.turnServer.password,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log("No TURN server: using fallback STUN server");
|
||||||
|
iceServers.push({ 'urls' : MatrixCall.FALLBACK_STUN_SERVER });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pc = new window.RTCPeerConnection({"iceServers":iceServers});
|
||||||
}
|
}
|
||||||
var self = this;
|
var self = this;
|
||||||
pc.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
|
pc.oniceconnectionstatechange = function() { self.onIceConnectionStateChanged(); };
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
angular.module('matrixFilter', [])
|
angular.module('matrixFilter', [])
|
||||||
|
|
||||||
// Compute the room name according to information we have
|
// Compute the room name according to information we have
|
||||||
.filter('mRoomName', ['$rootScope', 'matrixService', function($rootScope, matrixService) {
|
.filter('mRoomName', ['$rootScope', 'matrixService', 'eventHandlerService', function($rootScope, matrixService, eventHandlerService) {
|
||||||
return function(room_id) {
|
return function(room_id) {
|
||||||
var roomName;
|
var roomName;
|
||||||
|
|
||||||
|
@ -31,13 +31,20 @@ angular.module('matrixFilter', [])
|
||||||
if (room) {
|
if (room) {
|
||||||
// Get name from room state date
|
// Get name from room state date
|
||||||
var room_name_event = room["m.room.name"];
|
var room_name_event = room["m.room.name"];
|
||||||
|
|
||||||
|
// Determine if it is a public room
|
||||||
|
var isPublicRoom = false;
|
||||||
|
if (room["m.room.join_rules"] && room["m.room.join_rules"].content) {
|
||||||
|
isPublicRoom = ("public" === room["m.room.join_rules"].content.join_rule);
|
||||||
|
}
|
||||||
|
|
||||||
if (room_name_event) {
|
if (room_name_event) {
|
||||||
roomName = room_name_event.content.name;
|
roomName = room_name_event.content.name;
|
||||||
}
|
}
|
||||||
else if (alias) {
|
else if (alias) {
|
||||||
roomName = alias;
|
roomName = alias;
|
||||||
}
|
}
|
||||||
else if (room.members) {
|
else if (room.members && !isPublicRoom) { // Do not rename public room
|
||||||
|
|
||||||
var user_id = matrixService.config().user_id;
|
var user_id = matrixService.config().user_id;
|
||||||
|
|
||||||
|
@ -45,35 +52,36 @@ angular.module('matrixFilter', [])
|
||||||
// Limit the room renaming to 1:1 room
|
// Limit the room renaming to 1:1 room
|
||||||
if (2 === Object.keys(room.members).length) {
|
if (2 === Object.keys(room.members).length) {
|
||||||
for (var i in room.members) {
|
for (var i in room.members) {
|
||||||
|
if (!room.members.hasOwnProperty(i)) continue;
|
||||||
|
|
||||||
var member = room.members[i];
|
var member = room.members[i];
|
||||||
if (member.state_key !== user_id) {
|
if (member.state_key !== user_id) {
|
||||||
|
roomName = eventHandlerService.getUserDisplayName(room_id, member.state_key);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Object.keys(room.members).length <= 1) {
|
||||||
|
|
||||||
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) {
|
|
||||||
var otherUserId;
|
var otherUserId;
|
||||||
|
|
||||||
if (Object.keys(room.members)[0] !== user_id) {
|
if (Object.keys(room.members)[0] && Object.keys(room.members)[0] !== user_id) {
|
||||||
otherUserId = Object.keys(room.members)[0];
|
otherUserId = Object.keys(room.members)[0];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
// it's got to be an invite, or failing that a self-chat;
|
||||||
|
otherUserId = room.inviter || user_id;
|
||||||
|
/*
|
||||||
|
// XXX: This should all be unnecessary now thanks to using the /rooms/<room>/roomid API
|
||||||
|
|
||||||
// The other member may be in the invite list, get all invited users
|
// The other member may be in the invite list, get all invited users
|
||||||
var invitedUserIDs = [];
|
var invitedUserIDs = [];
|
||||||
|
|
||||||
|
// XXX: *SURELY* we shouldn't have to trawl through the whole messages list to
|
||||||
|
// find invite - surely the other user should be in room.members with state invited? :/ --Matthew
|
||||||
for (var i in room.messages) {
|
for (var i in room.messages) {
|
||||||
var message = room.messages[i];
|
var message = room.messages[i];
|
||||||
if ("m.room.member" === message.type && "invite" === message.membership) {
|
if ("m.room.member" === message.type && "invite" === message.content.membership) {
|
||||||
// Filter out the current user
|
// Filter out the current user
|
||||||
var member_id = message.state_key;
|
var member_id = message.state_key;
|
||||||
if (member_id === user_id) {
|
if (member_id === user_id) {
|
||||||
|
@ -92,15 +100,11 @@ angular.module('matrixFilter', [])
|
||||||
if (1 === invitedUserIDs.length) {
|
if (1 === invitedUserIDs.length) {
|
||||||
otherUserId = invitedUserIDs[0];
|
otherUserId = invitedUserIDs[0];
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to resolve his displayname in presence global data
|
// Get the user display name
|
||||||
if (otherUserId in $rootScope.presence) {
|
roomName = eventHandlerService.getUserDisplayName(room_id, otherUserId);
|
||||||
roomName = $rootScope.presence[otherUserId].content.displayname;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
roomName = otherUserId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,37 +131,9 @@ angular.module('matrixFilter', [])
|
||||||
};
|
};
|
||||||
}])
|
}])
|
||||||
|
|
||||||
// Compute the user display name in a room according to the data already downloaded
|
// Return the user display name
|
||||||
.filter('mUserDisplayName', ['$rootScope', function($rootScope) {
|
.filter('mUserDisplayName', ['eventHandlerService', function(eventHandlerService) {
|
||||||
return function(user_id, room_id) {
|
return function(user_id, room_id) {
|
||||||
var displayName;
|
return eventHandlerService.getUserDisplayName(room_id, user_id);
|
||||||
|
|
||||||
// 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;
|
|
||||||
};
|
};
|
||||||
}]);
|
}]);
|
||||||
|
|
|
@ -265,6 +265,12 @@ angular.module('matrixService', [])
|
||||||
return doRequest("GET", path, params);
|
return doRequest("GET", path, params);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// get room state for a specific room
|
||||||
|
roomState: function(room_id) {
|
||||||
|
var path = "/rooms/" + room_id + "/state";
|
||||||
|
return doRequest("GET", path);
|
||||||
|
},
|
||||||
|
|
||||||
// Joins a room
|
// Joins a room
|
||||||
join: function(room_id) {
|
join: function(room_id) {
|
||||||
return this.membershipChange(room_id, undefined, "join");
|
return this.membershipChange(room_id, undefined, "join");
|
||||||
|
@ -697,11 +703,10 @@ angular.module('matrixService', [])
|
||||||
createRoomIdToAliasMapping: function(roomId, alias) {
|
createRoomIdToAliasMapping: function(roomId, alias) {
|
||||||
roomIdToAlias[roomId] = alias;
|
roomIdToAlias[roomId] = alias;
|
||||||
aliasToRoomId[alias] = roomId;
|
aliasToRoomId[alias] = roomId;
|
||||||
// localStorage.setItem(MAPPING_PREFIX+roomId, alias);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
getRoomIdToAliasMapping: function(roomId) {
|
getRoomIdToAliasMapping: function(roomId) {
|
||||||
var alias = roomIdToAlias[roomId]; // was localStorage.getItem(MAPPING_PREFIX+roomId)
|
var alias = roomIdToAlias[roomId];
|
||||||
//console.log("looking for alias for " + roomId + "; found: " + alias);
|
//console.log("looking for alias for " + roomId + "; found: " + alias);
|
||||||
return alias;
|
return alias;
|
||||||
},
|
},
|
||||||
|
@ -762,6 +767,10 @@ angular.module('matrixService', [])
|
||||||
var deferred = $q.defer();
|
var deferred = $q.defer();
|
||||||
deferred.reject({data:{error: "Invalid room: " + room_id}});
|
deferred.reject({data:{error: "Invalid room: " + room_id}});
|
||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
|
},
|
||||||
|
|
||||||
|
getTurnServer: function() {
|
||||||
|
return doRequest("GET", "/voip/turnServer");
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -43,6 +43,10 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
|
||||||
avatarUrl: ""
|
avatarUrl: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$scope.newChat = {
|
||||||
|
user: ""
|
||||||
|
};
|
||||||
|
|
||||||
var refresh = function() {
|
var refresh = function() {
|
||||||
|
|
||||||
matrixService.publicRooms().then(
|
matrixService.publicRooms().then(
|
||||||
|
@ -82,18 +86,24 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
|
||||||
|
|
||||||
// Go to a room
|
// Go to a room
|
||||||
$scope.goToRoom = function(room_id) {
|
$scope.goToRoom = function(room_id) {
|
||||||
// Simply open the room page on this room id
|
|
||||||
//$location.url("room/" + room_id);
|
|
||||||
matrixService.join(room_id).then(
|
matrixService.join(room_id).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
|
var final_room_id = room_id;
|
||||||
if (response.data.hasOwnProperty("room_id")) {
|
if (response.data.hasOwnProperty("room_id")) {
|
||||||
if (response.data.room_id != room_id) {
|
final_room_id = response.data.room_id;
|
||||||
$location.url("room/" + response.data.room_id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$location.url("room/" + room_id);
|
// TODO: factor out the common housekeeping whenever we try to join a room or alias
|
||||||
|
matrixService.roomState(final_room_id).then(
|
||||||
|
function(response) {
|
||||||
|
eventHandlerService.handleEvents(response.data, false, true);
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
$scope.feedback = "Failed to get room state for: " + final_room_id;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
$location.url("room/" + final_room_id);
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
$scope.feedback = "Can't join room: " + JSON.stringify(error.data);
|
$scope.feedback = "Can't join room: " + JSON.stringify(error.data);
|
||||||
|
@ -104,6 +114,15 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
|
||||||
$scope.joinAlias = function(room_alias) {
|
$scope.joinAlias = function(room_alias) {
|
||||||
matrixService.joinAlias(room_alias).then(
|
matrixService.joinAlias(room_alias).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
|
// TODO: factor out the common housekeeping whenever we try to join a room or alias
|
||||||
|
matrixService.roomState(response.room_id).then(
|
||||||
|
function(response) {
|
||||||
|
eventHandlerService.handleEvents(response.data, false, true);
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
$scope.feedback = "Failed to get room state for: " + response.room_id;
|
||||||
|
}
|
||||||
|
);
|
||||||
// Go to this room
|
// Go to this room
|
||||||
$location.url("room/" + room_alias);
|
$location.url("room/" + room_alias);
|
||||||
},
|
},
|
||||||
|
@ -113,6 +132,32 @@ angular.module('HomeController', ['matrixService', 'eventHandlerService', 'Recen
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// FIXME: factor this out between user-controller and home-controller etc.
|
||||||
|
$scope.messageUser = function() {
|
||||||
|
|
||||||
|
// FIXME: create a new room every time, for now
|
||||||
|
|
||||||
|
matrixService.create(null, 'private').then(
|
||||||
|
function(response) {
|
||||||
|
// This room has been created. Refresh the rooms list
|
||||||
|
var room_id = response.data.room_id;
|
||||||
|
console.log("Created room with id: "+ room_id);
|
||||||
|
|
||||||
|
matrixService.invite(room_id, $scope.newChat.user).then(
|
||||||
|
function() {
|
||||||
|
$scope.feedback = "Invite sent successfully";
|
||||||
|
$scope.$parent.goToPage("/room/" + room_id);
|
||||||
|
},
|
||||||
|
function(reason) {
|
||||||
|
$scope.feedback = "Failure: " + JSON.stringify(reason);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
$scope.feedback = "Failure: " + JSON.stringify(error.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
$scope.onInit = function() {
|
$scope.onInit = function() {
|
||||||
// Load profile data
|
// Load profile data
|
||||||
// Display name
|
// Display name
|
||||||
|
|
|
@ -52,17 +52,24 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<form>
|
<form>
|
||||||
<input size="40" ng-model="newRoom.room_alias" ng-enter="createNewRoom(newRoom.room_alias, newRoom.private)" placeholder="(e.g. foo_channel)"/>
|
<input size="40" ng-model="newRoom.room_alias" ng-enter="createNewRoom(newRoom.room_alias, newRoom.private)" placeholder="(e.g. foo)"/>
|
||||||
<input type="checkbox" ng-model="newRoom.private">private
|
<input type="checkbox" ng-model="newRoom.private">private
|
||||||
<button ng-disabled="!newRoom.room_alias" ng-click="createNewRoom(newRoom.room_alias, newRoom.private)">Create room</button>
|
<button ng-disabled="!newRoom.room_alias" ng-click="createNewRoom(newRoom.room_alias, newRoom.private)">Create room</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<form>
|
<form>
|
||||||
<input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo_channel:example.org)"/>
|
<input size="40" ng-model="joinAlias.room_alias" ng-enter="joinAlias(joinAlias.room_alias)" placeholder="(e.g. #foo:example.org)"/>
|
||||||
<button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>
|
<button ng-disabled="!joinAlias.room_alias" ng-click="joinAlias(joinAlias.room_alias)">Join room</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<input size="40" ng-model="newChat.user" ng-enter="messageUser()" placeholder="e.g. @user:domain.com"/>
|
||||||
|
<button ng-disabled="!newChat.user" ng-click="messageUser()">Message user</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
{{ feedback }}
|
{{ feedback }}
|
||||||
|
|
BIN
webclient/img/close.png
Normal file
BIN
webclient/img/close.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 397 B |
|
@ -69,7 +69,7 @@
|
||||||
<span ng-show="currentCall.state == 'ringing' && currentCall && currentCall.type == 'voice'">Incoming Voice Call</span>
|
<span ng-show="currentCall.state == 'ringing' && currentCall && currentCall.type == 'voice'">Incoming Voice Call</span>
|
||||||
<span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
|
<span ng-show="currentCall.state == 'connecting'">Call Connecting...</span>
|
||||||
<span ng-show="currentCall.state == 'connected'">Call Connected</span>
|
<span ng-show="currentCall.state == 'connected'">Call Connected</span>
|
||||||
<span ng-show="currentCall.state == 'ended' && currentCall.hangupReason == 'ice_failed'">Media Connection Failed</span>
|
<span ng-show="currentCall.state == 'ended' && currentCall.hangupReason == 'ice_failed'">Media Connection Failed{{ haveTurn ? "" : " (VoIP relaying unsupported by Home Server)" }}</span>
|
||||||
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'remote'">Call Rejected</span>
|
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'remote'">Call Rejected</span>
|
||||||
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">Call Canceled</span>
|
<span ng-show="currentCall.state == 'ended' && !currentCall.hangupReason && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">Call Canceled</span>
|
||||||
<span ng-show="currentCall.state == 'ended' && currentCall.hangupReason == 'invite_timeout' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">User Not Responding</span>
|
<span ng-show="currentCall.state == 'ended' && currentCall.hangupReason == 'invite_timeout' && !currentCall.didConnect && currentCall.direction == 'outbound' && currentCall.hangupParty == 'local'">User Not Responding</span>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<table class="recentsTable">
|
<table class="recentsTable">
|
||||||
<tbody ng-repeat="(index, room) in events.rooms | orderRecents"
|
<tbody ng-repeat="(index, room) in events.rooms | orderRecents"
|
||||||
ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )"
|
ng-click="goToPage('room/' + (room.room_alias ? room.room_alias : room.room_id) )"
|
||||||
class ="recentsRoom"
|
class="recentsRoom"
|
||||||
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
|
ng-class="{'recentsRoomSelected': (room.room_id === recentsSelectedRoomID)}">
|
||||||
<tr>
|
<tr>
|
||||||
<td ng-class="room['m.room.join_rules'].content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
|
<td ng-class="room['m.room.join_rules'].content.join_rule == 'public' ? 'recentsRoomName recentsPublicRoom' : 'recentsRoomName'">
|
||||||
|
@ -19,6 +19,8 @@
|
||||||
{{ lastMsg = eventHandlerService.getLastMessage(room.room_id, true);"" }}
|
{{ lastMsg = eventHandlerService.getLastMessage(room.room_id, true);"" }}
|
||||||
|
|
||||||
{{ (lastMsg.ts) | date:'MMM d HH:mm' }}
|
{{ (lastMsg.ts) | date:'MMM d HH:mm' }}
|
||||||
|
|
||||||
|
<img ng-click="leave(room.room_id); $event.stopPropagation();" src="img/close.png" width="10" height="10" style="margin-bottom: -1px; margin-left: 2px;" alt="close"/>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
|
@ -31,8 +33,10 @@
|
||||||
|
|
||||||
<div ng-hide="room.membership === 'invite'" ng-switch="lastMsg.type">
|
<div ng-hide="room.membership === 'invite'" ng-switch="lastMsg.type">
|
||||||
<div ng-switch-when="m.room.member">
|
<div ng-switch-when="m.room.member">
|
||||||
|
<span ng-switch="lastMsg.changedKey">
|
||||||
|
<span ng-switch-when="membership">
|
||||||
<span ng-if="'join' === lastMsg.content.membership">
|
<span ng-if="'join' === lastMsg.content.membership">
|
||||||
{{ lastMsg.state_key | mUserDisplayName: room.room_id}} joined
|
{{ lastMsg.state_key | mUserDisplayName: room.room_id }} joined
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="'leave' === lastMsg.content.membership">
|
<span ng-if="'leave' === lastMsg.content.membership">
|
||||||
<span ng-if="lastMsg.user_id === lastMsg.state_key">
|
<span ng-if="lastMsg.user_id === lastMsg.state_key">
|
||||||
|
@ -55,6 +59,11 @@
|
||||||
: {{ lastMsg.content.reason }}
|
: {{ lastMsg.content.reason }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
|
<span ng-switch-when="displayname">
|
||||||
|
{{ lastMsg.user_id }} changed their display name from {{ lastMsg.prev_content.displayname }} to {{ lastMsg.content.displayname }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-switch-when="m.room.message">
|
<div ng-switch-when="m.room.message">
|
||||||
|
|
|
@ -400,6 +400,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||||
// Find the max power level
|
// Find the max power level
|
||||||
var maxPowerLevel = 0;
|
var maxPowerLevel = 0;
|
||||||
for (var i in $scope.members) {
|
for (var i in $scope.members) {
|
||||||
|
if (!$scope.members.hasOwnProperty(i)) continue;
|
||||||
|
|
||||||
var member = $scope.members[i];
|
var member = $scope.members[i];
|
||||||
if (member.powerLevel) {
|
if (member.powerLevel) {
|
||||||
maxPowerLevel = Math.max(maxPowerLevel, member.powerLevel);
|
maxPowerLevel = Math.max(maxPowerLevel, member.powerLevel);
|
||||||
|
@ -409,6 +411,8 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||||
// Normalized them on a 0..100% scale to be use in css width
|
// Normalized them on a 0..100% scale to be use in css width
|
||||||
if (maxPowerLevel) {
|
if (maxPowerLevel) {
|
||||||
for (var i in $scope.members) {
|
for (var i in $scope.members) {
|
||||||
|
if (!$scope.members.hasOwnProperty(i)) continue;
|
||||||
|
|
||||||
var member = $scope.members[i];
|
var member = $scope.members[i];
|
||||||
member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel;
|
member.powerLevelNorm = (member.powerLevel * 100) / maxPowerLevel;
|
||||||
}
|
}
|
||||||
|
@ -479,6 +483,15 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||||
else {
|
else {
|
||||||
promise = matrixService.joinAlias(room_alias).then(
|
promise = matrixService.joinAlias(room_alias).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
|
// TODO: factor out the common housekeeping whenever we try to join a room or alias
|
||||||
|
matrixService.roomState(response.room_id).then(
|
||||||
|
function(response) {
|
||||||
|
eventHandlerService.handleEvents(response.data, false, true);
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
$scope.feedback = "Failed to get room state for: " + response.room_id;
|
||||||
|
}
|
||||||
|
);
|
||||||
$location.url("room/" + room_alias);
|
$location.url("room/" + room_alias);
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
|
@ -702,19 +715,24 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||||
// The room members is available in the data fetched by initialSync
|
// The room members is available in the data fetched by initialSync
|
||||||
if ($rootScope.events.rooms[$scope.room_id]) {
|
if ($rootScope.events.rooms[$scope.room_id]) {
|
||||||
|
|
||||||
// There is no need to do a 1st pagination (initialSync provided enough to fill a page)
|
var messages = $rootScope.events.rooms[$scope.room_id].messages;
|
||||||
if ($rootScope.events.rooms[$scope.room_id].messages.length) {
|
|
||||||
$scope.state.first_pagination = false;
|
if (0 === messages.length
|
||||||
|
|| (1 === messages.length && "m.room.member" === messages[0].type && "invite" === messages[0].content.membership && $scope.state.user_id === messages[0].state_key)) {
|
||||||
|
// If we just joined a room, we won't have this history from initial sync, so we should try to paginate it anyway
|
||||||
|
$scope.state.first_pagination = true;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// except if we just joined a room, we won't have this history from initial sync, so we should try to paginate it anyway
|
// There is no need to do a 1st pagination (initialSync provided enough to fill a page)
|
||||||
$scope.state.first_pagination = true;
|
$scope.state.first_pagination = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var members = $rootScope.events.rooms[$scope.room_id].members;
|
var members = $rootScope.events.rooms[$scope.room_id].members;
|
||||||
|
|
||||||
// Update the member list
|
// Update the member list
|
||||||
for (var i in members) {
|
for (var i in members) {
|
||||||
|
if (!members.hasOwnProperty(i)) continue;
|
||||||
|
|
||||||
var member = members[i];
|
var member = members[i];
|
||||||
updateMemberList(member);
|
updateMemberList(member);
|
||||||
}
|
}
|
||||||
|
@ -732,6 +750,16 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||||
$scope.state.waiting_for_joined_event = true;
|
$scope.state.waiting_for_joined_event = true;
|
||||||
matrixService.join($scope.room_id).then(
|
matrixService.join($scope.room_id).then(
|
||||||
function() {
|
function() {
|
||||||
|
// TODO: factor out the common housekeeping whenever we try to join a room or alias
|
||||||
|
matrixService.roomState($scope.room_id).then(
|
||||||
|
function(response) {
|
||||||
|
eventHandlerService.handleEvents(response.data, false, true);
|
||||||
|
},
|
||||||
|
function(error) {
|
||||||
|
console.error("Failed to get room state for: " + $scope.room_id);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// onInit3 will be called once the joined m.room.member event is received from the events stream
|
// onInit3 will be called once the joined m.room.member event is received from the events stream
|
||||||
// This avoids to get the joined information twice in parallel:
|
// This avoids to get the joined information twice in parallel:
|
||||||
// - one from the events stream
|
// - one from the events stream
|
||||||
|
@ -740,6 +768,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||||
},
|
},
|
||||||
function(reason) {
|
function(reason) {
|
||||||
console.log("Can't join room: " + JSON.stringify(reason));
|
console.log("Can't join room: " + JSON.stringify(reason));
|
||||||
|
// FIXME: what if it wasn't a perms problem?
|
||||||
$scope.state.permission_denied = "You do not have permission to join this room";
|
$scope.state.permission_denied = "You do not have permission to join this room";
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -809,7 +838,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
|
||||||
|
|
||||||
matrixService.leave($scope.room_id).then(
|
matrixService.leave($scope.room_id).then(
|
||||||
function(response) {
|
function(response) {
|
||||||
console.log("Left room ");
|
console.log("Left room " + $scope.room_id);
|
||||||
$location.url("home");
|
$location.url("home");
|
||||||
},
|
},
|
||||||
function(error) {
|
function(error) {
|
||||||
|
|
|
@ -21,39 +21,62 @@ angular.module('RoomController')
|
||||||
return function (scope, element, attrs) {
|
return function (scope, element, attrs) {
|
||||||
element.bind("keydown keypress", function (event) {
|
element.bind("keydown keypress", function (event) {
|
||||||
// console.log("event: " + event.which);
|
// console.log("event: " + event.which);
|
||||||
if (event.which === 9) {
|
var TAB = 9;
|
||||||
|
var SHIFT = 16;
|
||||||
|
var keypressCode = event.which;
|
||||||
|
if (keypressCode === TAB) {
|
||||||
if (!scope.tabCompleting) { // cache our starting text
|
if (!scope.tabCompleting) { // cache our starting text
|
||||||
// console.log("caching " + element[0].value);
|
|
||||||
scope.tabCompleteOriginal = element[0].value;
|
scope.tabCompleteOriginal = element[0].value;
|
||||||
scope.tabCompleting = true;
|
scope.tabCompleting = true;
|
||||||
|
scope.tabCompleteIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loop in the right direction
|
||||||
if (event.shiftKey) {
|
if (event.shiftKey) {
|
||||||
scope.tabCompleteIndex--;
|
scope.tabCompleteIndex--;
|
||||||
if (scope.tabCompleteIndex < 0) {
|
if (scope.tabCompleteIndex < 0) {
|
||||||
scope.tabCompleteIndex = 0;
|
// wrap to the last search match, and fix up to a real
|
||||||
|
// index value after we've matched
|
||||||
|
scope.tabCompleteIndex = Number.MAX_VALUE;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
scope.tabCompleteIndex++;
|
scope.tabCompleteIndex++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
var searchIndex = 0;
|
var searchIndex = 0;
|
||||||
var targetIndex = scope.tabCompleteIndex;
|
var targetIndex = scope.tabCompleteIndex;
|
||||||
var text = scope.tabCompleteOriginal;
|
var text = scope.tabCompleteOriginal;
|
||||||
|
|
||||||
// console.log("targetIndex: " + targetIndex + ", text=" + text);
|
// console.log("targetIndex: " + targetIndex + ",
|
||||||
|
// text=" + text);
|
||||||
|
|
||||||
|
// FIXME: use the correct regexp to recognise userIDs --M
|
||||||
|
//
|
||||||
|
// XXX: I don't really know what the point of this is. You
|
||||||
|
// WANT to match freeform text given you want to match display
|
||||||
|
// names AND user IDs. Surely you just want to get the last
|
||||||
|
// word out of the input text and that's that?
|
||||||
|
// Am I missing something here? -- Kegan
|
||||||
|
//
|
||||||
|
// You're not missing anything - my point was that we should
|
||||||
|
// explicitly define the syntax for user IDs /somewhere/.
|
||||||
|
// Meanwhile as long as the delimeters are well defined, we
|
||||||
|
// could just pick "the last word". But to know what the
|
||||||
|
// correct delimeters are, we probably do need a formal
|
||||||
|
// syntax for user IDs to refer to... --Matthew
|
||||||
|
|
||||||
// FIXME: use the correct regexp to recognise userIDs
|
|
||||||
var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text);
|
var search = /@?([a-zA-Z0-9_\-:\.]+)$/.exec(text);
|
||||||
if (targetIndex === 0) {
|
|
||||||
element[0].value = text;
|
|
||||||
|
|
||||||
// Force angular to wake up and update the input ng-model by firing up input event
|
if (targetIndex === 0) { // 0 is always the original 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');
|
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+" from "+text);
|
||||||
var expansion;
|
var expansion;
|
||||||
|
|
||||||
// FIXME: could do better than linear search here
|
// FIXME: could do better than linear search here
|
||||||
|
@ -68,6 +91,7 @@ angular.module('RoomController')
|
||||||
if (searchIndex < targetIndex) { // then search raw mxids
|
if (searchIndex < targetIndex) { // then search raw mxids
|
||||||
angular.forEach(scope.members, function(item, name) {
|
angular.forEach(scope.members, function(item, name) {
|
||||||
if (searchIndex < targetIndex) {
|
if (searchIndex < targetIndex) {
|
||||||
|
// === 1 because mxids are @username
|
||||||
if (name.toLowerCase().indexOf(search[1].toLowerCase()) === 1) {
|
if (name.toLowerCase().indexOf(search[1].toLowerCase()) === 1) {
|
||||||
expansion = name;
|
expansion = name;
|
||||||
searchIndex++;
|
searchIndex++;
|
||||||
|
@ -76,18 +100,22 @@ angular.module('RoomController')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchIndex === targetIndex) {
|
if (searchIndex === targetIndex ||
|
||||||
// xchat-style tab complete
|
targetIndex === Number.MAX_VALUE) {
|
||||||
|
// xchat-style tab complete, add a colon if tab
|
||||||
|
// completing at the start of the text
|
||||||
if (search[0].length === text.length)
|
if (search[0].length === text.length)
|
||||||
expansion += " : ";
|
expansion += ": ";
|
||||||
else
|
else
|
||||||
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 = "";
|
||||||
|
if (targetIndex === Number.MAX_VALUE) {
|
||||||
// Force angular to wake up and update the input ng-model by firing up input event
|
// wrap the index around to the last index found
|
||||||
angular.element(element[0]).triggerHandler('input');
|
scope.tabCompleteIndex = searchIndex;
|
||||||
|
targetIndex = searchIndex;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// console.log("wrapped!");
|
// console.log("wrapped!");
|
||||||
|
@ -97,23 +125,40 @@ 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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Force angular to wak up and update the input ng-model by
|
||||||
|
// firing up input event
|
||||||
|
angular.element(element[0]).triggerHandler('input');
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
scope.tabCompleteIndex = 0;
|
scope.tabCompleteIndex = 0;
|
||||||
}
|
}
|
||||||
|
// prevent the default TAB operation (typically focus shifting)
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
else if (event.which !== 16 && scope.tabCompleting) {
|
else if (keypressCode !== SHIFT && scope.tabCompleting) {
|
||||||
scope.tabCompleting = false;
|
scope.tabCompleting = false;
|
||||||
scope.tabCompleteIndex = 0;
|
scope.tabCompleteIndex = 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}])
|
}])
|
||||||
|
.directive('commandHistory', [ function() {
|
||||||
|
return function (scope, element, attrs) {
|
||||||
|
element.bind("keydown", function (event) {
|
||||||
|
var keycodePressed = event.which;
|
||||||
|
var UP_ARROW = 38;
|
||||||
|
var DOWN_ARROW = 40;
|
||||||
|
if (keycodePressed === UP_ARROW) {
|
||||||
|
scope.history.goUp(event);
|
||||||
|
}
|
||||||
|
else if (keycodePressed === DOWN_ARROW) {
|
||||||
|
scope.history.goDown(event);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}])
|
||||||
|
|
||||||
// A directive to anchor the scroller position at the bottom when the browser is resizing.
|
// A directive to anchor the scroller position at the bottom when the browser is resizing.
|
||||||
// When the screen resizes, the bottom of the element remains the same, not the top.
|
// When the screen resizes, the bottom of the element remains the same, not the top.
|
||||||
|
|
|
@ -48,7 +48,15 @@
|
||||||
width="80" height="80"/>
|
width="80" height="80"/>
|
||||||
<img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/>
|
<img class="userAvatarGradient" src="img/gradient.png" title="{{ member.id }}" width="80" height="24"/>
|
||||||
<div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div>
|
<div class="userPowerLevel" ng-style="{'width': member.powerLevelNorm +'%'}"></div>
|
||||||
<div class="userName">{{ member.displayname || member.id.substr(0, member.id.indexOf(':')) }}<br/>{{ member.displayname ? "" : member.id.substr(member.id.indexOf(':')) }}</div>
|
<div class="userName">
|
||||||
|
<div ng-show="member.displayname">
|
||||||
|
{{ member.id | mUserDisplayName: room_id }}
|
||||||
|
</div>
|
||||||
|
<div ng-hide="member.displayname">
|
||||||
|
{{ member.id.substr(0, member.id.indexOf(':')) }}<br/>
|
||||||
|
{{ member.id.substr(member.id.indexOf(':')) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
|
<td class="userPresence" ng-class="(member.presence === 'online' ? 'online' : (member.presence === 'unavailable' ? 'unavailable' : '')) + ' ' + (member.membership == 'invite' ? 'invited' : '')">
|
||||||
<span ng-show="member.last_active_ago">{{ member.last_active_ago + (now - member.last_updated) | duration }}<br/>ago</span>
|
<span ng-show="member.last_active_ago">{{ member.last_active_ago + (now - member.last_updated) | duration }}<br/>ago</span>
|
||||||
|
@ -65,7 +73,7 @@
|
||||||
<tr ng-repeat="msg in events.rooms[room_id].messages"
|
<tr ng-repeat="msg in events.rooms[room_id].messages"
|
||||||
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"> {{ msg.user_id | mUserDisplayName: room_id }}</div>
|
||||||
<div class="timestamp"
|
<div class="timestamp"
|
||||||
ng-class="msg.echo_msg_state">
|
ng-class="msg.echo_msg_state">
|
||||||
{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}
|
{{ (msg.content.hsob_ts || msg.ts) | date:'MMM d HH:mm' }}
|
||||||
|
@ -77,10 +85,10 @@
|
||||||
</td>
|
</td>
|
||||||
<td ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
|
<td ng-class="(!msg.content.membership && ('m.room.topic' !== msg.type && 'm.room.name' !== msg.type))? (msg.content.msgtype === 'm.emote' ? 'emote text' : 'text') : 'membership text'">
|
||||||
<div class="bubble">
|
<div class="bubble">
|
||||||
<span ng-if="'join' === msg.content.membership">
|
<span ng-if="'join' === msg.content.membership && msg.changedKey === 'membership'">
|
||||||
{{ members[msg.state_key].displayname || msg.state_key }} joined
|
{{ members[msg.state_key].displayname || msg.state_key }} joined
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="'leave' === msg.content.membership">
|
<span ng-if="'leave' === msg.content.membership && msg.changedKey === 'membership'">
|
||||||
<span ng-if="msg.user_id === msg.state_key">
|
<span ng-if="msg.user_id === msg.state_key">
|
||||||
{{ members[msg.state_key].displayname || msg.state_key }} left
|
{{ members[msg.state_key].displayname || msg.state_key }} left
|
||||||
</span>
|
</span>
|
||||||
|
@ -93,7 +101,8 @@
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span ng-if="'invite' === msg.content.membership || 'ban' === msg.content.membership">
|
<span ng-if="'invite' === msg.content.membership && msg.changedKey === 'membership' ||
|
||||||
|
'ban' === msg.content.membership && msg.changedKey === '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 }}
|
||||||
|
@ -101,6 +110,9 @@
|
||||||
: {{ msg.content.reason }}
|
: {{ msg.content.reason }}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
<span ng-if="msg.changedKey === 'displayname'">
|
||||||
|
{{ msg.user_id }} changed their display name from {{ msg.prev_content.displayname }} to {{ msg.content.displayname }}
|
||||||
|
</span>
|
||||||
|
|
||||||
<span ng-show='msg.content.msgtype === "m.emote"'
|
<span ng-show='msg.content.msgtype === "m.emote"'
|
||||||
ng-class="msg.echo_msg_state"
|
ng-class="msg.echo_msg_state"
|
||||||
|
@ -159,8 +171,7 @@
|
||||||
<td width="*">
|
<td width="*">
|
||||||
<textarea id="mainInput" rows="1" ng-enter="send()"
|
<textarea id="mainInput" rows="1" ng-enter="send()"
|
||||||
ng-disabled="state.permission_denied"
|
ng-disabled="state.permission_denied"
|
||||||
ng-keydown="(38 === $event.which) ? history.goUp($event) : ((40 === $event.which) ? history.goDown($event) : 0)"
|
ng-focus="true" autocomplete="off" tab-complete command-history/>
|
||||||
ng-focus="true" autocomplete="off" tab-complete/>
|
|
||||||
</td>
|
</td>
|
||||||
<td id="buttonsCell">
|
<td id="buttonsCell">
|
||||||
<button ng-click="send()" ng-disabled="state.permission_denied">Send</button>
|
<button ng-click="send()" ng-disabled="state.permission_denied">Send</button>
|
||||||
|
|
|
@ -39,6 +39,7 @@ angular.module('UserController', ['matrixService'])
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// FIXME: factor this out between user-controller and home-controller etc.
|
||||||
$scope.messageUser = function() {
|
$scope.messageUser = function() {
|
||||||
|
|
||||||
// FIXME: create a new room every time, for now
|
// FIXME: create a new room every time, for now
|
||||||
|
|
Loading…
Reference in a new issue