marqetive-lib 0.1.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.
- marqetive/__init__.py +113 -0
- marqetive/core/__init__.py +5 -0
- marqetive/core/account_factory.py +212 -0
- marqetive/core/base_manager.py +303 -0
- marqetive/core/client.py +108 -0
- marqetive/core/progress.py +291 -0
- marqetive/core/registry.py +257 -0
- marqetive/platforms/__init__.py +55 -0
- marqetive/platforms/base.py +390 -0
- marqetive/platforms/exceptions.py +238 -0
- marqetive/platforms/instagram/__init__.py +7 -0
- marqetive/platforms/instagram/client.py +786 -0
- marqetive/platforms/instagram/exceptions.py +311 -0
- marqetive/platforms/instagram/factory.py +106 -0
- marqetive/platforms/instagram/manager.py +112 -0
- marqetive/platforms/instagram/media.py +669 -0
- marqetive/platforms/linkedin/__init__.py +7 -0
- marqetive/platforms/linkedin/client.py +733 -0
- marqetive/platforms/linkedin/exceptions.py +335 -0
- marqetive/platforms/linkedin/factory.py +130 -0
- marqetive/platforms/linkedin/manager.py +119 -0
- marqetive/platforms/linkedin/media.py +549 -0
- marqetive/platforms/models.py +345 -0
- marqetive/platforms/tiktok/__init__.py +0 -0
- marqetive/platforms/twitter/__init__.py +7 -0
- marqetive/platforms/twitter/client.py +647 -0
- marqetive/platforms/twitter/exceptions.py +311 -0
- marqetive/platforms/twitter/factory.py +151 -0
- marqetive/platforms/twitter/manager.py +121 -0
- marqetive/platforms/twitter/media.py +779 -0
- marqetive/platforms/twitter/threads.py +442 -0
- marqetive/py.typed +0 -0
- marqetive/registry_init.py +66 -0
- marqetive/utils/__init__.py +45 -0
- marqetive/utils/file_handlers.py +438 -0
- marqetive/utils/helpers.py +99 -0
- marqetive/utils/media.py +399 -0
- marqetive/utils/oauth.py +265 -0
- marqetive/utils/retry.py +239 -0
- marqetive/utils/token_validator.py +240 -0
- marqetive_lib-0.1.0.dist-info/METADATA +261 -0
- marqetive_lib-0.1.0.dist-info/RECORD +43 -0
- marqetive_lib-0.1.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""Twitter-specific exception handling and error code mapping.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive error handling for Twitter API errors including:
|
|
4
|
+
- HTTP status code mapping
|
|
5
|
+
- Twitter-specific error codes
|
|
6
|
+
- Retry strategies
|
|
7
|
+
- User-friendly error messages
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from marqetive.platforms.exceptions import (
|
|
13
|
+
MediaUploadError,
|
|
14
|
+
PlatformAuthError,
|
|
15
|
+
PlatformError,
|
|
16
|
+
RateLimitError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Twitter API error codes
|
|
22
|
+
# Source: https://developer.x.com/en/support/twitter-api/error-troubleshooting
|
|
23
|
+
class TwitterErrorCode:
|
|
24
|
+
"""Twitter API error codes."""
|
|
25
|
+
|
|
26
|
+
# Authentication errors (200-299)
|
|
27
|
+
COULD_NOT_AUTHENTICATE = 32
|
|
28
|
+
INVALID_OR_EXPIRED_TOKEN = 89
|
|
29
|
+
UNABLE_TO_VERIFY_CREDENTIALS = 99
|
|
30
|
+
BAD_AUTHENTICATION_DATA = 215
|
|
31
|
+
|
|
32
|
+
# Authorization errors (300-399)
|
|
33
|
+
FORBIDDEN = 64
|
|
34
|
+
ACCOUNT_SUSPENDED = 63
|
|
35
|
+
API_ACCESS_REVOKED = 87
|
|
36
|
+
|
|
37
|
+
# Resource errors (400-499)
|
|
38
|
+
PAGE_NOT_EXIST = 34
|
|
39
|
+
USER_NOT_FOUND = 17
|
|
40
|
+
TWEET_NOT_FOUND = 144
|
|
41
|
+
NO_STATUS_FOUND = 34
|
|
42
|
+
|
|
43
|
+
# Rate limiting (500-599)
|
|
44
|
+
RATE_LIMIT_EXCEEDED = 88
|
|
45
|
+
TOO_MANY_REQUESTS = 429
|
|
46
|
+
|
|
47
|
+
# Validation errors (600-699)
|
|
48
|
+
STATUS_TOO_LONG = 186
|
|
49
|
+
DUPLICATE_STATUS = 187
|
|
50
|
+
INVALID_MEDIA = 324
|
|
51
|
+
MEDIA_ID_NOT_FOUND = 325
|
|
52
|
+
|
|
53
|
+
# Media upload errors
|
|
54
|
+
MEDIA_TYPE_UNSUPPORTED = 323
|
|
55
|
+
MEDIA_SIZE_TOO_LARGE = 324
|
|
56
|
+
VIDEO_DURATION_TOO_LONG = 324
|
|
57
|
+
MEDIA_PROCESSING_FAILED = 324
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Mapping of error codes to user-friendly messages
|
|
61
|
+
ERROR_MESSAGES: dict[int, str] = {
|
|
62
|
+
# Authentication
|
|
63
|
+
32: "Could not authenticate. Please check your API credentials.",
|
|
64
|
+
89: "Invalid or expired access token. Please re-authenticate.",
|
|
65
|
+
99: "Unable to verify your credentials. Please check your API keys.",
|
|
66
|
+
215: "Bad authentication data. Please verify your OAuth credentials.",
|
|
67
|
+
# Authorization
|
|
68
|
+
64: "Your account is not authorized to access this resource.",
|
|
69
|
+
63: "Your account is suspended and cannot be accessed.",
|
|
70
|
+
87: "API access has been revoked for this application.",
|
|
71
|
+
# Resources
|
|
72
|
+
34: "The requested page or resource does not exist.",
|
|
73
|
+
17: "User not found.",
|
|
74
|
+
144: "Tweet not found.",
|
|
75
|
+
# Rate limiting
|
|
76
|
+
88: "Rate limit exceeded. Please wait before making more requests.",
|
|
77
|
+
429: "Too many requests. You have hit a rate limit.",
|
|
78
|
+
# Validation
|
|
79
|
+
186: "Tweet is too long. Maximum length is 280 characters.",
|
|
80
|
+
187: "Duplicate tweet. You've already posted this content.",
|
|
81
|
+
324: "Invalid media for upload.",
|
|
82
|
+
325: "Media ID not found.",
|
|
83
|
+
323: "Media type not supported.",
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# Retryable error codes
|
|
88
|
+
RETRYABLE_ERROR_CODES = {
|
|
89
|
+
88, # Rate limit exceeded
|
|
90
|
+
130, # Over capacity
|
|
91
|
+
131, # Internal error
|
|
92
|
+
429, # Too many requests
|
|
93
|
+
500, # Internal server error
|
|
94
|
+
502, # Bad gateway
|
|
95
|
+
503, # Service unavailable
|
|
96
|
+
504, # Gateway timeout
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
# Non-retryable error codes
|
|
100
|
+
NON_RETRYABLE_ERROR_CODES = {
|
|
101
|
+
32, # Authentication failed
|
|
102
|
+
89, # Invalid token
|
|
103
|
+
99, # Unable to verify credentials
|
|
104
|
+
64, # Forbidden
|
|
105
|
+
63, # Account suspended
|
|
106
|
+
186, # Status too long
|
|
107
|
+
187, # Duplicate status
|
|
108
|
+
144, # Tweet not found
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def map_twitter_error(
|
|
113
|
+
status_code: int | None,
|
|
114
|
+
error_code: int | None = None,
|
|
115
|
+
error_message: str | None = None,
|
|
116
|
+
response_data: dict[str, Any] | None = None,
|
|
117
|
+
) -> PlatformError:
|
|
118
|
+
"""Map Twitter API error to appropriate exception.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
status_code: HTTP status code.
|
|
122
|
+
error_code: Twitter-specific error code.
|
|
123
|
+
error_message: Error message from API.
|
|
124
|
+
response_data: Full response data from API.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Appropriate PlatformError subclass.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> error = map_twitter_error(401, 32, "Could not authenticate")
|
|
131
|
+
>>> print(type(error).__name__)
|
|
132
|
+
PlatformAuthError
|
|
133
|
+
"""
|
|
134
|
+
# Extract error details from response if provided
|
|
135
|
+
if response_data and "errors" in response_data:
|
|
136
|
+
errors = response_data["errors"]
|
|
137
|
+
if errors and isinstance(errors, list):
|
|
138
|
+
first_error = errors[0]
|
|
139
|
+
if not error_code:
|
|
140
|
+
error_code = first_error.get("code")
|
|
141
|
+
if not error_message:
|
|
142
|
+
error_message = first_error.get("message")
|
|
143
|
+
|
|
144
|
+
# Get user-friendly message
|
|
145
|
+
friendly_message = ERROR_MESSAGES.get(
|
|
146
|
+
error_code or 0, error_message or "Unknown error"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Determine retry-after for rate limits
|
|
150
|
+
retry_after = None
|
|
151
|
+
if status_code == 429:
|
|
152
|
+
# Twitter typically uses 15-minute windows
|
|
153
|
+
retry_after = 900 # 15 minutes in seconds
|
|
154
|
+
|
|
155
|
+
# Map to appropriate exception type
|
|
156
|
+
# Authentication errors
|
|
157
|
+
if error_code in (32, 89, 99, 215) or status_code in (401, 403):
|
|
158
|
+
return PlatformAuthError(
|
|
159
|
+
friendly_message,
|
|
160
|
+
platform="twitter",
|
|
161
|
+
status_code=status_code,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# Rate limit errors
|
|
165
|
+
if error_code in (88, 429) or status_code == 429:
|
|
166
|
+
return RateLimitError(
|
|
167
|
+
friendly_message,
|
|
168
|
+
platform="twitter",
|
|
169
|
+
status_code=status_code or 429,
|
|
170
|
+
retry_after=retry_after,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Validation errors
|
|
174
|
+
if error_code in (186, 187, 324, 325, 323):
|
|
175
|
+
return ValidationError(
|
|
176
|
+
friendly_message,
|
|
177
|
+
platform="twitter",
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
# Media upload errors
|
|
181
|
+
if error_code in (323, 324, 325):
|
|
182
|
+
return MediaUploadError(
|
|
183
|
+
friendly_message,
|
|
184
|
+
platform="twitter",
|
|
185
|
+
status_code=status_code,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Generic platform error
|
|
189
|
+
return PlatformError(
|
|
190
|
+
friendly_message,
|
|
191
|
+
platform="twitter",
|
|
192
|
+
status_code=status_code,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def is_retryable_twitter_error(
|
|
197
|
+
status_code: int | None,
|
|
198
|
+
error_code: int | None = None,
|
|
199
|
+
) -> bool:
|
|
200
|
+
"""Determine if a Twitter error is retryable.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
status_code: HTTP status code.
|
|
204
|
+
error_code: Twitter-specific error code.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
True if error is retryable, False otherwise.
|
|
208
|
+
|
|
209
|
+
Example:
|
|
210
|
+
>>> is_retryable_twitter_error(503)
|
|
211
|
+
True
|
|
212
|
+
>>> is_retryable_twitter_error(401, 32)
|
|
213
|
+
False
|
|
214
|
+
"""
|
|
215
|
+
# Check explicit non-retryable codes first
|
|
216
|
+
if error_code in NON_RETRYABLE_ERROR_CODES:
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
# Check retryable codes
|
|
220
|
+
if error_code in RETRYABLE_ERROR_CODES:
|
|
221
|
+
return True
|
|
222
|
+
|
|
223
|
+
# Check HTTP status codes
|
|
224
|
+
if status_code:
|
|
225
|
+
# 5xx errors are generally retryable
|
|
226
|
+
if 500 <= status_code < 600:
|
|
227
|
+
return True
|
|
228
|
+
# 429 is retryable
|
|
229
|
+
if status_code == 429:
|
|
230
|
+
return True
|
|
231
|
+
# 4xx errors (except 429) are generally not retryable
|
|
232
|
+
if 400 <= status_code < 500:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
# Default to not retryable for safety
|
|
236
|
+
return False
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def get_retry_delay(
|
|
240
|
+
status_code: int | None,
|
|
241
|
+
error_code: int | None = None,
|
|
242
|
+
attempt: int = 1,
|
|
243
|
+
) -> float:
|
|
244
|
+
"""Get recommended retry delay for Twitter error.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
status_code: HTTP status code.
|
|
248
|
+
error_code: Twitter-specific error code.
|
|
249
|
+
attempt: Current retry attempt number.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
Recommended delay in seconds.
|
|
253
|
+
|
|
254
|
+
Example:
|
|
255
|
+
>>> get_retry_delay(503, attempt=1)
|
|
256
|
+
5.0
|
|
257
|
+
>>> get_retry_delay(429)
|
|
258
|
+
900.0
|
|
259
|
+
"""
|
|
260
|
+
# Rate limit errors - wait full window
|
|
261
|
+
if error_code in (88, 429) or status_code == 429:
|
|
262
|
+
return 900.0 # 15 minutes
|
|
263
|
+
|
|
264
|
+
# Server errors - exponential backoff
|
|
265
|
+
if status_code and 500 <= status_code < 600:
|
|
266
|
+
base_delay = 5.0
|
|
267
|
+
return min(base_delay * (2 ** (attempt - 1)), 60.0)
|
|
268
|
+
|
|
269
|
+
# Default delay
|
|
270
|
+
return 5.0
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class TwitterAPIError(PlatformError):
|
|
274
|
+
"""Twitter API specific error with detailed information.
|
|
275
|
+
|
|
276
|
+
Attributes:
|
|
277
|
+
error_code: Twitter-specific error code.
|
|
278
|
+
is_retryable: Whether the error is retryable.
|
|
279
|
+
retry_delay: Recommended retry delay in seconds.
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
def __init__(
|
|
283
|
+
self,
|
|
284
|
+
message: str,
|
|
285
|
+
*,
|
|
286
|
+
status_code: int | None = None,
|
|
287
|
+
error_code: int | None = None,
|
|
288
|
+
response_data: dict[str, Any] | None = None,
|
|
289
|
+
) -> None:
|
|
290
|
+
"""Initialize Twitter API error.
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
message: Error message.
|
|
294
|
+
status_code: HTTP status code.
|
|
295
|
+
error_code: Twitter-specific error code.
|
|
296
|
+
response_data: Full response data from API.
|
|
297
|
+
"""
|
|
298
|
+
super().__init__(message, platform="twitter", status_code=status_code)
|
|
299
|
+
self.error_code = error_code
|
|
300
|
+
self.response_data = response_data
|
|
301
|
+
self.is_retryable = is_retryable_twitter_error(status_code, error_code)
|
|
302
|
+
self.retry_delay = get_retry_delay(status_code, error_code)
|
|
303
|
+
|
|
304
|
+
def __repr__(self) -> str:
|
|
305
|
+
"""String representation of error."""
|
|
306
|
+
return (
|
|
307
|
+
f"TwitterAPIError(message={self.message!r}, "
|
|
308
|
+
f"status_code={self.status_code}, "
|
|
309
|
+
f"error_code={self.error_code}, "
|
|
310
|
+
f"retryable={self.is_retryable})"
|
|
311
|
+
)
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Twitter account factory for managing credentials and client creation."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from marqetive.core.account_factory import BaseAccountFactory
|
|
8
|
+
from marqetive.platforms.exceptions import PlatformAuthError
|
|
9
|
+
from marqetive.platforms.models import AccountStatus, AuthCredentials
|
|
10
|
+
from marqetive.platforms.twitter.client import TwitterClient
|
|
11
|
+
from marqetive.utils.oauth import refresh_twitter_token
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TwitterAccountFactory(BaseAccountFactory):
|
|
17
|
+
"""Factory for creating and managing Twitter/X accounts and clients.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> factory = TwitterAccountFactory(
|
|
21
|
+
... client_id="your_client_id",
|
|
22
|
+
... client_secret="your_client_secret"
|
|
23
|
+
... )
|
|
24
|
+
>>> credentials = AuthCredentials(
|
|
25
|
+
... platform="twitter",
|
|
26
|
+
... access_token="token",
|
|
27
|
+
... refresh_token="refresh"
|
|
28
|
+
... )
|
|
29
|
+
>>> client = await factory.create_authenticated_client(credentials)
|
|
30
|
+
>>> async with client:
|
|
31
|
+
... tweet = await client.create_post(request)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
client_id: str | None = None,
|
|
37
|
+
client_secret: str | None = None,
|
|
38
|
+
on_status_update: Callable[[str, AccountStatus], None] | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
"""Initialize Twitter account factory.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
client_id: Twitter OAuth client ID (uses TWITTER_CLIENT_ID env if None).
|
|
44
|
+
client_secret: Twitter OAuth client secret (uses TWITTER_CLIENT_SECRET env if None).
|
|
45
|
+
on_status_update: Optional callback when account status changes.
|
|
46
|
+
"""
|
|
47
|
+
super().__init__(on_status_update=on_status_update)
|
|
48
|
+
self.client_id = client_id or os.getenv("TWITTER_CLIENT_ID")
|
|
49
|
+
self.client_secret = client_secret or os.getenv("TWITTER_CLIENT_SECRET")
|
|
50
|
+
|
|
51
|
+
if not self.client_id or not self.client_secret:
|
|
52
|
+
logger.warning(
|
|
53
|
+
"Twitter client_id/client_secret not provided. "
|
|
54
|
+
"Token refresh will not work."
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def platform_name(self) -> str:
|
|
59
|
+
"""Get platform name."""
|
|
60
|
+
return "twitter"
|
|
61
|
+
|
|
62
|
+
async def refresh_token(self, credentials: AuthCredentials) -> AuthCredentials:
|
|
63
|
+
"""Refresh Twitter OAuth2 access token.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
credentials: Current credentials with refresh token.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Updated credentials with new access token.
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
PlatformAuthError: If refresh fails or credentials missing.
|
|
73
|
+
"""
|
|
74
|
+
if not self.client_id or not self.client_secret:
|
|
75
|
+
raise PlatformAuthError(
|
|
76
|
+
"Twitter client_id and client_secret are required for token refresh",
|
|
77
|
+
platform=self.platform_name,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if not credentials.refresh_token:
|
|
81
|
+
raise PlatformAuthError(
|
|
82
|
+
"No refresh token available",
|
|
83
|
+
platform=self.platform_name,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
logger.info("Refreshing Twitter access token...")
|
|
87
|
+
return await refresh_twitter_token(
|
|
88
|
+
credentials,
|
|
89
|
+
self.client_id,
|
|
90
|
+
self.client_secret,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
async def create_client(self, credentials: AuthCredentials) -> TwitterClient:
|
|
94
|
+
"""Create Twitter API client.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
credentials: Valid Twitter credentials.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
TwitterClient instance.
|
|
101
|
+
|
|
102
|
+
Raises:
|
|
103
|
+
PlatformAuthError: If credentials are invalid.
|
|
104
|
+
"""
|
|
105
|
+
if not credentials.access_token:
|
|
106
|
+
raise PlatformAuthError(
|
|
107
|
+
"Access token is required",
|
|
108
|
+
platform=self.platform_name,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Get additional data for Twitter
|
|
112
|
+
additional_data = credentials.additional_data or {}
|
|
113
|
+
|
|
114
|
+
# Twitter needs API key/secret for some operations
|
|
115
|
+
api_key = additional_data.get("api_key") or os.getenv("TWITTER_API_KEY")
|
|
116
|
+
api_secret = additional_data.get("api_secret") or os.getenv(
|
|
117
|
+
"TWITTER_API_SECRET"
|
|
118
|
+
)
|
|
119
|
+
additional_data.get("access_secret")
|
|
120
|
+
|
|
121
|
+
# Update additional_data if we have env values
|
|
122
|
+
if api_key:
|
|
123
|
+
additional_data["api_key"] = api_key
|
|
124
|
+
if api_secret:
|
|
125
|
+
additional_data["api_secret"] = api_secret
|
|
126
|
+
|
|
127
|
+
credentials.additional_data = additional_data
|
|
128
|
+
|
|
129
|
+
return TwitterClient(credentials=credentials)
|
|
130
|
+
|
|
131
|
+
async def validate_credentials(self, credentials: AuthCredentials) -> bool:
|
|
132
|
+
"""Validate Twitter credentials by making a test API call.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
credentials: Credentials to validate.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
True if credentials are valid, False otherwise.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
client = await self.create_client(credentials)
|
|
142
|
+
async with client:
|
|
143
|
+
# Try to verify credentials by getting current user
|
|
144
|
+
# This is a lightweight call to test authentication
|
|
145
|
+
if client._tweepy_client:
|
|
146
|
+
me = client._tweepy_client.get_me()
|
|
147
|
+
return me is not None
|
|
148
|
+
return False
|
|
149
|
+
except Exception as e:
|
|
150
|
+
logger.error(f"Error validating Twitter credentials: {e}")
|
|
151
|
+
return False
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Twitter post manager for handling post operations."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from marqetive.core.base_manager import BasePostManager
|
|
7
|
+
from marqetive.platforms.models import AuthCredentials, Post, PostCreateRequest
|
|
8
|
+
from marqetive.platforms.twitter.client import TwitterClient
|
|
9
|
+
from marqetive.platforms.twitter.factory import TwitterAccountFactory
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TwitterPostManager(BasePostManager):
|
|
15
|
+
"""Manager for Twitter/X post operations.
|
|
16
|
+
|
|
17
|
+
Coordinates post creation, media uploads, and progress tracking for Twitter.
|
|
18
|
+
|
|
19
|
+
Example:
|
|
20
|
+
>>> manager = TwitterPostManager()
|
|
21
|
+
>>> credentials = AuthCredentials(
|
|
22
|
+
... platform="twitter",
|
|
23
|
+
... access_token="token",
|
|
24
|
+
... refresh_token="refresh"
|
|
25
|
+
... )
|
|
26
|
+
>>> request = PostCreateRequest(content="Hello Twitter!")
|
|
27
|
+
>>> post = await manager.execute_post(credentials, request)
|
|
28
|
+
>>> print(f"Tweet ID: {post.post_id}")
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
account_factory: TwitterAccountFactory | None = None,
|
|
34
|
+
client_id: str | None = None,
|
|
35
|
+
client_secret: str | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialize Twitter post manager.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
account_factory: Twitter account factory (creates default if None).
|
|
41
|
+
client_id: Twitter OAuth client ID (for default factory).
|
|
42
|
+
client_secret: Twitter OAuth client secret (for default factory).
|
|
43
|
+
"""
|
|
44
|
+
if account_factory is None:
|
|
45
|
+
account_factory = TwitterAccountFactory(
|
|
46
|
+
client_id=client_id,
|
|
47
|
+
client_secret=client_secret,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
super().__init__(account_factory=account_factory)
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def platform_name(self) -> str:
|
|
54
|
+
"""Get platform name."""
|
|
55
|
+
return "twitter"
|
|
56
|
+
|
|
57
|
+
async def _execute_post_impl(
|
|
58
|
+
self,
|
|
59
|
+
client: Any,
|
|
60
|
+
request: PostCreateRequest,
|
|
61
|
+
credentials: AuthCredentials, # noqa: ARG002
|
|
62
|
+
) -> Post:
|
|
63
|
+
"""Execute Twitter post creation.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
client: TwitterClient instance.
|
|
67
|
+
request: Post creation request.
|
|
68
|
+
credentials: Twitter credentials.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Created Post object.
|
|
72
|
+
"""
|
|
73
|
+
if not isinstance(client, TwitterClient):
|
|
74
|
+
raise TypeError(f"Expected TwitterClient, got {type(client)}")
|
|
75
|
+
|
|
76
|
+
# Handle media uploads with progress tracking
|
|
77
|
+
media_ids: list[str] = []
|
|
78
|
+
if request.media_urls:
|
|
79
|
+
self._progress_tracker.emit_start(
|
|
80
|
+
"upload_media",
|
|
81
|
+
total=len(request.media_urls),
|
|
82
|
+
message=f"Uploading {len(request.media_urls)} media files...",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
for idx, media_url in enumerate(request.media_urls):
|
|
86
|
+
if self.is_cancelled():
|
|
87
|
+
raise InterruptedError("Post creation was cancelled")
|
|
88
|
+
|
|
89
|
+
self._progress_tracker.emit_progress(
|
|
90
|
+
"upload_media",
|
|
91
|
+
progress=idx,
|
|
92
|
+
total=len(request.media_urls),
|
|
93
|
+
message=f"Uploading media {idx + 1}/{len(request.media_urls)}...",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
# Twitter media upload is simplified in the client
|
|
97
|
+
# In production, this would use tweepy's media upload
|
|
98
|
+
media_attachment = await client.upload_media(
|
|
99
|
+
media_url=media_url,
|
|
100
|
+
media_type="image", # Default to image
|
|
101
|
+
alt_text=None,
|
|
102
|
+
)
|
|
103
|
+
media_ids.append(media_attachment.media_id)
|
|
104
|
+
|
|
105
|
+
self._progress_tracker.emit_complete(
|
|
106
|
+
"upload_media",
|
|
107
|
+
message="All media uploaded successfully",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
# Create post with progress tracking
|
|
111
|
+
self._progress_tracker.emit_progress(
|
|
112
|
+
"execute_post",
|
|
113
|
+
progress=50,
|
|
114
|
+
total=100,
|
|
115
|
+
message="Creating tweet...",
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
# Use the client to create the post
|
|
119
|
+
post = await client.create_post(request)
|
|
120
|
+
|
|
121
|
+
return post
|