imbot-sdk-python 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.
imbot_sdk/consts.py ADDED
@@ -0,0 +1,36 @@
1
+ """Connection states and client error codes."""
2
+
3
+ from enum import IntEnum
4
+
5
+
6
+ class ConnectState(IntEnum):
7
+ """Connection lifecycle state."""
8
+
9
+ DISCONNECT = 0
10
+ CONNECTING = 1
11
+ CONNECTED = 2
12
+
13
+
14
+ class ClientErrorCode:
15
+ """Client error codes.
16
+
17
+ These are plain integer constants rather than an ``IntEnum`` because the
18
+ server may return arbitrary business codes that are surfaced verbatim.
19
+ """
20
+
21
+ SUCCESS = 0
22
+
23
+ UNKNOWN = 20000
24
+ SOCKET_FAILED = 20001
25
+ CONNECT_TIMEOUT = 20002
26
+ NEED_REDIRECT = 20003
27
+ CONNECT_EXISTED = 20004
28
+ PING_TIMEOUT = 20005
29
+ CONNECT_FAILED = 20006
30
+ CONNECT_CLOSED = 20007
31
+ INIT_ENCRYPTOR_FAILED = 20008
32
+ NEGOTIATE_FAILED = 20009
33
+
34
+ SEND_TIMEOUT = 21001
35
+
36
+ QUERY_TIMEOUT = 22001
@@ -0,0 +1,35 @@
1
+ """Synchronous request/response rendezvous with timeout.
2
+
3
+ A one-shot slot that a producer thread fills via :meth:`put` while a consumer
4
+ thread blocks in :meth:`get_with_timeout`. Used to wait for ACK frames keyed by
5
+ request index.
6
+ """
7
+
8
+ import threading
9
+ from typing import Any, Optional
10
+
11
+
12
+ class TimeoutError(Exception):
13
+ """Raised by :meth:`DataAccessor.get_with_timeout` when no data arrives."""
14
+
15
+
16
+ class DataAccessor:
17
+ def __init__(self) -> None:
18
+ self._event = threading.Event()
19
+ self._data: Optional[Any] = None
20
+ self._lock = threading.Lock()
21
+
22
+ def put(self, data: Any) -> None:
23
+ with self._lock:
24
+ self._data = data
25
+ self._event.set()
26
+
27
+ def get_with_timeout(self, timeout: float) -> Any:
28
+ """Block up to ``timeout`` seconds for a value.
29
+
30
+ :raises TimeoutError: if nothing was put in time.
31
+ """
32
+ if not self._event.wait(timeout):
33
+ raise TimeoutError("time up")
34
+ with self._lock:
35
+ return self._data
imbot_sdk/listeners.py ADDED
@@ -0,0 +1,67 @@
1
+ """Listener base classes.
2
+
3
+ Subclass and override the callbacks you care about; every method has a no-op
4
+ default so you only implement what you need. Callbacks run on internal threads
5
+ — avoid long blocking work inside them.
6
+ """
7
+
8
+ from typing import List
9
+
10
+ from .consts import ClientErrorCode, ConnectState
11
+ from .models import (
12
+ Conversation,
13
+ ConversationInfo,
14
+ Message,
15
+ MessageReaction,
16
+ )
17
+
18
+
19
+ class ConnectionStatusChangeListener:
20
+ """Receives connection status changes."""
21
+
22
+ def on_status_change(self, status: ConnectState, code: int) -> None:
23
+ pass
24
+
25
+
26
+ class MessageListener:
27
+ """Receives message events (receive / recall / update / reaction / top)."""
28
+
29
+ def on_message_receive(self, msg: Message) -> None:
30
+ pass
31
+
32
+ def on_message_recall(self, msg: Message) -> None:
33
+ pass
34
+
35
+ def on_message_update(self, msg: Message) -> None:
36
+ pass
37
+
38
+ def on_message_delete(self, conver: Conversation, msg_ids: List[str]) -> None:
39
+ pass
40
+
41
+ def on_message_clear(self, conver: Conversation, time: int, sender_id: str) -> None:
42
+ pass
43
+
44
+ def on_message_reaction_add(self, conver: Conversation, reaction: MessageReaction) -> None:
45
+ pass
46
+
47
+ def on_message_reaction_remove(self, conver: Conversation, reaction: MessageReaction) -> None:
48
+ pass
49
+
50
+ def on_message_set_top(self, message: Message, operator_id: str, is_top: bool) -> None:
51
+ pass
52
+
53
+
54
+ class ConversationChangeListener:
55
+ """Receives conversation list and total-unread-count changes."""
56
+
57
+ def on_conversation_info_add(self, convers: List[ConversationInfo]) -> None:
58
+ pass
59
+
60
+ def on_conversation_info_update(self, convers: List[ConversationInfo]) -> None:
61
+ pass
62
+
63
+ def on_conversation_info_delete(self, convers: List[ConversationInfo]) -> None:
64
+ pass
65
+
66
+ def on_total_unread_message_count_update(self, count: int) -> None:
67
+ pass
@@ -0,0 +1,93 @@
1
+ """Message content models and a decoder registry."""
2
+
3
+ from ..models import MESSAGE_CONTENT_TYPE_UNKNOWN
4
+ from .base import (
5
+ MessageContent,
6
+ MESSAGE_CONTENT_TYPE_TEXT,
7
+ MESSAGE_CONTENT_TYPE_IMAGE,
8
+ MESSAGE_CONTENT_TYPE_FILE,
9
+ MESSAGE_CONTENT_TYPE_VIDEO,
10
+ MESSAGE_CONTENT_TYPE_VOICE,
11
+ MESSAGE_CONTENT_TYPE_STREAM_TEXT,
12
+ MESSAGE_CONTENT_TYPE_RECALL_INFO,
13
+ MESSAGE_CONTENT_TYPE_MERGE,
14
+ MESSAGE_CONTENT_TYPE_THUMBNAIL_PACKED_IMAGE,
15
+ MESSAGE_CONTENT_TYPE_SNAPSHOT_PACKED_VIDEO,
16
+ )
17
+ from .contents import (
18
+ TextMessage,
19
+ ImageMessage,
20
+ FileMessage,
21
+ VideoMessage,
22
+ VoiceMessage,
23
+ StreamTextMessage,
24
+ RecallInfoMessage,
25
+ MergeMessage,
26
+ MergeMessagePreviewUnit,
27
+ ThumbnailPackedImageMessage,
28
+ SnapshotPackedVideoMessage,
29
+ UnknownMessage,
30
+ )
31
+
32
+ # Factory map: content type -> zero-arg constructor.
33
+ _CONTENT_FACTORIES = {
34
+ MESSAGE_CONTENT_TYPE_TEXT: TextMessage,
35
+ MESSAGE_CONTENT_TYPE_IMAGE: ImageMessage,
36
+ MESSAGE_CONTENT_TYPE_FILE: FileMessage,
37
+ MESSAGE_CONTENT_TYPE_VIDEO: VideoMessage,
38
+ MESSAGE_CONTENT_TYPE_VOICE: VoiceMessage,
39
+ MESSAGE_CONTENT_TYPE_STREAM_TEXT: StreamTextMessage,
40
+ MESSAGE_CONTENT_TYPE_RECALL_INFO: RecallInfoMessage,
41
+ MESSAGE_CONTENT_TYPE_MERGE: MergeMessage,
42
+ MESSAGE_CONTENT_TYPE_THUMBNAIL_PACKED_IMAGE: ThumbnailPackedImageMessage,
43
+ MESSAGE_CONTENT_TYPE_SNAPSHOT_PACKED_VIDEO: SnapshotPackedVideoMessage,
44
+ }
45
+
46
+
47
+ def decode_message_content(msg_type: str, data: bytes) -> MessageContent:
48
+ """Decode raw content bytes into a typed model.
49
+
50
+ Unknown types or decode failures fall back to :class:`UnknownMessage`.
51
+ """
52
+ factory = _CONTENT_FACTORIES.get(msg_type)
53
+ if factory is None:
54
+ unknown = UnknownMessage(msg_type)
55
+ unknown.decode(data)
56
+ return unknown
57
+ content = factory()
58
+ try:
59
+ content.decode(data)
60
+ except Exception:
61
+ unknown = UnknownMessage(msg_type)
62
+ unknown.decode(data)
63
+ return unknown
64
+ return content
65
+
66
+
67
+ __all__ = [
68
+ "MessageContent",
69
+ "TextMessage",
70
+ "ImageMessage",
71
+ "FileMessage",
72
+ "VideoMessage",
73
+ "VoiceMessage",
74
+ "StreamTextMessage",
75
+ "RecallInfoMessage",
76
+ "MergeMessage",
77
+ "MergeMessagePreviewUnit",
78
+ "ThumbnailPackedImageMessage",
79
+ "SnapshotPackedVideoMessage",
80
+ "UnknownMessage",
81
+ "decode_message_content",
82
+ "MESSAGE_CONTENT_TYPE_TEXT",
83
+ "MESSAGE_CONTENT_TYPE_IMAGE",
84
+ "MESSAGE_CONTENT_TYPE_FILE",
85
+ "MESSAGE_CONTENT_TYPE_VIDEO",
86
+ "MESSAGE_CONTENT_TYPE_VOICE",
87
+ "MESSAGE_CONTENT_TYPE_STREAM_TEXT",
88
+ "MESSAGE_CONTENT_TYPE_RECALL_INFO",
89
+ "MESSAGE_CONTENT_TYPE_MERGE",
90
+ "MESSAGE_CONTENT_TYPE_THUMBNAIL_PACKED_IMAGE",
91
+ "MESSAGE_CONTENT_TYPE_SNAPSHOT_PACKED_VIDEO",
92
+ "MESSAGE_CONTENT_TYPE_UNKNOWN",
93
+ ]
@@ -0,0 +1,54 @@
1
+ """Base class and type constants for message content models.
2
+
3
+ Concrete content types serialize to/from compact JSON, so messages are
4
+ interchangeable with the other JuggleIM client SDKs.
5
+ """
6
+
7
+ import json
8
+
9
+ from ..models import DEFAULT_MESSAGE_FLAGS, MESSAGE_CONTENT_TYPE_UNKNOWN
10
+
11
+ # Built-in content type identifiers.
12
+ MESSAGE_CONTENT_TYPE_TEXT = "jg:text"
13
+ MESSAGE_CONTENT_TYPE_IMAGE = "jg:img"
14
+ MESSAGE_CONTENT_TYPE_FILE = "jg:file"
15
+ MESSAGE_CONTENT_TYPE_VIDEO = "jg:video"
16
+ MESSAGE_CONTENT_TYPE_VOICE = "jg:voice"
17
+ MESSAGE_CONTENT_TYPE_STREAM_TEXT = "jg:streamtext"
18
+ MESSAGE_CONTENT_TYPE_RECALL_INFO = "jg:recallinfo"
19
+ MESSAGE_CONTENT_TYPE_MERGE = "jg:merge"
20
+ MESSAGE_CONTENT_TYPE_THUMBNAIL_PACKED_IMAGE = "jg:tpimg"
21
+ MESSAGE_CONTENT_TYPE_SNAPSHOT_PACKED_VIDEO = "jg:spvideo"
22
+
23
+
24
+ def _json_bytes(obj: dict) -> bytes:
25
+ """Serialize to compact JSON bytes (no whitespace between tokens)."""
26
+ return json.dumps(obj, separators=(",", ":"), ensure_ascii=False).encode("utf-8")
27
+
28
+
29
+ class MessageContent:
30
+ """Base for all message content types."""
31
+
32
+ #: content type identifier, e.g. ``jg:text``.
33
+ CONTENT_TYPE = MESSAGE_CONTENT_TYPE_UNKNOWN
34
+
35
+ def get_content_type(self) -> str:
36
+ return self.CONTENT_TYPE or MESSAGE_CONTENT_TYPE_UNKNOWN
37
+
38
+ def encode(self) -> bytes:
39
+ return _json_bytes({})
40
+
41
+ def decode(self, data: bytes) -> None:
42
+ return None
43
+
44
+ def conversation_digest(self) -> str:
45
+ return ""
46
+
47
+ def get_flags(self) -> int:
48
+ return DEFAULT_MESSAGE_FLAGS
49
+
50
+ def get_search_content(self) -> str:
51
+ return ""
52
+
53
+ def __repr__(self) -> str: # pragma: no cover - debug aid
54
+ return f"<{type(self).__name__} type={self.get_content_type()}>"