tuitui-bot 1.0.11__tar.gz
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.
- tuitui_bot-1.0.11/.gitignore +25 -0
- tuitui_bot-1.0.11/LICENSE +21 -0
- tuitui_bot-1.0.11/PKG-INFO +113 -0
- tuitui_bot-1.0.11/README.md +80 -0
- tuitui_bot-1.0.11/pyproject.toml +76 -0
- tuitui_bot-1.0.11/samples/.env.example +2 -0
- tuitui_bot-1.0.11/samples/README.md +34 -0
- tuitui_bot-1.0.11/samples/config.py +22 -0
- tuitui_bot-1.0.11/samples/receive.py +76 -0
- tuitui_bot-1.0.11/samples/send_file.py +40 -0
- tuitui_bot-1.0.11/samples/send_interactive.py +60 -0
- tuitui_bot-1.0.11/samples/send_mixed.py +47 -0
- tuitui_bot-1.0.11/samples/send_post.py +30 -0
- tuitui_bot-1.0.11/samples/send_text.py +39 -0
- tuitui_bot-1.0.11/src/tuitui_bot/__init__.py +162 -0
- tuitui_bot-1.0.11/src/tuitui_bot/_deduplicator.py +59 -0
- tuitui_bot-1.0.11/src/tuitui_bot/_mime.py +174 -0
- tuitui_bot-1.0.11/src/tuitui_bot/_time.py +86 -0
- tuitui_bot-1.0.11/src/tuitui_bot/client.py +46 -0
- tuitui_bot-1.0.11/src/tuitui_bot/config.py +101 -0
- tuitui_bot-1.0.11/src/tuitui_bot/errors.py +20 -0
- tuitui_bot-1.0.11/src/tuitui_bot/event.py +142 -0
- tuitui_bot-1.0.11/src/tuitui_bot/events.py +95 -0
- tuitui_bot-1.0.11/src/tuitui_bot/file.py +20 -0
- tuitui_bot-1.0.11/src/tuitui_bot/file_space.py +194 -0
- tuitui_bot-1.0.11/src/tuitui_bot/http.py +132 -0
- tuitui_bot-1.0.11/src/tuitui_bot/im.py +313 -0
- tuitui_bot-1.0.11/src/tuitui_bot/interactive.py +105 -0
- tuitui_bot-1.0.11/src/tuitui_bot/mention.py +13 -0
- tuitui_bot-1.0.11/src/tuitui_bot/property.py +85 -0
- tuitui_bot-1.0.11/src/tuitui_bot/py.typed +1 -0
- tuitui_bot-1.0.11/src/tuitui_bot/records.py +121 -0
- tuitui_bot-1.0.11/src/tuitui_bot/teams.py +376 -0
- tuitui_bot-1.0.11/src/tuitui_bot/to_target.py +85 -0
- tuitui_bot-1.0.11/src/tuitui_bot/upload.py +182 -0
- tuitui_bot-1.0.11/src/tuitui_bot/websocket.py +276 -0
- tuitui_bot-1.0.11/tests/e2e/README.md +23 -0
- tuitui_bot-1.0.11/tests/e2e/config.example.json +17 -0
- tuitui_bot-1.0.11/tests/e2e/conftest.py +115 -0
- tuitui_bot-1.0.11/tests/e2e/test_im.py +28 -0
- tuitui_bot-1.0.11/tests/e2e/test_property.py +16 -0
- tuitui_bot-1.0.11/tests/e2e/test_roundtrip.py +49 -0
- tuitui_bot-1.0.11/tests/e2e/test_teams_file_space.py +35 -0
- tuitui_bot-1.0.11/tests/mock/test_config_http.py +140 -0
- tuitui_bot-1.0.11/tests/mock/test_events_records.py +202 -0
- tuitui_bot-1.0.11/tests/mock/test_targets_utils.py +109 -0
- tuitui_bot-1.0.11/tests/mock/test_teams_file_space_property.py +189 -0
- tuitui_bot-1.0.11/tests/mock/test_upload_im.py +151 -0
- tuitui_bot-1.0.11/tests/mock/test_websocket_client_public.py +156 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.DS_Store
|
|
2
|
+
node_modules
|
|
3
|
+
dist/
|
|
4
|
+
coverage/
|
|
5
|
+
build/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
__pycache__/
|
|
10
|
+
*.py[cod]
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
.package-install-test/
|
|
16
|
+
tests/e2e/config.local.json
|
|
17
|
+
samples/.env
|
|
18
|
+
samples/dist/
|
|
19
|
+
package-lock.json
|
|
20
|
+
.wolf
|
|
21
|
+
.wiscode/
|
|
22
|
+
docs/
|
|
23
|
+
governance/
|
|
24
|
+
.build-stage/
|
|
25
|
+
.cc-connect
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tuitui.cn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tuitui-bot
|
|
3
|
+
Version: 1.0.11
|
|
4
|
+
Summary: 推推机器人 Python SDK
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Typing :: Typed
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: httpx<1,>=0.27
|
|
18
|
+
Requires-Dist: websockets<18,>=14
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
21
|
+
Requires-Dist: hatchling>=1.27; extra == 'dev'
|
|
22
|
+
Requires-Dist: mypy>=1.13; extra == 'dev'
|
|
23
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-cov>=6; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest-timeout>=2.3; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=8.3; extra == 'dev'
|
|
27
|
+
Requires-Dist: respx>=0.22; extra == 'dev'
|
|
28
|
+
Requires-Dist: ruff>=0.9; extra == 'dev'
|
|
29
|
+
Requires-Dist: twine<7,>=6; extra == 'dev'
|
|
30
|
+
Provides-Extra: samples
|
|
31
|
+
Requires-Dist: python-dotenv>=1; extra == 'samples'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# tuitui-bot
|
|
35
|
+
|
|
36
|
+
推推机器人异步 Python SDK,移植自 npm 包 `@qihoo/tuitui-bot-sdk`。支持 Python 3.10 及以上版本。
|
|
37
|
+
|
|
38
|
+
## 安装
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install tuitui-bot
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 发送消息
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
|
|
49
|
+
from tuitui_bot import TuituiBotClient
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def main() -> None:
|
|
53
|
+
client = TuituiBotClient("your-app-id", "your-app-secret")
|
|
54
|
+
await client.im.send_text(
|
|
55
|
+
to=client.to.account("user-account"),
|
|
56
|
+
text="**你好**,这是一条 `Markdown` 消息。",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`TuituiBotClient` 构造时不连接网络,默认 HTTP 会话由 SDK 在每次请求后自动关闭,
|
|
64
|
+
不需要 `async with` 或手动关闭客户端。
|
|
65
|
+
|
|
66
|
+
`client.to` 提供 `account()`、`uid()`、`group()`、`accounts()` 和 `uids()` 五种结构化目标。
|
|
67
|
+
IM 位于 `client.im`,频道和帖子能力位于 `client.teams`。
|
|
68
|
+
|
|
69
|
+
## 事件订阅
|
|
70
|
+
|
|
71
|
+
构造客户端或发送消息不会建立 WebSocket。只有调用 `client.event.subscribe()` 才会订阅事件:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
import asyncio
|
|
75
|
+
|
|
76
|
+
from tuitui_bot import TuituiBotClient, TuituiSubscribeOptions
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def main() -> None:
|
|
80
|
+
client = TuituiBotClient("...", "...")
|
|
81
|
+
|
|
82
|
+
async def on_event(raw: dict[str, object]) -> None:
|
|
83
|
+
print(raw)
|
|
84
|
+
|
|
85
|
+
subscription = client.event.subscribe(
|
|
86
|
+
TuituiSubscribeOptions(on_event=on_event),
|
|
87
|
+
)
|
|
88
|
+
try:
|
|
89
|
+
await asyncio.Event().wait()
|
|
90
|
+
finally:
|
|
91
|
+
await subscription.unsubscribe()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
asyncio.run(main())
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
SDK 自动处理鉴权、ACK、保活过滤、去重、心跳超时和断线重连。原始事件字段会被保留;需要正文或
|
|
98
|
+
媒体时,显式使用 `client.event.render_message_body(data)` 和
|
|
99
|
+
`client.event.get_message_media(data)`。
|
|
100
|
+
|
|
101
|
+
## API 概览
|
|
102
|
+
|
|
103
|
+
- `client.im`:文本、图文混排、页面、链接、文件、交互卡片、编辑、表情和历史记录。
|
|
104
|
+
- `client.teams`:帖子、回复、编辑、文件、表情、频道、标签、公告、成员和帖子链。
|
|
105
|
+
- `client.file`:底层文件上传。
|
|
106
|
+
- `client.file_space`:文件空间节点、目录和文件。
|
|
107
|
+
- `client.property`:机器人信息、名称、头像、Webhook、交互地址和快捷指令。
|
|
108
|
+
- `client.event`:事件订阅、正文渲染和媒体提取。
|
|
109
|
+
- `client.request()`:调用尚未封装的 Bot API。
|
|
110
|
+
|
|
111
|
+
文本消息和帖子支持 Markdown;图文混排中的文字是纯文本。上传支持 bytes、Path、本地路径、二进制
|
|
112
|
+
文件对象、data URL 和 HTTP/HTTPS URL,最大 100MB。远程 URL 下载可通过
|
|
113
|
+
`TuituiClientOptions(fetch_with_ssrf=...)` 注入 SSRF 防护实现。
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# tuitui-bot
|
|
2
|
+
|
|
3
|
+
推推机器人异步 Python SDK,移植自 npm 包 `@qihoo/tuitui-bot-sdk`。支持 Python 3.10 及以上版本。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install tuitui-bot
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 发送消息
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import asyncio
|
|
15
|
+
|
|
16
|
+
from tuitui_bot import TuituiBotClient
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
async def main() -> None:
|
|
20
|
+
client = TuituiBotClient("your-app-id", "your-app-secret")
|
|
21
|
+
await client.im.send_text(
|
|
22
|
+
to=client.to.account("user-account"),
|
|
23
|
+
text="**你好**,这是一条 `Markdown` 消息。",
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
asyncio.run(main())
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`TuituiBotClient` 构造时不连接网络,默认 HTTP 会话由 SDK 在每次请求后自动关闭,
|
|
31
|
+
不需要 `async with` 或手动关闭客户端。
|
|
32
|
+
|
|
33
|
+
`client.to` 提供 `account()`、`uid()`、`group()`、`accounts()` 和 `uids()` 五种结构化目标。
|
|
34
|
+
IM 位于 `client.im`,频道和帖子能力位于 `client.teams`。
|
|
35
|
+
|
|
36
|
+
## 事件订阅
|
|
37
|
+
|
|
38
|
+
构造客户端或发送消息不会建立 WebSocket。只有调用 `client.event.subscribe()` 才会订阅事件:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import asyncio
|
|
42
|
+
|
|
43
|
+
from tuitui_bot import TuituiBotClient, TuituiSubscribeOptions
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
async def main() -> None:
|
|
47
|
+
client = TuituiBotClient("...", "...")
|
|
48
|
+
|
|
49
|
+
async def on_event(raw: dict[str, object]) -> None:
|
|
50
|
+
print(raw)
|
|
51
|
+
|
|
52
|
+
subscription = client.event.subscribe(
|
|
53
|
+
TuituiSubscribeOptions(on_event=on_event),
|
|
54
|
+
)
|
|
55
|
+
try:
|
|
56
|
+
await asyncio.Event().wait()
|
|
57
|
+
finally:
|
|
58
|
+
await subscription.unsubscribe()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
SDK 自动处理鉴权、ACK、保活过滤、去重、心跳超时和断线重连。原始事件字段会被保留;需要正文或
|
|
65
|
+
媒体时,显式使用 `client.event.render_message_body(data)` 和
|
|
66
|
+
`client.event.get_message_media(data)`。
|
|
67
|
+
|
|
68
|
+
## API 概览
|
|
69
|
+
|
|
70
|
+
- `client.im`:文本、图文混排、页面、链接、文件、交互卡片、编辑、表情和历史记录。
|
|
71
|
+
- `client.teams`:帖子、回复、编辑、文件、表情、频道、标签、公告、成员和帖子链。
|
|
72
|
+
- `client.file`:底层文件上传。
|
|
73
|
+
- `client.file_space`:文件空间节点、目录和文件。
|
|
74
|
+
- `client.property`:机器人信息、名称、头像、Webhook、交互地址和快捷指令。
|
|
75
|
+
- `client.event`:事件订阅、正文渲染和媒体提取。
|
|
76
|
+
- `client.request()`:调用尚未封装的 Bot API。
|
|
77
|
+
|
|
78
|
+
文本消息和帖子支持 Markdown;图文混排中的文字是纯文本。上传支持 bytes、Path、本地路径、二进制
|
|
79
|
+
文件对象、data URL 和 HTTP/HTTPS URL,最大 100MB。远程 URL 下载可通过
|
|
80
|
+
`TuituiClientOptions(fetch_with_ssrf=...)` 注入 SSRF 防护实现。
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tuitui-bot"
|
|
7
|
+
version = "1.0.11"
|
|
8
|
+
description = "推推机器人 Python SDK"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"httpx>=0.27,<1",
|
|
15
|
+
"websockets>=14,<18",
|
|
16
|
+
]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.optional-dependencies]
|
|
30
|
+
dev = [
|
|
31
|
+
"build>=1.2",
|
|
32
|
+
"hatchling>=1.27",
|
|
33
|
+
"mypy>=1.13",
|
|
34
|
+
"pytest>=8.3",
|
|
35
|
+
"pytest-asyncio>=0.24",
|
|
36
|
+
"pytest-cov>=6",
|
|
37
|
+
"pytest-timeout>=2.3",
|
|
38
|
+
"respx>=0.22",
|
|
39
|
+
"ruff>=0.9",
|
|
40
|
+
"twine>=6,<7",
|
|
41
|
+
]
|
|
42
|
+
samples = ["python-dotenv>=1"]
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/tuitui_bot"]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.sdist]
|
|
48
|
+
exclude = [
|
|
49
|
+
"/.codex",
|
|
50
|
+
"/AGENTS.md",
|
|
51
|
+
"/MIGRATION.md",
|
|
52
|
+
"/dist",
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
addopts = "-ra"
|
|
57
|
+
asyncio_mode = "auto"
|
|
58
|
+
testpaths = ["tests/mock"]
|
|
59
|
+
markers = ["e2e: 调用真实推推服务的测试"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 100
|
|
63
|
+
target-version = "py310"
|
|
64
|
+
|
|
65
|
+
[tool.ruff.lint]
|
|
66
|
+
select = ["E", "F", "I", "UP", "B", "ASYNC", "RUF"]
|
|
67
|
+
ignore = ["RUF001", "RUF002", "RUF003"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff.format]
|
|
70
|
+
quote-style = "double"
|
|
71
|
+
|
|
72
|
+
[tool.mypy]
|
|
73
|
+
python_version = "3.10"
|
|
74
|
+
strict = true
|
|
75
|
+
packages = ["tuitui_bot"]
|
|
76
|
+
mypy_path = "src"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# SDK 使用示例
|
|
2
|
+
|
|
3
|
+
所有示例都需要机器人的 App ID 和 App Secret。复制 `.env.example` 为 `.env`
|
|
4
|
+
并填写:
|
|
5
|
+
|
|
6
|
+
```dotenv
|
|
7
|
+
TUITUI_BOT_APPID=机器人 App ID
|
|
8
|
+
TUITUI_BOT_SECRET=机器人 App Secret
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
安装 SDK 和示例依赖:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e ".[samples]"
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
发送目标通过命令行参数指定,与 JavaScript 版示例一致:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python samples/send_text.py --account alice
|
|
21
|
+
python samples/send_text.py --uid 123456
|
|
22
|
+
python samples/send_text.py --group 987654
|
|
23
|
+
|
|
24
|
+
python samples/send_file.py --account alice
|
|
25
|
+
python samples/send_mixed.py --account alice
|
|
26
|
+
python samples/send_interactive.py --account alice
|
|
27
|
+
python samples/send_post.py --team 123456 --channel 789012
|
|
28
|
+
python samples/receive.py
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
所有 `send_*.py` 示例都会在发送成功后输出服务端返回的完整 JSON。
|
|
32
|
+
|
|
33
|
+
`receive.py` 连接成功后会立即输出监听状态,随后持续输出原始事件和解析文本。
|
|
34
|
+
按 `Ctrl+C` 取消订阅并退出。`.env` 已被 Git 忽略,不要提交真实凭证。
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from dotenv import load_dotenv
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True, slots=True)
|
|
9
|
+
class SampleConfig:
|
|
10
|
+
app_id: str
|
|
11
|
+
app_secret: str
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def load_sample_config() -> SampleConfig:
|
|
15
|
+
env_path = Path(__file__).with_name(".env")
|
|
16
|
+
load_dotenv(dotenv_path=env_path)
|
|
17
|
+
|
|
18
|
+
app_id = os.getenv("TUITUI_BOT_APPID", "").strip()
|
|
19
|
+
app_secret = os.getenv("TUITUI_BOT_SECRET", "").strip()
|
|
20
|
+
if not app_id or not app_secret:
|
|
21
|
+
raise RuntimeError(f"请设置 TUITUI_BOT_APPID 和 TUITUI_BOT_SECRET,或在 {env_path} 中填写")
|
|
22
|
+
return SampleConfig(app_id=app_id, app_secret=app_secret)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from datetime import datetime, timezone
|
|
5
|
+
|
|
6
|
+
from config import load_sample_config
|
|
7
|
+
|
|
8
|
+
from tuitui_bot import TuituiBotClient, TuituiRawEvent, TuituiSubscribeOptions
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def timestamp() -> str:
|
|
12
|
+
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def js_value(value: object) -> str:
|
|
16
|
+
if value is None:
|
|
17
|
+
return "undefined"
|
|
18
|
+
if isinstance(value, bool):
|
|
19
|
+
return str(value).lower()
|
|
20
|
+
return str(value)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
async def main() -> None:
|
|
24
|
+
config = load_sample_config()
|
|
25
|
+
client = TuituiBotClient(config.app_id, config.app_secret)
|
|
26
|
+
|
|
27
|
+
def on_connected() -> None:
|
|
28
|
+
print(f"[{timestamp()}] 已连接bot({config.app_id}),正在监听事件", flush=True)
|
|
29
|
+
|
|
30
|
+
def on_event(event: TuituiRawEvent) -> None:
|
|
31
|
+
body = event.get("body")
|
|
32
|
+
event_name = body.get("event", "unknown") if isinstance(body, dict) else "unknown"
|
|
33
|
+
|
|
34
|
+
print(f"[{timestamp()}] 收到事件:{event_name}", flush=True)
|
|
35
|
+
print(json.dumps(event, ensure_ascii=False, indent=2), flush=True)
|
|
36
|
+
|
|
37
|
+
if not isinstance(body, dict):
|
|
38
|
+
return
|
|
39
|
+
data = body.get("data")
|
|
40
|
+
|
|
41
|
+
if event_name == client.event.SINGLE_CHAT:
|
|
42
|
+
text = client.event.render_message_body(data)
|
|
43
|
+
print(f"[{timestamp()}] 单聊解析文本:\n{text}", flush=True)
|
|
44
|
+
elif event_name == client.event.GROUP_CHAT:
|
|
45
|
+
text = client.event.render_message_body(data)
|
|
46
|
+
print(f"[{timestamp()}] 群聊解析文本:\n{text}", flush=True)
|
|
47
|
+
at_me = data.get("at_me", False) if isinstance(data, dict) else False
|
|
48
|
+
print(f"[{timestamp()}] 群聊是否@自己:{js_value(at_me)}", flush=True)
|
|
49
|
+
elif event_name in (client.event.TEAMS_POST_CREATE, client.event.TEAMS_POST_MODIFY):
|
|
50
|
+
content = data.get("content", "") if isinstance(data, dict) else ""
|
|
51
|
+
at_me = data.get("at_me") if isinstance(data, dict) else None
|
|
52
|
+
print(f"[{timestamp()}] 团队帖子解析文本:\n{content}", flush=True)
|
|
53
|
+
print(f"[{timestamp()}] 团队帖子是否@自己:{js_value(at_me)}", flush=True)
|
|
54
|
+
|
|
55
|
+
def on_error(error: Exception) -> None:
|
|
56
|
+
print(f"[{timestamp()}] WebSocket 错误", error, file=sys.stderr, flush=True)
|
|
57
|
+
|
|
58
|
+
subscription = client.event.subscribe(
|
|
59
|
+
TuituiSubscribeOptions(
|
|
60
|
+
on_connected=on_connected,
|
|
61
|
+
on_event=on_event,
|
|
62
|
+
on_error=on_error,
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
try:
|
|
66
|
+
await asyncio.Event().wait()
|
|
67
|
+
finally:
|
|
68
|
+
print(f"\n[{timestamp()}] 停止监听", flush=True)
|
|
69
|
+
await subscription.unsubscribe()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
try:
|
|
74
|
+
asyncio.run(main())
|
|
75
|
+
except KeyboardInterrupt:
|
|
76
|
+
pass
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from argparse import ArgumentParser, Namespace
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from config import load_sample_config
|
|
7
|
+
|
|
8
|
+
from tuitui_bot import ToTarget, TuituiBotClient
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def parse_args() -> Namespace:
|
|
12
|
+
parser = ArgumentParser(description="发送 IM 文件")
|
|
13
|
+
target = parser.add_mutually_exclusive_group(required=True)
|
|
14
|
+
target.add_argument("--account", help="接收者账号")
|
|
15
|
+
target.add_argument("--uid", help="接收者 UID")
|
|
16
|
+
target.add_argument("--group", help="接收群 ID")
|
|
17
|
+
return parser.parse_args()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def resolve_target(client: TuituiBotClient, args: Namespace) -> ToTarget:
|
|
21
|
+
if args.account:
|
|
22
|
+
return client.to.account(args.account)
|
|
23
|
+
if args.uid:
|
|
24
|
+
return client.to.uid(args.uid)
|
|
25
|
+
return client.to.group(args.group)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def main() -> None:
|
|
29
|
+
args = parse_args()
|
|
30
|
+
config = load_sample_config()
|
|
31
|
+
client = TuituiBotClient(config.app_id, config.app_secret)
|
|
32
|
+
response = await client.im.send_file(
|
|
33
|
+
to=resolve_target(client, args),
|
|
34
|
+
source=Path(__file__).with_name("README.md"),
|
|
35
|
+
)
|
|
36
|
+
print("文件发送成功")
|
|
37
|
+
print(json.dumps(response, ensure_ascii=False, indent=2))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from argparse import ArgumentParser, Namespace
|
|
4
|
+
|
|
5
|
+
from config import load_sample_config
|
|
6
|
+
|
|
7
|
+
from tuitui_bot import ToTarget, TuituiBotClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_args() -> Namespace:
|
|
11
|
+
parser = ArgumentParser(description="发送 IM 交互消息")
|
|
12
|
+
target = parser.add_mutually_exclusive_group(required=True)
|
|
13
|
+
target.add_argument("--account", help="接收者账号")
|
|
14
|
+
target.add_argument("--uid", help="接收者 UID")
|
|
15
|
+
target.add_argument("--group", help="接收群 ID")
|
|
16
|
+
return parser.parse_args()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_target(client: TuituiBotClient, args: Namespace) -> ToTarget:
|
|
20
|
+
if args.account:
|
|
21
|
+
return client.to.account(args.account)
|
|
22
|
+
if args.uid:
|
|
23
|
+
return client.to.uid(args.uid)
|
|
24
|
+
return client.to.group(args.group)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def main() -> None:
|
|
28
|
+
args = parse_args()
|
|
29
|
+
config = load_sample_config()
|
|
30
|
+
interactive = {
|
|
31
|
+
"head": {"text": "交互消息示例"},
|
|
32
|
+
"body": {"content": "请选择确认或取消"},
|
|
33
|
+
"action": [
|
|
34
|
+
{
|
|
35
|
+
"text": "确认",
|
|
36
|
+
"name": "confirm",
|
|
37
|
+
"value": "confirm",
|
|
38
|
+
"color": "FFFFFF",
|
|
39
|
+
"bgcolor": "#3873FA",
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"text": "取消",
|
|
43
|
+
"name": "cancel",
|
|
44
|
+
"value": "cancel",
|
|
45
|
+
"color": "FFFFFF",
|
|
46
|
+
"bgcolor": "#FA5151",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
client = TuituiBotClient(config.app_id, config.app_secret)
|
|
52
|
+
response = await client.im.send_interactive(
|
|
53
|
+
to=resolve_target(client, args),
|
|
54
|
+
interactive=interactive,
|
|
55
|
+
)
|
|
56
|
+
print("交互消息发送成功")
|
|
57
|
+
print(json.dumps(response, ensure_ascii=False, indent=2))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from argparse import ArgumentParser, Namespace
|
|
4
|
+
|
|
5
|
+
from config import load_sample_config
|
|
6
|
+
|
|
7
|
+
from tuitui_bot import ToTarget, TuituiBotClient, TuituiMixedItem
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_args() -> Namespace:
|
|
11
|
+
parser = ArgumentParser(description="发送 IM Mixed 消息")
|
|
12
|
+
target = parser.add_mutually_exclusive_group(required=True)
|
|
13
|
+
target.add_argument("--account", help="接收者账号")
|
|
14
|
+
target.add_argument("--uid", help="接收者 UID")
|
|
15
|
+
target.add_argument("--group", help="接收群 ID")
|
|
16
|
+
return parser.parse_args()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_target(client: TuituiBotClient, args: Namespace) -> ToTarget:
|
|
20
|
+
if args.account:
|
|
21
|
+
return client.to.account(args.account)
|
|
22
|
+
if args.uid:
|
|
23
|
+
return client.to.uid(args.uid)
|
|
24
|
+
return client.to.group(args.group)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def main() -> None:
|
|
28
|
+
args = parse_args()
|
|
29
|
+
config = load_sample_config()
|
|
30
|
+
sample_image = (
|
|
31
|
+
"data:image/png;base64,"
|
|
32
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42Y"
|
|
33
|
+
"AAAAASUVORK5CYII="
|
|
34
|
+
)
|
|
35
|
+
items: list[TuituiMixedItem] = [
|
|
36
|
+
{"type": "text", "text": "Mixed 消息中的第一段文字"},
|
|
37
|
+
{"type": "image", "source": sample_image},
|
|
38
|
+
{"type": "text", "text": "Mixed 消息中的第二段文字"},
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
client = TuituiBotClient(config.app_id, config.app_secret)
|
|
42
|
+
response = await client.im.send_mixed(to=resolve_target(client, args), items=items)
|
|
43
|
+
print("Mixed 消息发送成功")
|
|
44
|
+
print(json.dumps(response, ensure_ascii=False, indent=2))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from argparse import ArgumentParser, Namespace
|
|
4
|
+
|
|
5
|
+
from config import load_sample_config
|
|
6
|
+
|
|
7
|
+
from tuitui_bot import TuituiBotClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_args() -> Namespace:
|
|
11
|
+
parser = ArgumentParser(description="发送频道帖子")
|
|
12
|
+
parser.add_argument("--team", required=True, help="团队 ID")
|
|
13
|
+
parser.add_argument("--channel", required=True, help="频道 ID")
|
|
14
|
+
return parser.parse_args()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def main() -> None:
|
|
18
|
+
args = parse_args()
|
|
19
|
+
config = load_sample_config()
|
|
20
|
+
client = TuituiBotClient(config.app_id, config.app_secret)
|
|
21
|
+
response = await client.teams.send_post(
|
|
22
|
+
team_id=args.team,
|
|
23
|
+
channel_id=args.channel,
|
|
24
|
+
text="**Python SDK** 频道帖子",
|
|
25
|
+
)
|
|
26
|
+
print("频道帖子发送成功")
|
|
27
|
+
print(json.dumps(response, ensure_ascii=False, indent=2))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from argparse import ArgumentParser, Namespace
|
|
4
|
+
|
|
5
|
+
from config import load_sample_config
|
|
6
|
+
|
|
7
|
+
from tuitui_bot import ToTarget, TuituiBotClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def parse_args() -> Namespace:
|
|
11
|
+
parser = ArgumentParser(description="发送 IM 文本消息")
|
|
12
|
+
target = parser.add_mutually_exclusive_group(required=True)
|
|
13
|
+
target.add_argument("--account", help="接收者账号")
|
|
14
|
+
target.add_argument("--uid", help="接收者 UID")
|
|
15
|
+
target.add_argument("--group", help="接收群 ID")
|
|
16
|
+
return parser.parse_args()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def resolve_target(client: TuituiBotClient, args: Namespace) -> ToTarget:
|
|
20
|
+
if args.account:
|
|
21
|
+
return client.to.account(args.account)
|
|
22
|
+
if args.uid:
|
|
23
|
+
return client.to.uid(args.uid)
|
|
24
|
+
return client.to.group(args.group)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
async def main() -> None:
|
|
28
|
+
args = parse_args()
|
|
29
|
+
config = load_sample_config()
|
|
30
|
+
client = TuituiBotClient(config.app_id, config.app_secret)
|
|
31
|
+
response = await client.im.send_text(
|
|
32
|
+
to=resolve_target(client, args),
|
|
33
|
+
text="**你好**,这是一条来自 Python SDK 的消息。",
|
|
34
|
+
)
|
|
35
|
+
print("消息发送成功")
|
|
36
|
+
print(json.dumps(response, ensure_ascii=False, indent=2))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
asyncio.run(main())
|