magic-campus-sdk 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.
- magic_campus_sdk-0.1.0.dist-info/METADATA +165 -0
- magic_campus_sdk-0.1.0.dist-info/RECORD +50 -0
- magic_campus_sdk-0.1.0.dist-info/WHEEL +5 -0
- magic_campus_sdk-0.1.0.dist-info/entry_points.txt +2 -0
- magic_campus_sdk-0.1.0.dist-info/top_level.txt +2 -0
- magiccampus_cli/__init__.py +1 -0
- magiccampus_cli/__main__.py +9 -0
- magiccampus_cli/accounts.py +126 -0
- magiccampus_cli/cli.py +118 -0
- magiccampus_cli/client.py +99 -0
- magiccampus_cli/commands/auth.py +26 -0
- magiccampus_cli/commands/config.py +95 -0
- magiccampus_cli/commands/feed.py +23 -0
- magiccampus_cli/commands/im.py +20 -0
- magiccampus_cli/commands/notify.py +92 -0
- magiccampus_cli/commands/user.py +66 -0
- magiccampus_cli/config_store.py +57 -0
- magiccampus_cli/http_send.py +565 -0
- magiccampus_cli/notify.py +357 -0
- magiccampus_cli/output.py +101 -0
- magiccampus_cli/prompts.py +58 -0
- magiccampus_cli/targets.py +26 -0
- magiccampus_cli/utils.py +67 -0
- magiccampus_sdk/__init__.py +118 -0
- magiccampus_sdk/auth.py +111 -0
- magiccampus_sdk/client.py +89 -0
- magiccampus_sdk/example.py +46 -0
- magiccampus_sdk/example_ws.py +59 -0
- magiccampus_sdk/feed_client.py +21 -0
- magiccampus_sdk/im/__init__.py +85 -0
- magiccampus_sdk/im/client.py +913 -0
- magiccampus_sdk/im/gob_codec.py +289 -0
- magiccampus_sdk/im/http_api.py +58 -0
- magiccampus_sdk/im/media.py +761 -0
- magiccampus_sdk/im/models.py +583 -0
- magiccampus_sdk/im/proto/__init__.py +21 -0
- magiccampus_sdk/im/proto/sdkws/__init__.py +1 -0
- magiccampus_sdk/im/proto/sdkws/sdkws_pb2.py +215 -0
- magiccampus_sdk/im/proto/wrapperspb/__init__.py +1 -0
- magiccampus_sdk/im/proto/wrapperspb/wrapperspb_pb2.py +45 -0
- magiccampus_sdk/im/self_user_info_api.py +48 -0
- magiccampus_sdk/im/storage.py +213 -0
- magiccampus_sdk/im/text.py +49 -0
- magiccampus_sdk/im/ws_client.py +1235 -0
- magiccampus_sdk/models.py +164 -0
- magiccampus_sdk/platform_client.py +119 -0
- magiccampus_sdk/platform_http.py +71 -0
- magiccampus_sdk/platform_ws.py +122 -0
- magiccampus_sdk/user_client.py +36 -0
- magiccampus_sdk/user_info_api.py +55 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: magic-campus-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MagicCampus platform SDK and CLI with bundled IM transport support.
|
|
5
|
+
Author: MagicCampus Contributors
|
|
6
|
+
License-Expression: AGPL-3.0-or-later
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: httpx<1,>=0.27
|
|
10
|
+
Requires-Dist: protobuf<7,>=6.32.0
|
|
11
|
+
Requires-Dist: websocket-client<2,>=1.8.0
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
14
|
+
|
|
15
|
+
# magic-campus-sdk
|
|
16
|
+
|
|
17
|
+
Python SDK and CLI for MagicCampus platform integrations. IM remains a supported domain, and this repository keeps the bundled OpenIM transport inside `magiccampus_sdk`.
|
|
18
|
+
|
|
19
|
+
This project removes direct user-facing dependency on OpenIM token and endpoint configuration. You only configure:
|
|
20
|
+
|
|
21
|
+
- `apiurl`
|
|
22
|
+
- `apikey`
|
|
23
|
+
- `platform`
|
|
24
|
+
|
|
25
|
+
At runtime, IM-backed flows call:
|
|
26
|
+
|
|
27
|
+
`GET /api/v1/accounts/imToken?platform=<PLATFORM>`
|
|
28
|
+
|
|
29
|
+
and uses the response fields:
|
|
30
|
+
|
|
31
|
+
- `userId`
|
|
32
|
+
- `imToken`
|
|
33
|
+
- `wsApi`
|
|
34
|
+
- `httpApi`
|
|
35
|
+
|
|
36
|
+
## Install locally
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv sync
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This repository vendors only the OpenIM transport pieces that `magiccampus_sdk` actually uses, and layers platform-facing SDK / CLI commands on top.
|
|
43
|
+
|
|
44
|
+
IM-specific low-level internals, models, and protobufs now live under `magiccampus_sdk.im`.
|
|
45
|
+
|
|
46
|
+
## CLI quick start
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
magic-campus --help
|
|
50
|
+
magic-campus config init --apiurl https://api-test.pearlapi.com --apikey "$MAGICCAMPUS_APIKEY" --platform IOS --default true
|
|
51
|
+
magic-campus auth status --format pretty
|
|
52
|
+
magic-campus im notify send --to user:10134002 --text "hello"
|
|
53
|
+
magic-campus user get --id 10134002 --format pretty
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`magic-campus config init` also supports an interactive TTY flow when you omit auth flags.
|
|
57
|
+
|
|
58
|
+
`im notify send` sends over MagicCampus HTTP `POST /agent/v1/im/sendMessage`. `im notify history` still relies on the websocket transport and local sync database.
|
|
59
|
+
|
|
60
|
+
Legacy compatibility commands remain available:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
magic-campus notify +send --to user:10134002 --text "hello"
|
|
64
|
+
magic-campus notify +history --to user:10134002
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
CLI config is stored at:
|
|
68
|
+
|
|
69
|
+
`~/.mushroom_agent/auth/magiccampus-im-sdk.json`
|
|
70
|
+
|
|
71
|
+
Supported commands:
|
|
72
|
+
|
|
73
|
+
- `config init|list|use|remove`
|
|
74
|
+
- `auth status`
|
|
75
|
+
- `im notify send`
|
|
76
|
+
- `im notify history`
|
|
77
|
+
- `user get`
|
|
78
|
+
- `feed list`
|
|
79
|
+
- `notify +send` (compat)
|
|
80
|
+
- `notify +history` (compat)
|
|
81
|
+
|
|
82
|
+
## Python SDK quick start
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from magiccampus_sdk import MagicCampusClient, MagicCampusClientConfig
|
|
86
|
+
|
|
87
|
+
client = MagicCampusClient(
|
|
88
|
+
MagicCampusClientConfig(
|
|
89
|
+
apiurl="https://api-test.pearlapi.com",
|
|
90
|
+
apikey="YOUR_API_KEY",
|
|
91
|
+
platform="IOS",
|
|
92
|
+
data_dir="./magiccampus_data",
|
|
93
|
+
)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
user = client.user.get("10134002")
|
|
97
|
+
print(user.nickname)
|
|
98
|
+
print(user.avatar_url)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Python websocket quick start
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from magiccampus_sdk import MagicCampusWSSDK, MagicCampusWSConfig
|
|
105
|
+
|
|
106
|
+
sdk = MagicCampusWSSDK(
|
|
107
|
+
MagicCampusWSConfig(
|
|
108
|
+
apiurl="https://api-test.pearlapi.com",
|
|
109
|
+
apikey="YOUR_API_KEY",
|
|
110
|
+
platform="IOS",
|
|
111
|
+
data_dir="./magiccampus_data",
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
sdk.login()
|
|
116
|
+
sdk.start()
|
|
117
|
+
sdk.send_text("hello", recv_id="10134002")
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Runnable examples live in [magiccampus_sdk/example.py](magiccampus_sdk/example.py) and [magiccampus_sdk/example_ws.py](magiccampus_sdk/example_ws.py).
|
|
121
|
+
|
|
122
|
+
## CLI smoke script
|
|
123
|
+
|
|
124
|
+
An executable smoke helper lives at [scripts/smoke_cli.py](scripts/smoke_cli.py).
|
|
125
|
+
|
|
126
|
+
Offline-safe mode only exercises config persistence and notify dry-run:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
uv run python scripts/smoke_cli.py
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This mode does not require a real API key. It uses a placeholder key by default and avoids any live auth call.
|
|
133
|
+
|
|
134
|
+
## Real network mode
|
|
135
|
+
|
|
136
|
+
To run the live auth probe, provide a real API key and enable `--live`:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
export MAGICCAMPUS_APIURL=https://api-test.pearlapi.com
|
|
140
|
+
export MAGICCAMPUS_APIKEY='your real api key'
|
|
141
|
+
export MAGICCAMPUS_PLATFORM=IOS
|
|
142
|
+
uv run python scripts/smoke_cli.py --live
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Optional variables:
|
|
146
|
+
|
|
147
|
+
- `MAGICCAMPUS_SMOKE_TARGET`: target used by dry-run and optional live send, default `user:smoke-target`
|
|
148
|
+
- `MAGICCAMPUS_PLATFORM`: defaults to `IOS`
|
|
149
|
+
|
|
150
|
+
If you also want to send a real text message, set a real target and pass `--live-send`:
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
export MAGICCAMPUS_SMOKE_TARGET='user:10134002'
|
|
154
|
+
uv run python scripts/smoke_cli.py --live --live-send --text 'hello from smoke script'
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`--live-send` performs a real outbound send, so use it only with a valid target and credentials.
|
|
158
|
+
|
|
159
|
+
## Notes
|
|
160
|
+
|
|
161
|
+
- `apiurl` can point to any MagicCampus API base URL; IM-backed flows append `/api/v1/accounts/imToken`
|
|
162
|
+
- `client.user.get(...)` calls `GET /api/v1/accounts/getUserInfo` with `apiurl` and `apikey` directly
|
|
163
|
+
- `platform` defaults to `IOS`
|
|
164
|
+
- low-level IM send / receive behavior comes from the bundled transport modules inside `magiccampus_sdk`
|
|
165
|
+
- the wrapper does not persist raw OpenIM credentials in CLI config
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
magiccampus_cli/__init__.py,sha256=Pru0BlFBASFCFo7McHdohtKkUtgMPDwbGfyUZlE2_Vw,21
|
|
2
|
+
magiccampus_cli/__main__.py,sha256=8M3vaTgigNUzQIxx-_rJFzfrAea2Oh_bU176_Fg1ywI,134
|
|
3
|
+
magiccampus_cli/accounts.py,sha256=UfxnKzWca3lJMTxC2AVe_k8J73uF2Np9rJcU5AE5OD8,4521
|
|
4
|
+
magiccampus_cli/cli.py,sha256=Cyp1mg_SuX59MUHLCGeZZiR-mZgxVwlEr07udS3eLPQ,4648
|
|
5
|
+
magiccampus_cli/client.py,sha256=4cJaWhGDXDu0sDhmIQvnXDzSZjPatLGKsekBh2oCB6w,3092
|
|
6
|
+
magiccampus_cli/config_store.py,sha256=_LUbyo7M7g6gh1dNzynwERxTMYAhm7z9EqGMf32j8GI,1693
|
|
7
|
+
magiccampus_cli/http_send.py,sha256=feP3R7a2IpR4r-FN1HiHt_7ZYP1nRnA2PlS5X7k8JGc,20050
|
|
8
|
+
magiccampus_cli/notify.py,sha256=9eUpHyNolnN9_XY6oY1eNTgRtzQGBAwRZkcSjGVrR-Y,13118
|
|
9
|
+
magiccampus_cli/output.py,sha256=ljIi6sC10D9Mu1DTIJhrXO2pLSH5aQO6fdjgeoFnwU8,3058
|
|
10
|
+
magiccampus_cli/prompts.py,sha256=gTNnSSWlXiFNEeAQjHtp2vhCUt88yp3QcX7gQD3ZX2Y,1648
|
|
11
|
+
magiccampus_cli/targets.py,sha256=1Vi8hEXDuAuXDCHi6SNOY2A2hNW9NIbjNukAKatk6s4,885
|
|
12
|
+
magiccampus_cli/utils.py,sha256=poyTOYbAD1h-4wmJPCCA_BzLp4Xh79kQXEGwh65_HDk,1683
|
|
13
|
+
magiccampus_cli/commands/auth.py,sha256=daxjFUvN5I_l-zC-YvbXWRavO9AdleIZpeQZT68ua8s,927
|
|
14
|
+
magiccampus_cli/commands/config.py,sha256=yUx0sq7zfcVv6OzeJaDgrz1_OLgajMGyHSFzBXZGYHM,3994
|
|
15
|
+
magiccampus_cli/commands/feed.py,sha256=keAXbZjY7Y9kVwT2Wz47YGU8565G4QIak6tJi-nEVi4,786
|
|
16
|
+
magiccampus_cli/commands/im.py,sha256=d5MF3OAdIlGbaYlXqVBMIjFTVHze-cXFIqBxJ5v6gu8,637
|
|
17
|
+
magiccampus_cli/commands/notify.py,sha256=B4QMziSLkzETPQE6m3PuAEUC_pxgYG9tpfz-LIKBuRk,3729
|
|
18
|
+
magiccampus_cli/commands/user.py,sha256=yuWIJowHZstsmFurfPMEbTmyTb_7DeTKWPWDQEe1VpM,2361
|
|
19
|
+
magiccampus_sdk/__init__.py,sha256=nL0AlWeOW-UvIJ8Wv2OgbdZUSrH2yFGGqqCWaTfcaSk,4838
|
|
20
|
+
magiccampus_sdk/auth.py,sha256=CPJIgOrc6pdXNU3rZFFeSsadbOoCU8FfwDqnxmD7wPw,3423
|
|
21
|
+
magiccampus_sdk/client.py,sha256=c6bX5iGKEAc157sS742KDN6RU1JurdTARUrnpkRP78I,2912
|
|
22
|
+
magiccampus_sdk/example.py,sha256=1ZsVTmABesIpfHf0HNGBg3aLfhsqpK4aE_Xi9zVvYSM,1276
|
|
23
|
+
magiccampus_sdk/example_ws.py,sha256=SeSs_3kIx5FFzKHgFuCOOJYUCGt5vgQ2rra9ntkDPbs,1833
|
|
24
|
+
magiccampus_sdk/feed_client.py,sha256=zpGEdOsMmys0vtmmfGC5biHQ1bllaxFZjKXyXQgX8Vc,566
|
|
25
|
+
magiccampus_sdk/models.py,sha256=dC54GbAsNUK8CXEHd0Rew9HQuizYHTBorrHIdsfPjQ8,5970
|
|
26
|
+
magiccampus_sdk/platform_client.py,sha256=-1381AmkZ2p74ut_obBF1Sad3wNIwC6CfudUDEV3QlU,4618
|
|
27
|
+
magiccampus_sdk/platform_http.py,sha256=NxFPXcLs9wY2QwNh0rj_9NkIEZJZKhmSnd4bah5ZO9E,1995
|
|
28
|
+
magiccampus_sdk/platform_ws.py,sha256=gZYcHyq6s38-2a6QwiVHngqPxBlv-tvhXI0i20DBE-g,4330
|
|
29
|
+
magiccampus_sdk/user_client.py,sha256=R7_nqkPZOh6RLH-HTkdUWOwExV0rw9M2dojU5Fxp9_c,1328
|
|
30
|
+
magiccampus_sdk/user_info_api.py,sha256=W1AEya19aV5QYW8-2k_0L_ePS31s3MX1dlRVergRkDk,1763
|
|
31
|
+
magiccampus_sdk/im/__init__.py,sha256=Jt5qXuYi1uh7x0JsPwf7PcXRdJIDgql0vCQxb7ffyiY,3177
|
|
32
|
+
magiccampus_sdk/im/client.py,sha256=L7OL98lZJSODsXg71oOnFzYJfdCdjV0P5WJYyAaRqKI,33509
|
|
33
|
+
magiccampus_sdk/im/gob_codec.py,sha256=Uafb9SIEDeS6KlD1yndWvzMUwAaIlftmyg8eJGxheqI,9386
|
|
34
|
+
magiccampus_sdk/im/http_api.py,sha256=qgtgs1LxlA9vRvHj0rJ0vEFC6L2azv5E4EiJMoDIVTw,2094
|
|
35
|
+
magiccampus_sdk/im/media.py,sha256=fgXdm5EZcFzqmjFTFgx3KVugaphIVRQFKugx2VeHy3Y,25641
|
|
36
|
+
magiccampus_sdk/im/models.py,sha256=O5_wvOA_uQib11HwbZsTz5JestwJ6Ia3aMsb76Znt3I,19170
|
|
37
|
+
magiccampus_sdk/im/self_user_info_api.py,sha256=5flZrEVLIRd28AIk2V1qeAGmvRsy6v4oR7IbWQIGatg,1408
|
|
38
|
+
magiccampus_sdk/im/storage.py,sha256=Jt7kwMbyHfeSNMg0aVHu3cZmtq1Z6gqAONYdAXVWIBE,7742
|
|
39
|
+
magiccampus_sdk/im/text.py,sha256=vAnJrr4n2-ngRcRQIA2LlgJtpcihZcvCmJFHktl0KBY,1703
|
|
40
|
+
magiccampus_sdk/im/ws_client.py,sha256=a3pNKjw74e74K2WaKLqEd8wguC_kURLUwlGpboDLYjA,45611
|
|
41
|
+
magiccampus_sdk/im/proto/__init__.py,sha256=auqeK5gGo0RuuOj-q_2EHV_QiSzfwWZYb7cJQsh5fGE,490
|
|
42
|
+
magiccampus_sdk/im/proto/sdkws/__init__.py,sha256=_7YAtN3EZzkli5KjDykOU-efPqOaTDYSAXEvtjFPBnQ,28
|
|
43
|
+
magiccampus_sdk/im/proto/sdkws/sdkws_pb2.py,sha256=m9ZfzJr6UUbba-j0tsK-6qe1bro5-wVe6BvxoHYwr_w,32349
|
|
44
|
+
magiccampus_sdk/im/proto/wrapperspb/__init__.py,sha256=_7YAtN3EZzkli5KjDykOU-efPqOaTDYSAXEvtjFPBnQ,28
|
|
45
|
+
magiccampus_sdk/im/proto/wrapperspb/wrapperspb_pb2.py,sha256=nO0wYxrJyCLLB0_rmowidpalKYHD9EnWd3DbsKz5-uI,2160
|
|
46
|
+
magic_campus_sdk-0.1.0.dist-info/METADATA,sha256=RjEwQG2MmvGL7LhPSowSRoMFohm2gVM0Osm7cYVoQ5k,4743
|
|
47
|
+
magic_campus_sdk-0.1.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
48
|
+
magic_campus_sdk-0.1.0.dist-info/entry_points.txt,sha256=UBHHJkoSwWB6A3of7m1A4m40pK-qJMGVKodCcfOLywA,63
|
|
49
|
+
magic_campus_sdk-0.1.0.dist-info/top_level.txt,sha256=N-wijR5BdV8-iLhE9AuPoZ5PUhKrl1I5yTszUw7TQAs,32
|
|
50
|
+
magic_campus_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
|
|
5
|
+
from magiccampus_sdk.auth import normalize_apiurl, normalize_platform
|
|
6
|
+
|
|
7
|
+
from .utils import env_string, mask_secret, trim_string
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class AccountConfig:
|
|
12
|
+
account_id: str
|
|
13
|
+
enabled: bool
|
|
14
|
+
apiurl: str
|
|
15
|
+
apikey: str
|
|
16
|
+
platform: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def account_config_to_raw(account: AccountConfig) -> dict[str, object]:
|
|
20
|
+
return {
|
|
21
|
+
"enabled": account.enabled,
|
|
22
|
+
"apiUrl": account.apiurl,
|
|
23
|
+
"apiKey": account.apikey,
|
|
24
|
+
"platform": account.platform,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def normalize_stored_account(raw: object) -> dict[str, object]:
|
|
29
|
+
source = raw if isinstance(raw, dict) else {}
|
|
30
|
+
apiurl = trim_string(source.get("apiUrl") or source.get("apiurl"))
|
|
31
|
+
platform = trim_string(source.get("platform")) or "IOS"
|
|
32
|
+
normalized: dict[str, object] = {
|
|
33
|
+
"enabled": source.get("enabled") is not False,
|
|
34
|
+
"apiUrl": normalize_apiurl(apiurl) if apiurl else "",
|
|
35
|
+
"apiKey": trim_string(source.get("apiKey") or source.get("apikey")),
|
|
36
|
+
"platform": normalize_platform(platform),
|
|
37
|
+
}
|
|
38
|
+
return normalized
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def env_default_account() -> dict[str, object] | None:
|
|
42
|
+
apiurl = env_string("MAGICCAMPUS_APIURL")
|
|
43
|
+
apikey = env_string("MAGICCAMPUS_APIKEY")
|
|
44
|
+
if not apiurl or not apikey:
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
"enabled": True,
|
|
49
|
+
"apiUrl": apiurl,
|
|
50
|
+
"apiKey": apikey,
|
|
51
|
+
"platform": env_string("MAGICCAMPUS_PLATFORM") or "IOS",
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def normalize_account(account_id: str, raw: object) -> AccountConfig | None:
|
|
56
|
+
stored = normalize_stored_account(raw)
|
|
57
|
+
if not stored["apiUrl"] or not stored["apiKey"]:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
return AccountConfig(
|
|
61
|
+
account_id=account_id,
|
|
62
|
+
enabled=stored.get("enabled") is not False,
|
|
63
|
+
apiurl=str(stored["apiUrl"]),
|
|
64
|
+
apikey=str(stored["apiKey"]),
|
|
65
|
+
platform=str(stored["platform"]),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def list_account_ids(root: dict[str, object]) -> list[str]:
|
|
70
|
+
accounts = root.get("accounts")
|
|
71
|
+
ids = list(accounts.keys()) if isinstance(accounts, dict) else []
|
|
72
|
+
if env_default_account() and "default" not in ids:
|
|
73
|
+
ids.insert(0, "default")
|
|
74
|
+
return ids
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_account_config(root: dict[str, object], account_id: str | None) -> AccountConfig | None:
|
|
78
|
+
resolved_id = account_id or str(root.get("defaultAccountId") or "default")
|
|
79
|
+
if resolved_id == "default":
|
|
80
|
+
env_account = env_default_account()
|
|
81
|
+
if env_account is not None:
|
|
82
|
+
return normalize_account("default", env_account)
|
|
83
|
+
|
|
84
|
+
accounts = root.get("accounts")
|
|
85
|
+
configured = accounts.get(resolved_id) if isinstance(accounts, dict) else None
|
|
86
|
+
if configured is not None:
|
|
87
|
+
return normalize_account(resolved_id, configured)
|
|
88
|
+
return None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def list_account_summaries(root: dict[str, object]) -> list[dict[str, object]]:
|
|
92
|
+
summaries: list[dict[str, object]] = []
|
|
93
|
+
default_account_id = str(root.get("defaultAccountId") or "default")
|
|
94
|
+
for account_id in list_account_ids(root):
|
|
95
|
+
account = get_account_config(root, account_id)
|
|
96
|
+
if account is None:
|
|
97
|
+
continue
|
|
98
|
+
summaries.append(
|
|
99
|
+
{
|
|
100
|
+
"accountId": account.account_id,
|
|
101
|
+
"default": account.account_id == default_account_id,
|
|
102
|
+
"enabled": account.enabled,
|
|
103
|
+
"apiUrl": account.apiurl,
|
|
104
|
+
"platform": account.platform,
|
|
105
|
+
}
|
|
106
|
+
)
|
|
107
|
+
return summaries
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def upsert_stored_account(root: dict[str, object], account_id: str, raw_account: dict[str, object], make_default: bool = False) -> dict[str, object]:
|
|
111
|
+
accounts = root.get("accounts")
|
|
112
|
+
next_accounts = dict(accounts) if isinstance(accounts, dict) else {}
|
|
113
|
+
next_accounts[account_id] = normalize_stored_account(raw_account)
|
|
114
|
+
default_account_id = account_id if make_default else str(root.get("defaultAccountId") or account_id)
|
|
115
|
+
return {"defaultAccountId": default_account_id, "accounts": next_accounts}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def remove_stored_account(root: dict[str, object], account_id: str) -> dict[str, object]:
|
|
119
|
+
accounts = root.get("accounts")
|
|
120
|
+
next_accounts = dict(accounts) if isinstance(accounts, dict) else {}
|
|
121
|
+
next_accounts.pop(account_id, None)
|
|
122
|
+
remaining_ids = list(next_accounts.keys())
|
|
123
|
+
default_account_id = str(root.get("defaultAccountId") or "default")
|
|
124
|
+
if default_account_id == account_id:
|
|
125
|
+
default_account_id = remaining_ids[0] if remaining_ids else "default"
|
|
126
|
+
return {"defaultAccountId": default_account_id, "accounts": next_accounts}
|
magiccampus_cli/cli.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from . import __version__
|
|
6
|
+
from .commands.auth import run_auth_command
|
|
7
|
+
from .commands.config import run_config_command
|
|
8
|
+
from .commands.feed import run_feed_command
|
|
9
|
+
from .commands.im import run_im_command
|
|
10
|
+
from .commands.notify import run_notify_command
|
|
11
|
+
from .commands.user import run_user_command
|
|
12
|
+
from .output import print_output
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _has_explicit_output_format(argv: list[str]) -> bool:
|
|
16
|
+
return "--format" in argv
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def parse_argv(argv: list[str]) -> tuple[str, list[str]]:
|
|
20
|
+
args = list(argv)
|
|
21
|
+
output_format = "json"
|
|
22
|
+
index = 0
|
|
23
|
+
while index < len(args):
|
|
24
|
+
if args[index] == "--format":
|
|
25
|
+
if index + 1 >= len(args):
|
|
26
|
+
raise RuntimeError("--format requires a value.")
|
|
27
|
+
output_format = args[index + 1]
|
|
28
|
+
del args[index:index + 2]
|
|
29
|
+
continue
|
|
30
|
+
index += 1
|
|
31
|
+
return output_format, args
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def parse_options(argv: list[str]) -> tuple[dict[str, object], list[str]]:
|
|
35
|
+
options: dict[str, object] = {}
|
|
36
|
+
positionals: list[str] = []
|
|
37
|
+
index = 0
|
|
38
|
+
while index < len(argv):
|
|
39
|
+
value = argv[index]
|
|
40
|
+
if value.startswith("--"):
|
|
41
|
+
key = value[2:]
|
|
42
|
+
next_value = argv[index + 1] if index + 1 < len(argv) else None
|
|
43
|
+
if next_value is not None and not next_value.startswith("--"):
|
|
44
|
+
options[key] = next_value
|
|
45
|
+
index += 2
|
|
46
|
+
continue
|
|
47
|
+
options[key] = True
|
|
48
|
+
index += 1
|
|
49
|
+
continue
|
|
50
|
+
positionals.append(value)
|
|
51
|
+
index += 1
|
|
52
|
+
return options, positionals
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def usage() -> str:
|
|
56
|
+
return "\n".join(
|
|
57
|
+
[
|
|
58
|
+
"magic-campus",
|
|
59
|
+
"",
|
|
60
|
+
"Commands:",
|
|
61
|
+
" config init [--account <id>] --apiurl <url> --apikey <token> [--platform <name> --enabled <bool> --default <bool>]",
|
|
62
|
+
" config list",
|
|
63
|
+
" config use <accountId>",
|
|
64
|
+
" config remove <accountId>",
|
|
65
|
+
" auth status [--account <id>]",
|
|
66
|
+
" im notify send --to <user:ID|group:ID> (--text <text> | --image <path|url> | --file <path|url> | --video <path|url>) [--name <filename>] [--account <id>] [--dry-run]",
|
|
67
|
+
" im notify history --to <user:ID|group:ID> [--limit <count>] [--account <id>]",
|
|
68
|
+
" user get --id <userId> [--account <id>]",
|
|
69
|
+
" feed list [--account <id>] [--limit <count>] [--cursor <value>]",
|
|
70
|
+
" notify +send --to <user:ID|group:ID> (--text <text> | --image <path|url> | --file <path|url> | --video <path|url>) [--name <filename>] [--account <id>] [--dry-run] (compat)",
|
|
71
|
+
" notify +history --to <user:ID|group:ID> [--limit <count>] [--account <id>] (compat)",
|
|
72
|
+
"",
|
|
73
|
+
"Global flags:",
|
|
74
|
+
" --format json|pretty|table|ndjson",
|
|
75
|
+
]
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def main(raw_argv: list[str] | None = None) -> int:
|
|
80
|
+
argv = list(sys.argv[1:] if raw_argv is None else raw_argv)
|
|
81
|
+
try:
|
|
82
|
+
has_explicit_output_format = _has_explicit_output_format(argv)
|
|
83
|
+
output_format, args = parse_argv(argv)
|
|
84
|
+
if not args or "--help" in args or "-h" in args:
|
|
85
|
+
if not has_explicit_output_format:
|
|
86
|
+
sys.stdout.write(f"{usage()}\n")
|
|
87
|
+
return 0
|
|
88
|
+
print_output({"ok": True, "usage": usage()}, output_format)
|
|
89
|
+
return 0
|
|
90
|
+
|
|
91
|
+
if args[0] == "--version":
|
|
92
|
+
print_output({"ok": True, "version": __version__, "runtime": "python"}, output_format)
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
if args[0] == "config":
|
|
96
|
+
result = run_config_command(args[1] if len(args) > 1 else None, args[2:], parse_options)
|
|
97
|
+
elif args[0] == "auth":
|
|
98
|
+
result = run_auth_command(args[1] if len(args) > 1 else None, args[2:], parse_options)
|
|
99
|
+
elif args[0] == "im":
|
|
100
|
+
result = run_im_command(args[1] if len(args) > 1 else None, args[2:], parse_options)
|
|
101
|
+
elif args[0] == "user":
|
|
102
|
+
result = run_user_command(args[1] if len(args) > 1 else None, args[2:], parse_options)
|
|
103
|
+
elif args[0] == "feed":
|
|
104
|
+
result = run_feed_command(args[1] if len(args) > 1 else None, args[2:], parse_options)
|
|
105
|
+
elif args[0] == "notify":
|
|
106
|
+
result = run_notify_command(args[1] if len(args) > 1 else None, args[2:], parse_options)
|
|
107
|
+
else:
|
|
108
|
+
raise RuntimeError(f"Unknown command: {args[0]}")
|
|
109
|
+
|
|
110
|
+
print_output(result, output_format)
|
|
111
|
+
return 0
|
|
112
|
+
except Exception as error:
|
|
113
|
+
print_output({"ok": False, "error": str(error)}, output_format if "output_format" in locals() else "json")
|
|
114
|
+
return 1
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import time
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, TypeVar
|
|
6
|
+
|
|
7
|
+
from magiccampus_sdk.auth import resolve_platform_id
|
|
8
|
+
|
|
9
|
+
from . import __version__
|
|
10
|
+
from .accounts import AccountConfig
|
|
11
|
+
from .config_store import get_runtime_data_dir
|
|
12
|
+
from .utils import mask_secret
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from magiccampus_sdk.platform_ws import MagicCampusWSSDK
|
|
16
|
+
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _build_data_dir(account: AccountConfig) -> Path:
|
|
21
|
+
path = get_runtime_data_dir() / account.account_id
|
|
22
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
return path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_account_db_path(account_id: str, user_id: str) -> Path:
|
|
27
|
+
return get_runtime_data_dir() / account_id / f"OpenIM_pyws_{user_id}.db"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_sdk(account: AccountConfig) -> "MagicCampusWSSDK":
|
|
31
|
+
from magiccampus_sdk.platform_ws import MagicCampusWSConfig, MagicCampusWSSDK
|
|
32
|
+
|
|
33
|
+
config = MagicCampusWSConfig(
|
|
34
|
+
apiurl=account.apiurl,
|
|
35
|
+
apikey=account.apikey,
|
|
36
|
+
platform=account.platform,
|
|
37
|
+
data_dir=str(_build_data_dir(account)),
|
|
38
|
+
timeout_seconds=30.0,
|
|
39
|
+
sdk_version=f"magic-campus/{__version__}",
|
|
40
|
+
auto_sync_on_connect=False,
|
|
41
|
+
auto_sync_on_reconnect=False,
|
|
42
|
+
auto_reconnect=False,
|
|
43
|
+
enable_heartbeat=False,
|
|
44
|
+
)
|
|
45
|
+
return MagicCampusWSSDK(config)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def with_connected_sdk(account: AccountConfig, action: Callable[[Any], T]) -> T:
|
|
49
|
+
sdk = build_sdk(account)
|
|
50
|
+
sdk.login()
|
|
51
|
+
try:
|
|
52
|
+
sdk.start()
|
|
53
|
+
return action(sdk)
|
|
54
|
+
finally:
|
|
55
|
+
try:
|
|
56
|
+
time.sleep(0.25)
|
|
57
|
+
finally:
|
|
58
|
+
sdk.logout()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def with_http_send_client(account: AccountConfig, action: Callable[[object], T]) -> T:
|
|
62
|
+
from .http_send import MagicCampusHTTPSendClient
|
|
63
|
+
|
|
64
|
+
client = MagicCampusHTTPSendClient(account)
|
|
65
|
+
client.login()
|
|
66
|
+
try:
|
|
67
|
+
return action(client)
|
|
68
|
+
finally:
|
|
69
|
+
client.logout()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def get_auth_status(account: AccountConfig) -> dict[str, object]:
|
|
73
|
+
def _probe(sdk: MagicCampusWSSDK) -> dict[str, object]:
|
|
74
|
+
credentials = sdk.credentials
|
|
75
|
+
now = int(time.time())
|
|
76
|
+
expire_at = credentials.expire_time if credentials else 0
|
|
77
|
+
return {
|
|
78
|
+
"ok": True,
|
|
79
|
+
"accountId": account.account_id,
|
|
80
|
+
"userID": sdk.user_id,
|
|
81
|
+
"platformID": resolve_platform_id(account.platform),
|
|
82
|
+
"provider": "magiccampus",
|
|
83
|
+
"transport": "ws_client",
|
|
84
|
+
"connected": True,
|
|
85
|
+
"loginStatus": "connected",
|
|
86
|
+
"wsAddr": credentials.ws_addr if credentials else "",
|
|
87
|
+
"apiAddr": credentials.api_addr if credentials else "",
|
|
88
|
+
"magiccampus": {
|
|
89
|
+
"enabled": account.enabled,
|
|
90
|
+
"platform": account.platform,
|
|
91
|
+
"apiUrl": account.apiurl,
|
|
92
|
+
"apiKeyPreview": mask_secret(account.apikey),
|
|
93
|
+
"expireAt": expire_at,
|
|
94
|
+
"expiresInSeconds": max(0, expire_at - now) if expire_at else 0,
|
|
95
|
+
"dataDir": str(_build_data_dir(account)),
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return with_connected_sdk(account, _probe)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ..accounts import get_account_config
|
|
4
|
+
from ..config_store import read_config_root
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def resolve_account_or_throw(root: dict[str, object], account_id: str | None):
|
|
8
|
+
account = get_account_config(root, account_id)
|
|
9
|
+
if account is None:
|
|
10
|
+
resolved = account_id or str(root.get("defaultAccountId") or "default")
|
|
11
|
+
raise RuntimeError(
|
|
12
|
+
f"No usable MagicCampus account found for '{resolved}'. Run 'config init' or set MAGICCAMPUS_* env vars."
|
|
13
|
+
)
|
|
14
|
+
return account
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def run_auth_command(subcommand: str | None, argv: list[str], parse_options):
|
|
18
|
+
if subcommand != "status":
|
|
19
|
+
raise RuntimeError(f"Unknown auth subcommand: {subcommand}")
|
|
20
|
+
|
|
21
|
+
options, _ = parse_options(argv)
|
|
22
|
+
root = read_config_root()
|
|
23
|
+
account = resolve_account_or_throw(root, options.get("account"))
|
|
24
|
+
from ..client import get_auth_status
|
|
25
|
+
|
|
26
|
+
return get_auth_status(account)
|