universal-mcp-applications 0.1.28__py3-none-any.whl → 0.1.30rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of universal-mcp-applications might be problematic. Click here for more details.

@@ -1,6 +1,8 @@
1
- from typing import Any
2
- from urllib.parse import quote
1
+ import json
2
+ import os
3
+ from typing import Any, Callable, Literal
3
4
 
5
+ from loguru import logger
4
6
  from universal_mcp.applications.application import APIApplication
5
7
  from universal_mcp.integrations import Integration
6
8
 
@@ -10,229 +12,701 @@ class LinkedinApp(APIApplication):
10
12
  Base class for Universal MCP Applications.
11
13
  """
12
14
 
13
- def __init__(self, integration: Integration | None = None, **kwargs) -> None:
14
- super().__init__(name="linkedin", integration=integration, **kwargs)
15
- self.base_url = "https://api.linkedin.com"
15
+ def __init__(self, integration: Integration) -> None:
16
+ """
17
+ Initialize the LinkedinApp.
16
18
 
17
- def _get_headers(self):
19
+ Args:
20
+ integration: The integration configuration containing credentials and other settings.
21
+ It is expected that the integration provides the 'x-api-key'
22
+ via headers in `integration.get_credentials()`, e.g.,
23
+ `{"headers": {"x-api-key": "YOUR_API_KEY"}}`.
24
+ """
25
+ super().__init__(name="linkedin", integration=integration)
26
+
27
+ self._base_url = None
28
+ self.account_id = None
29
+ if self.integration:
30
+ credentials = self.integration.get_credentials()
31
+ if credentials:
32
+ self.account_id = credentials.get("account_id")
33
+
34
+ @property
35
+ def base_url(self) -> str:
36
+ if not self._base_url:
37
+ unipile_dsn = os.getenv("UNIPILE_DSN")
38
+ if not unipile_dsn:
39
+ logger.error(
40
+ "UnipileApp: UNIPILE_DSN environment variable is not set."
41
+ )
42
+ raise ValueError(
43
+ "UnipileApp: UNIPILE_DSN environment variable is required."
44
+ )
45
+ self._base_url = f"https://{unipile_dsn}"
46
+ return self._base_url
47
+
48
+ @base_url.setter
49
+ def base_url(self, base_url: str) -> None:
50
+ self._base_url = base_url
51
+ logger.info(f"UnipileApp: Base URL set to {self._base_url}")
52
+
53
+ def _get_headers(self) -> dict[str, str]:
54
+ """
55
+ Get the headers for Unipile API requests.
56
+ Overrides the base class method to use X-Api-Key.
57
+ """
18
58
  if not self.integration:
19
- raise ValueError("Integration not found")
20
- credentials = self.integration.get_credentials()
21
- if "headers" in credentials:
22
- return credentials["headers"]
59
+ logger.warning(
60
+ "UnipileApp: No integration configured, returning empty headers."
61
+ )
62
+ return {}
63
+
64
+ api_key = os.getenv("UNIPILE_API_KEY")
65
+ if not api_key:
66
+ logger.error(
67
+ "UnipileApp: API key not found in integration credentials for Unipile."
68
+ )
69
+ return { # Or return minimal headers if some calls might not need auth (unlikely for Unipile)
70
+ "Content-Type": "application/json",
71
+ "Cache-Control": "no-cache",
72
+ }
73
+
74
+ logger.debug("UnipileApp: Using X-Api-Key for authentication.")
23
75
  return {
24
- "Authorization": f"Bearer {credentials['access_token']}",
25
- "X-Restli-Protocol-Version": "2.0.0",
76
+ "x-api-key": api_key,
26
77
  "Content-Type": "application/json",
27
- "LinkedIn-Version": "202507",
78
+ "Cache-Control": "no-cache", # Often good practice for APIs
28
79
  }
29
80
 
30
- def create_post(
81
+ def list_all_chats(
82
+ self,
83
+ unread: bool | None = None,
84
+ cursor: str | None = None,
85
+ before: str | None = None, # ISO 8601 UTC datetime
86
+ after: str | None = None, # ISO 8601 UTC datetime
87
+ limit: int | None = None, # 1-250
88
+ account_type: str | None = None,
89
+ ) -> dict[str, Any]:
90
+ """
91
+ Retrieves a paginated list of all chat conversations across linked accounts. Supports filtering by unread status, date range, and account provider, distinguishing it from functions listing messages within a single chat.
92
+
93
+ Args:
94
+ unread: Filter for unread chats only or read chats only.
95
+ cursor: Pagination cursor for the next page of entries.
96
+ before: Filter for items created before this ISO 8601 UTC datetime (exclusive).
97
+ after: Filter for items created after this ISO 8601 UTC datetime (exclusive).
98
+ limit: Number of items to return (1-250).
99
+ account_type: Filter by provider (e.g., "linkedin").
100
+
101
+ Returns:
102
+ A dictionary containing a list of chat objects and a pagination cursor.
103
+
104
+ Raises:
105
+ httpx.HTTPError: If the API request fails.
106
+
107
+ Tags:
108
+ linkedin, chat, list, messaging, api
109
+ """
110
+ url = f"{self.base_url}/api/v1/chats"
111
+ params: dict[str, Any] = {}
112
+
113
+ params["account_id"] = self.account_id
114
+
115
+ if unread is not None:
116
+ params["unread"] = unread
117
+ if cursor:
118
+ params["cursor"] = cursor
119
+ if before:
120
+ params["before"] = before
121
+ if after:
122
+ params["after"] = after
123
+ if limit:
124
+ params["limit"] = limit
125
+ if account_type:
126
+ params["account_type"] = account_type
127
+
128
+
129
+ response = self._get(url, params=params)
130
+ return response.json()
131
+
132
+ def list_chat_messages(
31
133
  self,
32
- commentary: str,
33
- author: str,
34
- visibility: str = "PUBLIC",
35
- distribution: dict[str, Any] | None = None,
36
- lifecycle_state: str = "PUBLISHED",
37
- is_reshare_disabled: bool = False,
38
- ) -> dict[str, str]:
134
+ chat_id: str,
135
+ cursor: str | None = None,
136
+ before: str | None = None, # ISO 8601 UTC datetime
137
+ after: str | None = None, # ISO 8601 UTC datetime
138
+ limit: int | None = None, # 1-250
139
+ sender_id: str | None = None,
140
+ ) -> dict[str, Any]:
39
141
  """
40
- Publishes a new text post to a specified LinkedIn author's feed (person or organization). It allows configuring visibility, distribution, and lifecycle state. Upon success, it returns the unique URN and URL for the new post, distinguishing this creation operation from the update or delete functions.
142
+ Retrieves messages from a specific chat identified by `chat_id`. Supports pagination and filtering by date or sender. Unlike `list_all_messages`, which fetches from all chats, this function targets the contents of a single conversation.
41
143
 
42
144
  Args:
43
- commentary (str): The user generated commentary for the post. Supports mentions using format "@[Entity Name](urn:li:organization:123456)" and hashtags using "#keyword". Text linking to annotated entities must match the name exactly (case sensitive). For member mentions, partial name matching is supported.
44
- author (str): The URN of the author creating the post. Use "urn:li:person:{id}" for individual posts or "urn:li:organization:{id}" for company page posts. Example: "urn:li:person:wGgGaX_xbB" or "urn:li:organization:2414183"
45
- visibility (str): Controls who can view the post. Use "PUBLIC" for posts viewable by anyone on LinkedIn or "CONNECTIONS" for posts viewable by 1st-degree connections only. Defaults to "PUBLIC".
46
- distribution (dict[str, Any], optional): Distribution settings for the post. If not provided, defaults to {"feedDistribution": "MAIN_FEED", "targetEntities": [], "thirdPartyDistributionChannels": []}. feedDistribution controls where the post appears in feeds, targetEntities specifies entities to target, and thirdPartyDistributionChannels defines external distribution channels.
47
- lifecycle_state (str): The state of the post. Use "PUBLISHED" for live posts accessible to all entities, "DRAFT" for posts accessible only to author, "PUBLISH_REQUESTED" for posts submitted but processing, or "PUBLISH_FAILED" for posts that failed to publish. Defaults to "PUBLISHED".
48
- is_reshare_disabled (bool): Whether resharing is disabled by the author. Set to True to prevent other users from resharing this post, or False to allow resharing. Defaults to False.
145
+ chat_id: The ID of the chat to retrieve messages from.
146
+ cursor: Pagination cursor for the next page of entries.
147
+ before: Filter for items created before this ISO 8601 UTC datetime (exclusive).
148
+ after: Filter for items created after this ISO 8601 UTC datetime (exclusive).
149
+ limit: Number of items to return (1-250).
150
+ sender_id: Filter messages from a specific sender ID.
49
151
 
50
152
  Returns:
51
- dict[str, str]: Dictionary containing the post ID with key "post_id". Example: {"post_id": "urn:li:share:6844785523593134080"}
153
+ A dictionary containing a list of message objects and a pagination cursor.
52
154
 
53
155
  Raises:
54
- ValueError: If required parameters (commentary, author) are missing or if x-restli-id header is not found
55
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body
156
+ httpx.HTTPError: If the API request fails.
157
+
158
+ Tags:
159
+ linkedin, chat, message, list, messaging, api
160
+ """
161
+ url = f"{self.base_url}/api/v1/chats/{chat_id}/messages"
162
+ params: dict[str, Any] = {}
163
+ if cursor:
164
+ params["cursor"] = cursor
165
+ if before:
166
+ params["before"] = before
167
+ if after:
168
+ params["after"] = after
169
+ if limit:
170
+ params["limit"] = limit
171
+ if sender_id:
172
+ params["sender_id"] = sender_id
173
+
174
+ response = self._get(url, params=params)
175
+ return response.json()
176
+
177
+ def send_chat_message(
178
+ self,
179
+ chat_id: str,
180
+ text: str,
181
+ ) -> dict[str, Any]:
182
+ """
183
+ Sends a text message to a specific chat conversation using its `chat_id`. This function creates a new message via a POST request, distinguishing it from read-only functions like `list_chat_messages`. It returns the API's response, which typically confirms the successful creation of the message.
184
+
185
+ Args:
186
+ chat_id: The ID of the chat where the message will be sent.
187
+ text: The text content of the message.
188
+ attachments: Optional list of attachment objects to include with the message.
189
+
190
+ Returns:
191
+ A dictionary containing the ID of the sent message.
56
192
 
57
- Notes:
58
- Requires LinkedIn API permissions: w_member_social (for individual posts) or w_organization_social (for company posts). All requests require headers: X-Restli-Protocol-Version: 2.0.0 and LinkedIn-Version: 202507. Rate limits: 150 requests per day per member, 100,000 requests per day per application. The Posts API replaces the deprecated ugcPosts API.
193
+ Raises:
194
+ httpx.HTTPError: If the API request fails.
59
195
 
60
196
  Tags:
61
- posts, important
197
+ linkedin, chat, message, send, create, messaging, api
62
198
  """
63
- # Set default distribution if not provided
64
- if distribution is None:
65
- distribution = {
66
- "feedDistribution": "MAIN_FEED",
67
- "targetEntities": [],
68
- "thirdPartyDistributionChannels": [],
69
- }
199
+ url = f"{self.base_url}/api/v1/chats/{chat_id}/messages"
200
+ payload: dict[str, Any] = {"text": text}
70
201
 
71
- request_body_data = {
72
- "author": author,
73
- "commentary": commentary,
74
- "visibility": visibility,
75
- "distribution": distribution,
76
- "lifecycleState": lifecycle_state,
77
- "isReshareDisabledByAuthor": is_reshare_disabled,
78
- }
202
+ response = self._post(url, data=payload)
203
+ return response.json()
79
204
 
80
- url = f"{self.base_url}/rest/posts"
81
- query_params = {}
205
+ def retrieve_chat(self, chat_id: str) -> dict[str, Any]:
206
+ """
207
+ Retrieves a single chat's details using its Unipile or provider-specific ID. This function is distinct from `list_all_chats`, which returns a collection, by targeting one specific conversation.
82
208
 
83
- response = self._post(
84
- url,
85
- data=request_body_data,
86
- params=query_params,
87
- )
209
+ Args:
210
+ chat_id: The Unipile or provider ID of the chat.
88
211
 
89
- self._handle_response(response)
212
+ Returns:
213
+ A dictionary containing the chat object details.
90
214
 
91
- post_id = response.headers.get("x-restli-id")
92
- if not post_id:
93
- raise ValueError("x-restli-id header not found in response")
215
+ Raises:
216
+ httpx.HTTPError: If the API request fails.
94
217
 
95
- return {
96
- "post_urn": post_id,
97
- "post_url": f"https://www.linkedin.com/feed/update/{post_id}",
98
- }
218
+ Tags:
219
+ linkedin, chat, retrieve, get, messaging, api
220
+ """
221
+ url = f"{self.base_url}/api/v1/chats/{chat_id}"
222
+ params: dict[str, Any] = {}
223
+ if self.account_id:
224
+ params["account_id"] = self.account_id
225
+
226
+ response = self._get(url, params=params)
227
+ return response.json()
99
228
 
100
- def get_authenticated_user_profile(self) -> dict[str, Any]:
229
+ def list_all_messages(
230
+ self,
231
+ cursor: str | None = None,
232
+ before: str | None = None, # ISO 8601 UTC datetime
233
+ after: str | None = None, # ISO 8601 UTC datetime
234
+ limit: int | None = None, # 1-250
235
+ sender_id: str | None = None,
236
+ ) -> dict[str, Any]:
101
237
  """
102
- Retrieves the authenticated user's profile from the LinkedIn `/v2/userinfo` endpoint. Using credentials from the active integration, it returns a dictionary with basic user details like name and email. This function is for fetching user data, distinct from others that create, update, or delete posts.
238
+ Retrieves a paginated list of messages from all chats associated with the account. Unlike `list_chat_messages` which targets a specific conversation, this function provides a global message view, filterable by sender and date range.
239
+
240
+ Args:
241
+ cursor: Pagination cursor.
242
+ before: Filter for items created before this ISO 8601 UTC datetime.
243
+ after: Filter for items created after this ISO 8601 UTC datetime.
244
+ limit: Number of items to return (1-250).
245
+ sender_id: Filter messages from a specific sender.
103
246
 
104
247
  Returns:
105
- dict[str, Any]: Dictionary containing your LinkedIn profile information.
248
+ A dictionary containing a list of message objects and a pagination cursor.
106
249
 
107
250
  Raises:
108
- ValueError: If integration is not found
109
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body
251
+ httpx.HTTPError: If the API request fails.
110
252
 
111
253
  Tags:
112
- profile, info
254
+ linkedin, message, list, all_messages, messaging, api
255
+ """
256
+ url = f"{self.base_url}/api/v1/messages"
257
+ params: dict[str, Any] = {}
258
+ if cursor:
259
+ params["cursor"] = cursor
260
+ if before:
261
+ params["before"] = before
262
+ if after:
263
+ params["after"] = after
264
+ if limit:
265
+ params["limit"] = limit
266
+ if sender_id:
267
+ params["sender_id"] = sender_id
268
+ if self.account_id:
269
+ params["account_id"] = self.account_id
270
+
271
+ response = self._get(url, params=params)
272
+ return response.json()
273
+
274
+ def list_all_accounts(
275
+ self,
276
+ cursor: str | None = None,
277
+ limit: int | None = None, # 1-259 according to spec
278
+ ) -> dict[str, Any]:
113
279
  """
114
- url = f"{self.base_url}/v2/userinfo"
115
- query_params = {}
280
+ Retrieves a paginated list of all social media accounts linked to the Unipile service. This is crucial for obtaining the `account_id` required by other methods to specify which user account should perform an action, like sending a message or retrieving user-specific posts.
116
281
 
117
- response = self._get(
118
- url,
119
- params=query_params,
120
- )
282
+ Args:
283
+ cursor: Pagination cursor.
284
+ limit: Number of items to return (1-259).
121
285
 
122
- return self._handle_response(response)
286
+ Returns:
287
+ A dictionary containing a list of account objects and a pagination cursor.
288
+
289
+ Raises:
290
+ httpx.HTTPError: If the API request fails.
123
291
 
124
- def delete_post(self, post_urn: str) -> dict[str, str]:
292
+ Tags:
293
+ linkedin, account, list, unipile, api, important
294
+ """
295
+ url = f"{self.base_url}/api/v1/accounts"
296
+ params: dict[str, Any] = {}
297
+ if cursor:
298
+ params["cursor"] = cursor
299
+ if limit:
300
+ params["limit"] = limit
301
+
302
+ response = self._get(url, params=params)
303
+ return response.json()
304
+
305
+ # def retrieve_linked_account(self) -> dict[str, Any]:
306
+ # """
307
+ # Retrieves details for the account linked to Unipile. It fetches metadata about the connection itself (e.g., a linked LinkedIn account), differentiating it from `retrieve_user_profile` which fetches a user's profile from the external platform.
308
+
309
+ # Returns:
310
+ # A dictionary containing the account object details.
311
+
312
+ # Raises:
313
+ # httpx.HTTPError: If the API request fails.
314
+
315
+ # Tags:
316
+ # linkedin, account, retrieve, get, unipile, api, important
317
+ # """
318
+ # url = f"{self.base_url}/api/v1/accounts/{self.account_id}"
319
+ # response = self._get(url)
320
+ # return response.json()
321
+
322
+ def list_profile_posts(
323
+ self,
324
+ identifier: str, # User or Company provider internal ID
325
+ cursor: str | None = None,
326
+ limit: int | None = None, # 1-100 (spec says max 250)
327
+ is_company: bool | None = None,
328
+ ) -> dict[str, Any]:
125
329
  """
126
- Deletes a LinkedIn post identified by its unique Uniform Resource Name (URN). This function sends a DELETE request to the API, permanently removing the content. Upon a successful HTTP 204 response, it returns a dictionary confirming the post's deletion status.
330
+ Retrieves a paginated list of posts from a specific user or company profile using their provider ID. An authorizing `account_id` is required, and the `is_company` flag must specify the entity type, distinguishing this from `retrieve_post` which fetches a single post by its own ID.
127
331
 
128
332
  Args:
129
- post_urn (str): The URN of the post to delete. Can be either a ugcPostUrn (urn:li:ugcPost:{id}) or shareUrn (urn:li:share:{id}).
333
+ identifier: The entity's provider internal ID (LinkedIn ID).
334
+ cursor: Pagination cursor.
335
+ limit: Number of items to return (1-100, as per Unipile example, though spec allows up to 250).
336
+ is_company: Boolean indicating if the identifier is for a company.
130
337
 
131
338
  Returns:
132
- dict[str, str]: Dictionary containing the deletion status. Example: {"status": "deleted", "post_urn": "urn:li:share:6844785523593134080"}
339
+ A dictionary containing a list of post objects and pagination details.
133
340
 
134
341
  Raises:
135
- ValueError: If required parameter (post_urn) is missing or if integration is not found
136
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body
342
+ httpx.HTTPError: If the API request fails.
343
+
344
+ Tags:
345
+ linkedin, post, list, user_posts, company_posts, content, api, important
346
+ """
347
+ url = f"{self.base_url}/api/v1/users/{identifier}/posts"
348
+ params: dict[str, Any] = {"account_id": self.account_id}
349
+ if cursor:
350
+ params["cursor"] = cursor
351
+ if limit:
352
+ params["limit"] = limit
353
+ if is_company is not None:
354
+ params["is_company"] = is_company
355
+
356
+ response = self._get(url, params=params)
357
+ return response.json()
358
+
359
+ def retrieve_own_profile(self) -> dict[str, Any]:
360
+ """
361
+ Retrieves the profile details for the user associated with the Unipile account. This function targets the API's 'me' endpoint to fetch the authenticated user's profile, distinct from `retrieve_user_profile` which fetches profiles of other users by their public identifier.
137
362
 
363
+ Returns:
364
+ A dictionary containing the user's profile details.
365
+
366
+ Raises:
367
+ httpx.HTTPError: If the API request fails.
138
368
 
139
369
  Tags:
140
- posts, important
370
+ linkedin, user, profile, me, retrieve, get, api
371
+ """
372
+ url = f"{self.base_url}/api/v1/users/me"
373
+ params: dict[str, Any] = {"account_id": self.account_id}
374
+ response = self._get(url, params=params)
375
+ return response.json()
376
+
377
+ def retrieve_post(self, post_id: str) -> dict[str, Any]:
141
378
  """
142
- url = f"{self.base_url}/rest/posts/{quote(post_urn, safe='')}"
143
- query_params = {}
379
+ Fetches a specific post's details by its unique ID. Unlike `list_profile_posts`, which retrieves a collection of posts from a user or company profile, this function targets one specific post and returns its full object.
144
380
 
145
- response = self._delete(
146
- url,
147
- params=query_params,
148
- )
381
+ Args:
382
+ post_id: The ID of the post to retrieve.
383
+
384
+ Returns:
385
+ A dictionary containing the post details.
386
+
387
+ Raises:
388
+ httpx.HTTPError: If the API request fails.
149
389
 
150
- if response.status_code == 204:
151
- return {"status": "deleted", "post_urn": post_urn}
152
- else:
153
- return self._handle_response(response)
390
+ Tags:
391
+ linkedin, post, retrieve, get, content, api, important
392
+ """
393
+ url = f"{self.base_url}/api/v1/posts/{post_id}"
394
+ params: dict[str, Any] = {"account_id": self.account_id}
395
+ response = self._get(url, params=params)
396
+ return response.json()
154
397
 
155
- def update_post(
398
+ def list_post_comments(
156
399
  self,
157
- post_urn: str,
158
- commentary: str | None = None,
159
- content_call_to_action_label: str | None = None,
160
- content_landing_page: str | None = None,
161
- lifecycle_state: str | None = None,
162
- ad_context_name: str | None = None,
163
- ad_context_status: str | None = None,
164
- ) -> dict[str, str]:
400
+ post_id: str,
401
+ comment_id: str | None = None,
402
+ cursor: str | None = None,
403
+ limit: int | None = None,
404
+ ) -> dict[str, Any]:
165
405
  """
166
- Modifies an existing LinkedIn post, identified by its URN, by performing a partial update. It selectively changes attributes like commentary or ad context, distinguishing it from `create_post` which creates new content. Returns a confirmation dictionary upon successful completion.
406
+ Fetches comments for a specific post. Providing an optional `comment_id` retrieves threaded replies instead of top-level comments. This read-only operation contrasts with `create_post_comment`, which publishes new comments, and `list_content_reactions`, which retrieves 'likes'.
167
407
 
168
408
  Args:
169
- post_urn (str): The URN of the post to update. Can be either a ugcPostUrn (urn:li:ugcPost:{id}) or shareUrn (urn:li:share:{id}).
170
- commentary (str | None, optional): The user generated commentary of this post in little format.
171
- content_call_to_action_label (str | None, optional): The call to action label that a member can act on that opens a landing page.
172
- content_landing_page (str | None, optional): URL of the landing page.
173
- lifecycle_state (str | None, optional): The state of the content. Can be DRAFT, PUBLISHED, PUBLISH_REQUESTED, or PUBLISH_FAILED.
174
- ad_context_name (str | None, optional): Update the name of the sponsored content.
175
- ad_context_status (str | None, optional): Update the status of the sponsored content.
409
+ post_id: The social ID of the post.
410
+ comment_id: If provided, retrieves replies to this comment ID instead of top-level comments.
411
+ cursor: Pagination cursor.
412
+ limit: Number of comments to return. (OpenAPI spec shows type string, passed as string if provided).
176
413
 
177
414
  Returns:
178
- dict[str, str]: Dictionary containing the update status. Example: {"status": "updated", "post_urn": "urn:li:share:6844785523593134080"}
415
+ A dictionary containing a list of comment objects and pagination details.
179
416
 
180
417
  Raises:
181
- ValueError: If required parameter (post_urn) is missing or if integration is not found
182
- HTTPStatusError: Raised when the API request fails with detailed error information including status code and response body
418
+ httpx.HTTPError: If the API request fails.
419
+
420
+ Tags:
421
+ linkedin, post, comment, list, content, api, important
422
+ """
423
+ url = f"{self.base_url}/api/v1/posts/{post_id}/comments"
424
+ params: dict[str, Any] = {"account_id": self.account_id}
425
+ if cursor:
426
+ params["cursor"] = cursor
427
+ if limit is not None:
428
+ params["limit"] = str(limit)
429
+ if comment_id:
430
+ params["comment_id"] = comment_id
431
+
432
+ response = self._get(url, params=params)
433
+ return response.json()
183
434
 
435
+ def create_post(
436
+ self,
437
+ text: str,
438
+ mentions: list[dict[str, Any]] | None = None,
439
+ external_link: str | None = None,
440
+ ) -> dict[str, Any]:
441
+ """
442
+ Publishes a new top-level post from the account, including text, user mentions, and an external link. This function creates original content, distinguishing it from `create_post_comment` which adds replies to existing posts.
184
443
 
444
+ Args:
445
+ text: The main text content of the post.
446
+ mentions: Optional list of dictionaries, each representing a mention.
447
+ Example: `[{"entity_urn": "urn:li:person:...", "start_index": 0, "end_index": 5}]`
448
+ external_link: Optional string, an external URL that should be displayed within a card.
185
449
 
450
+ Returns:
451
+ A dictionary containing the ID of the created post.
452
+
453
+ Raises:
454
+ httpx.HTTPError: If the API request fails.
186
455
 
187
456
  Tags:
188
- posts, update, important
457
+ linkedin, post, create, share, content, api, important
189
458
  """
190
- url = f"{self.base_url}/rest/posts/{quote(post_urn, safe='')}"
191
- query_params = {}
459
+ url = f"{self.base_url}/api/v1/posts"
192
460
 
193
- # Build the patch data
194
- patch_data = {"$set": {}}
195
- ad_context_data = {}
461
+ params: dict[str, str] = {
462
+ "account_id": self.account_id,
463
+ "text": text,
464
+ }
196
465
 
197
- if commentary is not None:
198
- patch_data["$set"]["commentary"] = commentary
199
- if content_call_to_action_label is not None:
200
- patch_data["$set"]["contentCallToActionLabel"] = (
201
- content_call_to_action_label
202
- )
203
- if content_landing_page is not None:
204
- patch_data["$set"]["contentLandingPage"] = content_landing_page
205
- if lifecycle_state is not None:
206
- patch_data["$set"]["lifecycleState"] = lifecycle_state
466
+ if mentions:
467
+ params["mentions"] = mentions
468
+ if external_link:
469
+ params["external_link"] = external_link
470
+
471
+ response = self._post(url, data=params)
472
+ return response.json()
473
+
474
+ def list_content_reactions(
475
+ self,
476
+ post_id: str,
477
+ comment_id: str | None = None,
478
+ cursor: str | None = None,
479
+ limit: int | None = None,
480
+ ) -> dict[str, Any]:
481
+ """
482
+ Retrieves a paginated list of reactions for a given post or, optionally, a specific comment. This read-only operation uses the account for the request, distinguishing it from the `create_reaction` function which adds new reactions.
483
+
484
+ Args:
485
+ post_id: The social ID of the post.
486
+ comment_id: If provided, retrieves reactions for this comment ID.
487
+ cursor: Pagination cursor.
488
+ limit: Number of reactions to return (1-100, spec max 250).
489
+
490
+ Returns:
491
+ A dictionary containing a list of reaction objects and pagination details.
492
+
493
+ Raises:
494
+ httpx.HTTPError: If the API request fails.
495
+
496
+ Tags:
497
+ linkedin, post, reaction, list, like, content, api
498
+ """
499
+ url = f"{self.base_url}/api/v1/posts/{post_id}/reactions"
500
+ params: dict[str, Any] = {"account_id": self.account_id}
501
+ if cursor:
502
+ params["cursor"] = cursor
503
+ if limit:
504
+ params["limit"] = limit
505
+ if comment_id:
506
+ params["comment_id"] = comment_id
507
+
508
+ response = self._get(url, params=params)
509
+ return response.json()
510
+
511
+ def create_post_comment(
512
+ self,
513
+ post_social_id: str,
514
+ text: str,
515
+ comment_id: str | None = None, # If provided, replies to a specific comment
516
+ mentions_body: list[dict[str, Any]] | None = None,
517
+ ) -> dict[str, Any]:
518
+ """
519
+ Publishes a comment on a specified post. By providing an optional `comment_id`, it creates a threaded reply to an existing comment instead of a new top-level one. This function's dual capability distinguishes it from `list_post_comments`, which only retrieves comments and their replies.
520
+
521
+ Args:
522
+ post_social_id: The social ID of the post to comment on.
523
+ text: The text content of the comment (passed as a query parameter).
524
+ Supports Unipile's mention syntax like "Hey {{0}}".
525
+ comment_id: Optional ID of a specific comment to reply to instead of commenting on the post.
526
+ mentions_body: Optional list of mention objects for the request body if needed.
527
+
528
+ Returns:
529
+ A dictionary, likely confirming comment creation. (Structure depends on actual API response)
530
+
531
+ Raises:
532
+ httpx.HTTPError: If the API request fails.
207
533
 
208
- if ad_context_name is not None or ad_context_status is not None:
209
- ad_context_data["$set"] = {}
210
- if ad_context_name is not None:
211
- ad_context_data["$set"]["dscName"] = ad_context_name
212
- if ad_context_status is not None:
213
- ad_context_data["$set"]["dscStatus"] = ad_context_status
214
- patch_data["adContext"] = ad_context_data
534
+ Tags:
535
+ linkedin, post, comment, create, content, api, important
536
+ """
537
+ url = f"{self.base_url}/api/v1/posts/{post_social_id}/comments"
538
+ params: dict[str, Any] = {
539
+ "account_id": self.account_id,
540
+ "text": text,
541
+ }
215
542
 
216
- request_body_data = {"patch": patch_data}
543
+ if comment_id:
544
+ params["comment_id"] = comment_id
217
545
 
218
- response = self._post(
219
- url,
220
- data=request_body_data,
221
- params=query_params,
222
- )
546
+ if mentions_body:
547
+ params = {"mentions": mentions_body}
223
548
 
224
- if response.status_code == 204:
225
- return {"status": "updated", "post_urn": post_urn}
226
- else:
227
- return self._handle_response(response)
549
+ response = self._post(url, data=params)
550
+
551
+ try:
552
+ return response.json()
553
+ except json.JSONDecodeError:
554
+ return {
555
+ "status": response.status_code,
556
+ "message": "Comment action processed.",
557
+ }
228
558
 
229
- def list_tools(self):
559
+ def create_reaction(
560
+ self,
561
+ post_social_id: str,
562
+ reaction_type: Literal[
563
+ "like", "celebrate", "love", "insightful", "funny", "support"
564
+ ],
565
+ comment_id: str | None = None,
566
+ ) -> dict[str, Any]:
230
567
  """
231
- Lists the available tools (methods) for this application.
568
+ Adds a specified reaction (e.g., 'like', 'love') to a LinkedIn post or, optionally, to a specific comment. This function performs a POST request to create the reaction, differentiating it from `list_content_reactions` which only retrieves existing ones.
569
+
570
+ Args:
571
+ post_social_id: The social ID of the post or comment to react to.
572
+ reaction_type: The type of reaction. Valid values are "like", "celebrate", "love", "insightful", "funny", or "support".
573
+ comment_id: Optional ID of a specific comment to react to instead of the post.
574
+
575
+ Returns:
576
+ A dictionary, likely confirming the reaction. (Structure depends on actual API response)
577
+
578
+ Raises:
579
+ httpx.HTTPError: If the API request fails.
580
+
581
+ Tags:
582
+ linkedin, post, reaction, create, like, content, api, important
232
583
  """
584
+ url = f"{self.base_url}/api/v1/posts/reaction"
585
+
586
+ params: dict[str, str] = {
587
+ "account_id": self.account_id,
588
+ "post_id": post_social_id,
589
+ "reaction_type": reaction_type,
590
+ }
591
+
592
+ if comment_id:
593
+ params["comment_id"] = comment_id
594
+
595
+ response = self._post(url, data=params)
596
+
597
+ try:
598
+ return response.json()
599
+ except json.JSONDecodeError:
600
+ return {
601
+ "status": response.status_code,
602
+ "message": "Reaction action processed.",
603
+ }
604
+
605
+ def search(
606
+ self,
607
+ category: Literal["people", "companies", "posts", "jobs"],
608
+ cursor: str | None = None,
609
+ limit: int | None = None,
610
+ keywords: str | None = None,
611
+ date_posted: Literal["past_day", "past_week", "past_month"] | None = None,
612
+ sort_by: Literal["relevance", "date"] = "relevance",
613
+ minimum_salary_value: int = 40,
614
+ ) -> dict[str, Any]:
615
+ """
616
+ Performs a comprehensive LinkedIn search for people, companies, posts, or jobs using keywords.
617
+ Supports pagination and targets either the classic or Sales Navigator API for posts.
618
+ For people, companies, and jobs, it uses the classic API.
619
+
620
+ Args:
621
+ category: Type of search to perform. Valid values are "people", "companies", "posts", or "jobs".
622
+ cursor: Pagination cursor for the next page of entries.
623
+ limit: Number of items to return (up to 50 for Classic search).
624
+ keywords: Keywords to search for.
625
+ date_posted: Filter by when the post was posted (posts only). Valid values are "past_day", "past_week", or "past_month".
626
+ sort_by: How to sort the results (for posts and jobs). Valid values are "relevance" or "date".
627
+ minimum_salary_value: The minimum salary to filter for (jobs only).
628
+
629
+ Returns:
630
+ A dictionary containing search results and pagination details.
631
+
632
+ Raises:
633
+ httpx.HTTPError: If the API request fails.
634
+ ValueError: If the category is empty.
635
+
636
+ Tags:
637
+ linkedin, search, people, companies, posts, jobs, api, important
638
+ """
639
+ if not category:
640
+ raise ValueError("Category cannot be empty.")
641
+
642
+ url = f"{self.base_url}/api/v1/linkedin/search"
643
+
644
+ params: dict[str, Any] = {"account_id": self.account_id}
645
+ if cursor:
646
+ params["cursor"] = cursor
647
+ if limit is not None:
648
+ params["limit"] = limit
649
+
650
+ payload: dict[str, Any] = {"api": "classic", "category": category}
651
+
652
+ if keywords:
653
+ payload["keywords"] = keywords
654
+
655
+ if category == "posts":
656
+ if date_posted:
657
+ payload["date_posted"] = date_posted
658
+ if sort_by:
659
+ payload["sort_by"] = sort_by
660
+
661
+ elif category == "jobs":
662
+ payload["minimum_salary"] = {
663
+ "currency": "USD",
664
+ "value": minimum_salary_value,
665
+ }
666
+ if sort_by:
667
+ payload["sort_by"] = sort_by
668
+
669
+ response = self._post(url, params=params, data=payload)
670
+ return self._handle_response(response)
671
+
672
+ def retrieve_user_profile(self, identifier: str) -> dict[str, Any]:
673
+ """
674
+ Retrieves a specific LinkedIn user's profile using their public or internal ID. Unlike `retrieve_own_profile`, which fetches the authenticated user's details, this function targets and returns data for any specified third-party user profile on the platform.
675
+
676
+ Args:
677
+ identifier: Can be the provider's internal id OR the provider's public id of the requested user.For example, for https://www.linkedin.com/in/manojbajaj95/, the identifier is "manojbajaj95".
678
+
679
+ Returns:
680
+ A dictionary containing the user's profile details.
681
+
682
+ Raises:
683
+ httpx.HTTPError: If the API request fails.
684
+
685
+ Tags:
686
+ linkedin, user, profile, retrieve, get, api, important
687
+ """
688
+ url = f"{self.base_url}/api/v1/users/{identifier}"
689
+ params: dict[str, Any] = {"account_id": self.account_id}
690
+ response = self._get(url, params=params)
691
+ return self._handle_response(response)
692
+
693
+ def list_tools(self) -> list[Callable]:
233
694
  return [
695
+ self.list_all_chats,
696
+ self.list_chat_messages,
697
+ self.send_chat_message,
698
+ self.retrieve_chat,
699
+ self.list_all_messages,
700
+ self.list_all_accounts,
701
+ # self.retrieve_linked_account,
702
+ self.list_profile_posts,
703
+ self.retrieve_own_profile,
704
+ self.retrieve_user_profile,
705
+ self.retrieve_post,
706
+ self.list_post_comments,
234
707
  self.create_post,
235
- self.get_authenticated_user_profile,
236
- self.delete_post,
237
- self.update_post,
708
+ self.list_content_reactions,
709
+ self.create_post_comment,
710
+ self.create_reaction,
711
+ self.search,
238
712
  ]