Skip to content

Commit

Permalink
feat: implement chat functionality with room management and menu inte…
Browse files Browse the repository at this point in the history
…gration
  • Loading branch information
kc1awv committed Jan 17, 2025
1 parent ed81d68 commit 49704e2
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 6 deletions.
18 changes: 16 additions & 2 deletions client/retibbs_textual.py
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,8 @@ def update_connection_status(self):
status += f" | {self.current_area}"
if hasattr(self, "current_board") and self.current_board:
status += f" | Board: {self.current_board}"
elif hasattr(self, "current_room") and self.current_room:
status += f" | Room: {self.current_room}"
else:
indicator = Text("\u2715 ", style="bold red")
status = "Not Connected"
Expand Down Expand Up @@ -470,8 +472,11 @@ def on_packet_received(self, message_bytes, packet):
area_name = decoded_message[len("CTRL AREA "):].strip()
self.current_area = area_name
self.write_debug_log(f"[INFO] Area update: {area_name}")
if self.current_area != "Message Boards":
#if self.current_area != "Message Boards":
# self.current_board = None
if self.current_area == "Main Menu":
self.current_board = None
self.current_room = None
self.update_connection_status()
except Exception as e:
self.write_log(f"[SERVER-PACKET] Error processing area update: {e}")
Expand All @@ -484,6 +489,15 @@ def on_packet_received(self, message_bytes, packet):
self.update_connection_status()
except Exception as e:
self.write_log(f"[SERVER-PACKET] Error processing board update: {e}")
elif message_bytes.startswith(b"CTRL ROOM"):
try:
decoded_message = message_bytes.decode("utf-8")
room_name = decoded_message[len("CTRL ROOM "):].strip()
self.current_room = room_name
self.write_debug_log(f"[INFO] Room update: {room_name}")
self.update_connection_status()
except Exception as e:
self.write_log(f"[SERVER-PACKET] Error processing room update: {e}")
else:
try:
text = message_bytes.decode("utf-8", "ignore")
Expand All @@ -497,7 +511,7 @@ def on_packet_received(self, message_bytes, packet):
async def on_input_submitted(self, message: Input.Submitted):
command = message.value.strip()
if command:
self.write_log(f"\nCommand: {command}")
self.write_debug_log(f"\nCommand: {command}")
if self.link and self.link.status == RNS.Link.ACTIVE:
try:
packet = RNS.Packet(self.link, command.encode("utf-8"))
Expand Down
237 changes: 237 additions & 0 deletions server/chat/chat_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import RNS

class ChatRoom:
def __init__(self, name, chat_manager):
self.name = name
self.clients = set()
self.chat_manager = chat_manager

def add_client(self, user_hash):
"""
Add a client to the chat room.
:param user_hash: The user's hash.
"""
self.clients.add(user_hash)

def remove_client(self, user_hash):
"""
Remove a client from the chat room.
:param user_hash: The user's hash.
"""
self.clients.discard(user_hash)
return len(self.clients) == 0

def broadcast(self, message, sender=None):
"""
Broadcast a message to all clients in the chat room.
:param message: The message to broadcast.
:param sender: The hash of the user who sent the message.
"""
for user_hash in self.clients:
if user_hash != sender:
self.chat_manager.broadcast_to_user(user_hash, f"[{self.name}] {message}")
RNS.log(f"[ChatRoom] Broadcast to {user_hash}: {message}", RNS.LOG_DEBUG)

class ChatManager:
def __init__(self, users_manager, reply_manager, lxmf_handler, theme_manager):
self.rooms = {}
self.user_links = {}
self.users_mgr = users_manager
self.theme_mgr = theme_manager
self.lxmf_handler = lxmf_handler
self.reply_handler = reply_manager

def register_user_link(self, user_hash, link):
"""
Register a user link.
:param user_hash: The user's hash.
:param link: The link to register.
"""
self.user_links[user_hash] = link
RNS.log(f"[ChatManager] Registered user link: {user_hash} -> {link}", RNS.LOG_DEBUG)

def unregister_user_link(self, user_hash):
"""
Unregister a user link.
:param user_hash: The user's hash.
"""
if user_hash in self.user_links:
del self.user_links[user_hash]
RNS.log(f"[ChatManager] Unregistered user link: {user_hash}", RNS.LOG_DEBUG)

def broadcast_to_user(self, user_hash, message):
"""
Broadcast a message to a user.
:param user_hash: The user's hash.
:param message: The message to broadcast.
"""
link = self.user_links.get(user_hash)
if link:
self.reply_handler.send_link_reply(link, message)
else:
RNS.log(f"[ChatManager] Failed to broadcast to {user_hash}: No link found.", RNS.LOG_WARNING)

def handle_chat_commands(self, command, packet, user_hash):
"""
Handle chat commands.
:param command: The command to handle.
:param packet: The incoming packet.
:param user_hash: The user's hash.
"""
tokens = command.split(None, 1)
if not tokens:
self.reply_handler.send_link_reply(packet.link, "UNKNOWN COMMAND\n")
return
cmd = tokens[0].lower()
remainder = tokens[1] if len(tokens) > 1 else ""
if cmd in ["/?", "/help"]:
self.handle_help(packet, user_hash)
elif cmd in ["/b", "/back"]:
self.handle_back(packet, user_hash)
elif cmd in ["/j", "/join"]:
self.handle_join_room(packet, remainder, user_hash)
elif cmd in ["/l", "/leave"]:
self.handle_leave_room(packet, remainder, user_hash)
elif cmd in ["/list"]:
self.handle_list_rooms(packet, user_hash)
else:
self.handle_chat_message(packet, command, user_hash)

def handle_help(self, packet, user_hash):
"""
Show the help screen.
:param packet: The incoming packet.
"""
user = self.users_mgr.get_user(user_hash)
help_text = (
"Available Chat Commands:\n"
" /? - Show this help screen\n"
" /back - Return to the main menu\n"
" /join <room_name> - Join a chat room\n"
" /leave - Leave the current chat room\n"
" /list - List available chat rooms\n"
" /msg <message> - Send a message to the current chat room\n"
)
self.reply_handler.send_resource_reply(packet.link, help_text)

def handle_back(self, packet, user_hash):
"""
Return to the main menu.
:param packet: The incoming packet.
:param user_hash: The user's hash.
"""
self.leave_room(user_hash)
self.unregister_user_link(user_hash)
self.reply_handler.send_clear_screen(packet.link)
self.users_mgr.set_user_area(user_hash, area="main_menu")
main_menu_message = self.theme_mgr.theme_files.get("header.txt", "Welcome to RetiBBS")
main_menu_message += "\n"
main_menu_message += self.theme_mgr.theme_files.get(
"main_menu.txt",
"Main Menu: [?] Help [h] Hello [n] Name [b] Boards Area [lo] Log Out"
)
self.reply_handler.send_resource_reply(packet.link, main_menu_message)
self.reply_handler.send_area_update(packet.link, "Main Menu")

def handle_join_room(self, packet, room_name, user_hash):
"""
Join a chat room.
:param packet: The incoming packet.
:param room_name: The name of the chat room to join.
:param user_hash: The user's hash.
"""
room_name = room_name.strip()
self.leave_room(user_hash)
room = self.join_room(user_hash, room_name)
user_display_name = self.users_mgr.get_user_display(user_hash)
room.broadcast(f"{user_display_name} has joined the room.")
self.reply_handler.send_link_reply(packet.link, f"Joined room: {room_name}")

def handle_leave_room(self, packet, _, user_hash):
"""
Leave the current chat room.
:param packet: The incoming packet.
:param user_hash: The user's hash.
"""
current_room_name = self.users_mgr.get_user_room(user_hash)
user_display_name = self.users_mgr.get_user_display(user_hash)
if current_room_name:
self.leave_room(user_hash)
self.reply_handler.send_link_reply(packet.link, f"Left room: {current_room_name}")
room = self.rooms.get(current_room_name)
if room:
room.broadcast(f"{user_display_name} has left the room.")
else:
self.reply_handler.send_link_reply(packet.link, "You are not in a chat room.")

def handle_list_rooms(self, packet, user_hash):
"""
List available chat rooms.
:param packet: The incoming packet.
"""
if not self.rooms:
self.reply_handler.send_link_reply(packet.link, "No chat rooms are currently open.\n\n /join <room_name> to create a new room.")
return
room_list = "Available Chat Rooms:\n"
for room_name, room in self.rooms.items():
participant_count = len(room.clients)
room_list += f" - {room_name} ({participant_count} participant{'s' if participant_count != 1 else ''})\n"
self.reply_handler.send_link_reply(packet.link, room_list)

def handle_chat_message(self, packet, message, user_hash):
"""
Handle a chat message.
:param packet: The incoming packet.
:param message: The message to send.
:param user_hash: The user's hash.
"""
current_room_name = self.users_mgr.get_user_room(user_hash)
if current_room_name:
room = self.rooms.get(current_room_name)
if room:
user_display_name = self.users_mgr.get_user_display(user_hash)
room.broadcast(f"{user_display_name}: {message.strip()}", sender=user_hash)
RNS.log(f"[ChatManager] Broadcast to {current_room_name}: {message.strip()}", RNS.LOG_DEBUG)
self.reply_handler.send_link_reply(packet.link, f"[{current_room_name}] (You): {message.strip()}")
else:
self.reply_handler.send_link_reply(packet.link, "ERROR: Chat room not found.")
else:
self.reply_handler.send_link_reply(packet.link, "You are not in a chat room.")

def get_or_create_room(self, room_name):
"""
Get or create a chat room.
:param room_name: The name of the chat room.
"""
if room_name not in self.rooms:
self.rooms[room_name] = ChatRoom(room_name, self)
return self.rooms[room_name]

def join_room(self, user_hash, room_name):
"""
Join a chat room.
:param user_hash: The user's hash.
:param room_name: The name of the chat room to join.
"""
room = self.get_or_create_room(room_name)
room.add_client(user_hash)
self.users_mgr.set_user_room(user_hash, room_name)
self.reply_handler.send_room_update(self.user_links[user_hash], room_name)
return room

def leave_room(self, user_hash):
"""
Leave the current chat room.
:param user_hash: The user's hash.
"""
current_room_name = self.users_mgr.get_user_room(user_hash)
if current_room_name:
room = self.rooms[current_room_name]
user_display_name = self.users_mgr.get_user_display(user_hash)
if room:
is_empty = room.remove_client(user_hash)
room.broadcast(f"{user_display_name} has left the room.")
if is_empty:
del self.rooms[current_room_name]
RNS.log(f"[ChatManager] Removed empty room: {current_room_name}", RNS.LOG_DEBUG)
self.users_mgr.set_user_room(user_hash, None)
25 changes: 24 additions & 1 deletion server/main_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@
import RNS

class MainMenuHandler:
def __init__(self, users_manager, reply_handler, lxmf_handler, theme_manager):
def __init__(self, users_manager, reply_handler, lxmf_handler, theme_manager, chat_manager):
self.users_mgr = users_manager
self.reply_handler = reply_handler
self.lxmf_handler = lxmf_handler
self.theme_mgr = theme_manager
self.chat_mgr = chat_manager

def handle_main_menu_commands(self, command, packet, user_hash):
"""
Expand All @@ -32,6 +33,8 @@ def handle_main_menu_commands(self, command, packet, user_hash):
self.handle_test_destination(packet, user_hash)
elif cmd in ["b", "boards"]:
self.handle_boards(packet, user_hash)
elif cmd in ["c", "chat"]:
self.handle_chat(packet, user_hash)
elif cmd in ["lo", "logout"]:
self.handle_logout(packet)
elif cmd in ["lu", "listusers"]:
Expand Down Expand Up @@ -153,6 +156,26 @@ def handle_boards(self, packet, user_hash):
current_board = self.users_mgr.get_user_board(user_hash)
self.reply_handler.send_area_update(packet.link, "Message Boards")
self.reply_handler.send_board_update(packet.link, current_board)

def handle_chat(self, packet, user_hash):
"""
Command to switch to the chat area.
"""
if self.users_mgr.get_user_area(user_hash) != "chat":
self.chat_mgr.register_user_link(user_hash, packet.link)
self.reply_handler.send_clear_screen(packet.link)
chat_menu_message = self.theme_mgr.theme_files.get("header.txt", "Welcome to the Chat Room!")
chat_menu_message += "\n"
chat_menu_message += self.theme_mgr.theme_files.get(
"chat_menu.txt",
"Chat Menu: [/?] Help [/b] Back [/j] Join [/l] Leave [/msg] Post Message"
)
self.reply_handler.send_resource_reply(packet.link, chat_menu_message)
self.users_mgr.set_user_area(user_hash, area="chat")
else:
self.reply_handler.send_link_reply(packet.link, "You are already in the chat area.")
self.reply_handler.send_area_update(packet.link, "Chat")
self.reply_handler.send_room_update(packet.link, self.users_mgr.get_user_room(user_hash))

def handle_logout(self, packet):
"""
Expand Down
15 changes: 15 additions & 0 deletions server/reply_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,20 @@ def send_board_update(link, board_name):
packet = RNS.Packet(link, control_packet)
packet.send()
RNS.log(f"[ReplyHandler] Sent board update: {board_name}", RNS.LOG_DEBUG)
except Exception as e:
RNS.log(f"[ReplyHandler] Error sending board update: {e}", RNS.LOG_ERROR)

@staticmethod
def send_room_update(link, room_name):
"""
Send a room update command to a link.
:param link: The link to send the command to.
:param room_name: The board to switch to.
"""
try:
control_packet = f"CTRL ROOM {room_name}".encode("utf-8")
packet = RNS.Packet(link, control_packet)
packet.send()
RNS.log(f"[ReplyHandler] Sent room update: {room_name}", RNS.LOG_DEBUG)
except Exception as e:
RNS.log(f"[ReplyHandler] Error sending board update: {e}", RNS.LOG_ERROR)
Loading

0 comments on commit 49704e2

Please sign in to comment.