realchat-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.
realchat/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from realchat.client import Client
2
+ from realchat.models import Message, Server, Channel, User, Bot
3
+
4
+ __version__ = "0.1.0"
5
+ __all__ = ["Client", "Message", "Server", "Channel", "User", "Bot"]
realchat/client.py ADDED
@@ -0,0 +1,152 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ import logging
6
+ from typing import Any, Callable, Coroutine, Optional
7
+
8
+ import websockets
9
+ from websockets.client import WebSocketClientProtocol
10
+
11
+ from realchat.http import HTTPClient, RealChatAPIError
12
+ from realchat.models import Message, Server, Channel, User
13
+
14
+ logger = logging.getLogger("realchat")
15
+
16
+ Callback = Callable[..., Coroutine[Any, Any, Any]]
17
+
18
+
19
+ class Client:
20
+ """Main client for interacting with RealChat as a bot.
21
+
22
+ Usage:
23
+ from realchat import Client
24
+
25
+ client = Client(token="RC.your_bot_token_here")
26
+
27
+ @client.event("on_ready")
28
+ async def ready():
29
+ print(f"Logged in as {client.user.username}")
30
+
31
+ @client.event("on_message")
32
+ async def on_message(message: Message):
33
+ if message.content == "!ping":
34
+ await message.channel.send("Pong!")
35
+
36
+ await client.start()
37
+ """
38
+
39
+ def __init__(
40
+ self,
41
+ token: str,
42
+ *,
43
+ api_url: str = "https://realchat-server.onrender.com/api",
44
+ ws_url: str = "wss://realchat-server.onrender.com",
45
+ ):
46
+ self.token = token
47
+ self.http = HTTPClient(api_url, token)
48
+ self.user: Optional[User] = None
49
+ self._ws: Optional[WebSocketClientProtocol] = None
50
+ self._events: dict[str, list[Callback]] = {}
51
+ self._running = False
52
+
53
+ def event(self, name: str) -> Callable[[Callback], Callback]:
54
+ """Decorator to register an event handler.
55
+
56
+ Events:
57
+ on_ready - Called when connected
58
+ on_message - Called when a message is received
59
+ on_disconnect - Called when disconnected
60
+ """
61
+ def decorator(func: Callback) -> Callback:
62
+ self._events.setdefault(name, []).append(func)
63
+ return func
64
+ return decorator
65
+
66
+ async def start(self):
67
+ """Start the bot. Connects to API and WebSocket."""
68
+ self.user = await self.http.get_me()
69
+ logger.info(f"Authenticated as {self.user.username} (id: {self.user.id})")
70
+ await self._dispatch("on_ready")
71
+ self._running = True
72
+ await self._connect_ws()
73
+
74
+ async def close(self):
75
+ """Disconnect and clean up."""
76
+ self._running = False
77
+ if self._ws:
78
+ await self._ws.close()
79
+ await self.http.close()
80
+
81
+ async def _connect_ws(self):
82
+ """Connect to the WebSocket server and listen for events."""
83
+ while self._running:
84
+ try:
85
+ async with websockets.connect(
86
+ self.ws_url,
87
+ additional_headers={"Cookie": f"token={self.token}"},
88
+ ) as ws:
89
+ self._ws = ws
90
+ logger.info("WebSocket connected")
91
+
92
+ # Authenticate
93
+ await ws.send(json.dumps({
94
+ "type": "auth",
95
+ "token": self.token,
96
+ }))
97
+
98
+ async for raw in ws:
99
+ try:
100
+ data = json.loads(raw)
101
+ await self._handle_event(data)
102
+ except json.JSONDecodeError:
103
+ logger.warning(f"Invalid JSON: {raw}")
104
+
105
+ except websockets.ConnectionClosed:
106
+ logger.warning("WebSocket disconnected, reconnecting in 5s...")
107
+ await self._dispatch("on_disconnect")
108
+ await asyncio.sleep(5)
109
+ except Exception as e:
110
+ logger.error(f"WebSocket error: {e}")
111
+ await self._dispatch("on_disconnect")
112
+ if self._running:
113
+ await asyncio.sleep(5)
114
+
115
+ async def _handle_event(self, data: dict):
116
+ """Route incoming WebSocket events to handlers."""
117
+ event_type = data.get("type")
118
+
119
+ if event_type == "message":
120
+ msg = Message.from_dict(data.get("message", data))
121
+ await self._dispatch("on_message", msg)
122
+
123
+ elif event_type == "typing":
124
+ user_id = data.get("userId")
125
+ channel_id = data.get("channelId")
126
+ await self._dispatch("on_typing", user_id, channel_id)
127
+
128
+ elif event_type == "user_status":
129
+ user_id = data.get("userId")
130
+ status = data.get("status")
131
+ await self._dispatch("on_presence", user_id, status)
132
+
133
+ elif event_type == "error":
134
+ logger.error(f"Server error: {data.get('message', 'Unknown error')}")
135
+
136
+ async def _dispatch(self, event_name: str, *args):
137
+ """Call all registered handlers for an event."""
138
+ for handler in self._events.get(event_name, []):
139
+ try:
140
+ await handler(*args)
141
+ except Exception as e:
142
+ logger.error(f"Error in {event_name} handler: {e}")
143
+
144
+ async def get_servers(self) -> list[Server]:
145
+ return await self.http.get_servers()
146
+
147
+ async def get_channels(self, server_id: str) -> list[Channel]:
148
+ return await self.http.get_channels(server_id)
149
+
150
+ async def send(self, channel_id: str, content: str) -> Message:
151
+ """Send a message to a channel."""
152
+ return await self.http.send_message(channel_id, content)
realchat/http.py ADDED
@@ -0,0 +1,77 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Optional
4
+ import aiohttp
5
+
6
+ from realchat.models import User, Server, Channel, Message
7
+
8
+
9
+ class HTTPClient:
10
+ def __init__(self, base_url: str, token: str):
11
+ self.base_url = base_url.rstrip("/")
12
+ self.token = token
13
+ self._session: Optional[aiohttp.ClientSession] = None
14
+
15
+ async def _get_session(self) -> aiohttp.ClientSession:
16
+ if self._session is None or self._session.closed:
17
+ self._session = aiohttp.ClientSession(
18
+ headers={"Authorization": f"Bot {self.token}"},
19
+ timeout=aiohttp.ClientTimeout(total=30),
20
+ )
21
+ return self._session
22
+
23
+ async def close(self):
24
+ if self._session and not self._session.closed:
25
+ await self._session.close()
26
+
27
+ async def _request(self, method: str, path: str, **kwargs) -> Any:
28
+ session = await self._get_session()
29
+ url = f"{self.base_url}{path}"
30
+ async with session.request(method, url, **kwargs) as resp:
31
+ data = await resp.json()
32
+ if resp.status >= 400:
33
+ error_msg = data.get("error", f"HTTP {resp.status}")
34
+ raise RealChatAPIError(resp.status, error_msg)
35
+ return data
36
+
37
+ async def get_me(self) -> User:
38
+ data = await self._request("GET", "/auth/me")
39
+ return User.from_dict(data["user"])
40
+
41
+ async def get_servers(self) -> list[Server]:
42
+ data = await self._request("GET", "/servers")
43
+ return [Server.from_dict(s) for s in data["servers"]]
44
+
45
+ async def get_server(self, server_id: str) -> Server:
46
+ data = await self._request("GET", f"/servers/{server_id}")
47
+ return Server.from_dict(data["server"])
48
+
49
+ async def get_channels(self, server_id: str) -> list[Channel]:
50
+ data = await self._request("GET", f"/servers/{server_id}")
51
+ return [Channel.from_dict(c) for c in data.get("channels", [])]
52
+
53
+ async def create_channel(self, server_id: str, name: str) -> Channel:
54
+ data = await self._request("POST", "/channels", json={"serverId": server_id, "name": name})
55
+ return Channel.from_dict(data["channel"])
56
+
57
+ async def get_messages(self, channel_id: str, limit: int = 50, before: Optional[str] = None) -> list[Message]:
58
+ params = {"limit": str(limit)}
59
+ if before:
60
+ params["before"] = before
61
+ data = await self._request("GET", f"/messages/{channel_id}", params=params)
62
+ return [Message.from_dict(m) for m in data["messages"]]
63
+
64
+ async def send_message(self, channel_id: str, content: str) -> Message:
65
+ data = await self._request("POST", f"/messages/{channel_id}", json={"content": content})
66
+ return Message.from_dict(data["message"])
67
+
68
+ async def delete_message(self, channel_id: str, message_id: str) -> bool:
69
+ await self._request("DELETE", f"/messages/{channel_id}/{message_id}")
70
+ return True
71
+
72
+
73
+ class RealChatAPIError(Exception):
74
+ def __init__(self, status: int, message: str):
75
+ self.status = status
76
+ self.message = message
77
+ super().__init__(f"[{status}] {message}")
realchat/models.py ADDED
@@ -0,0 +1,144 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from datetime import datetime
5
+ from typing import TYPE_CHECKING, Optional
6
+
7
+ if TYPE_CHECKING:
8
+ from realchat.client import Client
9
+
10
+
11
+ @dataclass
12
+ class User:
13
+ id: str
14
+ email: str
15
+ username: str
16
+ nickname: Optional[str] = None
17
+ avatar: Optional[str] = None
18
+ role: str = "user"
19
+ status: str = "online"
20
+ kids_mode: bool = False
21
+ created_at: Optional[datetime] = None
22
+
23
+ @classmethod
24
+ def from_dict(cls, data: dict) -> User:
25
+ return cls(
26
+ id=data["id"],
27
+ email=data.get("email", ""),
28
+ username=data["username"],
29
+ nickname=data.get("nickname"),
30
+ avatar=data.get("avatar"),
31
+ role=data.get("role", "user"),
32
+ status=data.get("status", "online"),
33
+ kids_mode=data.get("kidsMode", False),
34
+ created_at=_parse_dt(data.get("createdAt")),
35
+ )
36
+
37
+
38
+ @dataclass
39
+ class Server:
40
+ id: str
41
+ name: str
42
+ icon: Optional[str] = None
43
+ invite_code: str = ""
44
+ owner_id: str = ""
45
+ created_at: Optional[datetime] = None
46
+
47
+ @classmethod
48
+ def from_dict(cls, data: dict) -> Server:
49
+ return cls(
50
+ id=data["id"],
51
+ name=data["name"],
52
+ icon=data.get("icon"),
53
+ invite_code=data.get("inviteCode", ""),
54
+ owner_id=data.get("ownerId", ""),
55
+ created_at=_parse_dt(data.get("createdAt")),
56
+ )
57
+
58
+
59
+ @dataclass
60
+ class Channel:
61
+ id: str
62
+ name: str
63
+ type: str = "text"
64
+ server_id: str = ""
65
+ created_at: Optional[datetime] = None
66
+
67
+ @classmethod
68
+ def from_dict(cls, data: dict) -> Channel:
69
+ return cls(
70
+ id=data["id"],
71
+ name=data["name"],
72
+ type=data.get("type", "text"),
73
+ server_id=data.get("serverId", ""),
74
+ created_at=_parse_dt(data.get("createdAt")),
75
+ )
76
+
77
+
78
+ @dataclass
79
+ class Message:
80
+ id: str
81
+ content: str
82
+ author: Optional[User] = None
83
+ channel_id: str = ""
84
+ edited: bool = False
85
+ created_at: Optional[datetime] = None
86
+
87
+ @classmethod
88
+ def from_dict(cls, data: dict) -> Message:
89
+ author = None
90
+ if "author" in data and data["author"]:
91
+ author = User.from_dict(data["author"])
92
+ return cls(
93
+ id=data["id"],
94
+ content=data["content"],
95
+ author=author,
96
+ channel_id=data.get("channelId", ""),
97
+ edited=data.get("edited", False),
98
+ created_at=_parse_dt(data.get("createdAt")),
99
+ )
100
+
101
+
102
+ @dataclass
103
+ class Bot:
104
+ id: str
105
+ name: str
106
+ description: Optional[str] = None
107
+ token: str = ""
108
+ secret: str = ""
109
+ public: bool = False
110
+ owner_id: str = ""
111
+ created_at: Optional[datetime] = None
112
+
113
+ @classmethod
114
+ def from_dict(cls, data: dict) -> Bot:
115
+ return cls(
116
+ id=data["id"],
117
+ name=data["name"],
118
+ description=data.get("description"),
119
+ token=data.get("token", ""),
120
+ secret=data.get("secret", ""),
121
+ public=data.get("public", False),
122
+ owner_id=data.get("ownerId", ""),
123
+ created_at=_parse_dt(data.get("createdAt")),
124
+ )
125
+
126
+
127
+ def _parse_dt(value: Optional[str]) -> Optional[datetime]:
128
+ if not value:
129
+ return None
130
+ try:
131
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
132
+ except (ValueError, TypeError):
133
+ return None
134
+
135
+
136
+ class ChannelProxy:
137
+ """Proxy that lets you do message.channel.send("content")."""
138
+
139
+ def __init__(self, channel_id: str, client: "Client"):
140
+ self.id = channel_id
141
+ self._client = client
142
+
143
+ async def send(self, content: str) -> "Message":
144
+ return await self._client.send(self.id, content)
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: realchat-sdk
3
+ Version: 0.1.0
4
+ Summary: Python SDK for building bots on the RealChat platform
5
+ Project-URL: Homepage, https://github.com/838288383838383/realchat-sdk
6
+ Project-URL: Documentation, https://github.com/838288383838383/realchat-sdk#readme
7
+ Project-URL: Repository, https://github.com/838288383838383/realchat-sdk
8
+ Project-URL: Issues, https://github.com/838288383838383/realchat-sdk/issues
9
+ Author-email: Real Inc <real@realinc.dev>
10
+ License-Expression: MIT
11
+ Keywords: bot,chat,realchat,sdk,websocket
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Topic :: Communications :: Chat
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: aiohttp>=3.14.1
23
+ Requires-Dist: websockets>=16.0
24
+ Description-Content-Type: text/markdown
25
+
26
+ # realchat-sdk
27
+
28
+ Python SDK for building bots on the [RealChat](https://realchat.vercel.app) platform.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install realchat-sdk
34
+ ```
35
+
36
+ ## Quick Start
37
+
38
+ ```python
39
+ import asyncio
40
+ from realchat import Client
41
+
42
+ client = Client(token="RC.your_bot_token_here")
43
+
44
+ @client.event("on_ready")
45
+ async def on_ready():
46
+ print(f"Logged in as {client.user.username}")
47
+
48
+ @client.event("on_message")
49
+ async def on_message(message):
50
+ if message.content == "!ping":
51
+ await message.channel.send("Pong!")
52
+
53
+ if message.content == "!hello":
54
+ await message.channel.send(f"Hello {message.author.username}!")
55
+
56
+ asyncio.run(client.start())
57
+ ```
58
+
59
+ ## Events
60
+
61
+ | Event | Description |
62
+ |-------|-------------|
63
+ | `on_ready` | Bot connected and authenticated |
64
+ | `on_message` | Message received in any channel |
65
+ | `on_typing` | User started typing |
66
+ | `on_presence` | User status changed |
67
+ | `on_disconnect` | WebSocket disconnected |
68
+
69
+ ## REST API
70
+
71
+ ```python
72
+ from realchat import Client
73
+
74
+ client = Client(token="RC.your_bot_token_here")
75
+
76
+ async def main():
77
+ # Get bot's user info
78
+ user = await client.http.get_me()
79
+ print(f"I am {user.username}")
80
+
81
+ # List servers the bot is in
82
+ servers = await client.http.get_servers()
83
+ for server in servers:
84
+ print(f" - {server.name} ({server.id})")
85
+
86
+ # Get channels in a server
87
+ channels = await client.http.get_channels(servers[0].id)
88
+ for channel in channels:
89
+ print(f" # {channel.name}")
90
+
91
+ # Send a message
92
+ await client.http.send_message(channel.id, "Hello from my bot!")
93
+
94
+ # Read messages
95
+ messages = await client.http.get_messages(channel.id, limit=10)
96
+ for msg in messages:
97
+ print(f" {msg.author.username}: {msg.content}")
98
+
99
+ await client.close()
100
+
101
+ import asyncio
102
+ asyncio.run(main())
103
+ ```
104
+
105
+ ## Getting a Bot Token
106
+
107
+ 1. Go to the [Developer Portal](https://realchat.vercel.app/developer)
108
+ 2. Create a new bot
109
+ 3. Copy the bot token (starts with `RC.`)
110
+ 4. Use it in your code
111
+
112
+ ## License
113
+
114
+ MIT
@@ -0,0 +1,7 @@
1
+ realchat/__init__.py,sha256=xs5KjwENBaJl_5-vCcpUMtW58NTC8Y3Ki7He7oUsbdU,190
2
+ realchat/client.py,sha256=q8qN6d5Emog8nc4k7rvacwgsbwI0NiN2qwoGRgLwfTs,5224
3
+ realchat/http.py,sha256=okSaCkKCDXBs4aSOqeSDQdmlAYj28FfeOc-sQDYWZIc,3121
4
+ realchat/models.py,sha256=xcEXJLRwC7RZ9OZfoqr4tuWxHxwBYBbkRBSyY5BqNjs,3752
5
+ realchat_sdk-0.1.0.dist-info/METADATA,sha256=s7MZL4iK964p-GLAWMqSNDTyDPvLH7lvgIPHh-Bls_U,3092
6
+ realchat_sdk-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ realchat_sdk-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any