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 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
@@ -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"]