From c36a0db4a1ce4bbc5ea066a8475cef4a5c59238b Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 6 Jul 2026 19:49:55 -0700 Subject: [PATCH 1/3] add profile command example to retrieve member information --- examples/client_info.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 examples/client_info.py diff --git a/examples/client_info.py b/examples/client_info.py new file mode 100644 index 0000000..74c5060 --- /dev/null +++ b/examples/client_info.py @@ -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") From ac232c89d15670f5bf5c179ff356b28012e17892 Mon Sep 17 00:00:00 2001 From: Chris Date: Mon, 6 Jul 2026 19:50:34 -0700 Subject: [PATCH 2/3] Add Member class to manage member-related functionalities --- matrix/context.py | 2 + matrix/member.py | 126 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 matrix/member.py diff --git a/matrix/context.py b/matrix/context.py index 185b5f0..ab4d3cb 100644 --- a/matrix/context.py +++ b/matrix/context.py @@ -7,6 +7,7 @@ from .message import Message from .room import Room from .types import File, Image +from .member import Member if TYPE_CHECKING: from .bot import Bot # pragma: no cover @@ -23,6 +24,7 @@ def __init__(self, bot: "Bot", room: Room, event: Event): self.bot = bot self.room = room self.event = event + self.member = Member(event.sender, bot.client) self.body: str = getattr(event, "body", "") self.sender: str = event.sender diff --git a/matrix/member.py b/matrix/member.py new file mode 100644 index 0000000..6fc705e --- /dev/null +++ b/matrix/member.py @@ -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: + """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: + """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: + """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) + + 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) + 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) From 98e0133d99b96e0f45020f6ffa6e2d016a3eea60 Mon Sep 17 00:00:00 2001 From: Chris Date: Tue, 7 Jul 2026 15:27:32 -0700 Subject: [PATCH 3/3] Add unit tests for Member class method --- tests/test_member.py | 208 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 tests/test_member.py diff --git a/tests/test_member.py b/tests/test_member.py new file mode 100644 index 0000000..8a244bf --- /dev/null +++ b/tests/test_member.py @@ -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