Merge remote-tracking branch 'origin/develop' into test-sqlite-memory

This commit is contained in:
Paul "LeoNerd" Evans 2014-09-15 14:15:10 +01:00
commit b0406b9ead
44 changed files with 269 additions and 108 deletions

View File

@ -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) Changes in synapse 0.2.2 (2014-09-06)
===================================== =====================================

View File

@ -1 +1 @@
0.2.2 0.2.3

View File

@ -5,3 +5,5 @@ Broad-sweeping stuff which would be nice to have
- homeserver implementation in go - homeserver implementation in go
- homeserver implementation in node.js - homeserver implementation in node.js
- client SDKs - client SDKs
- libpurple library
- irssi plugin?

View File

@ -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' - ``type`` : "string" - The type of session description, in this case 'offer'
- ``sdp`` : "string" - The SDP text of the session description - ``sdp`` : "string" - The SDP text of the session description
``m.call.candidate`` ``m.call.candidates``
This event is sent by callers after sending an invite and by the callee after answering. This event is sent by callers after sending an invite and by the callee after answering.
Its purpose is to give the other party an additional ICE candidate to try using to Its purpose is to give the other party additional ICE candidates to try using to
communicate. communicate.
Required keys: Required keys:
- ``call_id`` : "string" - The ID of the call this event relates to - ``call_id`` : "string" - The ID of the call this event relates to
- ``version`` : "integer" - The version of the VoIP specification this messages - ``version`` : "integer" - The version of the VoIP specification this messages
adheres to. his specification is version 0. adheres to. his specification is version 0.
- ``candidate`` : "candidate object" - Object describing the candidate. - ``candidates`` : "array of candidate objects" - Array of object describing the candidates.
``Candidate Object`` ``Candidate Object``

View File

@ -16,4 +16,4 @@
""" This is a reference implementation of a synapse home server. """ This is a reference implementation of a synapse home server.
""" """
__version__ = "0.2.2" __version__ = "0.2.3"

View File

@ -17,6 +17,18 @@ from synapse.api.errors import SynapseError, Codes
from synapse.util.jsonobject import JsonEncodedObject from synapse.util.jsonobject import JsonEncodedObject
def serialize_event(hs, e):
# FIXME(erikj): To handle the case of presence events and the like
if not isinstance(e, SynapseEvent):
return e
d = e.get_dict()
if "age_ts" in d:
d["age"] = int(hs.get_clock().time_msec()) - d["age_ts"]
return d
class SynapseEvent(JsonEncodedObject): class SynapseEvent(JsonEncodedObject):
"""Base class for Synapse events. These are JSON objects which must abide """Base class for Synapse events. These are JSON objects which must abide
@ -43,6 +55,7 @@ class SynapseEvent(JsonEncodedObject):
"content", # HTTP body, JSON "content", # HTTP body, JSON
"state_key", "state_key",
"required_power_level", "required_power_level",
"age_ts",
] ]
internal_keys = [ internal_keys = [

View File

@ -59,6 +59,14 @@ class EventFactory(object):
if "ts" not in kwargs: if "ts" not in kwargs:
kwargs["ts"] = int(self.clock.time_msec()) kwargs["ts"] = int(self.clock.time_msec())
# The "age" key is a delta timestamp that should be converted into an
# absolute timestamp the minute we see it.
if "age" in kwargs:
kwargs["age_ts"] = int(self.clock.time_msec()) - int(kwargs["age"])
del kwargs["age"]
elif "age_ts" not in kwargs:
kwargs["age_ts"] = int(self.clock.time_msec())
if etype in self._event_list: if etype in self._event_list:
handler = self._event_list[etype] handler = self._event_list[etype]
else: else:

View File

@ -291,6 +291,12 @@ class ReplicationLayer(object):
def on_incoming_transaction(self, transaction_data): def on_incoming_transaction(self, transaction_data):
transaction = Transaction(**transaction_data) transaction = Transaction(**transaction_data)
for p in transaction.pdus:
if "age" in p:
p["age_ts"] = int(self.clock.time_msec()) - int(p["age"])
pdu_list = [Pdu(**p) for p in transaction.pdus]
logger.debug("[%s] Got transaction", transaction.transaction_id) logger.debug("[%s] Got transaction", transaction.transaction_id)
response = yield self.transaction_actions.have_responded(transaction) response = yield self.transaction_actions.have_responded(transaction)
@ -303,8 +309,6 @@ class ReplicationLayer(object):
logger.debug("[%s] Transacition is new", transaction.transaction_id) logger.debug("[%s] Transacition is new", transaction.transaction_id)
pdu_list = [Pdu(**p) for p in transaction.pdus]
dl = [] dl = []
for pdu in pdu_list: for pdu in pdu_list:
dl.append(self._handle_new_pdu(pdu)) dl.append(self._handle_new_pdu(pdu))
@ -405,9 +409,14 @@ class ReplicationLayer(object):
"""Returns a new Transaction containing the given PDUs suitable for """Returns a new Transaction containing the given PDUs suitable for
transmission. transmission.
""" """
pdus = [p.get_dict() for p in pdu_list]
for p in pdus:
if "age_ts" in pdus:
p["age"] = int(self.clock.time_msec()) - p["age_ts"]
return Transaction( return Transaction(
pdus=[p.get_dict() for p in pdu_list],
origin=self.server_name, origin=self.server_name,
pdus=pdus,
ts=int(self._clock.time_msec()), ts=int(self._clock.time_msec()),
destination=None, destination=None,
) )

View File

@ -15,7 +15,6 @@
from twisted.internet import defer from twisted.internet import defer
from synapse.api.events import SynapseEvent
from synapse.util.logutils import log_function from synapse.util.logutils import log_function
from ._base import BaseHandler from ._base import BaseHandler
@ -71,10 +70,7 @@ class EventStreamHandler(BaseHandler):
auth_user, room_ids, pagin_config, timeout auth_user, room_ids, pagin_config, timeout
) )
chunks = [ chunks = [self.hs.serialize_event(e) for e in events]
e.get_dict() if isinstance(e, SynapseEvent) else e
for e in events
]
chunk = { chunk = {
"chunk": chunks, "chunk": chunks,
@ -92,7 +88,9 @@ class EventStreamHandler(BaseHandler):
# 10 seconds of grace to allow the client to reconnect again # 10 seconds of grace to allow the client to reconnect again
# before we think they're gone # before we think they're gone
def _later(): def _later():
logger.debug("_later stopped_user_eventstream %s", auth_user) logger.debug(
"_later stopped_user_eventstream %s", auth_user
)
self.distributor.fire( self.distributor.fire(
"stopped_user_eventstream", auth_user "stopped_user_eventstream", auth_user
) )

View File

@ -124,7 +124,7 @@ class MessageHandler(BaseHandler):
) )
chunk = { chunk = {
"chunk": [e.get_dict() for e in events], "chunk": [self.hs.serialize_event(e) for e in events],
"start": pagin_config.from_token.to_string(), "start": pagin_config.from_token.to_string(),
"end": next_token.to_string(), "end": next_token.to_string(),
} }
@ -296,7 +296,7 @@ class MessageHandler(BaseHandler):
end_token = now_token.copy_and_replace("room_key", token[1]) end_token = now_token.copy_and_replace("room_key", token[1])
d["messages"] = { d["messages"] = {
"chunk": [m.get_dict() for m in messages], "chunk": [self.hs.serialize_event(m) for m in messages],
"start": start_token.to_string(), "start": start_token.to_string(),
"end": end_token.to_string(), "end": end_token.to_string(),
} }
@ -304,7 +304,7 @@ class MessageHandler(BaseHandler):
current_state = yield self.store.get_current_state( current_state = yield self.store.get_current_state(
event.room_id event.room_id
) )
d["state"] = [c.get_dict() for c in current_state] d["state"] = [self.hs.serialize_event(c) for c in current_state]
except: except:
logger.exception("Failed to get snapshot") logger.exception("Failed to get snapshot")

View File

@ -335,7 +335,7 @@ class RoomMemberHandler(BaseHandler):
member_list = yield self.store.get_room_members(room_id=room_id) member_list = yield self.store.get_room_members(room_id=room_id)
event_list = [ event_list = [
entry.get_dict() self.hs.serialize_event(entry)
for entry in member_list for entry in member_list
] ]
chunk_data = { chunk_data = {

View File

@ -59,7 +59,7 @@ class EventRestServlet(RestServlet):
event = yield handler.get_event(auth_user, event_id) event = yield handler.get_event(auth_user, event_id)
if event: if event:
defer.returnValue((200, event.get_dict())) defer.returnValue((200, self.hs.serialize_event(event)))
else: else:
defer.returnValue((404, "Event not found.")) defer.returnValue((404, "Event not found."))

View File

@ -378,7 +378,7 @@ class RoomTriggerBackfill(RestServlet):
handler = self.handlers.federation_handler handler = self.handlers.federation_handler
events = yield handler.backfill(remote_server, room_id, limit) events = yield handler.backfill(remote_server, room_id, limit)
res = [event.get_dict() for event in events] res = [self.hs.serialize_event(event) for event in events]
defer.returnValue((200, res)) defer.returnValue((200, res))

View File

@ -20,6 +20,7 @@
# Imports required for the default HomeServer() implementation # Imports required for the default HomeServer() implementation
from synapse.federation import initialize_http_replication from synapse.federation import initialize_http_replication
from synapse.api.events import serialize_event
from synapse.api.events.factory import EventFactory from synapse.api.events.factory import EventFactory
from synapse.notifier import Notifier from synapse.notifier import Notifier
from synapse.api.auth import Auth from synapse.api.auth import Auth
@ -139,6 +140,9 @@ class BaseHomeServer(object):
object.""" object."""
return RoomID.from_string(s, hs=self) return RoomID.from_string(s, hs=self)
def serialize_event(self, e):
return serialize_event(self, e)
# Build magic accessors for every dependency # Build magic accessors for every dependency
for depname in BaseHomeServer.DEPENDENCIES: for depname in BaseHomeServer.DEPENDENCIES:
BaseHomeServer._make_dependency_method(depname) BaseHomeServer._make_dependency_method(depname)

View File

@ -36,7 +36,7 @@ from .registration import RegistrationStore
from .room import RoomStore from .room import RoomStore
from .roommember import RoomMemberStore from .roommember import RoomMemberStore
from .stream import StreamStore from .stream import StreamStore
from .pdu import StatePduStore, PduStore from .pdu import StatePduStore, PduStore, PdusTable
from .transactions import TransactionStore from .transactions import TransactionStore
from .keys import KeyStore from .keys import KeyStore
@ -140,6 +140,12 @@ class DataStore(RoomMemberStore, RoomStore,
del cols["content"] del cols["content"]
del cols["prev_pdus"] del cols["prev_pdus"]
cols["content_json"] = json.dumps(pdu.content) 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) cols["unrecognized_keys"] = json.dumps(unrec_keys)
logger.debug("Persisting: %s", repr(cols)) logger.debug("Persisting: %s", repr(cols))

View File

@ -355,6 +355,10 @@ class SQLBaseStore(object):
d["content"] = json.loads(d["content"]) d["content"] = json.loads(d["content"])
del d["unrecognized_keys"] del d["unrecognized_keys"]
if "age_ts" not in d:
# For compatibility
d["age_ts"] = d["ts"] if "ts" in d else 0
return self.event_factory.create_event( return self.event_factory.create_event(
etype=d["type"], etype=d["type"],
**d **d

View File

@ -66,8 +66,8 @@ class RoomMemberStore(SQLBaseStore):
# Check if this was the last person to have left. # Check if this was the last person to have left.
member_events = self._get_members_query_txn( member_events = self._get_members_query_txn(
txn, txn,
where_clause="c.room_id = ? AND m.membership = ?", where_clause="c.room_id = ? AND m.membership = ? AND m.user_id != ?",
where_values=(event.room_id, Membership.JOIN,) where_values=(event.room_id, Membership.JOIN, target_user_id,)
) )
joined_domains = set() joined_domains = set()

View File

@ -1,6 +1,6 @@
from synapse.api.ratelimiting import Ratelimiter from synapse.api.ratelimiting import Ratelimiter
import unittest from tests import unittest
class TestRatelimiter(unittest.TestCase): class TestRatelimiter(unittest.TestCase):

View File

@ -15,7 +15,7 @@
from synapse.api.events import SynapseEvent from synapse.api.events import SynapseEvent
import unittest from tests import unittest
class SynapseTemplateCheckTestCase(unittest.TestCase): class SynapseTemplateCheckTestCase(unittest.TestCase):

View File

@ -14,11 +14,10 @@
# trial imports # trial imports
from twisted.internet import defer from twisted.internet import defer
from twisted.trial import unittest from tests import unittest
# python imports # python imports
from mock import Mock from mock import Mock
import logging
from ..utils import MockHttpResource, MockClock from ..utils import MockHttpResource, MockClock
@ -28,9 +27,6 @@ from synapse.federation.units import Pdu
from synapse.storage.pdu import PduTuple, PduEntry from synapse.storage.pdu import PduTuple, PduEntry
logging.getLogger().addHandler(logging.NullHandler())
def make_pdu(prev_pdus=[], **kwargs): def make_pdu(prev_pdus=[], **kwargs):
"""Provide some default fields for making a PduTuple.""" """Provide some default fields for making a PduTuple."""
pdu_fields = { pdu_fields = {

View File

@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from twisted.trial import unittest from tests import unittest
from synapse.federation.pdu_codec import ( from synapse.federation.pdu_codec import (
PduCodec, encode_event_id, decode_event_id PduCodec, encode_event_id, decode_event_id

View File

@ -14,11 +14,10 @@
# limitations under the License. # limitations under the License.
from twisted.trial import unittest from tests import unittest
from twisted.internet import defer from twisted.internet import defer
from mock import Mock from mock import Mock
import logging
from synapse.server import HomeServer from synapse.server import HomeServer
from synapse.http.client import HttpClient from synapse.http.client import HttpClient
@ -28,9 +27,6 @@ from synapse.storage.directory import RoomAliasMapping
from tests.utils import SQLiteMemoryDbPool from tests.utils import SQLiteMemoryDbPool
logging.getLogger().addHandler(logging.NullHandler())
class DirectoryHandlers(object): class DirectoryHandlers(object):
def __init__(self, hs): def __init__(self, hs):
self.directory_handler = DirectoryHandler(hs) self.directory_handler = DirectoryHandler(hs)

View File

@ -14,7 +14,7 @@
from twisted.internet import defer from twisted.internet import defer
from twisted.trial import unittest from tests import unittest
from synapse.api.events.room import ( from synapse.api.events.room import (
InviteJoinEvent, MessageEvent, RoomMemberEvent InviteJoinEvent, MessageEvent, RoomMemberEvent
@ -26,12 +26,8 @@ from synapse.federation.units import Pdu
from mock import NonCallableMock, ANY from mock import NonCallableMock, ANY
import logging
from ..utils import get_mock_call_args from ..utils import get_mock_call_args
logging.getLogger().addHandler(logging.NullHandler())
class FederationTestCase(unittest.TestCase): class FederationTestCase(unittest.TestCase):

View File

@ -14,11 +14,10 @@
# limitations under the License. # limitations under the License.
from twisted.trial import unittest from tests import unittest
from twisted.internet import defer, reactor from twisted.internet import defer, reactor
from mock import Mock, call, ANY from mock import Mock, call, ANY
import logging
import json import json
from tests.utils import ( from tests.utils import (
@ -36,9 +35,6 @@ UNAVAILABLE = PresenceState.UNAVAILABLE
ONLINE = PresenceState.ONLINE ONLINE = PresenceState.ONLINE
logging.getLogger().addHandler(logging.NullHandler())
def _expect_edu(destination, edu_type, content, origin="test"): def _expect_edu(destination, edu_type, content, origin="test"):
return { return {
"origin": origin, "origin": origin,
@ -85,7 +81,6 @@ class PresenceStateTestCase(unittest.TestCase):
# Mock the RoomMemberHandler # Mock the RoomMemberHandler
room_member_handler = Mock(spec=[]) room_member_handler = Mock(spec=[])
hs.handlers.room_member_handler = room_member_handler hs.handlers.room_member_handler = room_member_handler
logging.getLogger().debug("Mocking room_member_handler=%r", room_member_handler)
# Some local users to test with # Some local users to test with
self.u_apple = hs.parse_userid("@apple:test") self.u_apple = hs.parse_userid("@apple:test")

View File

@ -16,11 +16,10 @@
"""This file contains tests of the "presence-like" data that is shared between """This file contains tests of the "presence-like" data that is shared between
presence and profiles; namely, the displayname and avatar_url.""" presence and profiles; namely, the displayname and avatar_url."""
from twisted.trial import unittest from tests import unittest
from twisted.internet import defer from twisted.internet import defer
from mock import Mock, call, ANY from mock import Mock, call, ANY
import logging
from ..utils import MockClock from ..utils import MockClock
@ -35,9 +34,6 @@ UNAVAILABLE = PresenceState.UNAVAILABLE
ONLINE = PresenceState.ONLINE ONLINE = PresenceState.ONLINE
logging.getLogger().addHandler(logging.NullHandler())
class MockReplication(object): class MockReplication(object):
def __init__(self): def __init__(self):
self.edu_handlers = {} self.edu_handlers = {}

View File

@ -14,11 +14,10 @@
# limitations under the License. # limitations under the License.
from twisted.trial import unittest from tests import unittest
from twisted.internet import defer from twisted.internet import defer
from mock import Mock from mock import Mock
import logging
from synapse.api.errors import AuthError from synapse.api.errors import AuthError
from synapse.server import HomeServer from synapse.server import HomeServer
@ -27,9 +26,6 @@ from synapse.handlers.profile import ProfileHandler
from tests.utils import SQLiteMemoryDbPool from tests.utils import SQLiteMemoryDbPool
logging.getLogger().addHandler(logging.NullHandler())
class ProfileHandlers(object): class ProfileHandlers(object):
def __init__(self, hs): def __init__(self, hs):
self.profile_handler = ProfileHandler(hs) self.profile_handler = ProfileHandler(hs)

View File

@ -15,7 +15,7 @@
from twisted.internet import defer from twisted.internet import defer
from twisted.trial import unittest from tests import unittest
from synapse.api.events.room import ( from synapse.api.events.room import (
InviteJoinEvent, RoomMemberEvent, RoomConfigEvent InviteJoinEvent, RoomMemberEvent, RoomConfigEvent
@ -27,10 +27,6 @@ from synapse.server import HomeServer
from mock import Mock, NonCallableMock from mock import Mock, NonCallableMock
import logging
logging.getLogger().addHandler(logging.NullHandler())
class RoomMemberHandlerTestCase(unittest.TestCase): class RoomMemberHandlerTestCase(unittest.TestCase):

View File

@ -14,12 +14,11 @@
# limitations under the License. # limitations under the License.
from twisted.trial import unittest from tests import unittest
from twisted.internet import defer from twisted.internet import defer
from mock import Mock, call, ANY from mock import Mock, call, ANY
import json import json
import logging
from ..utils import MockHttpResource, MockClock, DeferredMockCallable from ..utils import MockHttpResource, MockClock, DeferredMockCallable
@ -27,9 +26,6 @@ from synapse.server import HomeServer
from synapse.handlers.typing import TypingNotificationHandler from synapse.handlers.typing import TypingNotificationHandler
logging.getLogger().addHandler(logging.NullHandler())
def _expect_edu(destination, edu_type, content, origin="test"): def _expect_edu(destination, edu_type, content, origin="test"):
return { return {
"origin": origin, "origin": origin,

View File

@ -14,7 +14,7 @@
# limitations under the License. # limitations under the License.
""" Tests REST events for /events paths.""" """ Tests REST events for /events paths."""
from twisted.trial import unittest from tests import unittest
# twisted imports # twisted imports
from twisted.internet import defer from twisted.internet import defer
@ -27,14 +27,12 @@ from synapse.server import HomeServer
# python imports # python imports
import json import json
import logging
from ..utils import MockHttpResource, MemoryDataStore from ..utils import MockHttpResource, MemoryDataStore
from .utils import RestTestCase from .utils import RestTestCase
from mock import Mock, NonCallableMock from mock import Mock, NonCallableMock
logging.getLogger().addHandler(logging.NullHandler())
PATH_PREFIX = "/_matrix/client/api/v1" PATH_PREFIX = "/_matrix/client/api/v1"

View File

@ -15,11 +15,10 @@
"""Tests REST events for /presence paths.""" """Tests REST events for /presence paths."""
from twisted.trial import unittest from tests import unittest
from twisted.internet import defer from twisted.internet import defer
from mock import Mock from mock import Mock
import logging
from ..utils import MockHttpResource from ..utils import MockHttpResource
@ -28,9 +27,6 @@ from synapse.handlers.presence import PresenceHandler
from synapse.server import HomeServer from synapse.server import HomeServer
logging.getLogger().addHandler(logging.NullHandler())
OFFLINE = PresenceState.OFFLINE OFFLINE = PresenceState.OFFLINE
UNAVAILABLE = PresenceState.UNAVAILABLE UNAVAILABLE = PresenceState.UNAVAILABLE
ONLINE = PresenceState.ONLINE ONLINE = PresenceState.ONLINE

View File

@ -15,7 +15,7 @@
"""Tests REST events for /profile paths.""" """Tests REST events for /profile paths."""
from twisted.trial import unittest from tests import unittest
from twisted.internet import defer from twisted.internet import defer
from mock import Mock from mock import Mock
@ -28,6 +28,7 @@ from synapse.server import HomeServer
myid = "@1234ABCD:test" myid = "@1234ABCD:test"
PATH_PREFIX = "/_matrix/client/api/v1" PATH_PREFIX = "/_matrix/client/api/v1"
class ProfileTestCase(unittest.TestCase): class ProfileTestCase(unittest.TestCase):
""" Tests profile management. """ """ Tests profile management. """

View File

@ -17,7 +17,7 @@
from twisted.internet import defer from twisted.internet import defer
# trial imports # trial imports
from twisted.trial import unittest from tests import unittest
from synapse.api.constants import Membership from synapse.api.constants import Membership

View File

@ -14,7 +14,7 @@
# limitations under the License. # limitations under the License.
from twisted.trial import unittest from tests import unittest
from twisted.internet import defer from twisted.internet import defer
from mock import Mock, call from mock import Mock, call

View File

@ -13,8 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from tests import unittest
from twisted.internet import defer from twisted.internet import defer
from twisted.trial import unittest
from mock import Mock, patch from mock import Mock, patch

View File

@ -13,8 +13,8 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
from tests import unittest
from twisted.internet import defer from twisted.internet import defer
from twisted.trial import unittest
from twisted.python.log import PythonLoggingObserver from twisted.python.log import PythonLoggingObserver
from synapse.state import StateHandler from synapse.state import StateHandler
@ -26,7 +26,6 @@ from collections import namedtuple
from mock import Mock from mock import Mock
import logging
import mock import mock

View File

@ -13,7 +13,7 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import unittest from tests import unittest
from synapse.server import BaseHomeServer from synapse.server import BaseHomeServer
from synapse.types import UserID, RoomAlias from synapse.types import UserID, RoomAlias

79
tests/unittest.py Normal file
View File

@ -0,0 +1,79 @@
# -*- coding: utf-8 -*-
# Copyright 2014 OpenMarket Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from twisted.trial import unittest
import logging
# logging doesn't have a "don't log anything at all EVARRRR setting,
# but since the highest value is 50, 1000000 should do ;)
NEVER = 1000000
logging.getLogger().addHandler(logging.StreamHandler())
logging.getLogger().setLevel(NEVER)
def around(target):
"""A CLOS-style 'around' modifier, which wraps the original method of the
given instance with another piece of code.
@around(self)
def method_name(orig, *args, **kwargs):
return orig(*args, **kwargs)
"""
def _around(code):
name = code.__name__
orig = getattr(target, name)
def new(*args, **kwargs):
return code(orig, *args, **kwargs)
setattr(target, name, new)
return _around
class TestCase(unittest.TestCase):
"""A subclass of twisted.trial's TestCase which looks for 'loglevel'
attributes on both itself and its individual test methods, to override the
root logger's logging level while that test (case|method) runs."""
def __init__(self, methodName, *args, **kwargs):
super(TestCase, self).__init__(methodName, *args, **kwargs)
method = getattr(self, methodName)
level = getattr(method, "loglevel",
getattr(self, "loglevel",
NEVER))
@around(self)
def setUp(orig):
old_level = logging.getLogger().level
if old_level != level:
@around(self)
def tearDown(orig):
ret = orig()
logging.getLogger().setLevel(old_level)
return ret
logging.getLogger().setLevel(level)
return orig()
def DEBUG(target):
"""A decorator to set the .loglevel attribute to logging.DEBUG.
Can apply to either a TestCase or an individual test method."""
target.loglevel = logging.DEBUG
return target

View File

@ -15,7 +15,7 @@
from twisted.internet import defer from twisted.internet import defer
from twisted.trial import unittest from tests import unittest
from synapse.util.lockutils import LockManager from synapse.util.lockutils import LockManager
@ -105,4 +105,4 @@ class LockManagerTestCase(unittest.TestCase):
pass pass
with (yield self.lock_manager.lock(key)): with (yield self.lock_manager.lock(key)):
pass pass

View File

@ -528,8 +528,8 @@ a:active { color: #000; }
} }
.bubble .message { .bubble .message {
/* Break lines when encountering CR+LF */ /* Wrap words and break lines on CR+LF */
white-space: pre; white-space: pre-wrap;
} }
.bubble .messagePending { .bubble .messagePending {
opacity: 0.3 opacity: 0.3

View File

@ -38,6 +38,13 @@ angular.module('eventHandlerService', [])
var TOPIC_EVENT = "TOPIC_EVENT"; var TOPIC_EVENT = "TOPIC_EVENT";
var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted var RESET_EVENT = "RESET_EVENT"; // eventHandlerService has been resetted
// used for dedupping events - could be expanded in future...
// FIXME: means that we leak memory over time (along with lots of the rest
// of the app, given we never try to reap memory yet)
var eventMap = {};
$rootScope.presence = {};
var initialSyncDeferred; var initialSyncDeferred;
var reset = function() { var reset = function() {
@ -46,16 +53,13 @@ angular.module('eventHandlerService', [])
$rootScope.events = { $rootScope.events = {
rooms: {} // will contain roomId: { messages:[], members:{userid1: event} } rooms: {} // will contain roomId: { messages:[], members:{userid1: event} }
}; };
}
$rootScope.presence = {};
eventMap = {};
};
reset(); 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) { var initRoom = function(room_id) {
if (!(room_id in $rootScope.events.rooms)) { if (!(room_id in $rootScope.events.rooms)) {
console.log("Creating new handler entry for " + room_id); console.log("Creating new handler entry for " + room_id);
@ -204,7 +208,7 @@ angular.module('eventHandlerService', [])
var handleCallEvent = function(event, isLiveEvent) { var handleCallEvent = function(event, isLiveEvent) {
$rootScope.$broadcast(CALL_EVENT, event, isLiveEvent); $rootScope.$broadcast(CALL_EVENT, event, isLiveEvent);
if (event.type == 'm.call.invite') { if (event.type === 'm.call.invite') {
$rootScope.events.rooms[event.room_id].messages.push(event); $rootScope.events.rooms[event.room_id].messages.push(event);
} }
}; };
@ -231,7 +235,7 @@ angular.module('eventHandlerService', [])
} }
} }
return index; return index;
} };
return { return {
ROOM_CREATE_EVENT: ROOM_CREATE_EVENT, ROOM_CREATE_EVENT: ROOM_CREATE_EVENT,

View File

@ -47,6 +47,10 @@ angular.module('MatrixCall', [])
this.call_id = "c" + new Date().getTime(); this.call_id = "c" + new Date().getTime();
this.state = 'fledgling'; this.state = 'fledgling';
this.didConnect = false; this.didConnect = false;
// a queue for candidates waiting to go out. We try to amalgamate candidates into a single candidate message where possible
this.candidateSendQueue = [];
this.candidateSendTries = 0;
} }
MatrixCall.prototype.createPeerConnection = function() { MatrixCall.prototype.createPeerConnection = function() {
@ -174,12 +178,7 @@ angular.module('MatrixCall', [])
MatrixCall.prototype.gotLocalIceCandidate = function(event) { MatrixCall.prototype.gotLocalIceCandidate = function(event) {
console.log(event); console.log(event);
if (event.candidate) { if (event.candidate) {
var content = { this.sendCandidate(event.candidate);
version: 0,
call_id: this.call_id,
candidate: event.candidate
};
this.sendEventWithRetry('m.call.candidate', content);
} }
} }
@ -370,5 +369,53 @@ angular.module('MatrixCall', [])
}, delayMs); }, delayMs);
}; };
// Sends candidates with are sent in a special way because we try to amalgamate them into one message
MatrixCall.prototype.sendCandidate = function(content) {
this.candidateSendQueue.push(content);
var self = this;
if (this.candidateSendTries == 0) $timeout(function() { self.sendCandidateQueue(); }, 100);
};
MatrixCall.prototype.sendCandidateQueue = function(content) {
if (this.candidateSendQueue.length == 0) return;
var cands = this.candidateSendQueue;
this.candidateSendQueue = [];
++this.candidateSendTries;
var content = {
version: 0,
call_id: this.call_id,
candidates: cands
};
var self = this;
console.log("Attempting to send "+cands.length+" candidates");
matrixService.sendEvent(self.room_id, 'm.call.candidates', undefined, content).then(function() { self.candsSent(); }, function(error) { self.candsSendFailed(cands, error); } );
};
MatrixCall.prototype.candsSent = function() {
this.candidateSendTries = 0;
this.sendCandidateQueue();
};
MatrixCall.prototype.candsSendFailed = function(cands, error) {
for (var i = 0; i < cands.length; ++i) {
this.candidateSendQueue.push(cands[i]);
}
if (this.candidateSendTries > 5) {
console.log("Failed to send candidates on attempt "+ev.tries+". Giving up for now.");
this.candidateSendTries = 0;
return;
}
var delayMs = 500 * Math.pow(2, this.candidateSendTries);
++this.candidateSendTries;
console.log("Failed to send candidates. Retrying in "+delayMs+"ms");
var self = this;
$timeout(function() {
self.sendCandidateQueue();
}, delayMs);
};
return MatrixCall; return MatrixCall;
}]); }]);

View File

@ -77,13 +77,15 @@ angular.module('matrixPhoneService', [])
return; return;
} }
call.receivedAnswer(msg); call.receivedAnswer(msg);
} else if (event.type == 'm.call.candidate') { } else if (event.type == 'm.call.candidates') {
var call = matrixPhoneService.allCalls[msg.call_id]; var call = matrixPhoneService.allCalls[msg.call_id];
if (!call) { if (!call) {
console.log("Got candidate for unknown call ID "+msg.call_id); console.log("Got candidates for unknown call ID "+msg.call_id);
return; 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') { } else if (event.type == 'm.call.hangup') {
var call = matrixPhoneService.allCalls[msg.call_id]; var call = matrixPhoneService.allCalls[msg.call_id];
if (!call) { if (!call) {

View File

@ -22,7 +22,7 @@
<td colspan="3" class="recentsRoomSummary"> <td colspan="3" class="recentsRoomSummary">
<div ng-show="room.membership === 'invite'"> <div ng-show="room.membership === 'invite'">
{{ room.lastMsg.inviter | mUserDisplayName: room.room_id }} invited you {{ room.inviter | mUserDisplayName: room.room_id }} invited you
</div> </div>
<div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type"> <div ng-hide="room.membership === 'invite'" ng-switch="room.lastMsg.type">

View File

@ -220,7 +220,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
}; };
var paginate = function(numItems) { 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) { if ($scope.state.paginating || !$scope.room_id) {
return; return;
} }
@ -260,7 +260,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
} }
if ($scope.state.first_pagination) { if ($scope.state.first_pagination) {
scrollToBottom(); scrollToBottom(true);
$scope.state.first_pagination = false; $scope.state.first_pagination = false;
} }
else { else {
@ -598,6 +598,7 @@ angular.module('RoomController', ['ngSanitize', 'matrixFilter', 'mFileInput'])
promise.then( promise.then(
function(response) { function(response) {
console.log("Request successfully sent"); console.log("Request successfully sent");
if (echo) { if (echo) {
// Mark this fake message event with its allocated event_id // Mark this fake message event with its allocated event_id
// When the true message event will come from the events stream (in handleMessage), // When the true message event will come from the events stream (in handleMessage),