1from .api.v1
import ServerConfig, can_join_room, RoomJoinStatus, get_auth_bot_token
2from .commands
import Command, CommandParameter
3from .dataclasses
import *
5from socketio
import AsyncClient
7from asyncio
import run
as async_run
8from asyncio
import sleep
as async_sleep
10from typing
import Callable, Awaitable, Any
13_server_config = ServerConfig.get()
14Binding = Callable[..., Awaitable[Any]] |
None
18 A wrapper class around the socket.io Talkomatic API. This bot uses
19 an asynchronous socket.io client to optimize for concurrency,
20 while abstracting error handling, disconnecting, and other low-level
21 details, to make writing bots easier.
24 username (str): The username of the bot.
25 location (str): The location of the bot.
26 The bot's name will show up as "username / location".
29 rate_limits (RateLimits): The rate limits of the bot. (disabled by default to make
30 development easier, consider putting rate limits in to prevent getting blocked by the API)
31 debug (bool): Whether to enable debug mode for the internal socket.io
32 client. Defaults to False.
36 rate_limits: RateLimits
38 current_room: Room |
None=
None
39 commands: dict[str, Command] = {}
41 rooms: list[Room] = []
42 user_database: dict[str, User] = {}
43 user_messages: dict[User, str] = {}
44 _fully_started: bool =
False
46 on_connect_binding : Binding =
None
47 on_inactivity_disconnect_binding : Binding =
None
48 on_room_creation_binding : Binding =
None
49 on_room_join_binding : Binding =
None
50 on_room_leave_binding : Binding =
None
51 on_user_message_binding : Binding =
None
52 on_user_join_binding : Binding =
None
53 on_user_leave_binding : Binding =
None
54 on_user_vote_binding : Binding =
None
56 _time_since_last_message: float = 0
57 _time_since_last_room_join: float = 0
58 _time_since_last_room_creation: float = 0
60 def __init__(self, command_marker: str =
"/", rate_limits: RateLimits = DISABLE_RATE_LIMITS, debug: bool =
False) ->
None:
61 self.
sio = AsyncClient(engineio_logger = debug, logger = debug)
65 def run(self, username: str, location: str, create_help_command: bool =
False) ->
None:
70 username (str): The username of the bot.
71 location (str): The location of the bot.
74 assert len(username) <= _server_config.max_username_length, f
"Username must be less (or equal) than {_server_config.max_username_length} characters."
75 assert len(location) <= _server_config.max_location_length, f
"Location must be less (or equal) than {_server_config.max_location_length} characters."
77 if create_help_command:
80 description =
"Shows this help menu.",
84 "The command to get help for.",
91 async def _help_command(user: User, command_name: str |
None =
None) ->
None:
94 command = self.
commands.get(command_name)
96 if not command.hidden:
97 message += f
"{self.command_marker}{command_name} - {command.description}\n"
98 for parameter
in command.parameters:
99 message += f
" {'' if parameter.positional else '--'}{parameter.name} - {parameter.description} ({parameter.data_type.__name__}){'' if parameter.required else ' [optional]'}\n"
101 message += f
"Command '{self.command_marker}{command_name}' not found."
103 message += f
"Command '{self.command_marker}{command_name}' not found."
105 for command_name, command
in self.
commands.items():
106 if not command.hidden:
107 message += f
" {', '.join([self.command_marker + alias for alias in [command.name] + command.aliases])} - {command.description}\n"
111 async_run(self.
_run(username, location))
112 except KeyboardInterrupt:
113 print(
"\033[1;33mCtrl+C pressed, disconnecting...\033[0m")
116 async def _run(self, username: str, location: str) ->
None:
117 await self.
sio.connect(
"https://classic.talkomatic.co/", auth = {
118 "token": get_auth_bot_token()
122 await self.
sio.emit(
"check signin status")
124 def _bind_event_ignore_args(event_name: str, binding: Binding) ->
None:
126 async def _(*_) -> None:
128 self.
sio.on(event_name, _)
130 def _bind_room_created(binding: Binding) ->
None:
132 async def _(room_id: str) ->
None:
133 await binding(int(room_id))
134 self.
sio.on(
"room created", _)
147 await async_sleep(0.01)
152 await self.
sio.wait()
158 user =
User(data[
"userId"], data[
"username"],
"", id_only =
True)
163 if diff[
"type"] ==
"full-replace":
165 elif diff[
"type"] ==
"add":
167 elif diff[
"type"] ==
"delete":
169 elif diff[
"type"] ==
"replace":
172 raise RuntimeError(f
"Unknown chat update type: {diff['type']}")
175 for command_name, command
in self.
commands.items():
178 val = await command.execute(user, first_line[len(self.
command_marker + command_name):].strip())
185 async def _error(self, data: dict) ->
None:
186 error = data[
"error"]
187 if error[
"code"] ==
"ACCESS_DENIED" and error[
"message"] ==
"Disconnected due to inactivity":
193 print(f
"Uncaught error socket.io event: {error}")
195 async def _join_lobby(self, username: str, location: str) ->
None:
196 await self.
sio.emit(
"join lobby", {
197 "username": username,
202 rooms = [Room.from_raw_json(room_data)
for room_data
in rooms_data]
205 for room
in self.
rooms:
206 for user
in room.users:
212 self.
user = User.from_raw_json(data)
217 if lobby_room_info !=
None:
218 self.
current_room.banned_users = lobby_room_info.banned_users
219 self.
current_room.last_time_active = lobby_room_info.last_time_active
233 user = User.from_raw_json(data)
254 new_votes = Room._parse_votes({
270 for voted, voters
in new_votes.items():
273 if voter
not in old_voters:
282 for voter
in old_voters:
283 if voter
not in voters:
296 description: str =
"",
297 parameters: list[CommandParameter] = [],
298 aliases: list[str] = [],
300 ) -> Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]:
302 Decorator for registering commands.
305 name (str): The name of the command.
306 description (str): The description of the command.
307 parameters (list[CommandParameter]): The parameters of the command.
308 aliases (list[str]): The aliases of the command.
309 hidden (bool): Whether the command will show up in the built-in /help command.
312 def register_command(function: Callable[..., Awaitable[Any]]) -> Callable[..., Awaitable[Any]]:
313 command =
Command(name, description, aliases, hidden)
314 command.function = function
315 for parameter
in parameters:
316 command.add_argument(
317 [parameter.name] + parameter.aliases,
318 parameter.description,
320 parameter.positional,
322 parameter.number_of_args
324 command.parameters = parameters
327 return register_command
329 async def create_room(self, room_name: str, room_type: RoomType = RoomType.PUBLIC, room_layout: RoomLayoutType = RoomLayoutType.VERTICAL, room_password: int |
None =
None) -> bool:
331 Creates a room with the given name, type, layout, and password.
334 room_name (str): The name of the room.
335 room_type (RoomType): The type of the room.
336 room_layout (RoomLayoutType): The layout of the room.
337 room_password (int, **must be 6 digits**): The password of the room (if the room is not public).
340 bool: Whether the room was created successfully (returns False if the room was
341 not created due to rate limits).
347 if room_password !=
None:
348 assert 100000 <= room_password <= 999999,
"Room password must be a 6 digits integer."
350 assert len(room_name) <= _server_config.max_room_name_length, f
"Room name must be less (or equal) than {_server_config.max_room_name_length} characters."
352 await self.
sio.emit(
"create room", {
354 "type": room_type.value,
355 "layout": room_layout.value,
356 "accessCode": str(room_password)
if room_password !=
None else ""
362 Disconnects the bot from the server.
369 Gets a room by its ID.
370 Returns None if the room does not exist.
373 id (int): The ID of the room to get.
376 Room: The room object. (None if the room does not exist)
379 for room
in self.
rooms:
380 if room.room_id == id:
386 Allows you to get a user from the user database by their ID.
387 Useful for getting a user from a "id-only" user, but is not
388 guaranteed to work if the user is not in a room anymore.
391 id (str): The ID of the user to get.
394 User: The full user object. (None if the user is not in the database)
401 Gets the message of a user.
404 user (User): The user to get the message of.
407 str: The message of the user.
412 async def join_room(self, room: Room | int, access_code: int |
None =
None, do_api_check: bool =
True) -> bool:
414 Joins a room with the given room/room ID and access code.
415 **Reminder: your bot can only be in 1 room at a time!**
418 room (Room | int): The room (or ID of the room) to join.
419 access_code (int | None): The 6 digit access code of the room to join (if the room is not public).
420 do_api_check (bool): Whether to check if the room can be joined using the Talkomatic API.
423 bool: Whether the room was joined successfully (returns False if the room was
424 not joined due to rate limits).
430 if isinstance(room, int):
433 room_id = room.room_id
436 status = can_join_room(room_id, access_code)
437 if status != RoomJoinStatus.SUCCESS:
438 raise RuntimeError(f
"Failed to join room {room_id}: {status}")
440 await self.
sio.emit(
"join room", {
441 "roomId": str(room_id),
442 "accessCode": str(access_code)
if access_code !=
None else None
448 Leaves the room the bot is currently in.
451 await self.
sio.emit(
"leave room")
455 Binds a function to when the bot finishes connection to the server.
458 binding (Coroutine): The function to bind to the event.
466 Binds a function to when the bot is disconnected due to having not interacted for too long.
469 binding (Coroutine): The function to bind to the event.
477 Binds a function to when the bot has successfully created the room.
480 binding (Coroutine): The function to bind to the event.
488 Binds a function to when the bot has successfully joined the room.
491 binding (Coroutine): The function to bind to the event.
499 Binds a function to when the bot has successfully left the room.
502 binding (Coroutine): The function to bind to the event.
510 Binds a function to when a user sent a message in the bot's current room.
513 binding (Coroutine): The function to bind to the event.
521 Binds a function to when another user has joined the bot's current room.
524 binding (Coroutine): The function to bind to the event.
532 Binds a function to when a user has left the bot's current room.
535 binding (Coroutine): The function to bind to the event.
543 Binds a function to when a user has voted to a ban another user from the bot's current room.
546 binding (Coroutine): The function to bind to the event.
554 Sends the message to the room the bot is currently in.
557 message (str): The message to send.
560 bool: Whether the message was sent successfully (returns False if the message was
561 not sent due to rate limits).
564 assert len(message) <= _server_config.max_message_length, f
"Message is too long, max length is {_server_config.max_message_length} characters."
569 await self.
sio.emit(
"chat update", {
571 "type":
"full-replace",
579 Votes the user to be banned (or remove vote if the user is already voted by the bot)
580 from the room the bot is currently in.
583 user (User): The user to vote for.
586 await self.
sio.emit(
"vote", {
587 "targetUserId": user.id
Binding on_room_join_binding
Binding on_connect(self, Binding binding)
None _join_lobby(self, str username, str location)
Binding on_connect_binding
Binding on_inactivity_disconnect_binding
Binding on_user_message_binding
float _time_since_last_message
Room|None get_room_by_id(self, int id)
str get_user_message(self, User user)
bool send_message(self, str message)
Binding on_user_join(self, Binding binding)
Binding on_room_join(self, Binding binding)
Binding on_room_leave(self, Binding binding)
Binding on_user_vote(self, Binding binding)
None _run(self, str username, str location)
None __init__(self, str command_marker="/", RateLimits rate_limits=DISABLE_RATE_LIMITS, bool debug=False)
Binding on_user_vote_binding
User|None get_user_by_id(self, str id)
Binding on_room_creation_binding
Binding on_room_leave_binding
None run(self, str username, str location, bool create_help_command=False)
float _time_since_last_room_join
Binding on_inactivity_disconnect(self, Binding binding)
Binding on_room_creation(self, Binding binding)
Binding on_user_message(self, Binding binding)
None toggle_vote(self, User user)
bool join_room(self, Room|int room, int|None access_code=None, bool do_api_check=True)
Binding on_user_join_binding
Binding on_user_leave(self, Binding binding)
Binding on_user_leave_binding
float _time_since_last_room_creation