-
Notifications
You must be signed in to change notification settings - Fork 6
Added Member Class Information retrival #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
c36a0db
ac232c8
98e0133
0ae0df9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| from matrix import Bot | ||
|
|
||
| bot = Bot() | ||
|
|
||
|
|
||
| @bot.command() | ||
| async def profile(ctx): | ||
| member = ctx.member | ||
|
|
||
| name = await member.get_display_name() | ||
| avatar = await member.get_avatar_url() | ||
| level = await member.get_room_power_level(ctx.room) | ||
| presence = await member.get_presence() | ||
| has_permission = await member.has_permission(ctx.room, "ban") | ||
| has_event_permission = await member.has_event_permission(ctx.room, "m.room.message") | ||
|
|
||
| await ctx.reply( | ||
| f"User ID: {member.user_id}\n" | ||
| f"Display name: {name}\n" | ||
| f"Avatar URL: {avatar}\n" | ||
| f"Power level: {level}\n" | ||
| f"Presence: {presence}\n" | ||
| f"Has permission to ban users: {has_permission}\n" | ||
| f"Has permission to send messages: {has_event_permission}" | ||
| ) | ||
|
|
||
|
|
||
| bot.start(config="config.yaml") |
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While looking at this, I think one of the most interesting addition to member you could've made is def mention(self) -> str:
"""Get a Markdown-formatted mention link (matrix.to pill) for this member.
## Example
```python
await ctx.reply(f"Welcome {member.mention()}!")
```
"""
return f"[{self._user_id}](https://matrix.to/#/{self._user_id})"
def __str__(self) -> str:
return self.mention()You can see that I also implemented await ctx.reply(f"Welcome {member}!")But this could be another PR. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| from matrix.room import Room | ||
| from nio import AsyncClient | ||
|
|
||
|
|
||
| class Member: | ||
| def __init__(self, user_id: str, client: AsyncClient) -> None: | ||
| self._user_id: str = user_id | ||
| self._client: AsyncClient = client | ||
|
|
||
| @property | ||
| def user_id(self) -> str: | ||
| return self._user_id | ||
|
|
||
| async def get_profile(self) -> dict | None: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be cleaner to have a dataclass for the MemberProfile instead of returning a plain dictionary; it would give callers type safety and make the fields explicit. |
||
| """Get the profile information for this member. | ||
|
|
||
| ## Example | ||
|
|
||
| ```python | ||
| profile = await member.get_profile() | ||
| for key, value in profile.other_info.items(): | ||
| print(f"{key}: {value}") | ||
| ``` | ||
| """ | ||
| profile = await self._client.get_profile(self._user_id) | ||
| return profile if profile else None | ||
|
|
||
| async def get_display_name(self) -> str | None: | ||
| """Get the display name for this member. | ||
|
|
||
| ## Example | ||
|
|
||
| ```python | ||
| display_name = await member.get_display_name() | ||
| print(f"Display name: {display_name}") | ||
| ``` | ||
| """ | ||
| display_name = await self._client.get_displayname(self._user_id) | ||
| return display_name.displayname if display_name else None | ||
|
|
||
| async def get_avatar_url(self) -> str | None: | ||
| """Get the avatar URL for this member. | ||
|
|
||
| ## Example | ||
|
|
||
| ```python | ||
| avatar_url = await member.get_avatar_url() | ||
| print(f"Avatar URL: {avatar_url}") | ||
| ``` | ||
| """ | ||
| avatar = await self._client.get_avatar(self._user_id) | ||
| return await self._client.mxc_to_http(avatar.avatar_url) if avatar else None | ||
|
|
||
| async def get_room_power_level(self, room: Room) -> int: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you should move this closer (just above them) to |
||
| """Get the power level for this member in a specific room. | ||
|
|
||
| ## Example | ||
|
|
||
| ```python | ||
| level = await member.get_room_power_level(ctx.room) | ||
| print(f"Power level in room {room.room_id}: {level}") | ||
| ``` | ||
| """ | ||
| power_level = await self._client.room_get_state_event( | ||
| room.room_id, "m.room.power_levels", "" | ||
| ) | ||
|
|
||
| content = getattr(power_level, "content", {}) or {} | ||
| users = content.get("users", {}) | ||
| default = content.get("users_default", 0) | ||
|
|
||
| return int(users.get(self._user_id, default)) | ||
|
|
||
| async def get_presence(self) -> str | None: | ||
| """Get the presence status for this member. | ||
|
|
||
| ## Example | ||
|
|
||
| ```python | ||
| presence = await member.get_presence() | ||
| print(f"Presence status: {presence}") | ||
| ``` | ||
| """ | ||
| presence = await self._client.get_presence(self._user_id) | ||
| return presence.presence if presence else None | ||
|
|
||
| async def has_permission(self, room: Room, permission: str) -> bool: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be consistent with the following method, I think we could rename this |
||
| """Check if this member has a specific permission in a room. | ||
|
|
||
| ## Example | ||
|
|
||
| ```python | ||
| has_permission = await member.has_permission(ctx.room, "ban") | ||
| print(f"Has ban permission: {has_permission}") | ||
| ``` | ||
| """ | ||
| power_level = await self.get_room_power_level(room) | ||
| power_levels_event = await self._client.room_get_state_event( | ||
| room.room_id, "m.room.power_levels", "" | ||
| ) | ||
|
|
||
| content = getattr(power_levels_event, "content", {}) or {} | ||
| required_level = content.get(permission, 0) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This fallback would cause the power level to always be true. For example, if the response is an error, it will fallback to 0. This could give access to someone when it shouldn't and it's going to be hard to debug because it's doing it silently. In the worst cases, a permission check should never default to "allowed". If you use Also, there was no test covering that and same thing for |
||
|
|
||
| return bool(power_level >= required_level) | ||
|
|
||
| async def has_event_permission(self, room: Room, event_type: str) -> bool: | ||
| """Check if this member has permission to send a specific event type in a room. | ||
|
|
||
| ## Example | ||
|
|
||
| ```python | ||
| can_send_message = await member.has_event_permission(ctx.room, "m.room.message") | ||
| print(f"Can send messages: {can_send_message}") | ||
| ``` | ||
| """ | ||
| power_level = await self.get_room_power_level(room) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to be sure, does event really use room power level too? |
||
| power_levels_event = await self._client.room_get_state_event( | ||
| room.room_id, "m.room.power_levels", "" | ||
| ) | ||
|
|
||
| content = getattr(power_levels_event, "content", {}) or {} | ||
| events = content.get("events", {}) | ||
| required_level = events.get(event_type, 0) | ||
|
|
||
| return bool(power_level >= required_level) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,208 @@ | ||
| import pytest | ||
| from unittest.mock import AsyncMock, Mock | ||
|
|
||
| from nio import AsyncClient | ||
| from matrix.member import Member | ||
| from matrix.room import Room | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def client(): | ||
| return AsyncMock(spec=AsyncClient) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def room(): | ||
| room = Mock(spec=Room) | ||
| room.room_id = "!room:example.com" | ||
| return room | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def member(client): | ||
| return Member("@user:matrix.org", client) | ||
|
|
||
|
|
||
| def test_member_user_id__expect_correct_value(member): | ||
| assert member.user_id == "@user:matrix.org" | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_profile__expect_profile_returned(member, client): | ||
| mock_profile = Mock() | ||
| mock_profile.displayname = "Alice" | ||
| mock_profile.avatar_url = "mxc://matrix.org/avatar" | ||
| mock_profile.other_info = {"chat.commet.profile_status": "Online"} | ||
|
|
||
| client.get_profile = AsyncMock(return_value=mock_profile) | ||
|
|
||
| result = await member.get_profile() | ||
|
|
||
| client.get_profile.assert_awaited_once_with("@user:matrix.org") | ||
| assert result is mock_profile | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_profile_empty__expect_none(member, client): | ||
| client.get_profile = AsyncMock(return_value=None) | ||
|
|
||
| result = await member.get_profile() | ||
|
|
||
| assert result is None | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_display_name__expect_display_name(member, client): | ||
| response = Mock() | ||
| response.displayname = "Alice" | ||
| client.get_displayname = AsyncMock(return_value=response) | ||
|
|
||
| result = await member.get_display_name() | ||
|
|
||
| client.get_displayname.assert_awaited_once_with("@user:matrix.org") | ||
| assert result == "Alice" | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_display_name_empty__expect_none(member, client): | ||
| client.get_displayname = AsyncMock(return_value=None) | ||
|
|
||
| result = await member.get_display_name() | ||
|
|
||
| assert result is None | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_avatar_url__expect_http_avatar_url(member, client): | ||
| response = Mock() | ||
| response.avatar_url = "mxc://matrix.org/avatar" | ||
|
|
||
| client.get_avatar = AsyncMock(return_value=response) | ||
| client.mxc_to_http = AsyncMock(return_value="https://matrix.org/avatar") | ||
|
|
||
| result = await member.get_avatar_url() | ||
|
|
||
| client.get_avatar.assert_awaited_once_with("@user:matrix.org") | ||
| client.mxc_to_http.assert_awaited_once_with("mxc://matrix.org/avatar") | ||
| assert result == "https://matrix.org/avatar" | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_avatar_url_empty__expect_none(member, client): | ||
| client.get_avatar = AsyncMock(return_value=None) | ||
|
|
||
| result = await member.get_avatar_url() | ||
|
|
||
| assert result is None | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_room_power_level__expect_user_power_level(member, client, room): | ||
| response = Mock() | ||
| response.content = { | ||
| "users": {"@user:matrix.org": 50}, | ||
| "users_default": 0, | ||
| } | ||
| client.room_get_state_event = AsyncMock(return_value=response) | ||
|
|
||
| result = await member.get_room_power_level(room) | ||
|
|
||
| client.room_get_state_event.assert_awaited_once_with( | ||
| "!room:example.com", "m.room.power_levels", "" | ||
| ) | ||
| assert result == 50 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_room_power_level_missing_user__expect_default(member, client, room): | ||
| response = Mock() | ||
| response.content = { | ||
| "users": {}, | ||
| "users_default": 10, | ||
| } | ||
| client.room_get_state_event = AsyncMock(return_value=response) | ||
|
|
||
| result = await member.get_room_power_level(room) | ||
|
|
||
| assert result == 10 | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_presence__expect_presence(member, client): | ||
| response = Mock() | ||
| response.presence = "online" | ||
| client.get_presence = AsyncMock(return_value=response) | ||
|
|
||
| result = await member.get_presence() | ||
|
|
||
| client.get_presence.assert_awaited_once_with("@user:matrix.org") | ||
| assert result == "online" | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_get_presence_empty__expect_none(member, client): | ||
| client.get_presence = AsyncMock(return_value=None) | ||
|
|
||
| result = await member.get_presence() | ||
|
|
||
| assert result is None | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_has_permission__expect_true(member, client, room): | ||
| response = Mock() | ||
| response.content = { | ||
| "users": {"@user:matrix.org": 50}, | ||
| "users_default": 0, | ||
| "ban": 50, | ||
| } | ||
| client.room_get_state_event = AsyncMock(return_value=response) | ||
|
|
||
| result = await member.has_permission(room, "ban") | ||
|
|
||
| assert result is True | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_has_permission__expect_false(member, client, room): | ||
| response = Mock() | ||
| response.content = { | ||
| "users": {"@user:matrix.org": 10}, | ||
| "users_default": 0, | ||
| "ban": 50, | ||
| } | ||
| client.room_get_state_event = AsyncMock(return_value=response) | ||
|
|
||
| result = await member.has_permission(room, "ban") | ||
|
|
||
| assert result is False | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_has_event_permission__expect_true(member, client, room): | ||
| response = Mock() | ||
| response.content = { | ||
| "users": {"@user:matrix.org": 50}, | ||
| "users_default": 0, | ||
| "events": {"m.room.message": 0}, | ||
| } | ||
| client.room_get_state_event = AsyncMock(return_value=response) | ||
|
|
||
| result = await member.has_event_permission(room, "m.room.message") | ||
|
|
||
| assert result is True | ||
|
|
||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_has_event_permission__expect_false(member, client, room): | ||
| response = Mock() | ||
| response.content = { | ||
| "users": {"@user:matrix.org": 10}, | ||
| "users_default": 0, | ||
| "events": {"m.room.power_levels": 100}, | ||
| } | ||
| client.room_get_state_event = AsyncMock(return_value=response) | ||
|
|
||
| result = await member.has_event_permission(room, "m.room.power_levels") | ||
|
|
||
| assert result is False |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name of the file is a bit inaccurate, I think it should be something like
member_info.pyor justmember.py