forked from MirrorHub/synapse
Add creation_ts
to list users admin API (#10448)
Signed-off-by: Dirk Klimpel dirk@klimpel.org
This commit is contained in:
parent
38b346a504
commit
89c4ca81bb
6 changed files with 46 additions and 33 deletions
1
changelog.d/10448.feature
Normal file
1
changelog.d/10448.feature
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Add `creation_ts` to list users admin API.
|
|
@ -144,7 +144,8 @@ A response body like the following is returned:
|
||||||
"deactivated": 0,
|
"deactivated": 0,
|
||||||
"shadow_banned": 0,
|
"shadow_banned": 0,
|
||||||
"displayname": "<User One>",
|
"displayname": "<User One>",
|
||||||
"avatar_url": null
|
"avatar_url": null,
|
||||||
|
"creation_ts": 1560432668000
|
||||||
}, {
|
}, {
|
||||||
"name": "<user_id2>",
|
"name": "<user_id2>",
|
||||||
"is_guest": 0,
|
"is_guest": 0,
|
||||||
|
@ -153,7 +154,8 @@ A response body like the following is returned:
|
||||||
"deactivated": 0,
|
"deactivated": 0,
|
||||||
"shadow_banned": 0,
|
"shadow_banned": 0,
|
||||||
"displayname": "<User Two>",
|
"displayname": "<User Two>",
|
||||||
"avatar_url": "<avatar_url>"
|
"avatar_url": "<avatar_url>",
|
||||||
|
"creation_ts": 1561550621000
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"next_token": "100",
|
"next_token": "100",
|
||||||
|
@ -197,11 +199,12 @@ The following parameters should be set in the URL:
|
||||||
- `shadow_banned` - Users are ordered by `shadow_banned` status.
|
- `shadow_banned` - Users are ordered by `shadow_banned` status.
|
||||||
- `displayname` - Users are ordered alphabetically by `displayname`.
|
- `displayname` - Users are ordered alphabetically by `displayname`.
|
||||||
- `avatar_url` - Users are ordered alphabetically by avatar URL.
|
- `avatar_url` - Users are ordered alphabetically by avatar URL.
|
||||||
|
- `creation_ts` - Users are ordered by when the users was created in ms.
|
||||||
|
|
||||||
- `dir` - Direction of media order. Either `f` for forwards or `b` for backwards.
|
- `dir` - Direction of media order. Either `f` for forwards or `b` for backwards.
|
||||||
Setting this value to `b` will reverse the above sort order. Defaults to `f`.
|
Setting this value to `b` will reverse the above sort order. Defaults to `f`.
|
||||||
|
|
||||||
Caution. The database only has indexes on the columns `name` and `created_ts`.
|
Caution. The database only has indexes on the columns `name` and `creation_ts`.
|
||||||
This means that if a different sort order is used (`is_guest`, `admin`,
|
This means that if a different sort order is used (`is_guest`, `admin`,
|
||||||
`user_type`, `deactivated`, `shadow_banned`, `avatar_url` or `displayname`),
|
`user_type`, `deactivated`, `shadow_banned`, `avatar_url` or `displayname`),
|
||||||
this can cause a large load on the database, especially for large environments.
|
this can cause a large load on the database, especially for large environments.
|
||||||
|
@ -222,6 +225,7 @@ The following fields are returned in the JSON response body:
|
||||||
- `shadow_banned` - bool - Status if that user has been marked as shadow banned.
|
- `shadow_banned` - bool - Status if that user has been marked as shadow banned.
|
||||||
- `displayname` - string - The user's display name if they have set one.
|
- `displayname` - string - The user's display name if they have set one.
|
||||||
- `avatar_url` - string - The user's avatar URL if they have set one.
|
- `avatar_url` - string - The user's avatar URL if they have set one.
|
||||||
|
- `creation_ts` - integer - The user's creation timestamp in ms.
|
||||||
|
|
||||||
- `next_token`: string representing a positive integer - Indication for pagination. See above.
|
- `next_token`: string representing a positive integer - Indication for pagination. See above.
|
||||||
- `total` - integer - Total number of media.
|
- `total` - integer - Total number of media.
|
||||||
|
|
|
@ -62,6 +62,7 @@ class UsersRestServletV2(RestServlet):
|
||||||
The parameter `name` can be used to filter by user id or display name.
|
The parameter `name` can be used to filter by user id or display name.
|
||||||
The parameter `guests` can be used to exclude guest users.
|
The parameter `guests` can be used to exclude guest users.
|
||||||
The parameter `deactivated` can be used to include deactivated users.
|
The parameter `deactivated` can be used to include deactivated users.
|
||||||
|
The parameter `order_by` can be used to order the result.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, hs: "HomeServer"):
|
def __init__(self, hs: "HomeServer"):
|
||||||
|
@ -108,6 +109,7 @@ class UsersRestServletV2(RestServlet):
|
||||||
UserSortOrder.USER_TYPE.value,
|
UserSortOrder.USER_TYPE.value,
|
||||||
UserSortOrder.AVATAR_URL.value,
|
UserSortOrder.AVATAR_URL.value,
|
||||||
UserSortOrder.SHADOW_BANNED.value,
|
UserSortOrder.SHADOW_BANNED.value,
|
||||||
|
UserSortOrder.CREATION_TS.value,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -297,27 +297,22 @@ class DataStore(
|
||||||
|
|
||||||
where_clause = "WHERE " + " AND ".join(filters) if len(filters) > 0 else ""
|
where_clause = "WHERE " + " AND ".join(filters) if len(filters) > 0 else ""
|
||||||
|
|
||||||
sql_base = """
|
sql_base = f"""
|
||||||
FROM users as u
|
FROM users as u
|
||||||
LEFT JOIN profiles AS p ON u.name = '@' || p.user_id || ':' || ?
|
LEFT JOIN profiles AS p ON u.name = '@' || p.user_id || ':' || ?
|
||||||
{}
|
{where_clause}
|
||||||
""".format(
|
"""
|
||||||
where_clause
|
|
||||||
)
|
|
||||||
sql = "SELECT COUNT(*) as total_users " + sql_base
|
sql = "SELECT COUNT(*) as total_users " + sql_base
|
||||||
txn.execute(sql, args)
|
txn.execute(sql, args)
|
||||||
count = txn.fetchone()[0]
|
count = txn.fetchone()[0]
|
||||||
|
|
||||||
sql = """
|
sql = f"""
|
||||||
SELECT name, user_type, is_guest, admin, deactivated, shadow_banned, displayname, avatar_url
|
SELECT name, user_type, is_guest, admin, deactivated, shadow_banned,
|
||||||
|
displayname, avatar_url, creation_ts * 1000 as creation_ts
|
||||||
{sql_base}
|
{sql_base}
|
||||||
ORDER BY {order_by_column} {order}, u.name ASC
|
ORDER BY {order_by_column} {order}, u.name ASC
|
||||||
LIMIT ? OFFSET ?
|
LIMIT ? OFFSET ?
|
||||||
""".format(
|
"""
|
||||||
sql_base=sql_base,
|
|
||||||
order_by_column=order_by_column,
|
|
||||||
order=order,
|
|
||||||
)
|
|
||||||
args += [limit, start]
|
args += [limit, start]
|
||||||
txn.execute(sql, args)
|
txn.execute(sql, args)
|
||||||
users = self.db_pool.cursor_to_dict(txn)
|
users = self.db_pool.cursor_to_dict(txn)
|
||||||
|
|
|
@ -75,6 +75,7 @@ class UserSortOrder(Enum):
|
||||||
USER_TYPE = ordered alphabetically by `user_type`
|
USER_TYPE = ordered alphabetically by `user_type`
|
||||||
AVATAR_URL = ordered alphabetically by `avatar_url`
|
AVATAR_URL = ordered alphabetically by `avatar_url`
|
||||||
SHADOW_BANNED = ordered by `shadow_banned`
|
SHADOW_BANNED = ordered by `shadow_banned`
|
||||||
|
CREATION_TS = ordered by `creation_ts`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MEDIA_LENGTH = "media_length"
|
MEDIA_LENGTH = "media_length"
|
||||||
|
@ -88,6 +89,7 @@ class UserSortOrder(Enum):
|
||||||
USER_TYPE = "user_type"
|
USER_TYPE = "user_type"
|
||||||
AVATAR_URL = "avatar_url"
|
AVATAR_URL = "avatar_url"
|
||||||
SHADOW_BANNED = "shadow_banned"
|
SHADOW_BANNED = "shadow_banned"
|
||||||
|
CREATION_TS = "creation_ts"
|
||||||
|
|
||||||
|
|
||||||
class StatsStore(StateDeltasStore):
|
class StatsStore(StateDeltasStore):
|
||||||
|
|
|
@ -473,7 +473,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
"""
|
"""
|
||||||
channel = self.make_request("GET", self.url, b"{}")
|
channel = self.make_request("GET", self.url, b"{}")
|
||||||
|
|
||||||
self.assertEqual(401, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(401, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
|
self.assertEqual(Codes.MISSING_TOKEN, channel.json_body["errcode"])
|
||||||
|
|
||||||
def test_requester_is_no_admin(self):
|
def test_requester_is_no_admin(self):
|
||||||
|
@ -485,7 +485,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
|
|
||||||
channel = self.make_request("GET", self.url, access_token=other_user_token)
|
channel = self.make_request("GET", self.url, access_token=other_user_token)
|
||||||
|
|
||||||
self.assertEqual(403, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(403, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
self.assertEqual(Codes.FORBIDDEN, channel.json_body["errcode"])
|
||||||
|
|
||||||
def test_all_users(self):
|
def test_all_users(self):
|
||||||
|
@ -497,11 +497,11 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
self.url + "?deactivated=true",
|
self.url + "?deactivated=true",
|
||||||
b"{}",
|
{},
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(3, len(channel.json_body["users"]))
|
self.assertEqual(3, len(channel.json_body["users"]))
|
||||||
self.assertEqual(3, channel.json_body["total"])
|
self.assertEqual(3, channel.json_body["total"])
|
||||||
|
|
||||||
|
@ -532,7 +532,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
)
|
)
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
url.encode("ascii"),
|
url,
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
self.assertEqual(expected_http_code, channel.code, msg=channel.json_body)
|
self.assertEqual(expected_http_code, channel.code, msg=channel.json_body)
|
||||||
|
@ -598,7 +598,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
|
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
|
||||||
|
|
||||||
# negative from
|
# negative from
|
||||||
|
@ -608,7 +608,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
|
self.assertEqual(Codes.INVALID_PARAM, channel.json_body["errcode"])
|
||||||
|
|
||||||
# invalid guests
|
# invalid guests
|
||||||
|
@ -618,7 +618,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"])
|
self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"])
|
||||||
|
|
||||||
# invalid deactivated
|
# invalid deactivated
|
||||||
|
@ -628,7 +628,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"])
|
self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"])
|
||||||
|
|
||||||
# unkown order_by
|
# unkown order_by
|
||||||
|
@ -648,7 +648,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(400, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(400, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"])
|
self.assertEqual(Codes.UNKNOWN, channel.json_body["errcode"])
|
||||||
|
|
||||||
def test_limit(self):
|
def test_limit(self):
|
||||||
|
@ -666,7 +666,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(channel.json_body["total"], number_users)
|
self.assertEqual(channel.json_body["total"], number_users)
|
||||||
self.assertEqual(len(channel.json_body["users"]), 5)
|
self.assertEqual(len(channel.json_body["users"]), 5)
|
||||||
self.assertEqual(channel.json_body["next_token"], "5")
|
self.assertEqual(channel.json_body["next_token"], "5")
|
||||||
|
@ -687,7 +687,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(channel.json_body["total"], number_users)
|
self.assertEqual(channel.json_body["total"], number_users)
|
||||||
self.assertEqual(len(channel.json_body["users"]), 15)
|
self.assertEqual(len(channel.json_body["users"]), 15)
|
||||||
self.assertNotIn("next_token", channel.json_body)
|
self.assertNotIn("next_token", channel.json_body)
|
||||||
|
@ -708,7 +708,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(channel.json_body["total"], number_users)
|
self.assertEqual(channel.json_body["total"], number_users)
|
||||||
self.assertEqual(channel.json_body["next_token"], "15")
|
self.assertEqual(channel.json_body["next_token"], "15")
|
||||||
self.assertEqual(len(channel.json_body["users"]), 10)
|
self.assertEqual(len(channel.json_body["users"]), 10)
|
||||||
|
@ -731,7 +731,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(channel.json_body["total"], number_users)
|
self.assertEqual(channel.json_body["total"], number_users)
|
||||||
self.assertEqual(len(channel.json_body["users"]), number_users)
|
self.assertEqual(len(channel.json_body["users"]), number_users)
|
||||||
self.assertNotIn("next_token", channel.json_body)
|
self.assertNotIn("next_token", channel.json_body)
|
||||||
|
@ -744,7 +744,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(channel.json_body["total"], number_users)
|
self.assertEqual(channel.json_body["total"], number_users)
|
||||||
self.assertEqual(len(channel.json_body["users"]), number_users)
|
self.assertEqual(len(channel.json_body["users"]), number_users)
|
||||||
self.assertNotIn("next_token", channel.json_body)
|
self.assertNotIn("next_token", channel.json_body)
|
||||||
|
@ -757,7 +757,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(channel.json_body["total"], number_users)
|
self.assertEqual(channel.json_body["total"], number_users)
|
||||||
self.assertEqual(len(channel.json_body["users"]), 19)
|
self.assertEqual(len(channel.json_body["users"]), 19)
|
||||||
self.assertEqual(channel.json_body["next_token"], "19")
|
self.assertEqual(channel.json_body["next_token"], "19")
|
||||||
|
@ -771,7 +771,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(200, int(channel.result["code"]), msg=channel.result["body"])
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
self.assertEqual(channel.json_body["total"], number_users)
|
self.assertEqual(channel.json_body["total"], number_users)
|
||||||
self.assertEqual(len(channel.json_body["users"]), 1)
|
self.assertEqual(len(channel.json_body["users"]), 1)
|
||||||
self.assertNotIn("next_token", channel.json_body)
|
self.assertNotIn("next_token", channel.json_body)
|
||||||
|
@ -781,7 +781,10 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
Testing order list with parameter `order_by`
|
Testing order list with parameter `order_by`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# make sure that the users do not have the same timestamps
|
||||||
|
self.reactor.advance(10)
|
||||||
user1 = self.register_user("user1", "pass1", admin=False, displayname="Name Z")
|
user1 = self.register_user("user1", "pass1", admin=False, displayname="Name Z")
|
||||||
|
self.reactor.advance(10)
|
||||||
user2 = self.register_user("user2", "pass2", admin=False, displayname="Name Y")
|
user2 = self.register_user("user2", "pass2", admin=False, displayname="Name Y")
|
||||||
|
|
||||||
# Modify user
|
# Modify user
|
||||||
|
@ -841,6 +844,11 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
self._order_test([self.admin_user, user2, user1], "avatar_url", "f")
|
self._order_test([self.admin_user, user2, user1], "avatar_url", "f")
|
||||||
self._order_test([user1, user2, self.admin_user], "avatar_url", "b")
|
self._order_test([user1, user2, self.admin_user], "avatar_url", "b")
|
||||||
|
|
||||||
|
# order by creation_ts
|
||||||
|
self._order_test([self.admin_user, user1, user2], "creation_ts")
|
||||||
|
self._order_test([self.admin_user, user1, user2], "creation_ts", "f")
|
||||||
|
self._order_test([user2, user1, self.admin_user], "creation_ts", "b")
|
||||||
|
|
||||||
def _order_test(
|
def _order_test(
|
||||||
self,
|
self,
|
||||||
expected_user_list: List[str],
|
expected_user_list: List[str],
|
||||||
|
@ -863,7 +871,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
url += "dir=%s" % (dir,)
|
url += "dir=%s" % (dir,)
|
||||||
channel = self.make_request(
|
channel = self.make_request(
|
||||||
"GET",
|
"GET",
|
||||||
url.encode("ascii"),
|
url,
|
||||||
access_token=self.admin_user_tok,
|
access_token=self.admin_user_tok,
|
||||||
)
|
)
|
||||||
self.assertEqual(200, channel.code, msg=channel.json_body)
|
self.assertEqual(200, channel.code, msg=channel.json_body)
|
||||||
|
@ -887,6 +895,7 @@ class UsersListTestCase(unittest.HomeserverTestCase):
|
||||||
self.assertIn("shadow_banned", u)
|
self.assertIn("shadow_banned", u)
|
||||||
self.assertIn("displayname", u)
|
self.assertIn("displayname", u)
|
||||||
self.assertIn("avatar_url", u)
|
self.assertIn("avatar_url", u)
|
||||||
|
self.assertIn("creation_ts", u)
|
||||||
|
|
||||||
def _create_users(self, number_users: int):
|
def _create_users(self, number_users: int):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in a new issue