awacke1 commited on
Commit
962f379
·
verified ·
1 Parent(s): e2ffbc8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +73 -513
app.py CHANGED
@@ -12,6 +12,7 @@ import os
12
  import shutil
13
  from pathlib import Path
14
  import time
 
15
 
16
  # Configure logging
17
  logging.basicConfig(
@@ -21,54 +22,50 @@ logging.basicConfig(
21
  )
22
  logger = logging.getLogger("chat-node")
23
 
24
- # Dictionary to store active connections
25
  active_connections = {}
26
- # Dictionary to store message history for each chat room (in-memory cache)
27
  chat_history = {}
28
- # Dictionary to store message history for logs
29
  log_history = []
30
-
31
- # Dictionary to track file modification times
32
  file_modification_times = {}
33
-
34
- # Dictionary to track users in each room/sector
35
  sector_users = {}
36
 
37
- # Grid dimensions for 2D sector map
38
  GRID_WIDTH = 10
39
  GRID_HEIGHT = 10
40
 
41
- # Directories for persistent storage
42
  HISTORY_DIR = "chat_history"
43
  LOG_DIR = "server_logs"
44
  os.makedirs(HISTORY_DIR, exist_ok=True)
45
  os.makedirs(LOG_DIR, exist_ok=True)
46
 
47
  # README files
48
- README_PATH = os.path.join(HISTORY_DIR, "README.md")
49
- if not os.path.exists(README_PATH):
50
- with open(README_PATH, "w") as f:
51
- f.write("# Chat History\n\nThis directory contains persistent chat history files.\n")
52
-
53
- LOG_README_PATH = os.path.join(LOG_DIR, "README.md")
54
- if not os.path.exists(LOG_README_PATH):
55
- with open(LOG_README_PATH, "w") as f:
56
- f.write("# Server Logs\n\nThis directory contains server log files.\n")
57
-
58
- # Get node name from URL or command line
 
 
 
 
 
 
 
 
59
  def get_node_name():
60
  parser = argparse.ArgumentParser(description='Start a chat node with a specific name')
61
  parser.add_argument('--node-name', type=str, default=None, help='Name for this chat node')
62
  parser.add_argument('--port', type=int, default=7860, help='Port to run the Gradio interface on')
63
-
64
  args = parser.parse_args()
65
- node_name = args.node_name
66
- port = args.port
67
-
68
- if not node_name:
69
- node_name = f"node-{uuid.uuid4().hex[:8]}"
70
-
71
- return node_name, port
72
 
73
  def get_room_history_file(room_id):
74
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
@@ -79,93 +76,56 @@ def get_log_file():
79
  return os.path.join(LOG_DIR, f"log_{timestamp}.jsonl")
80
 
81
  def get_all_room_history_files(room_id):
82
- files = []
83
- for file in os.listdir(HISTORY_DIR):
84
- if file.startswith(f"{room_id}_") and file.endswith(".jsonl"):
85
- files.append(os.path.join(HISTORY_DIR, file))
86
- files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
87
  return files
88
 
89
  def list_all_history_files():
90
- """List all history files with room IDs and modification times."""
91
  files = []
92
  for file in os.listdir(HISTORY_DIR):
93
  if file.endswith(".jsonl") and file != "README.md":
94
- parts = file.split('_', 1)
95
- if len(parts) > 0:
96
- room_id = parts[0]
97
- file_path = os.path.join(HISTORY_DIR, file)
98
- mod_time = os.path.getmtime(file_path)
99
- files.append((room_id, file_path, mod_time))
100
- files.sort(key=lambda x: x[2], reverse=True) # Sort by modification time
101
  return files
102
 
103
  def load_room_history(room_id):
104
  if room_id not in chat_history:
105
  chat_history[room_id] = []
106
  history_files = get_all_room_history_files(room_id)
107
-
108
  for file in history_files:
109
- if file not in file_modification_times:
110
- file_modification_times[file] = os.path.getmtime(file)
111
-
112
- messages = []
113
- for history_file in history_files:
114
  try:
115
- with open(history_file, 'r') as f:
116
  for line in f:
117
- line = line.strip()
118
- if line:
119
  try:
120
- data = json.loads(line)
121
- messages.append(data)
122
  except json.JSONDecodeError:
123
- logger.error(f"Error parsing JSON line in {history_file}")
124
  except Exception as e:
125
- logger.error(f"Error loading history from {history_file}: {e}")
126
-
127
- messages.sort(key=lambda x: x.get("timestamp", ""), reverse=False)
128
- chat_history[room_id] = messages
129
- logger.info(f"Loaded {len(messages)} messages from {len(history_files)} files for room {room_id}")
130
-
131
- if room_id not in sector_users:
132
- sector_users[room_id] = set()
133
-
134
  return chat_history[room_id]
135
 
136
  def save_message_to_history(room_id, message):
137
  history_files = get_all_room_history_files(room_id)
138
- if not history_files:
139
- history_file = get_room_history_file(room_id)
140
- else:
141
- newest_file = history_files[0]
142
- if os.path.getsize(newest_file) > 1024 * 1024: # 1 MB
143
- history_file = get_room_history_file(room_id)
144
- else:
145
- history_file = newest_file
146
-
147
  try:
148
  with open(history_file, 'a') as f:
149
  f.write(json.dumps(message) + '\n')
150
  file_modification_times[history_file] = os.path.getmtime(history_file)
151
- logger.debug(f"Saved message to {history_file}")
152
  except Exception as e:
153
  logger.error(f"Error saving message to {history_file}: {e}")
154
 
155
  def save_log_entry(entry):
156
- """Save a log entry to the newest log file."""
157
- log_files = [f for f in os.listdir(LOG_DIR) if f.startswith("log_") and f.endswith(".jsonl")]
158
- log_files.sort(key=lambda x: os.path.getmtime(os.path.join(LOG_DIR, x)), reverse=True)
159
-
160
- if not log_files:
161
- log_file = get_log_file()
162
- else:
163
- newest_log = os.path.join(LOG_DIR, log_files[0])
164
- if os.path.getsize(newest_log) > 1024 * 1024: # 1 MB
165
- log_file = get_log_file()
166
- else:
167
- log_file = newest_log
168
-
169
  try:
170
  with open(log_file, 'a') as f:
171
  f.write(json.dumps(entry) + '\n')
@@ -173,80 +133,14 @@ def save_log_entry(entry):
173
  except Exception as e:
174
  logger.error(f"Error saving log to {log_file}: {e}")
175
 
176
- def check_for_new_messages():
177
- updated_rooms = set()
178
- for file in os.listdir(HISTORY_DIR):
179
- if file.endswith(".jsonl"):
180
- file_path = os.path.join(HISTORY_DIR, file)
181
- current_mtime = os.path.getmtime(file_path)
182
- if file_path not in file_modification_times or current_mtime > file_modification_times[file_path]:
183
- parts = file.split('_', 1)
184
- if len(parts) > 0:
185
- room_id = parts[0]
186
- updated_rooms.add(room_id)
187
- file_modification_times[file_path] = current_mtime
188
-
189
- for room_id in updated_rooms:
190
- if room_id in chat_history:
191
- old_history_len = len(chat_history[room_id])
192
- chat_history[room_id] = []
193
- load_room_history(room_id)
194
- new_history_len = len(chat_history[room_id])
195
- if new_history_len > old_history_len:
196
- logger.info(f"Found {new_history_len - old_history_len} new messages for room {room_id}")
197
-
198
- return updated_rooms
199
-
200
- def check_for_new_logs():
201
- """Check for new log entries and broadcast them."""
202
- global log_history
203
- updated = False
204
-
205
- for file in os.listdir(LOG_DIR):
206
- if file.endswith(".jsonl"):
207
- file_path = os.path.join(LOG_DIR, file)
208
- current_mtime = os.path.getmtime(file_path)
209
- if file_path not in file_modification_times or current_mtime > file_modification_times[file_path]:
210
- updated = True
211
- file_modification_times[file_path] = current_mtime
212
-
213
- if updated:
214
- log_history = []
215
- for file in sorted([f for f in os.listdir(LOG_DIR) if f.endswith(".jsonl")], key=lambda x: os.path.getmtime(os.path.join(LOG_DIR, x))):
216
- file_path = os.path.join(LOG_DIR, file)
217
- try:
218
- with open(file_path, 'r') as f:
219
- for line in f:
220
- line = line.strip()
221
- if line:
222
- try:
223
- log_history.append(json.loads(line))
224
- except json.JSONDecodeError:
225
- logger.error(f"Error parsing log line in {file_path}")
226
- except Exception as e:
227
- logger.error(f"Error loading logs from {file_path}: {e}")
228
-
229
- # Broadcast logs to all connected clients in a "logs" room
230
- log_msg = {
231
- "type": "log",
232
- "content": "\n".join([entry["message"] for entry in log_history[-10:]]), # Last 10 entries
233
- "timestamp": datetime.now().isoformat(),
234
- "sender": "system",
235
- "room_id": "logs"
236
- }
237
- asyncio.create_task(broadcast_message(log_msg, "logs"))
238
-
239
  def get_sector_coordinates(room_id):
240
  try:
241
  if ',' in room_id:
242
  x, y = map(int, room_id.split(','))
243
  return max(0, min(x, GRID_WIDTH-1)), max(0, min(y, GRID_HEIGHT-1))
244
  except:
245
- pass
246
- hash_val = hash(room_id)
247
- x = abs(hash_val) % GRID_WIDTH
248
- y = abs(hash_val >> 8) % GRID_HEIGHT
249
- return x, y
250
 
251
  def generate_sector_map():
252
  grid = [[' ' for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
@@ -255,13 +149,8 @@ def generate_sector_map():
255
  x, y = get_sector_coordinates(room_id)
256
  user_count = len(users)
257
  grid[y][x] = str(min(user_count, 9)) if user_count < 10 else '+'
258
-
259
  header = ' ' + ''.join([str(i % 10) for i in range(GRID_WIDTH)])
260
- map_str = header + '\n'
261
- for y in range(GRID_HEIGHT):
262
- row = f"{y % 10}|" + ''.join(grid[y]) + '|'
263
- map_str += row + '\n'
264
- map_str += header
265
  return f"```\n{map_str}\n```\n\nLegend: Number indicates users in sector. '+' means 10+ users."
266
 
267
  async def clear_all_history():
@@ -270,378 +159,49 @@ async def clear_all_history():
270
  for file in os.listdir(HISTORY_DIR):
271
  if file.endswith(".jsonl"):
272
  os.remove(os.path.join(HISTORY_DIR, file))
273
-
274
- clear_msg = {
275
- "type": "system",
276
- "content": "🧹 All chat history has been cleared by a user",
277
- "timestamp": datetime.now().isoformat(),
278
- "sender": "system"
279
- }
280
-
281
  for room_id in list(active_connections.keys()):
282
  clear_msg["room_id"] = room_id
283
  await broadcast_message(clear_msg, room_id)
284
-
285
  logger.info("All chat history cleared")
286
  return "All chat history cleared"
287
 
288
  async def websocket_handler(websocket, path):
289
  try:
290
- path_parts = path.strip('/').split('/')
291
- room_id = path_parts[0] if path_parts else "default"
292
-
293
  client_id = str(uuid.uuid4())
294
- if room_id not in active_connections:
295
- active_connections[room_id] = {}
296
- active_connections[room_id][client_id] = websocket
297
-
298
- if room_id not in sector_users:
299
- sector_users[room_id] = set()
300
- sector_users[room_id].add(client_id)
301
-
302
  x, y = get_sector_coordinates(room_id)
303
  room_history = load_room_history(room_id)
304
-
305
- welcome_msg = {
306
- "type": "system",
307
- "content": f"Welcome to room '{room_id}' (Sector {x},{y})! Connected from node '{NODE_NAME}'",
308
- "timestamp": datetime.now().isoformat(),
309
- "sender": "system",
310
- "room_id": room_id
311
- }
312
- await websocket.send(json.dumps(welcome_msg))
313
-
314
- map_msg = {
315
- "type": "system",
316
- "content": f"Sector Map:\n{generate_sector_map()}",
317
- "timestamp": datetime.now().isoformat(),
318
- "sender": "system",
319
- "room_id": room_id
320
- }
321
- await websocket.send(json.dumps(map_msg))
322
-
323
- for msg in room_history:
324
  await websocket.send(json.dumps(msg))
325
-
326
- join_msg = {
327
- "type": "system",
328
- "content": f"User joined the room (Sector {x},{y}) - {len(sector_users[room_id])} users now present",
329
- "timestamp": datetime.now().isoformat(),
330
- "sender": "system",
331
- "room_id": room_id
332
- }
333
  await broadcast_message(join_msg, room_id)
334
  save_message_to_history(room_id, join_msg)
335
-
336
- logger.info(f"New client {client_id} connected to room {room_id} (Sector {x},{y})")
337
-
338
  if room_id == "logs":
339
- for log_entry in log_history[-10:]: # Send last 10 log entries
340
- log_msg = {
341
- "type": "log",
342
- "content": log_entry["message"],
343
- "timestamp": log_entry["timestamp"],
344
- "sender": "system",
345
- "room_id": "logs"
346
- }
347
- await websocket.send(json.dumps(log_msg))
348
-
349
  async for message in websocket:
350
  try:
351
  data = json.loads(message)
352
-
353
- if data.get("type") == "command" and data.get("command") == "clear_history":
354
- result = await clear_all_history()
355
- continue
356
-
357
- if data.get("type") == "command" and data.get("command") == "show_map":
358
- map_msg = {
359
- "type": "system",
360
- "content": f"Sector Map:\n{generate_sector_map()}",
361
- "timestamp": datetime.now().isoformat(),
362
- "sender": "system",
363
- "room_id": room_id
364
- }
365
- await websocket.send(json.dumps(map_msg))
366
- continue
367
-
368
- data["timestamp"] = datetime.now().isoformat()
369
- data["sender_node"] = NODE_NAME
370
- data["room_id"] = room_id
371
-
372
  chat_history[room_id].append(data)
373
  if len(chat_history[room_id]) > 500:
374
  chat_history[room_id] = chat_history[room_id][-500:]
375
-
376
- save_message_to_history(room_id, data)
377
- await broadcast_message(data, room_id)
378
-
379
- except json.JSONDecodeError:
380
- error_msg = {
381
- "type": "error",
382
- "content": "Invalid JSON format",
383
- "timestamp": datetime.now().isoformat(),
384
- "sender": "system",
385
- "room_id": room_id
386
- }
387
- await websocket.send(json.dumps(error_msg))
388
-
389
- except websockets.exceptions.ConnectionClosed:
390
- logger.info(f"Client {client_id} disconnected from room {room_id}")
391
- finally:
392
- if room_id in active_connections and client_id in active_connections[room_id]:
393
- del active_connections[room_id][client_id]
394
- if room_id in sector_users and client_id in sector_users[room_id]:
395
- sector_users[room_id].remove(client_id)
396
-
397
- x, y = get_sector_coordinates(room_id)
398
- leave_msg = {
399
- "type": "system",
400
- "content": f"User left the room (Sector {x},{y}) - {len(sector_users.get(room_id, set()))} users remaining",
401
- "timestamp": datetime.now().isoformat(),
402
- "sender": "system",
403
- "room_id": room_id
404
- }
405
- await broadcast_message(leave_msg, room_id)
406
- save_message_to_history(room_id, leave_msg)
407
-
408
- if not active_connections[room_id]:
409
- del active_connections[room_id]
410
-
411
- async def broadcast_message(message, room_id):
412
- if room_id in active_connections:
413
- disconnected_clients = []
414
- for client_id, websocket in active_connections[room_id].items():
415
- try:
416
- await websocket.send(json.dumps(message))
417
- except websockets.exceptions.ConnectionClosed:
418
- disconnected_clients.append(client_id)
419
-
420
- for client_id in disconnected_clients:
421
- del active_connections[room_id][client_id]
422
-
423
- async def start_websocket_server(host='0.0.0.0', port=8765):
424
- server = await websockets.serve(websocket_handler, host, port)
425
- logger.info(f"WebSocket server started on ws://{host}:{port}")
426
- return server
427
-
428
- main_event_loop = None
429
- message_queue = []
430
-
431
- def send_message(message, username, room_id):
432
- if not message.strip():
433
- return None
434
-
435
- global message_queue
436
- msg_data = {
437
- "type": "chat",
438
- "content": message,
439
- "username": username,
440
- "room_id": room_id
441
- }
442
- message_queue.append(msg_data)
443
- formatted_msg = f"{username}: {message}"
444
- return formatted_msg
445
-
446
- def join_room(room_id, chat_history_output):
447
- if not room_id.strip():
448
- return "Please enter a valid room ID", chat_history_output
449
-
450
- room_id = urllib.parse.quote(room_id.strip())
451
- history = load_room_history(room_id)
452
-
453
- formatted_history = []
454
- for msg in history:
455
- if msg.get("type") == "chat":
456
- sender_node = f" [{msg.get('sender_node', 'unknown')}]" if "sender_node" in msg else ""
457
- time_str = ""
458
- if "timestamp" in msg:
459
- try:
460
- dt = datetime.fromisoformat(msg["timestamp"])
461
- time_str = f"[{dt.strftime('%H:%M:%S')}] "
462
- except:
463
- pass
464
- formatted_history.append(f"{time_str}{msg.get('username', 'Anonymous')}{sender_node}: {msg.get('content', '')}")
465
- elif msg.get("type") == "system":
466
- formatted_history.append(f"System: {msg.get('content', '')}")
467
-
468
- return f"Joined room: {room_id}", formatted_history
469
-
470
- def send_clear_command():
471
- global message_queue
472
- msg_data = {
473
- "type": "command",
474
- "command": "clear_history",
475
- "username": "System"
476
- }
477
- message_queue.append(msg_data)
478
- return "🧹 Clearing all chat history..."
479
-
480
- def list_available_rooms():
481
- history_files = list_all_history_files()
482
-
483
- if not history_files:
484
- return "No chat rooms available yet. Create one by joining a room!"
485
-
486
- room_list = "### Available Chat Rooms\n\n"
487
- for room_id, file_path, mod_time in history_files:
488
- last_activity = datetime.fromtimestamp(mod_time).strftime("%Y-%m-%d %H:%M:%S")
489
- room_list += f"- **{room_id}**: Last activity {last_activity}\n"
490
-
491
- return room_list
492
-
493
- def create_gradio_interface():
494
- with gr.Blocks(title=f"Chat Node: {NODE_NAME}") as interface:
495
- gr.Markdown(f"# Chat Node: {NODE_NAME}")
496
- gr.Markdown("Join a room by entering a room ID below or create a new one.")
497
-
498
- with gr.Row():
499
- with gr.Column(scale=3):
500
- room_list = gr.Markdown(value="Loading available rooms...")
501
- refresh_button = gr.Button("🔄 Refresh Room List")
502
- with gr.Column(scale=1):
503
- clear_button = gr.Button("🧹 Clear All Chat History", variant="stop")
504
-
505
- with gr.Row():
506
- with gr.Column(scale=2):
507
- room_id_input = gr.Textbox(label="Room ID", placeholder="Enter room ID or use x,y coordinates")
508
- join_button = gr.Button("Join Room")
509
- with gr.Column(scale=1):
510
- with gr.Row():
511
- x_coord = gr.Number(label="X", value=0, minimum=0, maximum=GRID_WIDTH-1, step=1)
512
- y_coord = gr.Number(label="Y", value=0, minimum=0, maximum=GRID_HEIGHT-1, step=1)
513
- grid_join_button = gr.Button("Join by Coordinates")
514
-
515
- chat_history_output = gr.Textbox(label="Chat History", lines=20, max_lines=20)
516
-
517
- with gr.Row():
518
- username_input = gr.Textbox(label="Username", placeholder="Enter your username", value="User")
519
- with gr.Column(scale=3):
520
- message_input = gr.Textbox(
521
- label="Message",
522
- placeholder="Type your message here. Press Shift+Enter for new line, Enter to send.",
523
- lines=3
524
- )
525
- with gr.Column(scale=1):
526
- send_button = gr.Button("Send")
527
- map_button = gr.Button("🗺️ Show Map")
528
-
529
- current_room_display = gr.Textbox(label="Current Room", value="Not joined any room yet")
530
-
531
- refresh_button.click(list_available_rooms, inputs=[], outputs=[room_list])
532
- clear_button.click(send_clear_command, inputs=[], outputs=[room_list])
533
-
534
- def join_by_coordinates(x, y):
535
- return f"{int(x)},{int(y)}"
536
-
537
- grid_join_button.click(join_by_coordinates, inputs=[x_coord, y_coord], outputs=[room_id_input]).then(
538
- join_room, inputs=[room_id_input, chat_history_output], outputs=[current_room_display, chat_history_output]
539
- )
540
-
541
- join_button.click(join_room, inputs=[room_id_input, chat_history_output], outputs=[current_room_display, chat_history_output])
542
-
543
- def send_and_clear(message, username, room_id):
544
- if not room_id.startswith("Joined room:"):
545
- return "Please join a room first", message
546
-
547
- actual_room_id = room_id.replace("Joined room: ", "").strip()
548
- message_lines = message.strip().split("\n")
549
- formatted_msg = ""
550
-
551
- for line in message_lines:
552
- if line.strip():
553
- sent_msg = send_message(line.strip(), username, actual_room_id)
554
- if sent_msg:
555
- formatted_msg += sent_msg + "\n"
556
-
557
- return "", formatted_msg if formatted_msg else message, None
558
-
559
- send_button.click(
560
- send_and_clear,
561
- inputs=[message_input, username_input, current_room_display],
562
- outputs=[message_input, chat_history_output]
563
- )
564
-
565
- def show_sector_map(room_id):
566
- if not room_id.startswith("Joined room:"):
567
- return "Please join a room first to view the map"
568
- return generate_sector_map()
569
-
570
- map_button.click(show_sector_map, inputs=[current_room_display], outputs=[chat_history_output])
571
-
572
- def on_message_submit(message, username, room_id):
573
- return send_and_clear(message, username, room_id)
574
-
575
- message_input.submit(
576
- on_message_submit,
577
- inputs=[message_input, username_input, current_room_display],
578
- outputs=[message_input, chat_history_output]
579
- )
580
-
581
- interface.load(list_available_rooms, inputs=[], outputs=[room_list])
582
-
583
- return interface
584
-
585
- async def process_message_queue():
586
- global message_queue
587
- while True:
588
- if message_queue:
589
- msg_data = message_queue.pop(0)
590
- await broadcast_message(msg_data, msg_data["room_id"])
591
- await asyncio.sleep(0.1)
592
-
593
- async def process_logs():
594
- """Periodically check and broadcast new log entries."""
595
- while True:
596
- check_for_new_logs()
597
- await asyncio.sleep(1) # Check every second
598
-
599
- # Custom logging handler to save logs and broadcast them
600
- class LogBroadcastHandler(logging.Handler):
601
- def emit(self, record):
602
- log_entry = {
603
- "timestamp": datetime.now().isoformat(),
604
- "level": record.levelname,
605
- "message": self.format(record),
606
- "name": record.name
607
- }
608
- save_log_entry(log_entry)
609
- log_history.append(log_entry)
610
-
611
- logger.addHandler(LogBroadcastHandler())
612
-
613
- async def main():
614
- global NODE_NAME, main_event_loop
615
- NODE_NAME, port = get_node_name()
616
- main_event_loop = asyncio.get_running_loop()
617
-
618
- server = await start_websocket_server()
619
- asyncio.create_task(process_message_queue())
620
- asyncio.create_task(process_logs()) # Start log processor
621
-
622
- interface = create_gradio_interface()
623
-
624
- from starlette.middleware.base import BaseHTTPMiddleware
625
-
626
- class NodeNameMiddleware(BaseHTTPMiddleware):
627
- async def dispatch(self, request, call_next):
628
- global NODE_NAME
629
- query_params = dict(request.query_params)
630
- if "node_name" in query_params:
631
- NODE_NAME = query_params["node_name"]
632
- logger.info(f"Node name set to {NODE_NAME} from URL parameter")
633
- response = await call_next(request)
634
- return response
635
-
636
- app = gr.routes.App.create_app(interface)
637
- app.add_middleware(NodeNameMiddleware)
638
-
639
- import uvicorn
640
- config = uvicorn.Config(app, host="0.0.0.0", port=port)
641
- server = uvicorn.Server(config)
642
-
643
- logger.info(f"Starting Gradio interface on http://0.0.0.0:{port} with node name '{NODE_NAME}'")
644
- await server.serve()
645
-
646
- if __name__ == "__main__":
647
- asyncio.run(main())
 
12
  import shutil
13
  from pathlib import Path
14
  import time
15
+ import random
16
 
17
  # Configure logging
18
  logging.basicConfig(
 
22
  )
23
  logger = logging.getLogger("chat-node")
24
 
25
+ # Dictionaries for state
26
  active_connections = {}
 
27
  chat_history = {}
 
28
  log_history = []
 
 
29
  file_modification_times = {}
 
 
30
  sector_users = {}
31
 
32
+ # Grid dimensions
33
  GRID_WIDTH = 10
34
  GRID_HEIGHT = 10
35
 
36
+ # Directories
37
  HISTORY_DIR = "chat_history"
38
  LOG_DIR = "server_logs"
39
  os.makedirs(HISTORY_DIR, exist_ok=True)
40
  os.makedirs(LOG_DIR, exist_ok=True)
41
 
42
  # README files
43
+ for dir_path, content in [
44
+ (HISTORY_DIR, "# Chat History\n\nThis directory contains persistent chat history files.\n"),
45
+ (LOG_DIR, "# Server Logs\n\nThis directory contains server log files.\n")
46
+ ]:
47
+ readme_path = os.path.join(dir_path, "README.md")
48
+ if not os.path.exists(readme_path):
49
+ with open(readme_path, "w") as f:
50
+ f.write(content)
51
+
52
+ # Fun usernames with emojis
53
+ FUN_USERNAMES = [
54
+ "CosmicJester 🌌", "PixelPanda 🐼", "QuantumQuack 🦆", "StellarSquirrel 🐿️",
55
+ "GizmoGuru ⚙️", "NebulaNinja 🌠", "ByteBuster 💾", "GalacticGopher 🌍",
56
+ "RocketRaccoon 🚀", "EchoElf 🧝", "PhantomFox 🦊", "WittyWizard 🧙",
57
+ "LunarLlama 🌙", "SolarSloth ☀️", "AstroAlpaca 🦙", "CyberCoyote 🐺",
58
+ "MysticMoose 🦌", "GlitchGnome 🧚", "VortexViper 🐍", "ChronoChimp 🐒"
59
+ ]
60
+
61
+ # Node name
62
  def get_node_name():
63
  parser = argparse.ArgumentParser(description='Start a chat node with a specific name')
64
  parser.add_argument('--node-name', type=str, default=None, help='Name for this chat node')
65
  parser.add_argument('--port', type=int, default=7860, help='Port to run the Gradio interface on')
 
66
  args = parser.parse_args()
67
+ node_name = args.node_name or f"node-{uuid.uuid4().hex[:8]}"
68
+ return node_name, args.port
 
 
 
 
 
69
 
70
  def get_room_history_file(room_id):
71
  timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
 
76
  return os.path.join(LOG_DIR, f"log_{timestamp}.jsonl")
77
 
78
  def get_all_room_history_files(room_id):
79
+ files = [os.path.join(HISTORY_DIR, f) for f in os.listdir(HISTORY_DIR) if f.startswith(f"{room_id}_") and f.endswith(".jsonl")]
80
+ files.sort(key=os.path.getmtime, reverse=True)
 
 
 
81
  return files
82
 
83
  def list_all_history_files():
 
84
  files = []
85
  for file in os.listdir(HISTORY_DIR):
86
  if file.endswith(".jsonl") and file != "README.md":
87
+ room_id = file.split('_', 1)[0]
88
+ file_path = os.path.join(HISTORY_DIR, file)
89
+ mod_time = os.path.getmtime(file_path)
90
+ files.append((room_id, file_path, mod_time))
91
+ files.sort(key=lambda x: x[2], reverse=True)
 
 
92
  return files
93
 
94
  def load_room_history(room_id):
95
  if room_id not in chat_history:
96
  chat_history[room_id] = []
97
  history_files = get_all_room_history_files(room_id)
 
98
  for file in history_files:
99
+ file_modification_times[file] = file_modification_times.get(file, os.path.getmtime(file))
 
 
 
 
100
  try:
101
+ with open(file, 'r') as f:
102
  for line in f:
103
+ if line.strip():
 
104
  try:
105
+ chat_history[room_id].append(json.loads(line))
 
106
  except json.JSONDecodeError:
107
+ logger.error(f"Error parsing JSON line in {file}")
108
  except Exception as e:
109
+ logger.error(f"Error loading history from {file}: {e}")
110
+ chat_history[room_id].sort(key=lambda x: x.get("timestamp", ""))
111
+ logger.info(f"Loaded {len(chat_history[room_id])} messages from {len(history_files)} files for room {room_id}")
112
+ sector_users[room_id] = sector_users.get(room_id, set())
 
 
 
 
 
113
  return chat_history[room_id]
114
 
115
  def save_message_to_history(room_id, message):
116
  history_files = get_all_room_history_files(room_id)
117
+ history_file = get_room_history_file(room_id) if not history_files or os.path.getsize(history_files[0]) > 1024 * 1024 else history_files[0]
 
 
 
 
 
 
 
 
118
  try:
119
  with open(history_file, 'a') as f:
120
  f.write(json.dumps(message) + '\n')
121
  file_modification_times[history_file] = os.path.getmtime(history_file)
 
122
  except Exception as e:
123
  logger.error(f"Error saving message to {history_file}: {e}")
124
 
125
  def save_log_entry(entry):
126
+ log_files = [os.path.join(LOG_DIR, f) for f in os.listdir(LOG_DIR) if f.startswith("log_") and f.endswith(".jsonl")]
127
+ log_files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
128
+ log_file = get_log_file() if not log_files or os.path.getsize(log_files[0]) > 1024 * 1024 else log_files[0]
 
 
 
 
 
 
 
 
 
 
129
  try:
130
  with open(log_file, 'a') as f:
131
  f.write(json.dumps(entry) + '\n')
 
133
  except Exception as e:
134
  logger.error(f"Error saving log to {log_file}: {e}")
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  def get_sector_coordinates(room_id):
137
  try:
138
  if ',' in room_id:
139
  x, y = map(int, room_id.split(','))
140
  return max(0, min(x, GRID_WIDTH-1)), max(0, min(y, GRID_HEIGHT-1))
141
  except:
142
+ hash_val = hash(room_id)
143
+ return abs(hash_val) % GRID_WIDTH, abs(hash_val >> 8) % GRID_HEIGHT
 
 
 
144
 
145
  def generate_sector_map():
146
  grid = [[' ' for _ in range(GRID_WIDTH)] for _ in range(GRID_HEIGHT)]
 
149
  x, y = get_sector_coordinates(room_id)
150
  user_count = len(users)
151
  grid[y][x] = str(min(user_count, 9)) if user_count < 10 else '+'
 
152
  header = ' ' + ''.join([str(i % 10) for i in range(GRID_WIDTH)])
153
+ map_str = header + '\n' + '\n'.join(f"{y % 10}|{''.join(grid[y])}|" for y in range(GRID_HEIGHT)) + '\n' + header
 
 
 
 
154
  return f"```\n{map_str}\n```\n\nLegend: Number indicates users in sector. '+' means 10+ users."
155
 
156
  async def clear_all_history():
 
159
  for file in os.listdir(HISTORY_DIR):
160
  if file.endswith(".jsonl"):
161
  os.remove(os.path.join(HISTORY_DIR, file))
162
+ clear_msg = {"type": "system", "content": "🧹 All chat history cleared", "timestamp": datetime.now().isoformat(), "sender": "system"}
 
 
 
 
 
 
 
163
  for room_id in list(active_connections.keys()):
164
  clear_msg["room_id"] = room_id
165
  await broadcast_message(clear_msg, room_id)
 
166
  logger.info("All chat history cleared")
167
  return "All chat history cleared"
168
 
169
  async def websocket_handler(websocket, path):
170
  try:
171
+ room_id = path.strip('/').split('/')[0] or "default"
 
 
172
  client_id = str(uuid.uuid4())
173
+ active_connections.setdefault(room_id, {})[client_id] = websocket
174
+ sector_users.setdefault(room_id, set()).add(client_id)
 
 
 
 
 
 
175
  x, y = get_sector_coordinates(room_id)
176
  room_history = load_room_history(room_id)
177
+
178
+ for msg in [
179
+ {"type": "system", "content": f"Welcome to room '{room_id}' (Sector {x},{y})! Node: '{NODE_NAME}'", "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": room_id},
180
+ {"type": "system", "content": f"Sector Map:\n{generate_sector_map()}", "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": room_id}
181
+ ] + room_history:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  await websocket.send(json.dumps(msg))
183
+
184
+ join_msg = {"type": "system", "content": f"User joined (Sector {x},{y}) - {len(sector_users[room_id])} users", "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": room_id}
 
 
 
 
 
 
185
  await broadcast_message(join_msg, room_id)
186
  save_message_to_history(room_id, join_msg)
187
+ logger.info(f"Client {client_id} connected to room {room_id} (Sector {x},{y})")
188
+
 
189
  if room_id == "logs":
190
+ for entry in log_history[-10:]:
191
+ await websocket.send(json.dumps({"type": "log", "content": entry["message"], "timestamp": entry["timestamp"], "sender": "system", "room_id": "logs"}))
192
+
 
 
 
 
 
 
 
193
  async for message in websocket:
194
  try:
195
  data = json.loads(message)
196
+ if data.get("type") == "command":
197
+ if data.get("command") == "clear_history":
198
+ await clear_all_history()
199
+ continue
200
+ if data.get("command") == "show_map":
201
+ await websocket.send(json.dumps({"type": "system", "content": f"Sector Map:\n{generate_sector_map()}", "timestamp": datetime.now().isoformat(), "sender": "system", "room_id": room_id}))
202
+ continue
203
+ data.update({"timestamp": datetime.now().isoformat(), "sender_node": NODE_NAME, "room_id": room_id})
 
 
 
 
 
 
 
 
 
 
 
 
204
  chat_history[room_id].append(data)
205
  if len(chat_history[room_id]) > 500:
206
  chat_history[room_id] = chat_history[room_id][-500:]
207
+ save_message_to_history