mirror of
https://github.com/element-hq/synapse.git
synced 2026-08-28 19:18:18 +00:00
Fix tests on postgresql (#3740)
This commit is contained in:
@@ -37,18 +37,14 @@ class ApplicationServiceStoreTestCase(unittest.TestCase):
|
||||
@defer.inlineCallbacks
|
||||
def setUp(self):
|
||||
self.as_yaml_files = []
|
||||
config = Mock(
|
||||
app_service_config_files=self.as_yaml_files,
|
||||
event_cache_size=1,
|
||||
password_providers=[],
|
||||
)
|
||||
hs = yield setup_test_homeserver(
|
||||
self.addCleanup,
|
||||
config=config,
|
||||
federation_sender=Mock(),
|
||||
federation_client=Mock(),
|
||||
self.addCleanup, federation_sender=Mock(), federation_client=Mock()
|
||||
)
|
||||
|
||||
hs.config.app_service_config_files = self.as_yaml_files
|
||||
hs.config.event_cache_size = 1
|
||||
hs.config.password_providers = []
|
||||
|
||||
self.as_token = "token1"
|
||||
self.as_url = "some_url"
|
||||
self.as_id = "as1"
|
||||
@@ -58,7 +54,7 @@ class ApplicationServiceStoreTestCase(unittest.TestCase):
|
||||
self._add_appservice("token2", "as2", "some_url", "some_hs_token", "bob")
|
||||
self._add_appservice("token3", "as3", "some_url", "some_hs_token", "bob")
|
||||
# must be done after inserts
|
||||
self.store = ApplicationServiceStore(None, hs)
|
||||
self.store = ApplicationServiceStore(hs.get_db_conn(), hs)
|
||||
|
||||
def tearDown(self):
|
||||
# TODO: suboptimal that we need to create files for tests!
|
||||
@@ -105,18 +101,16 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.as_yaml_files = []
|
||||
|
||||
config = Mock(
|
||||
app_service_config_files=self.as_yaml_files,
|
||||
event_cache_size=1,
|
||||
password_providers=[],
|
||||
)
|
||||
hs = yield setup_test_homeserver(
|
||||
self.addCleanup,
|
||||
config=config,
|
||||
federation_sender=Mock(),
|
||||
federation_client=Mock(),
|
||||
self.addCleanup, federation_sender=Mock(), federation_client=Mock()
|
||||
)
|
||||
|
||||
hs.config.app_service_config_files = self.as_yaml_files
|
||||
hs.config.event_cache_size = 1
|
||||
hs.config.password_providers = []
|
||||
|
||||
self.db_pool = hs.get_db_pool()
|
||||
self.engine = hs.database_engine
|
||||
|
||||
self.as_list = [
|
||||
{"token": "token1", "url": "https://matrix-as.org", "id": "id_1"},
|
||||
@@ -129,7 +123,7 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase):
|
||||
|
||||
self.as_yaml_files = []
|
||||
|
||||
self.store = TestTransactionStore(None, hs)
|
||||
self.store = TestTransactionStore(hs.get_db_conn(), hs)
|
||||
|
||||
def _add_service(self, url, as_token, id):
|
||||
as_yaml = dict(
|
||||
@@ -146,29 +140,35 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase):
|
||||
self.as_yaml_files.append(as_token)
|
||||
|
||||
def _set_state(self, id, state, txn=None):
|
||||
return self.db_pool.runQuery(
|
||||
"INSERT INTO application_services_state(as_id, state, last_txn) "
|
||||
"VALUES(?,?,?)",
|
||||
return self.db_pool.runOperation(
|
||||
self.engine.convert_param_style(
|
||||
"INSERT INTO application_services_state(as_id, state, last_txn) "
|
||||
"VALUES(?,?,?)"
|
||||
),
|
||||
(id, state, txn),
|
||||
)
|
||||
|
||||
def _insert_txn(self, as_id, txn_id, events):
|
||||
return self.db_pool.runQuery(
|
||||
"INSERT INTO application_services_txns(as_id, txn_id, event_ids) "
|
||||
"VALUES(?,?,?)",
|
||||
return self.db_pool.runOperation(
|
||||
self.engine.convert_param_style(
|
||||
"INSERT INTO application_services_txns(as_id, txn_id, event_ids) "
|
||||
"VALUES(?,?,?)"
|
||||
),
|
||||
(as_id, txn_id, json.dumps([e.event_id for e in events])),
|
||||
)
|
||||
|
||||
def _set_last_txn(self, as_id, txn_id):
|
||||
return self.db_pool.runQuery(
|
||||
"INSERT INTO application_services_state(as_id, last_txn, state) "
|
||||
"VALUES(?,?,?)",
|
||||
return self.db_pool.runOperation(
|
||||
self.engine.convert_param_style(
|
||||
"INSERT INTO application_services_state(as_id, last_txn, state) "
|
||||
"VALUES(?,?,?)"
|
||||
),
|
||||
(as_id, txn_id, ApplicationServiceState.UP),
|
||||
)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_get_appservice_state_none(self):
|
||||
service = Mock(id=999)
|
||||
service = Mock(id="999")
|
||||
state = yield self.store.get_appservice_state(service)
|
||||
self.assertEquals(None, state)
|
||||
|
||||
@@ -200,7 +200,9 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase):
|
||||
service = Mock(id=self.as_list[1]["id"])
|
||||
yield self.store.set_appservice_state(service, ApplicationServiceState.DOWN)
|
||||
rows = yield self.db_pool.runQuery(
|
||||
"SELECT as_id FROM application_services_state WHERE state=?",
|
||||
self.engine.convert_param_style(
|
||||
"SELECT as_id FROM application_services_state WHERE state=?"
|
||||
),
|
||||
(ApplicationServiceState.DOWN,),
|
||||
)
|
||||
self.assertEquals(service.id, rows[0][0])
|
||||
@@ -212,7 +214,9 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase):
|
||||
yield self.store.set_appservice_state(service, ApplicationServiceState.DOWN)
|
||||
yield self.store.set_appservice_state(service, ApplicationServiceState.UP)
|
||||
rows = yield self.db_pool.runQuery(
|
||||
"SELECT as_id FROM application_services_state WHERE state=?",
|
||||
self.engine.convert_param_style(
|
||||
"SELECT as_id FROM application_services_state WHERE state=?"
|
||||
),
|
||||
(ApplicationServiceState.UP,),
|
||||
)
|
||||
self.assertEquals(service.id, rows[0][0])
|
||||
@@ -279,14 +283,19 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase):
|
||||
yield self.store.complete_appservice_txn(txn_id=txn_id, service=service)
|
||||
|
||||
res = yield self.db_pool.runQuery(
|
||||
"SELECT last_txn FROM application_services_state WHERE as_id=?",
|
||||
self.engine.convert_param_style(
|
||||
"SELECT last_txn FROM application_services_state WHERE as_id=?"
|
||||
),
|
||||
(service.id,),
|
||||
)
|
||||
self.assertEquals(1, len(res))
|
||||
self.assertEquals(txn_id, res[0][0])
|
||||
|
||||
res = yield self.db_pool.runQuery(
|
||||
"SELECT * FROM application_services_txns WHERE txn_id=?", (txn_id,)
|
||||
self.engine.convert_param_style(
|
||||
"SELECT * FROM application_services_txns WHERE txn_id=?"
|
||||
),
|
||||
(txn_id,),
|
||||
)
|
||||
self.assertEquals(0, len(res))
|
||||
|
||||
@@ -300,7 +309,9 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase):
|
||||
yield self.store.complete_appservice_txn(txn_id=txn_id, service=service)
|
||||
|
||||
res = yield self.db_pool.runQuery(
|
||||
"SELECT last_txn, state FROM application_services_state WHERE " "as_id=?",
|
||||
self.engine.convert_param_style(
|
||||
"SELECT last_txn, state FROM application_services_state WHERE as_id=?"
|
||||
),
|
||||
(service.id,),
|
||||
)
|
||||
self.assertEquals(1, len(res))
|
||||
@@ -308,7 +319,10 @@ class ApplicationServiceTransactionStoreTestCase(unittest.TestCase):
|
||||
self.assertEquals(ApplicationServiceState.UP, res[0][1])
|
||||
|
||||
res = yield self.db_pool.runQuery(
|
||||
"SELECT * FROM application_services_txns WHERE txn_id=?", (txn_id,)
|
||||
self.engine.convert_param_style(
|
||||
"SELECT * FROM application_services_txns WHERE txn_id=?"
|
||||
),
|
||||
(txn_id,),
|
||||
)
|
||||
self.assertEquals(0, len(res))
|
||||
|
||||
@@ -394,37 +408,31 @@ class ApplicationServiceStoreConfigTestCase(unittest.TestCase):
|
||||
f1 = self._write_config(suffix="1")
|
||||
f2 = self._write_config(suffix="2")
|
||||
|
||||
config = Mock(
|
||||
app_service_config_files=[f1, f2], event_cache_size=1, password_providers=[]
|
||||
)
|
||||
hs = yield setup_test_homeserver(
|
||||
self.addCleanup,
|
||||
config=config,
|
||||
datastore=Mock(),
|
||||
federation_sender=Mock(),
|
||||
federation_client=Mock(),
|
||||
self.addCleanup, federation_sender=Mock(), federation_client=Mock()
|
||||
)
|
||||
|
||||
ApplicationServiceStore(None, hs)
|
||||
hs.config.app_service_config_files = [f1, f2]
|
||||
hs.config.event_cache_size = 1
|
||||
hs.config.password_providers = []
|
||||
|
||||
ApplicationServiceStore(hs.get_db_conn(), hs)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_duplicate_ids(self):
|
||||
f1 = self._write_config(id="id", suffix="1")
|
||||
f2 = self._write_config(id="id", suffix="2")
|
||||
|
||||
config = Mock(
|
||||
app_service_config_files=[f1, f2], event_cache_size=1, password_providers=[]
|
||||
)
|
||||
hs = yield setup_test_homeserver(
|
||||
self.addCleanup,
|
||||
config=config,
|
||||
datastore=Mock(),
|
||||
federation_sender=Mock(),
|
||||
federation_client=Mock(),
|
||||
self.addCleanup, federation_sender=Mock(), federation_client=Mock()
|
||||
)
|
||||
|
||||
hs.config.app_service_config_files = [f1, f2]
|
||||
hs.config.event_cache_size = 1
|
||||
hs.config.password_providers = []
|
||||
|
||||
with self.assertRaises(ConfigError) as cm:
|
||||
ApplicationServiceStore(None, hs)
|
||||
ApplicationServiceStore(hs.get_db_conn(), hs)
|
||||
|
||||
e = cm.exception
|
||||
self.assertIn(f1, str(e))
|
||||
@@ -436,19 +444,16 @@ class ApplicationServiceStoreConfigTestCase(unittest.TestCase):
|
||||
f1 = self._write_config(as_token="as_token", suffix="1")
|
||||
f2 = self._write_config(as_token="as_token", suffix="2")
|
||||
|
||||
config = Mock(
|
||||
app_service_config_files=[f1, f2], event_cache_size=1, password_providers=[]
|
||||
)
|
||||
hs = yield setup_test_homeserver(
|
||||
self.addCleanup,
|
||||
config=config,
|
||||
datastore=Mock(),
|
||||
federation_sender=Mock(),
|
||||
federation_client=Mock(),
|
||||
self.addCleanup, federation_sender=Mock(), federation_client=Mock()
|
||||
)
|
||||
|
||||
hs.config.app_service_config_files = [f1, f2]
|
||||
hs.config.event_cache_size = 1
|
||||
hs.config.password_providers = []
|
||||
|
||||
with self.assertRaises(ConfigError) as cm:
|
||||
ApplicationServiceStore(None, hs)
|
||||
ApplicationServiceStore(hs.get_db_conn(), hs)
|
||||
|
||||
e = cm.exception
|
||||
self.assertIn(f1, str(e))
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.storage.directory import DirectoryStore
|
||||
from synapse.types import RoomAlias, RoomID
|
||||
|
||||
from tests import unittest
|
||||
@@ -28,7 +27,7 @@ class DirectoryStoreTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
hs = yield setup_test_homeserver(self.addCleanup)
|
||||
|
||||
self.store = DirectoryStore(None, hs)
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
self.room = RoomID.from_string("!abcde:test")
|
||||
self.alias = RoomAlias.from_string("#my-room:test")
|
||||
|
||||
@@ -37,10 +37,10 @@ class EventFederationWorkerStoreTestCase(tests.unittest.TestCase):
|
||||
(
|
||||
"INSERT INTO events ("
|
||||
" room_id, event_id, type, depth, topological_ordering,"
|
||||
" content, processed, outlier) "
|
||||
"VALUES (?, ?, 'm.test', ?, ?, 'test', ?, ?)"
|
||||
" content, processed, outlier, stream_ordering) "
|
||||
"VALUES (?, ?, 'm.test', ?, ?, 'test', ?, ?, ?)"
|
||||
),
|
||||
(room_id, event_id, i, i, True, False),
|
||||
(room_id, event_id, i, i, True, False, i),
|
||||
)
|
||||
|
||||
txn.execute(
|
||||
|
||||
@@ -13,25 +13,22 @@
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
import tests.unittest
|
||||
import tests.utils
|
||||
from tests.utils import setup_test_homeserver
|
||||
from tests.unittest import HomeserverTestCase
|
||||
|
||||
FORTY_DAYS = 40 * 24 * 60 * 60
|
||||
|
||||
|
||||
class MonthlyActiveUsersTestCase(tests.unittest.TestCase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(MonthlyActiveUsersTestCase, self).__init__(*args, **kwargs)
|
||||
class MonthlyActiveUsersTestCase(HomeserverTestCase):
|
||||
def make_homeserver(self, reactor, clock):
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def setUp(self):
|
||||
self.hs = yield setup_test_homeserver(self.addCleanup)
|
||||
self.store = self.hs.get_datastore()
|
||||
hs = self.setup_test_homeserver()
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
# Advance the clock a bit
|
||||
reactor.advance(FORTY_DAYS)
|
||||
|
||||
return hs
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_initialise_reserved_users(self):
|
||||
self.hs.config.max_mau_value = 5
|
||||
user1 = "@user1:server"
|
||||
@@ -44,88 +41,101 @@ class MonthlyActiveUsersTestCase(tests.unittest.TestCase):
|
||||
]
|
||||
user_num = len(threepids)
|
||||
|
||||
yield self.store.register(user_id=user1, token="123", password_hash=None)
|
||||
|
||||
yield self.store.register(user_id=user2, token="456", password_hash=None)
|
||||
self.store.register(user_id=user1, token="123", password_hash=None)
|
||||
self.store.register(user_id=user2, token="456", password_hash=None)
|
||||
self.pump()
|
||||
|
||||
now = int(self.hs.get_clock().time_msec())
|
||||
yield self.store.user_add_threepid(user1, "email", user1_email, now, now)
|
||||
yield self.store.user_add_threepid(user2, "email", user2_email, now, now)
|
||||
yield self.store.initialise_reserved_users(threepids)
|
||||
self.store.user_add_threepid(user1, "email", user1_email, now, now)
|
||||
self.store.user_add_threepid(user2, "email", user2_email, now, now)
|
||||
self.store.initialise_reserved_users(threepids)
|
||||
self.pump()
|
||||
|
||||
active_count = yield self.store.get_monthly_active_count()
|
||||
active_count = self.store.get_monthly_active_count()
|
||||
|
||||
# Test total counts
|
||||
self.assertEquals(active_count, user_num)
|
||||
self.assertEquals(self.get_success(active_count), user_num)
|
||||
|
||||
# Test user is marked as active
|
||||
|
||||
timestamp = yield self.store.user_last_seen_monthly_active(user1)
|
||||
self.assertTrue(timestamp)
|
||||
timestamp = yield self.store.user_last_seen_monthly_active(user2)
|
||||
self.assertTrue(timestamp)
|
||||
timestamp = self.store.user_last_seen_monthly_active(user1)
|
||||
self.assertTrue(self.get_success(timestamp))
|
||||
timestamp = self.store.user_last_seen_monthly_active(user2)
|
||||
self.assertTrue(self.get_success(timestamp))
|
||||
|
||||
# Test that users are never removed from the db.
|
||||
self.hs.config.max_mau_value = 0
|
||||
|
||||
self.hs.get_clock().advance_time(FORTY_DAYS)
|
||||
self.reactor.advance(FORTY_DAYS)
|
||||
|
||||
yield self.store.reap_monthly_active_users()
|
||||
self.store.reap_monthly_active_users()
|
||||
self.pump()
|
||||
|
||||
active_count = yield self.store.get_monthly_active_count()
|
||||
self.assertEquals(active_count, user_num)
|
||||
active_count = self.store.get_monthly_active_count()
|
||||
self.assertEquals(self.get_success(active_count), user_num)
|
||||
|
||||
# Test that regalar users are removed from the db
|
||||
ru_count = 2
|
||||
yield self.store.upsert_monthly_active_user("@ru1:server")
|
||||
yield self.store.upsert_monthly_active_user("@ru2:server")
|
||||
active_count = yield self.store.get_monthly_active_count()
|
||||
self.store.upsert_monthly_active_user("@ru1:server")
|
||||
self.store.upsert_monthly_active_user("@ru2:server")
|
||||
self.pump()
|
||||
|
||||
self.assertEqual(active_count, user_num + ru_count)
|
||||
active_count = self.store.get_monthly_active_count()
|
||||
self.assertEqual(self.get_success(active_count), user_num + ru_count)
|
||||
self.hs.config.max_mau_value = user_num
|
||||
yield self.store.reap_monthly_active_users()
|
||||
self.store.reap_monthly_active_users()
|
||||
self.pump()
|
||||
|
||||
active_count = yield self.store.get_monthly_active_count()
|
||||
self.assertEquals(active_count, user_num)
|
||||
active_count = self.store.get_monthly_active_count()
|
||||
self.assertEquals(self.get_success(active_count), user_num)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_can_insert_and_count_mau(self):
|
||||
count = yield self.store.get_monthly_active_count()
|
||||
self.assertEqual(0, count)
|
||||
count = self.store.get_monthly_active_count()
|
||||
self.assertEqual(0, self.get_success(count))
|
||||
|
||||
yield self.store.upsert_monthly_active_user("@user:server")
|
||||
count = yield self.store.get_monthly_active_count()
|
||||
self.store.upsert_monthly_active_user("@user:server")
|
||||
self.pump()
|
||||
|
||||
self.assertEqual(1, count)
|
||||
count = self.store.get_monthly_active_count()
|
||||
self.assertEqual(1, self.get_success(count))
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_user_last_seen_monthly_active(self):
|
||||
user_id1 = "@user1:server"
|
||||
user_id2 = "@user2:server"
|
||||
user_id3 = "@user3:server"
|
||||
|
||||
result = yield self.store.user_last_seen_monthly_active(user_id1)
|
||||
self.assertFalse(result == 0)
|
||||
yield self.store.upsert_monthly_active_user(user_id1)
|
||||
yield self.store.upsert_monthly_active_user(user_id2)
|
||||
result = yield self.store.user_last_seen_monthly_active(user_id1)
|
||||
self.assertTrue(result > 0)
|
||||
result = yield self.store.user_last_seen_monthly_active(user_id3)
|
||||
self.assertFalse(result == 0)
|
||||
result = self.store.user_last_seen_monthly_active(user_id1)
|
||||
self.assertFalse(self.get_success(result) == 0)
|
||||
|
||||
self.store.upsert_monthly_active_user(user_id1)
|
||||
self.store.upsert_monthly_active_user(user_id2)
|
||||
self.pump()
|
||||
|
||||
result = self.store.user_last_seen_monthly_active(user_id1)
|
||||
self.assertGreater(self.get_success(result), 0)
|
||||
|
||||
result = self.store.user_last_seen_monthly_active(user_id3)
|
||||
self.assertNotEqual(self.get_success(result), 0)
|
||||
|
||||
@defer.inlineCallbacks
|
||||
def test_reap_monthly_active_users(self):
|
||||
self.hs.config.max_mau_value = 5
|
||||
initial_users = 10
|
||||
for i in range(initial_users):
|
||||
yield self.store.upsert_monthly_active_user("@user%d:server" % i)
|
||||
count = yield self.store.get_monthly_active_count()
|
||||
self.assertTrue(count, initial_users)
|
||||
yield self.store.reap_monthly_active_users()
|
||||
count = yield self.store.get_monthly_active_count()
|
||||
self.assertEquals(count, initial_users - self.hs.config.max_mau_value)
|
||||
self.store.upsert_monthly_active_user("@user%d:server" % i)
|
||||
self.pump()
|
||||
|
||||
self.hs.get_clock().advance_time(FORTY_DAYS)
|
||||
yield self.store.reap_monthly_active_users()
|
||||
count = yield self.store.get_monthly_active_count()
|
||||
self.assertEquals(count, 0)
|
||||
count = self.store.get_monthly_active_count()
|
||||
self.assertTrue(self.get_success(count), initial_users)
|
||||
|
||||
self.store.reap_monthly_active_users()
|
||||
self.pump()
|
||||
count = self.store.get_monthly_active_count()
|
||||
self.assertEquals(
|
||||
self.get_success(count), initial_users - self.hs.config.max_mau_value
|
||||
)
|
||||
|
||||
self.reactor.advance(FORTY_DAYS)
|
||||
self.store.reap_monthly_active_users()
|
||||
self.pump()
|
||||
|
||||
count = self.store.get_monthly_active_count()
|
||||
self.assertEquals(self.get_success(count), 0)
|
||||
|
||||
@@ -16,19 +16,18 @@
|
||||
|
||||
from twisted.internet import defer
|
||||
|
||||
from synapse.storage.presence import PresenceStore
|
||||
from synapse.types import UserID
|
||||
|
||||
from tests import unittest
|
||||
from tests.utils import MockClock, setup_test_homeserver
|
||||
from tests.utils import setup_test_homeserver
|
||||
|
||||
|
||||
class PresenceStoreTestCase(unittest.TestCase):
|
||||
@defer.inlineCallbacks
|
||||
def setUp(self):
|
||||
hs = yield setup_test_homeserver(self.addCleanup, clock=MockClock())
|
||||
hs = yield setup_test_homeserver(self.addCleanup)
|
||||
|
||||
self.store = PresenceStore(None, hs)
|
||||
self.store = hs.get_datastore()
|
||||
|
||||
self.u_apple = UserID.from_string("@apple:test")
|
||||
self.u_banana = UserID.from_string("@banana:test")
|
||||
|
||||
@@ -28,7 +28,7 @@ class ProfileStoreTestCase(unittest.TestCase):
|
||||
def setUp(self):
|
||||
hs = yield setup_test_homeserver(self.addCleanup)
|
||||
|
||||
self.store = ProfileStore(None, hs)
|
||||
self.store = ProfileStore(hs.get_db_conn(), hs)
|
||||
|
||||
self.u_frank = UserID.from_string("@frank:test")
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class UserDirectoryStoreTestCase(unittest.TestCase):
|
||||
@defer.inlineCallbacks
|
||||
def setUp(self):
|
||||
self.hs = yield setup_test_homeserver(self.addCleanup)
|
||||
self.store = UserDirectoryStore(None, self.hs)
|
||||
self.store = UserDirectoryStore(self.hs.get_db_conn(), self.hs)
|
||||
|
||||
# alice and bob are both in !room_id. bobby is not but shares
|
||||
# a homeserver with alice.
|
||||
|
||||
Reference in New Issue
Block a user