Skip to content

TalkAPI: added list_participants method + fix for statuses #142

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

Merged
merged 2 commits into from
Oct 6, 2023
Merged
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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@

All notable changes to this project will be documented in this file.

## [0.3.1 - 2023-10-05]
## [0.3.1 - 2023-10-07]

### Added

- CalendarAPI with the help of [caldav](https://pypi.org/project/caldav/) package. #136
- [NotesAPI](https://github.com/nextcloud/notes) #137
- TalkAPI: `list_participants` method to list conversation participants. #142

### Fixed

- TalkAPI: In One-to-One conversations the `status_message` and `status_icon` fields were always empty.

## [0.3.0 - 2023-09-28]

Expand Down
4 changes: 4 additions & 0 deletions docs/reference/Talk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ Talk API
:members:
:inherited-members:

.. autoclass:: nc_py_api.talk.Participant
:members:
:inherited-members:

.. autoclass:: nc_py_api.talk.TalkMessage
:members:
:inherited-members:
Expand Down
17 changes: 16 additions & 1 deletion nc_py_api/_talk_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
ConversationType,
MessageReactions,
NotificationLevel,
Participant,
Poll,
TalkFileMessage,
TalkMessage,
Expand Down Expand Up @@ -67,7 +68,7 @@ def get_user_conversations(
if no_status_update:
params["noStatusUpdate"] = 1
if include_status:
params["includeStatus"] = True
params["includeStatus"] = 1
if modified_since:
params["modifiedSince"] = self.modified_since if modified_since is True else modified_since

Expand All @@ -76,6 +77,20 @@ def get_user_conversations(
self._update_config_sha()
return [Conversation(i) for i in result]

def list_participants(
self, conversation: typing.Union[Conversation, str], include_status: bool = False
) -> list[Participant]:
"""Returns a list of conversation participants.

:param conversation: conversation token or :py:class:`~nc_py_api.talk.Conversation`.
:param include_status: Whether the user status information of all one-to-one conversations should be loaded.
"""
token = conversation.token if isinstance(conversation, Conversation) else conversation
result = self._session.ocs(
"GET", self._ep_base + f"/api/v4/room/{token}/participants", params={"includeStatus": int(include_status)}
)
return [Participant(i) for i in result]

def create_conversation(
self,
conversation_type: ConversationType,
Expand Down
96 changes: 92 additions & 4 deletions nc_py_api/talk.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
import typing

from . import files as _files
from .user_status import _UserStatus


class ConversationType(enum.IntEnum):
Expand Down Expand Up @@ -91,7 +90,7 @@ class ListableScope(enum.IntEnum):
class NotificationLevel(enum.IntEnum):
"""The notification level for the user.

.. note:: Default: ``1`` for one-to-one conversations, ``2`` for other conversations.
.. note:: Default: ``1`` for ``one-to-one`` conversations, ``2`` for other conversations.
"""

DEFAULT = 0
Expand Down Expand Up @@ -309,8 +308,29 @@ def to_fs_node(self) -> _files.FsNode:
)


@dataclasses.dataclass
class _TalkUserStatus:
def __init__(self, raw_data: dict):
self._raw_data = raw_data

@property
def status_message(self) -> str:
"""Message of the status."""
return str(self._raw_data.get("statusMessage", "") or "")

@property
def status_icon(self) -> str:
"""The icon picked by the user (must be one emoji)."""
return str(self._raw_data.get("statusIcon", "") or "")

@property
def status_type(self) -> str:
"""Status type, on of the: online, away, dnd, invisible, offline."""
return str(self._raw_data.get("status", "") or "")


@dataclasses.dataclass(init=False)
class Conversation(_UserStatus):
class Conversation(_TalkUserStatus):
"""Talk conversation."""

@property
Expand Down Expand Up @@ -447,7 +467,7 @@ def can_start_call(self) -> bool:
def can_delete_conversation(self) -> bool:
"""Flag if the user can delete the conversation for everyone.

.. note: Not possible without moderator permissions or in one-to-one conversations.
.. note: Not possible without moderator permissions or in ``one-to-one`` conversations.
"""
return bool(self._raw_data.get("canDeleteConversation", False))

Expand Down Expand Up @@ -597,6 +617,74 @@ def recording_status(self) -> CallRecordingStatus:
"""
return CallRecordingStatus(self._raw_data.get("callRecording", CallRecordingStatus.NO_RECORDING))

@property
def status_clear_at(self) -> typing.Optional[int]:
"""Unix Timestamp representing the time to clear the status.

.. note:: Available only for ``one-to-one`` conversations.
"""
return self._raw_data.get("statusClearAt", None)


@dataclasses.dataclass(init=False)
class Participant(_TalkUserStatus):
"""Conversation participant information."""

@property
def attendee_id(self) -> int:
"""Unique attendee id."""
return self._raw_data["attendeeId"]

@property
def actor_type(self) -> str:
"""The actor type of the participant that voted: **users**, **groups**, **circles**, **guests**, **emails**."""
return self._raw_data["actorType"]

@property
def actor_id(self) -> str:
"""The unique identifier for the given actor type."""
return self._raw_data["actorId"]

@property
def display_name(self) -> str:
"""Can be empty for guests."""
return self._raw_data["displayName"]

@property
def participant_type(self) -> ParticipantType:
"""Permissions level, see: :py:class:`~nc_py_api.talk.ParticipantType`."""
return ParticipantType(self._raw_data["participantType"])

@property
def last_ping(self) -> int:
"""Timestamp of the last ping. Should be used for sorting."""
return self._raw_data["lastPing"]

@property
def participant_flags(self) -> InCallFlags:
"""Current call flags."""
return InCallFlags(self._raw_data.get("inCall", InCallFlags.DISCONNECTED))

@property
def permissions(self) -> AttendeePermissions:
"""Final permissions, combined :py:class:`~nc_py_api.talk.AttendeePermissions` values."""
return AttendeePermissions(self._raw_data["permissions"])

@property
def attendee_permissions(self) -> AttendeePermissions:
"""Dedicated permissions for the current participant, if not ``Custom``, they are not the resulting ones."""
return AttendeePermissions(self._raw_data["attendeePermissions"])

@property
def session_ids(self) -> list[str]:
"""A list of session IDs, each one 512 characters long, or empty if there is no session."""
return self._raw_data["sessionIds"]

@property
def breakout_token(self) -> str:
"""Only available with breakout-rooms-v1 capability."""
return self._raw_data.get("roomToken", "")


@dataclasses.dataclass
class BotInfoBasic:
Expand Down
20 changes: 7 additions & 13 deletions nc_py_api/user_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,15 @@ def __init__(self, raw_status: dict):


@dataclasses.dataclass
class _UserStatus:
class UserStatus:
"""Information about user status."""

user_id: str
"""The ID of the user this status is for"""

def __init__(self, raw_data: dict):
self._raw_data = raw_data
self.user_id = raw_data["userId"]

@property
def status_message(self) -> str:
Expand All @@ -72,18 +78,6 @@ def status_type(self) -> str:
return self._raw_data.get("status", "")


@dataclasses.dataclass
class UserStatus(_UserStatus):
"""Information about user status."""

user_id: str
"""The ID of the user this status is for"""

def __init__(self, raw_data: dict):
super().__init__(raw_data)
self.user_id = raw_data["userId"]


@dataclasses.dataclass(init=False)
class CurrentUserStatus(UserStatus):
"""Information about current user status."""
Expand Down
25 changes: 25 additions & 0 deletions tests/actual_tests/talk_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ def test_conversation_create_delete(nc):
assert isinstance(conversation.is_custom_avatar, bool)
assert isinstance(conversation.call_start_time, int)
assert isinstance(conversation.recording_status, talk.CallRecordingStatus)
assert isinstance(conversation.status_type, str)
assert isinstance(conversation.status_message, str)
assert isinstance(conversation.status_icon, str)
assert isinstance(conversation.status_clear_at, int) or conversation.status_clear_at is None
if conversation.last_message is None:
return
talk_msg = conversation.last_message
Expand Down Expand Up @@ -99,6 +103,7 @@ def test_get_conversations_include_status(nc, nc_client):
pytest.skip("Nextcloud Talk is not installed")
nc_second_user = Nextcloud(nc_auth_user=environ["TEST_USER_ID"], nc_auth_pass=environ["TEST_USER_PASS"])
nc_second_user.user_status.set_status_type("away")
nc_second_user.user_status.set_status("my status message", status_icon="😇")
conversation = nc.talk.create_conversation(talk.ConversationType.ONE_TO_ONE, environ["TEST_USER_ID"])
try:
conversations = nc.talk.get_user_conversations(include_status=False)
Expand All @@ -109,6 +114,26 @@ def test_get_conversations_include_status(nc, nc_client):
assert conversations
first_conv = next(i for i in conversations if i.conversation_id == conversation.conversation_id)
assert first_conv.status_type == "away"
assert first_conv.status_message == "my status message"
assert first_conv.status_icon == "😇"
participants = nc.talk.list_participants(first_conv)
assert len(participants) == 2
second_participant = next(i for i in participants if i.actor_id == environ["TEST_USER_ID"])
assert second_participant.actor_type == "users"
assert isinstance(second_participant.attendee_id, int)
assert isinstance(second_participant.display_name, str)
assert isinstance(second_participant.participant_type, talk.ParticipantType)
assert isinstance(second_participant.last_ping, int)
assert second_participant.participant_flags == talk.InCallFlags.DISCONNECTED
assert isinstance(second_participant.permissions, talk.AttendeePermissions)
assert isinstance(second_participant.attendee_permissions, talk.AttendeePermissions)
assert isinstance(second_participant.session_ids, list)
assert isinstance(second_participant.breakout_token, str)
assert second_participant.status_message == ""
participants = nc.talk.list_participants(first_conv, include_status=True)
assert len(participants) == 2
second_participant = next(i for i in participants if i.actor_id == environ["TEST_USER_ID"])
assert second_participant.status_message == "my status message"
finally:
nc.talk.leave_conversation(conversation.token)

Expand Down