upload-post 0.1.1__py3-none-any.whl → 2.0.0__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.
- upload_post/__init__.py +20 -2
- upload_post/api_client.py +911 -38
- upload_post/cli.py +2 -2
- upload_post-2.0.0.dist-info/METADATA +352 -0
- upload_post-2.0.0.dist-info/RECORD +7 -0
- {upload_post-0.1.1.dist-info → upload_post-2.0.0.dist-info}/WHEEL +1 -1
- upload_post-0.1.1.dist-info/METADATA +0 -90
- upload_post-0.1.1.dist-info/RECORD +0 -8
- upload_post-0.1.1.dist-info/entry_points.txt +0 -2
- {upload_post-0.1.1.dist-info → upload_post-2.0.0.dist-info}/top_level.txt +0 -0
upload_post/api_client.py
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"""
|
|
2
|
+
Upload-Post API Client
|
|
3
|
+
|
|
4
|
+
Cross-platform social media upload for TikTok, Instagram, YouTube, LinkedIn,
|
|
5
|
+
Facebook, Pinterest, Threads, Reddit, Bluesky, and X (Twitter).
|
|
6
|
+
"""
|
|
3
7
|
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, List, Union, Optional, Any, Literal
|
|
4
10
|
import requests
|
|
5
11
|
|
|
6
12
|
|
|
@@ -8,65 +14,932 @@ class UploadPostError(Exception):
|
|
|
8
14
|
"""Base exception for Upload-Post API errors"""
|
|
9
15
|
pass
|
|
10
16
|
|
|
17
|
+
|
|
11
18
|
class UploadPostClient:
|
|
19
|
+
"""
|
|
20
|
+
Upload-Post API Client
|
|
21
|
+
|
|
22
|
+
Supports uploading to: TikTok, Instagram, YouTube, LinkedIn, Facebook,
|
|
23
|
+
Pinterest, Threads, Reddit, Bluesky, X (Twitter)
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
>>> client = UploadPostClient("YOUR_API_KEY")
|
|
27
|
+
>>> response = client.upload_video(
|
|
28
|
+
... "video.mp4",
|
|
29
|
+
... title="My awesome video",
|
|
30
|
+
... user="my-profile",
|
|
31
|
+
... platforms=["tiktok", "instagram"]
|
|
32
|
+
... )
|
|
33
|
+
"""
|
|
34
|
+
|
|
12
35
|
BASE_URL = "https://api.upload-post.com/api"
|
|
13
36
|
|
|
14
37
|
def __init__(self, api_key: str):
|
|
38
|
+
"""
|
|
39
|
+
Initialize the Upload-Post client.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
api_key: Your API key from Upload-Post
|
|
43
|
+
"""
|
|
15
44
|
self.api_key = api_key
|
|
16
45
|
self.session = requests.Session()
|
|
17
46
|
self.session.headers.update({
|
|
18
47
|
"Authorization": f"Apikey {self.api_key}",
|
|
19
|
-
"User-Agent":
|
|
48
|
+
"User-Agent": "upload-post-python-client/2.0.0"
|
|
20
49
|
})
|
|
21
50
|
|
|
51
|
+
def _request(
|
|
52
|
+
self,
|
|
53
|
+
endpoint: str,
|
|
54
|
+
method: str = "GET",
|
|
55
|
+
data: Optional[List[tuple]] = None,
|
|
56
|
+
files: Optional[List[tuple]] = None,
|
|
57
|
+
json_data: Optional[Dict] = None,
|
|
58
|
+
params: Optional[Dict] = None
|
|
59
|
+
) -> Dict:
|
|
60
|
+
"""Make an API request."""
|
|
61
|
+
url = f"{self.BASE_URL}{endpoint}"
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
if method == "GET":
|
|
65
|
+
response = self.session.get(url, params=params)
|
|
66
|
+
elif method == "POST":
|
|
67
|
+
if json_data:
|
|
68
|
+
response = self.session.post(url, json=json_data)
|
|
69
|
+
else:
|
|
70
|
+
response = self.session.post(url, data=data, files=files)
|
|
71
|
+
elif method == "DELETE":
|
|
72
|
+
if json_data:
|
|
73
|
+
response = self.session.delete(url, json=json_data)
|
|
74
|
+
else:
|
|
75
|
+
response = self.session.delete(url)
|
|
76
|
+
else:
|
|
77
|
+
raise UploadPostError(f"Unsupported HTTP method: {method}")
|
|
78
|
+
|
|
79
|
+
response.raise_for_status()
|
|
80
|
+
return response.json()
|
|
81
|
+
|
|
82
|
+
except requests.exceptions.RequestException as e:
|
|
83
|
+
error_msg = str(e)
|
|
84
|
+
if hasattr(e, 'response') and e.response is not None:
|
|
85
|
+
try:
|
|
86
|
+
error_data = e.response.json()
|
|
87
|
+
error_msg = error_data.get('message') or error_data.get('detail') or str(error_data)
|
|
88
|
+
except:
|
|
89
|
+
pass
|
|
90
|
+
raise UploadPostError(f"API request failed: {error_msg}") from e
|
|
91
|
+
|
|
92
|
+
def _add_common_params(
|
|
93
|
+
self,
|
|
94
|
+
data: List[tuple],
|
|
95
|
+
user: str,
|
|
96
|
+
title: str,
|
|
97
|
+
platforms: List[str],
|
|
98
|
+
first_comment: Optional[str] = None,
|
|
99
|
+
alt_text: Optional[str] = None,
|
|
100
|
+
scheduled_date: Optional[str] = None,
|
|
101
|
+
timezone: Optional[str] = None,
|
|
102
|
+
add_to_queue: Optional[bool] = None,
|
|
103
|
+
async_upload: Optional[bool] = None,
|
|
104
|
+
**kwargs
|
|
105
|
+
):
|
|
106
|
+
"""Add common upload parameters."""
|
|
107
|
+
data.append(("user", user))
|
|
108
|
+
data.append(("title", title))
|
|
109
|
+
for p in platforms:
|
|
110
|
+
data.append(("platform[]", p))
|
|
111
|
+
|
|
112
|
+
if first_comment:
|
|
113
|
+
data.append(("first_comment", first_comment))
|
|
114
|
+
if alt_text:
|
|
115
|
+
data.append(("alt_text", alt_text))
|
|
116
|
+
if scheduled_date:
|
|
117
|
+
data.append(("scheduled_date", scheduled_date))
|
|
118
|
+
if timezone:
|
|
119
|
+
data.append(("timezone", timezone))
|
|
120
|
+
if add_to_queue is not None:
|
|
121
|
+
data.append(("add_to_queue", str(add_to_queue).lower()))
|
|
122
|
+
if async_upload is not None:
|
|
123
|
+
data.append(("async_upload", str(async_upload).lower()))
|
|
124
|
+
|
|
125
|
+
# Platform-specific title overrides
|
|
126
|
+
title_overrides = [
|
|
127
|
+
"bluesky_title", "instagram_title", "facebook_title", "tiktok_title",
|
|
128
|
+
"linkedin_title", "x_title", "youtube_title", "pinterest_title", "threads_title"
|
|
129
|
+
]
|
|
130
|
+
for key in title_overrides:
|
|
131
|
+
if kwargs.get(key):
|
|
132
|
+
data.append((key, kwargs[key]))
|
|
133
|
+
|
|
134
|
+
# Platform-specific description overrides
|
|
135
|
+
desc_overrides = [
|
|
136
|
+
"description", "linkedin_description", "youtube_description",
|
|
137
|
+
"facebook_description", "tiktok_description", "pinterest_description"
|
|
138
|
+
]
|
|
139
|
+
for key in desc_overrides:
|
|
140
|
+
if kwargs.get(key):
|
|
141
|
+
data.append((key, kwargs[key]))
|
|
142
|
+
|
|
143
|
+
# Platform-specific first comment overrides
|
|
144
|
+
comment_overrides = [
|
|
145
|
+
"instagram_first_comment", "facebook_first_comment", "x_first_comment",
|
|
146
|
+
"threads_first_comment", "youtube_first_comment", "reddit_first_comment",
|
|
147
|
+
"bluesky_first_comment"
|
|
148
|
+
]
|
|
149
|
+
for key in comment_overrides:
|
|
150
|
+
if kwargs.get(key):
|
|
151
|
+
data.append((key, kwargs[key]))
|
|
152
|
+
|
|
153
|
+
def _add_tiktok_params(self, data: List[tuple], is_video: bool = True, **kwargs):
|
|
154
|
+
"""Add TikTok-specific parameters."""
|
|
155
|
+
if kwargs.get("disable_comment") is not None:
|
|
156
|
+
data.append(("disable_comment", str(kwargs["disable_comment"]).lower()))
|
|
157
|
+
if kwargs.get("brand_content_toggle") is not None:
|
|
158
|
+
data.append(("brand_content_toggle", str(kwargs["brand_content_toggle"]).lower()))
|
|
159
|
+
if kwargs.get("brand_organic_toggle") is not None:
|
|
160
|
+
data.append(("brand_organic_toggle", str(kwargs["brand_organic_toggle"]).lower()))
|
|
161
|
+
|
|
162
|
+
if is_video:
|
|
163
|
+
if kwargs.get("privacy_level"):
|
|
164
|
+
data.append(("privacy_level", kwargs["privacy_level"]))
|
|
165
|
+
if kwargs.get("disable_duet") is not None:
|
|
166
|
+
data.append(("disable_duet", str(kwargs["disable_duet"]).lower()))
|
|
167
|
+
if kwargs.get("disable_stitch") is not None:
|
|
168
|
+
data.append(("disable_stitch", str(kwargs["disable_stitch"]).lower()))
|
|
169
|
+
if kwargs.get("cover_timestamp") is not None:
|
|
170
|
+
data.append(("cover_timestamp", str(kwargs["cover_timestamp"])))
|
|
171
|
+
if kwargs.get("is_aigc") is not None:
|
|
172
|
+
data.append(("is_aigc", str(kwargs["is_aigc"]).lower()))
|
|
173
|
+
if kwargs.get("post_mode"):
|
|
174
|
+
data.append(("post_mode", kwargs["post_mode"]))
|
|
175
|
+
else:
|
|
176
|
+
if kwargs.get("auto_add_music") is not None:
|
|
177
|
+
data.append(("auto_add_music", str(kwargs["auto_add_music"]).lower()))
|
|
178
|
+
if kwargs.get("photo_cover_index") is not None:
|
|
179
|
+
data.append(("photo_cover_index", str(kwargs["photo_cover_index"])))
|
|
180
|
+
|
|
181
|
+
def _add_instagram_params(self, data: List[tuple], is_video: bool = True, **kwargs):
|
|
182
|
+
"""Add Instagram-specific parameters."""
|
|
183
|
+
if kwargs.get("media_type"):
|
|
184
|
+
data.append(("media_type", kwargs["media_type"]))
|
|
185
|
+
if kwargs.get("collaborators"):
|
|
186
|
+
data.append(("collaborators", kwargs["collaborators"]))
|
|
187
|
+
if kwargs.get("user_tags"):
|
|
188
|
+
data.append(("user_tags", kwargs["user_tags"]))
|
|
189
|
+
if kwargs.get("location_id"):
|
|
190
|
+
data.append(("location_id", kwargs["location_id"]))
|
|
191
|
+
|
|
192
|
+
if is_video:
|
|
193
|
+
if kwargs.get("share_to_feed") is not None:
|
|
194
|
+
data.append(("share_to_feed", str(kwargs["share_to_feed"]).lower()))
|
|
195
|
+
if kwargs.get("cover_url"):
|
|
196
|
+
data.append(("cover_url", kwargs["cover_url"]))
|
|
197
|
+
if kwargs.get("audio_name"):
|
|
198
|
+
data.append(("audio_name", kwargs["audio_name"]))
|
|
199
|
+
if kwargs.get("thumb_offset"):
|
|
200
|
+
data.append(("thumb_offset", kwargs["thumb_offset"]))
|
|
201
|
+
|
|
202
|
+
def _add_youtube_params(self, data: List[tuple], **kwargs):
|
|
203
|
+
"""Add YouTube-specific parameters."""
|
|
204
|
+
if kwargs.get("tags"):
|
|
205
|
+
tags = kwargs["tags"]
|
|
206
|
+
if isinstance(tags, str):
|
|
207
|
+
tags = [t.strip() for t in tags.split(",")]
|
|
208
|
+
for tag in tags:
|
|
209
|
+
data.append(("tags[]", tag))
|
|
210
|
+
if kwargs.get("categoryId"):
|
|
211
|
+
data.append(("categoryId", kwargs["categoryId"]))
|
|
212
|
+
if kwargs.get("privacyStatus"):
|
|
213
|
+
data.append(("privacyStatus", kwargs["privacyStatus"]))
|
|
214
|
+
if kwargs.get("embeddable") is not None:
|
|
215
|
+
data.append(("embeddable", str(kwargs["embeddable"]).lower()))
|
|
216
|
+
if kwargs.get("license"):
|
|
217
|
+
data.append(("license", kwargs["license"]))
|
|
218
|
+
if kwargs.get("publicStatsViewable") is not None:
|
|
219
|
+
data.append(("publicStatsViewable", str(kwargs["publicStatsViewable"]).lower()))
|
|
220
|
+
if kwargs.get("thumbnail_url"):
|
|
221
|
+
data.append(("thumbnail_url", kwargs["thumbnail_url"]))
|
|
222
|
+
if kwargs.get("selfDeclaredMadeForKids") is not None:
|
|
223
|
+
data.append(("selfDeclaredMadeForKids", str(kwargs["selfDeclaredMadeForKids"]).lower()))
|
|
224
|
+
if kwargs.get("containsSyntheticMedia") is not None:
|
|
225
|
+
data.append(("containsSyntheticMedia", str(kwargs["containsSyntheticMedia"]).lower()))
|
|
226
|
+
if kwargs.get("defaultLanguage"):
|
|
227
|
+
data.append(("defaultLanguage", kwargs["defaultLanguage"]))
|
|
228
|
+
if kwargs.get("defaultAudioLanguage"):
|
|
229
|
+
data.append(("defaultAudioLanguage", kwargs["defaultAudioLanguage"]))
|
|
230
|
+
if kwargs.get("allowedCountries"):
|
|
231
|
+
data.append(("allowedCountries", kwargs["allowedCountries"]))
|
|
232
|
+
if kwargs.get("blockedCountries"):
|
|
233
|
+
data.append(("blockedCountries", kwargs["blockedCountries"]))
|
|
234
|
+
if kwargs.get("hasPaidProductPlacement") is not None:
|
|
235
|
+
data.append(("hasPaidProductPlacement", str(kwargs["hasPaidProductPlacement"]).lower()))
|
|
236
|
+
if kwargs.get("recordingDate"):
|
|
237
|
+
data.append(("recordingDate", kwargs["recordingDate"]))
|
|
238
|
+
|
|
239
|
+
def _add_linkedin_params(self, data: List[tuple], **kwargs):
|
|
240
|
+
"""Add LinkedIn-specific parameters."""
|
|
241
|
+
if kwargs.get("visibility"):
|
|
242
|
+
data.append(("visibility", kwargs["visibility"]))
|
|
243
|
+
if kwargs.get("target_linkedin_page_id"):
|
|
244
|
+
data.append(("target_linkedin_page_id", kwargs["target_linkedin_page_id"]))
|
|
245
|
+
|
|
246
|
+
def _add_facebook_params(self, data: List[tuple], is_video: bool = False, is_text: bool = False, **kwargs):
|
|
247
|
+
"""Add Facebook-specific parameters."""
|
|
248
|
+
if kwargs.get("facebook_page_id"):
|
|
249
|
+
data.append(("facebook_page_id", kwargs["facebook_page_id"]))
|
|
250
|
+
|
|
251
|
+
if is_video:
|
|
252
|
+
if kwargs.get("video_state"):
|
|
253
|
+
data.append(("video_state", kwargs["video_state"]))
|
|
254
|
+
if kwargs.get("facebook_media_type"):
|
|
255
|
+
data.append(("facebook_media_type", kwargs["facebook_media_type"]))
|
|
256
|
+
|
|
257
|
+
if is_text and kwargs.get("facebook_link_url"):
|
|
258
|
+
data.append(("facebook_link_url", kwargs["facebook_link_url"]))
|
|
259
|
+
|
|
260
|
+
def _add_pinterest_params(self, data: List[tuple], is_video: bool = False, **kwargs):
|
|
261
|
+
"""Add Pinterest-specific parameters."""
|
|
262
|
+
if kwargs.get("pinterest_board_id"):
|
|
263
|
+
data.append(("pinterest_board_id", kwargs["pinterest_board_id"]))
|
|
264
|
+
if kwargs.get("pinterest_alt_text"):
|
|
265
|
+
data.append(("pinterest_alt_text", kwargs["pinterest_alt_text"]))
|
|
266
|
+
if kwargs.get("pinterest_link"):
|
|
267
|
+
data.append(("pinterest_link", kwargs["pinterest_link"]))
|
|
268
|
+
|
|
269
|
+
if is_video:
|
|
270
|
+
if kwargs.get("pinterest_cover_image_url"):
|
|
271
|
+
data.append(("pinterest_cover_image_url", kwargs["pinterest_cover_image_url"]))
|
|
272
|
+
if kwargs.get("pinterest_cover_image_content_type"):
|
|
273
|
+
data.append(("pinterest_cover_image_content_type", kwargs["pinterest_cover_image_content_type"]))
|
|
274
|
+
if kwargs.get("pinterest_cover_image_data"):
|
|
275
|
+
data.append(("pinterest_cover_image_data", kwargs["pinterest_cover_image_data"]))
|
|
276
|
+
if kwargs.get("pinterest_cover_image_key_frame_time") is not None:
|
|
277
|
+
data.append(("pinterest_cover_image_key_frame_time", str(kwargs["pinterest_cover_image_key_frame_time"])))
|
|
278
|
+
|
|
279
|
+
def _add_x_params(self, data: List[tuple], is_text: bool = False, **kwargs):
|
|
280
|
+
"""Add X (Twitter) specific parameters."""
|
|
281
|
+
reply_settings = kwargs.get("reply_settings")
|
|
282
|
+
if reply_settings and reply_settings != "everyone":
|
|
283
|
+
data.append(("reply_settings", reply_settings))
|
|
284
|
+
if kwargs.get("nullcast") is not None:
|
|
285
|
+
data.append(("nullcast", str(kwargs["nullcast"]).lower()))
|
|
286
|
+
if kwargs.get("quote_tweet_id"):
|
|
287
|
+
data.append(("quote_tweet_id", kwargs["quote_tweet_id"]))
|
|
288
|
+
if kwargs.get("geo_place_id"):
|
|
289
|
+
data.append(("geo_place_id", kwargs["geo_place_id"]))
|
|
290
|
+
if kwargs.get("for_super_followers_only") is not None:
|
|
291
|
+
data.append(("for_super_followers_only", str(kwargs["for_super_followers_only"]).lower()))
|
|
292
|
+
if kwargs.get("community_id"):
|
|
293
|
+
data.append(("community_id", kwargs["community_id"]))
|
|
294
|
+
if kwargs.get("share_with_followers") is not None:
|
|
295
|
+
data.append(("share_with_followers", str(kwargs["share_with_followers"]).lower()))
|
|
296
|
+
if kwargs.get("direct_message_deep_link"):
|
|
297
|
+
data.append(("direct_message_deep_link", kwargs["direct_message_deep_link"]))
|
|
298
|
+
if kwargs.get("x_long_text_as_post") is not None:
|
|
299
|
+
data.append(("x_long_text_as_post", str(kwargs["x_long_text_as_post"]).lower()))
|
|
300
|
+
|
|
301
|
+
if not is_text:
|
|
302
|
+
if kwargs.get("tagged_user_ids"):
|
|
303
|
+
ids = kwargs["tagged_user_ids"]
|
|
304
|
+
if isinstance(ids, str):
|
|
305
|
+
ids = [t.strip() for t in ids.split(",")]
|
|
306
|
+
for uid in ids:
|
|
307
|
+
data.append(("tagged_user_ids[]", uid))
|
|
308
|
+
if kwargs.get("place_id"):
|
|
309
|
+
data.append(("place_id", kwargs["place_id"]))
|
|
310
|
+
else:
|
|
311
|
+
if kwargs.get("post_url"):
|
|
312
|
+
data.append(("post_url", kwargs["post_url"]))
|
|
313
|
+
if kwargs.get("card_uri"):
|
|
314
|
+
data.append(("card_uri", kwargs["card_uri"]))
|
|
315
|
+
|
|
316
|
+
if kwargs.get("poll_options"):
|
|
317
|
+
poll_opts = kwargs["poll_options"]
|
|
318
|
+
if isinstance(poll_opts, str):
|
|
319
|
+
poll_opts = [o.strip() for o in poll_opts.split(",")]
|
|
320
|
+
for opt in poll_opts:
|
|
321
|
+
data.append(("poll_options[]", opt))
|
|
322
|
+
if kwargs.get("poll_duration"):
|
|
323
|
+
data.append(("poll_duration", str(kwargs["poll_duration"])))
|
|
324
|
+
if kwargs.get("poll_reply_settings"):
|
|
325
|
+
data.append(("poll_reply_settings", kwargs["poll_reply_settings"]))
|
|
326
|
+
|
|
327
|
+
def _add_threads_params(self, data: List[tuple], **kwargs):
|
|
328
|
+
"""Add Threads-specific parameters."""
|
|
329
|
+
if kwargs.get("threads_long_text_as_post") is not None:
|
|
330
|
+
data.append(("threads_long_text_as_post", str(kwargs["threads_long_text_as_post"]).lower()))
|
|
331
|
+
|
|
332
|
+
def _add_reddit_params(self, data: List[tuple], **kwargs):
|
|
333
|
+
"""Add Reddit-specific parameters."""
|
|
334
|
+
if kwargs.get("subreddit"):
|
|
335
|
+
data.append(("subreddit", kwargs["subreddit"]))
|
|
336
|
+
if kwargs.get("flair_id"):
|
|
337
|
+
data.append(("flair_id", kwargs["flair_id"]))
|
|
338
|
+
|
|
22
339
|
def upload_video(
|
|
23
340
|
self,
|
|
24
341
|
video_path: Union[str, Path],
|
|
25
342
|
title: str,
|
|
26
343
|
user: str,
|
|
27
|
-
platforms: List[str]
|
|
344
|
+
platforms: List[str],
|
|
345
|
+
**kwargs
|
|
28
346
|
) -> Dict:
|
|
29
347
|
"""
|
|
30
|
-
Upload a video to
|
|
348
|
+
Upload a video to social media platforms.
|
|
349
|
+
|
|
350
|
+
Args:
|
|
351
|
+
video_path: Path to video file or video URL.
|
|
352
|
+
title: Video title/caption.
|
|
353
|
+
user: User identifier (profile name).
|
|
354
|
+
platforms: Target platforms. Supported: tiktok, instagram, youtube,
|
|
355
|
+
linkedin, facebook, pinterest, threads, bluesky, x
|
|
356
|
+
|
|
357
|
+
Keyword Args:
|
|
358
|
+
description: Video description
|
|
359
|
+
first_comment: First comment to post
|
|
360
|
+
scheduled_date: ISO date for scheduling (e.g., "2024-12-25T10:00:00Z")
|
|
361
|
+
timezone: Timezone for scheduled date (e.g., "Europe/Madrid")
|
|
362
|
+
add_to_queue: Add to posting queue
|
|
363
|
+
async_upload: Process asynchronously (default: True)
|
|
364
|
+
|
|
365
|
+
TikTok:
|
|
366
|
+
privacy_level: PUBLIC_TO_EVERYONE, MUTUAL_FOLLOW_FRIENDS,
|
|
367
|
+
FOLLOWER_OF_CREATOR, SELF_ONLY
|
|
368
|
+
disable_duet: Disable duet
|
|
369
|
+
disable_comment: Disable comments
|
|
370
|
+
disable_stitch: Disable stitch
|
|
371
|
+
cover_timestamp: Timestamp in ms for cover
|
|
372
|
+
is_aigc: AI-generated content flag
|
|
373
|
+
post_mode: DIRECT_POST or MEDIA_UPLOAD
|
|
374
|
+
brand_content_toggle: Branded content toggle
|
|
375
|
+
brand_organic_toggle: Brand organic toggle
|
|
376
|
+
|
|
377
|
+
Instagram:
|
|
378
|
+
media_type: REELS or STORIES
|
|
379
|
+
share_to_feed: Share to feed
|
|
380
|
+
collaborators: Comma-separated collaborator usernames
|
|
381
|
+
cover_url: Custom cover URL
|
|
382
|
+
audio_name: Audio track name
|
|
383
|
+
user_tags: Comma-separated user tags
|
|
384
|
+
location_id: Location ID
|
|
385
|
+
thumb_offset: Thumbnail offset
|
|
386
|
+
|
|
387
|
+
YouTube:
|
|
388
|
+
tags: List or comma-separated tags
|
|
389
|
+
categoryId: Category ID (default: "22")
|
|
390
|
+
privacyStatus: public, unlisted, private
|
|
391
|
+
embeddable: Allow embedding
|
|
392
|
+
license: youtube, creativeCommon
|
|
393
|
+
publicStatsViewable: Show public stats
|
|
394
|
+
thumbnail_url: Custom thumbnail URL
|
|
395
|
+
selfDeclaredMadeForKids: Made for kids (COPPA)
|
|
396
|
+
containsSyntheticMedia: AI/synthetic content flag
|
|
397
|
+
defaultLanguage: Title/description language (BCP-47)
|
|
398
|
+
defaultAudioLanguage: Audio language (BCP-47)
|
|
399
|
+
allowedCountries: Comma-separated country codes
|
|
400
|
+
blockedCountries: Comma-separated country codes
|
|
401
|
+
hasPaidProductPlacement: Paid placement flag
|
|
402
|
+
recordingDate: Recording date (ISO 8601)
|
|
403
|
+
|
|
404
|
+
LinkedIn:
|
|
405
|
+
visibility: PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
|
|
406
|
+
target_linkedin_page_id: Page ID for organization posts
|
|
407
|
+
|
|
408
|
+
Facebook:
|
|
409
|
+
facebook_page_id: Facebook Page ID
|
|
410
|
+
video_state: PUBLISHED or DRAFT
|
|
411
|
+
facebook_media_type: REELS or STORIES
|
|
412
|
+
|
|
413
|
+
Pinterest:
|
|
414
|
+
pinterest_board_id: Board ID
|
|
415
|
+
pinterest_link: Destination link
|
|
416
|
+
pinterest_cover_image_url: Cover image URL
|
|
417
|
+
pinterest_cover_image_key_frame_time: Key frame time in ms
|
|
418
|
+
|
|
419
|
+
X (Twitter):
|
|
420
|
+
reply_settings: everyone, following, mentionedUsers, subscribers, verified
|
|
421
|
+
nullcast: Promoted-only post
|
|
422
|
+
tagged_user_ids: User IDs to tag
|
|
423
|
+
place_id: Location place ID
|
|
424
|
+
geo_place_id: Geographic place ID
|
|
425
|
+
for_super_followers_only: Exclusive for super followers
|
|
426
|
+
community_id: Community ID
|
|
427
|
+
share_with_followers: Share community post with followers
|
|
428
|
+
x_long_text_as_post: Post long text as single post
|
|
429
|
+
|
|
430
|
+
Threads:
|
|
431
|
+
threads_long_text_as_post: Post long text as single post
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
API response with request_id for async uploads.
|
|
435
|
+
|
|
436
|
+
Raises:
|
|
437
|
+
UploadPostError: If upload fails or video file not found.
|
|
438
|
+
"""
|
|
439
|
+
data: List[tuple] = []
|
|
440
|
+
files: List[tuple] = []
|
|
441
|
+
video_file = None
|
|
31
442
|
|
|
443
|
+
try:
|
|
444
|
+
video_str = str(video_path)
|
|
445
|
+
if video_str.lower().startswith(("http://", "https://")):
|
|
446
|
+
data.append(("video", video_str))
|
|
447
|
+
else:
|
|
448
|
+
video_p = Path(video_path)
|
|
449
|
+
if not video_p.exists():
|
|
450
|
+
raise UploadPostError(f"Video file not found: {video_p}")
|
|
451
|
+
video_file = video_p.open("rb")
|
|
452
|
+
files.append(("video", (video_p.name, video_file)))
|
|
453
|
+
|
|
454
|
+
self._add_common_params(data, user, title, platforms, **kwargs)
|
|
455
|
+
|
|
456
|
+
if "tiktok" in platforms:
|
|
457
|
+
self._add_tiktok_params(data, is_video=True, **kwargs)
|
|
458
|
+
if "instagram" in platforms:
|
|
459
|
+
self._add_instagram_params(data, is_video=True, **kwargs)
|
|
460
|
+
if "youtube" in platforms:
|
|
461
|
+
self._add_youtube_params(data, **kwargs)
|
|
462
|
+
if "linkedin" in platforms:
|
|
463
|
+
self._add_linkedin_params(data, **kwargs)
|
|
464
|
+
if "facebook" in platforms:
|
|
465
|
+
self._add_facebook_params(data, is_video=True, **kwargs)
|
|
466
|
+
if "pinterest" in platforms:
|
|
467
|
+
self._add_pinterest_params(data, is_video=True, **kwargs)
|
|
468
|
+
if "x" in platforms:
|
|
469
|
+
self._add_x_params(data, is_text=False, **kwargs)
|
|
470
|
+
if "threads" in platforms:
|
|
471
|
+
self._add_threads_params(data, **kwargs)
|
|
472
|
+
|
|
473
|
+
return self._request("/upload", "POST", data=data, files=files if files else None)
|
|
474
|
+
|
|
475
|
+
finally:
|
|
476
|
+
if video_file:
|
|
477
|
+
video_file.close()
|
|
478
|
+
|
|
479
|
+
def upload_photos(
|
|
480
|
+
self,
|
|
481
|
+
photos: List[Union[str, Path]],
|
|
482
|
+
title: str,
|
|
483
|
+
user: str,
|
|
484
|
+
platforms: List[str],
|
|
485
|
+
**kwargs
|
|
486
|
+
) -> Dict:
|
|
487
|
+
"""
|
|
488
|
+
Upload photos to social media platforms.
|
|
489
|
+
|
|
32
490
|
Args:
|
|
33
|
-
|
|
34
|
-
title:
|
|
35
|
-
user: User identifier
|
|
36
|
-
platforms:
|
|
491
|
+
photos: List of photo file paths or URLs.
|
|
492
|
+
title: Post title/caption.
|
|
493
|
+
user: User identifier (profile name).
|
|
494
|
+
platforms: Target platforms. Supported: tiktok, instagram, linkedin,
|
|
495
|
+
facebook, pinterest, threads, reddit, bluesky, x
|
|
496
|
+
|
|
497
|
+
Keyword Args:
|
|
498
|
+
description: Photo description
|
|
499
|
+
first_comment: First comment to post
|
|
500
|
+
alt_text: Alt text for accessibility
|
|
501
|
+
scheduled_date: ISO date for scheduling
|
|
502
|
+
timezone: Timezone for scheduled date
|
|
503
|
+
add_to_queue: Add to posting queue
|
|
504
|
+
async_upload: Process asynchronously
|
|
37
505
|
|
|
506
|
+
TikTok:
|
|
507
|
+
auto_add_music: Auto add music
|
|
508
|
+
disable_comment: Disable comments
|
|
509
|
+
photo_cover_index: Index of photo for cover (0-based)
|
|
510
|
+
brand_content_toggle: Branded content toggle
|
|
511
|
+
brand_organic_toggle: Brand organic toggle
|
|
512
|
+
|
|
513
|
+
Instagram:
|
|
514
|
+
media_type: IMAGE or STORIES
|
|
515
|
+
collaborators: Comma-separated collaborator usernames
|
|
516
|
+
user_tags: Comma-separated user tags
|
|
517
|
+
location_id: Location ID
|
|
518
|
+
|
|
519
|
+
LinkedIn:
|
|
520
|
+
visibility: PUBLIC (only PUBLIC supported for photos)
|
|
521
|
+
target_linkedin_page_id: Page ID for organization posts
|
|
522
|
+
|
|
523
|
+
Facebook:
|
|
524
|
+
facebook_page_id: Facebook Page ID
|
|
525
|
+
|
|
526
|
+
Pinterest:
|
|
527
|
+
pinterest_board_id: Board ID
|
|
528
|
+
pinterest_alt_text: Alt text
|
|
529
|
+
pinterest_link: Destination link
|
|
530
|
+
|
|
531
|
+
X (Twitter):
|
|
532
|
+
reply_settings: Who can reply
|
|
533
|
+
nullcast: Promoted-only post
|
|
534
|
+
tagged_user_ids: User IDs to tag
|
|
535
|
+
x_long_text_as_post: Post long text as single post
|
|
536
|
+
|
|
537
|
+
Threads:
|
|
538
|
+
threads_long_text_as_post: Post long text as single post
|
|
539
|
+
|
|
540
|
+
Reddit:
|
|
541
|
+
subreddit: Subreddit name (without r/)
|
|
542
|
+
flair_id: Flair template ID
|
|
543
|
+
|
|
38
544
|
Returns:
|
|
39
|
-
API response
|
|
545
|
+
API response.
|
|
546
|
+
|
|
547
|
+
Raises:
|
|
548
|
+
UploadPostError: If upload fails or photo file not found.
|
|
549
|
+
"""
|
|
550
|
+
data: List[tuple] = []
|
|
551
|
+
files: List[tuple] = []
|
|
552
|
+
opened_files: List = []
|
|
553
|
+
|
|
554
|
+
try:
|
|
555
|
+
for photo in photos:
|
|
556
|
+
photo_str = str(photo)
|
|
557
|
+
if photo_str.lower().startswith(("http://", "https://")):
|
|
558
|
+
data.append(("photos[]", photo_str))
|
|
559
|
+
else:
|
|
560
|
+
photo_p = Path(photo)
|
|
561
|
+
if not photo_p.exists():
|
|
562
|
+
raise UploadPostError(f"Photo file not found: {photo_p}")
|
|
563
|
+
photo_file = photo_p.open("rb")
|
|
564
|
+
opened_files.append(photo_file)
|
|
565
|
+
files.append(("photos[]", (photo_p.name, photo_file)))
|
|
566
|
+
|
|
567
|
+
self._add_common_params(data, user, title, platforms, **kwargs)
|
|
40
568
|
|
|
569
|
+
if "tiktok" in platforms:
|
|
570
|
+
self._add_tiktok_params(data, is_video=False, **kwargs)
|
|
571
|
+
if "instagram" in platforms:
|
|
572
|
+
self._add_instagram_params(data, is_video=False, **kwargs)
|
|
573
|
+
if "linkedin" in platforms:
|
|
574
|
+
self._add_linkedin_params(data, **kwargs)
|
|
575
|
+
if "facebook" in platforms:
|
|
576
|
+
self._add_facebook_params(data, is_video=False, **kwargs)
|
|
577
|
+
if "pinterest" in platforms:
|
|
578
|
+
self._add_pinterest_params(data, is_video=False, **kwargs)
|
|
579
|
+
if "x" in platforms:
|
|
580
|
+
self._add_x_params(data, is_text=False, **kwargs)
|
|
581
|
+
if "threads" in platforms:
|
|
582
|
+
self._add_threads_params(data, **kwargs)
|
|
583
|
+
if "reddit" in platforms:
|
|
584
|
+
self._add_reddit_params(data, **kwargs)
|
|
585
|
+
|
|
586
|
+
return self._request("/upload_photos", "POST", data=data, files=files if files else None)
|
|
587
|
+
|
|
588
|
+
finally:
|
|
589
|
+
for f in opened_files:
|
|
590
|
+
f.close()
|
|
591
|
+
|
|
592
|
+
def upload_text(
|
|
593
|
+
self,
|
|
594
|
+
title: str,
|
|
595
|
+
user: str,
|
|
596
|
+
platforms: List[str],
|
|
597
|
+
**kwargs
|
|
598
|
+
) -> Dict:
|
|
599
|
+
"""
|
|
600
|
+
Upload text posts to social media platforms.
|
|
601
|
+
|
|
602
|
+
Args:
|
|
603
|
+
title: Text content for the post.
|
|
604
|
+
user: User identifier (profile name).
|
|
605
|
+
platforms: Target platforms. Supported: x, linkedin, facebook,
|
|
606
|
+
threads, reddit, bluesky
|
|
607
|
+
|
|
608
|
+
Keyword Args:
|
|
609
|
+
first_comment: First comment to post
|
|
610
|
+
scheduled_date: ISO date for scheduling
|
|
611
|
+
timezone: Timezone for scheduled date
|
|
612
|
+
add_to_queue: Add to posting queue
|
|
613
|
+
async_upload: Process asynchronously
|
|
614
|
+
|
|
615
|
+
LinkedIn:
|
|
616
|
+
target_linkedin_page_id: Page ID for organization posts
|
|
617
|
+
|
|
618
|
+
Facebook:
|
|
619
|
+
facebook_page_id: Facebook Page ID
|
|
620
|
+
facebook_link_url: URL to attach as link preview
|
|
621
|
+
|
|
622
|
+
X (Twitter):
|
|
623
|
+
reply_settings: Who can reply
|
|
624
|
+
post_url: URL to attach
|
|
625
|
+
quote_tweet_id: Tweet ID to quote
|
|
626
|
+
poll_options: Poll options (2-4 options)
|
|
627
|
+
poll_duration: Poll duration in minutes (5-10080)
|
|
628
|
+
poll_reply_settings: Who can reply to poll
|
|
629
|
+
card_uri: Card URI for Twitter Cards
|
|
630
|
+
x_long_text_as_post: Post long text as single post
|
|
631
|
+
|
|
632
|
+
Threads:
|
|
633
|
+
threads_long_text_as_post: Post long text as single post
|
|
634
|
+
|
|
635
|
+
Reddit:
|
|
636
|
+
subreddit: Subreddit name (without r/)
|
|
637
|
+
flair_id: Flair template ID
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
API response.
|
|
641
|
+
|
|
41
642
|
Raises:
|
|
42
|
-
UploadPostError: If upload fails
|
|
643
|
+
UploadPostError: If upload fails.
|
|
644
|
+
"""
|
|
645
|
+
data: List[tuple] = []
|
|
646
|
+
|
|
647
|
+
self._add_common_params(data, user, title, platforms, **kwargs)
|
|
648
|
+
|
|
649
|
+
if "linkedin" in platforms:
|
|
650
|
+
self._add_linkedin_params(data, **kwargs)
|
|
651
|
+
if "facebook" in platforms:
|
|
652
|
+
self._add_facebook_params(data, is_video=False, is_text=True, **kwargs)
|
|
653
|
+
if "x" in platforms:
|
|
654
|
+
self._add_x_params(data, is_text=True, **kwargs)
|
|
655
|
+
if "threads" in platforms:
|
|
656
|
+
self._add_threads_params(data, **kwargs)
|
|
657
|
+
if "reddit" in platforms:
|
|
658
|
+
self._add_reddit_params(data, **kwargs)
|
|
659
|
+
|
|
660
|
+
return self._request("/upload_text", "POST", data=data)
|
|
661
|
+
|
|
662
|
+
def upload_document(
|
|
663
|
+
self,
|
|
664
|
+
document_path: Union[str, Path],
|
|
665
|
+
title: str,
|
|
666
|
+
user: str,
|
|
667
|
+
description: Optional[str] = None,
|
|
668
|
+
visibility: Optional[str] = None,
|
|
669
|
+
target_linkedin_page_id: Optional[str] = None,
|
|
670
|
+
scheduled_date: Optional[str] = None,
|
|
671
|
+
timezone: Optional[str] = None,
|
|
672
|
+
add_to_queue: Optional[bool] = None,
|
|
673
|
+
async_upload: Optional[bool] = None,
|
|
674
|
+
) -> Dict:
|
|
43
675
|
"""
|
|
44
|
-
|
|
45
|
-
if not video_path.exists():
|
|
46
|
-
raise UploadPostError(f"Video file not found: {video_path}")
|
|
676
|
+
Upload a document to LinkedIn (PDF, PPT, PPTX, DOC, DOCX).
|
|
47
677
|
|
|
678
|
+
Args:
|
|
679
|
+
document_path: Path to document file or document URL.
|
|
680
|
+
title: Post title/caption.
|
|
681
|
+
user: User identifier (profile name).
|
|
682
|
+
description: Document description/commentary.
|
|
683
|
+
visibility: PUBLIC, CONNECTIONS, LOGGED_IN, CONTAINER
|
|
684
|
+
target_linkedin_page_id: Page ID for organization posts.
|
|
685
|
+
scheduled_date: ISO date for scheduling.
|
|
686
|
+
timezone: Timezone for scheduled date.
|
|
687
|
+
add_to_queue: Add to posting queue.
|
|
688
|
+
async_upload: Process asynchronously.
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
API response.
|
|
692
|
+
|
|
693
|
+
Raises:
|
|
694
|
+
UploadPostError: If upload fails or document file not found.
|
|
695
|
+
"""
|
|
696
|
+
data: List[tuple] = []
|
|
697
|
+
files: List[tuple] = []
|
|
698
|
+
doc_file = None
|
|
699
|
+
|
|
48
700
|
try:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
data
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
701
|
+
doc_str = str(document_path)
|
|
702
|
+
if doc_str.lower().startswith(("http://", "https://")):
|
|
703
|
+
data.append(("document", doc_str))
|
|
704
|
+
else:
|
|
705
|
+
doc_p = Path(document_path)
|
|
706
|
+
if not doc_p.exists():
|
|
707
|
+
raise UploadPostError(f"Document file not found: {doc_p}")
|
|
708
|
+
doc_file = doc_p.open("rb")
|
|
709
|
+
files.append(("document", (doc_p.name, doc_file)))
|
|
710
|
+
|
|
711
|
+
data.append(("user", user))
|
|
712
|
+
data.append(("title", title))
|
|
713
|
+
data.append(("platform[]", "linkedin"))
|
|
714
|
+
|
|
715
|
+
if description:
|
|
716
|
+
data.append(("description", description))
|
|
717
|
+
if visibility:
|
|
718
|
+
data.append(("visibility", visibility))
|
|
719
|
+
if target_linkedin_page_id:
|
|
720
|
+
data.append(("target_linkedin_page_id", target_linkedin_page_id))
|
|
721
|
+
if scheduled_date:
|
|
722
|
+
data.append(("scheduled_date", scheduled_date))
|
|
723
|
+
if timezone:
|
|
724
|
+
data.append(("timezone", timezone))
|
|
725
|
+
if add_to_queue is not None:
|
|
726
|
+
data.append(("add_to_queue", str(add_to_queue).lower()))
|
|
727
|
+
if async_upload is not None:
|
|
728
|
+
data.append(("async_upload", str(async_upload).lower()))
|
|
729
|
+
|
|
730
|
+
return self._request("/upload_document", "POST", data=data, files=files if files else None)
|
|
731
|
+
|
|
732
|
+
finally:
|
|
733
|
+
if doc_file:
|
|
734
|
+
doc_file.close()
|
|
735
|
+
|
|
736
|
+
# ==================== Status & History ====================
|
|
737
|
+
|
|
738
|
+
def get_status(self, request_id: str) -> Dict:
|
|
739
|
+
"""
|
|
740
|
+
Get the status of an async upload.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
request_id: The request_id from an async upload.
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
Upload status.
|
|
747
|
+
"""
|
|
748
|
+
return self._request("/uploadposts/status", "GET", params={"request_id": request_id})
|
|
749
|
+
|
|
750
|
+
def get_history(self, page: int = 1, limit: int = 20) -> Dict:
|
|
751
|
+
"""
|
|
752
|
+
Get upload history.
|
|
753
|
+
|
|
754
|
+
Args:
|
|
755
|
+
page: Page number.
|
|
756
|
+
limit: Items per page (20, 50, or 100).
|
|
757
|
+
|
|
758
|
+
Returns:
|
|
759
|
+
Upload history.
|
|
760
|
+
"""
|
|
761
|
+
return self._request("/uploadposts/history", "GET", params={"page": page, "limit": limit})
|
|
762
|
+
|
|
763
|
+
def get_analytics(self, profile_username: str, platforms: Optional[List[str]] = None) -> Dict:
|
|
764
|
+
"""
|
|
765
|
+
Get analytics for a profile.
|
|
766
|
+
|
|
767
|
+
Args:
|
|
768
|
+
profile_username: Profile username.
|
|
769
|
+
platforms: Filter by platforms (instagram, linkedin, facebook, x).
|
|
770
|
+
|
|
771
|
+
Returns:
|
|
772
|
+
Analytics data.
|
|
773
|
+
"""
|
|
774
|
+
params = {}
|
|
775
|
+
if platforms:
|
|
776
|
+
params["platforms"] = ",".join(platforms)
|
|
777
|
+
return self._request(f"/analytics/{profile_username}", "GET", params=params if params else None)
|
|
778
|
+
|
|
779
|
+
# ==================== Scheduled Posts ====================
|
|
780
|
+
|
|
781
|
+
def list_scheduled(self) -> Dict:
|
|
782
|
+
"""
|
|
783
|
+
List scheduled posts.
|
|
784
|
+
|
|
785
|
+
Returns:
|
|
786
|
+
List of scheduled posts.
|
|
787
|
+
"""
|
|
788
|
+
return self._request("/uploadposts/schedule", "GET")
|
|
789
|
+
|
|
790
|
+
def cancel_scheduled(self, job_id: str) -> Dict:
|
|
791
|
+
"""
|
|
792
|
+
Cancel a scheduled post.
|
|
793
|
+
|
|
794
|
+
Args:
|
|
795
|
+
job_id: Scheduled job ID.
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
Cancellation result.
|
|
799
|
+
"""
|
|
800
|
+
return self._request(f"/uploadposts/schedule/{job_id}", "DELETE")
|
|
801
|
+
|
|
802
|
+
def edit_scheduled(
|
|
803
|
+
self,
|
|
804
|
+
job_id: str,
|
|
805
|
+
scheduled_date: Optional[str] = None,
|
|
806
|
+
timezone: Optional[str] = None
|
|
807
|
+
) -> Dict:
|
|
808
|
+
"""
|
|
809
|
+
Edit a scheduled post.
|
|
810
|
+
|
|
811
|
+
Args:
|
|
812
|
+
job_id: Scheduled job ID.
|
|
813
|
+
scheduled_date: New scheduled date (ISO 8601).
|
|
814
|
+
timezone: New timezone.
|
|
815
|
+
|
|
816
|
+
Returns:
|
|
817
|
+
Edit result.
|
|
818
|
+
"""
|
|
819
|
+
body = {}
|
|
820
|
+
if scheduled_date:
|
|
821
|
+
body["scheduled_date"] = scheduled_date
|
|
822
|
+
if timezone:
|
|
823
|
+
body["timezone"] = timezone
|
|
824
|
+
return self._request(f"/uploadposts/schedule/{job_id}", "POST", json_data=body)
|
|
825
|
+
|
|
826
|
+
# ==================== User Management ====================
|
|
827
|
+
|
|
828
|
+
def list_users(self) -> Dict:
|
|
829
|
+
"""
|
|
830
|
+
List all users/profiles.
|
|
831
|
+
|
|
832
|
+
Returns:
|
|
833
|
+
List of users.
|
|
834
|
+
"""
|
|
835
|
+
return self._request("/uploadposts/users", "GET")
|
|
836
|
+
|
|
837
|
+
def create_user(self, username: str) -> Dict:
|
|
838
|
+
"""
|
|
839
|
+
Create a new user/profile.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
username: Profile name to create.
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
Created user.
|
|
846
|
+
"""
|
|
847
|
+
return self._request("/uploadposts/users", "POST", json_data={"username": username})
|
|
848
|
+
|
|
849
|
+
def delete_user(self, username: str) -> Dict:
|
|
850
|
+
"""
|
|
851
|
+
Delete a user/profile.
|
|
852
|
+
|
|
853
|
+
Args:
|
|
854
|
+
username: Profile name to delete.
|
|
855
|
+
|
|
856
|
+
Returns:
|
|
857
|
+
Deletion result.
|
|
858
|
+
"""
|
|
859
|
+
return self._request("/uploadposts/users", "DELETE", json_data={"username": username})
|
|
860
|
+
|
|
861
|
+
def generate_jwt(
|
|
862
|
+
self,
|
|
863
|
+
username: str,
|
|
864
|
+
redirect_url: Optional[str] = None,
|
|
865
|
+
logo_image: Optional[str] = None,
|
|
866
|
+
redirect_button_text: Optional[str] = None,
|
|
867
|
+
platforms: Optional[List[str]] = None
|
|
868
|
+
) -> Dict:
|
|
869
|
+
"""
|
|
870
|
+
Generate a JWT for platform integration.
|
|
871
|
+
Used when integrating Upload-Post into your own platform.
|
|
872
|
+
|
|
873
|
+
Args:
|
|
874
|
+
username: Profile username.
|
|
875
|
+
redirect_url: URL to redirect after linking.
|
|
876
|
+
logo_image: Logo image URL for the linking page.
|
|
877
|
+
redirect_button_text: Text for redirect button.
|
|
878
|
+
platforms: Platforms to show for connection.
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
JWT and connection URL.
|
|
882
|
+
"""
|
|
883
|
+
body: Dict[str, Any] = {"username": username}
|
|
884
|
+
if redirect_url:
|
|
885
|
+
body["redirect_url"] = redirect_url
|
|
886
|
+
if logo_image:
|
|
887
|
+
body["logo_image"] = logo_image
|
|
888
|
+
if redirect_button_text:
|
|
889
|
+
body["redirect_button_text"] = redirect_button_text
|
|
890
|
+
if platforms:
|
|
891
|
+
body["platforms"] = platforms
|
|
892
|
+
return self._request("/uploadposts/users/generate-jwt", "POST", json_data=body)
|
|
893
|
+
|
|
894
|
+
def validate_jwt(self, jwt: str) -> Dict:
|
|
895
|
+
"""
|
|
896
|
+
Validate a JWT token.
|
|
897
|
+
|
|
898
|
+
Args:
|
|
899
|
+
jwt: JWT token to validate.
|
|
900
|
+
|
|
901
|
+
Returns:
|
|
902
|
+
Validation result.
|
|
903
|
+
"""
|
|
904
|
+
return self._request("/uploadposts/users/validate-jwt", "POST", json_data={"jwt": jwt})
|
|
905
|
+
|
|
906
|
+
# ==================== Helper Endpoints ====================
|
|
907
|
+
|
|
908
|
+
def get_facebook_pages(self, profile: Optional[str] = None) -> Dict:
|
|
909
|
+
"""
|
|
910
|
+
Get Facebook pages for a profile.
|
|
911
|
+
|
|
912
|
+
Args:
|
|
913
|
+
profile: Profile username.
|
|
914
|
+
|
|
915
|
+
Returns:
|
|
916
|
+
List of Facebook pages.
|
|
917
|
+
"""
|
|
918
|
+
params = {"profile": profile} if profile else None
|
|
919
|
+
return self._request("/uploadposts/facebook/pages", "GET", params=params)
|
|
920
|
+
|
|
921
|
+
def get_linkedin_pages(self, profile: Optional[str] = None) -> Dict:
|
|
922
|
+
"""
|
|
923
|
+
Get LinkedIn pages for a profile.
|
|
924
|
+
|
|
925
|
+
Args:
|
|
926
|
+
profile: Profile username.
|
|
927
|
+
|
|
928
|
+
Returns:
|
|
929
|
+
List of LinkedIn pages.
|
|
930
|
+
"""
|
|
931
|
+
params = {"profile": profile} if profile else None
|
|
932
|
+
return self._request("/uploadposts/linkedin/pages", "GET", params=params)
|
|
933
|
+
|
|
934
|
+
def get_pinterest_boards(self, profile: Optional[str] = None) -> Dict:
|
|
935
|
+
"""
|
|
936
|
+
Get Pinterest boards for a profile.
|
|
937
|
+
|
|
938
|
+
Args:
|
|
939
|
+
profile: Profile username.
|
|
940
|
+
|
|
941
|
+
Returns:
|
|
942
|
+
List of Pinterest boards.
|
|
943
|
+
"""
|
|
944
|
+
params = {"profile": profile} if profile else None
|
|
945
|
+
return self._request("/uploadposts/pinterest/boards", "GET", params=params)
|