marqetive-lib 0.1.21__tar.gz → 0.1.22__tar.gz
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_lib-0.1.21 → marqetive_lib-0.1.22}/PKG-INFO +1 -1
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/pyproject.toml +1 -1
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/__init__.py +9 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/core/base.py +75 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/core/models.py +135 -1
- marqetive_lib-0.1.22/src/marqetive/platforms/twitter/__init__.py +15 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/twitter/client.py +346 -2
- marqetive_lib-0.1.22/src/marqetive/platforms/twitter/models.py +130 -0
- marqetive_lib-0.1.21/src/marqetive/platforms/twitter/__init__.py +0 -6
- marqetive_lib-0.1.21/src/marqetive/platforms/twitter/models.py +0 -58
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/README.md +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/core/__init__.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/core/client.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/core/exceptions.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/factory.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/__init__.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/instagram/__init__.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/instagram/client.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/instagram/exceptions.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/instagram/media.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/instagram/models.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/linkedin/__init__.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/linkedin/client.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/linkedin/exceptions.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/linkedin/media.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/linkedin/models.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/tiktok/__init__.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/tiktok/client.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/tiktok/exceptions.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/tiktok/media.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/tiktok/models.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/twitter/exceptions.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/twitter/media.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/py.typed +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/utils/__init__.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/utils/file_handlers.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/utils/helpers.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/utils/media.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/utils/oauth.py +0 -0
- {marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/utils/retry.py +0 -0
|
@@ -56,6 +56,10 @@ from marqetive.core.models import (
|
|
|
56
56
|
AuthCredentials,
|
|
57
57
|
Comment,
|
|
58
58
|
CommentStatus,
|
|
59
|
+
Conversation,
|
|
60
|
+
DirectMessage,
|
|
61
|
+
DMCreateRequest,
|
|
62
|
+
GroupDMCreateRequest,
|
|
59
63
|
MediaAttachment,
|
|
60
64
|
MediaType,
|
|
61
65
|
Post,
|
|
@@ -102,6 +106,11 @@ __all__ = [
|
|
|
102
106
|
# Base Request Models
|
|
103
107
|
"PostCreateRequest",
|
|
104
108
|
"PostUpdateRequest",
|
|
109
|
+
# Direct Message Models
|
|
110
|
+
"DMCreateRequest",
|
|
111
|
+
"GroupDMCreateRequest",
|
|
112
|
+
"DirectMessage",
|
|
113
|
+
"Conversation",
|
|
105
114
|
# Exceptions
|
|
106
115
|
"PlatformError",
|
|
107
116
|
"PlatformAuthError",
|
|
@@ -20,6 +20,10 @@ from marqetive.core.exceptions import (
|
|
|
20
20
|
from marqetive.core.models import (
|
|
21
21
|
AuthCredentials,
|
|
22
22
|
Comment,
|
|
23
|
+
Conversation,
|
|
24
|
+
DirectMessage,
|
|
25
|
+
DMCreateRequest,
|
|
26
|
+
GroupDMCreateRequest,
|
|
23
27
|
MediaAttachment,
|
|
24
28
|
PlatformResponse,
|
|
25
29
|
Post,
|
|
@@ -418,6 +422,77 @@ class SocialMediaPlatform(ABC):
|
|
|
418
422
|
f"{self.platform_name} does not support thread creation"
|
|
419
423
|
)
|
|
420
424
|
|
|
425
|
+
# ==================== Direct Message Methods ====================
|
|
426
|
+
|
|
427
|
+
async def send_direct_message(
|
|
428
|
+
self,
|
|
429
|
+
request: DMCreateRequest,
|
|
430
|
+
) -> DirectMessage:
|
|
431
|
+
"""Send a direct message to a user or conversation.
|
|
432
|
+
|
|
433
|
+
Not all platforms support direct messaging. The default implementation
|
|
434
|
+
raises NotImplementedError. Platforms that support DMs (like Twitter)
|
|
435
|
+
should override this method.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
request: DM creation request with text and recipient info.
|
|
439
|
+
Must provide either participant_id (for new 1-to-1 DM)
|
|
440
|
+
or conversation_id (for existing conversation).
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
DirectMessage object with message ID and metadata.
|
|
444
|
+
|
|
445
|
+
Raises:
|
|
446
|
+
NotImplementedError: If platform doesn't support DMs.
|
|
447
|
+
ValidationError: If request is invalid (missing recipient, text too long).
|
|
448
|
+
PlatformAuthError: If not authenticated or unauthorized.
|
|
449
|
+
MediaUploadError: If media attachment fails.
|
|
450
|
+
|
|
451
|
+
Example:
|
|
452
|
+
>>> request = DMCreateRequest(
|
|
453
|
+
... text="Hello!",
|
|
454
|
+
... participant_id="user123"
|
|
455
|
+
... )
|
|
456
|
+
>>> dm = await client.send_direct_message(request)
|
|
457
|
+
>>> print(f"Sent: {dm.message_id}")
|
|
458
|
+
"""
|
|
459
|
+
raise NotImplementedError(
|
|
460
|
+
f"{self.platform_name} does not support direct messages"
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
async def create_group_conversation(
|
|
464
|
+
self,
|
|
465
|
+
request: GroupDMCreateRequest,
|
|
466
|
+
) -> Conversation:
|
|
467
|
+
"""Create a group DM conversation with an initial message.
|
|
468
|
+
|
|
469
|
+
Not all platforms support group DMs. The default implementation
|
|
470
|
+
raises NotImplementedError. Platforms that support group DMs
|
|
471
|
+
should override this method.
|
|
472
|
+
|
|
473
|
+
Args:
|
|
474
|
+
request: Group DM request with participant IDs and initial message.
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
Conversation object with conversation ID and participant info.
|
|
478
|
+
|
|
479
|
+
Raises:
|
|
480
|
+
NotImplementedError: If platform doesn't support group DMs.
|
|
481
|
+
ValidationError: If request is invalid (too few/many participants).
|
|
482
|
+
PlatformAuthError: If not authenticated or unauthorized.
|
|
483
|
+
|
|
484
|
+
Example:
|
|
485
|
+
>>> request = GroupDMCreateRequest(
|
|
486
|
+
... participant_ids=["user1", "user2", "user3"],
|
|
487
|
+
... text="Welcome to the group!"
|
|
488
|
+
... )
|
|
489
|
+
>>> conversation = await client.create_group_conversation(request)
|
|
490
|
+
>>> print(f"Created: {conversation.conversation_id}")
|
|
491
|
+
"""
|
|
492
|
+
raise NotImplementedError(
|
|
493
|
+
f"{self.platform_name} does not support group conversations"
|
|
494
|
+
)
|
|
495
|
+
|
|
421
496
|
# ==================== Abstract Comment Methods ====================
|
|
422
497
|
|
|
423
498
|
@abstractmethod
|
|
@@ -7,7 +7,7 @@ and type safety.
|
|
|
7
7
|
|
|
8
8
|
from datetime import UTC, datetime, timedelta
|
|
9
9
|
from enum import Enum, StrEnum
|
|
10
|
-
from typing import Any
|
|
10
|
+
from typing import Any, Literal
|
|
11
11
|
|
|
12
12
|
from pydantic import BaseModel, ConfigDict, Field, HttpUrl
|
|
13
13
|
|
|
@@ -451,3 +451,137 @@ class PostUpdateRequest(BaseModel):
|
|
|
451
451
|
content: str | None = None
|
|
452
452
|
tags: list[str] | None = None
|
|
453
453
|
location: str | None = None
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ==================== Direct Message Models ====================
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class DMCreateRequest(BaseModel):
|
|
460
|
+
"""Base request model for sending a direct message.
|
|
461
|
+
|
|
462
|
+
Supports sending DMs to individuals or existing conversations.
|
|
463
|
+
For platform-specific features, use dedicated request models like TwitterDMRequest.
|
|
464
|
+
|
|
465
|
+
Attributes:
|
|
466
|
+
text: Message content (required, max length varies by platform)
|
|
467
|
+
participant_id: User ID for new 1-to-1 conversation
|
|
468
|
+
conversation_id: ID of existing conversation (1-1 or group)
|
|
469
|
+
media_url: URL to media file to attach (single attachment)
|
|
470
|
+
media_id: Pre-uploaded media ID
|
|
471
|
+
additional_data: Platform-specific data
|
|
472
|
+
|
|
473
|
+
Example:
|
|
474
|
+
>>> # Send 1-to-1 DM
|
|
475
|
+
>>> request = DMCreateRequest(
|
|
476
|
+
... text="Hello!",
|
|
477
|
+
... participant_id="1234567890"
|
|
478
|
+
... )
|
|
479
|
+
|
|
480
|
+
>>> # Send to existing conversation
|
|
481
|
+
>>> request = DMCreateRequest(
|
|
482
|
+
... text="Following up...",
|
|
483
|
+
... conversation_id="dm_conv_123"
|
|
484
|
+
... )
|
|
485
|
+
"""
|
|
486
|
+
|
|
487
|
+
text: str
|
|
488
|
+
participant_id: str | None = None
|
|
489
|
+
conversation_id: str | None = None
|
|
490
|
+
media_url: str | None = None
|
|
491
|
+
media_id: str | None = None
|
|
492
|
+
additional_data: dict[str, Any] = Field(default_factory=dict)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
class GroupDMCreateRequest(BaseModel):
|
|
496
|
+
"""Request model for creating a group conversation with initial message.
|
|
497
|
+
|
|
498
|
+
Creates a new group DM conversation with multiple participants and sends
|
|
499
|
+
an initial message to the group.
|
|
500
|
+
|
|
501
|
+
Attributes:
|
|
502
|
+
participant_ids: List of user IDs to include in group (platform limits vary)
|
|
503
|
+
text: Initial message content (required)
|
|
504
|
+
media_url: Optional media URL to attach
|
|
505
|
+
media_id: Optional pre-uploaded media ID
|
|
506
|
+
additional_data: Platform-specific data
|
|
507
|
+
|
|
508
|
+
Example:
|
|
509
|
+
>>> request = GroupDMCreateRequest(
|
|
510
|
+
... participant_ids=["user1_id", "user2_id", "user3_id"],
|
|
511
|
+
... text="Welcome to the group!"
|
|
512
|
+
... )
|
|
513
|
+
"""
|
|
514
|
+
|
|
515
|
+
participant_ids: list[str] = Field(min_length=2)
|
|
516
|
+
text: str
|
|
517
|
+
media_url: str | None = None
|
|
518
|
+
media_id: str | None = None
|
|
519
|
+
additional_data: dict[str, Any] = Field(default_factory=dict)
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
class DirectMessage(BaseModel):
|
|
523
|
+
"""Universal representation of a direct message.
|
|
524
|
+
|
|
525
|
+
Provides a consistent interface for DMs across platforms that support messaging.
|
|
526
|
+
|
|
527
|
+
Attributes:
|
|
528
|
+
message_id: Platform-specific message identifier
|
|
529
|
+
conversation_id: ID of the conversation this message belongs to
|
|
530
|
+
platform: Name of the platform (twitter, etc.)
|
|
531
|
+
text: Message content
|
|
532
|
+
sender_id: ID of the user who sent the message
|
|
533
|
+
created_at: Timestamp when message was sent
|
|
534
|
+
media: Optional media attachment
|
|
535
|
+
raw_data: Original platform-specific response data
|
|
536
|
+
|
|
537
|
+
Example:
|
|
538
|
+
>>> dm = DirectMessage(
|
|
539
|
+
... message_id="dm_event_123",
|
|
540
|
+
... conversation_id="dm_conv_456",
|
|
541
|
+
... platform="twitter",
|
|
542
|
+
... text="Hello!",
|
|
543
|
+
... sender_id="user789",
|
|
544
|
+
... created_at=datetime.now()
|
|
545
|
+
... )
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
message_id: str
|
|
549
|
+
conversation_id: str
|
|
550
|
+
platform: str
|
|
551
|
+
text: str
|
|
552
|
+
sender_id: str | None = None
|
|
553
|
+
created_at: datetime
|
|
554
|
+
media: MediaAttachment | None = None
|
|
555
|
+
raw_data: dict[str, Any] = Field(default_factory=dict)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
class Conversation(BaseModel):
|
|
559
|
+
"""Universal representation of a DM conversation.
|
|
560
|
+
|
|
561
|
+
Represents a direct message conversation (1-to-1 or group) on platforms
|
|
562
|
+
that support messaging.
|
|
563
|
+
|
|
564
|
+
Attributes:
|
|
565
|
+
conversation_id: Platform-specific conversation identifier
|
|
566
|
+
platform: Name of the platform
|
|
567
|
+
conversation_type: Type of conversation ("group" or "one_to_one")
|
|
568
|
+
participant_ids: List of participant user IDs
|
|
569
|
+
created_at: Timestamp when conversation was created
|
|
570
|
+
raw_data: Original platform-specific response data
|
|
571
|
+
|
|
572
|
+
Example:
|
|
573
|
+
>>> conversation = Conversation(
|
|
574
|
+
... conversation_id="dm_conv_123",
|
|
575
|
+
... platform="twitter",
|
|
576
|
+
... conversation_type="group",
|
|
577
|
+
... participant_ids=["user1", "user2", "user3"],
|
|
578
|
+
... created_at=datetime.now()
|
|
579
|
+
... )
|
|
580
|
+
"""
|
|
581
|
+
|
|
582
|
+
conversation_id: str
|
|
583
|
+
platform: str
|
|
584
|
+
conversation_type: Literal["group", "one_to_one"]
|
|
585
|
+
participant_ids: list[str] = Field(default_factory=list)
|
|
586
|
+
created_at: datetime
|
|
587
|
+
raw_data: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Twitter/X platform integration."""
|
|
2
|
+
|
|
3
|
+
from marqetive.platforms.twitter.client import TwitterClient
|
|
4
|
+
from marqetive.platforms.twitter.models import (
|
|
5
|
+
TwitterDMRequest,
|
|
6
|
+
TwitterGroupDMRequest,
|
|
7
|
+
TwitterPostRequest,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"TwitterClient",
|
|
12
|
+
"TwitterPostRequest",
|
|
13
|
+
"TwitterDMRequest",
|
|
14
|
+
"TwitterGroupDMRequest",
|
|
15
|
+
]
|
|
@@ -26,6 +26,10 @@ from marqetive.core.models import (
|
|
|
26
26
|
AuthCredentials,
|
|
27
27
|
Comment,
|
|
28
28
|
CommentStatus,
|
|
29
|
+
Conversation,
|
|
30
|
+
DirectMessage,
|
|
31
|
+
DMCreateRequest,
|
|
32
|
+
GroupDMCreateRequest,
|
|
29
33
|
MediaAttachment,
|
|
30
34
|
MediaType,
|
|
31
35
|
Post,
|
|
@@ -34,8 +38,8 @@ from marqetive.core.models import (
|
|
|
34
38
|
PostUpdateRequest,
|
|
35
39
|
ProgressStatus,
|
|
36
40
|
)
|
|
37
|
-
from marqetive.platforms.twitter.media import TwitterMediaManager
|
|
38
|
-
from marqetive.platforms.twitter.models import TwitterPostRequest
|
|
41
|
+
from marqetive.platforms.twitter.media import MediaCategory, TwitterMediaManager
|
|
42
|
+
from marqetive.platforms.twitter.models import TwitterDMRequest, TwitterPostRequest
|
|
39
43
|
|
|
40
44
|
|
|
41
45
|
class TwitterClient(SocialMediaPlatform):
|
|
@@ -755,6 +759,346 @@ class TwitterClient(SocialMediaPlatform):
|
|
|
755
759
|
|
|
756
760
|
return created_posts
|
|
757
761
|
|
|
762
|
+
# ==================== Direct Message Methods ====================
|
|
763
|
+
|
|
764
|
+
def _validate_dm_request(
|
|
765
|
+
self,
|
|
766
|
+
request: DMCreateRequest | TwitterDMRequest,
|
|
767
|
+
) -> None:
|
|
768
|
+
"""Validate DM request.
|
|
769
|
+
|
|
770
|
+
Twitter DM Requirements:
|
|
771
|
+
- Must have text content (required)
|
|
772
|
+
- Text max 10,000 characters
|
|
773
|
+
- Must have either participant_id OR conversation_id (not both)
|
|
774
|
+
- Max 1 media attachment
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
request: DM request to validate.
|
|
778
|
+
|
|
779
|
+
Raises:
|
|
780
|
+
ValidationError: If validation fails.
|
|
781
|
+
"""
|
|
782
|
+
if not request.text:
|
|
783
|
+
raise ValidationError(
|
|
784
|
+
"DM text content is required",
|
|
785
|
+
platform=self.platform_name,
|
|
786
|
+
field="text",
|
|
787
|
+
)
|
|
788
|
+
|
|
789
|
+
if len(request.text) > 10000:
|
|
790
|
+
raise ValidationError(
|
|
791
|
+
f"DM text exceeds 10,000 characters ({len(request.text)} characters)",
|
|
792
|
+
platform=self.platform_name,
|
|
793
|
+
field="text",
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# Must have exactly one of participant_id or conversation_id
|
|
797
|
+
has_participant = request.participant_id is not None
|
|
798
|
+
has_conversation = request.conversation_id is not None
|
|
799
|
+
|
|
800
|
+
if not has_participant and not has_conversation:
|
|
801
|
+
raise ValidationError(
|
|
802
|
+
"Either participant_id or conversation_id is required",
|
|
803
|
+
platform=self.platform_name,
|
|
804
|
+
field="participant_id",
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
if has_participant and has_conversation:
|
|
808
|
+
raise ValidationError(
|
|
809
|
+
"Cannot specify both participant_id and conversation_id",
|
|
810
|
+
platform=self.platform_name,
|
|
811
|
+
field="conversation_id",
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
async def _upload_dm_media(
|
|
815
|
+
self,
|
|
816
|
+
media_url: str,
|
|
817
|
+
alt_text: str | None = None,
|
|
818
|
+
) -> MediaAttachment:
|
|
819
|
+
"""Upload media for DM attachment.
|
|
820
|
+
|
|
821
|
+
Uses DM-specific media category for proper Twitter processing.
|
|
822
|
+
|
|
823
|
+
Args:
|
|
824
|
+
media_url: URL or file path of media.
|
|
825
|
+
alt_text: Optional alt text for accessibility.
|
|
826
|
+
|
|
827
|
+
Returns:
|
|
828
|
+
MediaAttachment with media ID.
|
|
829
|
+
|
|
830
|
+
Raises:
|
|
831
|
+
MediaUploadError: If upload fails.
|
|
832
|
+
"""
|
|
833
|
+
if not self._media_manager:
|
|
834
|
+
raise RuntimeError("Client must be used as async context manager")
|
|
835
|
+
|
|
836
|
+
try:
|
|
837
|
+
# Detect media type and use appropriate DM category
|
|
838
|
+
from marqetive.utils.media import detect_mime_type
|
|
839
|
+
|
|
840
|
+
# Determine DM-specific media category based on file type
|
|
841
|
+
if media_url.startswith(("http://", "https://")):
|
|
842
|
+
# Default to DM_IMAGE for URLs (will be validated by Twitter)
|
|
843
|
+
category = MediaCategory.DM_IMAGE
|
|
844
|
+
else:
|
|
845
|
+
mime_type = detect_mime_type(media_url)
|
|
846
|
+
if "video" in mime_type:
|
|
847
|
+
category = MediaCategory.DM_VIDEO
|
|
848
|
+
elif "gif" in mime_type:
|
|
849
|
+
category = MediaCategory.DM_GIF
|
|
850
|
+
else:
|
|
851
|
+
category = MediaCategory.DM_IMAGE
|
|
852
|
+
|
|
853
|
+
result = await self._media_manager.upload_media(
|
|
854
|
+
media_url,
|
|
855
|
+
media_category=category,
|
|
856
|
+
alt_text=alt_text,
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
return MediaAttachment(
|
|
860
|
+
media_id=result.media_id,
|
|
861
|
+
media_type=MediaType.IMAGE, # Simplified type
|
|
862
|
+
url=HttpUrl(media_url)
|
|
863
|
+
if media_url.startswith("http")
|
|
864
|
+
else HttpUrl(f"file://{media_url}"),
|
|
865
|
+
)
|
|
866
|
+
|
|
867
|
+
except Exception as e:
|
|
868
|
+
raise MediaUploadError(
|
|
869
|
+
f"Failed to upload DM media: {e}",
|
|
870
|
+
platform=self.platform_name,
|
|
871
|
+
) from e
|
|
872
|
+
|
|
873
|
+
async def send_direct_message(
|
|
874
|
+
self,
|
|
875
|
+
request: DMCreateRequest,
|
|
876
|
+
) -> DirectMessage:
|
|
877
|
+
"""Send a direct message on Twitter.
|
|
878
|
+
|
|
879
|
+
Supports both 1-to-1 DMs (new conversations) and messages to existing
|
|
880
|
+
conversations (including groups). Optionally attach a single media file.
|
|
881
|
+
|
|
882
|
+
Twitter Requirements:
|
|
883
|
+
- Text required (max 10,000 characters)
|
|
884
|
+
- Either participant_id (new 1-to-1) or conversation_id (existing)
|
|
885
|
+
- Max 1 media attachment per DM
|
|
886
|
+
|
|
887
|
+
Args:
|
|
888
|
+
request: DM creation request. Supports:
|
|
889
|
+
- text: Message content (required, max 10,000 chars)
|
|
890
|
+
- participant_id: User ID for new 1-to-1 DM
|
|
891
|
+
- conversation_id: Existing conversation ID
|
|
892
|
+
- media_url: URL of media to attach
|
|
893
|
+
- media_id: Pre-uploaded media ID
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
DirectMessage object with:
|
|
897
|
+
- message_id: Twitter DM event ID
|
|
898
|
+
- conversation_id: Conversation ID
|
|
899
|
+
- platform: "twitter"
|
|
900
|
+
- text: Message content
|
|
901
|
+
- sender_id: Authenticated user ID (if available)
|
|
902
|
+
- created_at: Timestamp
|
|
903
|
+
- media: Attached media (if any)
|
|
904
|
+
- raw_data: Full API response
|
|
905
|
+
|
|
906
|
+
Raises:
|
|
907
|
+
ValidationError: If request is invalid.
|
|
908
|
+
MediaUploadError: If media upload fails.
|
|
909
|
+
RateLimitError: If Twitter rate limit exceeded.
|
|
910
|
+
PlatformError: For other Twitter API errors.
|
|
911
|
+
RuntimeError: If client not used as context manager.
|
|
912
|
+
|
|
913
|
+
Example:
|
|
914
|
+
>>> async with TwitterClient(credentials) as client:
|
|
915
|
+
... request = DMCreateRequest(
|
|
916
|
+
... text="Hello!",
|
|
917
|
+
... participant_id="1234567890"
|
|
918
|
+
... )
|
|
919
|
+
... dm = await client.send_direct_message(request)
|
|
920
|
+
... print(f"Sent DM: {dm.message_id}")
|
|
921
|
+
"""
|
|
922
|
+
if not self._tweepy_client:
|
|
923
|
+
raise RuntimeError("Client must be used as async context manager")
|
|
924
|
+
|
|
925
|
+
# Validate request
|
|
926
|
+
self._validate_dm_request(request)
|
|
927
|
+
|
|
928
|
+
try:
|
|
929
|
+
# Handle media upload if provided
|
|
930
|
+
media_id = request.media_id
|
|
931
|
+
if request.media_url and not media_id:
|
|
932
|
+
media_attachment = await self._upload_dm_media(request.media_url)
|
|
933
|
+
media_id = media_attachment.media_id
|
|
934
|
+
|
|
935
|
+
# Build DM parameters
|
|
936
|
+
dm_params: dict[str, Any] = {"text": request.text}
|
|
937
|
+
|
|
938
|
+
if request.participant_id:
|
|
939
|
+
dm_params["participant_id"] = request.participant_id
|
|
940
|
+
elif request.conversation_id:
|
|
941
|
+
dm_params["dm_conversation_id"] = request.conversation_id
|
|
942
|
+
|
|
943
|
+
if media_id:
|
|
944
|
+
dm_params["media_id"] = media_id
|
|
945
|
+
|
|
946
|
+
# Send DM using tweepy
|
|
947
|
+
response = self._tweepy_client.create_direct_message(
|
|
948
|
+
**dm_params,
|
|
949
|
+
user_auth=False,
|
|
950
|
+
)
|
|
951
|
+
|
|
952
|
+
# Parse response
|
|
953
|
+
dm_data = response.data # type: ignore[attr-defined]
|
|
954
|
+
|
|
955
|
+
return DirectMessage(
|
|
956
|
+
message_id=str(dm_data["dm_event_id"]),
|
|
957
|
+
conversation_id=str(dm_data["dm_conversation_id"]),
|
|
958
|
+
platform=self.platform_name,
|
|
959
|
+
text=request.text,
|
|
960
|
+
sender_id=self.credentials.user_id,
|
|
961
|
+
created_at=datetime.now(),
|
|
962
|
+
media=None, # Media info not returned in create response
|
|
963
|
+
raw_data=dm_data,
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
except tweepy.TweepyException as e:
|
|
967
|
+
if "429" in str(e):
|
|
968
|
+
raise RateLimitError(
|
|
969
|
+
"Twitter rate limit exceeded",
|
|
970
|
+
platform=self.platform_name,
|
|
971
|
+
status_code=429,
|
|
972
|
+
) from e
|
|
973
|
+
if "403" in str(e) or "401" in str(e):
|
|
974
|
+
raise PlatformAuthError(
|
|
975
|
+
f"Not authorized to send DM: {e}",
|
|
976
|
+
platform=self.platform_name,
|
|
977
|
+
) from e
|
|
978
|
+
raise PlatformError(
|
|
979
|
+
f"Failed to send direct message: {e}",
|
|
980
|
+
platform=self.platform_name,
|
|
981
|
+
) from e
|
|
982
|
+
|
|
983
|
+
async def create_group_conversation(
|
|
984
|
+
self,
|
|
985
|
+
request: GroupDMCreateRequest,
|
|
986
|
+
) -> Conversation:
|
|
987
|
+
"""Create a Twitter group DM conversation with initial message.
|
|
988
|
+
|
|
989
|
+
Creates a new group conversation with 2-49 other participants
|
|
990
|
+
and sends an initial message to the group.
|
|
991
|
+
|
|
992
|
+
Args:
|
|
993
|
+
request: Group DM request with:
|
|
994
|
+
- participant_ids: List of user IDs (2-49 participants)
|
|
995
|
+
- text: Initial message text (required, max 10,000 chars)
|
|
996
|
+
- media_url: Optional media URL
|
|
997
|
+
- media_id: Optional pre-uploaded media ID
|
|
998
|
+
|
|
999
|
+
Returns:
|
|
1000
|
+
Conversation object with:
|
|
1001
|
+
- conversation_id: Twitter conversation ID
|
|
1002
|
+
- platform: "twitter"
|
|
1003
|
+
- conversation_type: "group"
|
|
1004
|
+
- participant_ids: List of participant IDs
|
|
1005
|
+
- created_at: Timestamp
|
|
1006
|
+
- raw_data: Full API response
|
|
1007
|
+
|
|
1008
|
+
Raises:
|
|
1009
|
+
ValidationError: If request is invalid.
|
|
1010
|
+
MediaUploadError: If media upload fails.
|
|
1011
|
+
RateLimitError: If rate limit exceeded.
|
|
1012
|
+
PlatformError: For other API errors.
|
|
1013
|
+
RuntimeError: If client not used as context manager.
|
|
1014
|
+
|
|
1015
|
+
Example:
|
|
1016
|
+
>>> async with TwitterClient(credentials) as client:
|
|
1017
|
+
... request = GroupDMCreateRequest(
|
|
1018
|
+
... participant_ids=["user1", "user2"],
|
|
1019
|
+
... text="Welcome to the group!"
|
|
1020
|
+
... )
|
|
1021
|
+
... conv = await client.create_group_conversation(request)
|
|
1022
|
+
... print(f"Created group: {conv.conversation_id}")
|
|
1023
|
+
"""
|
|
1024
|
+
if not self._tweepy_client:
|
|
1025
|
+
raise RuntimeError("Client must be used as async context manager")
|
|
1026
|
+
|
|
1027
|
+
# Validate participant count
|
|
1028
|
+
if len(request.participant_ids) < 2:
|
|
1029
|
+
raise ValidationError(
|
|
1030
|
+
"Group conversation requires at least 2 participants",
|
|
1031
|
+
platform=self.platform_name,
|
|
1032
|
+
field="participant_ids",
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
if len(request.participant_ids) > 49:
|
|
1036
|
+
raise ValidationError(
|
|
1037
|
+
"Group conversation cannot exceed 49 participants",
|
|
1038
|
+
platform=self.platform_name,
|
|
1039
|
+
field="participant_ids",
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
# Validate text
|
|
1043
|
+
if not request.text:
|
|
1044
|
+
raise ValidationError(
|
|
1045
|
+
"Initial message text is required",
|
|
1046
|
+
platform=self.platform_name,
|
|
1047
|
+
field="text",
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
if len(request.text) > 10000:
|
|
1051
|
+
raise ValidationError(
|
|
1052
|
+
f"Message text exceeds 10,000 characters ({len(request.text)} chars)",
|
|
1053
|
+
platform=self.platform_name,
|
|
1054
|
+
field="text",
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
try:
|
|
1058
|
+
# Handle media upload if provided
|
|
1059
|
+
media_id = request.media_id
|
|
1060
|
+
if request.media_url and not media_id:
|
|
1061
|
+
media_attachment = await self._upload_dm_media(request.media_url)
|
|
1062
|
+
media_id = media_attachment.media_id
|
|
1063
|
+
|
|
1064
|
+
# Create group conversation using tweepy
|
|
1065
|
+
conv_params: dict[str, Any] = {
|
|
1066
|
+
"conversation_type": "Group",
|
|
1067
|
+
"participant_ids": request.participant_ids,
|
|
1068
|
+
"text": request.text,
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
if media_id:
|
|
1072
|
+
conv_params["media_id"] = media_id
|
|
1073
|
+
|
|
1074
|
+
response = self._tweepy_client.create_direct_message_conversation(
|
|
1075
|
+
**conv_params,
|
|
1076
|
+
user_auth=False,
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
conv_data = response.data # type: ignore[attr-defined]
|
|
1080
|
+
|
|
1081
|
+
return Conversation(
|
|
1082
|
+
conversation_id=str(conv_data["dm_conversation_id"]),
|
|
1083
|
+
platform=self.platform_name,
|
|
1084
|
+
conversation_type="group",
|
|
1085
|
+
participant_ids=request.participant_ids,
|
|
1086
|
+
created_at=datetime.now(),
|
|
1087
|
+
raw_data=conv_data,
|
|
1088
|
+
)
|
|
1089
|
+
|
|
1090
|
+
except tweepy.TweepyException as e:
|
|
1091
|
+
if "429" in str(e):
|
|
1092
|
+
raise RateLimitError(
|
|
1093
|
+
"Twitter rate limit exceeded",
|
|
1094
|
+
platform=self.platform_name,
|
|
1095
|
+
status_code=429,
|
|
1096
|
+
) from e
|
|
1097
|
+
raise PlatformError(
|
|
1098
|
+
f"Failed to create group conversation: {e}",
|
|
1099
|
+
platform=self.platform_name,
|
|
1100
|
+
) from e
|
|
1101
|
+
|
|
758
1102
|
# ==================== Helper Methods ====================
|
|
759
1103
|
|
|
760
1104
|
def _parse_tweet(
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"""Twitter/X-specific models for post creation and direct messages.
|
|
2
|
+
|
|
3
|
+
This module defines Twitter-specific data models for creating tweets,
|
|
4
|
+
replies, quote tweets, polls, and direct messages.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, Field
|
|
8
|
+
|
|
9
|
+
# ==================== Post Models ====================
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TwitterPostRequest(BaseModel):
|
|
13
|
+
"""Twitter/X-specific post creation request.
|
|
14
|
+
|
|
15
|
+
Supports tweets, replies, quote tweets, and media attachments.
|
|
16
|
+
Twitter has a 280 character limit for text content.
|
|
17
|
+
|
|
18
|
+
Attributes:
|
|
19
|
+
content: Tweet text (max 280 characters)
|
|
20
|
+
media_urls: List of media URLs to attach (max 4 images or 1 video)
|
|
21
|
+
media_ids: List of pre-uploaded media IDs
|
|
22
|
+
reply_to_post_id: Tweet ID to reply to
|
|
23
|
+
quote_post_id: Tweet ID to quote
|
|
24
|
+
poll_options: List of poll options (2-4 options, each max 25 chars)
|
|
25
|
+
poll_duration_minutes: Poll duration in minutes (5-10080)
|
|
26
|
+
alt_texts: Alt text for each media item (for accessibility)
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> # Simple tweet
|
|
30
|
+
>>> request = TwitterPostRequest(content="Hello Twitter!")
|
|
31
|
+
|
|
32
|
+
>>> # Reply to a tweet
|
|
33
|
+
>>> request = TwitterPostRequest(
|
|
34
|
+
... content="Great point!",
|
|
35
|
+
... reply_to_post_id="1234567890"
|
|
36
|
+
... )
|
|
37
|
+
|
|
38
|
+
>>> # Quote tweet with media
|
|
39
|
+
>>> request = TwitterPostRequest(
|
|
40
|
+
... content="Check this out!",
|
|
41
|
+
... quote_post_id="1234567890",
|
|
42
|
+
... media_urls=["https://example.com/image.jpg"]
|
|
43
|
+
... )
|
|
44
|
+
|
|
45
|
+
>>> # Tweet with poll
|
|
46
|
+
>>> request = TwitterPostRequest(
|
|
47
|
+
... content="What's your favorite?",
|
|
48
|
+
... poll_options=["Option A", "Option B", "Option C"],
|
|
49
|
+
... poll_duration_minutes=1440
|
|
50
|
+
... )
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
content: str | None = None
|
|
54
|
+
media_urls: list[str] = Field(default_factory=list, max_length=4)
|
|
55
|
+
media_ids: list[str] = Field(default_factory=list, max_length=4)
|
|
56
|
+
reply_to_post_id: str | None = None
|
|
57
|
+
quote_post_id: str | None = None
|
|
58
|
+
poll_options: list[str] = Field(default_factory=list, max_length=4)
|
|
59
|
+
poll_duration_minutes: int | None = Field(default=None, ge=5, le=10080)
|
|
60
|
+
alt_texts: list[str] = Field(default_factory=list)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ==================== Direct Message Models ====================
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TwitterDMRequest(BaseModel):
|
|
67
|
+
"""Twitter/X-specific direct message request.
|
|
68
|
+
|
|
69
|
+
Supports sending DMs to individuals or existing conversations.
|
|
70
|
+
Twitter has a 10,000 character limit for DM text content.
|
|
71
|
+
Only one media attachment is allowed per DM.
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
text: Message text (max 10,000 characters)
|
|
75
|
+
participant_id: User ID for new 1-to-1 DM (mutually exclusive with conversation_id)
|
|
76
|
+
conversation_id: Existing conversation ID (mutually exclusive with participant_id)
|
|
77
|
+
media_url: URL of media to attach (max 1 attachment)
|
|
78
|
+
media_id: Pre-uploaded media ID (from TwitterMediaManager)
|
|
79
|
+
|
|
80
|
+
Example:
|
|
81
|
+
>>> # Send 1-to-1 DM
|
|
82
|
+
>>> request = TwitterDMRequest(
|
|
83
|
+
... text="Hello!",
|
|
84
|
+
... participant_id="1234567890"
|
|
85
|
+
... )
|
|
86
|
+
|
|
87
|
+
>>> # Send to existing conversation
|
|
88
|
+
>>> request = TwitterDMRequest(
|
|
89
|
+
... text="Hello group!",
|
|
90
|
+
... conversation_id="dm_conv_123456"
|
|
91
|
+
... )
|
|
92
|
+
|
|
93
|
+
>>> # Send DM with media
|
|
94
|
+
>>> request = TwitterDMRequest(
|
|
95
|
+
... text="Check this out!",
|
|
96
|
+
... participant_id="1234567890",
|
|
97
|
+
... media_url="https://example.com/image.jpg"
|
|
98
|
+
... )
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
text: str = Field(max_length=10000)
|
|
102
|
+
participant_id: str | None = None
|
|
103
|
+
conversation_id: str | None = None
|
|
104
|
+
media_url: str | None = None
|
|
105
|
+
media_id: str | None = None
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class TwitterGroupDMRequest(BaseModel):
|
|
109
|
+
"""Twitter/X-specific group DM creation request.
|
|
110
|
+
|
|
111
|
+
Creates a new group conversation with multiple participants and an initial message.
|
|
112
|
+
Twitter allows 2-49 participants in a group DM (excluding the sender).
|
|
113
|
+
|
|
114
|
+
Attributes:
|
|
115
|
+
participant_ids: List of user IDs (2-49 participants, excluding sender)
|
|
116
|
+
text: Initial message text (max 10,000 characters)
|
|
117
|
+
media_url: URL of media to attach
|
|
118
|
+
media_id: Pre-uploaded media ID
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> request = TwitterGroupDMRequest(
|
|
122
|
+
... participant_ids=["user1_id", "user2_id", "user3_id"],
|
|
123
|
+
... text="Welcome to the group!"
|
|
124
|
+
... )
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
participant_ids: list[str] = Field(min_length=2, max_length=49)
|
|
128
|
+
text: str = Field(max_length=10000)
|
|
129
|
+
media_url: str | None = None
|
|
130
|
+
media_id: str | None = None
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
"""Twitter/X-specific models for post creation.
|
|
2
|
-
|
|
3
|
-
This module defines Twitter-specific data models for creating tweets,
|
|
4
|
-
replies, quote tweets, and polls.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from pydantic import BaseModel, Field
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class TwitterPostRequest(BaseModel):
|
|
11
|
-
"""Twitter/X-specific post creation request.
|
|
12
|
-
|
|
13
|
-
Supports tweets, replies, quote tweets, and media attachments.
|
|
14
|
-
Twitter has a 280 character limit for text content.
|
|
15
|
-
|
|
16
|
-
Attributes:
|
|
17
|
-
content: Tweet text (max 280 characters)
|
|
18
|
-
media_urls: List of media URLs to attach (max 4 images or 1 video)
|
|
19
|
-
media_ids: List of pre-uploaded media IDs
|
|
20
|
-
reply_to_post_id: Tweet ID to reply to
|
|
21
|
-
quote_post_id: Tweet ID to quote
|
|
22
|
-
poll_options: List of poll options (2-4 options, each max 25 chars)
|
|
23
|
-
poll_duration_minutes: Poll duration in minutes (5-10080)
|
|
24
|
-
alt_texts: Alt text for each media item (for accessibility)
|
|
25
|
-
|
|
26
|
-
Example:
|
|
27
|
-
>>> # Simple tweet
|
|
28
|
-
>>> request = TwitterPostRequest(content="Hello Twitter!")
|
|
29
|
-
|
|
30
|
-
>>> # Reply to a tweet
|
|
31
|
-
>>> request = TwitterPostRequest(
|
|
32
|
-
... content="Great point!",
|
|
33
|
-
... reply_to_post_id="1234567890"
|
|
34
|
-
... )
|
|
35
|
-
|
|
36
|
-
>>> # Quote tweet with media
|
|
37
|
-
>>> request = TwitterPostRequest(
|
|
38
|
-
... content="Check this out!",
|
|
39
|
-
... quote_post_id="1234567890",
|
|
40
|
-
... media_urls=["https://example.com/image.jpg"]
|
|
41
|
-
... )
|
|
42
|
-
|
|
43
|
-
>>> # Tweet with poll
|
|
44
|
-
>>> request = TwitterPostRequest(
|
|
45
|
-
... content="What's your favorite?",
|
|
46
|
-
... poll_options=["Option A", "Option B", "Option C"],
|
|
47
|
-
... poll_duration_minutes=1440
|
|
48
|
-
... )
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
content: str | None = None
|
|
52
|
-
media_urls: list[str] = Field(default_factory=list, max_length=4)
|
|
53
|
-
media_ids: list[str] = Field(default_factory=list, max_length=4)
|
|
54
|
-
reply_to_post_id: str | None = None
|
|
55
|
-
quote_post_id: str | None = None
|
|
56
|
-
poll_options: list[str] = Field(default_factory=list, max_length=4)
|
|
57
|
-
poll_duration_minutes: int | None = Field(default=None, ge=5, le=10080)
|
|
58
|
-
alt_texts: list[str] = Field(default_factory=list)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/instagram/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{marqetive_lib-0.1.21 → marqetive_lib-0.1.22}/src/marqetive/platforms/linkedin/exceptions.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|