openim-sdk-core 0.1.3__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.
- openim_sdk/__init__.py +57 -0
- openim_sdk/client.py +381 -0
- openim_sdk/example.py +35 -0
- openim_sdk/example_ws.py +53 -0
- openim_sdk/gob_codec.py +243 -0
- openim_sdk/http_api.py +48 -0
- openim_sdk/models.py +583 -0
- openim_sdk/proto/__init__.py +21 -0
- openim_sdk/proto/conversation/__init__.py +1 -0
- openim_sdk/proto/conversation/conversation_pb2.py +145 -0
- openim_sdk/proto/msg/__init__.py +1 -0
- openim_sdk/proto/msg/msg_pb2.py +235 -0
- openim_sdk/proto/sdkws/__init__.py +1 -0
- openim_sdk/proto/sdkws/sdkws_pb2.py +214 -0
- openim_sdk/proto/wrapperspb/__init__.py +1 -0
- openim_sdk/proto/wrapperspb/wrapperspb_pb2.py +45 -0
- openim_sdk/self_user_info_api.py +48 -0
- openim_sdk/storage.py +178 -0
- openim_sdk/ws_client.py +830 -0
- openim_sdk_core-0.1.3.dist-info/METADATA +297 -0
- openim_sdk_core-0.1.3.dist-info/RECORD +23 -0
- openim_sdk_core-0.1.3.dist-info/WHEEL +5 -0
- openim_sdk_core-0.1.3.dist-info/top_level.txt +1 -0
openim_sdk/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
__version__ = "0.1.3"
|
|
2
|
+
|
|
3
|
+
from .client import OpenIMSDK, SDKConfig, SDKError, SDKHTTPError
|
|
4
|
+
from .models import (
|
|
5
|
+
CONTENT_TYPE_NAMES,
|
|
6
|
+
AdvancedTextElem,
|
|
7
|
+
AtTextElem,
|
|
8
|
+
CardElem,
|
|
9
|
+
ContentType,
|
|
10
|
+
CustomElem,
|
|
11
|
+
FaceElem,
|
|
12
|
+
FileElem,
|
|
13
|
+
LocationElem,
|
|
14
|
+
MarkdownTextElem,
|
|
15
|
+
MergeElem,
|
|
16
|
+
NotificationElem,
|
|
17
|
+
PictureElem,
|
|
18
|
+
QuoteElem,
|
|
19
|
+
SelfUserInfo,
|
|
20
|
+
SoundElem,
|
|
21
|
+
TextElem,
|
|
22
|
+
TypingElem,
|
|
23
|
+
VideoElem,
|
|
24
|
+
WSMessage,
|
|
25
|
+
)
|
|
26
|
+
from .ws_client import OpenIMWSSDK, WSConfig, SDKError as WSSDKError
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"__version__",
|
|
30
|
+
"OpenIMSDK",
|
|
31
|
+
"SDKConfig",
|
|
32
|
+
"SDKError",
|
|
33
|
+
"SDKHTTPError",
|
|
34
|
+
"WSSDKError",
|
|
35
|
+
"WSMessage",
|
|
36
|
+
"SelfUserInfo",
|
|
37
|
+
"ContentType",
|
|
38
|
+
"CONTENT_TYPE_NAMES",
|
|
39
|
+
"TextElem",
|
|
40
|
+
"PictureElem",
|
|
41
|
+
"SoundElem",
|
|
42
|
+
"VideoElem",
|
|
43
|
+
"FileElem",
|
|
44
|
+
"AtTextElem",
|
|
45
|
+
"MergeElem",
|
|
46
|
+
"CardElem",
|
|
47
|
+
"LocationElem",
|
|
48
|
+
"CustomElem",
|
|
49
|
+
"QuoteElem",
|
|
50
|
+
"FaceElem",
|
|
51
|
+
"AdvancedTextElem",
|
|
52
|
+
"TypingElem",
|
|
53
|
+
"MarkdownTextElem",
|
|
54
|
+
"NotificationElem",
|
|
55
|
+
"OpenIMWSSDK",
|
|
56
|
+
"WSConfig",
|
|
57
|
+
]
|
openim_sdk/client.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import random
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
import uuid
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Callable, Dict, Iterable, List, Optional, Sequence
|
|
11
|
+
|
|
12
|
+
from .http_api import post_json_data
|
|
13
|
+
from .models import SelfUserInfo
|
|
14
|
+
from .self_user_info_api import fetch_self_user_info
|
|
15
|
+
from .storage import SQLiteStorage, conversation_id_from_msg
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
CONTENT_TEXT = 101
|
|
19
|
+
MSG_FROM_USER = 100
|
|
20
|
+
SESSION_SINGLE_CHAT = 1
|
|
21
|
+
SESSION_GROUP_CHAT = 2
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SDKError(Exception):
|
|
25
|
+
def __init__(self, err_code: int, err_msg: str, err_dlt: str = "") -> None:
|
|
26
|
+
super().__init__(f"[{err_code}] {err_msg} {err_dlt}".strip())
|
|
27
|
+
self.err_code = int(err_code)
|
|
28
|
+
self.err_msg = err_msg
|
|
29
|
+
self.err_dlt = err_dlt
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SDKHTTPError(Exception):
|
|
33
|
+
def __init__(self, status_code: int, body: str) -> None:
|
|
34
|
+
super().__init__(f"http status={status_code}, body={body}")
|
|
35
|
+
self.status_code = status_code
|
|
36
|
+
self.body = body
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class SDKConfig:
|
|
41
|
+
api_addr: str
|
|
42
|
+
data_dir: str = "./data"
|
|
43
|
+
poll_interval_seconds: float = 2.0
|
|
44
|
+
pull_batch_size: int = 100
|
|
45
|
+
connect_timeout_seconds: float = 10.0
|
|
46
|
+
verify_token_on_login: bool = False
|
|
47
|
+
pull_message_routes: Sequence[str] = field(
|
|
48
|
+
default_factory=lambda: (
|
|
49
|
+
"/msg/pull_message_by_seqs",
|
|
50
|
+
"/msg/pull_msg_by_seqs",
|
|
51
|
+
"/msg/pull_msg_by_range",
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class OpenIMSDK:
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
config: SDKConfig,
|
|
60
|
+
*,
|
|
61
|
+
on_recv_new_message: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
62
|
+
on_recv_offline_new_message: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
63
|
+
on_connecting: Optional[Callable[[], None]] = None,
|
|
64
|
+
on_connect_success: Optional[Callable[[], None]] = None,
|
|
65
|
+
on_connect_failed: Optional[Callable[[Exception], None]] = None,
|
|
66
|
+
on_error: Optional[Callable[[Exception], None]] = None,
|
|
67
|
+
logger: Optional[Callable[[str], None]] = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
self._config = config
|
|
70
|
+
self._api_addr = config.api_addr.rstrip("/")
|
|
71
|
+
self._user_id = ""
|
|
72
|
+
self._token = ""
|
|
73
|
+
self._storage: Optional[SQLiteStorage] = None
|
|
74
|
+
self._poll_thread: Optional[threading.Thread] = None
|
|
75
|
+
self._stop_event = threading.Event()
|
|
76
|
+
self._route_lock = threading.Lock()
|
|
77
|
+
self._selected_pull_route: Optional[str] = None
|
|
78
|
+
self._logger = logger
|
|
79
|
+
|
|
80
|
+
self.on_recv_new_message = on_recv_new_message
|
|
81
|
+
self.on_recv_offline_new_message = on_recv_offline_new_message
|
|
82
|
+
self.on_connecting = on_connecting
|
|
83
|
+
self.on_connect_success = on_connect_success
|
|
84
|
+
self.on_connect_failed = on_connect_failed
|
|
85
|
+
self.on_error = on_error
|
|
86
|
+
|
|
87
|
+
@property
|
|
88
|
+
def user_id(self) -> str:
|
|
89
|
+
return self._user_id
|
|
90
|
+
|
|
91
|
+
def login(self, user_id: str, token: str) -> None:
|
|
92
|
+
if not user_id or not token:
|
|
93
|
+
raise ValueError("user_id and token are required")
|
|
94
|
+
|
|
95
|
+
self._user_id = user_id
|
|
96
|
+
self._token = token
|
|
97
|
+
self._selected_pull_route = None
|
|
98
|
+
|
|
99
|
+
if self._config.verify_token_on_login:
|
|
100
|
+
self._post("/auth/parse_token", {"token": token})
|
|
101
|
+
|
|
102
|
+
db_file = os.path.join(self._config.data_dir, f"OpenIM_py_{user_id}.db")
|
|
103
|
+
self._storage = SQLiteStorage(db_file)
|
|
104
|
+
self._storage.open()
|
|
105
|
+
self._log(f"login success, db={db_file}")
|
|
106
|
+
|
|
107
|
+
def logout(self) -> None:
|
|
108
|
+
self.stop_receiving()
|
|
109
|
+
if self._storage is not None:
|
|
110
|
+
self._storage.close()
|
|
111
|
+
self._storage = None
|
|
112
|
+
self._user_id = ""
|
|
113
|
+
self._token = ""
|
|
114
|
+
|
|
115
|
+
def start_receiving(self) -> None:
|
|
116
|
+
self._assert_logged_in()
|
|
117
|
+
if self._poll_thread is not None and self._poll_thread.is_alive():
|
|
118
|
+
return
|
|
119
|
+
self._stop_event.clear()
|
|
120
|
+
if self.on_connecting:
|
|
121
|
+
self.on_connecting()
|
|
122
|
+
self._poll_thread = threading.Thread(target=self._poll_loop, name="openim-poller", daemon=True)
|
|
123
|
+
self._poll_thread.start()
|
|
124
|
+
|
|
125
|
+
def stop_receiving(self) -> None:
|
|
126
|
+
self._stop_event.set()
|
|
127
|
+
if self._poll_thread is not None and self._poll_thread.is_alive():
|
|
128
|
+
self._poll_thread.join(timeout=2.0)
|
|
129
|
+
self._poll_thread = None
|
|
130
|
+
|
|
131
|
+
def sync_once(self) -> None:
|
|
132
|
+
self._assert_logged_in()
|
|
133
|
+
self._sync_messages_once()
|
|
134
|
+
|
|
135
|
+
def get_self_user_info(self) -> SelfUserInfo:
|
|
136
|
+
self._assert_logged_in()
|
|
137
|
+
return fetch_self_user_info(
|
|
138
|
+
api_addr=self._api_addr,
|
|
139
|
+
user_id=self._user_id,
|
|
140
|
+
token=self._token,
|
|
141
|
+
operation_id=self._operation_id(),
|
|
142
|
+
timeout_seconds=self._config.connect_timeout_seconds,
|
|
143
|
+
make_http_error=lambda status, body: SDKHTTPError(status, body),
|
|
144
|
+
make_sdk_error=lambda code, msg, dlt: SDKError(code, msg, dlt),
|
|
145
|
+
make_not_found_error=lambda uid: SDKError(-1, "self user info not found", f"userID={uid}"),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def send_text(
|
|
149
|
+
self,
|
|
150
|
+
text: str,
|
|
151
|
+
*,
|
|
152
|
+
recv_id: str = "",
|
|
153
|
+
group_id: str = "",
|
|
154
|
+
sender_platform_id: int = 0,
|
|
155
|
+
sender_nickname: str = "",
|
|
156
|
+
sender_face_url: str = "",
|
|
157
|
+
session_type: Optional[int] = None,
|
|
158
|
+
options: Optional[Dict[str, bool]] = None,
|
|
159
|
+
offline_push_info: Optional[Dict[str, Any]] = None,
|
|
160
|
+
) -> Dict[str, Any]:
|
|
161
|
+
self._assert_logged_in()
|
|
162
|
+
if not text:
|
|
163
|
+
raise ValueError("text is required")
|
|
164
|
+
if not recv_id and not group_id:
|
|
165
|
+
raise ValueError("either recv_id or group_id is required")
|
|
166
|
+
|
|
167
|
+
now = int(time.time() * 1000)
|
|
168
|
+
client_msg_id = self._generate_client_msg_id(self._user_id)
|
|
169
|
+
if session_type is None:
|
|
170
|
+
session_type = SESSION_SINGLE_CHAT if recv_id else SESSION_GROUP_CHAT
|
|
171
|
+
|
|
172
|
+
content = json.dumps({"content": text}, ensure_ascii=False, separators=(",", ":")).encode("utf-8")
|
|
173
|
+
msg_data: Dict[str, Any] = {
|
|
174
|
+
"sendID": self._user_id,
|
|
175
|
+
"recvID": recv_id,
|
|
176
|
+
"groupID": group_id,
|
|
177
|
+
"clientMsgID": client_msg_id,
|
|
178
|
+
"senderPlatformID": int(sender_platform_id),
|
|
179
|
+
"senderNickname": sender_nickname,
|
|
180
|
+
"senderFaceURL": sender_face_url,
|
|
181
|
+
"sessionType": int(session_type),
|
|
182
|
+
"msgFrom": MSG_FROM_USER,
|
|
183
|
+
"contentType": CONTENT_TEXT,
|
|
184
|
+
"content": base64.b64encode(content).decode("ascii"),
|
|
185
|
+
"seq": 0,
|
|
186
|
+
"sendTime": now,
|
|
187
|
+
"createTime": now,
|
|
188
|
+
"status": 1,
|
|
189
|
+
"isRead": False,
|
|
190
|
+
"options": options or self._default_send_options(),
|
|
191
|
+
"offlinePushInfo": offline_push_info,
|
|
192
|
+
"attachedInfo": "",
|
|
193
|
+
"ex": "",
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
resp = self._post("/msg/send_msg", {"msgData": self._drop_none(msg_data)})
|
|
197
|
+
msg_data["serverMsgID"] = resp.get("serverMsgID", "")
|
|
198
|
+
msg_data["sendTime"] = int(resp.get("sendTime", now) or now)
|
|
199
|
+
msg_data["status"] = 2
|
|
200
|
+
|
|
201
|
+
if self._storage is not None:
|
|
202
|
+
cid = conversation_id_from_msg(msg_data, self._user_id)
|
|
203
|
+
self._storage.upsert_message(cid, msg_data, content.decode("utf-8"))
|
|
204
|
+
return resp
|
|
205
|
+
|
|
206
|
+
def _poll_loop(self) -> None:
|
|
207
|
+
if self.on_connect_success:
|
|
208
|
+
self.on_connect_success()
|
|
209
|
+
|
|
210
|
+
while not self._stop_event.is_set():
|
|
211
|
+
try:
|
|
212
|
+
self._sync_messages_once()
|
|
213
|
+
except Exception as exc: # pragma: no cover
|
|
214
|
+
if self.on_error:
|
|
215
|
+
self.on_error(exc)
|
|
216
|
+
self._log(f"sync error: {exc}")
|
|
217
|
+
self._stop_event.wait(self._config.poll_interval_seconds)
|
|
218
|
+
|
|
219
|
+
def _sync_messages_once(self) -> None:
|
|
220
|
+
assert self._storage is not None
|
|
221
|
+
|
|
222
|
+
conversations_resp = self._post("/conversation/get_all_conversations", {"ownerUserID": self._user_id})
|
|
223
|
+
conversations = conversations_resp.get("conversations", []) if isinstance(conversations_resp, dict) else []
|
|
224
|
+
conversation_ids = [str(v.get("conversationID", "")) for v in conversations if v.get("conversationID")]
|
|
225
|
+
if not conversation_ids:
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
seq_resp = self._post(
|
|
229
|
+
"/msg/get_conversations_has_read_and_max_seq",
|
|
230
|
+
{
|
|
231
|
+
"userID": self._user_id,
|
|
232
|
+
"conversationIDs": conversation_ids,
|
|
233
|
+
"returnPinned": False,
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
seq_map = seq_resp.get("seqs", {}) if isinstance(seq_resp, dict) else {}
|
|
237
|
+
|
|
238
|
+
for cid in conversation_ids:
|
|
239
|
+
seqs_info = seq_map.get(cid, {}) if isinstance(seq_map, dict) else {}
|
|
240
|
+
max_seq = int(seqs_info.get("maxSeq", 0) or 0)
|
|
241
|
+
if max_seq <= 0:
|
|
242
|
+
continue
|
|
243
|
+
|
|
244
|
+
local_seq = self._storage.get_last_seq(cid)
|
|
245
|
+
if local_seq >= max_seq:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
begin = local_seq + 1
|
|
249
|
+
while begin <= max_seq:
|
|
250
|
+
end = min(max_seq, begin + self._config.pull_batch_size - 1)
|
|
251
|
+
pull_resp = self._pull_message_by_seq_ranges(
|
|
252
|
+
[
|
|
253
|
+
{
|
|
254
|
+
"conversationID": cid,
|
|
255
|
+
"begin": begin,
|
|
256
|
+
"end": end,
|
|
257
|
+
"num": self._config.pull_batch_size,
|
|
258
|
+
}
|
|
259
|
+
]
|
|
260
|
+
)
|
|
261
|
+
msgs = self._extract_pull_messages(pull_resp, cid)
|
|
262
|
+
newest_seq = local_seq
|
|
263
|
+
for msg in msgs:
|
|
264
|
+
inserted = self._persist_incoming_message(cid, msg)
|
|
265
|
+
seq = int(msg.get("seq", 0) or 0)
|
|
266
|
+
if seq > newest_seq:
|
|
267
|
+
newest_seq = seq
|
|
268
|
+
if inserted:
|
|
269
|
+
self._dispatch_incoming_message(msg)
|
|
270
|
+
|
|
271
|
+
if newest_seq > local_seq:
|
|
272
|
+
self._storage.set_last_seq(cid, newest_seq)
|
|
273
|
+
local_seq = newest_seq
|
|
274
|
+
begin = end + 1
|
|
275
|
+
|
|
276
|
+
def _pull_message_by_seq_ranges(self, seq_ranges: List[Dict[str, Any]]) -> Dict[str, Any]:
|
|
277
|
+
with self._route_lock:
|
|
278
|
+
selected = self._selected_pull_route
|
|
279
|
+
|
|
280
|
+
if selected is not None:
|
|
281
|
+
return self._post(selected, {"userID": self._user_id, "seqRanges": seq_ranges, "order": 0})
|
|
282
|
+
|
|
283
|
+
last_exc: Optional[Exception] = None
|
|
284
|
+
for route in self._config.pull_message_routes:
|
|
285
|
+
try:
|
|
286
|
+
resp = self._post(route, {"userID": self._user_id, "seqRanges": seq_ranges, "order": 0})
|
|
287
|
+
with self._route_lock:
|
|
288
|
+
self._selected_pull_route = route
|
|
289
|
+
self._log(f"selected pull route: {route}")
|
|
290
|
+
return resp
|
|
291
|
+
except SDKHTTPError as exc:
|
|
292
|
+
last_exc = exc
|
|
293
|
+
if exc.status_code != 404:
|
|
294
|
+
break
|
|
295
|
+
except SDKError as exc:
|
|
296
|
+
last_exc = exc
|
|
297
|
+
if last_exc is None:
|
|
298
|
+
last_exc = RuntimeError("no pull route available")
|
|
299
|
+
raise last_exc
|
|
300
|
+
|
|
301
|
+
def _extract_pull_messages(self, pull_resp: Dict[str, Any], conversation_id: str) -> List[Dict[str, Any]]:
|
|
302
|
+
msgs_map = pull_resp.get("msgs", {}) if isinstance(pull_resp, dict) else {}
|
|
303
|
+
one = msgs_map.get(conversation_id, {}) if isinstance(msgs_map, dict) else {}
|
|
304
|
+
msgs = one.get("Msgs")
|
|
305
|
+
if msgs is None:
|
|
306
|
+
msgs = one.get("msgs", [])
|
|
307
|
+
return msgs if isinstance(msgs, list) else []
|
|
308
|
+
|
|
309
|
+
def _persist_incoming_message(self, conversation_id: str, msg: Dict[str, Any]) -> bool:
|
|
310
|
+
assert self._storage is not None
|
|
311
|
+
content_json = self._decode_content_to_json(msg.get("content"))
|
|
312
|
+
return self._storage.upsert_message(conversation_id, msg, content_json)
|
|
313
|
+
|
|
314
|
+
def _dispatch_incoming_message(self, msg: Dict[str, Any]) -> None:
|
|
315
|
+
# Polling mode receives synced messages, so they are treated as offline/new pull messages.
|
|
316
|
+
if self.on_recv_offline_new_message:
|
|
317
|
+
self.on_recv_offline_new_message(msg)
|
|
318
|
+
return
|
|
319
|
+
if self.on_recv_new_message:
|
|
320
|
+
self.on_recv_new_message(msg)
|
|
321
|
+
|
|
322
|
+
def _post(self, route: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
|
323
|
+
self._assert_logged_in()
|
|
324
|
+
return post_json_data(
|
|
325
|
+
api_addr=self._api_addr,
|
|
326
|
+
route=route,
|
|
327
|
+
payload=payload,
|
|
328
|
+
token=self._token,
|
|
329
|
+
operation_id=self._operation_id(),
|
|
330
|
+
timeout_seconds=self._config.connect_timeout_seconds,
|
|
331
|
+
make_http_error=lambda status, body: SDKHTTPError(status, body),
|
|
332
|
+
make_sdk_error=lambda code, msg, dlt: SDKError(code, msg, dlt),
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
def _build_url(self, route: str) -> str:
|
|
336
|
+
if not route.startswith("/"):
|
|
337
|
+
route = "/" + route
|
|
338
|
+
return self._api_addr + route
|
|
339
|
+
|
|
340
|
+
def _assert_logged_in(self) -> None:
|
|
341
|
+
if not self._user_id or not self._token:
|
|
342
|
+
raise RuntimeError("not logged in")
|
|
343
|
+
|
|
344
|
+
def _operation_id(self) -> str:
|
|
345
|
+
return f"{int(time.time() * 1e9)}{random.randint(1000, 9999)}"
|
|
346
|
+
|
|
347
|
+
@staticmethod
|
|
348
|
+
def _generate_client_msg_id(send_id: str) -> str:
|
|
349
|
+
raw = f"{time.time_ns()}{send_id}{uuid.uuid4().hex}".encode("utf-8")
|
|
350
|
+
return hashlib.md5(raw).hexdigest()
|
|
351
|
+
|
|
352
|
+
@staticmethod
|
|
353
|
+
def _default_send_options() -> Dict[str, bool]:
|
|
354
|
+
return {
|
|
355
|
+
"history": True,
|
|
356
|
+
"persistent": True,
|
|
357
|
+
"senderSync": True,
|
|
358
|
+
"conversationUpdate": True,
|
|
359
|
+
"senderConversationUpdate": True,
|
|
360
|
+
"unreadCount": True,
|
|
361
|
+
"offlinePush": True,
|
|
362
|
+
"notPrivate": True,
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
@staticmethod
|
|
366
|
+
def _drop_none(values: Dict[str, Any]) -> Dict[str, Any]:
|
|
367
|
+
return {k: v for k, v in values.items() if v is not None}
|
|
368
|
+
|
|
369
|
+
@staticmethod
|
|
370
|
+
def _decode_content_to_json(content_value: Any) -> str:
|
|
371
|
+
if isinstance(content_value, str):
|
|
372
|
+
try:
|
|
373
|
+
b = base64.b64decode(content_value, validate=True)
|
|
374
|
+
return b.decode("utf-8", errors="replace")
|
|
375
|
+
except Exception:
|
|
376
|
+
return content_value
|
|
377
|
+
return ""
|
|
378
|
+
|
|
379
|
+
def _log(self, msg: str) -> None:
|
|
380
|
+
if self._logger:
|
|
381
|
+
self._logger(msg)
|
openim_sdk/example.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from openim_sdk import OpenIMSDK, SDKConfig
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def on_new_message(msg):
|
|
5
|
+
print("recv:", msg.get("clientMsgID"), msg.get("sendID"), msg.get("contentType"))
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main():
|
|
9
|
+
sdk = OpenIMSDK(
|
|
10
|
+
SDKConfig(
|
|
11
|
+
api_addr="https://im-api-test.kiwilightyear.com",
|
|
12
|
+
data_dir="./openim_py_data",
|
|
13
|
+
poll_interval_seconds=2.0,
|
|
14
|
+
),
|
|
15
|
+
on_recv_new_message=on_new_message,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
sdk.login(user_id="10628900", token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOiIxMDYyODkwMCIsIlBsYXRmb3JtSUQiOjEsImV4cCI6MTc4MDQ1MDAxMiwibmJmIjoxNzcyNjczNzEyLCJpYXQiOjE3NzI2NzQwMTJ9.p9_k73F7B2Hz2Iv_MhwLAmrlar641ryg2l2_JbVscZw")
|
|
19
|
+
sdk.start_receiving()
|
|
20
|
+
|
|
21
|
+
# Single chat text
|
|
22
|
+
resp = sdk.send_text("hello from python sdk", recv_id="10630400")
|
|
23
|
+
print("send resp:", resp)
|
|
24
|
+
|
|
25
|
+
# Keep process alive for demo.
|
|
26
|
+
try:
|
|
27
|
+
import time
|
|
28
|
+
|
|
29
|
+
time.sleep(20)
|
|
30
|
+
finally:
|
|
31
|
+
sdk.logout()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
if __name__ == "__main__":
|
|
35
|
+
main()
|
openim_sdk/example_ws.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from time import sleep
|
|
2
|
+
|
|
3
|
+
from openim_sdk import OpenIMWSSDK, PictureElem, SoundElem, TextElem, WSConfig, WSMessage
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def on_new_message(msg: WSMessage):
|
|
7
|
+
if isinstance(msg.content_obj, TextElem):
|
|
8
|
+
print("recv text:", msg.client_msg_id, msg.send_id, msg.content_obj.content)
|
|
9
|
+
return
|
|
10
|
+
if isinstance(msg.content_obj, PictureElem):
|
|
11
|
+
print("recv picture:", msg.client_msg_id, msg.send_id, msg.content_obj.source_picture)
|
|
12
|
+
return
|
|
13
|
+
if isinstance(msg.content_obj, SoundElem):
|
|
14
|
+
print("recv sound:", msg.client_msg_id, msg.send_id, msg.content_obj.duration)
|
|
15
|
+
return
|
|
16
|
+
print("recv other:", msg.client_msg_id, msg.send_id, msg.content_type, msg.content_obj)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def on_error(err):
|
|
20
|
+
print("error:", err)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
sdk = OpenIMWSSDK(
|
|
25
|
+
WSConfig(
|
|
26
|
+
ws_addr="wss://im-gateway-test.kiwilightyear.com",
|
|
27
|
+
api_addr="https://im-api-test.kiwilightyear.com",
|
|
28
|
+
data_dir="./openim_py_data",
|
|
29
|
+
),
|
|
30
|
+
on_recv_new_message=on_new_message,
|
|
31
|
+
on_error=on_error,
|
|
32
|
+
)
|
|
33
|
+
sdk.login(user_id="10628900", token="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJVc2VySUQiOiIxMDYyODkwMCIsIlBsYXRmb3JtSUQiOjEsImV4cCI6MTc4MDQ1MDAxMiwibmJmIjoxNzcyNjczNzEyLCJpYXQiOjE3NzI2NzQwMTJ9.p9_k73F7B2Hz2Iv_MhwLAmrlar641ryg2l2_JbVscZw")
|
|
34
|
+
sdk.start()
|
|
35
|
+
|
|
36
|
+
resp = sdk.send_text(
|
|
37
|
+
"hello from python ws sdk",
|
|
38
|
+
recv_id="10630400",
|
|
39
|
+
ex='{"biz":"order","id":"123"}',
|
|
40
|
+
)
|
|
41
|
+
print("send resp:", resp)
|
|
42
|
+
resp = sdk.send_text("hello from python ws sdk 2", recv_id="10630400")
|
|
43
|
+
print("send resp:", resp)
|
|
44
|
+
try:
|
|
45
|
+
import time
|
|
46
|
+
|
|
47
|
+
time.sleep(2000)
|
|
48
|
+
finally:
|
|
49
|
+
sdk.logout()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
main()
|