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.
Files changed (50) hide show
  1. magic_campus_sdk-0.1.0.dist-info/METADATA +165 -0
  2. magic_campus_sdk-0.1.0.dist-info/RECORD +50 -0
  3. magic_campus_sdk-0.1.0.dist-info/WHEEL +5 -0
  4. magic_campus_sdk-0.1.0.dist-info/entry_points.txt +2 -0
  5. magic_campus_sdk-0.1.0.dist-info/top_level.txt +2 -0
  6. magiccampus_cli/__init__.py +1 -0
  7. magiccampus_cli/__main__.py +9 -0
  8. magiccampus_cli/accounts.py +126 -0
  9. magiccampus_cli/cli.py +118 -0
  10. magiccampus_cli/client.py +99 -0
  11. magiccampus_cli/commands/auth.py +26 -0
  12. magiccampus_cli/commands/config.py +95 -0
  13. magiccampus_cli/commands/feed.py +23 -0
  14. magiccampus_cli/commands/im.py +20 -0
  15. magiccampus_cli/commands/notify.py +92 -0
  16. magiccampus_cli/commands/user.py +66 -0
  17. magiccampus_cli/config_store.py +57 -0
  18. magiccampus_cli/http_send.py +565 -0
  19. magiccampus_cli/notify.py +357 -0
  20. magiccampus_cli/output.py +101 -0
  21. magiccampus_cli/prompts.py +58 -0
  22. magiccampus_cli/targets.py +26 -0
  23. magiccampus_cli/utils.py +67 -0
  24. magiccampus_sdk/__init__.py +118 -0
  25. magiccampus_sdk/auth.py +111 -0
  26. magiccampus_sdk/client.py +89 -0
  27. magiccampus_sdk/example.py +46 -0
  28. magiccampus_sdk/example_ws.py +59 -0
  29. magiccampus_sdk/feed_client.py +21 -0
  30. magiccampus_sdk/im/__init__.py +85 -0
  31. magiccampus_sdk/im/client.py +913 -0
  32. magiccampus_sdk/im/gob_codec.py +289 -0
  33. magiccampus_sdk/im/http_api.py +58 -0
  34. magiccampus_sdk/im/media.py +761 -0
  35. magiccampus_sdk/im/models.py +583 -0
  36. magiccampus_sdk/im/proto/__init__.py +21 -0
  37. magiccampus_sdk/im/proto/sdkws/__init__.py +1 -0
  38. magiccampus_sdk/im/proto/sdkws/sdkws_pb2.py +215 -0
  39. magiccampus_sdk/im/proto/wrapperspb/__init__.py +1 -0
  40. magiccampus_sdk/im/proto/wrapperspb/wrapperspb_pb2.py +45 -0
  41. magiccampus_sdk/im/self_user_info_api.py +48 -0
  42. magiccampus_sdk/im/storage.py +213 -0
  43. magiccampus_sdk/im/text.py +49 -0
  44. magiccampus_sdk/im/ws_client.py +1235 -0
  45. magiccampus_sdk/models.py +164 -0
  46. magiccampus_sdk/platform_client.py +119 -0
  47. magiccampus_sdk/platform_http.py +71 -0
  48. magiccampus_sdk/platform_ws.py +122 -0
  49. magiccampus_sdk/user_client.py +36 -0
  50. 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ magic-campus = magiccampus_cli.__main__:main
@@ -0,0 +1,2 @@
1
+ magiccampus_cli
2
+ magiccampus_sdk
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,9 @@
1
+ from .cli import main as cli_main
2
+
3
+
4
+ def main() -> int:
5
+ return cli_main()
6
+
7
+
8
+ if __name__ == "__main__":
9
+ raise SystemExit(main())
@@ -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)