Skip to content

Commit 404dc8f

Browse files
inbox person view & one-time conversation history
1 parent d918027 commit 404dc8f

6 files changed

Lines changed: 123 additions & 6 deletions

File tree

‎linkedapi/__init__.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@
8787
verify_webhook_signature,
8888
)
8989

90-
__version__ = "1.4.4"
90+
__version__ = "1.4.5"
9191
PredefinedOperation = Operation
9292

9393
__all__ = [

‎linkedapi/client.py‎

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Any
3+
from typing import Any, overload
44
from urllib.parse import urlencode
55

66
from linkedapi.config import LinkedApiConfig
@@ -55,6 +55,8 @@
5555
ConversationPollRequest,
5656
ConversationPollResult,
5757
InboxMessage,
58+
InboxPersonPollRequest,
59+
InboxPersonPollResult,
5860
InboxPollRequest,
5961
)
6062
from linkedapi.types.network import NetworkEvent, NetworkPollRequest
@@ -161,7 +163,12 @@ def poll_conversations(
161163
self,
162164
conversations: list[ConversationPollRequest],
163165
) -> MappedResponse[list[ConversationPollResult]]:
164-
"""Poll previously synced standard or Sales Navigator conversations."""
166+
"""Poll previously synced standard or Sales Navigator conversations.
167+
168+
Deprecated: legacy, kept working for existing integrations. Use ``poll_inbox`` with
169+
``InboxPersonPollRequest(person_url=...)``: it returns the same history together with the
170+
messages the inbox captured, and does not fail for people whose history was never loaded.
171+
"""
165172

166173
try:
167174
response = self.http_client.post("/conversations/poll", serialize_value(conversations))
@@ -187,15 +194,39 @@ def poll_conversations(
187194
)
188195
raise
189196

197+
@overload
198+
def poll_inbox( # type: ignore[overload-overlap]
199+
self,
200+
request: InboxPersonPollRequest,
201+
) -> MappedResponse[InboxPersonPollResult]: ...
202+
203+
@overload
190204
def poll_inbox(
191205
self,
192206
request: InboxPollRequest | None = None,
193-
) -> MappedResponse[list[InboxMessage]]:
194-
"""Read the monitored standard or Sales Navigator inbox, newest messages first."""
207+
) -> MappedResponse[list[InboxMessage]]: ...
208+
209+
def poll_inbox(
210+
self,
211+
request: InboxPollRequest | None = None,
212+
) -> MappedResponse[list[InboxMessage]] | MappedResponse[InboxPersonPollResult]:
213+
"""Read the monitored standard or Sales Navigator inbox, newest messages first.
214+
215+
Without ``person_url`` the data is the list of inbox messages across every conversation.
216+
With ``person_url`` (use ``InboxPersonPollRequest``) the data is the whole known
217+
conversation with that person: the messages the inbox captured together with the history
218+
loaded by ``sync_conversation`` / ``nv_sync_conversation``, each message once, plus
219+
``threads`` with ``history_from`` and ``history_complete``.
220+
"""
195221

196222
payload = serialize_value(request) if request is not None else {}
197223
response = self.http_client.post("/inbox/poll", payload)
198224
if response.success and response.result is not None:
225+
if request is not None and request.person_url:
226+
return MappedResponse(
227+
data=InboxPersonPollResult.model_validate(response.result),
228+
errors=[],
229+
)
199230
messages = response.result.get("messages", [])
200231
return MappedResponse(
201232
data=[InboxMessage.model_validate(item) for item in messages],

‎linkedapi/types/__init__.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@
112112
ConversationPollResult,
113113
ConversationType,
114114
InboxMessage,
115+
InboxPersonMessage,
116+
InboxPersonPollRequest,
117+
InboxPersonPollResult,
118+
InboxPersonThread,
115119
InboxPollRequest,
116120
ManageConversationParams,
117121
Message,
@@ -357,6 +361,10 @@
357361
"GetLimitsUsageParams",
358362
"IgnoreInvitationParams",
359363
"InboxMessage",
364+
"InboxPersonMessage",
365+
"InboxPersonPollRequest",
366+
"InboxPersonPollResult",
367+
"InboxPersonThread",
360368
"InboxPollRequest",
361369
"Invitation",
362370
"InvitationTargetParams",

‎linkedapi/types/message.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from typing import Literal
44

5+
from pydantic import Field
6+
57
from linkedapi.types.base import LinkedApiModel
68
from linkedapi.types.params import BaseActionParams
79

@@ -90,6 +92,11 @@ class InboxPollRequest(LinkedApiModel):
9092
since: str | None = None
9193
type: ConversationType | None = None
9294
thread_id: str | None = None
95+
person_url: str | None = None
96+
97+
98+
class InboxPersonPollRequest(InboxPollRequest):
99+
person_url: str
93100

94101

95102
class InboxMessage(LinkedApiModel):
@@ -104,3 +111,20 @@ class InboxMessage(LinkedApiModel):
104111
sender: MessageSender | None = None
105112
text: str | None = None
106113
time: str | None = None
114+
115+
116+
class InboxPersonMessage(InboxMessage):
117+
inbox_message_id: str | None = None
118+
119+
120+
class InboxPersonThread(LinkedApiModel):
121+
thread_id: str | None = None
122+
type: ConversationType | None = None
123+
person_urn: str | None = None
124+
history_from: str | None = None
125+
history_complete: bool = False
126+
127+
128+
class InboxPersonPollResult(LinkedApiModel):
129+
messages: list[InboxPersonMessage] = Field(default_factory=list)
130+
threads: list[InboxPersonThread] = Field(default_factory=list)

‎pyproject.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "linkedapi"
7-
version = "1.4.4"
7+
version = "1.4.5"
88
description = "Official synchronous Python SDK for Linked API."
99
readme = "README.md"
1010
requires-python = ">=3.10"

‎tests/test_direct_methods.py‎

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from linkedapi import (
77
ApiUsageParams,
88
ConversationPollRequest,
9+
InboxPersonPollRequest,
910
InboxPollRequest,
1011
LinkedApi,
1112
LinkedApiActionError,
@@ -108,6 +109,59 @@ def test_poll_inbox_success(fake_http_client: FakeHttpClient) -> None:
108109
assert result.errors == []
109110

110111

112+
def test_poll_inbox_with_person_url_returns_messages_and_threads(
113+
fake_http_client: FakeHttpClient,
114+
) -> None:
115+
linkedapi = LinkedApi(fake_http_client)
116+
fake_http_client.queue_response(
117+
result={
118+
"messages": [
119+
{
120+
"id": "h1",
121+
"inboxMessageId": None,
122+
"type": "st",
123+
"threadId": None,
124+
"personHashedUrl": "hashed",
125+
"sender": "them",
126+
"text": "hi",
127+
"time": "2026-01-01T10:00:00.000Z",
128+
}
129+
],
130+
"threads": [
131+
{
132+
"threadId": None,
133+
"type": "st",
134+
"personUrn": "urn:li:member:1",
135+
"historyFrom": "2026-01-01T10:00:00.000Z",
136+
"historyComplete": True,
137+
}
138+
],
139+
}
140+
)
141+
142+
result = linkedapi.poll_inbox(InboxPersonPollRequest(person_url="u", since="s"))
143+
144+
assert fake_http_client.calls[0] == ("POST", "/inbox/poll", {"since": "s", "personUrl": "u"})
145+
assert result.data is not None
146+
assert result.data.messages[0].inbox_message_id is None
147+
assert result.data.messages[0].thread_id is None
148+
assert result.data.threads[0].history_complete is True
149+
assert result.data.threads[0].history_from == "2026-01-01T10:00:00.000Z"
150+
151+
152+
def test_poll_inbox_person_url_on_base_request_is_sent_not_dropped(
153+
fake_http_client: FakeHttpClient,
154+
) -> None:
155+
linkedapi = LinkedApi(fake_http_client)
156+
fake_http_client.queue_response(result={"messages": [], "threads": []})
157+
158+
result = linkedapi.poll_inbox(InboxPollRequest(person_url="u"))
159+
160+
assert fake_http_client.calls[0] == ("POST", "/inbox/poll", {"personUrl": "u"})
161+
assert result.data is not None
162+
assert not isinstance(result.data, list)
163+
164+
111165
def test_poll_inbox_without_request_sends_empty_body(fake_http_client: FakeHttpClient) -> None:
112166
linkedapi = LinkedApi(fake_http_client)
113167
fake_http_client.queue_response(result={"messages": []})

0 commit comments

Comments
 (0)