Skip to content

Commit ce60bb9

Browse files
committed
DEVEXP-1241: Conversation Messages - List (E2E)
1 parent 70a1247 commit ce60bb9

11 files changed

Lines changed: 310 additions & 12 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
from sinch.domains.conversation.api.v1.internal.messages_endpoints import (
22
DeleteMessageEndpoint,
33
GetMessageEndpoint,
4+
ListMessagesEndpoint,
45
UpdateMessageMetadataEndpoint,
56
SendMessageEndpoint,
67
)
78

89
__all__ = [
910
"DeleteMessageEndpoint",
1011
"GetMessageEndpoint",
12+
"ListMessagesEndpoint",
1113
"UpdateMessageMetadataEndpoint",
1214
"SendMessageEndpoint",
1315
]

sinch/domains/conversation/api/v1/internal/messages_endpoints.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22
from sinch.core.enums import HTTPAuthentication, HTTPMethods
33
from sinch.core.models.http_response import HTTPResponse
44
from sinch.domains.conversation.models.v1.messages.internal.request import (
5+
ListMessagesRequest,
56
MessageIdRequest,
67
UpdateMessageMetadataRequest,
78
SendMessageRequest,
89
)
10+
from sinch.domains.conversation.models.v1.messages.internal import (
11+
ListMessagesResponse,
12+
)
913
from sinch.domains.conversation.models.v1.messages.response.types import (
1014
ConversationMessageResponse,
1115
SendMessageResponse,
@@ -36,6 +40,43 @@ def build_query_params(self) -> dict:
3640
return query_params
3741

3842

43+
class ListMessagesEndpoint(MessageEndpoint):
44+
ENDPOINT_URL = "{origin}/v1/projects/{project_id}/messages"
45+
HTTP_METHOD = HTTPMethods.GET.value
46+
HTTP_AUTHENTICATION = HTTPAuthentication.OAUTH.value
47+
48+
QUERY_PARAM_FIELDS = {
49+
"conversation_id",
50+
"contact_id",
51+
"app_id",
52+
"channel_identity",
53+
"start_time",
54+
"end_time",
55+
"page_size",
56+
"page_token",
57+
"view",
58+
"messages_source",
59+
"only_recipient_originated",
60+
"channel",
61+
}
62+
63+
def __init__(self, project_id: str, request_data: ListMessagesRequest):
64+
super(ListMessagesEndpoint, self).__init__(project_id, request_data)
65+
self.project_id = project_id
66+
self.request_data = request_data
67+
68+
def handle_response(self, response: HTTPResponse) -> ListMessagesResponse:
69+
try:
70+
super(ListMessagesEndpoint, self).handle_response(response)
71+
except ConversationException as e:
72+
raise ConversationException(
73+
message=e.args[0],
74+
response=e.http_response,
75+
is_from_server=e.is_from_server,
76+
)
77+
return self.process_response_model(response.body, ListMessagesResponse)
78+
79+
3980
class DeleteMessageEndpoint(MessageEndpoint):
4081
ENDPOINT_URL = "{origin}/v1/projects/{project_id}/messages/{message_id}"
4182
HTTP_METHOD = HTTPMethods.DELETE.value

sinch/domains/conversation/api/v1/messages_apis.py

Lines changed: 83 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
from datetime import datetime
12
from typing import Any, Dict, List, Optional, Union
2-
3+
from sinch.core.pagination import Paginator, TokenBasedPaginator
34
from sinch.domains.conversation.models.v1.messages.internal.request import (
5+
ListMessagesRequest,
46
MessageIdRequest,
57
UpdateMessageMetadataRequest,
68
SendMessageRequest,
@@ -11,12 +13,13 @@
1113
SendMessageResponse,
1214
)
1315
from sinch.domains.conversation.models.v1.messages.types import (
14-
MessagesSourceType,
1516
ConversationChannelType,
16-
ProcessingStrategyType,
17-
MetadataUpdateStrategyType,
18-
MessageQueueType,
17+
ConversationMessagesViewType,
1918
MessageContentType,
19+
MessageQueueType,
20+
MessagesSourceType,
21+
MetadataUpdateStrategyType,
22+
ProcessingStrategyType,
2023
CardMessageDict,
2124
CarouselMessageDict,
2225
ChoiceMessageDict,
@@ -58,6 +61,7 @@
5861
from sinch.domains.conversation.api.v1.internal import (
5962
DeleteMessageEndpoint,
6063
GetMessageEndpoint,
64+
ListMessagesEndpoint,
6165
UpdateMessageMetadataEndpoint,
6266
SendMessageEndpoint,
6367
)
@@ -131,6 +135,80 @@ def get(
131135
)
132136
return self._request(GetMessageEndpoint, request_data)
133137

138+
def list(
139+
self,
140+
page_size: Optional[int] = None,
141+
page_token: Optional[str] = None,
142+
conversation_id: Optional[str] = None,
143+
contact_id: Optional[str] = None,
144+
app_id: Optional[str] = None,
145+
channel_identity: Optional[str] = None,
146+
start_time: Optional[datetime] = None,
147+
end_time: Optional[datetime] = None,
148+
view: Optional[ConversationMessagesViewType] = None,
149+
messages_source: Optional[MessagesSourceType] = None,
150+
only_recipient_originated: Optional[bool] = None,
151+
channel: Optional[ConversationChannelType] = None,
152+
**kwargs,
153+
) -> Paginator[ConversationMessageResponse]:
154+
"""
155+
List messages sent or received via particular Processing Modes.
156+
The messages are ordered by their accept_time property in descending order.
157+
158+
:param page_size: Maximum number of messages to fetch. Defaults to 10, maximum is 1000.
159+
:type page_size: Optional[int]
160+
:param page_token: Next page token previously returned if any.
161+
:type page_token: Optional[str]
162+
:param conversation_id: Filter messages by conversation ID.
163+
:type conversation_id: Optional[str]
164+
:param contact_id: Filter messages by contact ID.
165+
:type contact_id: Optional[str]
166+
:param app_id: Filter messages by app ID.
167+
:type app_id: Optional[str]
168+
:param channel_identity: Channel identity of the contact.
169+
:type channel_identity: Optional[str]
170+
:param start_time: Filter messages with accept_time after this timestamp.
171+
:type start_time: Optional[datetime]
172+
:param end_time: Filter messages with accept_time before this timestamp.
173+
:type end_time: Optional[datetime]
174+
:param view: Messages view type. WITH_METADATA or WITHOUT_METADATA.
175+
:type view: Optional[ConversationMessagesViewType]
176+
:param messages_source: Specifies the message source for the request.
177+
:type messages_source: Optional[MessagesSourceType]
178+
:param only_recipient_originated: Only fetch recipient-originated messages.
179+
:type only_recipient_originated: Optional[bool]
180+
:param channel: Only fetch messages from the specified channel.
181+
:type channel: Optional[ConversationChannelType]
182+
:param **kwargs: Additional parameters for the request.
183+
:type **kwargs: dict
184+
185+
:returns: TokenBasedPaginator with ConversationMessageResponse items
186+
:rtype: Paginator[ConversationMessageResponse]
187+
188+
For detailed documentation, visit https://developers.sinch.com/docs/conversation/.
189+
"""
190+
return TokenBasedPaginator(
191+
sinch=self._sinch,
192+
endpoint=ListMessagesEndpoint(
193+
project_id=self._sinch.configuration.project_id,
194+
request_data=ListMessagesRequest(
195+
page_size=page_size,
196+
page_token=page_token,
197+
conversation_id=conversation_id,
198+
contact_id=contact_id,
199+
app_id=app_id,
200+
channel_identity=channel_identity,
201+
start_time=start_time,
202+
end_time=end_time,
203+
view=view,
204+
messages_source=messages_source,
205+
only_recipient_originated=only_recipient_originated,
206+
channel=channel,
207+
**kwargs,
208+
),
209+
),
210+
)
211+
134212
def update(
135213
self,
136214
message_id: str,
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,7 @@
1-
__all__ = []
1+
from sinch.domains.conversation.models.v1.messages.internal.list_messages_response import (
2+
ListMessagesResponse,
3+
)
4+
5+
__all__ = [
6+
"ListMessagesResponse",
7+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from typing import List, Optional
2+
from pydantic import Field, StrictStr
3+
from sinch.domains.conversation.models.v1.messages.internal.base import (
4+
BaseModelConfiguration,
5+
)
6+
from sinch.domains.conversation.models.v1.messages.response.types import (
7+
ConversationMessageResponse,
8+
)
9+
10+
11+
class ListMessagesResponse(BaseModelConfiguration):
12+
messages: Optional[List[ConversationMessageResponse]] = Field(
13+
default=None,
14+
description="List of messages associated to the referenced conversation.",
15+
)
16+
next_page_token: Optional[StrictStr] = Field(
17+
default=None,
18+
description="Token that should be included in the next request to fetch the next page.",
19+
)
20+
21+
@property
22+
def content(self):
23+
"""Returns the messages as part of the response object for pagination compatibility."""
24+
return self.messages or []

sinch/domains/conversation/models/v1/messages/internal/request/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
from sinch.domains.conversation.models.v1.messages.internal.request.list_messages_request import (
2+
ListMessagesRequest,
3+
)
14
from sinch.domains.conversation.models.v1.messages.internal.request.message_id_request import (
25
MessageIdRequest,
36
)
@@ -17,6 +20,7 @@
1720
)
1821

1922
__all__ = [
23+
"ListMessagesRequest",
2024
"MessageIdRequest",
2125
"UpdateMessageMetadataRequest",
2226
"Recipient",
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from datetime import datetime
2+
from typing import Optional
3+
from pydantic import Field, StrictInt, StrictStr
4+
from sinch.domains.conversation.models.v1.messages.internal.base import (
5+
BaseModelConfiguration,
6+
)
7+
from sinch.domains.conversation.models.v1.messages.types import (
8+
ConversationChannelType,
9+
ConversationMessagesViewType,
10+
MessagesSourceType,
11+
)
12+
13+
14+
class ListMessagesRequest(BaseModelConfiguration):
15+
"""Request model for listing messages."""
16+
17+
conversation_id: Optional[StrictStr] = Field(
18+
default=None,
19+
description="Filter messages by conversation ID.",
20+
)
21+
contact_id: Optional[StrictStr] = Field(
22+
default=None,
23+
description="Filter messages by contact ID.",
24+
)
25+
app_id: Optional[StrictStr] = Field(
26+
default=None,
27+
description="Filter messages by app ID.",
28+
)
29+
channel_identity: Optional[StrictStr] = Field(
30+
default=None,
31+
description="Channel identity of the contact.",
32+
)
33+
start_time: Optional[datetime] = Field(
34+
default=None,
35+
description="Filter messages with accept_time after this timestamp.",
36+
)
37+
end_time: Optional[datetime] = Field(
38+
default=None,
39+
description="Filter messages with accept_time before this timestamp.",
40+
)
41+
page_size: Optional[StrictInt] = Field(
42+
default=None,
43+
description="Maximum number of messages to fetch. Defaults to 10, maximum is 1000.",
44+
)
45+
page_token: Optional[StrictStr] = Field(
46+
default=None,
47+
description="Next page token previously returned if any.",
48+
)
49+
view: Optional[ConversationMessagesViewType] = Field(
50+
default=None,
51+
description="Messages view type. WITH_METADATA or WITHOUT_METADATA.",
52+
)
53+
messages_source: Optional[MessagesSourceType] = Field(
54+
default=None,
55+
description="Specifies the message source for the request.",
56+
)
57+
only_recipient_originated: Optional[bool] = Field(
58+
default=None,
59+
description="Only fetch recipient-originated messages.",
60+
)
61+
channel: Optional[ConversationChannelType] = Field(
62+
default=None,
63+
description="Only fetch messages from the specified channel.",
64+
)

sinch/domains/conversation/models/v1/messages/types/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
from sinch.domains.conversation.models.v1.messages.types.conversation_channel_type import (
88
ConversationChannelType,
99
)
10+
from sinch.domains.conversation.models.v1.messages.types.conversation_messages_view_type import (
11+
ConversationMessagesViewType,
12+
)
1013
from sinch.domains.conversation.models.v1.messages.types.conversation_direction_type import (
1114
ConversationDirectionType,
1215
)
@@ -87,6 +90,7 @@
8790
__all__ = [
8891
"AgentType",
8992
"ConversationChannelType",
93+
"ConversationMessagesViewType",
9094
"ConversationDirectionType",
9195
"ProcessingModeType",
9296
"CardHeightType",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from typing import Literal, Union
2+
from pydantic import StrictStr
3+
4+
5+
ConversationMessagesViewType = Union[
6+
Literal["WITH_METADATA", "WITHOUT_METADATA"],
7+
StrictStr,
8+
]

tests/e2e/conversation/features/steps/conversation.steps.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,29 +85,60 @@ def step_validate_send_message_response(context):
8585

8686
@when('I send a request to list the existing messages')
8787
def step_list_messages(context):
88-
pass
88+
context.list_response = context.messages.list(page_size=2)
8989

9090

9191
@then('the response contains "{count}" messages')
9292
def step_validate_message_count(context, count):
93-
pass
93+
expected_messages_count = int(count)
94+
assert len(context.list_response.content()) == expected_messages_count, (
95+
f'Expected {expected_messages_count} messages, got {len(context.list_response.content())}'
96+
)
9497

9598

9699
@when('I send a request to list all the messages')
97100
def step_list_all_messages(context):
98-
pass
101+
"""List all messages using iterator"""
102+
response = context.messages.list(page_size=2)
103+
messages_list = []
104+
105+
for message in response.iterator():
106+
messages_list.append(message)
107+
108+
context.messages_list = messages_list
99109

100110

101111
@then('the messages list contains "{count}" messages')
102112
def step_validate_total_message_count(context, count):
103-
pass
113+
expected_messages_count = int(count)
114+
assert len(context.messages_list) == expected_messages_count, (
115+
f'Expected {expected_messages_count} messages, got {len(context.messages_list)}'
116+
)
104117

105118

106119
@when('I iterate manually over the messages pages')
107120
def step_iterate_messages_pages(context):
108-
pass
121+
"""Manually iterate over messages pages"""
122+
context.list_response = context.messages.list(
123+
page_size=2,
124+
)
125+
126+
context.messages_list = []
127+
context.pages_iteration = 0
128+
reached_end_of_pages = False
129+
130+
while not reached_end_of_pages:
131+
context.messages_list.extend(context.list_response.content())
132+
context.pages_iteration += 1
133+
if context.list_response.has_next_page:
134+
context.list_response = context.list_response.next_page()
135+
else:
136+
reached_end_of_pages = True
109137

110138

111139
@then('the result contains the data from "{count}" pages')
112140
def step_validate_page_count(context, count):
113-
pass
141+
expected_pages_count = int(count)
142+
assert context.pages_iteration == expected_pages_count, (
143+
f'Expected {expected_pages_count} pages, got {context.pages_iteration}'
144+
)

0 commit comments

Comments
 (0)