From fdec1b73396a68af5d30ca89db7a9ec014fa1a48 Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Wed, 11 Aug 2021 16:06:25 +0100 Subject: [PATCH 01/13] Remove pushers when deleting 3pid from account When a user deletes an email from their account it will now also remove all pushers for that email and that user (even if these pushers were created by a different client) --- synapse/handlers/auth.py | 3 ++ synapse/storage/databases/main/pusher.py | 47 ++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 22a855224188..72f4f95d79f7 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -1459,6 +1459,9 @@ async def delete_threepid( ) await self.store.user_delete_threepid(user_id, medium, address) + await self.store.delete_all_pushers_with_pushkey_and_user_id( + pushkey=address, user_id=user_id + ) return result async def hash(self, password: str) -> str: diff --git a/synapse/storage/databases/main/pusher.py b/synapse/storage/databases/main/pusher.py index b48fe086d4cc..6636c4abd7c4 100644 --- a/synapse/storage/databases/main/pusher.py +++ b/synapse/storage/databases/main/pusher.py @@ -478,6 +478,53 @@ def delete_pusher_txn(txn, stream_id): "delete_pusher", delete_pusher_txn, stream_id ) + async def delete_all_pushers_with_pushkey_and_user_id( + self, pushkey: str, user_id: str + ) -> None: + """Delete all pushers associated with an account with a certain pushkey.""" + + # We want to generate a row in `deleted_pushers` for each pusher we're + # deleting, so we fetch the list now so we can generate the appropriate + # number of stream IDs. + pushers = list( + await self.get_pushers_by({"user_name": user_id, "pushkey": pushkey}) + ) + + def delete_pushers_txn(txn, stream_ids): + # invalidate cache for get_if_user_has_pusher + self._invalidate_cache_and_stream( # type: ignore + txn, self.get_if_user_has_pusher, (user_id,) + ) + + # remove the pushers from the pushers table + self.db_pool.simple_delete_txn( + txn, + table="pushers", + keyvalues={"user_name": user_id, "pushkey": pushkey}, + ) + + # store a record of these deletions in the deleted_pushers table + self.db_pool.simple_insert_many_txn( + txn, + table="deleted_pushers", + values=[ + { + "stream_id": stream_id, + "app_id": pusher.app_id, + "pushkey": pushkey, + "user_id": user_id, + } + for stream_id, pusher in zip(stream_ids, pushers) + ], + ) + + async with self._pushers_id_gen.get_next_mult(len(pushers)) as stream_ids: + await self.db_pool.runInteraction( + "delete_all_pushers_with_pushkey_and_user_id", + delete_pushers_txn, + stream_ids, + ) + async def delete_all_pushers_for_user(self, user_id: str) -> None: """Delete all pushers associated with an account.""" From d92695242b23e6812e9d759ddd7ad02ca6d4b797 Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Wed, 11 Aug 2021 16:11:47 +0100 Subject: [PATCH 02/13] added changelog --- changelog.d/10581.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/10581.bugfix diff --git a/changelog.d/10581.bugfix b/changelog.d/10581.bugfix new file mode 100644 index 000000000000..cb5a8ee52867 --- /dev/null +++ b/changelog.d/10581.bugfix @@ -0,0 +1 @@ +Remove pushers when deleting 3pid from account. \ No newline at end of file From 1d42a1fb8f89966b00fda4095b06acd340866602 Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Thu, 12 Aug 2021 15:37:11 +0100 Subject: [PATCH 03/13] specify app_id = m.email when removing --- synapse/handlers/auth.py | 8 ++-- synapse/storage/databases/main/pusher.py | 47 ------------------------ 2 files changed, 4 insertions(+), 51 deletions(-) diff --git a/synapse/handlers/auth.py b/synapse/handlers/auth.py index 72f4f95d79f7..803783cb7966 100644 --- a/synapse/handlers/auth.py +++ b/synapse/handlers/auth.py @@ -1459,9 +1459,10 @@ async def delete_threepid( ) await self.store.user_delete_threepid(user_id, medium, address) - await self.store.delete_all_pushers_with_pushkey_and_user_id( - pushkey=address, user_id=user_id - ) + if medium == "email": + await self.store.delete_pusher_by_app_id_pushkey_user_id( + app_id="m.email", pushkey=address, user_id=user_id + ) return result async def hash(self, password: str) -> str: @@ -1730,7 +1731,6 @@ def add_query_param_to_url(url: str, param_name: str, param: Any): @attr.s(slots=True) class MacaroonGenerator: - hs = attr.ib() def generate_guest_access_token(self, user_id: str) -> str: diff --git a/synapse/storage/databases/main/pusher.py b/synapse/storage/databases/main/pusher.py index 6636c4abd7c4..b48fe086d4cc 100644 --- a/synapse/storage/databases/main/pusher.py +++ b/synapse/storage/databases/main/pusher.py @@ -478,53 +478,6 @@ def delete_pusher_txn(txn, stream_id): "delete_pusher", delete_pusher_txn, stream_id ) - async def delete_all_pushers_with_pushkey_and_user_id( - self, pushkey: str, user_id: str - ) -> None: - """Delete all pushers associated with an account with a certain pushkey.""" - - # We want to generate a row in `deleted_pushers` for each pusher we're - # deleting, so we fetch the list now so we can generate the appropriate - # number of stream IDs. - pushers = list( - await self.get_pushers_by({"user_name": user_id, "pushkey": pushkey}) - ) - - def delete_pushers_txn(txn, stream_ids): - # invalidate cache for get_if_user_has_pusher - self._invalidate_cache_and_stream( # type: ignore - txn, self.get_if_user_has_pusher, (user_id,) - ) - - # remove the pushers from the pushers table - self.db_pool.simple_delete_txn( - txn, - table="pushers", - keyvalues={"user_name": user_id, "pushkey": pushkey}, - ) - - # store a record of these deletions in the deleted_pushers table - self.db_pool.simple_insert_many_txn( - txn, - table="deleted_pushers", - values=[ - { - "stream_id": stream_id, - "app_id": pusher.app_id, - "pushkey": pushkey, - "user_id": user_id, - } - for stream_id, pusher in zip(stream_ids, pushers) - ], - ) - - async with self._pushers_id_gen.get_next_mult(len(pushers)) as stream_ids: - await self.db_pool.runInteraction( - "delete_all_pushers_with_pushkey_and_user_id", - delete_pushers_txn, - stream_ids, - ) - async def delete_all_pushers_for_user(self, user_id: str) -> None: """Delete all pushers associated with an account.""" From a64f31b83e792c6cc22ef0ea2831c076f0e72f2c Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Wed, 18 Aug 2021 10:21:41 +0100 Subject: [PATCH 04/13] Added background task that removes pushers --- synapse/storage/databases/main/pusher.py | 56 ++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/synapse/storage/databases/main/pusher.py b/synapse/storage/databases/main/pusher.py index b48fe086d4cc..11b0b741d738 100644 --- a/synapse/storage/databases/main/pusher.py +++ b/synapse/storage/databases/main/pusher.py @@ -48,6 +48,11 @@ def __init__(self, database: DatabasePool, db_conn: Connection, hs: "HomeServer" self._remove_stale_pushers, ) + self.db_pool.updates.register_background_update_handler( + "remove_delete_email_pushers", + self._remove_deleted_email_pushers, + ) + def _decode_pushers_rows(self, rows: Iterable[dict]) -> Iterator[PusherConfig]: """JSON-decode the data in the rows returned from the `pushers` table @@ -388,6 +393,57 @@ def _delete_pushers(txn) -> int: return number_deleted + async def _remove_deleted_email_pushers( + self, progress: dict, batch_size: int + ) -> int: + """A background update that deletes all pushers for email addresses no longer + associated with a user. + """ + + last_pusher = progress.get("last_pusher", 0) + + def _delete_pushers(txn) -> int: + + sql = """ + SELECT p.id, p.user_name, p.add_id, p.pushkey + FROM pushers AS p + JOIN user_threepids AS t ON t.user_id=p.user_name + WHERE p.app_id = 'm.email' + AND t.medium = 'email' + AND t.address = p.pushkey + AND p.id > ? + ORDER BY p.id ASC + LIMIT ? + """ + + txn.execute(sql, (last_pusher, batch_size)) + ids = [row[0] for row in txn] + + for row in txn: + self.db_pool.simple_delete_txn( + txn, + "pushers", + {"user_name": row[1], "app_id": row[2], "pushkey": row[3]}, + ) + + if ids: + self.db_pool.updates._background_update_progress_txn( + txn, "remove_deactivated_pushers", {"last_user": ids[-1]} + ) + + return len(ids) + + number_deleted = await self.db_pool.runInteraction( + "_remove_deleted_email_pushers", _delete_pushers + ) + + if number_deleted < batch_size: + await self.db_pool.updates._end_background_update( + "remove_deleted_email_pushers" + ) + + return number_deleted + class PusherStore(PusherWorkerStore): def get_pushers_stream_token(self) -> int: From bf31816677c6d2a5cbc7490dabb11633f992f043 Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Wed, 18 Aug 2021 10:25:49 +0100 Subject: [PATCH 05/13] Fixed mistake with wrong background update having progress updated --- synapse/storage/databases/main/pusher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/databases/main/pusher.py b/synapse/storage/databases/main/pusher.py index 11b0b741d738..b59752b50c3c 100644 --- a/synapse/storage/databases/main/pusher.py +++ b/synapse/storage/databases/main/pusher.py @@ -428,7 +428,7 @@ def _delete_pushers(txn) -> int: if ids: self.db_pool.updates._background_update_progress_txn( - txn, "remove_deactivated_pushers", {"last_user": ids[-1]} + txn, "remove_deleted_email_pushers", {"last_pusher": ids[-1]} ) return len(ids) From b6b506499bedb386b5e6317672f0da5dfadae455 Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Mon, 23 Aug 2021 10:02:55 +0100 Subject: [PATCH 06/13] Fixed typo (delete --> deleted) --- synapse/storage/databases/main/pusher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/synapse/storage/databases/main/pusher.py b/synapse/storage/databases/main/pusher.py index b59752b50c3c..b3a22324b1f6 100644 --- a/synapse/storage/databases/main/pusher.py +++ b/synapse/storage/databases/main/pusher.py @@ -49,7 +49,7 @@ def __init__(self, database: DatabasePool, db_conn: Connection, hs: "HomeServer" ) self.db_pool.updates.register_background_update_handler( - "remove_delete_email_pushers", + "remove_deleted_email_pushers", self._remove_deleted_email_pushers, ) From acf86093b0dbd8ab40f622fe9fd64376050e0b52 Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Mon, 23 Aug 2021 13:03:29 +0100 Subject: [PATCH 07/13] Fixed query --- synapse/storage/databases/main/pusher.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/synapse/storage/databases/main/pusher.py b/synapse/storage/databases/main/pusher.py index b3a22324b1f6..fdc2ea64e168 100644 --- a/synapse/storage/databases/main/pusher.py +++ b/synapse/storage/databases/main/pusher.py @@ -405,12 +405,14 @@ async def _remove_deleted_email_pushers( def _delete_pushers(txn) -> int: sql = """ - SELECT p.id, p.user_name, p.add_id, p.pushkey + SELECT p.id, p.user_name, p.app_id, p.pushkey FROM pushers AS p - JOIN user_threepids AS t ON t.user_id=p.user_name - WHERE p.app_id = 'm.email' - AND t.medium = 'email' - AND t.address = p.pushkey + LEFT JOIN user_threepids AS t + ON t.user_id=p.user_name + AND t.medium = 'email' + AND t.address = p.pushkey + WHERE t.user_id is NULL + AND p.app_id = 'm.email' AND p.id > ? ORDER BY p.id ASC LIMIT ? From c1967aeb45506cb1b54b943bd11593afe06c2ade Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Mon, 23 Aug 2021 13:11:07 +0100 Subject: [PATCH 08/13] Added docstring --- synapse/storage/databases/main/pusher.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/synapse/storage/databases/main/pusher.py b/synapse/storage/databases/main/pusher.py index fdc2ea64e168..53c386c3ec7b 100644 --- a/synapse/storage/databases/main/pusher.py +++ b/synapse/storage/databases/main/pusher.py @@ -396,8 +396,18 @@ def _delete_pushers(txn) -> int: async def _remove_deleted_email_pushers( self, progress: dict, batch_size: int ) -> int: - """A background update that deletes all pushers for email addresses no longer - associated with a user. + """A background update that deletes all pushers for deleted email addresses. + + In previous versions of synapse, when users deleted their email address, it didn't + also delete all the pushers for that email address. This background update removes + those to prevent unwanted emails + + Args: + progress: dict used to store progress of this background update + batch_size: the maximum number of rows to retrieve in a single select query + + Returns: + The number of deleted rows """ last_pusher = progress.get("last_pusher", 0) @@ -407,7 +417,7 @@ def _delete_pushers(txn) -> int: sql = """ SELECT p.id, p.user_name, p.app_id, p.pushkey FROM pushers AS p - LEFT JOIN user_threepids AS t + LEFT JOIN user_threepids AS t ON t.user_id=p.user_name AND t.medium = 'email' AND t.address = p.pushkey From dbe079c9bb3e462e015281217396b860f68b7759 Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Mon, 23 Aug 2021 13:18:11 +0100 Subject: [PATCH 09/13] Updated changelog, added entry in CHANGES.ms and docs/upgrade.md --- CHANGES.md | 2 ++ changelog.d/10581.bugfix | 2 +- docs/upgrade.md | 5 +++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 01766af39cc6..1763b7563cb6 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,5 @@ +Users will stop receiving message updates via email for addresses that were once, but not still, linked to their account + Synapse 1.41.0rc1 (2021-08-18) ============================== diff --git a/changelog.d/10581.bugfix b/changelog.d/10581.bugfix index cb5a8ee52867..1dc0e339731c 100644 --- a/changelog.d/10581.bugfix +++ b/changelog.d/10581.bugfix @@ -1 +1 @@ -Remove pushers when deleting 3pid from account. \ No newline at end of file +Remove pushers when deleting 3pid from account. Pushers for old unlinked emails will also be deleted. \ No newline at end of file diff --git a/docs/upgrade.md b/docs/upgrade.md index 6d4b8cb48edb..dcf0a7db5bf6 100644 --- a/docs/upgrade.md +++ b/docs/upgrade.md @@ -107,6 +107,11 @@ This may affect you if you make use of custom HTML templates for the The template is now provided an `error` variable if the authentication process failed. See the default templates linked above for an example. +# Upgrading to v1.42.0 + +## Removal of out-of-date email pushers +Users will stop receiving message updates via email for addresses that were +once, but not still, linked to their account. # Upgrading to v1.41.0 From 0490a4be20b307e407c69b74d8f3598a7dd2b611 Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Mon, 23 Aug 2021 15:06:46 +0100 Subject: [PATCH 10/13] Added schema update so that the job gets run. Also, only iterate through resultse once (or it doesn't work!) --- synapse/storage/databases/main/pusher.py | 11 ++++++---- .../63/02delete_unlinked_email_pushers.sql | 20 +++++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 synapse/storage/schema/main/delta/63/02delete_unlinked_email_pushers.sql diff --git a/synapse/storage/databases/main/pusher.py b/synapse/storage/databases/main/pusher.py index 53c386c3ec7b..484089368767 100644 --- a/synapse/storage/databases/main/pusher.py +++ b/synapse/storage/databases/main/pusher.py @@ -429,21 +429,24 @@ def _delete_pushers(txn) -> int: """ txn.execute(sql, (last_pusher, batch_size)) - ids = [row[0] for row in txn] + last = None + num_deleted = 0 for row in txn: + last = row[0] + num_deleted += 1 self.db_pool.simple_delete_txn( txn, "pushers", {"user_name": row[1], "app_id": row[2], "pushkey": row[3]}, ) - if ids: + if last is not None: self.db_pool.updates._background_update_progress_txn( - txn, "remove_deleted_email_pushers", {"last_pusher": ids[-1]} + txn, "remove_deleted_email_pushers", {"last_pusher": last} ) - return len(ids) + return num_deleted number_deleted = await self.db_pool.runInteraction( "_remove_deleted_email_pushers", _delete_pushers diff --git a/synapse/storage/schema/main/delta/63/02delete_unlinked_email_pushers.sql b/synapse/storage/schema/main/delta/63/02delete_unlinked_email_pushers.sql new file mode 100644 index 000000000000..611c4b95cf15 --- /dev/null +++ b/synapse/storage/schema/main/delta/63/02delete_unlinked_email_pushers.sql @@ -0,0 +1,20 @@ +/* Copyright 2021 The Matrix.org Foundation C.I.C + * + * 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. + */ + + +-- We may not have deleted all pushers for emails that are no longer linked +-- to an account, so we set up a background job to delete them. +INSERT INTO background_updates (ordering, update_name, progress_json) VALUES + (6302, 'remove_deleted_email_pushers', '{}'); From ee7cebe20a56704cb2c2c2c9164416a25f92437d Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Wed, 25 Aug 2021 15:54:21 +0100 Subject: [PATCH 11/13] Applied suggestions from code review --- CHANGES.md | 2 +- changelog.d/10581.bugfix | 2 +- synapse/storage/databases/main/pusher.py | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 1763b7563cb6..3e2e80275c7f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -Users will stop receiving message updates via email for addresses that were once, but not still, linked to their account +Users will stop receiving message updates via email for addresses that were previously linked to their account Synapse 1.41.0rc1 (2021-08-18) ============================== diff --git a/changelog.d/10581.bugfix b/changelog.d/10581.bugfix index 1dc0e339731c..15c7da449734 100644 --- a/changelog.d/10581.bugfix +++ b/changelog.d/10581.bugfix @@ -1 +1 @@ -Remove pushers when deleting 3pid from account. Pushers for old unlinked emails will also be deleted. \ No newline at end of file +Remove pushers when deleting a 3pid from an account. Pushers for old unlinked emails will also be deleted. \ No newline at end of file diff --git a/synapse/storage/databases/main/pusher.py b/synapse/storage/databases/main/pusher.py index 484089368767..e47caa212549 100644 --- a/synapse/storage/databases/main/pusher.py +++ b/synapse/storage/databases/main/pusher.py @@ -400,7 +400,8 @@ async def _remove_deleted_email_pushers( In previous versions of synapse, when users deleted their email address, it didn't also delete all the pushers for that email address. This background update removes - those to prevent unwanted emails + those to prevent unwanted emails. This should only need to be run once (when users + upgrade to v1.42.0 Args: progress: dict used to store progress of this background update @@ -418,7 +419,7 @@ def _delete_pushers(txn) -> int: SELECT p.id, p.user_name, p.app_id, p.pushkey FROM pushers AS p LEFT JOIN user_threepids AS t - ON t.user_id=p.user_name + ON t.user_id = p.user_name AND t.medium = 'email' AND t.address = p.pushkey WHERE t.user_id is NULL From be4e18ea4bd697b0297f0a8be5215d264abbe6b6 Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Wed, 25 Aug 2021 16:18:56 +0100 Subject: [PATCH 12/13] Wrote test for pusher removal after email deleted --- tests/push/test_email.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/push/test_email.py b/tests/push/test_email.py index e0a3342088d4..4da20aeda56c 100644 --- a/tests/push/test_email.py +++ b/tests/push/test_email.py @@ -125,6 +125,8 @@ def prepare(self, reactor, clock, hs): ) ) + self.auth_handler = hs.get_auth_handler() + def test_need_validated_email(self): """Test that we can only add an email pusher if the user has validated their email. @@ -305,6 +307,36 @@ def test_encrypted_message(self): # We should get emailed about that message self._check_for_mail() + def test_no_email_sent_after_removed(self): + # Create a simple room with two users + room = self.helper.create_room_as(self.user_id, tok=self.access_token) + self.helper.invite( + room=room, src=self.user_id, tok=self.access_token, targ=self.others[0].id + ) + self.helper.join(room=room, user=self.others[0].id, tok=self.others[0].token) + + # The other user sends a single message. + self.helper.send(room, body="Hi!", tok=self.others[0].token) + + # We should get emailed about that message + self._check_for_mail() + + # disassociate the user's email address + self.get_success( + self.auth_handler.delete_threepid( + user_id=self.user_id, + medium="email", + address="a@example.com", + ) + ) + + # check that the pusher for that email address has been deleted + pushers = self.get_success( + self.hs.get_datastore().get_pushers_by({"user_name": self.user_id}) + ) + pushers = list(pushers) + self.assertEqual(len(pushers), 0) + def _check_for_mail(self): """Check that the user receives an email notification""" From 5ed240aa1a3f58ca2881b8f38b81e47b6e7904fb Mon Sep 17 00:00:00 2001 From: Azrenbeth <7782548+Azrenbeth@users.noreply.github.com> Date: Thu, 26 Aug 2021 13:25:38 +0100 Subject: [PATCH 13/13] split arguments over multiple lines --- tests/push/test_email.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/push/test_email.py b/tests/push/test_email.py index 4da20aeda56c..eea07485a017 100644 --- a/tests/push/test_email.py +++ b/tests/push/test_email.py @@ -311,9 +311,16 @@ def test_no_email_sent_after_removed(self): # Create a simple room with two users room = self.helper.create_room_as(self.user_id, tok=self.access_token) self.helper.invite( - room=room, src=self.user_id, tok=self.access_token, targ=self.others[0].id + room=room, + src=self.user_id, + tok=self.access_token, + targ=self.others[0].id, + ) + self.helper.join( + room=room, + user=self.others[0].id, + tok=self.others[0].token, ) - self.helper.join(room=room, user=self.others[0].id, tok=self.others[0].token) # The other user sends a single message. self.helper.send(room, body="Hi!", tok=self.others[0].token)