Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit ccca141

Browse files
authored
Track device IDs for pushers (#13831)
Second half of the MSC3881 implementation
1 parent 0fd2f2d commit ccca141

File tree

7 files changed

+154
-10
lines changed

7 files changed

+154
-10
lines changed

changelog.d/13831.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add experimental support for [MSC3881: Remotely toggle push notifications for another client](https://github.com/matrix-org/matrix-spec-proposals/pull/3881).

synapse/push/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class PusherConfig:
117117
last_success: Optional[int]
118118
failing_since: Optional[int]
119119
enabled: bool
120+
device_id: Optional[str]
120121

121122
def as_dict(self) -> Dict[str, Any]:
122123
"""Information that can be retrieved about a pusher after creation."""
@@ -130,6 +131,7 @@ def as_dict(self) -> Dict[str, Any]:
130131
"profile_tag": self.profile_tag,
131132
"pushkey": self.pushkey,
132133
"enabled": self.enabled,
134+
"device_id": self.device_id,
133135
}
134136

135137

synapse/push/pusherpool.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ async def add_or_update_pusher(
107107
data: JsonDict,
108108
profile_tag: str = "",
109109
enabled: bool = True,
110+
device_id: Optional[str] = None,
110111
) -> Optional[Pusher]:
111112
"""Creates a new pusher and adds it to the pool
112113
@@ -149,18 +150,20 @@ async def add_or_update_pusher(
149150
last_success=None,
150151
failing_since=None,
151152
enabled=enabled,
153+
device_id=device_id,
152154
)
153155
)
154156

155157
# Before we actually persist the pusher, we check if the user already has one
156-
# for this app ID and pushkey. If so, we want to keep the access token in place,
157-
# since this could be one device modifying (e.g. enabling/disabling) another
158-
# device's pusher.
158+
# this app ID and pushkey. If so, we want to keep the access token and device ID
159+
# in place, since this could be one device modifying (e.g. enabling/disabling)
160+
# another device's pusher.
159161
existing_config = await self._get_pusher_config_for_user_by_app_id_and_pushkey(
160162
user_id, app_id, pushkey
161163
)
162164
if existing_config:
163165
access_token = existing_config.access_token
166+
device_id = existing_config.device_id
164167

165168
await self.store.add_pusher(
166169
user_id=user_id,
@@ -176,6 +179,7 @@ async def add_or_update_pusher(
176179
last_stream_ordering=last_stream_ordering,
177180
profile_tag=profile_tag,
178181
enabled=enabled,
182+
device_id=device_id,
179183
)
180184
pusher = await self.process_pusher_change_by_id(app_id, pushkey, user_id)
181185

synapse/rest/client/pusher.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ async def on_GET(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
5757
for pusher in pusher_dicts:
5858
if self._msc3881_enabled:
5959
pusher["org.matrix.msc3881.enabled"] = pusher["enabled"]
60+
pusher["org.matrix.msc3881.device_id"] = pusher["device_id"]
6061
del pusher["enabled"]
62+
del pusher["device_id"]
6163

6264
return 200, {"pushers": pusher_dicts}
6365

@@ -134,6 +136,7 @@ async def on_POST(self, request: SynapseRequest) -> Tuple[int, JsonDict]:
134136
data=content["data"],
135137
profile_tag=content.get("profile_tag", ""),
136138
enabled=enabled,
139+
device_id=requester.device_id,
137140
)
138141
except PusherConfigException as pce:
139142
raise SynapseError(

synapse/storage/databases/main/pusher.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def get_pushers_by_txn(txn: LoggingTransaction) -> List[Dict[str, Any]]:
124124
id, user_name, access_token, profile_tag, kind, app_id,
125125
app_display_name, device_display_name, pushkey, ts, lang, data,
126126
last_stream_ordering, last_success, failing_since,
127-
COALESCE(enabled, TRUE) AS enabled
127+
COALESCE(enabled, TRUE) AS enabled, device_id
128128
FROM pushers
129129
"""
130130

@@ -477,7 +477,74 @@ def _delete_pushers(txn: LoggingTransaction) -> int:
477477
return number_deleted
478478

479479

480-
class PusherStore(PusherWorkerStore):
480+
class PusherBackgroundUpdatesStore(SQLBaseStore):
481+
def __init__(
482+
self,
483+
database: DatabasePool,
484+
db_conn: LoggingDatabaseConnection,
485+
hs: "HomeServer",
486+
):
487+
super().__init__(database, db_conn, hs)
488+
489+
self.db_pool.updates.register_background_update_handler(
490+
"set_device_id_for_pushers", self._set_device_id_for_pushers
491+
)
492+
493+
async def _set_device_id_for_pushers(
494+
self, progress: JsonDict, batch_size: int
495+
) -> int:
496+
"""Background update to populate the device_id column of the pushers table."""
497+
last_pusher_id = progress.get("pusher_id", 0)
498+
499+
def set_device_id_for_pushers_txn(txn: LoggingTransaction) -> int:
500+
txn.execute(
501+
"""
502+
SELECT p.id, at.device_id
503+
FROM pushers AS p
504+
INNER JOIN access_tokens AS at
505+
ON p.access_token = at.id
506+
WHERE
507+
p.access_token IS NOT NULL
508+
AND at.device_id IS NOT NULL
509+
AND p.id > ?
510+
ORDER BY p.id
511+
LIMIT ?
512+
""",
513+
(last_pusher_id, batch_size),
514+
)
515+
516+
rows = self.db_pool.cursor_to_dict(txn)
517+
if len(rows) == 0:
518+
return 0
519+
520+
self.db_pool.simple_update_many_txn(
521+
txn=txn,
522+
table="pushers",
523+
key_names=("id",),
524+
key_values=[(row["id"],) for row in rows],
525+
value_names=("device_id",),
526+
value_values=[(row["device_id"],) for row in rows],
527+
)
528+
529+
self.db_pool.updates._background_update_progress_txn(
530+
txn, "set_device_id_for_pushers", {"pusher_id": rows[-1]["id"]}
531+
)
532+
533+
return len(rows)
534+
535+
nb_processed = await self.db_pool.runInteraction(
536+
"set_device_id_for_pushers", set_device_id_for_pushers_txn
537+
)
538+
539+
if nb_processed < batch_size:
540+
await self.db_pool.updates._end_background_update(
541+
"set_device_id_for_pushers"
542+
)
543+
544+
return nb_processed
545+
546+
547+
class PusherStore(PusherWorkerStore, PusherBackgroundUpdatesStore):
481548
def get_pushers_stream_token(self) -> int:
482549
return self._pushers_id_gen.get_current_token()
483550

@@ -496,6 +563,7 @@ async def add_pusher(
496563
last_stream_ordering: int,
497564
profile_tag: str = "",
498565
enabled: bool = True,
566+
device_id: Optional[str] = None,
499567
) -> None:
500568
async with self._pushers_id_gen.get_next() as stream_id:
501569
# no need to lock because `pushers` has a unique key on
@@ -515,6 +583,7 @@ async def add_pusher(
515583
"profile_tag": profile_tag,
516584
"id": stream_id,
517585
"enabled": enabled,
586+
"device_id": device_id,
518587
},
519588
desc="add_pusher",
520589
lock=False,
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* Copyright 2022 The Matrix.org Foundation C.I.C
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
-- Add a device_id column to track the device ID that created the pusher. It's NULLable
17+
-- on purpose, because a) it might not be possible to track down the device that created
18+
-- old pushers (pushers.access_token and access_tokens.device_id are both NULLable), and
19+
-- b) access tokens retrieved via the admin API don't have a device associated to them.
20+
ALTER TABLE pushers ADD COLUMN device_id TEXT;

tests/push/test_http.py

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from synapse.push import PusherConfig, PusherConfigException
2323
from synapse.rest.client import login, push_rule, pusher, receipts, room
2424
from synapse.server import HomeServer
25+
from synapse.storage.databases.main.registration import TokenLookupResult
2526
from synapse.types import JsonDict
2627
from synapse.util import Clock
2728

@@ -771,6 +772,7 @@ def _set_pusher(self, user_id: str, access_token: str, enabled: bool) -> None:
771772
lang=None,
772773
data={"url": "http://example.com/_matrix/push/v1/notify"},
773774
enabled=enabled,
775+
device_id=user_tuple.device_id,
774776
)
775777
)
776778

@@ -885,19 +887,21 @@ def test_null_enabled(self) -> None:
885887
self.assertEqual(len(channel.json_body["pushers"]), 1)
886888
self.assertTrue(channel.json_body["pushers"][0]["org.matrix.msc3881.enabled"])
887889

888-
def test_update_different_device_access_token(self) -> None:
890+
def test_update_different_device_access_token_device_id(self) -> None:
889891
"""Tests that if we create a pusher from one device, the update it from another
890-
device, the access token associated with the pusher stays the same.
892+
device, the access token and device ID associated with the pusher stays the
893+
same.
891894
"""
892895
# Create a user with a pusher.
893896
user_id, access_token = self._make_user_with_pusher("user")
894897

895898
# Get the token ID for the current access token, since that's what we store in
896-
# the pushers table.
899+
# the pushers table. Also get the device ID from it.
897900
user_tuple = self.get_success(
898901
self.hs.get_datastores().main.get_user_by_access_token(access_token)
899902
)
900903
token_id = user_tuple.token_id
904+
device_id = user_tuple.device_id
901905

902906
# Generate a new access token, and update the pusher with it.
903907
new_token = self.login("user", "pass")
@@ -909,7 +913,48 @@ def test_update_different_device_access_token(self) -> None:
909913
)
910914
pushers: List[PusherConfig] = list(ret)
911915

912-
# Check that we still have one pusher, and that the access token associated with
913-
# it didn't change.
916+
# Check that we still have one pusher, and that the access token and device ID
917+
# associated with it didn't change.
914918
self.assertEqual(len(pushers), 1)
915919
self.assertEqual(pushers[0].access_token, token_id)
920+
self.assertEqual(pushers[0].device_id, device_id)
921+
922+
@override_config({"experimental_features": {"msc3881_enabled": True}})
923+
def test_device_id(self) -> None:
924+
"""Tests that a pusher created with a given device ID shows that device ID in
925+
GET /pushers requests.
926+
"""
927+
self.register_user("user", "pass")
928+
access_token = self.login("user", "pass")
929+
930+
# We create the pusher with an HTTP request rather than with
931+
# _make_user_with_pusher so that we can test the device ID is correctly set when
932+
# creating a pusher via an API call.
933+
self.make_request(
934+
method="POST",
935+
path="/pushers/set",
936+
content={
937+
"kind": "http",
938+
"app_id": "m.http",
939+
"app_display_name": "HTTP Push Notifications",
940+
"device_display_name": "pushy push",
941+
"pushkey": "a@example.com",
942+
"lang": "en",
943+
"data": {"url": "http://example.com/_matrix/push/v1/notify"},
944+
},
945+
access_token=access_token,
946+
)
947+
948+
# Look up the user info for the access token so we can compare the device ID.
949+
lookup_result: TokenLookupResult = self.get_success(
950+
self.hs.get_datastores().main.get_user_by_access_token(access_token)
951+
)
952+
953+
# Get the user's devices and check it has the correct device ID.
954+
channel = self.make_request("GET", "/pushers", access_token=access_token)
955+
self.assertEqual(channel.code, 200)
956+
self.assertEqual(len(channel.json_body["pushers"]), 1)
957+
self.assertEqual(
958+
channel.json_body["pushers"][0]["org.matrix.msc3881.device_id"],
959+
lookup_result.device_id,
960+
)

0 commit comments

Comments
 (0)