karbo 0.1.0__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.
- karbo-0.1.0/.gitignore +31 -0
- karbo-0.1.0/PKG-INFO +132 -0
- karbo-0.1.0/README.md +109 -0
- karbo-0.1.0/example.py +56 -0
- karbo-0.1.0/karbo/__init__.py +34 -0
- karbo-0.1.0/karbo/client.py +250 -0
- karbo-0.1.0/karbo/errors.py +43 -0
- karbo-0.1.0/karbo/models.py +82 -0
- karbo-0.1.0/karbo/ws.py +171 -0
- karbo-0.1.0/karbo.png +0 -0
- karbo-0.1.0/pyproject.toml +37 -0
karbo-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Build outputs
|
|
2
|
+
build/
|
|
3
|
+
|
|
4
|
+
# Log files
|
|
5
|
+
*.log
|
|
6
|
+
|
|
7
|
+
# Dart/Flutter
|
|
8
|
+
.dart_tool/
|
|
9
|
+
.packages
|
|
10
|
+
.flutter-plugins
|
|
11
|
+
.flutter-plugins-dependencies
|
|
12
|
+
pubspec.lock
|
|
13
|
+
|
|
14
|
+
# IDE
|
|
15
|
+
.idea/
|
|
16
|
+
*.iml
|
|
17
|
+
.vscode/
|
|
18
|
+
|
|
19
|
+
# Python
|
|
20
|
+
__pycache__/
|
|
21
|
+
*.pyc
|
|
22
|
+
|
|
23
|
+
# Node / JavaScript (admin-panel, etc.)
|
|
24
|
+
node_modules/
|
|
25
|
+
|
|
26
|
+
# Server static files (user uploads)
|
|
27
|
+
server/KarboAI-python-v5/static/
|
|
28
|
+
|
|
29
|
+
# OS
|
|
30
|
+
.DS_Store
|
|
31
|
+
Thumbs.db
|
karbo-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: karbo
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for KarboAI Bot API
|
|
5
|
+
Project-URL: Homepage, https://karboai.com
|
|
6
|
+
Author: KarboAI
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: api,bot,karbo,karboai,sdk
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Framework :: AsyncIO
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Communications :: Chat
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: aiohttp>=3.9
|
|
21
|
+
Requires-Dist: python-socketio[asyncio-client]>=5.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# karbo
|
|
25
|
+
|
|
26
|
+
Official Python SDK for the [KarboAI](https://karboai.com) Bot API.
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install karbo
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
import asyncio
|
|
38
|
+
import karbo
|
|
39
|
+
|
|
40
|
+
BOT_TOKEN = "your-bot-token"
|
|
41
|
+
|
|
42
|
+
async def main():
|
|
43
|
+
bot = karbo.KarboBot(BOT_TOKEN)
|
|
44
|
+
ws = karbo.KarboBotWS(BOT_TOKEN)
|
|
45
|
+
|
|
46
|
+
me = await bot.get_me()
|
|
47
|
+
print(f"Bot: {me.name} ({me.bot_id})")
|
|
48
|
+
|
|
49
|
+
@ws.on_message
|
|
50
|
+
async def on_message(message: karbo.Message):
|
|
51
|
+
if message.user_id == me.bot_id:
|
|
52
|
+
return
|
|
53
|
+
await bot.send_message(message.chat_id, f"Echo: {message.content}")
|
|
54
|
+
|
|
55
|
+
await ws.run_forever()
|
|
56
|
+
|
|
57
|
+
asyncio.run(main())
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API Reference
|
|
61
|
+
|
|
62
|
+
### `KarboBot(token, *, base_url="https://api.karboai.com")`
|
|
63
|
+
|
|
64
|
+
Async HTTP client. Use as a context manager or call `.close()` when done.
|
|
65
|
+
|
|
66
|
+
| Method | Description |
|
|
67
|
+
|---|---|
|
|
68
|
+
| `get_me()` | Get bot info (`BotInfo`) |
|
|
69
|
+
| `send_message(chat_id, content, *, reply_to=None, images=None)` | Send a message with optional images (`SentMessage`) |
|
|
70
|
+
| `upload_image(file_path)` | Upload a local image, returns its URL (`str`) |
|
|
71
|
+
| `get_message(chat_id, message_id)` | Get a specific message (`Message`) |
|
|
72
|
+
| `get_chat_members(chat_id, *, limit=100, offset=0)` | List chat members (`list[Member]`) |
|
|
73
|
+
| `get_user(user_id)` | Get user profile (`User`) |
|
|
74
|
+
| `leave_chat(chat_id)` | Leave a chat |
|
|
75
|
+
| `kick_user(chat_id, user_id)` | Kick a user (requires helper role) |
|
|
76
|
+
|
|
77
|
+
#### Sending images
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
url = await bot.upload_image("photo.jpg")
|
|
81
|
+
await bot.send_message(chat_id, "Check this out!", images=[url])
|
|
82
|
+
|
|
83
|
+
# Send multiple images
|
|
84
|
+
urls = [await bot.upload_image(p) for p in ["a.png", "b.png"]]
|
|
85
|
+
await bot.send_message(chat_id, "", images=urls)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `KarboBotWS(token, *, ws_url="https://api.karboai.com")`
|
|
89
|
+
|
|
90
|
+
WebSocket client for real-time events.
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
ws = karbo.KarboBotWS(BOT_TOKEN)
|
|
94
|
+
|
|
95
|
+
@ws.on_message
|
|
96
|
+
async def handler(message: karbo.Message):
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
@ws.on_connect
|
|
100
|
+
async def connected():
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
@ws.on_disconnect
|
|
104
|
+
async def disconnected():
|
|
105
|
+
...
|
|
106
|
+
|
|
107
|
+
await ws.run_forever()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Models
|
|
111
|
+
|
|
112
|
+
- **`BotInfo`** — `bot_id`, `name`, `status`
|
|
113
|
+
- **`Message`** — `message_id`, `chat_id`, `user_id`, `content`, `created_time`, `type`, `reply_message_id`, `author`, `images`
|
|
114
|
+
- **`SentMessage`** — `message_id`, `created_time`
|
|
115
|
+
- **`User`** — `user_id`, `nickname`, `avatar`, `short_info`, `role`
|
|
116
|
+
- **`Member`** — `user_id`, `nickname`, `avatar`, `role`
|
|
117
|
+
- **`Author`** — `user_id`, `nickname`, `avatar`
|
|
118
|
+
|
|
119
|
+
### Exceptions
|
|
120
|
+
|
|
121
|
+
All inherit from `karbo.KarboError`:
|
|
122
|
+
|
|
123
|
+
- `AuthenticationError` — invalid token (401)
|
|
124
|
+
- `ForbiddenError` — no permission (403)
|
|
125
|
+
- `NotFoundError` — resource not found (404)
|
|
126
|
+
- `ValidationError` — bad request (400)
|
|
127
|
+
- `RateLimitError` — too many requests (429), has `.retry_after`
|
|
128
|
+
- `APIError` — other HTTP errors, has `.status` and `.body`
|
|
129
|
+
|
|
130
|
+
## License
|
|
131
|
+
|
|
132
|
+
MIT
|
karbo-0.1.0/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# karbo
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [KarboAI](https://karboai.com) Bot API.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install karbo
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import asyncio
|
|
15
|
+
import karbo
|
|
16
|
+
|
|
17
|
+
BOT_TOKEN = "your-bot-token"
|
|
18
|
+
|
|
19
|
+
async def main():
|
|
20
|
+
bot = karbo.KarboBot(BOT_TOKEN)
|
|
21
|
+
ws = karbo.KarboBotWS(BOT_TOKEN)
|
|
22
|
+
|
|
23
|
+
me = await bot.get_me()
|
|
24
|
+
print(f"Bot: {me.name} ({me.bot_id})")
|
|
25
|
+
|
|
26
|
+
@ws.on_message
|
|
27
|
+
async def on_message(message: karbo.Message):
|
|
28
|
+
if message.user_id == me.bot_id:
|
|
29
|
+
return
|
|
30
|
+
await bot.send_message(message.chat_id, f"Echo: {message.content}")
|
|
31
|
+
|
|
32
|
+
await ws.run_forever()
|
|
33
|
+
|
|
34
|
+
asyncio.run(main())
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API Reference
|
|
38
|
+
|
|
39
|
+
### `KarboBot(token, *, base_url="https://api.karboai.com")`
|
|
40
|
+
|
|
41
|
+
Async HTTP client. Use as a context manager or call `.close()` when done.
|
|
42
|
+
|
|
43
|
+
| Method | Description |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `get_me()` | Get bot info (`BotInfo`) |
|
|
46
|
+
| `send_message(chat_id, content, *, reply_to=None, images=None)` | Send a message with optional images (`SentMessage`) |
|
|
47
|
+
| `upload_image(file_path)` | Upload a local image, returns its URL (`str`) |
|
|
48
|
+
| `get_message(chat_id, message_id)` | Get a specific message (`Message`) |
|
|
49
|
+
| `get_chat_members(chat_id, *, limit=100, offset=0)` | List chat members (`list[Member]`) |
|
|
50
|
+
| `get_user(user_id)` | Get user profile (`User`) |
|
|
51
|
+
| `leave_chat(chat_id)` | Leave a chat |
|
|
52
|
+
| `kick_user(chat_id, user_id)` | Kick a user (requires helper role) |
|
|
53
|
+
|
|
54
|
+
#### Sending images
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
url = await bot.upload_image("photo.jpg")
|
|
58
|
+
await bot.send_message(chat_id, "Check this out!", images=[url])
|
|
59
|
+
|
|
60
|
+
# Send multiple images
|
|
61
|
+
urls = [await bot.upload_image(p) for p in ["a.png", "b.png"]]
|
|
62
|
+
await bot.send_message(chat_id, "", images=urls)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### `KarboBotWS(token, *, ws_url="https://api.karboai.com")`
|
|
66
|
+
|
|
67
|
+
WebSocket client for real-time events.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
ws = karbo.KarboBotWS(BOT_TOKEN)
|
|
71
|
+
|
|
72
|
+
@ws.on_message
|
|
73
|
+
async def handler(message: karbo.Message):
|
|
74
|
+
...
|
|
75
|
+
|
|
76
|
+
@ws.on_connect
|
|
77
|
+
async def connected():
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
@ws.on_disconnect
|
|
81
|
+
async def disconnected():
|
|
82
|
+
...
|
|
83
|
+
|
|
84
|
+
await ws.run_forever()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Models
|
|
88
|
+
|
|
89
|
+
- **`BotInfo`** — `bot_id`, `name`, `status`
|
|
90
|
+
- **`Message`** — `message_id`, `chat_id`, `user_id`, `content`, `created_time`, `type`, `reply_message_id`, `author`, `images`
|
|
91
|
+
- **`SentMessage`** — `message_id`, `created_time`
|
|
92
|
+
- **`User`** — `user_id`, `nickname`, `avatar`, `short_info`, `role`
|
|
93
|
+
- **`Member`** — `user_id`, `nickname`, `avatar`, `role`
|
|
94
|
+
- **`Author`** — `user_id`, `nickname`, `avatar`
|
|
95
|
+
|
|
96
|
+
### Exceptions
|
|
97
|
+
|
|
98
|
+
All inherit from `karbo.KarboError`:
|
|
99
|
+
|
|
100
|
+
- `AuthenticationError` — invalid token (401)
|
|
101
|
+
- `ForbiddenError` — no permission (403)
|
|
102
|
+
- `NotFoundError` — resource not found (404)
|
|
103
|
+
- `ValidationError` — bad request (400)
|
|
104
|
+
- `RateLimitError` — too many requests (429), has `.retry_after`
|
|
105
|
+
- `APIError` — other HTTP errors, has `.status` and `.body`
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
karbo-0.1.0/example.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Example: echo bot — replies with text echo + an image to every message."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import karbo
|
|
6
|
+
|
|
7
|
+
BOT_TOKEN = "your-token-here"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
async def main():
|
|
11
|
+
async with karbo.KarboBot(BOT_TOKEN) as bot:
|
|
12
|
+
ws = karbo.KarboBotWS(BOT_TOKEN)
|
|
13
|
+
|
|
14
|
+
me = await bot.get_me()
|
|
15
|
+
print(f"Logged in as {me.name} ({me.bot_id}) — status: {me.status}")
|
|
16
|
+
|
|
17
|
+
@ws.on_connect
|
|
18
|
+
async def on_connect():
|
|
19
|
+
print("WebSocket connected — listening for messages...")
|
|
20
|
+
|
|
21
|
+
@ws.on_disconnect
|
|
22
|
+
async def on_disconnect():
|
|
23
|
+
print("WebSocket disconnected")
|
|
24
|
+
|
|
25
|
+
@ws.on_message
|
|
26
|
+
async def on_message(message: karbo.Message):
|
|
27
|
+
if message.user_id == me.bot_id:
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
author_name = message.author.nickname if message.author else "Unknown"
|
|
31
|
+
print(f"[{message.chat_id}] {author_name}: {message.content}")
|
|
32
|
+
|
|
33
|
+
echo = await bot.send_message(
|
|
34
|
+
message.chat_id,
|
|
35
|
+
f"Echo: {message.content}",
|
|
36
|
+
reply_to=message.message_id,
|
|
37
|
+
)
|
|
38
|
+
print(f" → Echo reply: {echo.message_id}")
|
|
39
|
+
|
|
40
|
+
url = await bot.upload_image("karbo.png")
|
|
41
|
+
pic = await bot.send_message(
|
|
42
|
+
message.chat_id,
|
|
43
|
+
images=[url],
|
|
44
|
+
)
|
|
45
|
+
print(f" → Image sent: {pic.message_id}")
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
await ws.run_forever()
|
|
49
|
+
except KeyboardInterrupt:
|
|
50
|
+
pass
|
|
51
|
+
finally:
|
|
52
|
+
await ws.disconnect()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
if __name__ == "__main__":
|
|
56
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Karbo — Official Python SDK for KarboAI Bot API."""
|
|
2
|
+
|
|
3
|
+
from .client import KarboBot
|
|
4
|
+
from .errors import (
|
|
5
|
+
APIError,
|
|
6
|
+
AuthenticationError,
|
|
7
|
+
ForbiddenError,
|
|
8
|
+
KarboError,
|
|
9
|
+
NotFoundError,
|
|
10
|
+
RateLimitError,
|
|
11
|
+
ValidationError,
|
|
12
|
+
)
|
|
13
|
+
from .models import Author, BotInfo, Member, Message, SentMessage, User
|
|
14
|
+
from .ws import KarboBotWS
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"KarboBot",
|
|
18
|
+
"KarboBotWS",
|
|
19
|
+
"Author",
|
|
20
|
+
"BotInfo",
|
|
21
|
+
"Member",
|
|
22
|
+
"Message",
|
|
23
|
+
"SentMessage",
|
|
24
|
+
"User",
|
|
25
|
+
"KarboError",
|
|
26
|
+
"AuthenticationError",
|
|
27
|
+
"ForbiddenError",
|
|
28
|
+
"NotFoundError",
|
|
29
|
+
"RateLimitError",
|
|
30
|
+
"ValidationError",
|
|
31
|
+
"APIError",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Async HTTP client for the Karbo Bot API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
from .errors import (
|
|
10
|
+
APIError,
|
|
11
|
+
AuthenticationError,
|
|
12
|
+
ForbiddenError,
|
|
13
|
+
NotFoundError,
|
|
14
|
+
RateLimitError,
|
|
15
|
+
ValidationError,
|
|
16
|
+
)
|
|
17
|
+
from .models import Author, BotInfo, Member, Message, SentMessage, User
|
|
18
|
+
|
|
19
|
+
_DEFAULT_BASE_URL = "https://api.karboai.com"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class KarboBot:
|
|
23
|
+
"""Karbo Bot API client.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
token:
|
|
28
|
+
The bot token (``BOT_TOKEN``).
|
|
29
|
+
base_url:
|
|
30
|
+
API base URL. Defaults to ``https://api.karboai.com``.
|
|
31
|
+
session:
|
|
32
|
+
An existing ``aiohttp.ClientSession`` to reuse.
|
|
33
|
+
If not provided, a new session is created on first request.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
token: str,
|
|
39
|
+
*,
|
|
40
|
+
base_url: str = _DEFAULT_BASE_URL,
|
|
41
|
+
session: Optional[aiohttp.ClientSession] = None,
|
|
42
|
+
) -> None:
|
|
43
|
+
self._token = token
|
|
44
|
+
self._base_url = base_url.rstrip("/")
|
|
45
|
+
self._session = session
|
|
46
|
+
self._owns_session = session is None
|
|
47
|
+
|
|
48
|
+
async def _get_session(self) -> aiohttp.ClientSession:
|
|
49
|
+
if self._session is None or self._session.closed:
|
|
50
|
+
self._session = aiohttp.ClientSession(
|
|
51
|
+
headers={"Bot-Token": self._token},
|
|
52
|
+
)
|
|
53
|
+
self._owns_session = True
|
|
54
|
+
return self._session
|
|
55
|
+
|
|
56
|
+
async def close(self) -> None:
|
|
57
|
+
"""Close the underlying HTTP session (if we own it)."""
|
|
58
|
+
if self._session and not self._session.closed and self._owns_session:
|
|
59
|
+
await self._session.close()
|
|
60
|
+
|
|
61
|
+
async def __aenter__(self) -> KarboBot:
|
|
62
|
+
return self
|
|
63
|
+
|
|
64
|
+
async def __aexit__(self, *exc) -> None:
|
|
65
|
+
await self.close()
|
|
66
|
+
|
|
67
|
+
# ── internal ─────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
async def _request(
|
|
70
|
+
self,
|
|
71
|
+
method: str,
|
|
72
|
+
path: str,
|
|
73
|
+
*,
|
|
74
|
+
json: dict | None = None,
|
|
75
|
+
params: dict | None = None,
|
|
76
|
+
) -> dict:
|
|
77
|
+
session = await self._get_session()
|
|
78
|
+
url = f"{self._base_url}{path}"
|
|
79
|
+
|
|
80
|
+
async with session.request(method, url, json=json, params=params) as resp:
|
|
81
|
+
if resp.status == 401:
|
|
82
|
+
raise AuthenticationError(await resp.text())
|
|
83
|
+
if resp.status == 403:
|
|
84
|
+
raise ForbiddenError(await resp.text())
|
|
85
|
+
if resp.status == 404:
|
|
86
|
+
raise NotFoundError(await resp.text())
|
|
87
|
+
if resp.status == 400:
|
|
88
|
+
raise ValidationError(await resp.text())
|
|
89
|
+
if resp.status == 429:
|
|
90
|
+
retry = resp.headers.get("Retry-After")
|
|
91
|
+
raise RateLimitError(float(retry) if retry else None)
|
|
92
|
+
if resp.status >= 400:
|
|
93
|
+
raise APIError(resp.status, await resp.text())
|
|
94
|
+
return await resp.json()
|
|
95
|
+
|
|
96
|
+
# ── Bot info ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
async def get_me(self) -> BotInfo:
|
|
99
|
+
"""Get information about the authenticated bot."""
|
|
100
|
+
data = await self._request("GET", "/bot/me")
|
|
101
|
+
return BotInfo(
|
|
102
|
+
bot_id=data["bot_id"],
|
|
103
|
+
name=data["name"],
|
|
104
|
+
status=data["status"],
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# ── Messages ─────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
async def send_message(
|
|
110
|
+
self,
|
|
111
|
+
chat_id: str,
|
|
112
|
+
content: str = "",
|
|
113
|
+
*,
|
|
114
|
+
reply_to: Optional[str] = None,
|
|
115
|
+
images: Optional[list[str]] = None,
|
|
116
|
+
) -> SentMessage:
|
|
117
|
+
"""Send a message to a chat.
|
|
118
|
+
|
|
119
|
+
Parameters
|
|
120
|
+
----------
|
|
121
|
+
chat_id:
|
|
122
|
+
The chat to send the message to.
|
|
123
|
+
content:
|
|
124
|
+
Message text (up to 5000 characters). Can be empty if images are provided.
|
|
125
|
+
reply_to:
|
|
126
|
+
Optional ``message_id`` to reply to.
|
|
127
|
+
images:
|
|
128
|
+
Optional list of image URLs (from :meth:`upload_image`).
|
|
129
|
+
"""
|
|
130
|
+
payload: dict = {"chat_id": chat_id, "content": content}
|
|
131
|
+
if reply_to is not None:
|
|
132
|
+
payload["reply_message_id"] = reply_to
|
|
133
|
+
if images:
|
|
134
|
+
payload["images"] = images
|
|
135
|
+
data = await self._request("POST", "/bot/send-message", json=payload)
|
|
136
|
+
return SentMessage(
|
|
137
|
+
message_id=data["message_id"],
|
|
138
|
+
created_time=data["created_time"],
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
async def upload_image(self, file_path: str) -> str:
|
|
142
|
+
"""Upload an image file and return its URL.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
file_path:
|
|
147
|
+
Local path to the image file (.jpg, .png, .webp, .gif).
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
-------
|
|
151
|
+
str
|
|
152
|
+
The public URL of the uploaded image. Pass this to
|
|
153
|
+
:meth:`send_message` via the ``images`` parameter.
|
|
154
|
+
"""
|
|
155
|
+
session = await self._get_session()
|
|
156
|
+
url = f"{self._base_url}/bot/upload/image"
|
|
157
|
+
filename = file_path.rsplit("/", 1)[-1].rsplit("\\", 1)[-1]
|
|
158
|
+
|
|
159
|
+
with open(file_path, "rb") as fh:
|
|
160
|
+
form = aiohttp.FormData()
|
|
161
|
+
form.add_field("file", fh, filename=filename)
|
|
162
|
+
|
|
163
|
+
async with session.post(url, data=form) as resp:
|
|
164
|
+
if resp.status == 401:
|
|
165
|
+
raise AuthenticationError(await resp.text())
|
|
166
|
+
if resp.status == 403:
|
|
167
|
+
raise ForbiddenError(await resp.text())
|
|
168
|
+
if resp.status == 413:
|
|
169
|
+
raise ValidationError("File too large (max 20 MB)")
|
|
170
|
+
if resp.status >= 400:
|
|
171
|
+
raise APIError(resp.status, await resp.text())
|
|
172
|
+
data = await resp.json()
|
|
173
|
+
return data["url"]
|
|
174
|
+
|
|
175
|
+
async def get_message(self, chat_id: str, message_id: str) -> Message:
|
|
176
|
+
"""Get a specific message from a chat."""
|
|
177
|
+
data = await self._request("GET", f"/bot/chat/{chat_id}/message/{message_id}")
|
|
178
|
+
author = None
|
|
179
|
+
if "author" in data:
|
|
180
|
+
a = data["author"]
|
|
181
|
+
author = Author(
|
|
182
|
+
user_id=a["user_id"],
|
|
183
|
+
nickname=a["nickname"],
|
|
184
|
+
avatar=a.get("avatar", ""),
|
|
185
|
+
)
|
|
186
|
+
return Message(
|
|
187
|
+
message_id=data["message_id"],
|
|
188
|
+
chat_id=data["chat_id"],
|
|
189
|
+
user_id=data["user_id"],
|
|
190
|
+
content=data["content"],
|
|
191
|
+
created_time=data["created_time"],
|
|
192
|
+
type=data["type"],
|
|
193
|
+
reply_message_id=data.get("reply_message_id"),
|
|
194
|
+
author=author,
|
|
195
|
+
audio=data.get("audio"),
|
|
196
|
+
sticker=data.get("sticker"),
|
|
197
|
+
images=data.get("images", []),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# ── Chat members ─────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
async def get_chat_members(
|
|
203
|
+
self,
|
|
204
|
+
chat_id: str,
|
|
205
|
+
*,
|
|
206
|
+
limit: int = 100,
|
|
207
|
+
offset: int = 0,
|
|
208
|
+
) -> list[Member]:
|
|
209
|
+
"""List members of a chat the bot is in."""
|
|
210
|
+
data = await self._request(
|
|
211
|
+
"GET",
|
|
212
|
+
f"/bot/chat/{chat_id}/members",
|
|
213
|
+
params={"limit": limit, "offset": offset},
|
|
214
|
+
)
|
|
215
|
+
return [
|
|
216
|
+
Member(
|
|
217
|
+
user_id=m["user_id"],
|
|
218
|
+
nickname=m["nickname"],
|
|
219
|
+
avatar=m["avatar"],
|
|
220
|
+
role=m.get("role", 0),
|
|
221
|
+
)
|
|
222
|
+
for m in data["items"]
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
# ── User profiles ────────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
async def get_user(self, user_id: str) -> User:
|
|
228
|
+
"""Get a user's public profile."""
|
|
229
|
+
data = await self._request("GET", f"/bot/user/{user_id}")
|
|
230
|
+
return User(
|
|
231
|
+
user_id=data["user_id"],
|
|
232
|
+
nickname=data["nickname"],
|
|
233
|
+
avatar=data["avatar"],
|
|
234
|
+
short_info=data.get("short_info", ""),
|
|
235
|
+
role=data.get("role", 0),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# ── Chat actions ─────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
async def leave_chat(self, chat_id: str) -> None:
|
|
241
|
+
"""Leave a chat."""
|
|
242
|
+
await self._request("POST", f"/bot/leave-chat/{chat_id}")
|
|
243
|
+
|
|
244
|
+
async def kick_user(self, chat_id: str, user_id: str) -> None:
|
|
245
|
+
"""Kick a user from a chat (requires helper role)."""
|
|
246
|
+
await self._request(
|
|
247
|
+
"POST",
|
|
248
|
+
f"/bot/chat/{chat_id}/kick",
|
|
249
|
+
json={"user_id": user_id},
|
|
250
|
+
)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Exception hierarchy for the Karbo SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class KarboError(Exception):
|
|
7
|
+
"""Base exception for all Karbo SDK errors."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuthenticationError(KarboError):
|
|
11
|
+
"""Invalid or missing bot token."""
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ForbiddenError(KarboError):
|
|
15
|
+
"""The bot does not have permission to perform this action."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class NotFoundError(KarboError):
|
|
19
|
+
"""The requested resource was not found."""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ValidationError(KarboError):
|
|
23
|
+
"""The request payload is invalid."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class RateLimitError(KarboError):
|
|
27
|
+
"""Rate limit exceeded."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, retry_after: float | None = None):
|
|
30
|
+
self.retry_after = retry_after
|
|
31
|
+
msg = "Rate limit exceeded"
|
|
32
|
+
if retry_after is not None:
|
|
33
|
+
msg += f" (retry after {retry_after}s)"
|
|
34
|
+
super().__init__(msg)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class APIError(KarboError):
|
|
38
|
+
"""Unexpected API error."""
|
|
39
|
+
|
|
40
|
+
def __init__(self, status: int, body: str):
|
|
41
|
+
self.status = status
|
|
42
|
+
self.body = body
|
|
43
|
+
super().__init__(f"HTTP {status}: {body}")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Data models returned by the Karbo Bot API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True, slots=True)
|
|
10
|
+
class BotInfo:
|
|
11
|
+
"""Information about the authenticated bot (/bot/me)."""
|
|
12
|
+
|
|
13
|
+
bot_id: str
|
|
14
|
+
name: str
|
|
15
|
+
status: str # "not_official" | "official" | "banned"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True, slots=True)
|
|
19
|
+
class User:
|
|
20
|
+
"""Public user profile."""
|
|
21
|
+
|
|
22
|
+
user_id: str
|
|
23
|
+
nickname: str
|
|
24
|
+
avatar: str
|
|
25
|
+
short_info: str = ""
|
|
26
|
+
role: int = 0
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class Member:
|
|
31
|
+
"""Chat member entry."""
|
|
32
|
+
|
|
33
|
+
user_id: str
|
|
34
|
+
nickname: str
|
|
35
|
+
avatar: str
|
|
36
|
+
role: int = 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class Author:
|
|
41
|
+
"""Message author info embedded in a message."""
|
|
42
|
+
|
|
43
|
+
user_id: str
|
|
44
|
+
nickname: str
|
|
45
|
+
avatar: str = ""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True, slots=True)
|
|
49
|
+
class Message:
|
|
50
|
+
"""A single chat message.
|
|
51
|
+
|
|
52
|
+
Attributes
|
|
53
|
+
----------
|
|
54
|
+
type:
|
|
55
|
+
0 = normal text, other values = system messages.
|
|
56
|
+
audio:
|
|
57
|
+
URL of a voice message, if present.
|
|
58
|
+
sticker:
|
|
59
|
+
Sticker identifier or URL, if present.
|
|
60
|
+
images:
|
|
61
|
+
List of image URLs attached to the message.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
message_id: str
|
|
65
|
+
chat_id: str
|
|
66
|
+
user_id: str
|
|
67
|
+
content: str
|
|
68
|
+
created_time: int
|
|
69
|
+
type: int
|
|
70
|
+
reply_message_id: Optional[str] = None
|
|
71
|
+
author: Optional[Author] = None
|
|
72
|
+
audio: Optional[str] = None
|
|
73
|
+
sticker: Optional[str] = None
|
|
74
|
+
images: list[str] = field(default_factory=list)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True, slots=True)
|
|
78
|
+
class SentMessage:
|
|
79
|
+
"""Result of sending a message."""
|
|
80
|
+
|
|
81
|
+
message_id: str
|
|
82
|
+
created_time: int
|
karbo-0.1.0/karbo/ws.py
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"""WebSocket listener for real-time events from the Karbo Bot API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Awaitable, Callable, Optional
|
|
7
|
+
|
|
8
|
+
import socketio
|
|
9
|
+
|
|
10
|
+
from .models import Author, Message
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger("karbo.ws")
|
|
13
|
+
|
|
14
|
+
_DEFAULT_WS_URL = "https://api.karboai.com"
|
|
15
|
+
|
|
16
|
+
EventHandler = Callable[..., Awaitable[None]]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class KarboBotWS:
|
|
20
|
+
"""Real-time WebSocket client for receiving chat events.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
token:
|
|
25
|
+
The bot token.
|
|
26
|
+
ws_url:
|
|
27
|
+
WebSocket base URL. Defaults to ``https://api.karboai.com``.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
token: str,
|
|
33
|
+
*,
|
|
34
|
+
ws_url: str = _DEFAULT_WS_URL,
|
|
35
|
+
) -> None:
|
|
36
|
+
self._token = token
|
|
37
|
+
self._ws_url = ws_url.rstrip("/")
|
|
38
|
+
|
|
39
|
+
self._sio = socketio.AsyncClient(
|
|
40
|
+
reconnection=True,
|
|
41
|
+
reconnection_attempts=0, # unlimited
|
|
42
|
+
reconnection_delay=1,
|
|
43
|
+
reconnection_delay_max=30,
|
|
44
|
+
logger=False,
|
|
45
|
+
engineio_logger=False,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self._on_message: Optional[Callable[[Message], Awaitable[None]]] = None
|
|
49
|
+
self._on_connect: Optional[Callable[[], Awaitable[None]]] = None
|
|
50
|
+
self._on_disconnect: Optional[Callable[[], Awaitable[None]]] = None
|
|
51
|
+
self._on_raw: dict[str, list[EventHandler]] = {}
|
|
52
|
+
|
|
53
|
+
self._setup_internal_handlers()
|
|
54
|
+
|
|
55
|
+
def _setup_internal_handlers(self) -> None:
|
|
56
|
+
@self._sio.event
|
|
57
|
+
async def connect():
|
|
58
|
+
logger.info("Connected to Karbo WebSocket")
|
|
59
|
+
if self._on_connect:
|
|
60
|
+
await self._on_connect()
|
|
61
|
+
|
|
62
|
+
@self._sio.event
|
|
63
|
+
async def disconnect():
|
|
64
|
+
logger.info("Disconnected from Karbo WebSocket")
|
|
65
|
+
if self._on_disconnect:
|
|
66
|
+
await self._on_disconnect()
|
|
67
|
+
|
|
68
|
+
@self._sio.on("new_message")
|
|
69
|
+
async def on_new_message(data: dict):
|
|
70
|
+
msg = _parse_message(data)
|
|
71
|
+
if self._on_message:
|
|
72
|
+
await self._on_message(msg)
|
|
73
|
+
for handler in self._on_raw.get("new_message", []):
|
|
74
|
+
await handler(data)
|
|
75
|
+
|
|
76
|
+
# ── decorator API ────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
def on_message(
|
|
79
|
+
self, func: Callable[[Message], Awaitable[None]],
|
|
80
|
+
) -> Callable[[Message], Awaitable[None]]:
|
|
81
|
+
"""Register a handler for incoming chat messages.
|
|
82
|
+
|
|
83
|
+
Usage::
|
|
84
|
+
|
|
85
|
+
@ws.on_message
|
|
86
|
+
async def handle(message: karbo.Message):
|
|
87
|
+
print(message.content)
|
|
88
|
+
"""
|
|
89
|
+
self._on_message = func
|
|
90
|
+
return func
|
|
91
|
+
|
|
92
|
+
def on_connect(
|
|
93
|
+
self, func: Callable[[], Awaitable[None]],
|
|
94
|
+
) -> Callable[[], Awaitable[None]]:
|
|
95
|
+
"""Register a handler called when WebSocket connects."""
|
|
96
|
+
self._on_connect = func
|
|
97
|
+
return func
|
|
98
|
+
|
|
99
|
+
def on_disconnect(
|
|
100
|
+
self, func: Callable[[], Awaitable[None]],
|
|
101
|
+
) -> Callable[[], Awaitable[None]]:
|
|
102
|
+
"""Register a handler called when WebSocket disconnects."""
|
|
103
|
+
self._on_disconnect = func
|
|
104
|
+
return func
|
|
105
|
+
|
|
106
|
+
def on(self, event: str) -> Callable[[EventHandler], EventHandler]:
|
|
107
|
+
"""Register a raw event handler.
|
|
108
|
+
|
|
109
|
+
Usage::
|
|
110
|
+
|
|
111
|
+
@ws.on("custom_event")
|
|
112
|
+
async def handle(data):
|
|
113
|
+
...
|
|
114
|
+
"""
|
|
115
|
+
def decorator(func: EventHandler) -> EventHandler:
|
|
116
|
+
self._on_raw.setdefault(event, []).append(func)
|
|
117
|
+
|
|
118
|
+
@self._sio.on(event)
|
|
119
|
+
async def _wrapper(data: Any) -> None:
|
|
120
|
+
await func(data)
|
|
121
|
+
|
|
122
|
+
return func
|
|
123
|
+
return decorator
|
|
124
|
+
|
|
125
|
+
# ── lifecycle ────────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
async def connect(self) -> None:
|
|
128
|
+
"""Connect to the Karbo WebSocket server."""
|
|
129
|
+
await self._sio.connect(
|
|
130
|
+
self._ws_url,
|
|
131
|
+
socketio_path="/bot/ws",
|
|
132
|
+
auth={"bot_token": self._token},
|
|
133
|
+
transports=["websocket"],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
async def disconnect(self) -> None:
|
|
137
|
+
"""Disconnect from the WebSocket server."""
|
|
138
|
+
await self._sio.disconnect()
|
|
139
|
+
|
|
140
|
+
async def wait(self) -> None:
|
|
141
|
+
"""Block until the connection is closed."""
|
|
142
|
+
await self._sio.wait()
|
|
143
|
+
|
|
144
|
+
async def run_forever(self) -> None:
|
|
145
|
+
"""Connect and block until disconnected. Convenience wrapper."""
|
|
146
|
+
await self.connect()
|
|
147
|
+
await self.wait()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _parse_message(data: dict) -> Message:
|
|
151
|
+
author = None
|
|
152
|
+
if "author" in data and isinstance(data["author"], dict):
|
|
153
|
+
a = data["author"]
|
|
154
|
+
author = Author(
|
|
155
|
+
user_id=a.get("user_id", ""),
|
|
156
|
+
nickname=a.get("nickname", "User"),
|
|
157
|
+
avatar=a.get("avatar", ""),
|
|
158
|
+
)
|
|
159
|
+
return Message(
|
|
160
|
+
message_id=data.get("message_id", ""),
|
|
161
|
+
chat_id=data.get("chat_id", ""),
|
|
162
|
+
user_id=data.get("user_id", ""),
|
|
163
|
+
content=data.get("content", ""),
|
|
164
|
+
created_time=data.get("created_time", 0),
|
|
165
|
+
type=data.get("type", 0),
|
|
166
|
+
reply_message_id=data.get("reply_message_id"),
|
|
167
|
+
author=author,
|
|
168
|
+
audio=data.get("audio"),
|
|
169
|
+
sticker=data.get("sticker"),
|
|
170
|
+
images=data.get("images", []),
|
|
171
|
+
)
|
karbo-0.1.0/karbo.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "karbo"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Official Python SDK for KarboAI Bot API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "KarboAI" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["karbo", "karboai", "bot", "api", "sdk"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
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
|
+
"Framework :: AsyncIO",
|
|
26
|
+
"Topic :: Communications :: Chat",
|
|
27
|
+
]
|
|
28
|
+
dependencies = [
|
|
29
|
+
"aiohttp>=3.9",
|
|
30
|
+
"python-socketio[asyncio_client]>=5.10",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://karboai.com"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["karbo"]
|