diff --git a/CHANGES.rst b/CHANGES.rst index b9b3e9d0e..d3beea3ed 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,26 @@ +Changes in synapse 0.2.3 (2014-09-12) +===================================== + +Homeserver: + * Fix bug where we stopped sending events to remote home servers if a + user from that home server left, even if there were some still in the + room. + * Fix bugs in the state conflict resolution where it was incorrectly + rejecting events. + +Webclient: + * Display room names and topics. + * Allow setting/editing of room names and topics. + * Display information about rooms on the main page. + * Handle ban and kick events in real time. + * VoIP UI and reliability improvements. + * Add glare support for VoIP. + * Improvements to initial startup speed. + * Don't display duplicate join events. + * Local echo of messages. + * Differentiate sending and sent of local echo. + * Various minor bug fixes. + Changes in synapse 0.2.2 (2014-09-06) ===================================== diff --git a/VERSION b/VERSION index ee1372d33..717903969 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.2 +0.2.3 diff --git a/WISHLIST.rst b/WISHLIST.rst index 68324ad9d..a0713f196 100644 --- a/WISHLIST.rst +++ b/WISHLIST.rst @@ -5,3 +5,5 @@ Broad-sweeping stuff which would be nice to have - homeserver implementation in go - homeserver implementation in node.js - client SDKs + - libpurple library + - irssi plugin? diff --git a/docs/specification.rst b/docs/specification.rst index aad200c04..ab16a0bb6 100644 --- a/docs/specification.rst +++ b/docs/specification.rst @@ -1182,16 +1182,16 @@ This event is sent by the caller when they wish to establish a call. - ``type`` : "string" - The type of session description, in this case 'offer' - ``sdp`` : "string" - The SDP text of the session description -``m.call.candidate`` +``m.call.candidates`` This event is sent by callers after sending an invite and by the callee after answering. -Its purpose is to give the other party an additional ICE candidate to try using to +Its purpose is to give the other party additional ICE candidates to try using to communicate. Required keys: - ``call_id`` : "string" - The ID of the call this event relates to - ``version`` : "integer" - The version of the VoIP specification this messages adheres to. his specification is version 0. - - ``candidate`` : "candidate object" - Object describing the candidate. + - ``candidates`` : "array of candidate objects" - Array of object describing the candidates. ``Candidate Object`` diff --git a/synapse/__init__.py b/synapse/__init__.py index 1ed9cdcdf..d60267ebe 100644 --- a/synapse/__init__.py +++ b/synapse/__init__.py @@ -16,4 +16,4 @@ """ This is a reference implementation of a synapse home server. """ -__version__ = "0.2.2" +__version__ = "0.2.3" diff --git a/synapse/api/events/__init__.py b/synapse/api/events/__init__.py index 5f300de10..72c493db5 100644 --- a/synapse/api/events/__init__.py +++ b/synapse/api/events/__init__.py @@ -17,6 +17,18 @@ from synapse.api.errors import SynapseError, Codes from synapse.util.jsonobject import JsonEncodedObject +def serialize_event(hs, e): + # FIXME(erikj): To handle the case of presence events and the like + if not isinstance(e, SynapseEvent): + return e + + d = e.get_dict() + if "age_ts" in d: + d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"] + + return d + + class SynapseEvent(JsonEncodedObject): """Base class for Synapse events. These are JSON objects which must abide @@ -43,6 +55,7 @@ class SynapseEvent(JsonEncodedObject): "content", # HTTP body, JSON "state_key", "required_power_level", + "age_ts", ] internal_keys = [ diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py index 5e38cdbc4..d3d96d73e 100644 --- a/synapse/api/events/factory.py +++ b/synapse/api/events/factory.py @@ -59,6 +59,14 @@ class EventFactory(object): if "ts" not in kwargs: kwargs["ts"] = int(self.clock.time_msec()) + # The "age" key is a delta timestamp that should be converted into an + # absolute timestamp the minute we see it. + if "age" in kwargs: + kwargs["age_ts"] = int(self.clock.time_msec()) - int(kwargs["age"]) + del kwargs["age"] + elif "age_ts" not in kwargs: + kwargs["age_ts"] = int(self.clock.time_msec()) + if etype in self._event_list: handler = self._event_list[etype] else: diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py index e12510017..c79ce4468 100644 --- a/synapse/federation/replication.py +++ b/synapse/federation/replication.py @@ -291,6 +291,12 @@ class ReplicationLayer(object): def on_incoming_transaction(self, transaction_data): transaction = Transaction(**transaction_data) + for p in transaction.pdus: + if "age" in p: + p["age_ts"] = int(self.clock.time_msec()) - int(p["age"]) + + pdu_list = [Pdu(**p) for p in transaction.pdus] + logger.debug("[%s] Got transaction", transaction.transaction_id) response = yield self.transaction_actions.have_responded(transaction) @@ -303,8 +309,6 @@ class ReplicationLayer(object): logger.debug("[%s] Transacition is new", transaction.transaction_id) - pdu_list = [Pdu(**p) for p in transaction.pdus] - dl = [] for pdu in pdu_list: dl.append(self._handle_new_pdu(pdu)) @@ -405,9 +409,14 @@ class ReplicationLayer(object): """Returns a new Transaction containing the given PDUs suitable for transmission. """ + pdus = [p.get_dict() for p in pdu_list] + for p in pdus: + if "age_ts" in pdus: + p["age"] = int(self.clock.time_msec()) - p["age_ts"] + return Transaction( - pdus=[p.get_dict() for p in pdu_list], origin=self.server_name, + pdus=pdus, ts=int(self._clock.time_msec()), destination=None, ) diff --git a/synapse/handlers/events.py b/synapse/handlers/events.py index fd24a11fb..93dcd4032 100644 --- a/synapse/handlers/events.py +++ b/synapse/handlers/events.py @@ -15,7 +15,6 @@ from twisted.internet import defer -from synapse.api.events import SynapseEvent from synapse.util.logutils import log_function from ._base import BaseHandler @@ -71,10 +70,7 @@ class EventStreamHandler(BaseHandler): auth_user, room_ids, pagin_config, timeout ) - chunks = [ - e.get_dict() if isinstance(e, SynapseEvent) else e - for e in events - ] + chunks = [self.hs.serialize_event(e) for e in events] chunk = { "chunk": chunks, @@ -92,7 +88,9 @@ class EventStreamHandler(BaseHandler): # 10 seconds of grace to allow the client to reconnect again # before we think they're gone def _later(): - logger.debug("_later stopped_user_eventstream %s", auth_user) + logger.debug( + "_later stopped_user_eventstream %s", auth_user + ) self.distributor.fire( "stopped_user_eventstream", auth_user ) diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py index 87fc04478..b63863e5b 100644 --- a/synapse/handlers/message.py +++ b/synapse/handlers/message.py @@ -124,7 +124,7 @@ class MessageHandler(BaseHandler): ) chunk = { - "chunk": [e.get_dict() for e in events], + "chunk": [self.hs.serialize_event(e) for e in events], "start": pagin_config.from_token.to_string(), "end": next_token.to_string(), } @@ -296,7 +296,7 @@ class MessageHandler(BaseHandler): end_token = now_token.copy_and_replace("room_key", token[1]) d["messages"] = { - "chunk": [m.get_dict() for m in messages], + "chunk": [self.hs.serialize_event(m) for m in messages], "start": start_token.to_string(), "end": end_token.to_string(), } @@ -304,7 +304,7 @@ class MessageHandler(BaseHandler): current_state = yield self.store.get_current_state( event.room_id ) - d["state"] = [c.get_dict() for c in current_state] + d["state"] = [self.hs.serialize_event(c) for c in current_state] except: logger.exception("Failed to get snapshot") diff --git a/synapse/handlers/room.py b/synapse/handlers/room.py index 310cb46fe..5bc128043 100644 --- a/synapse/handlers/room.py +++ b/synapse/handlers/room.py @@ -335,7 +335,7 @@ class RoomMemberHandler(BaseHandler): member_list = yield self.store.get_room_members(room_id=room_id) event_list = [ - entry.get_dict() + self.hs.serialize_event(entry) for entry in member_list ] chunk_data = { diff --git a/synapse/rest/events.py b/synapse/rest/events.py index 7fde14320..097195d7c 100644 --- a/synapse/rest/events.py +++ b/synapse/rest/events.py @@ -59,7 +59,7 @@ class EventRestServlet(RestServlet): event = yield handler.get_event(auth_user, event_id) if event: - defer.returnValue((200, event.get_dict())) + defer.returnValue((200, self.hs.serialize_event(event))) else: defer.returnValue((404, "Event not found.")) diff --git a/synapse/rest/room.py b/synapse/rest/room.py index cef700c81..ecb1e346d 100644 --- a/synapse/rest/room.py +++ b/synapse/rest/room.py @@ -378,7 +378,7 @@ class RoomTriggerBackfill(RestServlet): handler = self.handlers.federation_handler events = yield handler.backfill(remote_server, room_id, limit) - res = [event.get_dict() for event in events] + res = [self.hs.serialize_event(event) for event in events] defer.returnValue((200, res)) diff --git a/synapse/server.py b/synapse/server.py index 1ba13f3df..cdea49e6a 100644 --- a/synapse/server.py +++ b/synapse/server.py @@ -20,6 +20,7 @@ # Imports required for the default HomeServer() implementation from synapse.federation import initialize_http_replication +from synapse.api.events import serialize_event from synapse.api.events.factory import EventFactory from synapse.notifier import Notifier from synapse.api.auth import Auth @@ -139,6 +140,9 @@ class BaseHomeServer(object): object.""" return RoomID.from_string(s, hs=self) + def serialize_event(self, e): + return serialize_event(self, e) + # Build magic accessors for every dependency for depname in BaseHomeServer.DEPENDENCIES: BaseHomeServer._make_dependency_method(depname) diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py index 0dbae504b..88c9a5b88 100644 --- a/synapse/storage/__init__.py +++ b/synapse/storage/__init__.py @@ -36,7 +36,7 @@ from .registration import RegistrationStore from .room import RoomStore from .roommember import RoomMemberStore from .stream import StreamStore -from .pdu import StatePduStore, PduStore +from .pdu import StatePduStore, PduStore, PdusTable from .transactions import TransactionStore from .keys import KeyStore @@ -140,6 +140,12 @@ class DataStore(RoomMemberStore, RoomStore, del cols["content"] del cols["prev_pdus"] cols["content_json"] = json.dumps(pdu.content) + + unrec_keys.update({ + k: v for k, v in cols.items() + if k not in PdusTable.fields + }) + cols["unrecognized_keys"] = json.dumps(unrec_keys) logger.debug("Persisting: %s", repr(cols)) diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py index efec11169..76ed7d06f 100644 --- a/synapse/storage/_base.py +++ b/synapse/storage/_base.py @@ -355,6 +355,10 @@ class SQLBaseStore(object): d["content"] = json.loads(d["content"]) del d["unrecognized_keys"] + if "age_ts" not in d: + # For compatibility + d["age_ts"] = d["ts"] if "ts" in d else 0 + return self.event_factory.create_event( etype=d["type"], **d diff --git a/synapse/storage/roommember.py b/synapse/storage/roommember.py index c98022410..04b4067d0 100644 --- a/synapse/storage/roommember.py +++ b/synapse/storage/roommember.py @@ -66,8 +66,8 @@ class RoomMemberStore(SQLBaseStore): # Check if this was the last person to have left. member_events = self._get_members_query_txn( txn, - where_clause="c.room_id = ? AND m.membership = ?", - where_values=(event.room_id, Membership.JOIN,) + where_clause="c.room_id = ? AND m.membership = ? AND m.user_id != ?", + where_values=(event.room_id, Membership.JOIN, target_user_id,) ) joined_domains = set() diff --git a/tests/api/test_ratelimiting.py b/tests/api/test_ratelimiting.py index dc2f83c7e..dd0bc19ec 100644 --- a/tests/api/test_ratelimiting.py +++ b/tests/api/test_ratelimiting.py @@ -1,6 +1,6 @@ from synapse.api.ratelimiting import Ratelimiter -import unittest +from tests import unittest class TestRatelimiter(unittest.TestCase): diff --git a/tests/events/test_events.py b/tests/events/test_events.py index 93d5c15c6..a4b6cb3af 100644 --- a/tests/events/test_events.py +++ b/tests/events/test_events.py @@ -15,7 +15,7 @@ from synapse.api.events import SynapseEvent -import unittest +from tests import unittest class SynapseTemplateCheckTestCase(unittest.TestCase): diff --git a/tests/federation/test_federation.py b/tests/federation/test_federation.py index 0b105fe72..954ccac2a 100644 --- a/tests/federation/test_federation.py +++ b/tests/federation/test_federation.py @@ -14,11 +14,10 @@ # trial imports from twisted.internet import defer -from twisted.trial import unittest +from tests import unittest # python imports from mock import Mock -import logging from ..utils import MockHttpResource, MockClock @@ -28,9 +27,6 @@ from synapse.federation.units import Pdu from synapse.storage.pdu import PduTuple, PduEntry -logging.getLogger().addHandler(logging.NullHandler()) - - def make_pdu(prev_pdus=[], **kwargs): """Provide some default fields for making a PduTuple.""" pdu_fields = { diff --git a/tests/federation/test_pdu_codec.py b/tests/federation/test_pdu_codec.py index 9f74ba119..344e1baf6 100644 --- a/tests/federation/test_pdu_codec.py +++ b/tests/federation/test_pdu_codec.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -from twisted.trial import unittest +from tests import unittest from synapse.federation.pdu_codec import ( PduCodec, encode_event_id, decode_event_id diff --git a/tests/handlers/test_directory.py b/tests/handlers/test_directory.py index f7eb7b4f0..dd5d85dde 100644 --- a/tests/handlers/test_directory.py +++ b/tests/handlers/test_directory.py @@ -14,11 +14,10 @@ # limitations under the License. -from twisted.trial import unittest +from tests import unittest from twisted.internet import defer from mock import Mock -import logging from synapse.server import HomeServer from synapse.http.client import HttpClient @@ -28,9 +27,6 @@ from synapse.storage.directory import RoomAliasMapping from tests.utils import SQLiteMemoryDbPool -logging.getLogger().addHandler(logging.NullHandler()) - - class DirectoryHandlers(object): def __init__(self, hs): self.directory_handler = DirectoryHandler(hs) diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py index 6fc3d8f7f..f0308a29d 100644 --- a/tests/handlers/test_federation.py +++ b/tests/handlers/test_federation.py @@ -14,7 +14,7 @@ from twisted.internet import defer -from twisted.trial import unittest +from tests import unittest from synapse.api.events.room import ( InviteJoinEvent, MessageEvent, RoomMemberEvent @@ -26,12 +26,8 @@ from synapse.federation.units import Pdu from mock import NonCallableMock, ANY -import logging - from ..utils import get_mock_call_args -logging.getLogger().addHandler(logging.NullHandler()) - class FederationTestCase(unittest.TestCase): diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py index d57ee563c..aa52a15b6 100644 --- a/tests/handlers/test_presence.py +++ b/tests/handlers/test_presence.py @@ -14,11 +14,10 @@ # limitations under the License. -from twisted.trial import unittest +from tests import unittest from twisted.internet import defer, reactor from mock import Mock, call, ANY -import logging import json from tests.utils import ( @@ -36,9 +35,6 @@ UNAVAILABLE = PresenceState.UNAVAILABLE ONLINE = PresenceState.ONLINE -logging.getLogger().addHandler(logging.NullHandler()) - - def _expect_edu(destination, edu_type, content, origin="test"): return { "origin": origin, @@ -85,7 +81,6 @@ class PresenceStateTestCase(unittest.TestCase): # Mock the RoomMemberHandler room_member_handler = Mock(spec=[]) hs.handlers.room_member_handler = room_member_handler - logging.getLogger().debug("Mocking room_member_handler=%r", room_member_handler) # Some local users to test with self.u_apple = hs.parse_userid("@apple:test") diff --git a/tests/handlers/test_presencelike.py b/tests/handlers/test_presencelike.py index b35980d94..72c55b366 100644 --- a/tests/handlers/test_presencelike.py +++ b/tests/handlers/test_presencelike.py @@ -16,11 +16,10 @@ """This file contains tests of the "presence-like" data that is shared between presence and profiles; namely, the displayname and avatar_url.""" -from twisted.trial import unittest +from tests import unittest from twisted.internet import defer from mock import Mock, call, ANY -import logging from ..utils import MockClock @@ -35,9 +34,6 @@ UNAVAILABLE = PresenceState.UNAVAILABLE ONLINE = PresenceState.ONLINE -logging.getLogger().addHandler(logging.NullHandler()) - - class MockReplication(object): def __init__(self): self.edu_handlers = {} diff --git a/tests/handlers/test_profile.py b/tests/handlers/test_profile.py index 63c929594..f95ddd701 100644 --- a/tests/handlers/test_profile.py +++ b/tests/handlers/test_profile.py @@ -14,11 +14,10 @@ # limitations under the License. -from twisted.trial import unittest +from tests import unittest from twisted.internet import defer from mock import Mock -import logging from synapse.api.errors import AuthError from synapse.server import HomeServer @@ -27,9 +26,6 @@ from synapse.handlers.profile import ProfileHandler from tests.utils import SQLiteMemoryDbPool -logging.getLogger().addHandler(logging.NullHandler()) - - class ProfileHandlers(object): def __init__(self, hs): self.profile_handler = ProfileHandler(hs) diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py index 5687bbea0..a1a2e8049 100644 --- a/tests/handlers/test_room.py +++ b/tests/handlers/test_room.py @@ -15,7 +15,7 @@ from twisted.internet import defer -from twisted.trial import unittest +from tests import unittest from synapse.api.events.room import ( InviteJoinEvent, RoomMemberEvent, RoomConfigEvent @@ -27,10 +27,6 @@ from synapse.server import HomeServer from mock import Mock, NonCallableMock -import logging - -logging.getLogger().addHandler(logging.NullHandler()) - class RoomMemberHandlerTestCase(unittest.TestCase): diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py index 6532ac94a..ab908cdfc 100644 --- a/tests/handlers/test_typing.py +++ b/tests/handlers/test_typing.py @@ -14,12 +14,11 @@ # limitations under the License. -from twisted.trial import unittest +from tests import unittest from twisted.internet import defer from mock import Mock, call, ANY import json -import logging from ..utils import MockHttpResource, MockClock, DeferredMockCallable @@ -27,9 +26,6 @@ from synapse.server import HomeServer from synapse.handlers.typing import TypingNotificationHandler -logging.getLogger().addHandler(logging.NullHandler()) - - def _expect_edu(destination, edu_type, content, origin="test"): return { "origin": origin, diff --git a/tests/rest/test_events.py b/tests/rest/test_events.py index fd2224f55..79b371c04 100644 --- a/tests/rest/test_events.py +++ b/tests/rest/test_events.py @@ -14,7 +14,7 @@ # limitations under the License. """ Tests REST events for /events paths.""" -from twisted.trial import unittest +from tests import unittest # twisted imports from twisted.internet import defer @@ -27,14 +27,12 @@ from synapse.server import HomeServer # python imports import json -import logging from ..utils import MockHttpResource, MemoryDataStore from .utils import RestTestCase from mock import Mock, NonCallableMock -logging.getLogger().addHandler(logging.NullHandler()) PATH_PREFIX = "/_matrix/client/api/v1" diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py index a1db0fbcf..ea3478ac5 100644 --- a/tests/rest/test_presence.py +++ b/tests/rest/test_presence.py @@ -15,11 +15,10 @@ """Tests REST events for /presence paths.""" -from twisted.trial import unittest +from tests import unittest from twisted.internet import defer from mock import Mock -import logging from ..utils import MockHttpResource @@ -28,9 +27,6 @@ from synapse.handlers.presence import PresenceHandler from synapse.server import HomeServer -logging.getLogger().addHandler(logging.NullHandler()) - - OFFLINE = PresenceState.OFFLINE UNAVAILABLE = PresenceState.UNAVAILABLE ONLINE = PresenceState.ONLINE diff --git a/tests/rest/test_profile.py b/tests/rest/test_profile.py index f41810df1..e6e51f6dd 100644 --- a/tests/rest/test_profile.py +++ b/tests/rest/test_profile.py @@ -15,7 +15,7 @@ """Tests REST events for /profile paths.""" -from twisted.trial import unittest +from tests import unittest from twisted.internet import defer from mock import Mock @@ -28,6 +28,7 @@ from synapse.server import HomeServer myid = "@1234ABCD:test" PATH_PREFIX = "/_matrix/client/api/v1" + class ProfileTestCase(unittest.TestCase): """ Tests profile management. """ diff --git a/tests/rest/utils.py b/tests/rest/utils.py index 77f5ecf0d..ce2e8fd98 100644 --- a/tests/rest/utils.py +++ b/tests/rest/utils.py @@ -17,7 +17,7 @@ from twisted.internet import defer # trial imports -from twisted.trial import unittest +from tests import unittest from synapse.api.constants import Membership diff --git a/tests/storage/test_base.py b/tests/storage/test_base.py index 330311448..3ad9a4b0c 100644 --- a/tests/storage/test_base.py +++ b/tests/storage/test_base.py @@ -14,7 +14,7 @@ # limitations under the License. -from twisted.trial import unittest +from tests import unittest from twisted.internet import defer from mock import Mock, call diff --git a/tests/test_distributor.py b/tests/test_distributor.py index 04933f0ec..39c5b8dff 100644 --- a/tests/test_distributor.py +++ b/tests/test_distributor.py @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from tests import unittest from twisted.internet import defer -from twisted.trial import unittest from mock import Mock, patch diff --git a/tests/test_state.py b/tests/test_state.py index 16af95b7b..b1624f0b2 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -13,8 +13,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from tests import unittest from twisted.internet import defer -from twisted.trial import unittest from twisted.python.log import PythonLoggingObserver from synapse.state import StateHandler @@ -26,7 +26,6 @@ from collections import namedtuple from mock import Mock -import logging import mock diff --git a/tests/test_types.py b/tests/test_types.py index 571938356..276ecc91f 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -13,7 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest +from tests import unittest from synapse.server import BaseHomeServer from synapse.types import UserID, RoomAlias diff --git a/tests/unittest.py b/tests/unittest.py new file mode 100644 index 000000000..fb97fb114 --- /dev/null +++ b/tests/unittest.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# Copyright 2014 OpenMarket Ltd +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from twisted.trial import unittest + +import logging + + +# logging doesn't have a "don't log anything at all EVARRRR setting, +# but since the highest value is 50, 1000000 should do ;) +NEVER = 1000000 + +logging.getLogger().addHandler(logging.StreamHandler()) +logging.getLogger().setLevel(NEVER) + + +def around(target): + """A CLOS-style 'around' modifier, which wraps the original method of the + given instance with another piece of code. + + @around(self) + def method_name(orig, *args, **kwargs): + return orig(*args, **kwargs) + """ + def _around(code): + name = code.__name__ + orig = getattr(target, name) + def new(*args, **kwargs): + return code(orig, *args, **kwargs) + setattr(target, name, new) + return _around + + +class TestCase(unittest.TestCase): + """A subclass of twisted.trial's TestCase which looks for 'loglevel' + attributes on both itself and its individual test methods, to override the + root logger's logging level while that test (case|method) runs.""" + + def __init__(self, methodName, *args, **kwargs): + super(TestCase, self).__init__(methodName, *args, **kwargs) + + method = getattr(self, methodName) + + level = getattr(method, "loglevel", + getattr(self, "loglevel", + NEVER)) + + @around(self) + def setUp(orig): + old_level = logging.getLogger().level + + if old_level != level: + @around(self) + def tearDown(orig): + ret = orig() + logging.getLogger().setLevel(old_level) + return ret + + logging.getLogger().setLevel(level) + return orig() + + +def DEBUG(target): + """A decorator to set the .loglevel attribute to logging.DEBUG. + Can apply to either a TestCase or an individual test method.""" + target.loglevel = logging.DEBUG + return target diff --git a/tests/util/test_lock.py b/tests/util/test_lock.py index 5623d7842..6a1e521b1 100644 --- a/tests/util/test_lock.py +++ b/tests/util/test_lock.py @@ -15,7 +15,7 @@ from twisted.internet import defer -from twisted.trial import unittest +from tests import unittest from synapse.util.lockutils import LockManager @@ -105,4 +105,4 @@ class LockManagerTestCase(unittest.TestCase): pass with (yield self.lock_manager.lock(key)): - pass \ No newline at end of file + pass diff --git a/webclient/app.css b/webclient/app.css index a277bd2a5..4a4ba7b8f 100755 --- a/webclient/app.css +++ b/webclient/app.css @@ -528,8 +528,8 @@ a:active { color: #000; } } .bubble .message { - /* Break lines when encountering CR+LF */ - white-space: pre; + /* Wrap words and break lines on CR+LF */ + white-space: pre-wrap; } .bubble .messagePending { opacity: 0.3 diff --git a/webclient/components/matrix/event-handler-service.js b/webclient/components/matrix/event-handler-service.js index 705a5a07f..4604ff619 100644 --- a/webclient/components/matrix/event-handler-service.js +++ b/webclient/components/matrix/event-handler-service.js @@ -38,6 +38,13 @@ angular.module('eventHandlerService', []) var TOPIC_EVENT = "TOPIC_EVENT"; var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted + // used for dedupping events - could be expanded in future... + // FIXME: means that we leak memory over time (along with lots of the rest + // of the app, given we never try to reap memory yet) + var eventMap = {}; + + $rootScope.presence = {}; + var initialSyncDeferred; var reset = function() { @@ -46,16 +53,13 @@ angular.module('eventHandlerService', []) $rootScope.events = { rooms: {} // will contain roomId: { messages:[], members:{userid1: event} } }; - } + + $rootScope.presence = {}; + + eventMap = {}; + }; reset(); - // used for dedupping events - could be expanded in future... - // FIXME: means that we leak memory over time (along with lots of the rest - // of the app, given we never try to reap memory yet) - var eventMap = {}; - - $rootScope.presence = {}; - var initRoom = function(room_id) { if (!(room_id in $rootScope.events.rooms)) { console.log("Creating new handler entry for " + room_id); @@ -204,7 +208,7 @@ angular.module('eventHandlerService', []) var handleCallEvent = function(event, isLiveEvent) { $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent); - if (event.type == 'm.call.invite') { + if (event.type === 'm.call.invite') { $rootScope.events.rooms[event.room_id].messages.push(event); } }; @@ -231,7 +235,7 @@ angular.module('eventHandlerService', []) } } return index; - } + }; return { ROOM_CREATE_EVENT: ROOM_CREATE_EVENT, diff --git a/webclient/components/matrix/matrix-call.js b/webclient/components/matrix/matrix-call.js index 2e3e2b096..fd21198d2 100644 --- a/webclient/components/matrix/matrix-call.js +++ b/webclient/components/matrix/matrix-call.js @@ -47,6 +47,10 @@ angular.module('MatrixCall', []) this.call_id = "c" + new Date().getTime(); this.state = 'fledgling'; this.didConnect = false; + + // a queue for candidates waiting to go out. We try to amalgamate candidates into a single candidate message where possible + this.candidateSendQueue = []; + this.candidateSendTries = 0; } MatrixCall.prototype.createPeerConnection = function() { @@ -174,12 +178,7 @@ angular.module('MatrixCall', []) MatrixCall.prototype.gotLocalIceCandidate = function(event) { console.log(event); if (event.candidate) { - var content = { - version: 0, - call_id: this.call_id, - candidate: event.candidate - }; - this.sendEventWithRetry('m.call.candidate', content); + this.sendCandidate(event.candidate); } } @@ -370,5 +369,53 @@ angular.module('MatrixCall', []) }, delayMs); }; + // Sends candidates with are sent in a special way because we try to amalgamate them into one message + MatrixCall.prototype.sendCandidate = function(content) { + this.candidateSendQueue.push(content); + var self = this; + if (this.candidateSendTries == 0) $timeout(function() { self.sendCandidateQueue(); }, 100); + }; + + MatrixCall.prototype.sendCandidateQueue = function(content) { + if (this.candidateSendQueue.length == 0) return; + + var cands = this.candidateSendQueue; + this.candidateSendQueue = []; + ++this.candidateSendTries; + var content = { + version: 0, + call_id: this.call_id, + candidates: cands + }; + var self = this; + console.log("Attempting to send "+cands.length+" candidates"); + matrixService.sendEvent(self.room_id, 'm.call.candidates', undefined, content).then(function() { self.candsSent(); }, function(error) { self.candsSendFailed(cands, error); } ); + }; + + MatrixCall.prototype.candsSent = function() { + this.candidateSendTries = 0; + this.sendCandidateQueue(); + }; + + MatrixCall.prototype.candsSendFailed = function(cands, error) { + for (var i = 0; i < cands.length; ++i) { + this.candidateSendQueue.push(cands[i]); + } + + if (this.candidateSendTries > 5) { + console.log("Failed to send candidates on attempt "+ev.tries+". Giving up for now."); + this.candidateSendTries = 0; + return; + } + + var delayMs = 500 * Math.pow(2, this.candidateSendTries); + ++this.candidateSendTries; + console.log("Failed to send candidates. Retrying in "+delayMs+"ms"); + var self = this; + $timeout(function() { + self.sendCandidateQueue(); + }, delayMs); + }; + return MatrixCall; }]); diff --git a/webclient/components/matrix/matrix-phone-service.js b/webclient/components/matrix/matrix-phone-service.js index b0dcf1910..2d0732a8d 100644 --- a/webclient/components/matrix/matrix-phone-service.js +++ b/webclient/components/matrix/matrix-phone-service.js @@ -77,13 +77,15 @@ angular.module('matrixPhoneService', []) return; } call.receivedAnswer(msg); - } else if (event.type == 'm.call.candidate') { + } else if (event.type == 'm.call.candidates') { var call = matrixPhoneService.allCalls[msg.call_id]; if (!call) { - console.log("Got candidate for unknown call ID "+msg.call_id); + console.log("Got candidates for unknown call ID "+msg.call_id); return; } - call.gotRemoteIceCandidate(msg.candidate); + for (var i = 0; i < msg.candidates.length; ++i) { + call.gotRemoteIceCandidate(msg.candidates[i]); + } } else if (event.type == 'm.call.hangup') { var call = matrixPhoneService.allCalls[msg.call_id]; if (!call) { diff --git a/webclient/recents/recents.html b/webclient/recents/recents.html index 3d736b669..ca7636e36 100644 --- a/webclient/recents/recents.html +++ b/webclient/recents/recents.html @@ -22,7 +22,7 @@
- {{ room.lastMsg.inviter | mUserDisplayName: room.room_id }} invited you + {{ room.inviter | mUserDisplayName: room.room_id }} invited you
diff --git a/webclient/room/room-controller.js b/webclient/room/room-controller.js index 3cc127140..50d902ae4 100644 --- a/webclient/room/room-controller.js +++ b/webclient/room/room-controller.js @@ -220,7 +220,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) }; var paginate = function(numItems) { - // console.log("paginate " + numItems); + //console.log("paginate " + numItems + " and first_pagination is " + $scope.state.first_pagination); if ($scope.state.paginating || !$scope.room_id) { return; } @@ -260,7 +260,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) } if ($scope.state.first_pagination) { - scrollToBottom(); + scrollToBottom(true); $scope.state.first_pagination = false; } else { @@ -598,6 +598,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput']) promise.then( function(response) { console.log("Request successfully sent"); + if (echo) { // Mark this fake message event with its allocated event_id // When the true message event will come from the events stream (in handleMessage),