Skip to content

Commit fb8cb4e

Browse files
authored
TalkAPI: added list_participants method + fix for statuses (#142)
While writing examples for documentation on how to work with the Talk API, I realized that there was no method for obtaining a list of participants in a conversation. **Now it is :)** A bug was also found that the statuses for one-to-one conversations were always empty. **Corrected and added a test for this.** --------- Signed-off-by: Alexander Piskun <bigcat88@icloud.com>
1 parent ddbd2c0 commit fb8cb4e

File tree

6 files changed

+150
-19
lines changed

6 files changed

+150
-19
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@
22

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

5-
## [0.3.1 - 2023-10-05]
5+
## [0.3.1 - 2023-10-07]
66

77
### Added
88

99
- CalendarAPI with the help of [caldav](https://pypi.org/project/caldav/) package. #136
1010
- [NotesAPI](https://github.com/nextcloud/notes) #137
11+
- TalkAPI: `list_participants` method to list conversation participants. #142
12+
13+
### Fixed
14+
15+
- TalkAPI: In One-to-One conversations the `status_message` and `status_icon` fields were always empty.
1116

1217
## [0.3.0 - 2023-09-28]
1318

docs/reference/Talk.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ Talk API
55
:members:
66
:inherited-members:
77

8+
.. autoclass:: nc_py_api.talk.Participant
9+
:members:
10+
:inherited-members:
11+
812
.. autoclass:: nc_py_api.talk.TalkMessage
913
:members:
1014
:inherited-members:

nc_py_api/_talk_api.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
ConversationType,
1919
MessageReactions,
2020
NotificationLevel,
21+
Participant,
2122
Poll,
2223
TalkFileMessage,
2324
TalkMessage,
@@ -67,7 +68,7 @@ def get_user_conversations(
6768
if no_status_update:
6869
params["noStatusUpdate"] = 1
6970
if include_status:
70-
params["includeStatus"] = True
71+
params["includeStatus"] = 1
7172
if modified_since:
7273
params["modifiedSince"] = self.modified_since if modified_since is True else modified_since
7374

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

80+
def list_participants(
81+
self, conversation: typing.Union[Conversation, str], include_status: bool = False
82+
) -> list[Participant]:
83+
"""Returns a list of conversation participants.
84+
85+
:param conversation: conversation token or :py:class:`~nc_py_api.talk.Conversation`.
86+
:param include_status: Whether the user status information of all one-to-one conversations should be loaded.
87+
"""
88+
token = conversation.token if isinstance(conversation, Conversation) else conversation
89+
result = self._session.ocs(
90+
"GET", self._ep_base + f"/api/v4/room/{token}/participants", params={"includeStatus": int(include_status)}
91+
)
92+
return [Participant(i) for i in result]
93+
7994
def create_conversation(
8095
self,
8196
conversation_type: ConversationType,

nc_py_api/talk.py

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import typing
77

88
from . import files as _files
9-
from .user_status import _UserStatus
109

1110

1211
class ConversationType(enum.IntEnum):
@@ -91,7 +90,7 @@ class ListableScope(enum.IntEnum):
9190
class NotificationLevel(enum.IntEnum):
9291
"""The notification level for the user.
9392
94-
.. note:: Default: ``1`` for one-to-one conversations, ``2`` for other conversations.
93+
.. note:: Default: ``1`` for ``one-to-one`` conversations, ``2`` for other conversations.
9594
"""
9695

9796
DEFAULT = 0
@@ -309,8 +308,29 @@ def to_fs_node(self) -> _files.FsNode:
309308
)
310309

311310

311+
@dataclasses.dataclass
312+
class _TalkUserStatus:
313+
def __init__(self, raw_data: dict):
314+
self._raw_data = raw_data
315+
316+
@property
317+
def status_message(self) -> str:
318+
"""Message of the status."""
319+
return str(self._raw_data.get("statusMessage", "") or "")
320+
321+
@property
322+
def status_icon(self) -> str:
323+
"""The icon picked by the user (must be one emoji)."""
324+
return str(self._raw_data.get("statusIcon", "") or "")
325+
326+
@property
327+
def status_type(self) -> str:
328+
"""Status type, on of the: online, away, dnd, invisible, offline."""
329+
return str(self._raw_data.get("status", "") or "")
330+
331+
312332
@dataclasses.dataclass(init=False)
313-
class Conversation(_UserStatus):
333+
class Conversation(_TalkUserStatus):
314334
"""Talk conversation."""
315335

316336
@property
@@ -447,7 +467,7 @@ def can_start_call(self) -> bool:
447467
def can_delete_conversation(self) -> bool:
448468
"""Flag if the user can delete the conversation for everyone.
449469
450-
.. note: Not possible without moderator permissions or in one-to-one conversations.
470+
.. note: Not possible without moderator permissions or in ``one-to-one`` conversations.
451471
"""
452472
return bool(self._raw_data.get("canDeleteConversation", False))
453473

@@ -597,6 +617,74 @@ def recording_status(self) -> CallRecordingStatus:
597617
"""
598618
return CallRecordingStatus(self._raw_data.get("callRecording", CallRecordingStatus.NO_RECORDING))
599619

620+
@property
621+
def status_clear_at(self) -> typing.Optional[int]:
622+
"""Unix Timestamp representing the time to clear the status.
623+
624+
.. note:: Available only for ``one-to-one`` conversations.
625+
"""
626+
return self._raw_data.get("statusClearAt", None)
627+
628+
629+
@dataclasses.dataclass(init=False)
630+
class Participant(_TalkUserStatus):
631+
"""Conversation participant information."""
632+
633+
@property
634+
def attendee_id(self) -> int:
635+
"""Unique attendee id."""
636+
return self._raw_data["attendeeId"]
637+
638+
@property
639+
def actor_type(self) -> str:
640+
"""The actor type of the participant that voted: **users**, **groups**, **circles**, **guests**, **emails**."""
641+
return self._raw_data["actorType"]
642+
643+
@property
644+
def actor_id(self) -> str:
645+
"""The unique identifier for the given actor type."""
646+
return self._raw_data["actorId"]
647+
648+
@property
649+
def display_name(self) -> str:
650+
"""Can be empty for guests."""
651+
return self._raw_data["displayName"]
652+
653+
@property
654+
def participant_type(self) -> ParticipantType:
655+
"""Permissions level, see: :py:class:`~nc_py_api.talk.ParticipantType`."""
656+
return ParticipantType(self._raw_data["participantType"])
657+
658+
@property
659+
def last_ping(self) -> int:
660+
"""Timestamp of the last ping. Should be used for sorting."""
661+
return self._raw_data["lastPing"]
662+
663+
@property
664+
def participant_flags(self) -> InCallFlags:
665+
"""Current call flags."""
666+
return InCallFlags(self._raw_data.get("inCall", InCallFlags.DISCONNECTED))
667+
668+
@property
669+
def permissions(self) -> AttendeePermissions:
670+
"""Final permissions, combined :py:class:`~nc_py_api.talk.AttendeePermissions` values."""
671+
return AttendeePermissions(self._raw_data["permissions"])
672+
673+
@property
674+
def attendee_permissions(self) -> AttendeePermissions:
675+
"""Dedicated permissions for the current participant, if not ``Custom``, they are not the resulting ones."""
676+
return AttendeePermissions(self._raw_data["attendeePermissions"])
677+
678+
@property
679+
def session_ids(self) -> list[str]:
680+
"""A list of session IDs, each one 512 characters long, or empty if there is no session."""
681+
return self._raw_data["sessionIds"]
682+
683+
@property
684+
def breakout_token(self) -> str:
685+
"""Only available with breakout-rooms-v1 capability."""
686+
return self._raw_data.get("roomToken", "")
687+
600688

601689
@dataclasses.dataclass
602690
class BotInfoBasic:

nc_py_api/user_status.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,15 @@ def __init__(self, raw_status: dict):
4747

4848

4949
@dataclasses.dataclass
50-
class _UserStatus:
50+
class UserStatus:
51+
"""Information about user status."""
52+
53+
user_id: str
54+
"""The ID of the user this status is for"""
55+
5156
def __init__(self, raw_data: dict):
5257
self._raw_data = raw_data
58+
self.user_id = raw_data["userId"]
5359

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

7480

75-
@dataclasses.dataclass
76-
class UserStatus(_UserStatus):
77-
"""Information about user status."""
78-
79-
user_id: str
80-
"""The ID of the user this status is for"""
81-
82-
def __init__(self, raw_data: dict):
83-
super().__init__(raw_data)
84-
self.user_id = raw_data["userId"]
85-
86-
8781
@dataclasses.dataclass(init=False)
8882
class CurrentUserStatus(UserStatus):
8983
"""Information about current user status."""

tests/actual_tests/talk_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ def test_conversation_create_delete(nc):
5656
assert isinstance(conversation.is_custom_avatar, bool)
5757
assert isinstance(conversation.call_start_time, int)
5858
assert isinstance(conversation.recording_status, talk.CallRecordingStatus)
59+
assert isinstance(conversation.status_type, str)
60+
assert isinstance(conversation.status_message, str)
61+
assert isinstance(conversation.status_icon, str)
62+
assert isinstance(conversation.status_clear_at, int) or conversation.status_clear_at is None
5963
if conversation.last_message is None:
6064
return
6165
talk_msg = conversation.last_message
@@ -99,6 +103,7 @@ def test_get_conversations_include_status(nc, nc_client):
99103
pytest.skip("Nextcloud Talk is not installed")
100104
nc_second_user = Nextcloud(nc_auth_user=environ["TEST_USER_ID"], nc_auth_pass=environ["TEST_USER_PASS"])
101105
nc_second_user.user_status.set_status_type("away")
106+
nc_second_user.user_status.set_status("my status message", status_icon="😇")
102107
conversation = nc.talk.create_conversation(talk.ConversationType.ONE_TO_ONE, environ["TEST_USER_ID"])
103108
try:
104109
conversations = nc.talk.get_user_conversations(include_status=False)
@@ -109,6 +114,26 @@ def test_get_conversations_include_status(nc, nc_client):
109114
assert conversations
110115
first_conv = next(i for i in conversations if i.conversation_id == conversation.conversation_id)
111116
assert first_conv.status_type == "away"
117+
assert first_conv.status_message == "my status message"
118+
assert first_conv.status_icon == "😇"
119+
participants = nc.talk.list_participants(first_conv)
120+
assert len(participants) == 2
121+
second_participant = next(i for i in participants if i.actor_id == environ["TEST_USER_ID"])
122+
assert second_participant.actor_type == "users"
123+
assert isinstance(second_participant.attendee_id, int)
124+
assert isinstance(second_participant.display_name, str)
125+
assert isinstance(second_participant.participant_type, talk.ParticipantType)
126+
assert isinstance(second_participant.last_ping, int)
127+
assert second_participant.participant_flags == talk.InCallFlags.DISCONNECTED
128+
assert isinstance(second_participant.permissions, talk.AttendeePermissions)
129+
assert isinstance(second_participant.attendee_permissions, talk.AttendeePermissions)
130+
assert isinstance(second_participant.session_ids, list)
131+
assert isinstance(second_participant.breakout_token, str)
132+
assert second_participant.status_message == ""
133+
participants = nc.talk.list_participants(first_conv, include_status=True)
134+
assert len(participants) == 2
135+
second_participant = next(i for i in participants if i.actor_id == environ["TEST_USER_ID"])
136+
assert second_participant.status_message == "my status message"
112137
finally:
113138
nc.talk.leave_conversation(conversation.token)
114139

0 commit comments

Comments
 (0)