Update app.py
Browse files
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 |
-
#
|
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
|
38 |
GRID_WIDTH = 10
|
39 |
GRID_HEIGHT = 10
|
40 |
|
41 |
-
# Directories
|
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 |
-
|
49 |
-
|
50 |
-
|
51 |
-
|
52 |
-
|
53 |
-
|
54 |
-
|
55 |
-
|
56 |
-
|
57 |
-
|
58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
-
|
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 |
-
|
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 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
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 |
-
|
110 |
-
file_modification_times[file] = os.path.getmtime(file)
|
111 |
-
|
112 |
-
messages = []
|
113 |
-
for history_file in history_files:
|
114 |
try:
|
115 |
-
with open(
|
116 |
for line in f:
|
117 |
-
|
118 |
-
if line:
|
119 |
try:
|
120 |
-
|
121 |
-
messages.append(data)
|
122 |
except json.JSONDecodeError:
|
123 |
-
logger.error(f"Error parsing JSON line in {
|
124 |
except Exception as e:
|
125 |
-
logger.error(f"Error loading history from {
|
126 |
-
|
127 |
-
|
128 |
-
|
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 |
-
|
157 |
-
log_files
|
158 |
-
|
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 |
-
|
246 |
-
|
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 |
-
|
291 |
-
room_id = path_parts[0] if path_parts else "default"
|
292 |
-
|
293 |
client_id = str(uuid.uuid4())
|
294 |
-
|
295 |
-
|
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 |
-
|
306 |
-
"type": "system",
|
307 |
-
"content": f"
|
308 |
-
|
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 |
-
|
337 |
-
|
338 |
if room_id == "logs":
|
339 |
-
for
|
340 |
-
|
341 |
-
|
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 |
-
|
354 |
-
|
355 |
-
|
356 |
-
|
357 |
-
|
358 |
-
|
359 |
-
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|