Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions examples/client_info.py
Comment thread
chrisdedman marked this conversation as resolved.
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")
2 changes: 2 additions & 0 deletions matrix/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
126 changes: 126 additions & 0 deletions matrix/member.py
Comment thread
chrisdedman marked this conversation as resolved.
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:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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:
Comment thread
chrisdedman marked this conversation as resolved.
"""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:
Comment thread
chrisdedman marked this conversation as resolved.
"""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)
Comment thread
chrisdedman marked this conversation as resolved.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be sure, does event really use room power level too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
208 changes: 208 additions & 0 deletions tests/test_member.py
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
Loading