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/__init__.py +57 -0
- imbot_sdk/client.py +1155 -0
- imbot_sdk/consts.py +36 -0
- imbot_sdk/data_accessor.py +35 -0
- imbot_sdk/listeners.py +67 -0
- imbot_sdk/messages/__init__.py +93 -0
- imbot_sdk/messages/base.py +54 -0
- imbot_sdk/messages/contents.py +463 -0
- imbot_sdk/models.py +99 -0
- imbot_sdk/pb/__init__.py +0 -0
- imbot_sdk/pb/appmessages_pb2.py +433 -0
- imbot_sdk/pb/chatroom_pb2.py +98 -0
- imbot_sdk/pb/connect_pb2.py +55 -0
- imbot_sdk/pb/rtcroom_pb2.py +82 -0
- imbot_sdk/py.typed +0 -0
- imbot_sdk_python-0.1.0.dist-info/METADATA +296 -0
- imbot_sdk_python-0.1.0.dist-info/RECORD +20 -0
- imbot_sdk_python-0.1.0.dist-info/WHEEL +5 -0
- imbot_sdk_python-0.1.0.dist-info/licenses/LICENSE +201 -0
- imbot_sdk_python-0.1.0.dist-info/top_level.txt +1 -0
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()}>"
|