summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--synapse/handlers/presence.py41
-rw-r--r--synapse/storage/_base.py1
-rw-r--r--synapse/storage/presence.py5
-rw-r--r--synapse/storage/schema/presence.sql1
-rw-r--r--tests/handlers/test_presence.py36
-rw-r--r--tests/handlers/test_presencelike.py34
-rw-r--r--tests/rest/test_presence.py12
7 files changed, 101 insertions, 29 deletions
diff --git a/synapse/handlers/presence.py b/synapse/handlers/presence.py
index 1c24efd454..8bdb0fe5c7 100644
--- a/synapse/handlers/presence.py
+++ b/synapse/handlers/presence.py
@@ -56,6 +56,8 @@ class PresenceHandler(BaseHandler):
 
         self.homeserver = hs
 
+        self.clock = hs.get_clock()
+
         distributor = hs.get_distributor()
         distributor.observe("registered_user", self.registered_user)
 
@@ -168,14 +170,15 @@ class PresenceHandler(BaseHandler):
                 state = yield self.store.get_presence_state(
                     target_user.localpart
                 )
-                defer.returnValue(state)
             else:
                 raise SynapseError(404, "Presence information not visible")
         else:
             # TODO(paul): Have remote server send us permissions set
-            defer.returnValue(
-                    self._get_or_offline_usercache(target_user).get_state()
-            )
+            state = self._get_or_offline_usercache(target_user).get_state()
+
+        if "mtime" in state:
+            state["mtime_age"] = self.clock.time_msec() - state.pop("mtime")
+        defer.returnValue(state)
 
     @defer.inlineCallbacks
     def set_state(self, target_user, auth_user, state):
@@ -209,6 +212,8 @@ class PresenceHandler(BaseHandler):
             ),
         ])
 
+        state["mtime"] = self.clock.time_msec()
+
         now_online = state["state"] != PresenceState.OFFLINE
         was_polling = target_user in self._user_cachemap
 
@@ -361,6 +366,8 @@ class PresenceHandler(BaseHandler):
             observed_user = self.hs.parse_userid(p.pop("observed_user_id"))
             p["observed_user"] = observed_user
             p.update(self._get_or_offline_usercache(observed_user).get_state())
+            if "mtime" in p:
+                p["mtime_age"] = self.clock.time_msec() - p.pop("mtime")
 
         defer.returnValue(presence)
 
@@ -546,10 +553,15 @@ class PresenceHandler(BaseHandler):
     def _push_presence_remote(self, user, destination, state=None):
         if state is None:
             state = yield self.store.get_presence_state(user.localpart)
+
             yield self.distributor.fire(
                 "collect_presencelike_data", user, state
             )
 
+        if "mtime" in state:
+            state = dict(state)
+            state["mtime_age"] = self.clock.time_msec() - state.pop("mtime")
+
         yield self.federation.send_edu(
             destination=destination,
             edu_type="m.presence",
@@ -585,6 +597,9 @@ class PresenceHandler(BaseHandler):
             state = dict(push)
             del state["user_id"]
 
+            if "mtime_age" in state:
+                state["mtime"] = self.clock.time_msec() - state.pop("mtime_age")
+
             statuscache = self._get_or_make_usercache(user)
 
             self._user_cachemap_latest_serial += 1
@@ -631,9 +646,14 @@ class PresenceHandler(BaseHandler):
 
     def push_update_to_clients(self, observer_user, observed_user,
                                statuscache):
+        state = statuscache.make_event(user=observed_user, clock=self.clock)
+
         self.notifier.on_new_user_event(
             observer_user.to_string(),
-            event_data=statuscache.make_event(user=observed_user),
+            event_data=statuscache.make_event(
+                user=observed_user,
+                clock=self.clock
+            ),
             stream_type=PresenceStreamData,
             store_id=statuscache.serial
         )
@@ -652,8 +672,10 @@ class PresenceStreamData(StreamData):
                    if from_key < cachemap[k].serial <= to_key]
 
         if updates:
+            clock = self.presence.clock
+
             latest_serial = max([x[1].serial for x in updates])
-            data = [x[1].make_event(user=x[0]) for x in updates]
+            data = [x[1].make_event(user=x[0], clock=clock) for x in updates]
             return ((data, latest_serial))
         else:
             return (([], self.presence._user_cachemap_latest_serial))
@@ -674,6 +696,8 @@ class UserPresenceCache(object):
         self.serial = None
 
     def update(self, state, serial):
+        assert("mtime_age" not in state)
+
         self.state.update(state)
         # Delete keys that are now 'None'
         for k in self.state.keys():
@@ -691,8 +715,11 @@ class UserPresenceCache(object):
         # clone it so caller can't break our cache
         return dict(self.state)
 
-    def make_event(self, user):
+    def make_event(self, user, clock):
         content = self.get_state()
         content["user_id"] = user.to_string()
 
+        if "mtime" in content:
+            content["mtime_age"] = clock.time_msec() - content.pop("mtime")
+
         return {"type": "m.presence", "content": content}
diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py
index 1b98bdfcef..bf1800f4bf 100644
--- a/synapse/storage/_base.py
+++ b/synapse/storage/_base.py
@@ -29,6 +29,7 @@ class SQLBaseStore(object):
     def __init__(self, hs):
         self.hs = hs
         self._db_pool = hs.get_db_pool()
+        self._clock = hs.get_clock()
 
     def cursor_to_dict(self, cursor):
         """Converts a SQL cursor into an list of dicts.
diff --git a/synapse/storage/presence.py b/synapse/storage/presence.py
index 6f5b042c25..23b6d1694e 100644
--- a/synapse/storage/presence.py
+++ b/synapse/storage/presence.py
@@ -35,7 +35,7 @@ class PresenceStore(SQLBaseStore):
         return self._simple_select_one(
             table="presence",
             keyvalues={"user_id": user_localpart},
-            retcols=["state", "status_msg"],
+            retcols=["state", "status_msg", "mtime"],
         )
 
     def set_presence_state(self, user_localpart, new_state):
@@ -43,7 +43,8 @@ class PresenceStore(SQLBaseStore):
             table="presence",
             keyvalues={"user_id": user_localpart},
             updatevalues={"state": new_state["state"],
-                          "status_msg": new_state["status_msg"]},
+                          "status_msg": new_state["status_msg"],
+                          "mtime": self._clock.time_msec()},
             retcols=["state"],
         )
 
diff --git a/synapse/storage/schema/presence.sql b/synapse/storage/schema/presence.sql
index b22e3ba863..b1081d3aab 100644
--- a/synapse/storage/schema/presence.sql
+++ b/synapse/storage/schema/presence.sql
@@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS presence(
   user_id INTEGER NOT NULL,
   state INTEGER,
   status_msg TEXT,
+  mtime INTEGER, -- miliseconds since last state change
   FOREIGN KEY(user_id) REFERENCES users(id)
 );
 
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
index 2299a2a7ba..b365741d99 100644
--- a/tests/handlers/test_presence.py
+++ b/tests/handlers/test_presence.py
@@ -20,6 +20,8 @@ from twisted.internet import defer
 from mock import Mock, call, ANY
 import logging
 
+from ..utils import MockClock
+
 from synapse.server import HomeServer
 from synapse.api.constants import PresenceState
 from synapse.api.errors import SynapseError
@@ -55,6 +57,7 @@ class PresenceStateTestCase(unittest.TestCase):
 
     def setUp(self):
         hs = HomeServer("test",
+                clock=MockClock(),
                 db_pool=None,
                 datastore=Mock(spec=[
                     "get_presence_state",
@@ -154,7 +157,11 @@ class PresenceStateTestCase(unittest.TestCase):
         mocked_set.assert_called_with("apple",
                 {"state": UNAVAILABLE, "status_msg": "Away"})
         self.mock_start.assert_called_with(self.u_apple,
-                state={"state": UNAVAILABLE, "status_msg": "Away"})
+                state={
+                    "state": UNAVAILABLE,
+                    "status_msg": "Away",
+                    "mtime": 1000000, # MockClock
+                })
 
         yield self.handler.set_state(
                 target_user=self.u_apple, auth_user=self.u_apple,
@@ -386,7 +393,10 @@ class PresencePushTestCase(unittest.TestCase):
         self.replication.send_edu = Mock()
         self.replication.send_edu.return_value = defer.succeed((200, "OK"))
 
+        self.clock = MockClock()
+
         hs = HomeServer("test",
+                clock=self.clock,
                 db_pool=None,
                 datastore=Mock(spec=[
                     "set_presence_state",
@@ -519,13 +529,18 @@ class PresencePushTestCase(unittest.TestCase):
         yield self.handler.set_state(self.u_banana, self.u_banana,
                 {"state": ONLINE})
 
+        self.clock.advance_time(2)
+
         presence = yield self.handler.get_presence_list(
                 observer_user=self.u_apple, accepted=True)
 
         self.assertEquals([
-                {"observed_user": self.u_banana, "state": ONLINE},
-                {"observed_user": self.u_clementine, "state": OFFLINE}],
-            presence)
+                {"observed_user": self.u_banana,
+                 "state": ONLINE,
+                 "mtime_age": 2000},
+                {"observed_user": self.u_clementine,
+                 "state": OFFLINE},
+        ], presence)
 
         self.mock_update_client.assert_has_calls([
                 call(observer_user=self.u_banana,
@@ -555,7 +570,8 @@ class PresencePushTestCase(unittest.TestCase):
                     content={
                         "push": [
                             {"user_id": "@apple:test",
-                            "state": "online"},
+                             "state": "online",
+                             "mtime_age": 0},
                         ],
                     }),
                 call(
@@ -564,7 +580,8 @@ class PresencePushTestCase(unittest.TestCase):
                     content={
                         "push": [
                             {"user_id": "@apple:test",
-                             "state": "online"},
+                             "state": "online",
+                             "mtime_age": 0},
                         ],
                     })
         ], any_order=True)
@@ -582,7 +599,8 @@ class PresencePushTestCase(unittest.TestCase):
                 "remote", "m.presence", {
                     "push": [
                         {"user_id": "@potato:remote",
-                         "state": "online"},
+                         "state": "online",
+                         "mtime_age": 1000},
                     ],
                 }
         )
@@ -596,9 +614,11 @@ class PresencePushTestCase(unittest.TestCase):
                     statuscache=ANY),
         ], any_order=True)
 
+        self.clock.advance_time(2)
+
         state = yield self.handler.get_state(self.u_potato, self.u_apple)
 
-        self.assertEquals({"state": ONLINE}, state)
+        self.assertEquals({"state": ONLINE, "mtime_age": 3000}, state)
 
     @defer.inlineCallbacks
     def test_join_room_local(self):
diff --git a/tests/handlers/test_presencelike.py b/tests/handlers/test_presencelike.py
index 54b92ba8e2..6eeb1bb522 100644
--- a/tests/handlers/test_presencelike.py
+++ b/tests/handlers/test_presencelike.py
@@ -22,6 +22,8 @@ from twisted.internet import defer
 from mock import Mock, call, ANY
 import logging
 
+from ..utils import MockClock
+
 from synapse.server import HomeServer
 from synapse.api.constants import PresenceState
 from synapse.handlers.presence import PresenceHandler
@@ -60,6 +62,7 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
 
     def setUp(self):
         hs = HomeServer("test",
+                clock=MockClock(),
                 db_pool=None,
                 datastore=Mock(spec=[
                     "set_presence_state",
@@ -156,10 +159,14 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
                 observer_user=self.u_apple, accepted=True)
 
         self.assertEquals([
-                {"observed_user": self.u_banana, "state": ONLINE,
-                    "displayname": "Frank", "avatar_url": "http://foo"},
-                {"observed_user": self.u_clementine, "state": OFFLINE}],
-            presence)
+            {"observed_user": self.u_banana,
+                "state": ONLINE,
+                "mtime_age": 0,
+                "displayname": "Frank",
+                "avatar_url": "http://foo"},
+            {"observed_user": self.u_clementine,
+                "state": OFFLINE}],
+        presence)
 
         self.mock_update_client.assert_has_calls([
             call(observer_user=self.u_apple,
@@ -171,9 +178,12 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
         ], any_order=True)
 
         statuscache = self.mock_update_client.call_args[1]["statuscache"]
-        self.assertEquals({"state": ONLINE,
-                           "displayname": "Frank",
-                           "avatar_url": "http://foo"}, statuscache.state)
+        self.assertEquals({
+            "state": ONLINE,
+            "mtime": 1000000, # MockClock
+            "displayname": "Frank",
+            "avatar_url": "http://foo",
+        }, statuscache.state)
 
         self.mock_update_client.reset_mock()
 
@@ -193,9 +203,12 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
         ], any_order=True)
 
         statuscache = self.mock_update_client.call_args[1]["statuscache"]
-        self.assertEquals({"state": ONLINE,
-                           "displayname": "I am an Apple",
-                           "avatar_url": "http://foo"}, statuscache.state)
+        self.assertEquals({
+            "state": ONLINE,
+            "mtime": 1000000, # MockClock
+            "displayname": "I am an Apple",
+            "avatar_url": "http://foo",
+        }, statuscache.state)
 
     @defer.inlineCallbacks
     def test_push_remote(self):
@@ -220,6 +233,7 @@ class PresenceProfilelikeDataTestCase(unittest.TestCase):
                     "push": [
                         {"user_id": "@apple:test",
                          "state": "online",
+                         "mtime_age": 0,
                          "displayname": "Frank",
                          "avatar_url": "http://foo"},
                     ],
diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py
index 7c54e067c9..f013abbee4 100644
--- a/tests/rest/test_presence.py
+++ b/tests/rest/test_presence.py
@@ -234,7 +234,11 @@ class PresenceEventStreamTestCase(unittest.TestCase):
         # I'll already get my own presence state change
         self.assertEquals({"start": "0", "end": "1", "chunk": [
             {"type": "m.presence",
-             "content": {"user_id": "@apple:test", "state": ONLINE}},
+             "content": {
+                 "user_id": "@apple:test",
+                 "state": ONLINE,
+                 "mtime_age": 0,
+            }},
         ]}, response)
 
         self.mock_datastore.set_presence_state.return_value = defer.succeed(
@@ -251,5 +255,9 @@ class PresenceEventStreamTestCase(unittest.TestCase):
         self.assertEquals(200, code)
         self.assertEquals({"start": "1", "end": "2", "chunk": [
             {"type": "m.presence",
-             "content": {"user_id": "@banana:test", "state": ONLINE}},
+             "content": {
+                 "user_id": "@banana:test",
+                 "state": ONLINE,
+                 "mtime_age": 0,
+            }},
         ]}, response)