universal-mcp-applications 0.1.22__py3-none-any.whl → 0.1.39rc8__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.

Files changed (120) hide show
  1. universal_mcp/applications/ahrefs/app.py +92 -238
  2. universal_mcp/applications/airtable/app.py +23 -122
  3. universal_mcp/applications/apollo/app.py +122 -475
  4. universal_mcp/applications/asana/app.py +605 -1755
  5. universal_mcp/applications/aws_s3/app.py +36 -103
  6. universal_mcp/applications/bill/app.py +644 -2055
  7. universal_mcp/applications/box/app.py +1246 -4159
  8. universal_mcp/applications/braze/app.py +410 -1476
  9. universal_mcp/applications/browser_use/README.md +15 -1
  10. universal_mcp/applications/browser_use/__init__.py +1 -0
  11. universal_mcp/applications/browser_use/app.py +94 -37
  12. universal_mcp/applications/cal_com_v2/app.py +207 -625
  13. universal_mcp/applications/calendly/app.py +103 -242
  14. universal_mcp/applications/canva/app.py +75 -140
  15. universal_mcp/applications/clickup/app.py +331 -798
  16. universal_mcp/applications/coda/app.py +240 -520
  17. universal_mcp/applications/confluence/app.py +497 -1285
  18. universal_mcp/applications/contentful/app.py +36 -151
  19. universal_mcp/applications/crustdata/app.py +42 -121
  20. universal_mcp/applications/dialpad/app.py +451 -924
  21. universal_mcp/applications/digitalocean/app.py +2071 -6082
  22. universal_mcp/applications/domain_checker/app.py +3 -54
  23. universal_mcp/applications/e2b/app.py +14 -64
  24. universal_mcp/applications/elevenlabs/app.py +9 -47
  25. universal_mcp/applications/exa/README.md +8 -4
  26. universal_mcp/applications/exa/app.py +408 -186
  27. universal_mcp/applications/falai/app.py +24 -101
  28. universal_mcp/applications/figma/app.py +91 -175
  29. universal_mcp/applications/file_system/app.py +2 -13
  30. universal_mcp/applications/firecrawl/app.py +186 -163
  31. universal_mcp/applications/fireflies/app.py +59 -281
  32. universal_mcp/applications/fpl/app.py +92 -529
  33. universal_mcp/applications/fpl/utils/fixtures.py +15 -49
  34. universal_mcp/applications/fpl/utils/helper.py +25 -89
  35. universal_mcp/applications/fpl/utils/league_utils.py +20 -64
  36. universal_mcp/applications/ghost_content/app.py +66 -175
  37. universal_mcp/applications/github/app.py +28 -65
  38. universal_mcp/applications/gong/app.py +140 -300
  39. universal_mcp/applications/google_calendar/app.py +26 -78
  40. universal_mcp/applications/google_docs/app.py +324 -354
  41. universal_mcp/applications/google_drive/app.py +194 -793
  42. universal_mcp/applications/google_gemini/app.py +29 -64
  43. universal_mcp/applications/google_mail/README.md +1 -0
  44. universal_mcp/applications/google_mail/app.py +93 -214
  45. universal_mcp/applications/google_searchconsole/app.py +25 -58
  46. universal_mcp/applications/google_sheet/app.py +174 -623
  47. universal_mcp/applications/google_sheet/helper.py +26 -53
  48. universal_mcp/applications/hashnode/app.py +57 -269
  49. universal_mcp/applications/heygen/app.py +77 -155
  50. universal_mcp/applications/http_tools/app.py +10 -32
  51. universal_mcp/applications/hubspot/README.md +1 -1
  52. universal_mcp/applications/hubspot/app.py +7508 -99
  53. universal_mcp/applications/jira/app.py +2419 -8334
  54. universal_mcp/applications/klaviyo/app.py +737 -1619
  55. universal_mcp/applications/linkedin/README.md +23 -4
  56. universal_mcp/applications/linkedin/app.py +861 -155
  57. universal_mcp/applications/mailchimp/app.py +696 -1851
  58. universal_mcp/applications/markitdown/app.py +8 -20
  59. universal_mcp/applications/miro/app.py +333 -815
  60. universal_mcp/applications/ms_teams/app.py +85 -207
  61. universal_mcp/applications/neon/app.py +144 -250
  62. universal_mcp/applications/notion/app.py +36 -51
  63. universal_mcp/applications/onedrive/README.md +24 -0
  64. universal_mcp/applications/onedrive/__init__.py +1 -0
  65. universal_mcp/applications/onedrive/app.py +316 -0
  66. universal_mcp/applications/openai/app.py +42 -165
  67. universal_mcp/applications/outlook/README.md +22 -9
  68. universal_mcp/applications/outlook/app.py +606 -262
  69. universal_mcp/applications/perplexity/README.md +2 -1
  70. universal_mcp/applications/perplexity/app.py +162 -20
  71. universal_mcp/applications/pipedrive/app.py +1021 -3331
  72. universal_mcp/applications/posthog/app.py +272 -541
  73. universal_mcp/applications/reddit/app.py +88 -204
  74. universal_mcp/applications/resend/app.py +41 -107
  75. universal_mcp/applications/retell/app.py +23 -50
  76. universal_mcp/applications/rocketlane/app.py +250 -963
  77. universal_mcp/applications/scraper/README.md +7 -4
  78. universal_mcp/applications/scraper/app.py +245 -283
  79. universal_mcp/applications/semanticscholar/app.py +36 -78
  80. universal_mcp/applications/semrush/app.py +43 -77
  81. universal_mcp/applications/sendgrid/app.py +826 -1576
  82. universal_mcp/applications/sentry/app.py +444 -1079
  83. universal_mcp/applications/serpapi/app.py +40 -143
  84. universal_mcp/applications/sharepoint/README.md +16 -14
  85. universal_mcp/applications/sharepoint/app.py +245 -154
  86. universal_mcp/applications/shopify/app.py +1743 -4479
  87. universal_mcp/applications/shortcut/app.py +272 -534
  88. universal_mcp/applications/slack/app.py +58 -109
  89. universal_mcp/applications/spotify/app.py +206 -405
  90. universal_mcp/applications/supabase/app.py +174 -283
  91. universal_mcp/applications/tavily/app.py +2 -2
  92. universal_mcp/applications/trello/app.py +853 -2816
  93. universal_mcp/applications/twilio/app.py +14 -50
  94. universal_mcp/applications/twitter/api_segments/compliance_api.py +4 -14
  95. universal_mcp/applications/twitter/api_segments/dm_conversations_api.py +6 -18
  96. universal_mcp/applications/twitter/api_segments/likes_api.py +1 -3
  97. universal_mcp/applications/twitter/api_segments/lists_api.py +5 -15
  98. universal_mcp/applications/twitter/api_segments/trends_api.py +1 -3
  99. universal_mcp/applications/twitter/api_segments/tweets_api.py +9 -31
  100. universal_mcp/applications/twitter/api_segments/usage_api.py +1 -5
  101. universal_mcp/applications/twitter/api_segments/users_api.py +14 -42
  102. universal_mcp/applications/whatsapp/app.py +35 -186
  103. universal_mcp/applications/whatsapp/audio.py +2 -6
  104. universal_mcp/applications/whatsapp/whatsapp.py +17 -51
  105. universal_mcp/applications/whatsapp_business/app.py +86 -299
  106. universal_mcp/applications/wrike/app.py +80 -153
  107. universal_mcp/applications/yahoo_finance/app.py +19 -65
  108. universal_mcp/applications/youtube/app.py +120 -306
  109. universal_mcp/applications/zenquotes/app.py +4 -4
  110. {universal_mcp_applications-0.1.22.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/METADATA +4 -2
  111. {universal_mcp_applications-0.1.22.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/RECORD +113 -117
  112. {universal_mcp_applications-0.1.22.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/WHEEL +1 -1
  113. universal_mcp/applications/hubspot/api_segments/__init__.py +0 -0
  114. universal_mcp/applications/hubspot/api_segments/api_segment_base.py +0 -54
  115. universal_mcp/applications/hubspot/api_segments/crm_api.py +0 -7337
  116. universal_mcp/applications/hubspot/api_segments/marketing_api.py +0 -1467
  117. universal_mcp/applications/unipile/README.md +0 -28
  118. universal_mcp/applications/unipile/__init__.py +0 -1
  119. universal_mcp/applications/unipile/app.py +0 -1077
  120. {universal_mcp_applications-0.1.22.dist-info → universal_mcp_applications-0.1.39rc8.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,10 @@
1
- from typing import Any
2
- from urllib.parse import quote
1
+ import os
2
+ from collections.abc import Callable
3
+ from typing import Any, Literal
4
+ import requests
3
5
 
4
- from universal_mcp.applications.application import APIApplication
6
+ from loguru import logger
7
+ from universal_mcp.applications.application import APIApplication, BaseApplication
5
8
  from universal_mcp.integrations import Integration
6
9
 
7
10
 
@@ -10,229 +13,932 @@ class LinkedinApp(APIApplication):
10
13
  Base class for Universal MCP Applications.
11
14
  """
12
15
 
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"
16
+ def __init__(self, integration: Integration) -> None:
17
+ """
18
+ Initialize the LinkedinApp.
16
19
 
17
- def _get_headers(self):
20
+ Args:
21
+ integration: The integration configuration containing credentials and other settings.
22
+ It is expected that the integration provides the 'x-api-key'
23
+ via headers in `integration.get_credentials()`, e.g.,
24
+ `{"headers": {"x-api-key": "YOUR_API_KEY"}}`.
25
+ """
26
+ super().__init__(name="linkedin", integration=integration)
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("UnipileApp: UNIPILE_DSN environment variable is not set.")
40
+ raise ValueError("UnipileApp: UNIPILE_DSN environment variable is required.")
41
+ self._base_url = f"https://{unipile_dsn}"
42
+ return self._base_url
43
+
44
+ @base_url.setter
45
+ def base_url(self, base_url: str) -> None:
46
+ self._base_url = base_url
47
+ logger.info(f"UnipileApp: Base URL set to {self._base_url}")
48
+
49
+ def _get_headers(self) -> dict[str, str]:
50
+ """
51
+ Get the headers for Unipile API requests.
52
+ Overrides the base class method to use X-Api-Key.
53
+ """
18
54
  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"]
23
- return {
24
- "Authorization": f"Bearer {credentials['access_token']}",
25
- "X-Restli-Protocol-Version": "2.0.0",
26
- "Content-Type": "application/json",
27
- "LinkedIn-Version": "202507",
28
- }
55
+ logger.warning("UnipileApp: No integration configured, returning empty headers.")
56
+ return {}
57
+ api_key = os.getenv("UNIPILE_API_KEY")
58
+ if not api_key:
59
+ logger.error("UnipileApp: API key not found in integration credentials for Unipile.")
60
+ return {"Content-Type": "application/json", "Cache-Control": "no-cache"}
61
+ logger.debug("UnipileApp: Using X-Api-Key for authentication.")
62
+ return {"x-api-key": api_key, "Content-Type": "application/json", "Cache-Control": "no-cache"}
63
+
64
+ async def _aget_headers(self) -> dict[str, str]:
65
+ """
66
+ Get the headers for Unipile API requests asynchronously.
67
+ Overrides the base class method to use X-Api-Key.
68
+ """
69
+ return self._get_headers()
29
70
 
30
- def create_post(
71
+ async def _aget_search_parameter_id(self, param_type: str, keywords: str) -> str:
72
+ """
73
+ Retrieves the ID for a given LinkedIn search parameter by its name asynchronously.
74
+
75
+ Args:
76
+ param_type: The type of parameter to search for (e.g., "LOCATION", "COMPANY").
77
+ keywords: The name of the parameter to find (e.g., "United States").
78
+
79
+ Returns:
80
+ The corresponding ID for the search parameter.
81
+
82
+ Raises:
83
+ ValueError: If no exact match for the keywords is found.
84
+ httpx.HTTPError: If the API request fails.
85
+ """
86
+ url = f"{self.base_url}/api/v1/linkedin/search/parameters"
87
+ params = {"account_id": self.account_id, "keywords": keywords, "type": param_type}
88
+ response = await self._aget(url, params=params)
89
+ results = self._handle_response(response)
90
+ items = results.get("items", [])
91
+ if items:
92
+ return items[0]["id"]
93
+ raise ValueError(f'Could not find a matching ID for {param_type}: "{keywords}"')
94
+
95
+ async def start_new_chat(self, provider_id: str, text: str) -> dict[str, Any]:
96
+ """
97
+ Starts a new chat conversation with a specified user by sending an initial message.
98
+ This function constructs a multipart/form-data request using the `files` parameter
99
+ to ensure correct formatting and headers, working around potential issues in the
100
+ underlying request method.
101
+
102
+ Args:
103
+ provider_id: The LinkedIn provider ID of the user to start the chat with.
104
+ This is available in the response of the `retrieve_user_profile` tool.
105
+ text: The initial message content. For LinkedIn Recruiter accounts, this can include
106
+ HTML tags like <strong>, <em>, <a>, <ul>, <ol>, and <li>.
107
+
108
+ Returns:
109
+ A dictionary containing the details of the newly created chat.
110
+
111
+ Raises:
112
+ httpx.HTTPError: If the API request fails.
113
+
114
+ Tags:
115
+ linkedin, chat, create, start, new, messaging, api, important
116
+ """
117
+ url = f"{self.base_url}/api/v1/chats"
118
+ form_payload = {"account_id": (None, self.account_id), "text": (None, text), "attendees_ids": (None, provider_id)}
119
+ api_key = os.getenv("UNIPILE_API_KEY")
120
+ if not api_key:
121
+ raise ValueError("UNIPILE_API_KEY environment variable is not set.")
122
+ headers = {"x-api-key": api_key}
123
+ response = requests.post(url, files=form_payload, headers=headers)
124
+ return self._handle_response(response)
125
+
126
+ async def list_all_chats(
31
127
  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]:
128
+ unread: bool | None = None,
129
+ cursor: str | None = None,
130
+ before: str | None = None,
131
+ after: str | None = None,
132
+ limit: int | None = None,
133
+ account_type: str | None = None,
134
+ ) -> dict[str, Any]:
39
135
  """
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.
136
+ 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.
41
137
 
42
138
  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.
139
+ unread: Filter for unread chats only or read chats only.
140
+ cursor: Pagination cursor for the next page of entries.
141
+ before: Filter for items created before this ISO 8601 UTC datetime (exclusive).
142
+ after: Filter for items created after this ISO 8601 UTC datetime (exclusive).
143
+ limit: Number of items to return (1-250).
144
+ account_type: Filter by provider (e.g., "linkedin").
49
145
 
50
146
  Returns:
51
- dict[str, str]: Dictionary containing the post ID with key "post_id". Example: {"post_id": "urn:li:share:6844785523593134080"}
147
+ A dictionary containing a list of chat objects and a pagination cursor.
52
148
 
53
149
  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
150
+ httpx.HTTPError: If the API request fails.
151
+
152
+ Tags:
153
+ linkedin, chat, list, messaging, api
154
+ """
155
+ url = f"{self.base_url}/api/v1/chats"
156
+ params: dict[str, Any] = {}
157
+ params["account_id"] = self.account_id
158
+ if unread is not None:
159
+ params["unread"] = unread
160
+ if cursor:
161
+ params["cursor"] = cursor
162
+ if before:
163
+ params["before"] = before
164
+ if after:
165
+ params["after"] = after
166
+ if limit:
167
+ params["limit"] = limit
168
+ if account_type:
169
+ params["account_type"] = account_type
170
+ response = await self._aget(url, params=params)
171
+ return self._handle_response(response)
172
+
173
+ async def list_chat_messages(
174
+ self,
175
+ chat_id: str,
176
+ cursor: str | None = None,
177
+ before: str | None = None,
178
+ after: str | None = None,
179
+ limit: int | None = None,
180
+ sender_id: str | None = None,
181
+ ) -> dict[str, Any]:
182
+ """
183
+ 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.
56
184
 
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.
185
+ Args:
186
+ chat_id: The ID of the chat to retrieve messages from.
187
+ cursor: Pagination cursor for the next page of entries.
188
+ before: Filter for items created before this ISO 8601 UTC datetime (exclusive).
189
+ after: Filter for items created after this ISO 8601 UTC datetime (exclusive).
190
+ limit: Number of items to return (1-250).
191
+ sender_id: Filter messages from a specific sender ID.
192
+
193
+ Returns:
194
+ A dictionary containing a list of message objects and a pagination cursor.
195
+
196
+ Raises:
197
+ httpx.HTTPError: If the API request fails.
59
198
 
60
199
  Tags:
61
- posts, important
200
+ linkedin, chat, message, list, messaging, api
62
201
  """
63
- # Set default distribution if not provided
64
- if distribution is None:
65
- distribution = {
66
- "feedDistribution": "MAIN_FEED",
67
- "targetEntities": [],
68
- "thirdPartyDistributionChannels": [],
69
- }
202
+ url = f"{self.base_url}/api/v1/chats/{chat_id}/messages"
203
+ params: dict[str, Any] = {}
204
+ if cursor:
205
+ params["cursor"] = cursor
206
+ if before:
207
+ params["before"] = before
208
+ if after:
209
+ params["after"] = after
210
+ if limit:
211
+ params["limit"] = limit
212
+ if sender_id:
213
+ params["sender_id"] = sender_id
214
+ response = await self._aget(url, params=params)
215
+ return self._handle_response(response)
70
216
 
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
- }
217
+ async def send_chat_message(self, chat_id: str, text: str) -> dict[str, Any]:
218
+ """
219
+ 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.
79
220
 
80
- url = f"{self.base_url}/rest/posts"
81
- query_params = {}
221
+ Args:
222
+ chat_id: The ID of the chat where the message will be sent.
223
+ text: The text content of the message.
224
+ attachments: Optional list of attachment objects to include with the message.
82
225
 
83
- response = self._post(
84
- url,
85
- data=request_body_data,
86
- params=query_params,
87
- )
226
+ Returns:
227
+ A dictionary containing the ID of the sent message.
88
228
 
89
- self._handle_response(response)
229
+ Raises:
230
+ httpx.HTTPError: If the API request fails.
90
231
 
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")
232
+ Tags:
233
+ linkedin, chat, message, send, create, messaging, api
234
+ """
235
+ url = f"{self.base_url}/api/v1/chats/{chat_id}/messages"
236
+ payload: dict[str, Any] = {"text": text}
237
+ response = await self._apost(url, data=payload)
238
+ return self._handle_response(response)
94
239
 
95
- return {
96
- "post_urn": post_id,
97
- "post_url": f"https://www.linkedin.com/feed/update/{post_id}",
98
- }
240
+ async def retrieve_chat(self, chat_id: str) -> dict[str, Any]:
241
+ """
242
+ 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.
243
+
244
+ Args:
245
+ chat_id: The Unipile or provider ID of the chat.
246
+
247
+ Returns:
248
+ A dictionary containing the chat object details.
249
+
250
+ Raises:
251
+ httpx.HTTPError: If the API request fails.
252
+
253
+ Tags:
254
+ linkedin, chat, retrieve, get, messaging, api
255
+ """
256
+ url = f"{self.base_url}/api/v1/chats/{chat_id}"
257
+ params: dict[str, Any] = {}
258
+ if self.account_id:
259
+ params["account_id"] = self.account_id
260
+ response = await self._aget(url, params=params)
261
+ return self._handle_response(response)
99
262
 
100
- def get_authenticated_user_profile(self) -> dict[str, Any]:
263
+ async def list_all_messages(
264
+ self,
265
+ cursor: str | None = None,
266
+ before: str | None = None,
267
+ after: str | None = None,
268
+ limit: int | None = None,
269
+ sender_id: str | None = None,
270
+ ) -> dict[str, Any]:
101
271
  """
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.
272
+ 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.
273
+
274
+ Args:
275
+ cursor: Pagination cursor.
276
+ before: Filter for items created before this ISO 8601 UTC datetime.
277
+ after: Filter for items created after this ISO 8601 UTC datetime.
278
+ limit: Number of items to return (1-250).
279
+ sender_id: Filter messages from a specific sender.
103
280
 
104
281
  Returns:
105
- dict[str, Any]: Dictionary containing your LinkedIn profile information.
282
+ A dictionary containing a list of message objects and a pagination cursor.
106
283
 
107
284
  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
285
+ httpx.HTTPError: If the API request fails.
110
286
 
111
287
  Tags:
112
- profile, info
288
+ linkedin, message, list, all_messages, messaging, api
113
289
  """
114
- url = f"{self.base_url}/v2/userinfo"
115
- query_params = {}
290
+ url = f"{self.base_url}/api/v1/messages"
291
+ params: dict[str, Any] = {}
292
+ if cursor:
293
+ params["cursor"] = cursor
294
+ if before:
295
+ params["before"] = before
296
+ if after:
297
+ params["after"] = after
298
+ if limit:
299
+ params["limit"] = limit
300
+ if sender_id:
301
+ params["sender_id"] = sender_id
302
+ if self.account_id:
303
+ params["account_id"] = self.account_id
304
+ response = await self._aget(url, params=params)
305
+ return self._handle_response(response)
116
306
 
117
- response = self._get(
118
- url,
119
- params=query_params,
120
- )
307
+ async def list_profile_posts(
308
+ self, identifier: str, cursor: str | None = None, limit: int | None = None, is_company: bool | None = None
309
+ ) -> dict[str, Any]:
310
+ """
311
+ 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.
121
312
 
313
+ Args:
314
+ identifier: The entity's provider internal ID (LinkedIn ID).
315
+ cursor: Pagination cursor.
316
+ limit: Number of items to return (1-100, as per Unipile example, though spec allows up to 250).
317
+ is_company: Boolean indicating if the identifier is for a company.
318
+
319
+ Returns:
320
+ A dictionary containing a list of post objects and pagination details.
321
+
322
+ Raises:
323
+ httpx.HTTPError: If the API request fails.
324
+
325
+ Tags:
326
+ linkedin, post, list, user_posts, company_posts, content, api, important
327
+ """
328
+ url = f"{self.base_url}/api/v1/users/{identifier}/posts"
329
+ params: dict[str, Any] = {"account_id": self.account_id}
330
+ if cursor:
331
+ params["cursor"] = cursor
332
+ if limit:
333
+ params["limit"] = limit
334
+ if is_company is not None:
335
+ params["is_company"] = is_company
336
+ response = await self._aget(url, params=params)
122
337
  return self._handle_response(response)
123
338
 
124
- def delete_post(self, post_urn: str) -> dict[str, str]:
339
+ async def retrieve_own_profile(self) -> dict[str, Any]:
125
340
  """
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.
341
+ 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.
342
+
343
+ Returns:
344
+ A dictionary containing the user's profile details.
345
+
346
+ Raises:
347
+ httpx.HTTPError: If the API request fails.
348
+
349
+ Tags:
350
+ linkedin, user, profile, me, retrieve, get, api
351
+ """
352
+ url = f"{self.base_url}/api/v1/users/me"
353
+ params: dict[str, Any] = {"account_id": self.account_id}
354
+ response = await self._aget(url, params=params)
355
+ return self._handle_response(response)
356
+
357
+ async def retrieve_post(self, post_id: str) -> dict[str, Any]:
358
+ """
359
+ 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.
127
360
 
128
361
  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}).
362
+ post_id: The ID of the post to retrieve.
130
363
 
131
364
  Returns:
132
- dict[str, str]: Dictionary containing the deletion status. Example: {"status": "deleted", "post_urn": "urn:li:share:6844785523593134080"}
365
+ A dictionary containing the post details.
133
366
 
134
367
  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
368
+ httpx.HTTPError: If the API request fails.
369
+
370
+ Tags:
371
+ linkedin, post, retrieve, get, content, api, important
372
+ """
373
+ url = f"{self.base_url}/api/v1/posts/{post_id}"
374
+ params: dict[str, Any] = {"account_id": self.account_id}
375
+ response = await self._aget(url, params=params)
376
+ return self._handle_response(response)
377
+
378
+ async def list_post_comments(
379
+ self, post_id: str, comment_id: str | None = None, cursor: str | None = None, limit: int | None = None
380
+ ) -> dict[str, Any]:
381
+ """
382
+ Fetches comments for a specific post. Providing an optional `comment_id` retrieves threaded replies instead of top-level comments. `retrieve_post` or `list_profile_posts` can be used to obtain the `post_id` which is the social_id in their response.
383
+
384
+ Args:
385
+ post_id: The social ID of the post which you get from using `retrieve_post` or `list_profile_posts` tools.
386
+ comment_id: If provided, retrieves replies to this comment ID instead of top-level comments.
387
+ cursor: Pagination cursor.
388
+ limit: Number of comments to return. (OpenAPI spec shows type string, passed as string if provided).
389
+
390
+ Returns:
391
+ A dictionary containing a list of comment objects and pagination details.
137
392
 
393
+ Raises:
394
+ httpx.HTTPError: If the API request fails.
138
395
 
139
396
  Tags:
140
- posts, important
397
+ linkedin, post, comment, list, content, api, important
141
398
  """
142
- url = f"{self.base_url}/rest/posts/{quote(post_urn, safe='')}"
143
- query_params = {}
399
+ url = f"{self.base_url}/api/v1/posts/{post_id}/comments"
400
+ params: dict[str, Any] = {"account_id": self.account_id}
401
+ if cursor:
402
+ params["cursor"] = cursor
403
+ if limit is not None:
404
+ params["limit"] = str(limit)
405
+ if comment_id:
406
+ params["comment_id"] = comment_id
407
+ response = await self._aget(url, params=params)
408
+ return self._handle_response(response)
144
409
 
145
- response = self._delete(
146
- url,
147
- params=query_params,
148
- )
410
+ async def create_post(
411
+ self, text: str, mentions: list[dict[str, Any]] | None = None, external_link: str | None = None
412
+ ) -> dict[str, Any]:
413
+ """
414
+ 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.
149
415
 
150
- if response.status_code == 204:
151
- return {"status": "deleted", "post_urn": post_urn}
152
- else:
153
- return self._handle_response(response)
416
+ Args:
417
+ text: The main text content of the post.
418
+ mentions: Optional list of dictionaries, each representing a mention.
419
+ Example: `[{"entity_urn": "urn:li:person:...", "start_index": 0, "end_index": 5}]`
420
+ external_link: Optional string, an external URL that should be displayed within a card.
154
421
 
155
- def update_post(
422
+ Returns:
423
+ A dictionary containing the ID of the created post.
424
+
425
+ Raises:
426
+ httpx.HTTPError: If the API request fails.
427
+
428
+ Tags:
429
+ linkedin, post, create, share, content, api, important
430
+ """
431
+ url = f"{self.base_url}/api/v1/posts"
432
+ params: dict[str, str] = {"account_id": self.account_id, "text": text}
433
+ if mentions:
434
+ params["mentions"] = mentions
435
+ if external_link:
436
+ params["external_link"] = external_link
437
+ response = await self._apost(url, data=params)
438
+ return self._handle_response(response)
439
+
440
+ async def list_content_reactions(
441
+ self, post_id: str, comment_id: str | None = None, cursor: str | None = None, limit: int | None = None
442
+ ) -> dict[str, Any]:
443
+ """
444
+ 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.
445
+
446
+ Args:
447
+ post_id: The social ID of the post.
448
+ comment_id: If provided, retrieves reactions for this comment ID.
449
+ cursor: Pagination cursor.
450
+ limit: Number of reactions to return (1-100, spec max 250).
451
+
452
+ Returns:
453
+ A dictionary containing a list of reaction objects and pagination details.
454
+
455
+ Raises:
456
+ httpx.HTTPError: If the API request fails.
457
+
458
+ Tags:
459
+ linkedin, post, reaction, list, like, content, api
460
+ """
461
+ url = f"{self.base_url}/api/v1/posts/{post_id}/reactions"
462
+ params: dict[str, Any] = {"account_id": self.account_id}
463
+ if cursor:
464
+ params["cursor"] = cursor
465
+ if limit:
466
+ params["limit"] = limit
467
+ if comment_id:
468
+ params["comment_id"] = comment_id
469
+ response = await self._aget(url, params=params)
470
+ return self._handle_response(response)
471
+
472
+ async def create_post_comment(
473
+ self, post_social_id: str, text: str, comment_id: str | None = None, mentions_body: list[dict[str, Any]] | None = None
474
+ ) -> dict[str, Any]:
475
+ """
476
+ 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.
477
+
478
+ Args:
479
+ post_social_id: The social ID of the post to comment on.
480
+ text: The text content of the comment (passed as a query parameter).
481
+ Supports Unipile's mention syntax like "Hey {{0}}".
482
+ comment_id: Optional ID of a specific comment to reply to instead of commenting on the post.
483
+ mentions_body: Optional list of mention objects for the request body if needed.
484
+
485
+ Returns:
486
+ A dictionary, likely confirming comment creation. (Structure depends on actual API response)
487
+
488
+ Raises:
489
+ httpx.HTTPError: If the API request fails.
490
+
491
+ Tags:
492
+ linkedin, post, comment, create, content, api, important
493
+ """
494
+ url = f"{self.base_url}/api/v1/posts/{post_social_id}/comments"
495
+ params: dict[str, Any] = {"account_id": self.account_id, "text": text}
496
+ if comment_id:
497
+ params["comment_id"] = comment_id
498
+ if mentions_body:
499
+ params = {"mentions": mentions_body}
500
+ response = await self._apost(url, data=params)
501
+ return self._handle_response(response)
502
+
503
+ async def create_reaction(
156
504
  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]:
505
+ post_social_id: str,
506
+ reaction_type: Literal["like", "celebrate", "love", "insightful", "funny", "support"],
507
+ comment_id: str | None = None,
508
+ ) -> dict[str, Any]:
165
509
  """
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.
510
+ 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.
167
511
 
168
512
  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.
513
+ post_social_id: The social ID of the post or comment to react to.
514
+ reaction_type: The type of reaction. Valid values are "like", "celebrate", "love", "insightful", "funny", or "support".
515
+ comment_id: Optional ID of a specific comment to react to instead of the post.
176
516
 
177
517
  Returns:
178
- dict[str, str]: Dictionary containing the update status. Example: {"status": "updated", "post_urn": "urn:li:share:6844785523593134080"}
518
+ A dictionary, likely confirming the reaction. (Structure depends on actual API response)
179
519
 
180
520
  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
521
+ httpx.HTTPError: If the API request fails.
183
522
 
523
+ Tags:
524
+ linkedin, post, reaction, create, like, content, api, important
525
+ """
526
+ url = f"{self.base_url}/api/v1/posts/reaction"
527
+ params: dict[str, str] = {"account_id": self.account_id, "post_id": post_social_id, "reaction_type": reaction_type}
528
+ if comment_id:
529
+ params["comment_id"] = comment_id
530
+ response = await self._apost(url, data=params)
531
+ return self._handle_response(response)
532
+
533
+ async def retrieve_user_profile(self, public_identifier: str) -> dict[str, Any]:
534
+ """
535
+ 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.
536
+
537
+ Args:
538
+ public_identifier: Extract this value from the response of `search_people` tool. The response contains a public_identifier field.For example, for https://www.linkedin.com/in/manojbajaj95/, the identifier is "manojbajaj95".
539
+
540
+ Returns:
541
+ A dictionary containing the user's profile details.
542
+
543
+ Raises:
544
+ httpx.HTTPError: If the API request fails.
545
+
546
+ Tags:
547
+ linkedin, user, profile, retrieve, get, api, important
548
+ """
549
+ url = f"{self.base_url}/api/v1/users/{public_identifier}"
550
+ params: dict[str, Any] = {"account_id": self.account_id}
551
+ response = await self._aget(url, params=params)
552
+ return self._handle_response(response)
553
+
554
+ async def search_people(
555
+ self,
556
+ cursor: str | None = None,
557
+ limit: int | None = None,
558
+ keywords: str | None = None,
559
+ location: str | None = None,
560
+ industry: str | None = None,
561
+ company: str | None = None,
562
+ ) -> dict[str, Any]:
563
+ """
564
+ Searches for LinkedIn user profiles using keywords, with optional filters for location, industry, and company. This function specifically targets the 'people' category, distinguishing it from other search methods like `search_companies` or `search_jobs` that query different entity types through the same API endpoint.
565
+
566
+ Args:
567
+ cursor: Pagination cursor for the next page of entries.
568
+ limit: Number of items to return (up to 50 for Classic search).
569
+ keywords: Keywords to search for.
570
+ location: The geographical location to filter people by (e.g., "United States").
571
+ industry: The industry to filter people by.(eg., "Information Technology and Services").
572
+ company: The company to filter people by.(e.g., "Google").
573
+
574
+ Returns:
575
+ A dictionary containing search results and pagination details.
576
+
577
+ Raises:
578
+ httpx.HTTPError: If the API request fails.
579
+ """
580
+ url = f"{self.base_url}/api/v1/linkedin/search"
581
+ params: dict[str, Any] = {"account_id": self.account_id}
582
+ if cursor:
583
+ params["cursor"] = cursor
584
+ if limit is not None:
585
+ params["limit"] = limit
586
+ payload: dict[str, Any] = {"api": "classic", "category": "people"}
587
+ if keywords:
588
+ payload["keywords"] = keywords
589
+ if location:
590
+ location_id = await self._aget_search_parameter_id("LOCATION", location)
591
+ payload["location"] = [location_id]
592
+ if industry:
593
+ industry_id = await self._aget_search_parameter_id("INDUSTRY", industry)
594
+ payload["industry"] = [industry_id]
595
+ if company:
596
+ company_id = await self._aget_search_parameter_id("COMPANY", company)
597
+ payload["company"] = [company_id]
598
+ response = await self._apost(url, params=params, data=payload)
599
+ return self._handle_response(response)
600
+
601
+ async def search_companies(
602
+ self,
603
+ cursor: str | None = None,
604
+ limit: int | None = None,
605
+ keywords: str | None = None,
606
+ location: str | None = None,
607
+ industry: str | None = None,
608
+ ) -> dict[str, Any]:
609
+ """
610
+ Performs a paginated search for companies on LinkedIn using keywords, with optional location and industry filters. Its specific 'companies' search category distinguishes it from other methods like `search_people` or `search_posts`, ensuring that only company profiles are returned.
611
+
612
+ Args:
613
+ cursor: Pagination cursor for the next page of entries.
614
+ limit: Number of items to return (up to 50 for Classic search).
615
+ keywords: Keywords to search for.
616
+ location: The geographical location to filter companies by (e.g., "United States").
617
+ industry: The industry to filter companies by.(e.g., "Information Technology and Services").
618
+
619
+ Returns:
620
+ A dictionary containing search results and pagination details.
621
+
622
+ Raises:
623
+ httpx.HTTPError: If the API request fails.
624
+ """
625
+ url = f"{self.base_url}/api/v1/linkedin/search"
626
+ params: dict[str, Any] = {"account_id": self.account_id}
627
+ if cursor:
628
+ params["cursor"] = cursor
629
+ if limit is not None:
630
+ params["limit"] = limit
631
+ payload: dict[str, Any] = {"api": "classic", "category": "companies"}
632
+ if keywords:
633
+ payload["keywords"] = keywords
634
+ if location:
635
+ location_id = await self._aget_search_parameter_id("LOCATION", location)
636
+ payload["location"] = [location_id]
637
+ if industry:
638
+ industry_id = await self._aget_search_parameter_id("INDUSTRY", industry)
639
+ payload["industry"] = [industry_id]
640
+ response = await self._apost(url, params=params, data=payload)
641
+ return self._handle_response(response)
642
+
643
+ async def search_posts(
644
+ self,
645
+ cursor: str | None = None,
646
+ limit: int | None = None,
647
+ keywords: str | None = None,
648
+ date_posted: Literal["past_day", "past_week", "past_month"] | None = None,
649
+ sort_by: Literal["relevance", "date"] = "relevance",
650
+ ) -> dict[str, Any]:
651
+ """
652
+ Performs a keyword-based search for LinkedIn posts, allowing filters for date and sorting by relevance. This function executes a general, platform-wide content search, distinguishing it from other search functions that target people, companies, or jobs, and from `list_profile_posts` which retrieves from a specific profile.
653
+
654
+ Args:
655
+ cursor: Pagination cursor for the next page of entries.
656
+ limit: Number of items to return (up to 50 for Classic search).
657
+ keywords: Keywords to search for.
658
+ date_posted: Filter by when the post was posted.
659
+ sort_by: How to sort the results.
660
+
661
+ Returns:
662
+ A dictionary containing search results and pagination details.
663
+
664
+ Raises:
665
+ httpx.HTTPError: If the API request fails.
666
+ """
667
+ url = f"{self.base_url}/api/v1/linkedin/search"
668
+ params: dict[str, Any] = {"account_id": self.account_id}
669
+ if cursor:
670
+ params["cursor"] = cursor
671
+ if limit is not None:
672
+ params["limit"] = limit
673
+ payload: dict[str, Any] = {"api": "classic", "category": "posts"}
674
+ if keywords:
675
+ payload["keywords"] = keywords
676
+ if date_posted:
677
+ payload["date_posted"] = date_posted
678
+ if sort_by:
679
+ payload["sort_by"] = sort_by
680
+ response = await self._apost(url, params=params, data=payload)
681
+ return self._handle_response(response)
682
+
683
+ async def search_jobs(
684
+ self,
685
+ cursor: str | None = None,
686
+ limit: int | None = None,
687
+ keywords: str | None = None,
688
+ region: str | None = None,
689
+ sort_by: Literal["relevance", "date"] = "relevance",
690
+ minimum_salary_value: Literal[40, 60, 80, 100, 120, 140, 160, 180, 200] = 40,
691
+ industry: str | None = None,
692
+ ) -> dict[str, Any]:
693
+ """
694
+ Performs a LinkedIn search for jobs, filtering results by keywords, region, industry, and minimum salary. Unlike other search functions (`search_people`, `search_companies`), this method is specifically configured to query the 'jobs' category, providing a paginated list of relevant employment opportunities.
695
+
696
+ Args:
697
+ cursor: Pagination cursor for the next page of entries.
698
+ limit: Number of items to return (up to 50 for Classic search).
699
+ keywords: Keywords to search for.
700
+ region: The geographical region to filter jobs by (e.g., "United States").
701
+ sort_by: How to sort the results.(e.g., "relevance" or "date".)
702
+ minimum_salary_value: The minimum salary to filter for. Allowed values are 40, 60, 80, 100, 120, 140, 160, 180, 200.
703
+ industry: The industry to filter jobs by.(e.g., "Software Development").
704
+
705
+ Returns:
706
+ A dictionary containing search results and pagination details.
707
+
708
+ Raises:
709
+ httpx.HTTPError: If the API request fails.
710
+ ValueError: If the specified location is not found.
711
+ """
712
+ url = f"{self.base_url}/api/v1/linkedin/search"
713
+ params: dict[str, Any] = {"account_id": self.account_id}
714
+ if cursor:
715
+ params["cursor"] = cursor
716
+ if limit is not None:
717
+ params["limit"] = limit
718
+ payload: dict[str, Any] = {
719
+ "api": "classic",
720
+ "category": "jobs",
721
+ "minimum_salary": {"currency": "USD", "value": minimum_salary_value},
722
+ }
723
+ if keywords:
724
+ payload["keywords"] = keywords
725
+ if sort_by:
726
+ payload["sort_by"] = sort_by
727
+ if region:
728
+ location_id = await self._aget_search_parameter_id("LOCATION", region)
729
+ payload["region"] = location_id
730
+ if industry:
731
+ industry_id = await self._aget_search_parameter_id("INDUSTRY", industry)
732
+ payload["industry"] = [industry_id]
733
+ response = await self._apost(url, params=params, data=payload)
734
+ return self._handle_response(response)
735
+
736
+ async def send_invitation(self, provider_id: str, user_email: str | None = None, message: str | None = None) -> dict[str, Any]:
737
+ """
738
+ Sends a connection invitation to a LinkedIn user specified by their provider ID. An optional message and the user's email can be included.
739
+
740
+ Args:
741
+ provider_id: The LinkedIn provider ID of the user to invite. This is available in response of `retrieve_user_profile` tool.
742
+ user_email: Optional. The email address of the user, which may be required by LinkedIn.
743
+ message: Optional. A personalized message to include with the invitation (max 300 characters).
744
+
745
+ Returns:
746
+ A dictionary confirming the invitation was sent.
184
747
 
748
+ Raises:
749
+ httpx.HTTPError: If the API request fails.
750
+ ValueError: If the message exceeds 300 characters.
185
751
 
752
+ Tags:
753
+ linkedin, user, invite, connect, contact, api, important
754
+ """
755
+ url = f"{self.base_url}/api/v1/users/invite"
756
+ payload: dict[str, Any] = {"account_id": self.account_id, "provider_id": provider_id}
757
+ if user_email:
758
+ payload["user_email"] = user_email
759
+ if message:
760
+ if len(message) > 300:
761
+ raise ValueError("Message cannot exceed 300 characters.")
762
+ payload["message"] = message
763
+ response = await self._apost(url, data=payload)
764
+ return self._handle_response(response)
765
+
766
+ async def list_sent_invitations(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
767
+ """
768
+ Retrieves a paginated list of all sent connection invitations that are currently pending. This function allows for iterating through the history of outstanding connection requests made from the specified account.
769
+
770
+ Args:
771
+ cursor: A pagination cursor for retrieving the next page of entries.
772
+ limit: The number of items to return, ranging from 1 to 100. Defaults to 10 if not specified.
773
+
774
+ Returns:
775
+ A dictionary containing a list of sent invitation objects and pagination details.
776
+
777
+ Raises:
778
+ httpx.HTTPError: If the API request fails.
186
779
 
187
780
  Tags:
188
- posts, update, important
781
+ linkedin, user, invite, sent, list, contacts, api
189
782
  """
190
- url = f"{self.base_url}/rest/posts/{quote(post_urn, safe='')}"
191
- query_params = {}
783
+ url = f"{self.base_url}/api/v1/users/invite/sent"
784
+ params: dict[str, Any] = {"account_id": self.account_id}
785
+ if cursor:
786
+ params["cursor"] = cursor
787
+ if limit is not None:
788
+ params["limit"] = limit
789
+ response = await self._aget(url, params=params)
790
+ return self._handle_response(response)
192
791
 
193
- # Build the patch data
194
- patch_data = {"$set": {}}
195
- ad_context_data = {}
792
+ async def list_received_invitations(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
793
+ """
794
+ Retrieves a paginated list of all received connection invitations. This function allows for reviewing and processing incoming connection requests to the specified account.
196
795
 
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
796
+ Args:
797
+ cursor: A pagination cursor for retrieving the next page of entries.
798
+ limit: The number of items to return, ranging from 1 to 100. Defaults to 10 if not specified.
207
799
 
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
800
+ Returns:
801
+ A dictionary containing a list of received invitation objects and pagination details.
215
802
 
216
- request_body_data = {"patch": patch_data}
803
+ Raises:
804
+ httpx.HTTPError: If the API request fails.
217
805
 
218
- response = self._post(
219
- url,
220
- data=request_body_data,
221
- params=query_params,
222
- )
806
+ Tags:
807
+ linkedin, user, invite, received, list, contacts, api
808
+ """
809
+ url = f"{self.base_url}/api/v1/users/invite/received"
810
+ params: dict[str, Any] = {"account_id": self.account_id}
811
+ if cursor:
812
+ params["cursor"] = cursor
813
+ if limit is not None:
814
+ params["limit"] = limit
815
+ response = await self._aget(url, params=params)
816
+ return self._handle_response(response)
817
+
818
+ async def handle_received_invitation(
819
+ self, invitation_id: str, action: Literal["accept", "decline"], shared_secret: str
820
+ ) -> dict[str, Any]:
821
+ """
822
+ Accepts or declines a received LinkedIn connection invitation using its ID and a required shared secret. This function performs a POST request to update the invitation's status, distinguishing it from read-only functions like `list_received_invitations`.
823
+
824
+ Args:
825
+ invitation_id: The ID of the invitation to handle.Get this ID from the 'list_received_invitations' tool.
826
+ action: The action to perform, either "accept" or "decline".
827
+ shared_secret: The token provided by LinkedIn, retrieved from the 'list_received_invitations' tool, which is mandatory for this action.
828
+
829
+ Returns:
830
+ A dictionary confirming the action was processed.
223
831
 
224
- if response.status_code == 204:
225
- return {"status": "updated", "post_urn": post_urn}
226
- else:
227
- return self._handle_response(response)
832
+ Raises:
833
+ httpx.HTTPError: If the API request fails.
228
834
 
229
- def list_tools(self):
835
+ Tags:
836
+ linkedin, user, invite, received, handle, accept, decline, api
230
837
  """
231
- Lists the available tools (methods) for this application.
838
+ url = f"{self.base_url}/api/v1/users/invite/received/{invitation_id}"
839
+ payload: dict[str, Any] = {"provider": "LINKEDIN", "action": action, "shared_secret": shared_secret, "account_id": self.account_id}
840
+ response = await self._apost(url, data=payload)
841
+ return self._handle_response(response)
842
+
843
+ async def cancel_sent_invitation(self, invitation_id: str) -> dict[str, Any]:
232
844
  """
845
+ Cancels a sent LinkedIn connection invitation that is currently pending. This function performs a DELETE request to remove the invitation, withdrawing the connection request.
846
+
847
+ Args:
848
+ invitation_id: The unique ID of the invitation to cancel. This ID can be obtained from the 'list_sent_invitations' tool.
849
+
850
+ Returns:
851
+ A dictionary confirming the invitation was cancelled.
852
+
853
+ Raises:
854
+ httpx.HTTPError: If the API request fails.
855
+
856
+ Tags:
857
+ linkedin, user, invite, sent, cancel, delete, api
858
+ """
859
+ url = f"{self.base_url}/api/v1/users/invite/sent/{invitation_id}"
860
+ params = {"account_id": self.account_id}
861
+ response = await self._adelete(url, params=params)
862
+ return self._handle_response(response)
863
+
864
+ async def list_followers(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
865
+ """
866
+ Retrieves a paginated list of all followers for the current user's account. This function is distinct from `list_following` as it shows who follows the user, not who the user follows.
867
+
868
+ Args:
869
+ cursor: A pagination cursor for retrieving the next page of entries.
870
+ limit: The number of items to return, ranging from 1 to 1000.
871
+
872
+ Returns:
873
+ A dictionary containing a list of follower objects and pagination details.
874
+
875
+ Raises:
876
+ httpx.HTTPError: If the API request fails.
877
+
878
+ Tags:
879
+ linkedin, user, followers, list, contacts, api
880
+ """
881
+ url = f"{self.base_url}/api/v1/users/followers"
882
+ params: dict[str, Any] = {"account_id": self.account_id}
883
+ if cursor:
884
+ params["cursor"] = cursor
885
+ if limit is not None:
886
+ params["limit"] = limit
887
+ response = await self._aget(url, params=params)
888
+ return self._handle_response(response)
889
+
890
+ async def list_following(self, cursor: str | None = None, limit: int | None = None) -> dict[str, Any]:
891
+ """
892
+ Retrieves a paginated list of all accounts that the current user is following. This function is the counterpart to `list_followers`, focusing on the user's outgoing connections rather than incoming ones.
893
+
894
+ Args:
895
+ cursor: A pagination cursor for retrieving the next page of entries.
896
+ limit: The number of items to return, ranging from 1 to 1000.
897
+
898
+ Returns:
899
+ A dictionary containing a list of followed account objects and pagination details.
900
+
901
+ Raises:
902
+ httpx.HTTPError: If the API request fails.
903
+
904
+ Tags:
905
+ linkedin, user, following, list, contacts, api
906
+ """
907
+ url = f"{self.base_url}/api/v1/users/following"
908
+ params: dict[str, Any] = {"account_id": self.account_id}
909
+ if cursor:
910
+ params["cursor"] = cursor
911
+ if limit is not None:
912
+ params["limit"] = limit
913
+ response = self._get(url, params=params)
914
+ return self._handle_response(response)
915
+
916
+ def list_tools(self) -> list[Callable]:
233
917
  return [
918
+ self.start_new_chat,
919
+ self.list_all_chats,
920
+ self.list_chat_messages,
921
+ self.send_chat_message,
922
+ self.retrieve_chat,
923
+ self.list_all_messages,
924
+ self.list_profile_posts,
925
+ self.retrieve_own_profile,
926
+ self.retrieve_user_profile,
927
+ self.retrieve_post,
928
+ self.list_post_comments,
234
929
  self.create_post,
235
- self.get_authenticated_user_profile,
236
- self.delete_post,
237
- self.update_post,
930
+ self.list_content_reactions,
931
+ self.create_post_comment,
932
+ self.create_reaction,
933
+ self.search_companies,
934
+ self.search_jobs,
935
+ self.search_people,
936
+ self.search_posts,
937
+ self.send_invitation,
938
+ self.list_sent_invitations,
939
+ self.cancel_sent_invitation,
940
+ self.list_received_invitations,
941
+ self.handle_received_invitation,
942
+ self.list_followers,
943
+ # self.list_following this endpoint is not yet implemented by unipile
238
944
  ]