Metadata-Version: 2.4
Name: openim-sdk-core
Version: 0.1.5
Summary: Python SDK and CLI for OpenIM-style login, messaging, and websocket sync.
Author: OpenIM Contributors
License-Expression: AGPL-3.0-or-later
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Operating System :: OS Independent
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: protobuf<7,>=6.32.0
Requires-Dist: websocket-client<2,>=1.8.0
Provides-Extra: release
Requires-Dist: build>=1.2.2; extra == "release"
Requires-Dist: twine>=6.0.1; extra == "release"

# openim_sdk

Python SDK and CLI for OpenIM-style login, send message, and receive message.

## Modes

- `OpenIMWSSDK` (recommended): websocket direct connection (`gob + gzip + protobuf`) with heartbeat and auto reconnect.
- `OpenIMSDK`: HTTP polling fallback.
- `openim-cli`: account bootstrap, auth status, and outbound notification commands built on top of `OpenIMWSSDK`.

## CLI quick start

Install this repository, then run:

```bash
openim-cli --help
openim-cli config init --token "$OPENIM_TOKEN" --ws-addr ws://127.0.0.1:10001/msg_gateway --default true
openim-cli auth status --format pretty
openim-cli notify +send --to user:u2 --text "hello"
```

Supported commands:

- `config init|list|use|remove`
- `auth status`
- `notify +send`

## WebSocket quick start

```python
from openim_sdk import OpenIMWSSDK, WSConfig

sdk = OpenIMWSSDK(
    WSConfig(ws_addr="ws://127.0.0.1:10001/msg_gateway", data_dir="./openim_py_data")
)
sdk.login(user_id="u1", token="YOUR_TOKEN")
sdk.start()
sdk.send_text("hello", recv_id="u2")
```

See `openim_sdk/example_ws.py` for a runnable demo script.

## WebSocket detailed usage

### 1. Build config

`WSConfig` parameters:

- `ws_addr` (required): websocket gateway address, e.g. `ws://127.0.0.1:10001/msg_gateway`
- `api_addr`: HTTP API address for profile fetch (used to cache self info for send), e.g. `http://127.0.0.1:10002`; when empty, sdk falls back to env `OPENIM_API_ADDR`
- `data_dir`: local sqlite directory, default `./data`
- `platform_id`: platform ID sent to server, default `1`
- `timeout_seconds`: request/response wait timeout, default `1000`
- `pull_batch_size`: sync page size, default `100`
- `sdk_version`: value sent in ws query, default `python-ws-0.1.0`
- `auto_sync_on_connect`: run `sync_once()` immediately after first connect, default `False`
- `auto_sync_on_reconnect`: auto sync after reconnect, default `True`
- `auto_reconnect`: reconnect automatically on disconnect, default `True`
- `max_reconnect_attempts`: reconnect attempt cap, default `300`
- `reconnect_backoff_seconds`: reconnect interval sequence, default `(1, 2, 4, 8, 16)`
- `enable_heartbeat`: send ws ping periodically, default `True`
- `heartbeat_interval_seconds`: ping interval, default `24`
- `heartbeat_pong_timeout_seconds`: pong timeout, default `30`
- `self_user_info_cache_ttl_seconds`: in-memory cache ttl for self profile (`senderNickname/senderFaceURL`), default `600` seconds

### 2. Register callbacks

Available callbacks in `OpenIMWSSDK(...)`:

- `on_recv_new_message(msg: WSMessage)`: called for online real-time messages
- `on_recv_offline_new_message(msg: WSMessage)`: called for offline/sync messages
- `on_connecting()`: called before connect/reconnect attempts
- `on_connect_success()`: called after successful connect/reconnect
- `on_connect_failed(exc)`: called when one connect attempt fails
- `on_kicked_offline()`: called when server returns kick event
- `on_error(exc)`: called on internal errors (decode/send/read/reconnect, etc.)
- `logger(text)`: sdk log output callback

### 2.1 `WSMessage` fields

Raw payload keys:

- `conversationID`: conversation id (sdk adds this key)
- `sendID`, `recvID`, `groupID`
- `clientMsgID`, `serverMsgID`
- `senderPlatformID`, `senderNickname`, `senderFaceURL`
- `sessionType` (`1` single, `2` group, `3` super group)
- `msgFrom`, `contentType`, `content`
- `seq`, `sendTime`, `createTime`
- `status`, `isRead`
- `options` (`dict[str, bool]`)
- `offlinePushInfo` (`dict`)
- `atUserIDList` (`list[str]`)
- `attachedInfo`, `ex`

Python field names in `WSMessage`:

- `conversation_id`, `send_id`, `recv_id`, `group_id`
- `client_msg_id`, `server_msg_id`
- `sender_platform_id`, `sender_nickname`, `sender_face_url`
- `session_type`, `msg_from`, `content_type`, `content`
- `seq`, `send_time`, `create_time`, `status`, `is_read`
- `options`, `offline_push_info`, `at_user_id_list`, `attached_info`, `ex`
- `raw`: original dictionary payload
- `content_json`: parsed JSON from `content` (or `None` when not valid JSON)
- `content_obj`: typed content object parsed by `content_type`

`content_type -> content_obj` mapping (same as `sdk_struct.go`):

- `101` -> `TextElem`
- `102` -> `PictureElem`
- `103` -> `SoundElem`
- `104` -> `VideoElem`
- `105` -> `FileElem`
- `106` -> `AtTextElem`
- `107` -> `MergeElem`
- `108` -> `CardElem`
- `109` -> `LocationElem`
- `110/119/120` -> `CustomElem`
- `113` -> `TypingElem`
- `114` -> `QuoteElem`
- `115` -> `FaceElem`
- `117` -> `AdvancedTextElem`
- `118` -> `MarkdownTextElem`
- other content types -> `NotificationElem`

Minimal callback example:

```python
from openim_sdk import OpenIMWSSDK, WSConfig, WSMessage, TextElem, PictureElem


def on_new_model(msg: WSMessage) -> None:
    if isinstance(msg.content_obj, TextElem):
        print("[text]", msg.content_obj.content)
    elif isinstance(msg.content_obj, PictureElem):
        print("[picture]", msg.content_obj.source_picture)
    else:
        print("[other]", msg.content_type, msg.content_obj)


sdk = OpenIMWSSDK(
    WSConfig(ws_addr="ws://127.0.0.1:10001/msg_gateway"),
    on_recv_new_message=on_new_model,
)
```

### 3. Full example (recommended pattern)

```python
import time

from openim_sdk import OpenIMWSSDK, TextElem, WSConfig, WSSDKError, WSMessage


def on_new(msg: WSMessage) -> None:
    if isinstance(msg.content_obj, TextElem):
        print("[new text]", msg.send_id, "->", msg.recv_id, msg.content_obj.content)
    else:
        print("[new]", msg.send_id, "->", msg.recv_id, msg.content_type, msg.content_obj)


def on_offline(msg: WSMessage) -> None:
    print("[offline]", msg.client_msg_id, msg.seq)


def on_err(exc: Exception) -> None:
    print("[error]", exc)


sdk = OpenIMWSSDK(
    WSConfig(
        ws_addr="ws://127.0.0.1:10001/msg_gateway",
        api_addr="http://127.0.0.1:10002",
        data_dir="./openim_py_data",
        auto_sync_on_connect=True,
        auto_sync_on_reconnect=True,
    ),
    on_recv_new_message=on_new,
    on_recv_offline_new_message=on_offline,
    on_connecting=lambda: print("[ws] connecting"),
    on_connect_success=lambda: print("[ws] connected"),
    on_connect_failed=lambda e: print("[ws] connect failed:", e),
    on_kicked_offline=lambda: print("[ws] kicked offline"),
    on_error=on_err,
    logger=lambda s: print("[sdk]", s),
)

try:
    # 1) login first
    sdk.login(user_id="u1", token="YOUR_TOKEN")
    # 2) then start websocket threads
    sdk.start()

    # send peer message
    ack = sdk.send_text("hello from ws sdk", recv_id="u2")
    print("send ack:", ack)  # {"serverMsgID": "...", "clientMsgID": "...", "sendTime": ...}

    # keep process alive
    while True:
        time.sleep(1)
except WSSDKError as e:
    print("business error:", e.err_code, e.err_msg)
except KeyboardInterrupt:
    pass
finally:
    # stop background threads and close db
    sdk.logout()
```

### 4. Sending messages

- Single chat:

```python
sdk.send_text("hello", recv_id="u2")
```

- Group chat:

```python
sdk.send_text("hello group", group_id="g123", session_type=2)
```

`send_text(...)` notes:

- must pass one of `recv_id` or `group_id`
- `session_type` defaults to single chat (`1`) if `recv_id` is set, otherwise group (`2`)
- if `WSConfig.api_addr` is set, sdk fetches self profile (`/user/get_users_info`) and caches it in memory (ttl controlled by `self_user_info_cache_ttl_seconds`, default 10 minutes), then auto-fills `senderNickname` and `senderFaceURL` on send
- supported session values:
  - `1`: single chat
  - `2`: group chat
  - `3`: super group chat

Media/file messages:

```python
sdk.send_image("/absolute/path/demo.png", recv_id="u2")
sdk.send_sound("/absolute/path/demo.mp3", duration=9, recv_id="u2")
sdk.send_video(
    "/absolute/path/demo.mp4",
    duration=15,
    snapshot_path="/absolute/path/demo-cover.jpg",
    recv_id="u2",
)
sdk.send_file("/absolute/path/report.pdf", recv_id="u2")
```

URL-based variants:

```python
sdk.send_image_by_url(source_url="https://cdn.example.com/demo.png", width=1280, height=720, recv_id="u2")
sdk.send_video_by_url(video_url="https://cdn.example.com/demo.mp4", duration=15, recv_id="u2")
sdk.send_file_by_url(source_url="https://cdn.example.com/report.pdf", file_name="report.pdf", recv_id="u2")
```

Media upload notes:

- local media/file sending uses OpenIM object upload APIs before message send
- `SDKConfig.api_addr` / `WSConfig.api_addr` must be configured for local media/file sending
- `send_video(...)` requires `duration`; `snapshot_path` is optional but recommended
- `send_sound(...)` requires `duration`
- `send_picture` is an alias of `send_image`

### 5. Manual sync

If you want explicit control (instead of `auto_sync_on_connect/reconnect`), call:

```python
sdk.sync_once()
```

This pulls missing seq ranges and dispatches them through `on_recv_offline_new_message`.

### 6. Lifecycle and best practices

- call order should be: `login(...) -> start() -> send/recv -> logout()`
- `stop()` only stops ws threads; `logout()` stops threads and closes local storage
- keep your process alive after `start()`, otherwise daemon threads exit with process
- for production:
  - keep `auto_reconnect=True`
  - keep heartbeat enabled
  - handle `on_error` and `on_connect_failed` for observability/retry alerts
  - persist your own app-level offsets/state if required

## HTTP polling quick start

```python
from openim_sdk import OpenIMSDK, SDKConfig

sdk = OpenIMSDK(
    SDKConfig(api_addr="http://127.0.0.1:10002", data_dir="./openim_py_data")
)
sdk.login(user_id="u1", token="YOUR_TOKEN")
sdk.start_receiving()
sdk.send_text("hello", recv_id="u2")
me = sdk.get_self_user_info()
print(me.user_id, me.nickname, me.face_url)
```

## WebSocket features

- Login with `user_id + token`
- Send text message with protobuf payload (`MsgData`)
- Receive `PushMessages` from ws binary frames
- Gob encode/decode for `GeneralWsReq/GeneralWsResp`
- Gzip frame compression/decompression
- Heartbeat (`ping/pong`)
- Auto reconnect with backoff (`1,2,4,8,16` seconds loop by default)
- Auto sync missed messages on connect/reconnect
- Local SQLite persistence:
  - message rows
  - per-conversation latest synced seq

## Local database

- WS mode: `<data_dir>/OpenIM_pyws_<user_id>.db`
- HTTP mode: `<data_dir>/OpenIM_py_<user_id>.db`

Tables:

- `conversation_seq(conversation_id, last_seq)`
- `messages(...)`
